// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers.Binary;

// Implemented from the specification at
// https://github.com/Cyan4973/xxHash/blob/f9155bd4c57e2270a4ffbb176485e5d713de1c9b/doc/xxhash_spec.md

namespace System.IO.Hashing
{
    /// <summary>
    ///   Provides an implementation of the XxHash64 algorithm.
    /// </summary>
    /// <remarks>
    /// For methods that persist the computed numerical hash value as bytes,
    /// the value is written in the Big Endian byte order.
    /// </remarks>
    public sealed partial class XxHash64 : NonCryptographicHashAlgorithm
    {
        private const int HashSize = sizeof(ulong);
        private const int StripeSize = 4 * sizeof(ulong);

        private readonly ulong _seed;
        private State _state;
        private byte[]? _holdback;
        private long _length;

        /// <summary>
        ///   Initializes a new instance of the <see cref="XxHash64"/> class.
        /// </summary>
        /// <remarks>
        ///   The XxHash64 algorithm supports an optional seed value.
        ///   Instances created with this constructor use the default seed, zero.
        /// </remarks>
        public XxHash64()
            : this(0)
        {
        }

        /// <summary>
        ///   Initializes a new instance of the <see cref="XxHash64"/> class with
        ///   a specified seed.
        /// </summary>
        /// <param name="seed">
        ///   The hash seed value for computations from this instance.
        /// </param>
        public XxHash64(long seed)
            : base(HashSize)
        {
            _seed = (ulong)seed;
            Reset();
        }

        /// <summary>
        ///   Resets the hash computation to the initial state.
        /// </summary>
        public override void Reset()
        {
            _state = new State(_seed);
            _length = 0;
        }

        /// <summary>
        ///   Appends the contents of <paramref name="source"/> to the data already
        ///   processed for the current hash computation.
        /// </summary>
        /// <param name="source">The data to process.</param>
        public override void Append(ReadOnlySpan<byte> source)
        {
            // Every time we've read 32 bytes, process the stripe.
            // Data that isn't perfectly mod-32 gets stored in a holdback
            // buffer.

            int held = (int)_length & 0x1F;

            if (held != 0)
            {
                int remain = StripeSize - held;

                if (source.Length >= remain)
                {
                    source.Slice(0, remain).CopyTo(_holdback.AsSpan(held));
                    _state.ProcessStripe(_holdback);

                    source = source.Slice(remain);
                    _length += remain;
                }
                else
                {
                    source.CopyTo(_holdback.AsSpan(held));
                    _length += source.Length;
                    return;
                }
            }

            while (source.Length >= StripeSize)
            {
                _state.ProcessStripe(source);
                source = source.Slice(StripeSize);
                _length += StripeSize;
            }

            if (source.Length > 0)
            {
                _holdback ??= new byte[StripeSize];
                source.CopyTo(_holdback);
                _length += source.Length;
            }
        }

        /// <summary>
        ///   Writes the computed hash value to <paramref name="destination"/>
        ///   without modifying accumulated state.
        /// </summary>
        protected override void GetCurrentHashCore(Span<byte> destination)
        {
            ulong hash = GetCurrentHashAsUInt64();
            BinaryPrimitives.WriteUInt64BigEndian(destination, hash);
        }

        /// <summary>Gets the current computed hash value without modifying accumulated state.</summary>
        /// <returns>The hash value for the data already provided.</returns>
        [CLSCompliant(false)]
        public ulong GetCurrentHashAsUInt64()
        {
            int remainingLength = (int)_length & 0x1F;
            ReadOnlySpan<byte> remaining = ReadOnlySpan<byte>.Empty;

            if (remainingLength > 0)
            {
                remaining = new ReadOnlySpan<byte>(_holdback, 0, remainingLength);
            }

            return _state.Complete(_length, remaining);
        }

        /// <summary>
        ///   Computes the XxHash64 hash of the provided data.
        /// </summary>
        /// <param name="source">The data to hash.</param>
        /// <returns>The XxHash64 hash of the provided data.</returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="source"/> is <see langword="null"/>.
        /// </exception>
        public static byte[] Hash(byte[] source)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            return Hash(new ReadOnlySpan<byte>(source));
        }

        /// <summary>
        ///   Computes the XxHash64 hash of the provided data using the provided seed.
        /// </summary>
        /// <param name="source">The data to hash.</param>
        /// <param name="seed">The seed value for this hash computation.</param>
        /// <returns>The XxHash64 hash of the provided data.</returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="source"/> is <see langword="null"/>.
        /// </exception>
        public static byte[] Hash(byte[] source, long seed)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            return Hash(new ReadOnlySpan<byte>(source), seed);
        }

        /// <summary>
        ///   Computes the XxHash64 hash of the provided data.
        /// </summary>
        /// <param name="source">The data to hash.</param>
        /// <param name="seed">The seed value for this hash computation. The default is zero.</param>
        /// <returns>The XxHash64 hash of the provided data.</returns>
        public static byte[] Hash(ReadOnlySpan<byte> source, long seed = 0)
        {
            byte[] ret = new byte[HashSize];
            ulong hash = HashToUInt64(source, seed);
            BinaryPrimitives.WriteUInt64BigEndian(ret, hash);
            return ret;
        }

        /// <summary>
        ///   Attempts to compute the XxHash64 hash of the provided data into the provided destination.
        /// </summary>
        /// <param name="source">The data to hash.</param>
        /// <param name="destination">The buffer that receives the computed hash value.</param>
        /// <param name="bytesWritten">
        ///   On success, receives the number of bytes written to <paramref name="destination"/>.
        /// </param>
        /// <param name="seed">The seed value for this hash computation. The default is zero.</param>
        /// <returns>
        ///   <see langword="true"/> if <paramref name="destination"/> is long enough to receive
        ///   the computed hash value (4 bytes); otherwise, <see langword="false"/>.
        /// </returns>
        public static bool TryHash(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten, long seed = 0)
        {
            if (destination.Length < HashSize)
            {
                bytesWritten = 0;
                return false;
            }

            ulong hash = HashToUInt64(source, seed);
            BinaryPrimitives.WriteUInt64BigEndian(destination, hash);
            bytesWritten = HashSize;
            return true;
        }

        /// <summary>
        ///   Computes the XxHash64 hash of the provided data into the provided destination.
        /// </summary>
        /// <param name="source">The data to hash.</param>
        /// <param name="destination">The buffer that receives the computed hash value.</param>
        /// <param name="seed">The seed value for this hash computation. The default is zero.</param>
        /// <returns>
        ///   The number of bytes written to <paramref name="destination"/>.
        /// </returns>
        public static int Hash(ReadOnlySpan<byte> source, Span<byte> destination, long seed = 0)
        {
            if (destination.Length < HashSize)
            {
                ThrowDestinationTooShort();
            }

            ulong hash = HashToUInt64(source, seed);
            BinaryPrimitives.WriteUInt64BigEndian(destination, hash);
            return HashSize;
        }

        /// <summary>Computes the XxHash64 hash of the provided data.</summary>
        /// <param name="source">The data to hash.</param>
        /// <param name="seed">The seed value for this hash computation.</param>
        /// <returns>The computed XxHash64 hash.</returns>
        [CLSCompliant(false)]
        public static ulong HashToUInt64(ReadOnlySpan<byte> source, long seed = 0)
        {
            int totalLength = source.Length;
            State state = new State((ulong)seed);

            while (source.Length >= StripeSize)
            {
                state.ProcessStripe(source);
                source = source.Slice(StripeSize);
            }

            return state.Complete((uint)totalLength, source);
        }
    }
}
