using System.Buffers.Binary;
using System.Text;

namespace Sunfish.Mac;

/// <summary>
/// BinHex 4.0 (.hqx): texto de 7 bits con un alfabeto de 64 caracteres, compresion RLE (0x90) y
/// tres CRC (cabecera, data fork, resource fork). Pensado para ficheros moderados: se trabaja en
/// memoria (el limite lo pone quien llama).
/// </summary>
public static class BinHex
{
    public const string Banner = "(This file must be converted with BinHex 4.0)";
    private const string Alphabet = "!\"#$%&'()*+,-012345689@ABCDEFGHIJKLMNPQRSTUVXYZ[`abcdefhijklmpqr";
    private const byte RunMarker = 0x90;

    private static readonly sbyte[] Reverse = BuildReverse();

    private static sbyte[] BuildReverse()
    {
        var table = new sbyte[128];
        Array.Fill(table, (sbyte)-1);
        for (int i = 0; i < Alphabet.Length; i++)
            table[Alphabet[i]] = (sbyte)i;
        return table;
    }

    /// <summary>El texto parece BinHex 4.0 (tiene la linea de aviso).</summary>
    public static bool LooksLikeBinHex(ReadOnlySpan<byte> start) =>
        Encoding.ASCII.GetString(start).Contains(Banner, StringComparison.Ordinal);

    /// <summary>Decodifica; devuelve el fichero y su data fork, o null si no es BinHex valido (CRC incluidos).</summary>
    public static (MacFile File, byte[] DataFork)? Decode(byte[] text)
    {
        string s = Encoding.Latin1.GetString(text);
        int banner = s.IndexOf(Banner, StringComparison.Ordinal);
        if (banner < 0)
            return null;
        int start = s.IndexOf(':', banner + Banner.Length);
        if (start < 0)
            return null;
        int end = s.IndexOf(':', start + 1);
        if (end < 0)
            return null;

        // 6 bits por caracter; los saltos de linea y espacios se ignoran.
        var packed = new List<byte>(end - start);
        int acc = 0, bits = 0;
        for (int i = start + 1; i < end; i++)
        {
            char c = s[i];
            if (c is '\r' or '\n' or ' ' or '\t')
                continue;
            if (c >= 128 || Reverse[c] < 0)
                return null;
            acc = (acc << 6) | (byte)Reverse[c];
            bits += 6;
            if (bits >= 8)
            {
                bits -= 8;
                packed.Add((byte)(acc >> bits));
                acc &= (1 << bits) - 1;
            }
        }
        var data = Unrun(packed);

        int p = 0;
        if (data.Count < 1)
            return null;
        int nameLength = data[p++];
        int headerLength = 1 + nameLength + 1 + 4 + 4 + 2 + 4 + 4;
        if (data.Count < headerLength + 2)
            return null;
        var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(data);
        if (Crc(span[..headerLength]) != BinaryPrimitives.ReadUInt16BigEndian(span[headerLength..]))
            return null;
        var file = new MacFile { Name = Encodings.MacRoman.GetString(span.Slice(1, nameLength)) };
        p = 1 + nameLength + 1; // version
        span.Slice(p, 8).CopyTo(file.FinderInfo);
        p += 8;
        file.FinderFlags = BinaryPrimitives.ReadUInt16BigEndian(span[p..]);
        p += 2;
        long dataLength = BinaryPrimitives.ReadUInt32BigEndian(span[p..]);
        long resourceLength = BinaryPrimitives.ReadUInt32BigEndian(span[(p + 4)..]);
        p += 8 + 2;
        if (p + dataLength + 2 + resourceLength + 2 > data.Count)
            return null;
        var dataFork = span.Slice(p, (int)dataLength).ToArray();
        if (Crc(dataFork) != BinaryPrimitives.ReadUInt16BigEndian(span[(p + (int)dataLength)..]))
            return null;
        p += (int)dataLength + 2;
        file.ResourceFork = span.Slice(p, (int)resourceLength).ToArray();
        if (Crc(file.ResourceFork) != BinaryPrimitives.ReadUInt16BigEndian(span[(p + (int)resourceLength)..]))
            return null;
        return (file, dataFork);
    }

    public static byte[] Encode(MacFile file, ReadOnlySpan<byte> dataFork)
    {
        var name = Encodings.MacRoman.GetBytes(MacNames.Shorten(file.Name, 63));
        var raw = new List<byte>(dataFork.Length + file.ResourceFork.Length + 128) { (byte)name.Length };
        raw.AddRange(name);
        raw.Add(0);
        raw.AddRange(file.FinderInfo.AsSpan(0, 8).ToArray());
        AddU16(raw, file.FinderFlags);
        AddU32(raw, (uint)dataFork.Length);
        AddU32(raw, (uint)file.ResourceFork.Length);
        AddU16(raw, Crc(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(raw)));
        raw.AddRange(dataFork.ToArray());
        AddU16(raw, Crc(dataFork));
        raw.AddRange(file.ResourceFork);
        AddU16(raw, Crc(file.ResourceFork));

        var run = Run(raw);
        var sb = new StringBuilder(Banner).Append("\r\n\r\n:");
        int column = 1, acc = 0, bits = 0;
        void Emit(int value)
        {
            sb.Append(Alphabet[value]);
            if (++column == 64)
            {
                sb.Append("\r\n");
                column = 0;
            }
        }
        foreach (byte b in run)
        {
            acc = (acc << 8) | b;
            bits += 8;
            while (bits >= 6)
            {
                bits -= 6;
                Emit((acc >> bits) & 0x3F);
            }
            acc &= (1 << bits) - 1;
        }
        if (bits > 0)
            Emit((acc << (6 - bits)) & 0x3F);
        sb.Append(":\r\n");
        return Encoding.ASCII.GetBytes(sb.ToString());
    }

    /// <summary>CRC de BinHex: CRC-16/XMODEM del bloque seguido de dos bytes a cero.</summary>
    internal static ushort Crc(ReadOnlySpan<byte> data) => Crc16.Compute([0, 0], Crc16.Compute(data));

    /// <summary>RLE de BinHex: 0x90 n repite n-1 veces el byte anterior; 0x90 0x00 es un 0x90 literal.</summary>
    private static List<byte> Unrun(List<byte> input)
    {
        var output = new List<byte>(input.Count * 2);
        for (int i = 0; i < input.Count; i++)
        {
            byte b = input[i];
            if (b != RunMarker)
            {
                output.Add(b);
                continue;
            }
            if (++i >= input.Count)
                break;
            int count = input[i];
            if (count == 0)
                output.Add(RunMarker);
            else if (output.Count > 0)
                for (int k = 1; k < count; k++)
                    output.Add(output[^1]);
        }
        return output;
    }

    private static List<byte> Run(List<byte> input)
    {
        var output = new List<byte>(input.Count + input.Count / 10);
        int i = 0;
        while (i < input.Count)
        {
            byte b = input[i];
            int n = 1;
            while (i + n < input.Count && input[i + n] == b && n < 255)
                n++;
            output.Add(b);
            if (b == RunMarker)
                output.Add(0);
            if (n > 2)
            {
                output.Add(RunMarker);
                output.Add((byte)n);
            }
            else if (n == 2)
            {
                output.Add(b);
                if (b == RunMarker)
                    output.Add(0);
            }
            i += n;
        }
        return output;
    }

    private static void AddU16(List<byte> b, ushort v)
    {
        b.Add((byte)(v >> 8));
        b.Add((byte)v);
    }

    private static void AddU32(List<byte> b, uint v)
    {
        AddU16(b, (ushort)(v >> 16));
        AddU16(b, (ushort)v);
    }
}
