using System.Buffers.Binary;

namespace Sunfish.Mac;

/// <summary>
/// MacBinary I, II y III: cabecera de 128 bytes, data fork y resource fork, cada uno rellenado
/// hasta multiplo de 128. Se lee y se escribe en flujo: el data fork puede ser muy grande.
/// </summary>
public static class MacBinary
{
    public const int HeaderSize = 128;
    public const int MaxResourceFork = 64 * 1024 * 1024;

    public enum Kind { None, MacBinary1, MacBinary2, MacBinary3 }

    /// <summary>Cabecera ya interpretada.</summary>
    public sealed record Header(MacFile File, long DataLength, long ResourceLength, int SecondaryHeader, int CommentLength, Kind Kind);

    /// <summary>
    /// Reconoce una cabecera MacBinary. II y III llevan CRC y la deteccion es fiable; MacBinary I
    /// no tiene CRC y solo se acepta si se pide (<paramref name="acceptVersion1"/>) y todo cuadra.
    /// </summary>
    public static Header? Parse(ReadOnlySpan<byte> h, long totalLength, bool acceptVersion1)
    {
        if (h.Length < HeaderSize || h[0] != 0 || h[74] != 0)
            return null;
        int nameLength = h[1];
        if (nameLength is < 1 or > 63)
            return null;
        long dataLength = BinaryPrimitives.ReadUInt32BigEndian(h[83..]);
        long resourceLength = BinaryPrimitives.ReadUInt32BigEndian(h[87..]);
        int secondary = BinaryPrimitives.ReadUInt16BigEndian(h[120..]);
        if (resourceLength > MaxResourceFork)
            return null;

        Kind kind;
        ushort storedCrc = BinaryPrimitives.ReadUInt16BigEndian(h[124..]);
        if (h[82] == 0 && storedCrc == Crc16.Compute(h[..124]) && storedCrc != 0)
            kind = h[102] == 'm' && h[103] == 'B' && h[104] == 'I' && h[105] == 'N' ? Kind.MacBinary3 : Kind.MacBinary2;
        else if (acceptVersion1 && h[82] == 0 && h[101] == 0 && h[122] == 0 && h[123] == 0 && storedCrc == 0)
            kind = Kind.MacBinary1;
        else
            return null;

        int commentLength = kind == Kind.MacBinary1 ? 0 : BinaryPrimitives.ReadUInt16BigEndian(h[99..]);
        long expected = HeaderSize + Pad(secondary) + Pad(dataLength) + Pad(resourceLength);
        // El ultimo bloque puede venir sin relleno; el comentario de Get Info, si lo hay, va al final.
        long minimum = HeaderSize + Pad(secondary) + Pad(dataLength) + resourceLength;
        if (resourceLength == 0)
            minimum = HeaderSize + Pad(secondary) + dataLength;
        if (totalLength < minimum || totalLength > expected + Pad(commentLength) + 128)
            return null;

        var file = new MacFile { Name = Encodings.MacRoman.GetString(h.Slice(2, nameLength)) };
        h.Slice(65, 8).CopyTo(file.FinderInfo);               // tipo y creador
        file.FinderInfo[8] = h[73];                           // indicadores del Finder (byte alto)
        file.FinderInfo[9] = kind == Kind.MacBinary1 ? (byte)0 : h[101]; // (byte bajo, II+)
        h.Slice(75, 4).CopyTo(file.FinderInfo.AsSpan(10));    // posicion en la ventana
        h.Slice(79, 2).CopyTo(file.FinderInfo.AsSpan(14));    // carpeta
        if (kind == Kind.MacBinary3)
        {
            file.FinderInfo[24] = h[106];                     // fdScript
            file.FinderInfo[25] = h[107];                     // fdXFlags
        }
        file.Created = MacFile.FromMacSeconds(BinaryPrimitives.ReadUInt32BigEndian(h[91..]));
        file.Modified = MacFile.FromMacSeconds(BinaryPrimitives.ReadUInt32BigEndian(h[95..]));
        return new Header(file, dataLength, resourceLength, secondary, commentLength, kind);
    }

    /// <summary>
    /// Separa un MacBinary: el data fork va a <paramref name="dataFork"/> y lo demas se devuelve
    /// en el <see cref="MacFile"/> (resource fork incluido).
    /// </summary>
    public static async Task<MacFile> DecodeAsync(Stream input, Header header, Stream dataFork, CancellationToken ct)
    {
        await SkipAsync(input, HeaderSize + Pad(header.SecondaryHeader), ct);
        await CopyExactAsync(input, dataFork, header.DataLength, ct);
        await SkipAsync(input, Pad(header.DataLength) - header.DataLength, ct, allowShort: header.ResourceLength == 0);
        var resource = new byte[header.ResourceLength];
        await input.ReadExactlyAsync(resource, ct);
        header.File.ResourceFork = resource;
        return header.File;
    }

    /// <summary>Tamano total del MacBinary III que <see cref="EncodeAsync"/> escribiria.</summary>
    public static long EncodedLength(long dataLength, int resourceLength) =>
        HeaderSize + Pad(dataLength) + Pad(resourceLength);

    /// <summary>Escribe MacBinary III (con CRC: tambien lo leen los decodificadores de MacBinary II).</summary>
    public static async Task EncodeAsync(MacFile file, Stream dataFork, long dataLength, Stream output, CancellationToken ct)
    {
        await output.WriteAsync(BuildHeader(file, dataLength), ct);
        await CopyExactAsync(dataFork, output, dataLength, ct);
        await output.WriteAsync(new byte[Pad(dataLength) - dataLength], ct);
        await output.WriteAsync(file.ResourceFork, ct);
        await output.WriteAsync(new byte[Pad(file.ResourceFork.Length) - file.ResourceFork.Length], ct);
    }

    internal static byte[] BuildHeader(MacFile file, long dataLength)
    {
        var h = new byte[HeaderSize];
        var name = Encodings.MacRoman.GetBytes(MacNames.Shorten(file.Name, 31));
        h[1] = (byte)name.Length;
        name.CopyTo(h, 2);
        file.FinderInfo.AsSpan(0, 8).CopyTo(h.AsSpan(65));
        h[73] = (byte)(file.FinderFlags >> 8);
        file.FinderInfo.AsSpan(10, 4).CopyTo(h.AsSpan(75));
        file.FinderInfo.AsSpan(14, 2).CopyTo(h.AsSpan(79));
        BinaryPrimitives.WriteUInt32BigEndian(h.AsSpan(83), (uint)dataLength);
        BinaryPrimitives.WriteUInt32BigEndian(h.AsSpan(87), (uint)file.ResourceFork.Length);
        BinaryPrimitives.WriteUInt32BigEndian(h.AsSpan(91), MacFile.ToMacSeconds(file.Created ?? file.Modified));
        BinaryPrimitives.WriteUInt32BigEndian(h.AsSpan(95), MacFile.ToMacSeconds(file.Modified));
        h[101] = (byte)(file.FinderFlags & 0xFF);
        "mBIN"u8.CopyTo(h.AsSpan(102));
        h[106] = file.FinderInfo[24];
        h[107] = file.FinderInfo[25];
        h[122] = 130; // MacBinary III
        h[123] = 129; // basta un lector de MacBinary II
        BinaryPrimitives.WriteUInt16BigEndian(h.AsSpan(124), Crc16.Compute(h.AsSpan(0, 124)));
        return h;
    }

    internal static long Pad(long length) => (length + 127) / 128 * 128;

    private static async Task SkipAsync(Stream input, long count, CancellationToken ct, bool allowShort = false)
    {
        var buffer = new byte[Math.Min(65536, Math.Max(1, count))];
        while (count > 0)
        {
            int n = await input.ReadAsync(buffer.AsMemory(0, (int)Math.Min(buffer.Length, count)), ct);
            if (n == 0)
            {
                if (allowShort)
                    return;
                throw new EndOfStreamException();
            }
            count -= n;
        }
    }

    private static async Task CopyExactAsync(Stream input, Stream output, long count, CancellationToken ct)
    {
        var buffer = new byte[65536];
        while (count > 0)
        {
            int n = await input.ReadAsync(buffer.AsMemory(0, (int)Math.Min(buffer.Length, count)), ct);
            if (n == 0)
                throw new EndOfStreamException();
            await output.WriteAsync(buffer.AsMemory(0, n), ct);
            count -= n;
        }
    }
}
