namespace AyCode.Core.Compression;

/// <summary>
/// High-level LZ4 compression helper. Pure managed implementation that works on all platforms including WASM.
/// </summary>
public static class Lz4
{
    /// <summary>
    /// Compresses data using LZ4 Block format.
    /// </summary>
    /// <param name="data">Data to compress.</param>
    /// <returns>Compressed data with 4-byte original size header.</returns>
    public static byte[] CompressBlock(byte[] data)
    {
        ArgumentNullException.ThrowIfNull(data);
        return CompressBlock(data.AsSpan());
    }

    /// <summary>
    /// Compresses data using LZ4 Block format.
    /// </summary>
    /// <param name="data">Data to compress.</param>
    /// <returns>Compressed data with 4-byte original size header.</returns>
    public static byte[] CompressBlock(ReadOnlySpan<byte> data)
    {
        if (data.Length == 0)
            return [];

        var compressed = Lz4Compressor.Compress(data);

        // Prepend original size (4 bytes, little-endian)
        var result = new byte[4 + compressed.Length];
        WriteInt32LittleEndian(result, 0, data.Length);
        compressed.CopyTo(result.AsSpan(4));

        return result;
    }

    /// <summary>
    /// Decompresses LZ4 Block format data.
    /// </summary>
    /// <param name="compressedData">Compressed data with 4-byte original size header.</param>
    /// <returns>Decompressed data.</returns>
    public static byte[] DecompressBlock(byte[] compressedData)
    {
        ArgumentNullException.ThrowIfNull(compressedData);
        return DecompressBlock(compressedData.AsSpan());
    }

    /// <summary>
    /// Decompresses LZ4 Block format data.
    /// </summary>
    /// <param name="compressedData">Compressed data with 4-byte original size header.</param>
    /// <returns>Decompressed data.</returns>
    public static byte[] DecompressBlock(ReadOnlySpan<byte> compressedData)
    {
        if (compressedData.Length < 4)
            return [];

        var originalSize = ReadInt32LittleEndian(compressedData, 0);
        if (originalSize <= 0)
            return [];

        var compressed = compressedData.Slice(4);
        return Lz4Decompressor.Decompress(compressed, originalSize);
    }

    /// <summary>
    /// Compresses data using LZ4 BlockArray format (chunked compression).
    /// Better for large data and streaming scenarios.
    /// </summary>
    /// <param name="data">Data to compress.</param>
    /// <param name="chunkSize">Size of each chunk (default 64KB).</param>
    /// <returns>Compressed data with chunk headers.</returns>
    public static byte[] CompressBlockArray(byte[] data, int chunkSize = Lz4Compressor.DefaultChunkSize)
    {
        ArgumentNullException.ThrowIfNull(data);
        return CompressBlockArray(data.AsSpan(), chunkSize);
    }

    /// <summary>
    /// Compresses data using LZ4 BlockArray format (chunked compression).
    /// Better for large data and streaming scenarios.
    /// </summary>
    /// <param name="data">Data to compress.</param>
    /// <param name="chunkSize">Size of each chunk (default 64KB).</param>
    /// <returns>Compressed data with chunk headers.</returns>
    public static byte[] CompressBlockArray(ReadOnlySpan<byte> data, int chunkSize = Lz4Compressor.DefaultChunkSize)
    {
        if (data.Length == 0)
            return [];

        return Lz4Compressor.CompressBlockArray(data, chunkSize);
    }

    /// <summary>
    /// Decompresses LZ4 BlockArray format data.
    /// </summary>
    /// <param name="compressedData">Compressed data with chunk headers.</param>
    /// <returns>Decompressed data.</returns>
    public static byte[] DecompressBlockArray(byte[] compressedData)
    {
        ArgumentNullException.ThrowIfNull(compressedData);
        return DecompressBlockArray(compressedData.AsSpan());
    }

    /// <summary>
    /// Decompresses LZ4 BlockArray format data.
    /// </summary>
    /// <param name="compressedData">Compressed data with chunk headers.</param>
    /// <returns>Decompressed data.</returns>
    public static byte[] DecompressBlockArray(ReadOnlySpan<byte> compressedData)
    {
        if (compressedData.Length < 4)
            return [];

        return Lz4Decompressor.DecompressBlockArray(compressedData);
    }

    /// <summary>
    /// Compresses data using the specified compression mode.
    /// </summary>
    /// <param name="data">Data to compress.</param>
    /// <param name="mode">Compression mode.</param>
    /// <returns>Compressed data.</returns>
    public static byte[] Compress(byte[] data, Lz4CompressionMode mode)
    {
        return mode switch
        {
            Lz4CompressionMode.None => data,
            Lz4CompressionMode.Block => CompressBlock(data),
            Lz4CompressionMode.BlockArray => CompressBlockArray(data),
            _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Invalid compression mode.")
        };
    }

    /// <summary>
    /// Compresses data using the specified compression mode.
    /// </summary>
    /// <param name="data">Data to compress.</param>
    /// <param name="mode">Compression mode.</param>
    /// <returns>Compressed data.</returns>
    public static byte[] Compress(ReadOnlySpan<byte> data, Lz4CompressionMode mode)
    {
        return mode switch
        {
            Lz4CompressionMode.None => data.ToArray(),
            Lz4CompressionMode.Block => CompressBlock(data),
            Lz4CompressionMode.BlockArray => CompressBlockArray(data),
            _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Invalid compression mode.")
        };
    }

    /// <summary>
    /// Decompresses data using the specified compression mode.
    /// </summary>
    /// <param name="compressedData">Compressed data.</param>
    /// <param name="mode">Compression mode used during compression.</param>
    /// <returns>Decompressed data.</returns>
    public static byte[] Decompress(byte[] compressedData, Lz4CompressionMode mode)
    {
        return mode switch
        {
            Lz4CompressionMode.None => compressedData,
            Lz4CompressionMode.Block => DecompressBlock(compressedData),
            Lz4CompressionMode.BlockArray => DecompressBlockArray(compressedData),
            _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Invalid compression mode.")
        };
    }

    /// <summary>
    /// Decompresses data using the specified compression mode.
    /// </summary>
    /// <param name="compressedData">Compressed data.</param>
    /// <param name="mode">Compression mode used during compression.</param>
    /// <returns>Decompressed data.</returns>
    public static byte[] Decompress(ReadOnlySpan<byte> compressedData, Lz4CompressionMode mode)
    {
        return mode switch
        {
            Lz4CompressionMode.None => compressedData.ToArray(),
            Lz4CompressionMode.Block => DecompressBlock(compressedData),
            Lz4CompressionMode.BlockArray => DecompressBlockArray(compressedData),
            _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Invalid compression mode.")
        };
    }

    /// <summary>
    /// Gets the maximum compressed length for a given input length.
    /// </summary>
    public static int GetMaxCompressedLength(int inputLength)
        => Lz4Compressor.GetMaxCompressedLength(inputLength);

    private static void WriteInt32LittleEndian(byte[] buffer, int offset, int value)
    {
        buffer[offset] = (byte)value;
        buffer[offset + 1] = (byte)(value >> 8);
        buffer[offset + 2] = (byte)(value >> 16);
        buffer[offset + 3] = (byte)(value >> 24);
    }

    private static int ReadInt32LittleEndian(ReadOnlySpan<byte> buffer, int offset)
    {
        return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24);
    }
}
