/*
 * 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.db;

import java.nio.ByteBuffer;

import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;

import org.apache.cassandra.db.context.CounterContext;
import org.apache.cassandra.db.marshal.ValueAccessor;
import org.apache.cassandra.utils.FastByteOperations;

public class Digest
{
    private static final ThreadLocal<byte[]> localBuffer = ThreadLocal.withInitial(() -> new byte[4096]);

    private final Hasher hasher;
    private long inputBytes = 0;

    @SuppressWarnings("deprecation")
    private static Hasher md5()
    {
        return Hashing.md5().newHasher();
    }

    public static Digest forReadResponse()
    {
        return new Digest(md5());
    }

    public static Digest forSchema()
    {
        return new Digest(md5());
    }

    public static Digest forValidator()
    {
        // Uses a Hasher that concatenates the hash code from 2 hash functions
        // (murmur3_128) with different seeds to produce a 256 bit hashcode
        return new Digest(Hashing.concatenating(Hashing.murmur3_128(1000),
                                                Hashing.murmur3_128(2000))
                                 .newHasher());
    }

    public static Digest forRepairedDataTracking()
    {
        return new Digest(Hashing.crc32c().newHasher())
        {
            @Override
            public <V> Digest updateWithCounterContext(V context, ValueAccessor<V> accessor)
            {
                // for the purposes of repaired data tracking on the read path, exclude
                // contexts with legacy shards as these may be irrevocably different on
                // different replicas

                // see super.updateWithCounterContext + CountersTest.testEmptyContext - counter context can be empty
                if (accessor.isEmpty(context))
                    return this;

                if (CounterContext.instance().hasLegacyShards(context, accessor))
                    return this;

                return super.updateWithCounterContext(context, accessor);
            }
        };
    }

    Digest(Hasher hasher)
    {
        this.hasher = hasher;
    }

    public Digest update(byte[] input, int offset, int len)
    {
        hasher.putBytes(input, offset, len);
        inputBytes += len;
        return this;
    }

    public <V> Digest update(V input, ValueAccessor<V> accessor)
    {
        accessor.digest(input, this);
        return this;
    }

    /**
     * Update the digest with the bytes from the supplied buffer. This does
     * not modify the position of the supplied buffer, so callers are not
     * required to duplicate() the source buffer before calling
     */
    public Digest update(ByteBuffer input)
    {
        return update(input, input.position(), input.remaining());
    }

    /**
     * Update the digest with the bytes sliced from the supplied buffer. This does
     * not modify the position of the supplied buffer, so callers are not
     * required to duplicate() the source buffer before calling
     */
    public Digest update(ByteBuffer input, int pos, int len)
    {
        if (len <= 0)
            return this;

        if (input.hasArray())
        {
            byte[] b = input.array();
            int ofs = input.arrayOffset();
            hasher.putBytes(b, ofs + pos, len);
            inputBytes += len;
        }
        else
        {
            byte[] tempArray = localBuffer.get();
            while (len > 0)
            {
                int chunk = Math.min(len, tempArray.length);
                FastByteOperations.copy(input, pos, tempArray, 0, chunk);
                hasher.putBytes(tempArray, 0, chunk);
                len -= chunk;
                pos += chunk;
                inputBytes += chunk;
            }
        }
        return this;
    }

    /**
     * Update the digest with the content of a counter context.
     * Note that this skips the header entirely since the header information
     * has local meaning only, while digests are meant for comparison across
     * nodes. This means in particular that we always have:
     *  updateDigest(ctx) == updateDigest(clearAllLocal(ctx))
     */
    public <V> Digest updateWithCounterContext(V context, ValueAccessor<V> accessor)
    {
        // context can be empty due to the optimization from CASSANDRA-10657
        if (accessor.isEmpty(context))
            return this;

        int pos = CounterContext.headerLength(context, accessor);
        int len = accessor.size(context) - pos;
        accessor.digest(context, pos, len, this);
        return this;
    }

    public Digest updateWithByte(int val)
    {
        hasher.putByte((byte) (val & 0xFF));
        inputBytes++;
        return this;
    }

    public Digest updateWithInt(int val)
    {
        hasher.putByte((byte) ((val >>> 24) & 0xFF));
        hasher.putByte((byte) ((val >>> 16) & 0xFF));
        hasher.putByte((byte) ((val >>>  8) & 0xFF));
        hasher.putByte((byte) ((val >>> 0) & 0xFF));
        inputBytes += 4;
        return this;
    }

    public Digest updateWithLong(long val)
    {
        hasher.putByte((byte) ((val >>> 56) & 0xFF));
        hasher.putByte((byte) ((val >>> 48) & 0xFF));
        hasher.putByte((byte) ((val >>> 40) & 0xFF));
        hasher.putByte((byte) ((val >>> 32) & 0xFF));
        hasher.putByte((byte) ((val >>> 24) & 0xFF));
        hasher.putByte((byte) ((val >>> 16) & 0xFF));
        hasher.putByte((byte) ((val >>>  8) & 0xFF));
        hasher.putByte((byte)  ((val >>> 0) & 0xFF));
        inputBytes += 8;
        return this;
    }

    public Digest updateWithBoolean(boolean val)
    {
        updateWithByte(val ? 0 : 1);
        return this;
    }

    public byte[] digest()
    {
        return hasher.hash().asBytes();
    }

    public long inputBytes()
    {
        return inputBytes;
    }
}

