using VPlasma.Core.Protocol;

namespace VPlasma.Core.Device;

/// <summary>How the text cursor is shown (set with <c>ESC G</c>).</summary>
public enum PlasmaCursorMode
{
    Hidden,
    Steady,
    Flashing,
}

/// <summary>Text rendering attributes (set with <c>ESC H</c>).</summary>
[Flags]
public enum PlasmaAttributes : byte
{
    None = 0,
    HalfIntensity = 1,
    Underline = 2,
    Reverse = 4,
    Flash = 8,
}

/// <summary>
/// The plasma display proper: a 128×32 1bpp frame plus the text-mode state
/// (cursor, font, attributes), driven by the byte stream a host writes to
/// COM2. Feed raw wire bytes to <see cref="OnReceived"/>; the parser is a
/// state machine, so commands may arrive split across any chunk boundaries.
///
/// <para>Pixels carry flags rather than a plain bit: graphics writes set
/// full-intensity dots, while text can stamp half-intensity or flashing dots
/// (<c>ESC H</c>); the UI renders <see cref="PixelHalf"/> dimmer and blinks
/// <see cref="PixelFlash"/>. The command set itself is documented on
/// <see cref="PlasmaProtocol"/>.</para>
///
/// <para>Grid geometry: fonts 0–3 are the 5×7 set in a 6×8 cell (21 columns
/// × 4 rows), fonts 4–7 the same glyphs doubled into a 12×16 cell
/// (10 × 2). Which of the eight slots the real panel mapped to which face is
/// lost with the hardware; two sizes cover what the surviving software
/// exercises.</para>
///
/// <para>Thread-safe: the serial reader feeds bytes while the UI snapshots
/// frames. Events are raised outside the lock, on the caller's thread.</para>
/// </summary>
public sealed class VPlasmaDevice
{
    public const int Width = 128;
    public const int Height = 32;
    public const int WidthBytes = Width / 8;

    // Per-pixel flag bits in the frame buffer.
    public const byte PixelLit = 0x01;
    public const byte PixelHalf = 0x02;
    public const byte PixelFlash = 0x04;

    private readonly object _sync = new();
    private readonly byte[] _pixels = new byte[Width * Height];

    // ---- text-mode state -------------------------------------------------
    private int _font;                    // 0..7
    private PlasmaAttributes _attributes;
    private int _col, _row;               // cursor, in cells of the current grid
    private PlasmaCursorMode _cursorMode = PlasmaCursorMode.Steady; // power-on default; the game hides it

    // ---- parser state ----------------------------------------------------
    private enum State
    {
        Text,           // printable chars + control bytes
        Escape,         // got ESC, awaiting the command letter
        Operand,        // awaiting the 1-byte operand of _pendingCommand
        GraphicsHeader, // collecting ESC P's 5 header bytes
        GraphicsData,   // consuming ESC P's w*h data bytes
    }

    private State _state;
    private byte _pendingCommand;
    private readonly byte[] _header = new byte[5]; // screen, y, x, w, h
    private int _headerFill;
    private int _dataIndex, _dataLength;

    private bool _dirty;                       // frame/cursor changed during this chunk
    private List<string>? _pendingLog;         // lines queued under the lock
    private bool _graphicsLogArmed = true;     // log the first ESC P of a stream, then go quiet
    private readonly HashSet<byte> _loggedUnknown = new();

    private long _bytesReceived, _graphicsRows, _textCharsDrawn;

    /// <summary>Frame or cursor changed. Raised on the feeding thread.</summary>
    public event Action? Updated;

    /// <summary>Decoded-command log lines. Raised on the feeding thread.</summary>
    public event Action<string>? Logged;

    public long BytesReceived { get { lock (_sync) return _bytesReceived; } }
    public long GraphicsRows { get { lock (_sync) return _graphicsRows; } }
    public long TextCharsDrawn { get { lock (_sync) return _textCharsDrawn; } }

    public PlasmaCursorMode CursorMode { get { lock (_sync) return _cursorMode; } }
    public int Font { get { lock (_sync) return _font; } }
    public PlasmaAttributes Attributes { get { lock (_sync) return _attributes; } }

    // Current font grid, for the UI's cursor overlay and status line.
    private int FontScale => _font >= 4 ? 2 : 1;
    public int CellWidth { get { lock (_sync) return 6 * FontScale; } }
    public int CellHeight { get { lock (_sync) return 8 * FontScale; } }
    private int Columns => Width / (6 * FontScale);
    private int Rows => Height / (8 * FontScale);
    public Point CursorCell { get { lock (_sync) return new Point(_col, _row); } }

    /// <summary>A cursor cell position (avoids dragging in System.Drawing).</summary>
    public readonly record struct Point(int Col, int Row);

    /// <summary>Copy the frame into <paramref name="destination"/> (Width*Height flag bytes).</summary>
    public void CopyFrame(byte[] destination)
    {
        if (destination.Length < _pixels.Length)
            throw new ArgumentException("Buffer too small.", nameof(destination));
        lock (_sync)
            Buffer.BlockCopy(_pixels, 0, destination, 0, _pixels.Length);
    }

    /// <summary>Power-on state: dark glass, home cursor, defaults.</summary>
    public void Reset()
    {
        lock (_sync)
        {
            Array.Clear(_pixels, 0, _pixels.Length);
            _col = _row = 0;
            _font = 0;
            _attributes = PlasmaAttributes.None;
            _cursorMode = PlasmaCursorMode.Steady;
            _state = State.Text;
            _dirty = true;
        }
        FlushEvents();
    }

    /// <summary>Feed <paramref name="count"/> received wire bytes.</summary>
    public void OnReceived(byte[] buffer, int count)
    {
        lock (_sync)
        {
            _bytesReceived += count;
            for (int i = 0; i < count; ++i)
                Step(buffer[i]);
        }
        FlushEvents();
    }

    // ---- parser ------------------------------------------------------------

    private void Step(byte b)
    {
        switch (_state)
        {
            case State.Text:
                StepText(b);
                break;

            case State.Escape:
                StepEscape(b);
                break;

            case State.Operand:
                _state = State.Text;
                ApplyOperand(_pendingCommand, b);
                break;

            case State.GraphicsHeader:
                _header[_headerFill++] = b;
                if (_headerFill == _header.Length)
                    BeginGraphicsData();
                break;

            case State.GraphicsData:
                StepGraphicsData(b);
                break;
        }
    }

    private void StepText(byte b)
    {
        switch (b)
        {
            case PlasmaProtocol.Esc:
                _state = State.Escape;
                return;

            case PlasmaProtocol.BackSpace:
                if (_col > 0) _col--;
                _dirty = true;
                return;

            case PlasmaProtocol.HorizontalTab:
                AdvanceCursor();
                _dirty = true;
                return;

            case PlasmaProtocol.LineFeed:
                _row = (_row + 1) % Rows;
                _dirty = true;
                return;

            case PlasmaProtocol.VerticalTab:
                _row = (_row + Rows - 1) % Rows;
                _dirty = true;
                return;

            case PlasmaProtocol.CarriageReturn:
                _col = 0;
                _dirty = true;
                return;
        }

        if (b < 0x20)
        {
            // A control byte the surviving software never sends: swallow it,
            // but say so once per value — it's the tell of a desynced stream.
            if (_loggedUnknown.Add(b))
                Log($"Unhandled control byte 0x{b:X2} ignored");
            return;
        }

        DrawChar(b);
        _graphicsLogArmed = true;
    }

    private void StepEscape(byte b)
    {
        _state = State.Text;
        switch (b)
        {
            case PlasmaProtocol.CmdClearScreen:
                Array.Clear(_pixels, 0, _pixels.Length);
                _col = _row = 0;
                _font = 0;
                _attributes = PlasmaAttributes.None;
                _dirty = true;
                Log("Clear screen (ESC @)");
                _graphicsLogArmed = true;
                break;

            case PlasmaProtocol.CmdHomeCursor:
                _col = _row = 0;
                _dirty = true;
                Log("Home cursor (ESC L)");
                _graphicsLogArmed = true;
                break;

            case PlasmaProtocol.CmdCursorMode:
            case PlasmaProtocol.CmdFontSelect:
            case PlasmaProtocol.CmdAttributes:
                _pendingCommand = b;
                _state = State.Operand;
                break;

            case PlasmaProtocol.CmdGraphicsWrite:
                _headerFill = 0;
                _state = State.GraphicsHeader;
                break;

            default:
                if (_loggedUnknown.Add(b))
                    Log($"Unknown command ESC 0x{b:X2} ('{(char)b}') ignored");
                break;
        }
    }

    private void ApplyOperand(byte command, byte operand)
    {
        switch (command)
        {
            case PlasmaProtocol.CmdCursorMode:
                // The game hides the cursor with 00, the test tool with FF;
                // 01 shows it steady, 03 flashing (bit 1 = blink).
                _cursorMode =
                    operand is 0x00 or 0xFF ? PlasmaCursorMode.Hidden :
                    (operand & 0x02) != 0 ? PlasmaCursorMode.Flashing :
                    PlasmaCursorMode.Steady;
                _dirty = true;
                Log($"Cursor {_cursorMode} (ESC G {operand:X2})");
                break;

            case PlasmaProtocol.CmdFontSelect:
                _font = operand == PlasmaProtocol.OperandDefault ? 0 : operand & 0x07;
                // The cursor keeps its cell coordinates but the grid changed size.
                _col = Math.Min(_col, Columns - 1);
                _row = Math.Min(_row, Rows - 1);
                _dirty = true;
                Log($"Font {_font}: {Columns}×{Rows} cells (ESC K {operand:X2})");
                break;

            case PlasmaProtocol.CmdAttributes:
                _attributes = DecodeAttributes(operand);
                Log($"Attributes {(_attributes == PlasmaAttributes.None ? "default" : _attributes.ToString())} (ESC H {operand:X2})");
                break;
        }
        _graphicsLogArmed = true;
    }

    /// <summary>
    /// <c>ESC H</c>'s operand indexes the style list PLASMA.EXE enumerates
    /// (its <c>/s</c> option): the 17 intensity/underline/reverse/flash
    /// combos below, in the tool's own order. FF (and anything out of range)
    /// restores the defaults.
    /// </summary>
    private static PlasmaAttributes DecodeAttributes(byte operand) => operand switch
    {
        0 => PlasmaAttributes.None,
        1 => PlasmaAttributes.HalfIntensity,
        2 => PlasmaAttributes.Underline,
        3 => PlasmaAttributes.Reverse,
        4 => PlasmaAttributes.Flash,
        5 => PlasmaAttributes.Underline,
        6 => PlasmaAttributes.Reverse,
        7 => PlasmaAttributes.Flash,
        8 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Underline,
        9 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Reverse,
        10 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Flash,
        11 => PlasmaAttributes.Underline | PlasmaAttributes.Reverse,
        12 => PlasmaAttributes.Underline | PlasmaAttributes.Flash,
        13 => PlasmaAttributes.Underline | PlasmaAttributes.Reverse | PlasmaAttributes.Flash,
        14 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Underline | PlasmaAttributes.Reverse,
        15 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Underline | PlasmaAttributes.Flash,
        16 => PlasmaAttributes.HalfIntensity | PlasmaAttributes.Underline | PlasmaAttributes.Reverse | PlasmaAttributes.Flash,
        _ => PlasmaAttributes.None,
    };

    // ---- graphics writes (ESC P) -------------------------------------------

    private void BeginGraphicsData()
    {
        int w = _header[3], h = _header[4];
        _dataLength = w * h;
        _dataIndex = 0;

        if (_graphicsLogArmed)
        {
            // The game streams row upon row; log the first of a run only.
            _graphicsLogArmed = false;
            Log($"Graphics stream: screen={_header[0]} y={_header[1]} xbyte={_header[2]} " +
                $"{w} byte(s)/row × {h} row(s) (further rows counted silently)");
        }

        _state = _dataLength > 0 ? State.GraphicsData : State.Text;
    }

    private void StepGraphicsData(byte b)
    {
        int w = _header[3];
        int rowOfBlock = _dataIndex / w;
        int byteOfRow = _dataIndex % w;

        int y = _header[1] + rowOfBlock;
        int xByte = _header[2] + byteOfRow;
        if (y < Height && xByte < WidthBytes)
        {
            // MSB is the leftmost pixel (L4PLASMA.CPP packs 0x80 first).
            // Graphics dots are plain full intensity: overwriting text clears
            // its half/flash flags, like repainting the glass.
            int offset = y * Width + xByte * 8;
            for (int bit = 0; bit < 8; ++bit)
                _pixels[offset + bit] = (b & (0x80 >> bit)) != 0 ? PixelLit : (byte)0;
            _dirty = true;
            if (byteOfRow == w - 1)
                _graphicsRows++;
        }

        if (++_dataIndex >= _dataLength)
            _state = State.Text;
    }

    // ---- text rendering ------------------------------------------------------

    private void DrawChar(byte code)
    {
        int scale = FontScale;
        int cellW = 6 * scale, cellH = 8 * scale;
        int ox = _col * cellW, oy = _row * cellH;

        Span<byte> columns = stackalloc byte[PlasmaFont.GlyphWidth];
        PlasmaFont.GetColumns(code, columns);

        bool reverse = (_attributes & PlasmaAttributes.Reverse) != 0;
        bool underline = (_attributes & PlasmaAttributes.Underline) != 0;
        byte litFlags = PixelLit;
        if ((_attributes & PlasmaAttributes.HalfIntensity) != 0) litFlags |= PixelHalf;
        if ((_attributes & PlasmaAttributes.Flash) != 0) litFlags |= PixelFlash;

        for (int cy = 0; cy < cellH; ++cy)
        {
            int glyphRow = cy / scale;              // 0..7; row 7 is the gap/underline row
            int rowOffset = (oy + cy) * Width + ox;
            for (int cx = 0; cx < cellW; ++cx)
            {
                int glyphCol = cx / scale;          // 0..5; column 5 is the gap column
                bool on = glyphCol < PlasmaFont.GlyphWidth
                    && glyphRow < PlasmaFont.GlyphHeight
                    && (columns[glyphCol] >> glyphRow & 1) != 0;
                if (underline && glyphRow == 7)
                    on = true;
                if (reverse)
                    on = !on;
                _pixels[rowOffset + cx] = on ? litFlags : (byte)0;
            }
        }

        _textCharsDrawn++;
        _dirty = true;
        AdvanceCursor();
    }

    private void AdvanceCursor()
    {
        if (++_col >= Columns)
        {
            _col = 0;
            // No scroll on these panels: writing past the last row wraps to the top.
            if (++_row >= Rows)
                _row = 0;
        }
    }

    // ---- event plumbing --------------------------------------------------------

    private void Log(string line) => (_pendingLog ??= new List<string>()).Add(line);

    /// <summary>Raise queued events outside the lock, on the caller's thread.</summary>
    private void FlushEvents()
    {
        List<string>? log;
        bool dirty;
        lock (_sync)
        {
            log = _pendingLog;
            _pendingLog = null;
            dirty = _dirty;
            _dirty = false;
        }

        if (log is not null && Logged is { } logged)
            foreach (string line in log)
                logged(line);
        if (dirty)
            Updated?.Invoke();
    }
}
