using System.Buffers.Binary;
using System.IO.Compression;
using System.Text;

namespace Sunfish.Qr;

/// <summary>Salidas de un <see cref="QrCode"/>: PNG en blanco y negro y texto con medios bloques.</summary>
public static class QrImage
{
    /// <summary>Margen claro alrededor que exige la norma (en modulos).</summary>
    public const int QuietZone = 4;

    /// <summary>PNG de 1 bit por pixel, <paramref name="scale"/> pixeles por modulo, con margen.</summary>
    public static byte[] Png(QrCode qr, int scale = 6, int quiet = QuietZone)
    {
        int size = (qr.Size + quiet * 2) * scale;
        int rowBytes = (size + 7) / 8;
        var raw = new byte[(rowBytes + 1) * size];
        for (int y = 0; y < size; y++)
        {
            int row = y * (rowBytes + 1); // el primer byte de cada fila es el filtro (0, ninguno)
            for (int x = 0; x < size; x++)
            {
                int mx = x / scale - quiet, my = y / scale - quiet;
                bool dark = mx >= 0 && my >= 0 && mx < qr.Size && my < qr.Size && qr[mx, my];
                if (!dark) // 1 = blanco en escala de grises de 1 bit
                    raw[row + 1 + x / 8] |= (byte)(0x80 >> (x % 8));
            }
        }

        var png = new MemoryStream();
        png.Write([0x89, (byte)'P', (byte)'N', (byte)'G', 0x0D, 0x0A, 0x1A, 0x0A]);
        var header = new byte[13];
        BinaryPrimitives.WriteUInt32BigEndian(header, (uint)size);
        BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(4), (uint)size);
        header[8] = 1; // bits por pixel
        header[9] = 0; // escala de grises
        Chunk(png, "IHDR", header);
        var compressed = new MemoryStream();
        using (var z = new ZLibStream(compressed, CompressionLevel.Optimal, leaveOpen: true))
            z.Write(raw);
        Chunk(png, "IDAT", compressed.ToArray());
        Chunk(png, "IEND", []);
        return png.ToArray();
    }

    private static void Chunk(Stream output, string type, byte[] data)
    {
        Span<byte> length = stackalloc byte[4];
        BinaryPrimitives.WriteUInt32BigEndian(length, (uint)data.Length);
        output.Write(length);
        var typeBytes = Encoding.ASCII.GetBytes(type);
        output.Write(typeBytes);
        output.Write(data);
        uint crc = Web.Crc32.Update(Web.Crc32.Update(0, typeBytes), data);
        Span<byte> crcBytes = stackalloc byte[4];
        BinaryPrimitives.WriteUInt32BigEndian(crcBytes, crc);
        output.Write(crcBytes);
    }

    /// <summary>
    /// Filas de texto con medios bloques (dos modulos por caracter en vertical). true = oscuro;
    /// quien lo pinta decide colores (la TUI pone fondo claro para que lo lean los moviles).
    /// </summary>
    public static IEnumerable<string> HalfBlocks(QrCode qr, int quiet = 2)
    {
        int size = qr.Size + quiet * 2;
        bool Dark(int x, int y)
        {
            x -= quiet;
            y -= quiet;
            return x >= 0 && y >= 0 && x < qr.Size && y < qr.Size && qr[x, y];
        }
        for (int y = 0; y < size; y += 2)
        {
            var sb = new StringBuilder(size);
            for (int x = 0; x < size; x++)
            {
                bool top = Dark(x, y), bottom = y + 1 < size && Dark(x, y + 1);
                sb.Append((top, bottom) switch
                {
                    (true, true) => '█',
                    (true, false) => '▀',
                    (false, true) => '▄',
                    _ => ' ',
                });
            }
            yield return sb.ToString();
        }
    }
}
