using System.Buffers.Binary;

namespace ZB.MOM.WW.CBDD.Core.Compression;

/// <summary>
/// Fixed header prefix for compressed payload blobs.
/// </summary>
public readonly struct CompressedPayloadHeader
{
    public const int Size = 16;

    /// <summary>
    /// Compression codec used for payload bytes.
    /// </summary>
    public CompressionCodec Codec { get; }

    /// <summary>
    /// Original uncompressed payload length.
    /// </summary>
    public int OriginalLength { get; }

    /// <summary>
    /// Compressed payload length.
    /// </summary>
    public int CompressedLength { get; }

    /// <summary>
    /// CRC32 checksum of compressed payload bytes.
    /// </summary>
    public uint Checksum { get; }

    /// <summary>
    /// Initializes a new instance of the <see cref="CompressedPayloadHeader"/> class.
    /// </summary>
    /// <param name="codec">Compression codec used for payload bytes.</param>
    /// <param name="originalLength">Original uncompressed payload length.</param>
    /// <param name="compressedLength">Compressed payload length.</param>
    /// <param name="checksum">CRC32 checksum of compressed payload bytes.</param>
    public CompressedPayloadHeader(CompressionCodec codec, int originalLength, int compressedLength, uint checksum)
    {
        if (originalLength < 0)
            throw new ArgumentOutOfRangeException(nameof(originalLength));
        if (compressedLength < 0)
            throw new ArgumentOutOfRangeException(nameof(compressedLength));

        Codec = codec;
        OriginalLength = originalLength;
        CompressedLength = compressedLength;
        Checksum = checksum;
    }

    /// <summary>
    /// Create.
    /// </summary>
    /// <param name="codec">Compression codec used for payload bytes.</param>
    /// <param name="originalLength">Original uncompressed payload length.</param>
    /// <param name="compressedPayload">Compressed payload bytes.</param>
    public static CompressedPayloadHeader Create(CompressionCodec codec, int originalLength, ReadOnlySpan<byte> compressedPayload)
    {
        var checksum = ComputeChecksum(compressedPayload);
        return new CompressedPayloadHeader(codec, originalLength, compressedPayload.Length, checksum);
    }

    /// <summary>
    /// Write To.
    /// </summary>
    /// <param name="destination">Destination span that receives the serialized header.</param>
    public void WriteTo(Span<byte> destination)
    {
        if (destination.Length < Size)
            throw new ArgumentException($"Destination must be at least {Size} bytes.", nameof(destination));

        destination[0] = (byte)Codec;
        destination[1] = 0;
        destination[2] = 0;
        destination[3] = 0;
        BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(4, 4), OriginalLength);
        BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(8, 4), CompressedLength);
        BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12, 4), Checksum);
    }

    /// <summary>
    /// Read From.
    /// </summary>
    /// <param name="source">Source span containing a serialized header.</param>
    public static CompressedPayloadHeader ReadFrom(ReadOnlySpan<byte> source)
    {
        if (source.Length < Size)
            throw new ArgumentException($"Source must be at least {Size} bytes.", nameof(source));

        var codec = (CompressionCodec)source[0];
        var originalLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(4, 4));
        var compressedLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(8, 4));
        var checksum = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(12, 4));
        return new CompressedPayloadHeader(codec, originalLength, compressedLength, checksum);
    }

    /// <summary>
    /// Validate Checksum.
    /// </summary>
    /// <param name="compressedPayload">Compressed payload bytes to validate.</param>
    public bool ValidateChecksum(ReadOnlySpan<byte> compressedPayload)
    {
        return Checksum == ComputeChecksum(compressedPayload);
    }

    /// <summary>
    /// Compute Checksum.
    /// </summary>
    /// <param name="payload">Payload bytes.</param>
    public static uint ComputeChecksum(ReadOnlySpan<byte> payload) => Crc32Calculator.Compute(payload);

    private static class Crc32Calculator
    {
        private const uint Polynomial = 0xEDB88320u;
        private static readonly uint[] Table = CreateTable();

        /// <summary>
        /// Compute.
        /// </summary>
        /// <param name="payload">Payload bytes.</param>
        public static uint Compute(ReadOnlySpan<byte> payload)
        {
            uint crc = 0xFFFFFFFFu;
            for (int i = 0; i < payload.Length; i++)
            {
                var index = (crc ^ payload[i]) & 0xFF;
                crc = (crc >> 8) ^ Table[index];
            }

            return ~crc;
        }

        private static uint[] CreateTable()
        {
            var table = new uint[256];
            for (uint i = 0; i < table.Length; i++)
            {
                uint value = i;
                for (int bit = 0; bit < 8; bit++)
                {
                    value = (value & 1) != 0 ? (value >> 1) ^ Polynomial : value >> 1;
                }

                table[i] = value;
            }

            return table;
        }
    }
}
