﻿using BizHawk.Common;
using BizHawk.Common.NumberExtensions;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Components.Z80A;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BizHawk.Emulation.Cores.Computers.AmstradCPC
{
    /// <summary>
    /// * Amstrad Gate Array *
    /// http://www.cpcwiki.eu/index.php/Gate_Array
    /// https://web.archive.org/web/20170612081209/http://www.grimware.org/doku.php/documentations/devices/gatearray
    /// </summary>
    public class AmstradGateArray : IPortIODevice, IVideoProvider
    {
        #region Devices

        private CPCBase _machine;
        private Z80A CPU => _machine.CPU;
        private CRCT_6845 CRCT => _machine.CRCT;
        //private CRTDevice CRT => _machine.CRT;
        private IPSG PSG => _machine.AYDevice;
        private NECUPD765 FDC => _machine.UPDDiskDevice;
        private DatacorderDevice DATACORDER => _machine.TapeDevice;
        private ushort BUSRQ => CPU.MEMRQ[CPU.bus_pntr];
        public const ushort PCh = 1;

        private GateArrayType ChipType;

        #endregion

        #region Palettes

        /// <summary>
        /// The standard CPC Pallete (ordered by firmware #)
        /// http://www.cpcwiki.eu/index.php/CPC_Palette
        /// </summary>
        public static readonly int[] CPCFirmwarePalette =
        {
            Colors.ARGB(0x00, 0x00, 0x00), // Black
            Colors.ARGB(0x00, 0x00, 0x80), // Blue
            Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue
            Colors.ARGB(0x80, 0x00, 0x00), // Red            
            Colors.ARGB(0x80, 0x00, 0x80), // Magenta
            Colors.ARGB(0x80, 0x00, 0xFF), // Mauve
            Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red
            Colors.ARGB(0xFF, 0x00, 0x80), // Purple
            Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta
            Colors.ARGB(0x00, 0x80, 0x00), // Green
            Colors.ARGB(0x00, 0x80, 0x80), // Cyan
            Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue
            Colors.ARGB(0x80, 0x80, 0x00), // Yellow
            Colors.ARGB(0x80, 0x80, 0x80), // White
            Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue
            Colors.ARGB(0xFF, 0x80, 0x00), // Orange
            Colors.ARGB(0xFF, 0x80, 0x80), // Pink
            Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta
            Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green
            Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green
            Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan
            Colors.ARGB(0x80, 0xFF, 0x00), // Lime
            Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green
            Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan
            Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow
            Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow
            Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White
        };

        /// <summary>
        /// The standard CPC Pallete (ordered by hardware #)
        /// http://www.cpcwiki.eu/index.php/CPC_Palette
        /// </summary>
        public static readonly int[] CPCHardwarePalette =
        {
            Colors.ARGB(0x80, 0x80, 0x80), // White
            Colors.ARGB(0x80, 0x80, 0x80), // White (duplicate)
            Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green
            Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow
            Colors.ARGB(0x00, 0x00, 0x80), // Blue
            Colors.ARGB(0xFF, 0x00, 0x80), // Purple
            Colors.ARGB(0x00, 0x80, 0x80), // Cyan
            Colors.ARGB(0xFF, 0x80, 0x80), // Pink
            Colors.ARGB(0xFF, 0x00, 0x80), // Purple (duplicate)
            Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow (duplicate)
            Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow
            Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White
            Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red
            Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta
            Colors.ARGB(0xFF, 0x80, 0x00), // Orange
            Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta
            Colors.ARGB(0x00, 0x00, 0x80), // Blue (duplicate)
            Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green (duplicate)
            Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green
            Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan
            Colors.ARGB(0x00, 0x00, 0x00), // Black
            Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue
            Colors.ARGB(0x00, 0x80, 0x00), // Green
            Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue
            Colors.ARGB(0x80, 0x00, 0x80), // Magenta
            Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green
            Colors.ARGB(0x80, 0xFF, 0x00), // Lime
            Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan
            Colors.ARGB(0x80, 0x00, 0x00), // Red     
            Colors.ARGB(0x80, 0x00, 0xFF), // Mauve
            Colors.ARGB(0x80, 0x80, 0x00), // Yellow            
            Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue
        };

        #endregion

        #region Clocks and Timing

        /// <summary>
        /// The Gate Array Clock Speed
        /// </summary>
        public int GAClockSpeed = 16000000;

        /// <summary>
        /// The CPU Clock Speed
        /// </summary>
        public int Z80ClockSpeed = 4000000;

        /// <summary>
        /// CRCT Clock Speed
        /// </summary>
        public int CRCTClockSpeed = 1000000;

        /// <summary>
        /// AY-3-8912 Clock Speed
        /// </summary>
        public int PSGClockSpeed = 1000000;

        /// <summary>
        /// Number of CPU cycles in one frame
        /// </summary>
        public int FrameLength = 79872;

        /// <summary>
        /// Number of Gate Array cycles within one frame
        /// </summary>
        public int GAFrameLength = 319488;

        #endregion

        #region Construction

        public AmstradGateArray(CPCBase machine, GateArrayType chipType)
        {
            _machine = machine;
            ChipType = chipType;
            //PenColours = new int[17];
            borderType = _machine.CPC.SyncSettings.BorderType;
            SetupScreenSize();
            //Reset();

            CRCT.AttachHSYNCCallback(OnHSYNC);
            CRCT.AttachVSYNCCallback(OnVSYNC);

            CurrentLine = new CharacterLine();
            InitByteLookup();
            CalculateNextScreenMemory();
        }

        #endregion

        #region Registers and Internal State

        /// <summary>
        /// PENR (register 0) - Pen Selection
        /// This register can be used to select one of the 17 color-registers (pen 0 to 15 or the border). 
        /// It will remain selected until another PENR command is executed.
        /// PENR	    Index	
        /// 7	6	5	4	3	2	1	0	color register selected
        /// 0	0	0	0	n   n   n   n   pen n from 0 to 15 (4bits)
        /// 0	0	0	1	x   x   x   x   border
        /// 
        /// x can be 0 or 1, it doesn't matter
        /// </summary>        
        private byte _PENR;
        public byte PENR
        {
            get { return _PENR; }
            set
            {
                _PENR = value;
                if (_PENR.Bit(4))
                {
                    // border select
                    CurrentPen = 16;
                }
                else
                {
                    // pen select
                    CurrentPen = _PENR & 0x0f;
                }
            }
        }

        /// <summary>
        /// 0-15:   Pen Registers
        /// 16:     Border Colour
        /// </summary>
        public int[] ColourRegisters = new int[17];

        /// <summary>
        /// The currently selected Pen
        /// </summary>
        private int CurrentPen;

        /// <summary>
        /// INKR (register 1) - Colour Selection
        /// This register takes a 5bits parameter which is a color-code. This color-code range from 0 to 31 but there's only 27 differents colors 
        /// (because the Gate Array use a 3-states logic on the R,G and B signals, thus 3x3x3=27).
        /// INKR	    Color	
        /// 7	6	5	4	3	2	1	0	
        /// 0	1	0	n   n   n   n   n   where n is a color code (5 bits)
        /// 
        /// The PEN affected by the INKR command is updated (almost) immediatly
        /// </summary>        
        private byte _INKR;
        public byte INKR
        {
            get { return _INKR; }
            set
            {
                _INKR = value;
                ColourRegisters[CurrentPen] = _INKR & 0x1f;
            }
        }

        /// <summary>
        /// RMR (register 2) - Select screen mode and ROM configuration
        /// This register control the interrupt counter (reset), the upper and lower ROM paging and the video mode.
        /// RMR	        Commands
        /// 7	6	5	4	3	2	1	0
        /// 1	0	0	I   UR  LR  VM-->
        /// 
        /// I   : if set (1), this will reset the interrupt counter
        /// UR  : Enable (0) or Disable (1) the upper ROM paging (&amp;C000 to &amp;FFFF). You can select which upper ROM with the I/O address &amp;DF00
        /// LR  : Enable (0) or Disable (1) the lower ROM paging
        /// VM  : Select the video mode 0,1,2 or 3 (it will take effect after the next HSync)
        /// </summary>
        private byte _RMR;
        public byte RMR
        {
            get { return _RMR; }
            set
            {
                _RMR = value;
                //ScreenMode = _RMR & 0x03;
                var sm = _RMR & 0x03;
                if (sm != 1)
                {

                }

                if ((_RMR & 0x08) != 0)
                    _machine.UpperROMPaged = false;
                else
                    _machine.UpperROMPaged = true;

                if ((_RMR & 0x04) != 0)
                    _machine.LowerROMPaged = false;
                else
                    _machine.LowerROMPaged = true;

                if (_RMR.Bit(4))
                {
                    // reset interrupt counter
                    InterruptCounter = 0;
                }
            }
        }

        /// <summary>
        /// RAMR (register 3) - RAM Banking
        /// This register exists only in CPCs with 128K RAM (like the CPC 6128, or CPCs with Standard Memory Expansions)
        /// Note: In the CPC 6128, the register is a separate PAL that assists the Gate Array chip
        /// 
        /// Bit	Value	Function
        /// 7	1	    Gate Array function 3
        /// 6	1
        /// 5	b	    64K bank number(0..7); always 0 on an unexpanded CPC6128, 0-7 on Standard Memory Expansions
        /// 4	b
        /// 3	b
        /// 2	x       RAM Config(0..7)
        /// 1	x       ""  
        /// 0	x       ""
        /// 
        /// The 3bit RAM Config value is used to access the second 64K of the total 128K RAM that is built into the CPC 6128 or the additional 64K-512K of standard memory expansions. 
        /// These contain up to eight 64K ram banks, which are selected with bit 3-5. A standard CPC 6128 only contains bank 0. Normally the register is set to 0, so that only the 
        /// first 64K RAM are used (identical to the CPC 464 and 664 models). The register can be used to select between the following eight predefined configurations only:
        /// 
        /// -Address-   0       1       2       3       4       5       6       7
        /// 0000-3FFF   RAM_0   RAM_0   RAM_4   RAM_0   RAM_0   RAM_0   RAM_0   RAM_0
        /// 4000-7FFF   RAM_1   RAM_1   RAM_5   RAM_3   RAM_4   RAM_5   RAM_6   RAM_7
        /// 8000-BFFF   RAM_2   RAM_2   RAM_6   RAM_2   RAM_2   RAM_2   RAM_2   RAM_2
        /// C000-FFFF   RAM_3   RAM_7   RAM_7   RAM_7   RAM_3   RAM_3   RAM_3   RAM_3
        /// 
        /// The Video RAM is always located in the first 64K, VRAM is in no way affected by this register
        /// </summary>
        private byte _RAMR;
        /// <summary>
        /// This is actually implemented outside of here. These values do nothing.
        /// </summary>
        public byte RAMR
        {
            get { return _RAMR; }
            set
            {
                _RAMR = value;
            }
        }

        /// <summary>
        /// The selected screen mode (updated after every HSYNC)
        /// </summary>
        private int ScreenMode;

        /// <summary>
        /// Simulates the internal 6bit INT counter
        /// </summary>
        private int _interruptCounter;
        public int InterruptCounter
        {
            get { return _interruptCounter; }
            set { _interruptCounter = value; }
        }

        /// <summary>
        /// Interrupts are delayed when a VSYNC occurs
        /// </summary>
        private int VSYNCDelay;

        /// <summary>
        /// Signals that the frame end has been reached
        /// </summary>
        public bool FrameEnd;

        /// <summary>
        /// Internal phase clock
        /// </summary>
        private int ClockCounter;

        /// <summary>
        /// Master frame clock counter
        /// </summary>
        public int FrameClock;

        /// <summary>
        /// Simulates the gate array memory /WAIT line
        /// </summary>
        private bool WaitLine;     

        /// <summary>
        /// 16-bit address - read from the CRCT
        /// </summary>
        private short _MA;
        private short MA
        {
            get
            {
                _MA = CRCT.MA;
                return _MA;
            }
        }

        /// <summary>
        /// Set when the HSYNC signal is detected from the CRCT
        /// </summary>
        private bool HSYNC;

//      /// <summary>
//      /// Is set when an initial HSYNC is seen from the CRCT
//      /// On real hardware interrupt generation is based on the falling edge of the HSYNC signal
//      /// So in this emulation, once the falling edge is detected, interrupt processing happens
//      /// </summary>
//      private bool HSYNC_falling;

        /// <summary>
        /// Used to count HSYNCs during a VSYNC
        /// </summary>
        private int HSYNC_counter;

        /// <summary>
        /// Set when the VSYNC signal is detected from the CRCT
        /// </summary>
        private bool VSYNC;

        /// <summary>
        /// TRUE when the /INT pin is held low
        /// </summary>
        private bool InterruptRaised;

        /// <summary>
        /// Counts the GA cycles that the /INT pin should be held low
        /// </summary>
        private int InterruptHoldCounter;

        /// <summary>
        /// Set at the start of a new frame
        /// </summary>
        public bool IsNewFrame;

        /// <summary>
        /// Set when a new line is beginning
        /// </summary>
        public bool IsNewLine;

        /// <summary>
        /// Horizontal Character Counter
        /// </summary>
        private int HCC;

        /// <summary>
        /// Vertical Line Counter
        /// </summary>
        private int VLC;

        /// <summary>
        /// The first video byte fetched
        /// </summary>
        private byte VideoByte1;

        /// <summary>
        /// The second video byte fetched
        /// </summary>
        private byte VideoByte2;

        #endregion

        #region Clock Business

        /// <summary>
        /// Called every CPU cycle
        /// In reality the GA is clocked at 16Mhz (4 times the frequency of the CPU)
        /// Therefore this method has to take care of:
        /// 4 GA cycles
        /// 1 CRCT cycle every 4 calls
        /// 1 PSG cycle every 4 calls
        /// 1 CPU cycle (uncontended)
        /// </summary>
        public void ClockCycle()
        {
            // gatearray uses 4-phase clock to supply clocks to other devices
            switch (ClockCounter)
            {
                case 0:
                    CRCT.ClockCycle();
                    WaitLine = false;
                    break;
                case 1:
                    WaitLine = true;
                    // detect new scanline and upcoming new frame on next render cycle
                    //FrameDetector();
                    break;
                case 2:
                    // video fetch
                    WaitLine = true;
                    //FetchByte(1);
                    break;
                case 3:
                    // video fetch and render
                    WaitLine = true;
                    //FetchByte(2);
                    GACharacterCycle();
                    //PixelGenerator();
                    break;
            }

            if (!HSYNC && CRCT.HSYNC)
            {
                HSYNC = true;
            }

            // run the interrupt generator routine
            InterruptGenerator();

            if (!CRCT.HSYNC)
            {
                HSYNC = false;
            }

            // conditional CPU cycle
            DoConditionalCPUCycle();

            AdvanceClock();
        }

        /// <summary>
        /// Increments the internal clock counters by one
        /// </summary>
        private void AdvanceClock()
        {
            FrameClock++;
            ClockCounter++;

            if (ClockCounter == 4)
                ClockCounter = 0;

            // check for frame end
            if (FrameClock == FrameLength)
            {
                FrameEnd = true;
            }
        }

        /// <summary>
        /// Runs a 4 Mhz CPU cycle if neccessary
        /// /WAIT line status is a factor here
        /// </summary>
        private void DoConditionalCPUCycle()
        {
            if (!WaitLine)
            {
                // /WAIT line is NOT active
                CPU.ExecuteOne();
                return;
            }

            // /WAIT line is active            
            switch (ClockCounter)
            {
                case 2:
                case 3:
                    // gate array video fetch is occuring
                    // check for memory access
                    if (BUSRQ > 0)
                    {
                        // memory action upcoming - CPU clock is halted
                        CPU.TotalExecutedCycles++;
                    }
                    break;

                case 1:
                    // CPU accesses RAM if it's performing a non-opcode read or write
                    // assume for now that an opcode fetch is always looking at PC
                    if (BUSRQ == PCh)
                    {
                        // opcode fetch memory action upcoming - CPU clock is halted
                        CPU.TotalExecutedCycles++;
                    }
                    else
                    {
                        // no fetch, or non-opcode fetch
                        CPU.ExecuteOne();
                    }
                    break;
            }
        }

        #endregion

        #region Frame & Interrupt Handling

        /// <summary>
        /// The CRCT builds the picture in a strange way, so that the top left of the display area is the first pixel from
        /// video RAM. The borders come either side of the HSYNC and VSYNCs later on:
        /// https://web.archive.org/web/20170501112330im_/http://www.grimware.org/lib/exe/fetch.php/documentations/devices/crtc.6845/crtc.standard.video.frame.png?w=800&amp;h=500
        /// Therefore when the gate array initialises, we will attempt end the frame early in order to
        /// sync up at the point where VSYNC is active and HSYNC just begins. This is roughly how a CRT monitor would display the picture.
        /// The CRT would start a new line at the point where an HSYNC is detected.
        /// </summary>
        private void FrameDetector()
        {
            if (CRCT.HSYNC && !IsNewLine)
            {
                // start of a new line on the next render cycle
                IsNewLine = true;

                // process scanline
                //CRT.CurrentLine.CommitScanline();

                // check for end of frame
                if (CRCT.VSYNC && !IsNewFrame)
                {
                    // start of a new frame on the next render cycle
                    IsNewFrame = true;
                    //FrameEnd = true;                    
                    VLC = 0;
                }
                else if (!CRCT.VSYNC)
                {
                    // increment line counter
                    VLC++;
                    IsNewFrame = false;
                }

                HCC = 0;

                // update screenmode
                //ScreenMode = RMR & 0x03;
                //CRT.CurrentLine.InitScanline(ScreenMode, VLC);
            }
            else if (!CRCT.HSYNC)
            {
                // reset the flags
                IsNewLine = false;
                IsNewFrame = false;
            }
        }

        /// <summary>
        /// Handles interrupt generation
        /// </summary>
        private void InterruptGenerator()
        {
            if (HSYNC && !CRCT.HSYNC)
            {
                // falling edge of the HSYNC detected
                InterruptCounter++;

                if (CRCT.VSYNC)
                {
                    if (HSYNC_counter >= 2)
                    {
                        // x2 HSYNC have happened during VSYNC
                        if (InterruptCounter >= 32)
                        {
                            // no interrupt
                            InterruptCounter = 0;
                        }
                        else if (InterruptCounter < 32)
                        {
                            // interrupt
                            InterruptRaised = true;
                            InterruptCounter = 0;
                        }

                        HSYNC_counter = 0;
                    }
                    else
                    {
                        HSYNC_counter++;
                    }
                }

                if (InterruptCounter == 52)
                {
                    // gatearray should raise an interrupt
                    InterruptRaised = true;
                    InterruptCounter = 0;
                }
            }

            if (InterruptRaised)
            {
                // interrupt should been raised
                CPU.FlagI = true;
                InterruptHoldCounter++;

                // the INT signal should be held low for 1.4us.
                // in gatearray cycles, this equates to 22.4
                // we will round down to 22 for emulation purposes
                if (InterruptHoldCounter >= 22)
                {
                    CPU.FlagI = false;
                    InterruptRaised = false;
                    InterruptHoldCounter = 0;
                }
            }
        }

        #endregion

        #region Rendering Business

        /// <summary>
        /// Builds up current scanline character information
        /// Ther GA modifies HSYNC and VSYNC signals before they are sent to the monitor
        /// This is handled here
        /// Runs at 1Mhz
        /// </summary>
        private void GACharacterCycle()
        {
            if (CRCT.VSYNC && CRCT.HSYNC)
            {
                // both hsync and vsync active
                CurrentLine.AddCharacter(Phase.HSYNCandVSYNC);
            }
            else if (CRCT.VSYNC)
            {
                // vsync is active but hsync is not
                CurrentLine.AddCharacter(Phase.VSYNC);
            }
            else if (CRCT.HSYNC)
            {
                // hsync is active but vsync is not
                CurrentLine.AddCharacter(Phase.HSYNC);
            }
            else if (!CRCT.DISPTMG)
            {
                // border generation
                CurrentLine.AddCharacter(Phase.BORDER);
            }
            else if (CRCT.DISPTMG)
            {
                // pixels generated from video RAM
                CurrentLine.AddCharacter(Phase.DISPLAY);
            }
        }

        /// <summary>
        /// Holds the upcoming video RAM addresses for the next scanline
        /// Firmware default size is 80 (40 characters - 2 bytes per character)
        /// </summary>
        private ushort[] NextVidRamLine = new ushort[40 * 2];

        /// <summary>
        /// The current character line we are working from
        /// </summary>
        private CharacterLine CurrentLine;

        /// <summary>
        /// List of screen lines as they are built up
        /// </summary>
        private List<CharacterLine> ScreenLines = new List<CharacterLine>();

        /// <summary>
        /// Pixel value lookups for every scanline byte value
        /// Based on the lookup at https://github.com/gavinpugh/xnacpc
        /// </summary>
        private int[][] ByteLookup = new int[4][];
        private void InitByteLookup()
        {
            int pix;
            for (int m = 0; m < 4; m++)
            {
                int pos = 0;
                ByteLookup[m] = new int[256 * 8];
                for (int b = 0; b < 256; b++)
                {
                    switch (m)
                    {
                        case 0:
                            pix = b & 0xaa;
                            pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2));
                            for (int c = 0; c < 4; c++)
                                ByteLookup[m][pos++] = pix;
                            pix = b & 0x55;
                            pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3));
                            for (int c = 0; c < 4; c++)
                                ByteLookup[m][pos++] = pix;
                            break;
                        case 1:
                            pix = (((b & 0x80) >> 7) | ((b & 0x08) >> 2));
                            ByteLookup[m][pos++] = pix;
                            ByteLookup[m][pos++] = pix;
                            pix = (((b & 0x40) >> 6) | ((b & 0x04) >> 1));
                            ByteLookup[m][pos++] = pix;
                            ByteLookup[m][pos++] = pix;
                            pix = (((b & 0x20) >> 5) | (b & 0x02));
                            ByteLookup[m][pos++] = pix;
                            ByteLookup[m][pos++] = pix;
                            pix = (((b & 0x10) >> 4) | ((b & 0x01) << 1));
                            ByteLookup[m][pos++] = pix;
                            ByteLookup[m][pos++] = pix;
                            break;
                        case 2:
                            for (int i = 7; i >= 0; i--)
                                ByteLookup[m][pos++] = ((b & (1 << i)) != 0) ? 1 : 0;
                            break;
                        case 3:
                            pix = b & 0xaa;
                            pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2));
                            for (int c = 0; c < 4; c++)
                                ByteLookup[m][pos++] = pix;
                            pix = b & 0x55;
                            pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3));
                            for (int c = 0; c < 4; c++)
                                ByteLookup[m][pos++] = pix;
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Runs at HSYNC *AFTER* the scanline has been commmitted
        /// Sets up the upcoming memory addresses for the next scanline
        /// </summary>
        private void CalculateNextScreenMemory()
        {
            var verCharCount = CRCT.VCC;
            var verRasCount = CRCT.VLC;

            var screenWidthByteCount = CRCT.DisplayWidth * 2;
            NextVidRamLine = new ushort[screenWidthByteCount * 2];
            var screenHeightCharCount = CRCT.DisplayHeightInChars;
            var screenAddress = CRCT.MA;

            int baseAddress = ((screenAddress << 2) & 0xf000);
            int offset = (screenAddress * 2) & 0x7ff;

            int x = offset + ((verCharCount * screenWidthByteCount) & 0x7ff);
            int y = baseAddress + (verRasCount * 0x800);

            for (int b = 0; b < screenWidthByteCount; b++)
            {
                NextVidRamLine[b] = (ushort)(y + x);
                x++;
                x &= 0x7ff;
            }
        }

        /// <summary>
        /// Called at the start of HSYNC, this renders the currently built-up scanline
        /// </summary>
        private void RenderScanline()
        {
            // memory addresses
            int cRow = CRCT.VCC;
            int cRas = CRCT.VLC;

            int screenByteWidth = CRCT.DisplayWidth * 2;
            var screenHeightCharCount = CRCT.DisplayHeightInChars;
            //CalculateNextScreenMemory();
            var crctAddr = CRCT.DStartHigh << 8;
            crctAddr |= CRCT.DStartLow;
            var baseAddr =  ((crctAddr << 2) & (0xF000)); //CRCT.VideoPageBase;//
            var baseOffset =  (crctAddr * 2) & 0x7FF; //CRCT.VideoRAMOffset * 2; //
            var xA = baseOffset + ((cRow * screenByteWidth) & 0x7ff);
            var yA = baseAddr + (cRas * 2048);

            // border and display
            int pix = 0;
            int scrByte = 0;
            
            for (int i = 0; i < CurrentLine.PhaseCount; i++)
            {
                // every character renders 8 pixels
                switch (CurrentLine.Phases[i])
                {
                    case Phase.NONE:
                        break;
                    
                    case Phase.HSYNC:
                        break;
                    case Phase.HSYNCandVSYNC:
                        break;
                    case Phase.VSYNC:
                        break;
                    case Phase.BORDER:
                        // output current border colour
                        for (pix = 0; pix < 16; pix++)
                        {
                            CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[16]]);
                        }
                        break;
                    case Phase.DISPLAY:
                        // each character references 2 bytes in video RAM
                        byte data;

                        for (int by = 0; by < 2; by++)
                        {
                            ushort addr = (ushort)(yA + xA);
                            data = _machine.FetchScreenMemory(addr);
                            scrByte++;

                            switch (CurrentLine.ScreenMode)
                            {
                                case 0:
                                    pix = data & 0xaa;
                                    pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2));
                                    for (int c = 0; c < 4; c++)
                                        CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    pix = data & 0x55;
                                    pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3));
                                    for (int c = 0; c < 4; c++)
                                        CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    break;
                                case 1:
                                    pix = (((data & 0x80) >> 7) | ((data & 0x08) >> 2));
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    pix = (((data & 0x40) >> 6) | ((data & 0x04) >> 1));
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    pix = (((data & 0x20) >> 5) | (data & 0x02));
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    pix = (((data & 0x10) >> 4) | ((data & 0x01) << 1));
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    break;
                                case 2:
                                    pix = data;
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(7) ? 1 : 0]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(6) ? 1 : 0]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(5) ? 1 : 0]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(4) ? 1 : 0]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(3) ? 1 : 0]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(2) ? 1 : 0]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(1) ? 1 : 0]]);
                                    CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(0) ? 1 : 0]]);
                                    break;
                                case 3:
                                    pix = data & 0xaa;
                                    pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2));
                                    for (int c = 0; c < 4; c++)
                                        CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    pix = data & 0x55;
                                    pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3));
                                    for (int c = 0; c < 4; c++)
                                        CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]);
                                    break;
                            }

                            xA++;
                            xA &= 0x7ff;
                        }

                        break;
                }
            }

            // add to the list
            ScreenLines.Add(new CharacterLine
            {
                ScreenMode = CurrentLine.ScreenMode,
                Phases = CurrentLine.Phases.ToList(),
                Pixels = CurrentLine.Pixels.ToList()
            });
        }

        #endregion       

        #region Public Methods

        /// <summary>
        /// Called when the Z80 acknowledges an interrupt
        /// </summary>
        public void IORQA()
        {
            // bit 5 of the interrupt counter is reset
            InterruptCounter &= ~(1 << 5);
        }

        private int slCounter = 0;
        private int slBackup = 0;

        /// <summary>
        /// Fired when the CRCT flags HSYNC
        /// </summary>
        public void OnHSYNC()
        {
            HSYNC = true;
            slCounter++;

            // commit the scanline
            RenderScanline();

            // setup vid memory for next scanline
            CalculateNextScreenMemory();

            if (CRCT.VLC == 0)
            {
                // update screenmode
                ScreenMode = _RMR & 0x03;
            }

            // setup scanline for next
            CurrentLine.Clear(ScreenMode);
        }

        /// <summary>
        /// Fired when the CRCT flags VSYNC
        /// </summary>
        public void OnVSYNC()
        {
            FrameEnd = true;
            slBackup = slCounter;
            slCounter = 0;
        }

        #endregion

        #region IVideoProvider

        public int[] ScreenBuffer;

        private int _virtualWidth;
        private int _virtualHeight;
        private int _bufferWidth;
        private int _bufferHeight;

        public int BackgroundColor
        {
            get { return CPCHardwarePalette[0]; }
        }

        public int VirtualWidth
        {
            get { return _virtualWidth; }
            set { _virtualWidth = value; }
        }

        public int VirtualHeight
        {
            get { return _virtualHeight; }
            set { _virtualHeight = value; }
        }

        public int BufferWidth
        {
            get { return _bufferWidth; }
            set { _bufferWidth = value; }
        }

        public int BufferHeight
        {
            get { return _bufferHeight; }
            set { _bufferHeight = value; }
        }

        public int SysBufferWidth;
        public int SysBufferHeight;

        public int VsyncNumerator
        {
            get { return 200000000; }
            set { }
        }

        public int VsyncDenominator
        {
            get { return Z80ClockSpeed; }
        }

        public int[] GetVideoBuffer()
        {
            // get only lines that have pixel data
            var lines = ScreenLines.Where(a => a.Pixels.Count > 0).ToList();
            var height = lines.Count();

            int pos = 0;
            int lCount = 0;
            foreach (var l in lines)
            {
                var lCop = l.Pixels.ToList();
                var len = l.Pixels.Count;
                if (l.Phases.Contains(Phase.VSYNC) && l.Phases.Contains(Phase.BORDER))
                    continue;

                if (len < 320)
                    continue;

                var pad = BufferWidth - len;
                if (pad < 0)
                {
                    // trim the left and right
                    var padPos = pad * -1;
                    var excessL = padPos / 2;
                    var excessR = excessL + (padPos % 2);
                    for (int i = 0; i < excessL; i++)
                    {
                        var lThing = lCop.First();

                        lCop.Remove(lThing);
                    }
                    for (int i = 0; i < excessL; i++)
                    {
                        var lThing = lCop.Last();

                        lCop.Remove(lThing);
                    }
                }

                var lPad = pad / 2;
                var rPad = lPad + (pad % 2);

                for (int i = 0; i < 2; i++)
                {
                    lCount++;

                    for (int pL = 0; pL < lPad; pL++)
                    {
                        ScreenBuffer[pos++] = 0;
                    }

                    for (int pix = 0; pix < lCop.Count; pix++)
                    {
                        ScreenBuffer[pos++] = lCop[pix];
                    }

                    for (int pR = 0; pR < rPad; pR++)
                    {
                        ScreenBuffer[pos++] = 0;
                    }
                } 
                
                if (lCount >= BufferHeight - 2)
                {
                    break;
                }       
            }

            ScreenLines.Clear();

            return ScreenBuffer;
			/*
            switch (borderType)
            {
                // crop to 768x272 (544)
                // borders 64px - 64 scanlines
                case AmstradCPC.BorderType.Uniform:
                    /*
                    var slSize = 64;
                    var dispLines = (24 * 8) * 2;
                    var origTopBorder = (7 * 8) * 2;                    
                    var origBotBorder = (5 * 8) * 2;

                    var lR = 16;
                    var rR = 16;

                    var trimTop = origTopBorder - slSize;

                    var startP = SysBufferWidth * (origTopBorder - 64);
                    var index1 = 0;

                    // line by line
                    int cnt = 0;
                    for (int line = startP; line < ScreenBuffer.Length; line += SysBufferWidth)
                    {
                        cnt++;
                        // pixels in line
                        for (int p = lR; p < SysBufferWidth - rR; p++)
                        {
                            if (index1 == croppedBuffer.Length)
                                break;

                            croppedBuffer[index1++] = ScreenBuffer[line + p];
                        }
                    }
                    return croppedBuffer;
                    */
			/*
			var slWidth = BufferWidth;
			return ScreenBuffer;

			break;


	}
	*/
			//return ScreenBuffer;
		}

		public void SetupScreenSize()
        {
            SysBufferWidth = 800;
            SysBufferHeight = 600;
            BufferHeight = SysBufferHeight;
            BufferWidth = SysBufferWidth;
            VirtualHeight = BufferHeight;
            VirtualWidth = BufferWidth;
            ScreenBuffer = new int[BufferWidth * BufferHeight];
            croppedBuffer = ScreenBuffer;

            switch (borderType)
            {
                case AmstradCPC.BorderType.Uncropped:
                    break;

                case AmstradCPC.BorderType.Uniform:
                    BufferWidth = 800;
                    BufferHeight = 600;
                    VirtualHeight = BufferHeight;
                    VirtualWidth = BufferWidth;
                    croppedBuffer = new int[BufferWidth * BufferHeight];
                    break;

                case AmstradCPC.BorderType.Widescreen:
                    break;
            }
        }

        protected int[] croppedBuffer;

        private AmstradCPC.BorderType _borderType;

        public AmstradCPC.BorderType borderType
        {
            get { return _borderType; }
            set { _borderType = value; }
        }

        #endregion

        #region IPortIODevice

        /// <summary>
        /// Device responds to an IN instruction
        /// </summary>
        public bool ReadPort(ushort port, ref int result)
        {
            // gate array is OUT only
            return false;
        }

        /// <summary>
        /// Device responds to an OUT instruction
        /// </summary>
        public bool WritePort(ushort port, int result)
        {
            BitArray portBits = new BitArray(BitConverter.GetBytes(port));
            BitArray dataBits = new BitArray(BitConverter.GetBytes((byte)result));
            byte portUpper = (byte)(port >> 8);
            byte portLower = (byte)(port & 0xff);

            // The gate array is selected when bit 15 of the I/O port address is set to "0" and bit 14 of the I/O port address is set to "1"
            bool accessed = false;
            if (!portUpper.Bit(7) && portUpper.Bit(6))
                accessed = true;

            if (!accessed)
                return accessed;

            // Bit 9 and 8 of the data byte define the function to access
            if (!dataBits[6] && !dataBits[7])
            {
                // select pen
                PENR = (byte)result;
            }

            if (dataBits[6] && !dataBits[7])
            {
                // select colour for selected pen
                INKR = (byte)result;
            }

            if (!dataBits[6] && dataBits[7])
            {
                // select screen mode, ROM configuration and interrupt control
                RMR = (byte)result;
            }

            if (dataBits[6] && dataBits[7])
            {
                // RAM memory management
                RAMR = (byte)result;
            }

            return true;
        }

        #endregion

        #region Serialization

        public void SyncState(Serializer ser)
        {
            ser.BeginSection("GateArray");
            ser.SyncEnum(nameof(ChipType), ref ChipType);
            ser.Sync(nameof(_PENR), ref _PENR);
            ser.Sync(nameof(_INKR), ref _INKR);
            ser.Sync(nameof(_RMR), ref _RMR);
            ser.Sync(nameof(_RAMR), ref _RAMR);
            ser.Sync(nameof(ColourRegisters), ref ColourRegisters, false);
            ser.Sync(nameof(CurrentPen), ref CurrentPen);
            ser.Sync(nameof(ClockCounter), ref ClockCounter);
            ser.Sync(nameof(FrameClock), ref FrameClock);
            ser.Sync(nameof(FrameEnd), ref FrameEnd);
            ser.Sync(nameof(WaitLine), ref WaitLine);
            ser.Sync(nameof(_interruptCounter), ref _interruptCounter);
            ser.Sync(nameof(VSYNCDelay), ref VSYNCDelay);
            ser.Sync(nameof(ScreenMode), ref ScreenMode);
            ser.Sync(nameof(HSYNC), ref HSYNC);
            //ser.Sync(nameof(HSYNC_falling), ref HSYNC_falling);
            ser.Sync(nameof(HSYNC_counter), ref HSYNC_counter);
            ser.Sync(nameof(VSYNC), ref VSYNC);
            ser.Sync(nameof(InterruptRaised), ref InterruptRaised);
            ser.Sync(nameof(InterruptHoldCounter), ref InterruptHoldCounter);
            ser.Sync(nameof(_MA), ref _MA);
            ser.Sync(nameof(IsNewFrame), ref IsNewFrame);
            ser.Sync(nameof(IsNewLine), ref IsNewLine);
            ser.Sync(nameof(HCC), ref HCC);
            ser.Sync(nameof(VLC), ref VLC);
            ser.Sync(nameof(VideoByte1), ref VideoByte1);
            ser.Sync(nameof(VideoByte2), ref VideoByte2);
            ser.Sync(nameof(NextVidRamLine), ref NextVidRamLine, false);
            ser.EndSection();
        }

        #endregion

        #region Enums, Classes & Lookups

        /// <summary>
        /// Represents a single scanline (in characters)
        /// </summary>
        public class CharacterLine
        {
            /// <summary>
            /// Screenmode is defined at each HSYNC (start of a new character line)
            /// Therefore we pass the mode in via constructor
            /// </summary>
            //public CharacterLine(int screenMode)
            //{
                //ScreenMode = screenMode;
            //}

            public int ScreenMode = 1;
            public List<Phase> Phases = new List<Phase>();
            public List<int> Pixels = new List<int>();

            /// <summary>
            /// Adds a new horizontal character to the list
            /// </summary>
            public void AddCharacter(Phase phase)
            {
                Phases.Add(phase);
            }

            public int PhaseCount
            {
                get { return Phases.Count(); }
            }

            public void Clear(int screenMode)
            {
                ScreenMode = screenMode;
                Phases.Clear();
                Pixels.Clear();
            }
        }

        [Flags]
        public enum Phase : int
        {
            /// <summary>
            /// Nothing
            /// </summary>
            NONE = 0,
            /// <summary>
            /// Border is being rendered
            /// </summary>
            BORDER = 1,
            /// <summary>
            /// Display rendered from video RAM
            /// </summary>
            DISPLAY = 2,
            /// <summary>
            /// HSYNC in progress
            /// </summary>
            HSYNC = 3,
            /// <summary>
            /// VSYNC in process
            /// </summary>
            VSYNC = 4,
            /// <summary>
            /// HSYNC occurs within a VSYNC
            /// </summary>
            HSYNCandVSYNC = 5
        }

        public enum GateArrayType
        {
            /// <summary>
            /// CPC 464
            /// The first version of the Gate Array is the 40007 and was released with the CPC 464
            /// </summary>
            Amstrad40007,
            /// <summary>
            /// CPC 664
            /// Later, the CPC 664 came out fitted with the 40008 version (and at the same time, the CPC 464 was also upgraded with this version). 
            /// This version is pinout incompatible with the 40007 (that's why the upgraded 464 of this period have two Gate Array slots on the motherboard, 
            /// one for a 40007 and one for a 40008)
            /// </summary>
            Amstrad40008,
            /// <summary>
            /// CPC 6128
            /// The CPC 6128 was released with the 40010 version (and the CPC 464 and 664 manufactured at that time were also upgraded to this version). 
            /// The 40010 is pinout compatible with the previous 40008
            /// </summary>
            Amstrad40010,
            /// <summary>
            /// Costdown CPC
            /// In the last serie of CPC 464 and 6128 produced by Amstrad in 1988, a small ASIC chip have been used to reduce the manufacturing costs. 
            /// This ASIC emulates the Gate Array, the PAL and the CRTC 6845. And no, there is no extra features like on the Amstrad Plus. 
            /// The only noticeable difference seems to be about the RGB output levels which are not exactly the same than those produced with a real Gate Array
            /// </summary>
            Amstrad40226,
            /// <summary>
            /// Plus & GX-4000
            /// All the Plus range is built upon a bigger ASIC chip which is integrating many features of the classic CPC (FDC, CRTC, PPI, Gate Array/PAL) and all 
            /// the new Plus specific features. The Gate Array on the Plus have a new register, named RMR2, to expand the ROM mapping functionnalities of the machine. 
            /// This register requires to be unlocked first to be available. And finally, the RGB levels produced by the ASIC on the Plus are noticeably differents
            /// </summary>
            Amstrad40489,
        }

        #endregion
    }
}
