/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.cassandra.io.sstable.metadata;

import java.io.DataInput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.commitlog.ReplayPosition;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.EstimatedHistogram;
import org.apache.cassandra.utils.StreamingHistogram;

/**
 * SSTable metadata that always stay on heap.
 */
public class StatsMetadata extends MetadataComponent
{
    public static final IMetadataComponentSerializer serializer = new StatsMetadataSerializer();

    public final EstimatedHistogram estimatedRowSize;
    public final EstimatedHistogram estimatedColumnCount;
    public final ReplayPosition replayPosition;
    public final long minTimestamp;
    public final long maxTimestamp;
    public final int maxLocalDeletionTime;
    public final double compressionRatio;
    public final StreamingHistogram estimatedTombstoneDropTime;
    public final int sstableLevel;
    public final List<ByteBuffer> maxColumnNames;
    public final List<ByteBuffer> minColumnNames;
    public final boolean hasLegacyCounterShards;
    public final long repairedAt;

    public StatsMetadata(EstimatedHistogram estimatedRowSize,
                         EstimatedHistogram estimatedColumnCount,
                         ReplayPosition replayPosition,
                         long minTimestamp,
                         long maxTimestamp,
                         int maxLocalDeletionTime,
                         double compressionRatio,
                         StreamingHistogram estimatedTombstoneDropTime,
                         int sstableLevel,
                         List<ByteBuffer> minColumnNames,
                         List<ByteBuffer> maxColumnNames,
                         boolean hasLegacyCounterShards,
                         long repairedAt)
    {
        this.estimatedRowSize = estimatedRowSize;
        this.estimatedColumnCount = estimatedColumnCount;
        this.replayPosition = replayPosition;
        this.minTimestamp = minTimestamp;
        this.maxTimestamp = maxTimestamp;
        this.maxLocalDeletionTime = maxLocalDeletionTime;
        this.compressionRatio = compressionRatio;
        this.estimatedTombstoneDropTime = estimatedTombstoneDropTime;
        this.sstableLevel = sstableLevel;
        this.minColumnNames = minColumnNames;
        this.maxColumnNames = maxColumnNames;
        this.hasLegacyCounterShards = hasLegacyCounterShards;
        this.repairedAt = repairedAt;
    }

    public MetadataType getType()
    {
        return MetadataType.STATS;
    }

    /**
     * @param gcBefore gc time in seconds
     * @return estimated droppable tombstone ratio at given gcBefore time.
     */
    public double getEstimatedDroppableTombstoneRatio(int gcBefore)
    {
        long estimatedColumnCount = this.estimatedColumnCount.mean() * this.estimatedColumnCount.count();
        if (estimatedColumnCount > 0)
        {
            double droppable = getDroppableTombstonesBefore(gcBefore);
            return droppable / estimatedColumnCount;
        }
        return 0.0f;
    }

    /**
     * @param gcBefore gc time in seconds
     * @return amount of droppable tombstones
     */
    public double getDroppableTombstonesBefore(int gcBefore)
    {
        return estimatedTombstoneDropTime.sum(gcBefore);
    }

    public StatsMetadata mutateLevel(int newLevel)
    {
        return new StatsMetadata(estimatedRowSize,
                                 estimatedColumnCount,
                                 replayPosition,
                                 minTimestamp,
                                 maxTimestamp,
                                 maxLocalDeletionTime,
                                 compressionRatio,
                                 estimatedTombstoneDropTime,
                                 newLevel,
                                 maxColumnNames,
                                 minColumnNames,
                                 hasLegacyCounterShards,
                                 repairedAt);
    }

    public StatsMetadata mutateRepairedAt(long newRepairedAt)
    {
        return new StatsMetadata(estimatedRowSize,
                                 estimatedColumnCount,
                                 replayPosition,
                                 minTimestamp,
                                 maxTimestamp,
                                 maxLocalDeletionTime,
                                 compressionRatio,
                                 estimatedTombstoneDropTime,
                                 sstableLevel,
                                 maxColumnNames,
                                 minColumnNames,
                                 hasLegacyCounterShards,
                                 newRepairedAt);
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        StatsMetadata that = (StatsMetadata) o;
        return new EqualsBuilder()
                       .append(estimatedRowSize, that.estimatedRowSize)
                       .append(estimatedColumnCount, that.estimatedColumnCount)
                       .append(replayPosition, that.replayPosition)
                       .append(minTimestamp, that.minTimestamp)
                       .append(maxTimestamp, that.maxTimestamp)
                       .append(maxLocalDeletionTime, that.maxLocalDeletionTime)
                       .append(compressionRatio, that.compressionRatio)
                       .append(estimatedTombstoneDropTime, that.estimatedTombstoneDropTime)
                       .append(sstableLevel, that.sstableLevel)
                       .append(repairedAt, that.repairedAt)
                       .append(maxColumnNames, that.maxColumnNames)
                       .append(minColumnNames, that.minColumnNames)
                       .append(hasLegacyCounterShards, that.hasLegacyCounterShards)
                       .build();
    }

    @Override
    public int hashCode()
    {
        return new HashCodeBuilder()
                       .append(estimatedRowSize)
                       .append(estimatedColumnCount)
                       .append(replayPosition)
                       .append(minTimestamp)
                       .append(maxTimestamp)
                       .append(maxLocalDeletionTime)
                       .append(compressionRatio)
                       .append(estimatedTombstoneDropTime)
                       .append(sstableLevel)
                       .append(repairedAt)
                       .append(maxColumnNames)
                       .append(minColumnNames)
                       .append(hasLegacyCounterShards)
                       .build();
    }

    public static class StatsMetadataSerializer implements IMetadataComponentSerializer<StatsMetadata>
    {
        public int serializedSize(StatsMetadata component) throws IOException
        {
            int size = 0;
            size += EstimatedHistogram.serializer.serializedSize(component.estimatedRowSize, TypeSizes.NATIVE);
            size += EstimatedHistogram.serializer.serializedSize(component.estimatedColumnCount, TypeSizes.NATIVE);
            size += ReplayPosition.serializer.serializedSize(component.replayPosition, TypeSizes.NATIVE);
            size += 8 + 8 + 4 + 8 + 8; // mix/max timestamp(long), maxLocalDeletionTime(int), compressionRatio(double), repairedAt (long)
            size += StreamingHistogram.serializer.serializedSize(component.estimatedTombstoneDropTime, TypeSizes.NATIVE);
            size += TypeSizes.NATIVE.sizeof(component.sstableLevel);
            // min column names
            size += 4;
            for (ByteBuffer columnName : component.minColumnNames)
                size += 2 + columnName.remaining(); // with short length
            // max column names
            size += 4;
            for (ByteBuffer columnName : component.maxColumnNames)
                size += 2 + columnName.remaining(); // with short length
            size += TypeSizes.NATIVE.sizeof(component.hasLegacyCounterShards);
            return size;
        }

        public void serialize(StatsMetadata component, DataOutputPlus out) throws IOException
        {
            EstimatedHistogram.serializer.serialize(component.estimatedRowSize, out);
            EstimatedHistogram.serializer.serialize(component.estimatedColumnCount, out);
            ReplayPosition.serializer.serialize(component.replayPosition, out);
            out.writeLong(component.minTimestamp);
            out.writeLong(component.maxTimestamp);
            out.writeInt(component.maxLocalDeletionTime);
            out.writeDouble(component.compressionRatio);
            StreamingHistogram.serializer.serialize(component.estimatedTombstoneDropTime, out);
            out.writeInt(component.sstableLevel);
            out.writeLong(component.repairedAt);
            out.writeInt(component.minColumnNames.size());
            for (ByteBuffer columnName : component.minColumnNames)
                ByteBufferUtil.writeWithShortLength(columnName, out);
            out.writeInt(component.maxColumnNames.size());
            for (ByteBuffer columnName : component.maxColumnNames)
                ByteBufferUtil.writeWithShortLength(columnName, out);
            out.writeBoolean(component.hasLegacyCounterShards);
        }

        public StatsMetadata deserialize(Descriptor.Version version, DataInput in) throws IOException
        {
            EstimatedHistogram rowSizes = EstimatedHistogram.serializer.deserialize(in);
            EstimatedHistogram columnCounts = EstimatedHistogram.serializer.deserialize(in);
            ReplayPosition replayPosition = ReplayPosition.serializer.deserialize(in);
            long minTimestamp = in.readLong();
            long maxTimestamp = in.readLong();
            int maxLocalDeletionTime = in.readInt();
            double compressionRatio = in.readDouble();
            StreamingHistogram tombstoneHistogram = StreamingHistogram.serializer.deserialize(in);
            int sstableLevel = in.readInt();
            long repairedAt = 0;
            if (version.hasRepairedAt)
                repairedAt = in.readLong();

            int colCount = in.readInt();
            List<ByteBuffer> minColumnNames = new ArrayList<>(colCount);
            for (int i = 0; i < colCount; i++)
                minColumnNames.add(ByteBufferUtil.readWithShortLength(in));

            colCount = in.readInt();
            List<ByteBuffer> maxColumnNames = new ArrayList<>(colCount);
            for (int i = 0; i < colCount; i++)
                maxColumnNames.add(ByteBufferUtil.readWithShortLength(in));

            boolean hasLegacyCounterShards = true;
            if (version.tracksLegacyCounterShards)
                hasLegacyCounterShards = in.readBoolean();

            return new StatsMetadata(rowSizes,
                                     columnCounts,
                                     replayPosition,
                                     minTimestamp,
                                     maxTimestamp,
                                     maxLocalDeletionTime,
                                     compressionRatio,
                                     tombstoneHistogram,
                                     sstableLevel,
                                     minColumnNames,
                                     maxColumnNames,
                                     hasLegacyCounterShards,
                                     repairedAt);
        }
    }
}
