using System.Text;
using Minint.Core.Models;
using Minint.Core.Services;

namespace Minint.Infrastructure.Serialization;

/// <summary>
/// Self-implemented binary reader/writer for the .minint container format.
/// All multi-byte integers are little-endian. Strings are UTF-8 with a 1-byte length prefix.
/// </summary>
public sealed class MinintSerializer : IMinintSerializer
{
    private static readonly byte[] Signature = "MININT"u8.ToArray();
    private const ushort CurrentVersion = 1;
    private const int ReservedBytes = 8;
    private const int MaxNameLength = 255;

    #region Write

    public void Write(Stream stream, MinintContainer container)
    {
        ArgumentNullException.ThrowIfNull(stream);
        ArgumentNullException.ThrowIfNull(container);

        using var w = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);

        WriteHeader(w, container);

        foreach (var doc in container.Documents)
            WriteDocument(w, doc, container.Width, container.Height);
    }

    private static void WriteHeader(BinaryWriter w, MinintContainer c)
    {
        w.Write(Signature);
        w.Write(CurrentVersion);
        w.Write((uint)c.Width);
        w.Write((uint)c.Height);
        w.Write((uint)c.Documents.Count);
        w.Write(new byte[ReservedBytes]);
    }

    private static void WriteDocument(BinaryWriter w, MinintDocument doc, int width, int height)
    {
        WritePrefixedString(w, doc.Name);
        w.Write(doc.FrameDelayMs);
        w.Write((uint)doc.Palette.Count);

        foreach (var color in doc.Palette)
        {
            w.Write(color.R);
            w.Write(color.G);
            w.Write(color.B);
            w.Write(color.A);
        }

        w.Write((uint)doc.Layers.Count);

        int byteWidth = doc.IndexByteWidth;
        foreach (var layer in doc.Layers)
            WriteLayer(w, layer, byteWidth, width * height);
    }

    private static void WriteLayer(BinaryWriter w, MinintLayer layer, int byteWidth, int pixelCount)
    {
        WritePrefixedString(w, layer.Name);
        w.Write(layer.IsVisible ? (byte)1 : (byte)0);
        w.Write(layer.Opacity);

        if (layer.Pixels.Length != pixelCount)
            throw new InvalidOperationException(
                $"Layer '{layer.Name}' has {layer.Pixels.Length} pixels, expected {pixelCount}.");

        for (int i = 0; i < pixelCount; i++)
            WriteIndex(w, layer.Pixels[i], byteWidth);
    }

    private static void WriteIndex(BinaryWriter w, int index, int byteWidth)
    {
        switch (byteWidth)
        {
            case 1:
                w.Write((byte)index);
                break;
            case 2:
                w.Write((ushort)index);
                break;
            case 3:
                w.Write((byte)(index & 0xFF));
                w.Write((byte)((index >> 8) & 0xFF));
                w.Write((byte)((index >> 16) & 0xFF));
                break;
            case 4:
                w.Write(index);
                break;
        }
    }

    private static void WritePrefixedString(BinaryWriter w, string value)
    {
        var bytes = Encoding.UTF8.GetBytes(value);
        if (bytes.Length > MaxNameLength)
            throw new InvalidOperationException(
                $"String '{value}' exceeds max length of {MaxNameLength} UTF-8 bytes.");
        w.Write((byte)bytes.Length);
        w.Write(bytes);
    }

    #endregion

    #region Read

    public MinintContainer Read(Stream stream)
    {
        ArgumentNullException.ThrowIfNull(stream);

        using var r = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);

        var (width, height, docCount) = ReadHeader(r);
        var container = new MinintContainer(width, height);

        for (int i = 0; i < docCount; i++)
            container.Documents.Add(ReadDocument(r, width, height));

        return container;
    }

    private static (int Width, int Height, int DocCount) ReadHeader(BinaryReader r)
    {
        byte[] sig = ReadExact(r, Signature.Length, "file signature");
        if (!sig.AsSpan().SequenceEqual(Signature))
            throw new InvalidDataException(
                "Invalid file signature. Expected 'MININT'.");

        ushort version = r.ReadUInt16();
        if (version != CurrentVersion)
            throw new InvalidDataException(
                $"Unsupported format version {version}. Only version {CurrentVersion} is supported.");

        uint width = r.ReadUInt32();
        uint height = r.ReadUInt32();
        uint docCount = r.ReadUInt32();

        if (width == 0 || height == 0)
            throw new InvalidDataException("Width and height must be at least 1.");
        if (width > 65_536 || height > 65_536)
            throw new InvalidDataException(
                $"Dimensions {width}x{height} exceed maximum supported size (65536).");
        if (docCount == 0)
            throw new InvalidDataException("Container must have at least 1 document.");

        byte[] reserved = ReadExact(r, ReservedBytes, "reserved bytes");
        for (int i = 0; i < reserved.Length; i++)
        {
            if (reserved[i] != 0)
                break; // non-zero reserved bytes: tolerated for forward compat
        }

        return ((int)width, (int)height, (int)docCount);
    }

    private static MinintDocument ReadDocument(BinaryReader r, int width, int height)
    {
        string name = ReadPrefixedString(r);
        uint frameDelay = r.ReadUInt32();
        uint paletteCount = r.ReadUInt32();

        if (paletteCount == 0)
            throw new InvalidDataException("Palette must have at least 1 color.");

        var palette = new List<RgbaColor>((int)paletteCount);
        for (uint i = 0; i < paletteCount; i++)
        {
            byte cr = r.ReadByte();
            byte cg = r.ReadByte();
            byte cb = r.ReadByte();
            byte ca = r.ReadByte();
            palette.Add(new RgbaColor(cr, cg, cb, ca));
        }

        int byteWidth = GetIndexByteWidth((int)paletteCount);

        uint layerCount = r.ReadUInt32();
        if (layerCount == 0)
            throw new InvalidDataException($"Document '{name}' must have at least 1 layer.");

        var layers = new List<MinintLayer>((int)layerCount);
        int pixelCount = width * height;
        for (uint i = 0; i < layerCount; i++)
            layers.Add(ReadLayer(r, byteWidth, pixelCount, (int)paletteCount));

        return new MinintDocument(name, frameDelay, palette, layers);
    }

    private static MinintLayer ReadLayer(BinaryReader r, int byteWidth, int pixelCount, int paletteCount)
    {
        string name = ReadPrefixedString(r);
        byte visByte = r.ReadByte();
        if (visByte > 1)
            throw new InvalidDataException(
                $"Layer '{name}': invalid visibility flag {visByte} (expected 0 or 1).");
        bool isVisible = visByte == 1;
        byte opacity = r.ReadByte();

        var pixels = new int[pixelCount];
        for (int i = 0; i < pixelCount; i++)
        {
            int idx = ReadIndex(r, byteWidth);
            if (idx < 0 || idx >= paletteCount)
                throw new InvalidDataException(
                    $"Layer '{name}': pixel index {idx} at position {i} is out of palette range [0, {paletteCount}).");
            pixels[i] = idx;
        }

        return new MinintLayer(name, isVisible, opacity, pixels);
    }

    private static int ReadIndex(BinaryReader r, int byteWidth)
    {
        return byteWidth switch
        {
            1 => r.ReadByte(),
            2 => r.ReadUInt16(),
            3 => r.ReadByte() | (r.ReadByte() << 8) | (r.ReadByte() << 16),
            4 => r.ReadInt32(),
            _ => throw new InvalidDataException($"Invalid index byte width: {byteWidth}")
        };
    }

    private static string ReadPrefixedString(BinaryReader r)
    {
        byte len = r.ReadByte();
        if (len == 0) return string.Empty;
        byte[] bytes = ReadExact(r, len, "string data");
        return Encoding.UTF8.GetString(bytes);
    }

    /// <summary>
    /// Reads exactly <paramref name="count"/> bytes or throws on premature EOF.
    /// </summary>
    private static byte[] ReadExact(BinaryReader r, int count, string context)
    {
        byte[] buf = r.ReadBytes(count);
        if (buf.Length < count)
            throw new InvalidDataException(
                $"Unexpected end of stream while reading {context} (expected {count} bytes, got {buf.Length}).");
        return buf;
    }

    #endregion

    #region Helpers

    /// <summary>
    /// Same logic as <see cref="MinintDocument.IndexByteWidth"/>,
    /// usable when only the palette count is known (during deserialization).
    /// </summary>
    public static int GetIndexByteWidth(int paletteCount) => paletteCount switch
    {
        <= 255 => 1,
        <= 65_535 => 2,
        <= 16_777_215 => 3,
        _ => 4
    };

    #endregion
}
