using System;
using System.Diagnostics;
using System.IO;

namespace Core.Video
{
    public class SmsVdp
    {
        // The VDP's private memory! The CPU cannot touch these arrays directly.
        public byte[] VRAM { get; private set; } = new byte[0x4000]; // 16KB Video RAM
        public byte[] smsCRAM { get; private set; } = new byte[0x20];   // Master System - 32 Bytes colour Palette
        public byte[] ggCRAM { get; private set; } = new byte[0x40]; // GameGear - 64 Bytes colour palette
        public byte[] Registers { get; private set; } = new byte[16]; // 11 Hardware Control Registers
        public int[] FrameBuffer { get; private set; } = new int[256 * 192];
        private bool[] _priorityBuffer = new bool[256 * 192]; // Tracks priority pixels!

        // Hardware Latches
        private int _latchedHScroll = 0;
        private int _latchedVScroll = 0;

        // The Control Port State Machine (Port 0xBF)
        private bool _isSecondControlByte = false;
        private ushort _controlWord = 0;
        private byte _readBuffer = 0;

        private int _tStateCounter = 0;
        private int _currentScanline = 0;
        private int _lineCounter = 0;
        private byte _statusRegister = 0x00;
        public bool IsGameGear { get; set; } = false;

        public bool InterruptPending =>
            ((_statusRegister & 0x80) != 0 && (Registers[1] & 0x20) != 0) || // VBlank
            ((_statusRegister & 0x40) != 0 && (Registers[0] & 0x10) != 0);   // Line Interrupt

        

        public byte ReadDataPort() // Port 0xBE
        {
            _isSecondControlByte = false; // Reading data resets the control latch
            byte value = _readBuffer;
            _readBuffer = VRAM[_controlWord & 0x3FFF];
            IncrementVdpAddress();
            return value;
        }

        public byte ReadControlPort() // Port 0xBF
        {
            _isSecondControlByte = false;
            byte currentStatus = _statusRegister;

            // Reading the status port physically 
            // clears the flags inside the chip If we don't clear this, 
            // the interrupt line gets stuck on forever.
            _statusRegister = 0x00;

            return currentStatus;
        }

        public void WriteDataPort(byte value) // Port 0xBE
        {
            _isSecondControlByte = false;
            _readBuffer = value;

            int address = _controlWord & 0x3FFF;
            int command = (_controlWord >> 14) & 0x03;

            if (command == 3) // Code 3: Write to Colour Palette (CRAM)
            {
                if (IsGameGear)
                {
                    ggCRAM[address & 0x3F] = value; // GG has 64 bytes of CRAM
                }
                else
                {
                    smsCRAM[address & 0x1F] = value; // SMS has 32 bytes of CRAM
                }
            }
            else // THE FIX: Code 0, 1, 2: Write graphics to VRAM!
            {
                VRAM[address] = value;
            }

            // THE FIX: The pointer MUST auto-increment so the CPU can blast data fast!
            IncrementVdpAddress();
        }

        public void WriteControlPort(byte value) // Port 0xBF
        {
            if (!_isSecondControlByte)
            {
                // First byte arrives: Store it in the lower 8 bits
                _controlWord = (ushort)((_controlWord & 0xFF00) | value);
                _isSecondControlByte = true;
            }
            else
            {
                // Second byte arrives: Store it in the upper 8 bits and execute!
                _controlWord = (ushort)((_controlWord & 0x00FF) | (value << 8));
                _isSecondControlByte = false;

                int command = (_controlWord >> 14) & 0x03;
                
                if (command == 0) // Code 0: Prep for VRAM Read
                {
                    _readBuffer = VRAM[_controlWord & 0x3FFF];
                    IncrementVdpAddress();
                }
                else if (command == 2) // Code 2: Write to Internal VDP Register
                {
                    int regIndex = value & 0x0F;
                    byte regData = (byte)(_controlWord & 0xFF);
                    
                    if (regIndex < 16) Registers[regIndex] = regData;
                }
            }
        }

        private void IncrementVdpAddress()
        {
            // The VDP address register is only 14 bits
            // When it increments past 0x3FFF, it rolls over to 0x0000.
            // It can't overflow and corrupt the 2-bit command register (bits 14 and 15).
            ushort address = (ushort)(_controlWord & 0x3FFF);
            ushort command = (ushort)(_controlWord & 0xC000);

            address = (ushort)((address + 1) & 0x3FFF);
            _controlWord = (ushort)(command | address);
        }

        public byte ReadVCounter()
        {
             // NTSC Math: 262 lines. Counts 0 to 218, jumps to 213 (0xD5), counts to 255.
                if (_currentScanline <= 218) return (byte)_currentScanline;
                else return (byte)(_currentScanline - 6);
            
        }
        public byte ReadHCounter()
        {
            // The Master System H-Counter counts from 0x00 to 0x93, then jumps forward to 0xE9, ending at 0xFF.

            // 1 T-State = 1.5 pixels. The H-Counter increments every 2 pixels.
            // So H = T * 0.75
            int h = (int)(_tStateCounter * 0.75);

            if (h <= 0x93)
            {
                return (byte)h;
            }
            else
            {
                // Emulate the hardware jump!
                return (byte)(h - 0x94 + 0xE9);
            }
        }

        public void Update(int tStates)
        {
            _tStateCounter += tStates;

            if (_tStateCounter >= 228)
            {
                _tStateCounter -= 228;

                // 1. RENDER THE CURRENT LINE FIRST!
                // The CPU just finished spending 228 cycles on this exact line.
                // We draw it now using whatever scroll values the CPU set during that time.
                if (_currentScanline < 192)
                {
                    RenderScanline(_currentScanline);
                }

                // 2. CHECK LINE INTERRUPTS
                // Now that the line is drawn, we check if we need to alert the CPU for the NEXT line.
                if (_currentScanline <= 192)
                {
                    _lineCounter--;
                    if (_lineCounter < 0)
                    {
                        _lineCounter = Registers[10]; // Reload counter
                        _statusRegister |= 0x40;      // Set Line Interrupt Flag (Bit 6)
                    }
                }
                else
                {
                    _lineCounter = Registers[10]; // Reload outside active display
                }

                // 3. MOVE TO THE NEXT LINE
                _currentScanline++;

                int maxLines = 262;

                if (_currentScanline > maxLines -1)
                {
                    _currentScanline = 0;
                }

                // 4. TRIGGER VBLANK
                // The Master System sets the VBlank flag at the exact start of scanline 192.
                if (_currentScanline == 192)
                {
                    _statusRegister |= 0x80;
                }
            }
        }

        private void RenderScanline(int screenY)
        {
            // If the display is disabled, fill the line with black and exit
            if ((Registers[1] & 0x40) == 0)
            {
                for (int x = 0; x < 256; x++) FrameBuffer[(screenY * 256) + x] = unchecked((int)0xFF000000);
                return;
            }
            // --- 1. RENDER BACKGROUND LINE ---
            ushort nameTableBase = (ushort)((Registers[2] & 0x0E) << 10);
            int scrollX = Registers[8]; 
            int scrollY = Registers[9]; 

            // THE FIX: The bits are now in the correct order!
            bool lockColScroll = (Registers[0] & 0x80) != 0; // Bit 7: Locks right 8 columns (Fixes R-Type!)
            bool lockRowScroll = (Registers[0] & 0x40) != 0; // Bit 6: Locks top 2 rows (Fixes Bart!)
            bool maskLeftCol = (Registers[0] & 0x20) != 0; // Bit 5: Hides leftmost column (Fixes Sonic 2!)

            for (int screenX = 0; screenX < 256; screenX++)
            {
                // --- LEFT COLUMN MASKING (OVERSCAN CURTAIN) ---
                if (maskLeftCol && screenX < 8)
                {
                    // REPLACE THE R/G/B MATH WITH THIS SINGLE LINE:
                    int bgAddress = (screenY * 256) + screenX;
                    FrameBuffer[bgAddress] = GetRgbColour(16, Registers[7] & 0x0F);

                    _priorityBuffer[bgAddress] = true;
                    continue;
                }

                // Apply Vertical Scrolling (R-Type HUD protection)
                int effectiveScrollY = scrollY;
                if (lockColScroll && screenX >= 192) effectiveScrollY = 0;

                int vdpY = (screenY + effectiveScrollY) % 224;
                int row = vdpY / 8;
                int tileY = vdpY % 8;

                // Apply Horizontal Scrolling (Bart's sky protection)
                int effectiveScrollX = scrollX;
                if (lockRowScroll && screenY < 16) effectiveScrollX = 0;

                int vdpX = (screenX - effectiveScrollX) & 0xFF;
                int col = vdpX / 8;
                int tileX = vdpX % 8;

                ushort nameTableAddr = (ushort)(nameTableBase + (row * 64) + (col * 2));
                ushort tileData = (ushort)((VRAM[nameTableAddr + 1] << 8) | VRAM[nameTableAddr]);

                int tileIndex = tileData & 0x01FF;
                bool flipH = (tileData & 0x0200) != 0;
                bool flipV = (tileData & 0x0400) != 0;
                bool useSpritePalette = (tileData & 0x0800) != 0;
                bool priority = (tileData & 0x1000) != 0;

                int readY = flipV ? (7 - tileY) : tileY;
                ushort tileAddress = (ushort)(tileIndex * 32);

                byte bp0 = VRAM[tileAddress + (readY * 4) + 0];
                byte bp1 = VRAM[tileAddress + (readY * 4) + 1];
                byte bp2 = VRAM[tileAddress + (readY * 4) + 2];
                byte bp3 = VRAM[tileAddress + (readY * 4) + 3];

                int readX = flipH ? tileX : (7 - tileX);
                int colourIndex = ((bp0 >> readX) & 1) | (((bp1 >> readX) & 1) << 1) |
                                 (((bp2 >> readX) & 1) << 2) | (((bp3 >> readX) & 1) << 3);

                int paletteOffset = useSpritePalette ? 16 : 0;
                int finalColour = GetRgbColour(paletteOffset, colourIndex);

                int screenAddress = (screenY * 256) + screenX;

                // Draw background and reset priority mask for this exact pixel
                FrameBuffer[screenAddress] = finalColour;
                _priorityBuffer[screenAddress] = (priority && colourIndex != 0);
            }

            // --- 2. RENDER SPRITE LINE ---
            ushort satBaseAddress = (ushort)((Registers[5] & 0x7E) << 7);
            ushort spritePatternBase = (ushort)((Registers[6] & 0x04) << 11);
            bool is8x16 = (Registers[1] & 0x02) != 0;
            bool shiftSpritesLeft = (Registers[0] & 0x08) != 0;
            int spriteHeight = is8x16 ? 16 : 8;

            // Step A: Find the visible sprites for THIS specific line
            var visibleSprites = new System.Collections.Generic.List<int>();
            for (int i = 0; i < 64; i++)
            {
                byte y = VRAM[satBaseAddress + i];
                if (y == 208) break; // End of Sprite List

                int spriteY = y + 1; // Physical hardware 1-pixel shift
                if (screenY >= spriteY && screenY < spriteY + spriteHeight)
                {
                    visibleSprites.Add(i);
                    // HARDWARE QUIRK: VDP stops drawing after 8 sprites on a single line!
                    if (visibleSprites.Count == 8) break;
                }
            }

            // Step B: Draw them backward so Sprite 0 (highest priority) draws LAST and stays on top
            for (int v = visibleSprites.Count - 1; v >= 0; v--)
            {
                int i = visibleSprites[v];
                byte y = VRAM[satBaseAddress + i];
                byte x = VRAM[satBaseAddress + 0x80 + (i * 2)];
                byte tileIndex = VRAM[satBaseAddress + 0x80 + (i * 2) + 1];

                if (is8x16) tileIndex = (byte)(tileIndex & 0xFE);

                // Calculate which row of the sprite we are physically on
                int py = screenY - (y + 1);
                ushort tileAddress = (ushort)(spritePatternBase + (tileIndex * 32) + (py * 4));

                byte bp0 = VRAM[tileAddress + 0];
                byte bp1 = VRAM[tileAddress + 1];
                byte bp2 = VRAM[tileAddress + 2];
                byte bp3 = VRAM[tileAddress + 3];

                for (int px = 0; px < 8; px++)
                {
                    int screenX = x + px;
                    if (shiftSpritesLeft) screenX -= 8;

                    if (screenX < 0 || screenX >= 256) continue;
                    if (_priorityBuffer[(screenY * 256) + screenX]) continue;

                    int shift = 7 - px;
                    int colourIndex = ((bp0 >> shift) & 1) | (((bp1 >> shift) & 1) << 1) |
                                     (((bp2 >> shift) & 1) << 2) | (((bp3 >> shift) & 1) << 3);

                    if (colourIndex == 0) continue;

                    // REPLACE THE R/G/B MATH WITH THIS SINGLE LINE:
                    FrameBuffer[(screenY * 256) + screenX] = GetRgbColour(16, colourIndex);
                }
            }
        }

        public int GetRgbColour(int paletteOffset, int colourIndex)
        {
            //Debug.WriteLine(_isGameGear);
            int r, g, b;

            if (IsGameGear)
            {
                // Game Gear: 2 bytes per colour. Format: ----BBBBGGGGRRRR
                int cramIndex = (paletteOffset + colourIndex) * 2;
                ushort ggcolour = (ushort)(ggCRAM[cramIndex] | (ggCRAM[cramIndex + 1] << 8));

                // Extract 4-bit values (0-15) and map them to standard 8-bit values (0-255). 
                // We multiply by 17 because 255 / 15 = 17!
                r = (ggcolour & 0x0F) * 17;
                g = ((ggcolour >> 4) & 0x0F) * 17;
                b = ((ggcolour >> 8) & 0x0F) * 17;
            }
            else
            {
                // Master System: 1 byte per colour. Format: --BBGGRR
                byte smscolour = smsCRAM[paletteOffset + colourIndex];

                // Extract 2-bit values (0-3) and map them to standard 8-bit values (0-255).
                // We multiply by 85 because 255 / 3 = 85!
                r = (smscolour & 0x03) * 85;
                g = ((smscolour >> 2) & 0x03) * 85;
                b = ((smscolour >> 4) & 0x03) * 85;
            }

            return (255 << 24) | (r << 16) | (g << 8) | b;
        }

        public void Reset()
        {
            _tStateCounter = 0;
            _currentScanline = 0;
            _lineCounter = 0;
            _statusRegister = 0x00;
            _controlWord = 0;
            _isSecondControlByte = false;
            _readBuffer = 0;
        }
        public void SaveState(BinaryWriter bw)
        {
            bw.Write(VRAM);
            bw.Write(smsCRAM);
            bw.Write(ggCRAM);
            bw.Write(Registers);
            bw.Write(_isSecondControlByte);
            bw.Write(_controlWord);
            bw.Write(_readBuffer);
            bw.Write(_tStateCounter);
            bw.Write(_currentScanline);
            bw.Write(_lineCounter);
            bw.Write(_statusRegister);

            // ADD THESE:
            bw.Write(_latchedHScroll);
            bw.Write(_latchedVScroll);
            bw.Write(IsGameGear);
        }

        public void LoadState(BinaryReader br)
        {
            Array.Copy(br.ReadBytes(VRAM.Length), VRAM, VRAM.Length);
            Array.Copy(br.ReadBytes(smsCRAM.Length), smsCRAM, smsCRAM.Length);
            Array.Copy(br.ReadBytes(ggCRAM.Length), ggCRAM, ggCRAM.Length);
            Array.Copy(br.ReadBytes(Registers.Length), Registers, Registers.Length);
            _isSecondControlByte = br.ReadBoolean();
            _controlWord = br.ReadUInt16();
            _readBuffer = br.ReadByte();
            _tStateCounter = br.ReadInt32();
            _currentScanline = br.ReadInt32();
            _lineCounter = br.ReadInt32();
            _statusRegister = br.ReadByte();

            // ADD THESE:
            _latchedHScroll = br.ReadInt32();
            _latchedVScroll = br.ReadInt32();
            IsGameGear = br.ReadBoolean();
        }
    }
}