/*
 * Maria.cs
 *
 * The Maria display device.
 * 
 * Derived from much of Dan Boris' work with 7800 emulation
 * within the MESS emulator.
 *
 * Thanks to Matthias Luedtke <matthias@atari8bit.de> for correcting
 * the BuildLineRAM320B() method to correspond closer to real hardware.
 * (Matthias credited an insightful response by Eckhard Stolberg on a forum on
 * Atari Age circa June 2005.)
 * 
 * Copyright © 2004-2012 Mike Murphy
 *
 */
using System;

namespace EMU7800.Core
{
    public sealed class Maria : IDevice
    {
        #region Constants

        const int
            INPTCTRL= 0x01, // Write: input port control (VBLANK in TIA)
            INPT0   = 0x08, // Read pot port: D7
            INPT1   = 0x09, // Read pot port: D7
            INPT2   = 0x0a, // Read pot port: D7
            INPT3   = 0x0b, // Read pot port: D7
            INPT4   = 0x0c, // Read P1 joystick trigger: D7
            INPT5   = 0x0d, // Read P2 joystick trigger: D7
            AUDC0   = 0x15, // Write: audio control 0 (D3-0)
            AUDC1   = 0x16, // Write: audio control 1 (D4-0)
            AUDF0   = 0x17, // Write: audio frequency 0 (D4-0)
            AUDF1   = 0x18, // Write: audio frequency 1 (D3-0)
            AUDV0   = 0x19, // Write: audio volume 0 (D3-0)
            AUDV1   = 0x1a, // Write: audio volume 1 (D3-0)

            BACKGRND= 0x20, // Background color
            P0C1    = 0x21, // Palette 0 - color 1
            P0C2    = 0x22, // Palette 0 - color 2
            P0C3    = 0x23, // Palette 0 - color 3
            WSYNC   = 0x24, // Wait for sync
            P1C1    = 0x25, // Palette 1 - color 1
            P1C2    = 0x26, // Palette 1 - color 2
            P1C3    = 0x27, // Palette 1 - color 3
            MSTAT   = 0x28, // Maria status
            P2C1    = 0x29, // Palette 2 - color 1
            P2C2    = 0x2a, // Palette 2 - color 2
            P2C3    = 0x2b, // Palette 2 - color 3
            DPPH    = 0x2c, // Display list list point high
            P3C1    = 0x2d, // Palette 3 - color 1
            P3C2    = 0x2e, // Palette 3 - color 2
            P3C3    = 0x2f, // Palette 3 - color 3
            DPPL    = 0x30, // Display list list point low
            P4C1    = 0x31, // Palette 4 - color 1
            P4C2    = 0x32, // Palette 4 - color 2
            P4C3    = 0x33, // Palette 4 - color 3
            CHARBASE= 0x34, // Character base address
            P5C1    = 0x35, // Palette 5 - color 1
            P5C2    = 0x36, // Palette 5 - color 2
            P5C3    = 0x37, // Palette 5 - color 3
            OFFSET  = 0x38, // Future expansion (store zero here)
            P6C1    = 0x39, // Palette 6 - color 1
            P6C2    = 0x3a, // Palette 6 - color 2
            P6C3    = 0x3b, // Palette 6 - color 3
            CTRL    = 0x3c, // Maria control register
            P7C1    = 0x3d, // Palette 7 - color 1
            P7C2    = 0x3e, // Palette 7 - color 2
            P7C3    = 0x3f; // Palette 7 - color 3

        const int CPU_TICKS_PER_AUDIO_SAMPLE = 57;

        #endregion

        #region Fields

        readonly byte[] LineRAM = new byte[0x200];
        readonly byte[] Registers = new byte[0x40];

        readonly Machine7800 M;
        readonly TIASound TIASound;

        ulong _startOfFrameCpuClock;
        int Scanline { get { return (int)(M.CPU.Clock - _startOfFrameCpuClock) / 114; } }
        int HPos { get { return (int)(M.CPU.Clock - _startOfFrameCpuClock) % 114; } }

        int FirstVisibleScanline, LastVisibleScanline;
        int _dmaClocks;
        bool _isPal;

        // For lightgun emulation.
        // Transient state, serialization unnecessary.
        ulong _lightgunFirstSampleCpuClock;
        int _lightgunFrameSamples, _lightgunSampledScanline, _lightgunSampledVisibleHpos;

        bool WM;
        ushort DLL;
        ushort DL;
        int Offset;
        int Holey;
        int Width;
        byte HPOS;
        int PaletteNo;
        bool INDMode;

        bool CtrlLock;

        // MARIA CNTL
        bool DMAEnabled;
        bool ColorKill;
        bool CWidth;
        bool BCntl;
        bool Kangaroo;
        byte RM;

        #endregion

        #region Public Members

        public void Reset()
        {
            CtrlLock = false;

            DMAEnabled = false;
            ColorKill = false;
            CWidth = false;
            BCntl = false;
            Kangaroo = false;
            RM = 0;

            TIASound.Reset();

            Log("{0} reset", this);
        }

        public byte this[ushort addr]
        {
            get { return peek(addr); }
            set { poke(addr, value); }
        }

        public override string ToString()
        {
            return GetType().Name;
        }

        public void StartFrame()
        {
            _startOfFrameCpuClock = M.CPU.Clock + (ulong)(M.CPU.RunClocks / M.CPU.RunClocksMultiple);
            _lightgunFirstSampleCpuClock = 0;

            AssertDebug(M.CPU.RunClocks <= 0 && (M.CPU.RunClocks % M.CPU.RunClocksMultiple) == 0);
            AssertDebug((_startOfFrameCpuClock % (114 * (ulong)M.FrameBuffer.Scanlines)) == 0);

            TIASound.StartFrame();
        }

        public int DoDMAProcessing()
        {
            OutputLineRAM();

            var sl = Scanline;

            if (!DMAEnabled || sl < FirstVisibleScanline || sl >= LastVisibleScanline)
                return 0;

            _dmaClocks = 0;

            if (DMAEnabled && sl == FirstVisibleScanline)
            {
                // DMA TIMING: End of VBLANK: DMA Startup + long shutdown
                _dmaClocks += 15;

                DLL = WORD(Registers[DPPL], Registers[DPPH]);

                ConsumeNextDLLEntry();
            }

            // DMA TIMING: DMA Startup, 5-9 cycles
            _dmaClocks += 5;

            BuildLineRAM();

            if (--Offset < 0)
            {
                ConsumeNextDLLEntry();

                // DMA TIMING: DMA Shutdown: Last line of zone, 10-13 cycles
                _dmaClocks += 10;
            }
            else
            {
                // DMA TIMING: DMA Shutdown: Other line of zone, 4-7 cycles
                _dmaClocks += 4;
            }

            return _dmaClocks;
        }

        public void EndFrame()
        {
            TIASound.EndFrame();
        }

        #endregion

        #region Constructors

        private Maria()
        {
        }

        public Maria(Machine7800 m, int scanlines)
        {
            if (m == null)
                throw new ArgumentNullException("m");

            M = m;
            InitializeVisibleScanlineValues(scanlines);
            TIASound = new TIASound(M, CPU_TICKS_PER_AUDIO_SAMPLE);
        }

        #endregion

        #region Scanline Builders

        void BuildLineRAM()
        {
            var dl = DL;

            // Iterate through Display List (DL)
            while (true)
            {
                var modeByte = DmaRead(dl + 1);
                if ((modeByte & 0x5f) == 0)
                    break;

                INDMode = false;
                ushort graphaddr;

                if ((modeByte & 0x1f) == 0)
                {
                    // Extended DL header
                    var dl0 = DmaRead(dl++); // low address
                    var dl1 = DmaRead(dl++); // mode
                    var dl2 = DmaRead(dl++); // high address
                    var dl3 = DmaRead(dl++); // palette(7-5)/width(4-0)
                    var dl4 = DmaRead(dl++); // horizontal position

                    graphaddr = WORD(dl0, dl2);
                    WM = (dl1 & 0x80) != 0;
                    INDMode = (dl1 & 0x20) != 0;
                    PaletteNo = (dl3 & 0xe0) >> 3;
                    Width = (~dl3 & 0x1f) + 1;
                    HPOS = dl4;

                    // DMA TIMING: DL 5 byte header
                    _dmaClocks += 10;
                }
                else
                {
                    // Normal DL header
                    var dl0 = DmaRead(dl++); // low address
                    var dl1 = DmaRead(dl++); // palette(7-5)/width(4-0)
                    var dl2 = DmaRead(dl++); // high address
                    var dl3 = DmaRead(dl++); // horizontal position

                    graphaddr = WORD(dl0, dl2);
                    PaletteNo = (dl1 & 0xe0) >> 3;
                    Width = (~dl1 & 0x1f) + 1;
                    HPOS = dl3;

                    // DMA TIMING: DL 4 byte header
                    _dmaClocks += 8;
                }

                // DMA TIMING: Graphic reads
                if (RM != 1)
                    _dmaClocks += (Width * (INDMode ? (CWidth ? 9 : 6) : 3));

                switch (RM)
                {
                    case 0:
                        if (WM) BuildLineRAM160B(graphaddr); else BuildLineRAM160A(graphaddr);
                        break;
                    case 1:
                        continue;
                    case 2:
                        if (WM) BuildLineRAM320B(graphaddr); else BuildLineRAM320D(graphaddr);
                        break;
                    case 3:
                        if (WM) BuildLineRAM320C(graphaddr); else BuildLineRAM320A(graphaddr);
                        break;
                }
            }
        }

        void BuildLineRAM160A(ushort graphaddr)
        {
            var indbytes = (INDMode && CWidth) ? 2 : 1;
            var hpos = HPOS << 1;
            var dataaddr = (ushort)(graphaddr + (Offset << 8));

            for (var i=0; i < Width; i++)
            {
                if (INDMode)
                {
                    dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset);
                }

                for (var j=0; j < indbytes; j++)
                {
                    if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800))
                    {
                        hpos += 8;
                        dataaddr++;
                        AssertDebug(!Kangaroo);
                        continue;
                    }

                    int d = DmaRead(dataaddr++);

                    var c = (d & 0xc0) >> 6;
                    if (c != 0)
                    {
                        var val = (byte)(PaletteNo | c);
                        LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val;
                    }
                    AssertDebug(c != 0 || c == 0 && !Kangaroo);

                    hpos += 2;

                    c = (d & 0x30) >> 4;
                    if (c != 0)
                    {
                        var val = (byte)(PaletteNo | c);
                        LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val;
                    }
                    AssertDebug(c != 0 || c == 0 && !Kangaroo);

                    hpos += 2;

                    c = (d & 0x0c) >> 2;
                    if (c != 0)
                    {
                        var val = (byte)(PaletteNo | c);
                        LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val;
                    }
                    AssertDebug(c != 0 || c == 0 && !Kangaroo);

                    hpos += 2;

                    c = d & 0x03;
                    if (c != 0)
                    {
                        var val = (byte)(PaletteNo | c);
                        LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val;
                    }
                    AssertDebug(c != 0 || c == 0 && !Kangaroo);

                    hpos += 2;
                }
            }
        }

        void BuildLineRAM160B(ushort graphaddr)
        {
            var indbytes = (INDMode && CWidth) ? 2 : 1;
            var hpos = HPOS << 1;
            var dataaddr = (ushort)(graphaddr + (Offset << 8));

            for (var i = 0; i < Width; i++)
            {
                if (INDMode)
                {
                    dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset);
                }

                for (var j=0; j < indbytes; j++)
                {
                    if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800))
                    {
                        hpos += 4;
                        dataaddr++;
                        continue;
                    }

                    int d = DmaRead(dataaddr++);

                    var c = (d & 0xc0) >> 6;
                    if (c != 0)
                    {
                        var p = ((PaletteNo >> 2) & 0x04) | ((d & 0x0c) >> 2);
                        var val = (byte)((p << 2) | c);
                        LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val;
                    }
                    else if (Kangaroo)
                    {
                        LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = 0;
                    }

                    hpos += 2;

                    c = (d & 0x30) >> 4;
                    if (c != 0)
                    {
                        var p = ((PaletteNo >> 2) & 0x04) | (d & 0x03);
                        var val = (byte)((p << 2) | c);
                        LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = val;
                    }
                    else if (Kangaroo)
                    {
                        LineRAM[hpos & 0x1ff] = LineRAM[(hpos+1) & 0x1ff] = 0;
                    }

                    hpos += 2;
                }
            }
        }

        void BuildLineRAM320A(ushort graphaddr)
        {
            var color = (byte)(PaletteNo | 2);
            var hpos = HPOS << 1;
            var dataaddr = (ushort)(graphaddr + (Offset << 8));

            AssertDebug(!CWidth);

            for (var i = 0; i < Width; i++)
            {
                if (INDMode)
                {
                    dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset);
                }

                if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800))
                {
                    hpos += 8;
                    dataaddr++;
                    continue;
                }

                int d = DmaRead(dataaddr++);

                if ((d & 0x80) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x40) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x20) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x10) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x08) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x04) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x02) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x01) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;
            }
        }

        void BuildLineRAM320B(ushort graphaddr)
        {
            var indbytes = (INDMode && CWidth) ? 2 : 1;
            var hpos = HPOS << 1;
            var dataaddr = (ushort)(graphaddr + (Offset << 8));

            for (var i = 0; i < Width; i++)
            {
                if (INDMode)
                {
                    dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset);
                }

                for (var j=0; j < indbytes; j++)
                {
                    if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800))
                    {
                        hpos += 4;
                        dataaddr++;
                        continue;
                    }

                    int d = DmaRead(dataaddr++);

                    var c = ((d & 0x80) >> 6) | ((d & 0x08) >> 3);
                    if (c != 0)
                    {
                        if ((d & 0xc0) != 0 || Kangaroo)
                            LineRAM[hpos & 0x1ff] = (byte)(PaletteNo | c);
                    }
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;
                    else if ((d & 0xcc) != 0)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x40) >> 5) | ((d & 0x04) >> 2);
                    if (c != 0)
                    {
                        if ((d & 0xc0) != 0 || Kangaroo)
                            LineRAM[hpos & 0x1ff] = (byte)(PaletteNo | c);
                    }
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;
                    else if ((d & 0xcc) != 0)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x20) >> 4) | ((d & 0x02) >> 1);
                    if (c != 0)
                    {
                        if ((d & 0x30) != 0 || Kangaroo)
                            LineRAM[hpos & 0x1ff] = (byte)(PaletteNo | c);
                    }
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;
                    else if ((d & 0x33) != 0)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x10) >> 3) | (d & 0x01);
                    if (c != 0)
                    {
                        if ((d & 0x30) != 0 || Kangaroo)
                            LineRAM[hpos & 0x1ff] = (byte)(PaletteNo | c);
                    }
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;
                    else if ((d & 0x33) != 0)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;
                }
            }
        }

        void BuildLineRAM320C(ushort graphaddr)
        {
            var hpos = HPOS << 1;
            var dataaddr = (ushort)(graphaddr + (Offset << 8));

            AssertDebug(!CWidth);

            for (var i = 0; i < Width; i++)
            {
                if (INDMode)
                {
                    dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset);
                }

                if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800))
                {
                    hpos += 4;
                    dataaddr++;
                    continue;
                }

                int d = DmaRead(dataaddr++);

                var color = (byte)(((((d & 0x0c) >> 2) | ((PaletteNo >> 2) & 0x04)) << 2) | 2);

                if ((d & 0x80) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x40) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                color = (byte)((((d & 0x03) | ((PaletteNo >> 2) & 0x04)) << 2) | 2);

                if ((d & 0x20) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;

                if ((d & 0x10) != 0)
                    LineRAM[hpos & 0x1ff] = color;
                else if (Kangaroo)
                    LineRAM[hpos & 0x1ff] = 0;

                hpos++;
            }
        }

        void BuildLineRAM320D(ushort graphaddr)
        {
            var indbytes = (INDMode && CWidth) ? 2 : 1;
            var hpos = HPOS << 1;
            var dataaddr = (ushort)(graphaddr + (Offset << 8));

            for (var i = 0; i < Width; i++)
            {
                if (INDMode)
                {
                    dataaddr = WORD(DmaRead(graphaddr + i), Registers[CHARBASE] + Offset);
                }

                for (var j=0; j < indbytes; j++)
                {
                    if (Holey == 0x02 && ((dataaddr & 0x9000) == 0x9000) || Holey == 0x01 && ((dataaddr & 0x8800) == 0x8800))
                    {
                        hpos += 8;
                        dataaddr++;
                        continue;
                    }

                    int d = DmaRead(dataaddr++);

                    var c = ((d & 0x80) >> 6) | (((PaletteNo >> 2) & 2) >> 1);
                    if (c != 0)
                        LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c);
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x40) >> 5) | ((PaletteNo >> 2) & 1);
                    if (c != 0)
                        LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c);
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x20) >> 4) | (((PaletteNo >> 2) & 2) >> 1);
                    if (c != 0)
                        LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c);
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x10) >> 3) | ((PaletteNo >> 2) & 1);
                    if (c != 0)
                        LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c);
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x08) >> 2) | (((PaletteNo >> 2) & 2) >> 1);
                    if (c != 0)
                        LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c);
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x04) >> 1) | ((PaletteNo >> 2) & 1);
                    if (c != 0)
                        LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c);
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = (d & 0x02) | (((PaletteNo >> 2) & 2) >> 1);
                    if (c != 0)
                        LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c);
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;

                    c = ((d & 0x01) << 1) | ((PaletteNo >> 2) & 1);
                    if (c != 0)
                        LineRAM[hpos & 0x1ff] = (byte)((PaletteNo & 0x10) | c);
                    else if (Kangaroo)
                        LineRAM[hpos & 0x1ff] = 0;

                    hpos++;
                }
            }
        }

        void OutputLineRAM()
        {
            var fbi = ((Scanline + 1) * M.FrameBuffer.VisiblePitch) % M.FrameBuffer.VideoBufferByteLength;

			for (int i = 0; i < M.FrameBuffer.VisiblePitch; i++)
            {
				var colorIndex = LineRAM[i];
				M.FrameBuffer.VideoBuffer[fbi++] = Registers[BACKGRND + ((colorIndex & 3) == 0 ? 0 : colorIndex)];
				if (fbi == M.FrameBuffer.VideoBufferByteLength)
					fbi = 0;
            }

            for (var i = 0; i < LineRAM.Length; i++)
            {
                LineRAM[i] = 0;
            }
        }

        #endregion

        #region Maria Peek

        byte peek(ushort addr)
        {
            addr &= 0x3f;
            var mi = M.InputState;

            switch(addr)
            {
                case MSTAT:
                    var sl = Scanline;
                    return (sl < FirstVisibleScanline || sl >= LastVisibleScanline)
                         ? (byte)0x80  // VBLANK ON
                         : (byte)0;    // VBLANK OFF
                case INPT0: return mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger)  ? (byte)0x80 : (byte)0;     // player1,button R
                case INPT1: return mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger2) ? (byte)0x80 : (byte)0;     // player1,button L
                case INPT2: return mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger)  ? (byte)0x80 : (byte)0;     // player2,button R
                case INPT3: return mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger2) ? (byte)0x80 : (byte)0;     // player2,button L
                case INPT4: return SampleINPTLatched(4)                                                 ? (byte)0    : (byte)0x80;  // player1,button L/R
                case INPT5: return SampleINPTLatched(5)                                                 ? (byte)0    : (byte)0x80;  // player2,button L/R
                default:
                    LogDebug("Maria: Unhandled peek at ${0:x4}, PC=${1:x4}", addr, M.CPU.PC);
                    var retval = Registers[addr];
                    return retval;
            }
        }

        #endregion

        #region Maria Poke

        void poke(ushort addr, byte data)
        {
            addr &= 0x3f;

            switch (addr)
            {
                // INPUT PORT CONTROL
                // Only the first four bits of INPTCTRL are used:
                //     D0: lock mode (after this bit has been set high, no more mode changes can be done until the console is turned off)
                //     D1: 0=disable MARIA (only RIOT RAM is available); 1=enable MARIA (also enables system RAM)
                //     D2: 0=enable BIOS at $8000-$FFFF (actually NTSC only uses 4KB and PAL uses 16KB); 1=disable BIOS and enable cartridge
                //     D3: 0=disable TIA video pull-ups (video output is MARIA instead of TIA); 1=enable TIA video pull-ups (video output is TIA instead of MARIA)
                //
                case INPTCTRL:
                    if (CtrlLock)
                    {
                        Log("Maria: INPTCTRL: LOCKED: Ignoring: ${0:x2}, PC=${1:x4}", data, M.CPU.PC);
                        break;
                    }

                    CtrlLock        = (data & (1 << 0)) != 0;
                    var mariaEnable = (data & (1 << 1)) != 0;
                    var biosDisable = (data & (1 << 2)) != 0;
                    var tiaopEnable = (data & (1 << 3)) != 0;

                    Log("Maria: INPTCTRL: ${0:x2}, PC=${1:x4}, lockMode={2}, mariaEnable={3} biosDisable={4} tiaOutput={5}",
                        data, M.CPU.PC, CtrlLock, mariaEnable, biosDisable, tiaopEnable);

                    if (biosDisable)
                    {
                        M.SwapOutBIOS();
                    }
                    else
                    {
                        M.SwapInBIOS();
                    }
                    break;
                case WSYNC:
                    // Request a CPU preemption to service the delay request
                    M.CPU.EmulatorPreemptRequest = true;
                    break;
                case CTRL:
                    ColorKill = (data & 0x80) != 0;
                    DMAEnabled = (data & 0x60) == 0x40;
                    CWidth = (data & 0x10) != 0;
                    BCntl = (data & 0x08) != 0;
                    Kangaroo = (data & 0x04) != 0;
                    RM = (byte)(data & 0x03);
                    break;
                case MSTAT:
                    break;
                case CHARBASE:
                case DPPH:
                case DPPL:
                    Registers[addr] = data;
                    break;
                case BACKGRND:
                case P0C1:
                case P0C2:
                case P0C3:
                case P1C1:
                case P1C2:
                case P1C3:
                case P2C1:
                case P2C2:
                case P2C3:
                case P3C1:
                case P3C2:
                case P3C3:
                case P4C1:
                case P4C2:
                case P4C3:
                case P5C1:
                case P5C2:
                case P5C3:
                case P6C1:
                case P6C2:
                case P6C3:
                case P7C1:
                case P7C2:
                case P7C3:
                    Registers[addr] = data;
                    break;
                case AUDC0:
                case AUDC1:
                case AUDF0:
                case AUDF1:
                case AUDV0:
                case AUDV1:
                    TIASound.Update(addr, data);
                    break;
                case OFFSET:
                    Log("Maria: OFFSET: ROM wrote ${0:x2}, PC=${1:x4} (reserved for future expansion)", data, M.CPU.PC);
                    break;
                default:
                    Registers[addr] = data;
                    LogDebug("Maria: Unhandled poke:${0:x4} w/${1:x2}, PC=${2:x4}", addr, data, M.CPU.PC);
                    break;
            }
        }

        #endregion

        #region Input Helpers

        bool SampleINPTLatched(int inpt)
        {
            var mi = M.InputState;
            var playerNo = inpt - 4;

            switch (playerNo == 0 ? mi.LeftControllerJack : mi.RightControllerJack)
            {
                case Controller.Joystick:
                    return mi.SampleCapturedControllerActionState(playerNo, ControllerAction.Trigger);
                case Controller.ProLineJoystick:
                    var portbline = 4 << (playerNo << 1);
                    if ((M.PIA.DDRB & portbline) != 0 && (M.PIA.WrittenPortB & portbline) == 0)
                        return false;
                    return mi.SampleCapturedControllerActionState(playerNo, ControllerAction.Trigger)
                        || mi.SampleCapturedControllerActionState(playerNo, ControllerAction.Trigger2);
                case Controller.Lightgun:

                    // This is one area where always running fixed at the faster CPU frequency creates emulation challenges.
                    // Fortunately since lightgun sampling is a dedicated activity on a frame, the job of compensating is tractable.

                    // Track the number of samples this frame, the time of the first sample, and capture the lightgun location.
                    if (_lightgunFirstSampleCpuClock == 0)
                    {
                        _lightgunFirstSampleCpuClock = M.CPU.Clock;
                        _lightgunFrameSamples = 0;
                        mi.SampleCapturedLightGunPosition(playerNo, out _lightgunSampledScanline, out _lightgunSampledVisibleHpos);
                    }
                    _lightgunFrameSamples++;

                    // Magic Adjustment Factor
                    // Seems sufficient to account for the timing impact of successive lightrun reads (i.e., 'slow' memory accesses.)
                    // Obtained through through trial-and-error.
                    const float magicAdjustmentFactor = 2.135f;

                    var firstLightgunSampleMariaFrameClock = (int)((_lightgunFirstSampleCpuClock - _startOfFrameCpuClock) << 2);
                    var mariaClocksSinceFirstLightgunSample = (int)((M.CPU.Clock - _lightgunFirstSampleCpuClock) << 2);
                    var adjustmentMariaClocks = (int)Math.Round(_lightgunFrameSamples * magicAdjustmentFactor);
                    var actualMariaFrameClock = firstLightgunSampleMariaFrameClock + mariaClocksSinceFirstLightgunSample + adjustmentMariaClocks;
                    var actualScanline = actualMariaFrameClock / 456;
                    var actualHpos = actualMariaFrameClock % 456;

                    // Lightgun sampling looks intended to begin at the start of the scanline.
                    // Compensate with another magic constant since we're always off by a fixed amount.
                    actualHpos -= 62;
                    if (actualHpos < 0)
                    {
                        actualHpos += 456;
                        actualScanline--;
                    }

                    var sampledScanline = _lightgunSampledScanline;
                    var sampledVisibleHpos = _lightgunSampledVisibleHpos;

                    // Seems reasonable the gun sees more than a single pixel (more like a circle or oval) and triggers sooner accordingly.
                    // These adjustments were obtained through trial-and-error.
                    if (_isPal)
                    {
                        sampledScanline -= 19;
                    }
                    else
                    {
                        sampledScanline -= 16;
                        sampledVisibleHpos += 4;
                    }
                    return (actualScanline >= sampledScanline) && (actualHpos >= (sampledVisibleHpos + 136 /* HBLANK clocks */));
            }
            return false;
        }

        #endregion

        #region Serialization Members

        public Maria(DeserializationContext input, Machine7800 m, int scanlines)
        {
            if (input == null)
                throw new ArgumentNullException("input");
            if (m == null)
                throw new ArgumentNullException("m");

            M = m;
            InitializeVisibleScanlineValues(scanlines);
            TIASound = new TIASound(input, M, CPU_TICKS_PER_AUDIO_SAMPLE);

            var version = input.CheckVersion(1, 2);
            LineRAM = input.ReadExpectedBytes(512);
            if (version == 1)
            {
                // formerly persisted values, MariaPalette[8,4]
                for (var i = 0; i < 32; i++) 
                    input.ReadByte();
            }
            Registers = input.ReadExpectedBytes(0x40);
            if (version == 1)
            {
                // formerly persisted value, Scanline
                input.ReadInt32();
            }
            switch (version)
            {
                case 1:
                    WM = (input.ReadByte() != 0);
                    break;
                case 2:
                    WM = input.ReadBoolean();
                    break;
            }
            DLL = input.ReadUInt16();
            DL = input.ReadUInt16();
            Offset = input.ReadInt32();
            Holey = input.ReadInt32();
            Width = input.ReadInt32();
            HPOS = input.ReadByte();
            PaletteNo = input.ReadByte();
            INDMode = input.ReadBoolean();
            if (version == 1)
            {
                // formerly persisted value (DLI)
                input.ReadBoolean();
            }
            CtrlLock = input.ReadBoolean();
            if (version == 1)
            {
                // formerly persisted value (VBLANK)
                input.ReadByte();
            }
            DMAEnabled = input.ReadBoolean();
            if (version == 1)
            {
                // formerly persisted value (DMAOn)
                input.ReadBoolean();
            }
            ColorKill = input.ReadBoolean();
            CWidth = input.ReadBoolean();
            BCntl = input.ReadBoolean();
            Kangaroo = input.ReadBoolean();
            RM = input.ReadByte();
        }

        public void GetObjectData(SerializationContext output)
        {
            if (output == null)
                throw new ArgumentNullException("output");

            output.Write(TIASound);

            output.WriteVersion(2);
            output.Write(LineRAM);
            output.Write(Registers);
            output.Write(WM);
            output.Write(DLL);
            output.Write(DL);
            output.Write(Offset);
            output.Write(Holey);
            output.Write(Width);
            output.Write(HPOS);
            output.Write((byte)PaletteNo);
            output.Write(INDMode);
            output.Write(CtrlLock);
            output.Write(DMAEnabled);
            output.Write(ColorKill);
            output.Write(CWidth);
            output.Write(BCntl);
            output.Write(Kangaroo);
            output.Write(RM);
        }

        #endregion

        #region Helpers

        void ConsumeNextDLLEntry()
        {
            // Display List List (DLL) entry
            var dll0 = DmaRead(DLL++);   // DLI, Holey, Offset
            var dll1 = DmaRead(DLL++);   // High DL address
            var dll2 = DmaRead(DLL++);   // Low DL address

            var dli = (dll0 & 0x80) != 0;
            Holey = (dll0 & 0x60) >> 5;
            Offset = dll0 & 0x0f;

            // Update current Display List (DL)
            DL = WORD(dll2, dll1);

            if (dli)
            {
                M.CPU.NMIInterruptRequest = true;

                // DMA TIMING: One tick between DMA Shutdown and DLI
                _dmaClocks += 1;
            }
        }

        void InitializeVisibleScanlineValues(int scanlines)
        {
            switch (scanlines)
            {
                case 262: // NTSC
                    FirstVisibleScanline = 11;
                    LastVisibleScanline = FirstVisibleScanline + 242;
                    _isPal = false;
                    break;
                case 312: // PAL
                    FirstVisibleScanline = 11;
                    LastVisibleScanline = FirstVisibleScanline + 292;
                    _isPal = true;
                    break;
                default:
                    throw new ArgumentException("scanlines must be 262 or 312.");
            }
        }

        void Log(string format, params object[] args)
        {
            if (M == null || M.Logger == null)
                return;
            M.Logger.WriteLine(format, args);
        }

        // convenience overload
        static ushort WORD(int lsb, int msb)
        {
            return WORD((byte)lsb, (byte)msb);
        }

        static ushort WORD(byte lsb, byte msb)
        {
            return (ushort)(lsb | msb << 8);
        }

        // convenience overload
        byte DmaRead(int addr)
        {
            return DmaRead((ushort)addr);
        }

        byte DmaRead(ushort addr)
        {
#if DEBUG
            if (addr < 0x1800)
                LogDebug("Maria: Questionable DMA read at ${0:x4} by PC=${1:x4}", addr, M.CPU.PC);
#endif
            return M.Mem[addr];
        }

        [System.Diagnostics.Conditional("DEBUG")]
        void LogDebug(string format, params object[] args)
        {
            if (M == null || M.Logger == null)
                return;
            M.Logger.WriteLine(format, args);
        }

        [System.Diagnostics.Conditional("DEBUG")]
        void AssertDebug(bool cond)
        {
            if (!cond)
                System.Diagnostics.Debugger.Break();
        }

        #endregion
    }
}
