using System;

namespace System.IO.Hashing
{
    public static class BinaryPrimitives
    {
        public static void WriteUInt32LittleEndian(Span<byte> destination, uint value)
        {
            if (destination.Length < 4)
            {
                throw new ArgumentOutOfRangeException();
            }

            destination[3] = (byte)(value >> 24);
            destination[2] = (byte)(value >> 16);
            destination[1] = (byte)(value >> 8);
            destination[0] = (byte)value;
        }
    }

    public sealed partial class Crc32
    {
        private const uint InitialState = 0xFFFF_FFFFu;
        private const int Size = sizeof(uint);
        private static readonly uint[] s_crcLookup = GenerateReflectedTable(0xEDB88320u);

        private static uint[] GenerateReflectedTable(uint reflectedPolynomial)
        {
            uint[] table = new uint[256];

            for (int i = 0; i < 256; i++)
            {
                uint val = (uint)i;

                for (int j = 0; j < 8; j++)
                {
                    if ((val & 0b0000_0001) == 0)
                    {
                        val >>= 1;
                    }
                    else
                    {
                        val = (val >> 1) ^ reflectedPolynomial;
                    }
                }

                table[i] = val;
            }

            return table;
        }

        public static bool TryHash(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten)
        {
            if (destination.Length < Size)
            {
                bytesWritten = 0;
                return false;
            }

            bytesWritten = StaticHash(source, destination);
            return true;
        }

        private static int StaticHash(ReadOnlySpan<byte> source, Span<byte> destination)
        {
            uint crc = InitialState;
            crc = Update(crc, source);
            BinaryPrimitives.WriteUInt32LittleEndian(destination, ~crc);
            return Size;
        }

        private static uint Update(uint crc, ReadOnlySpan<byte> source)
        {
            for (int i = 0; i < source.Length; i++)
            {
                byte idx = (byte)crc;
                idx ^= source[i];
                crc = s_crcLookup[idx] ^ (crc >> 8);
            }

            return crc;
        }
    }
}
