/*
 * TIA.cs
 *
 * The Television Interface Adaptor device.
 *
 * Copyright © 2003-2008 Mike Murphy
 *
 */
using System;

namespace EMU7800.Core
{
    #region Collision Flags

    [Flags]
    public enum TIACxFlags
    {
        PF = 1 << 0,
        BL = 1 << 1,
        M0 = 1 << 2,
        M1 = 1 << 3,
        P0 = 1 << 4,
        P1 = 1 << 5
    };

    [Flags]
    public enum TIACxPairFlags
    {
        M0P1 = 1 << 0,
        M0P0 = 1 << 1,
        M1P0 = 1 << 2,
        M1P1 = 1 << 3,
        P0PF = 1 << 4,
        P0BL = 1 << 5,
        P1PF = 1 << 6,
        P1BL = 1 << 7,
        M0PF = 1 << 8,
        M0BL = 1 << 9,
        M1PF = 1 << 10,
        M1BL = 1 << 11,
        BLPF = 1 << 12,
        P0P1 = 1 << 13,
        M0M1 = 1 << 14
    };

    #endregion

    public sealed class TIA : IDevice
    {
        #region Constants

        const int
            VSYNC   = 0x00, // Write: vertical sync set-clear (D1)
            VBLANK  = 0x01, // Write: vertical blank set-clear (D7-6,D1)
            WSYNC   = 0x02, // Write: wait for leading edge of hrz. blank (strobe)
            RSYNC   = 0x03, // Write: reset hrz. sync counter (strobe)
            NUSIZ0  = 0x04, // Write: number-size player-missle 0 (D5-0)
            NUSIZ1  = 0x05, // Write: number-size player-missle 1 (D5-0)
            COLUP0  = 0x06, // Write: color-lum player 0 (D7-1)
            COLUP1  = 0x07, // Write: color-lum player 1 (D7-1)
            COLUPF  = 0x08, // Write: color-lum playfield (D7-1)
            COLUBK  = 0x09, // Write: color-lum background (D7-1)
            CTRLPF  = 0x0a, // Write: cntrl playfield ballsize & coll. (D5-4,D2-0)
            REFP0   = 0x0b, // Write: reflect player 0 (D3)
            REFP1   = 0x0c, // Write: reflect player 1 (D3)
            PF0     = 0x0d, // Write: playfield register byte 0 (D7-4)
            PF1     = 0x0e, // Write: playfield register byte 1 (D7-0)
            PF2     = 0x0f, // Write: playfield register byte 2 (D7-0)
            RESP0   = 0x10, // Write: reset player 0 (strobe)
            RESP1   = 0x11, // Write: reset player 1 (strobe)
            RESM0   = 0x12, // Write: reset missle 0 (strobe)
            RESM1   = 0x13, // Write: reset missle 1 (strobe)
            RESBL   = 0x14, // Write: reset ball (strobe)
            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)
            GRP0    = 0x1b, // Write: graphics player 0 (D7-0)
            GRP1    = 0x1c, // Write: graphics player 1 (D7-0)
            ENAM0   = 0x1d, // Write: graphics (enable) missle 0 (D1)
            ENAM1   = 0x1e, // Write: graphics (enable) missle 1 (D1)
            ENABL   = 0x1f, // Write: graphics (enable) ball (D1)
            HMP0    = 0x20, // Write: horizontal motion player 0 (D7-4)
            HMP1    = 0x21, // Write: horizontal motion player 1 (D7-4)
            HMM0    = 0x22, // Write: horizontal motion missle 0 (D7-4)
            HMM1    = 0x23, // Write: horizontal motion missle 1 (D7-4)
            HMBL    = 0x24, // Write: horizontal motion ball (D7-4)
            VDELP0  = 0x25, // Write: vertical delay player 0 (D0)
            VDELP1  = 0x26, // Write: vertical delay player 1 (D0)
            VDELBL  = 0x27, // Write: vertical delay ball (D0)
            RESMP0  = 0x28, // Write: reset missle 0 to player 0 (D1)
            RESMP1  = 0x29, // Write: reset missle 1 to player 1 (D1)
            HMOVE   = 0x2a, // Write: apply horizontal motion (strobe)
            HMCLR   = 0x2b, // Write: clear horizontal motion registers (strobe)
            CXCLR   = 0x2c, // Write: clear collision latches (strobe)

            CXM0P   = 0x00, // Read collision: D7=(M0,P1); D6=(M0,P0)
            CXM1P   = 0x01, // Read collision: D7=(M1,P0); D6=(M1,P1)
            CXP0FB  = 0x02, // Read collision: D7=(P0,PF); D6=(P0,BL)
            CXP1FB  = 0x03, // Read collision: D7=(P1,PF); D6=(P1,BL)
            CXM0FB  = 0x04, // Read collision: D7=(M0,PF); D6=(M0,BL)
            CXM1FB  = 0x05, // Read collision: D7=(M1,PF); D6=(M1,BL)
            CXBLPF  = 0x06, // Read collision: D7=(BL,PF); D6=(unused)
            CXPPMM  = 0x07, // Read collision: D7=(P0,P1); D6=(M0,M1)
            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

        const int CPU_TICKS_PER_AUDIO_SAMPLE = 38;

        #endregion

        #region Data Structures

        readonly byte[] RegW = new byte[0x40];
        readonly MachineBase M;
        readonly TIASound TIASound;

        delegate void PokeOpTyp(ushort addr, byte data);

        PokeOpTyp[] PokeOp;

        #endregion

        #region Position Counters

        // backing fields for properties--dont reference directly
        int _HSync, _P0, _P1, _M0, _M1, _BL, _HMoveCounter;

        // Horizontal Sync Counter
        // this represents HSync of the last rendered CLK
        // has period of 57 counts, 0-56, at 1/4 CLK (57*4=228 CLK)
        // provide all horizontal timing for constructing a valid TV signal
        // other movable object counters can be reset out-of-phase with HSync, hence %228 and not %57
        int HSync
        {
            get { return _HSync; }
            set { _HSync = value % 228; }
        }

        // determines the difference between HSync and PokeOpHSync
        int PokeOpHSyncDelta
        {
            get { return (int)(Clock - LastEndClock); }
        }

        // this represents the current HSync
        int PokeOpHSync
        {
            get { return (HSync + PokeOpHSyncDelta) % 228; }
        }

        // scanline last rendered to
        int ScanLine;

        // current position in the frame buffer
        int FrameBufferIndex;

        // bytes are batched here for writing to the FrameBuffer
        BufferElement FrameBufferElement;

        // signals when to start an HMOVE
        ulong StartHMOVEClock;

        // indicates where in the HMOVE operation it is
        int HMoveCounter
        {
            get { return _HMoveCounter; }
            set { _HMoveCounter = value < 0 ? -1 : value & 0xf; }
        }

        // true when there is an HMOVE executing on the current scanline
        bool HMoveLatch;

        // represents the TIA color clock (CLK)
        // computed off of the CPU clock, but in real life, the CPU is driven by the color CLK signal
        ulong Clock
        {
            get { return 3 * M.CPU.Clock; }
        }

        // represents the first CLK of the unrendered scanline segment
        ulong StartClock;

        // represents the last CLK of the previously rendered scanline segment
        ulong LastEndClock
        {
            get { return StartClock - 1; }
        }

        #endregion

        #region Player 0 Object

        // Player 0 Horizontal Position Counter
        // has period of 40 counts, 0-39, at 1/4 CLK (40*4=160 CLK=visible scanline length)
        // player position counter controls the position of the respective player graphics object on each scanline
        // can be reset out-of-phase with HSync, hence %160 and not %40
        int P0
        {
            get { return _P0; }
            set { _P0 = value % 160; }
        }

        // HMOVE "more motion required" latch
        bool P0mmr;

        // Player 0 graphics registers
        byte EffGRP0, OldGRP0;

        // Player 0 stretch mode
        int P0type;

        // 1=currently suppressing copy 1 on Player 0
        int P0suppress;

        #endregion

        #region Player 1 Object

        // Player 1 Horizontal Position Counter (identical to P0)
        int P1
        {
            get { return _P1; }
            set { _P1 = value % 160; }
        }

        // HMOVE "more motion required" latch
        bool P1mmr;

        // Player 1 graphics registers
        byte EffGRP1, OldGRP1;

        // Player 1 stretch mode
        int P1type;

        // 1=currently suppressing copy 1 on Player 1
        int P1suppress;

        #endregion

        #region Missile 0 Object

        // Missile 0 Horizontal Position Counter
        // similar to player position counters
        int M0
        {
            get { return _M0; }
            set { _M0 = value % 160; }
        }

        // HMOVE "more motion required" latch
        bool M0mmr;

        int M0type, M0size;

        bool m0on;

        #endregion

        #region Missle 1 Object

        // Missile 1 Horizontal Position Counter (identical to M0)
        int M1
        {
            get { return _M1; }
            set { _M1 = value % 160; }
        }

        // HMOVE "more motion required" latch
        bool M1mmr;

        int M1type, M1size;

        bool m1on;

        #endregion

        #region Ball Object

        // Ball Horizontal Position Counter
        // similar to player position counters
        int BL
        {
            get { return _BL; }
            set { _BL = value % 160; }
        }

        // HMOVE "more motion required" latch
        bool BLmmr;

        bool OldENABL;

        int BLsize;

        bool blon;

        #endregion

        #region Misc

        uint PF210;
        int PFReflectionState;

        // color-luminance for background, playfield, player 0, player 1
        byte colubk, colupf, colup0, colup1;

        bool vblankon, scoreon, pfpriority;

        bool DumpEnabled;
        ulong DumpDisabledCycle;

        TIACxPairFlags Collisions;

        #endregion

        #region Public Members

        public int WSYNCDelayClocks { get; set; }
        public bool EndOfFrame { get; private set; }

        public void Reset()
        {
            for (var i = 0; i < RegW.Length; i++)
            {
                RegW[i] = 0;
            }
            vblankon = scoreon = pfpriority = false;
            m0on = m1on = blon = false;
            colubk = colupf = colup0 = colup1 = 0;
            PFReflectionState = 0;

            StartClock = Clock;
            HSync = -1;
            P0 = P1 = M0 = M1 = BL = -1;
            P0mmr = P1mmr = M0mmr = M1mmr = BLmmr = false;
            StartHMOVEClock = ulong.MaxValue;
            HMoveCounter = -1;

            FrameBufferIndex = 0;

            TIASound.Reset();

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

        public override String ToString()
        {
            return "TIA 1A";
        }

        public void StartFrame()
        {
            WSYNCDelayClocks = 0;
            EndOfFrame = false;
            ScanLine = 0;
            FrameBufferIndex %= 160;
            RenderFromStartClockTo(Clock);
            TIASound.StartFrame();
        }

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

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

        #endregion

        #region Constructors

        private TIA()
        {
            BuildPokeOpTable();
        }

        public TIA(MachineBase m) : this()
        {
            if (m == null)
                throw new ArgumentNullException("m");

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

        #endregion

        #region Scanline Segment Renderer

        void RenderFromStartClockTo(ulong endClock)
        {

        RenderClock:
            if (StartClock >= endClock)
                return;

            ++HSync;

            if (StartClock == StartHMOVEClock)
            {
                // turn on HMOVE
                HMoveLatch = true;
                HMoveCounter = 0xf;
                P0mmr = P1mmr = M0mmr = M1mmr = BLmmr = true;
            }
            else if (HSync == 0)
            {
                // just wrapped around, clear late HBLANK
                HMoveLatch = false;
            }

            // position counters are incremented during the visible portion of the scanline
            if (HSync >= 68 + (HMoveLatch ? 8 : 0))
            {
                ++P0; ++P1; ++M0; ++M1; ++BL;
            }

            // HMOVE compare, once every 1/4 CLK when on
            if (HMoveCounter >= 0 && (HSync & 3) == 0)
            {
                if (((HMoveCounter ^ RegW[HMP0]) & 0xf) == 0xf) P0mmr = false;
                if (((HMoveCounter ^ RegW[HMP1]) & 0xf) == 0xf) P1mmr = false;
                if (((HMoveCounter ^ RegW[HMM0]) & 0xf) == 0xf) M0mmr = false;
                if (((HMoveCounter ^ RegW[HMM1]) & 0xf) == 0xf) M1mmr = false;
                if (((HMoveCounter ^ RegW[HMBL]) & 0xf) == 0xf) BLmmr = false;
                if (HMoveCounter >= 0) HMoveCounter--;
            }

            // HMOVE increment, once every 1/4 CLK, 2 CLK after first compare when on
            if (HMoveCounter < 0xf && (HSync & 3) == 2)
            {
                if (HSync < 68 + (HMoveLatch ? 8 : 0))
                {
                    if (P0mmr) ++P0;
                    if (P1mmr) ++P1;
                    if (M0mmr) ++M0;
                    if (M1mmr) ++M1;
                    if (BLmmr) ++BL;
                }
            }

            if (HSync == 68 + 76) PFReflectionState = RegW[CTRLPF] & 0x01;

            var fbyte = (byte)0;
            var fbyte_colupf = colupf;
            TIACxFlags cxflags = 0;

            if (vblankon || HSync < 68 + (HMoveLatch ? 8 : 0)) goto WritePixel;

            fbyte = colubk;

            var colupfon = false;
            if ((PF210 & TIATables.PFMask[PFReflectionState][HSync - 68]) != 0)
            {
                if (scoreon) fbyte_colupf = (HSync - 68) < 80 ? colup0 : colup1;
                colupfon = true;
                cxflags |= TIACxFlags.PF;
            }
            if (blon && BL >= 0 && TIATables.BLMask[BLsize][BL])
            {
                colupfon = true;
                cxflags |= TIACxFlags.BL;
            }
            if (!pfpriority && colupfon)
            {
                fbyte = fbyte_colupf;
            }
            if (m1on && M1 >= 0 && TIATables.MxMask[M1size][M1type][M1])
            {
                fbyte = colup1;
                cxflags |= TIACxFlags.M1;
            }
            if (P1 >= 0 && (TIATables.PxMask[P1suppress][P1type][P1] & EffGRP1) != 0)
            {
                fbyte = colup1;
                cxflags |= TIACxFlags.P1;
            }
            if (m0on && M0 >= 0 && TIATables.MxMask[M0size][M0type][M0])
            {
                fbyte = colup0;
                cxflags |= TIACxFlags.M0;
            }
            if (P0 >= 0 && (TIATables.PxMask[P0suppress][P0type][P0] & EffGRP0) != 0)
            {
                fbyte = colup0;
                cxflags |= TIACxFlags.P0;
            } 
            if (pfpriority && colupfon)
            {
                fbyte = fbyte_colupf;
            }

        WritePixel:
            Collisions |= TIATables.CollisionMask[(int)cxflags];

            if (HSync >= 68)
            {
                var i = FrameBufferIndex++;
                FrameBufferElement[i] = fbyte;
                if ((i & BufferElement.MASK) == BufferElement.MASK)
                {
                    M.FrameBuffer.VideoBuffer[i >> BufferElement.SHIFT] = FrameBufferElement;
                    if (FrameBufferIndex == M.FrameBuffer.VideoBufferByteLength)
                        FrameBufferIndex = 0;
                }
                if (HSync == 227)
                    ScanLine++;
            }

            if (P0 >= 156) P0suppress = 0;
            if (P1 >= 156) P1suppress = 0;

            // denote this CLK has been completed by incrementing to the next
            StartClock++;

            goto RenderClock;
        }

        #endregion

        #region TIA Peek

        byte peek(ushort addr)
        {
            var retval = 0;
            addr &= 0xf;

            RenderFromStartClockTo(Clock);

            switch (addr)
            {
                case CXM0P:
                    retval |= ((Collisions & TIACxPairFlags.M0P1) != 0 ? 0x80 : 0);
                    retval |= ((Collisions & TIACxPairFlags.M0P0) != 0 ? 0x40 : 0);
                    break;
                case CXM1P:
                    retval |= ((Collisions & TIACxPairFlags.M1P0) != 0 ? 0x80 : 0);
                    retval |= ((Collisions & TIACxPairFlags.M1P1) != 0 ? 0x40 : 0);
                    break;
                case CXP0FB:
                    retval |= ((Collisions & TIACxPairFlags.P0PF) != 0 ? 0x80 : 0);
                    retval |= ((Collisions & TIACxPairFlags.P0BL) != 0 ? 0x40 : 0);
                    break;
                case CXP1FB:
                    retval |= ((Collisions & TIACxPairFlags.P1PF) != 0 ? 0x80 : 0);
                    retval |= ((Collisions & TIACxPairFlags.P1BL) != 0 ? 0x40 : 0);
                    break;
                case CXM0FB:
                    retval |= ((Collisions & TIACxPairFlags.M0PF) != 0 ? 0x80 : 0);
                    retval |= ((Collisions & TIACxPairFlags.M0BL) != 0 ? 0x40 : 0);
                    break;
                case CXM1FB:
                    retval |= ((Collisions & TIACxPairFlags.M1PF) != 0 ? 0x80 : 0);
                    retval |= ((Collisions & TIACxPairFlags.M1BL) != 0 ? 0x40 : 0);
                    break;
                case CXBLPF:
                    retval |= ((Collisions & TIACxPairFlags.BLPF) != 0 ? 0x80 : 0);
                    break;
                case CXPPMM:
                    retval |= ((Collisions & TIACxPairFlags.P0P1) != 0 ? 0x80 : 0);
                    retval |= ((Collisions & TIACxPairFlags.M0M1) != 0 ? 0x40 : 0);
                    break;
                case INPT0:
                    retval = DumpedInputPort(SampleINPT(0));
                    break;
                case INPT1:
                    retval = DumpedInputPort(SampleINPT(1));
                    break;
                case INPT2:
                    retval = DumpedInputPort(SampleINPT(2));
                    break;
                case INPT3:
                    retval = DumpedInputPort(SampleINPT(3));
                    break;
                case INPT4:
                    var scanline = ScanLine;
                    var hpos = PokeOpHSync - 68;
                    if (hpos < 0)
                    {
                        hpos += 228;
                        scanline--;
                    }
                    if (SampleINPTLatched(4, scanline, hpos))
                    {
                        retval &= 0x7f;
                    }
                    else
                    {
                        retval |= 0x80;
                    }
                    break;
                case INPT5:
                    scanline = ScanLine;
                    hpos = PokeOpHSync - 68;
                    if (hpos < 0)
                    {
                        hpos += 228;
                        scanline--;
                    }
                    if (SampleINPTLatched(5, scanline, hpos))
                    {
                        retval &= 0x7f;
                    }
                    else
                    {
                        retval |= 0x80;
                    }
                    break;
            }
            return (byte)(retval | (M.Mem.DataBusState & 0x3f));
        }

        byte DumpedInputPort(int resistance)
        {
            byte retval = 0;

            if (resistance == 0)
            {
                retval = 0x80;
            }
            else if (DumpEnabled || resistance == Int32.MaxValue)
            {
                retval = 0x00;
            }
            else
            {
                var chargeTime = 1.6 * resistance * 0.01e-6;
                var needed = (ulong)(chargeTime * M.FrameBuffer.Scanlines * 228 * M.FrameHZ / 3);
                if (M.CPU.Clock > DumpDisabledCycle + needed)
                {
                    retval = 0x80;
                }
            }
            return retval;
        }

        #endregion

        #region TIA Poke

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

            var endClock = Clock;

            // writes to the TIA may take a few extra CLKs to actually affect TIA state
            // such a delay would seem to be applicable across all possible TIA writes
            // without hardware to confirm conclusively, this is updated only as seemingly required
            switch (addr)
            {
                case GRP0:
                case GRP1:
                    // stem in the T in activision logo on older titles
                    endClock += 1;
                    break;
                case PF0:
                case PF1:
                case PF2:
                    // +2 prevents minor notching in berzerk walls
                    // +4 prevents shield fragments floating in fuzz field in yars revenge,
                    //    but creates minor artifact in chopper command
                    switch (PokeOpHSync & 3)
                    {
                        case 0: endClock += 4; break;
                        case 1: endClock += 3; break;
                        case 2: endClock += 2; break;
                        case 3: endClock += 5; break;
                    }
                    break;
            }

            RenderFromStartClockTo(endClock);

            PokeOp[addr](addr, data);
        }

        static void opNULL(ushort addr, byte data)
        {
        }

        void opVSYNC(ushort addr, byte data)
        {
            // Many games don't appear to supply 3 scanlines of
            // VSYNC in accordance with the Atari documentation.
            // Enduro turns on VSYNC, then turns it off twice.
            // Centipede turns off VSYNC several times, in addition to normal usage.
            // One of the Atari Bowling ROMs turns it off, but never turns it on.
            // So, we always end the frame if VSYNC is turned on and then off.
            // We also end the frame if VSYNC is turned off after scanline 258 to accomodate Bowling.
            if ((data & 0x02) == 0)
            {
                if ((RegW[VSYNC] & 0x02) != 0 || ScanLine > 258)
                {
                    EndOfFrame = true;
                    M.CPU.EmulatorPreemptRequest = true;
                }
            }

            RegW[VSYNC] = data;
        }

        void opVBLANK(ushort addr, byte data)
        {
            if ((RegW[VBLANK] & 0x80) == 0)
            {
                // dump to ground is clear and will be set
                // thus discharging all INPTx capacitors
                if ((data & 0x80) != 0)
                {
                    DumpEnabled = true;
                }
            }
            else
            {
                // dump to ground is set and will be cleared
                // thus starting all INPTx capacitors charging
                if ((data & 0x80) == 0)
                {
                    DumpEnabled = false;
                    DumpDisabledCycle = M.CPU.Clock;
                }
            }
            RegW[VBLANK] = data;
            vblankon = (data & 0x02) != 0;
        }

        void opWSYNC(ushort addr, byte data)
        {
            // on scanline=44, tetris seems to occasionally not get a WSYNC in until 3 clk in on scanline=45 causing jitter
            if (PokeOpHSync > 0)
            {
                // report the number of remaining system clocks on the scanline to delay the CPU
                WSYNCDelayClocks = 228 - PokeOpHSync;
                // request a CPU preemption to service the delay request (only if there is a delay necessary)
                M.CPU.EmulatorPreemptRequest = true;
            }
        }

        void opRSYNC(ushort addr, byte data)
        {
            LogDebug("TIA RSYNC: frame={0} scanline={0} hsync={0}", M.FrameNumber, ScanLine, PokeOpHSync);
        }

        void opNUSIZ0(ushort addr, byte data)
        {
            RegW[NUSIZ0] = (byte)(data & 0x37);

            M0size = (RegW[NUSIZ0] & 0x30) >> 4;
            M0type = RegW[NUSIZ0] & 0x07;
            P0type = M0type;

            P0suppress = 0;
        }

        void opNUSIZ1(ushort addr, byte data)
        {
            RegW[NUSIZ1] = (byte)(data & 0x37);

            M1size = (RegW[NUSIZ1] & 0x30) >> 4;
            M1type = RegW[NUSIZ1] & 0x07;
            P1type = M1type;

            P1suppress = 0;
        }

        void opCOLUBK(ushort addr, byte data)
        {
            colubk = data;
        }

        void opCOLUPF(ushort addr, byte data)
        {
            colupf = data;
        }

        void opCOLUP0(ushort addr, byte data)
        {
            colup0 = data;
        }

        void opCOLUP1(ushort addr, byte data)
        {
            colup1 = data;
        }

        void opCTRLPF(ushort addr, byte data)
        {
            RegW[CTRLPF] = data;

            BLsize = (data & 0x30) >> 4;
            scoreon = (data & 0x02) != 0;
            pfpriority = (data & 0x04) != 0;
        }

        void SetEffGRP0()
        {
            var grp0 = RegW[VDELP0] != 0 ? OldGRP0 : RegW[GRP0];
            EffGRP0 = RegW[REFP0] != 0 ? TIATables.GRPReflect[grp0] : grp0;
        }

        void SetEffGRP1()
        {
            var grp1 = RegW[VDELP1] != 0 ? OldGRP1 : RegW[GRP1];
            EffGRP1 = RegW[REFP1] != 0 ? TIATables.GRPReflect[grp1] : grp1;
        }

        void Setblon()
        {
            blon = RegW[VDELBL] != 0 ? OldENABL : RegW[ENABL] != 0;
        }

        void opREFP0(ushort addr, byte data)
        {
            RegW[REFP0] = (byte)(data & 0x08);
            SetEffGRP0();
        }

        void opREFP1(ushort addr, byte data)
        {
            RegW[REFP1] = (byte)(data & 0x08);
            SetEffGRP1();
        }

        void opPF(ushort addr, byte data)
        {
            RegW[addr] = data;
            PF210 = (uint)((RegW[PF2] << 12) | (RegW[PF1] << 4) |((RegW[PF0] >> 4) & 0x0f));
        }

        void opRESP0(ushort addr, byte data)
        {
            if (PokeOpHSync < 68)
            {
                P0 = 0;
            }
            else if (HMoveLatch && PokeOpHSync >= 68 && PokeOpHSync < 76)
            {
                // this is an attempt to model observed behavior--may not be completely correct
                // only three hsync values are really possible:
                // 69: parkerbros actionman
                // 72: activision grandprix
                // 75: barnstorming, riverraid
                P0 = -((PokeOpHSync - 68) >> 1);
            }
            else
            {
                P0 = -4;
            }
            P0 -= PokeOpHSyncDelta;
            P0suppress = 1;
        }

        void opRESP1(ushort addr, byte data)
        {
            if (PokeOpHSync < 68)
            {
                P1 = 0;
            }
            else if (HMoveLatch && PokeOpHSync >= 68 && PokeOpHSync < 76)
            {
                // this is an attempt to model observed behavior--may not be completely correct
                // only three hsync values are really possible:
                // 69: parkerbros actionman
                // 72: parkerbros actionman
                // 75: parkerbros actionman
                P1 = -((PokeOpHSync - 68) >> 1);
            }
            else
            {
                P1 = -4;
            }
            P1 -= PokeOpHSyncDelta;
            P1suppress = 1;
        }

        void opRESM0(ushort addr, byte data)
        {
            // -2 to mirror M1
            M0 = PokeOpHSync < 68 ? -2 : -4;
            M0 -= PokeOpHSyncDelta;
        }

        void opRESM1(ushort addr, byte data)
        {
            // -2 cleans up edges on activision pitfall ii
            M1 = PokeOpHSync < 68 ? -2 : -4;
            M1 -= PokeOpHSyncDelta;
        }

        void opRESBL(ushort addr, byte data)
        {
            // -2 cleans up edges on activision boxing
            // -4 confirmed via activision choppercommand; used to clean up edges
            BL = PokeOpHSync < 68 ? -2 : -4;
            BL -= PokeOpHSyncDelta;
        }

        void opAUD(ushort addr, byte data)
        {
            RegW[addr] = data;
            TIASound.Update(addr, data);
        }

        void opGRP0(ushort addr, byte data)
        {
            RegW[GRP0] = data;
            OldGRP1 = RegW[GRP1];

            SetEffGRP0();
            SetEffGRP1();
        }

        void opGRP1(ushort addr, byte data)
        {
            RegW[GRP1] = data;
            OldGRP0 = RegW[GRP0];

            OldENABL = RegW[ENABL] != 0;

            SetEffGRP0();
            SetEffGRP1();
            Setblon();
        }

        void opENAM0(ushort addr, byte data)
        {
            RegW[ENAM0] = (byte)(data & 0x02);
            m0on = RegW[ENAM0] != 0 && RegW[RESMP0] == 0;
        }

        void opENAM1(ushort addr, byte data)
        {
            RegW[ENAM1] = (byte)(data & 0x02);
            m1on = RegW[ENAM1] != 0 && RegW[RESMP1] == 0;
        }

        void opENABL(ushort addr, byte data)
        {
            RegW[ENABL] = (byte)(data & 0x02);
            Setblon();
        }

        void SetHmr(int hmr, byte data)
        {
            // marshal via >>4 for compare convenience
            RegW[hmr] = (byte)((data ^ 0x80) >> 4);
        }

        void opHM(ushort addr, byte data)
        {
            SetHmr(addr, data);
        }

        void opVDELP0(ushort addr, byte data)
        {
            RegW[VDELP0] = (byte)(data & 0x01);
            SetEffGRP0();
        }

        void opVDELP1(ushort addr, byte data)
        {
            RegW[VDELP1] = (byte)(data & 0x01);
            SetEffGRP1();
        }

        void opVDELBL(ushort addr, byte data)
        {
            RegW[VDELBL] = (byte)(data & 0x01);
            Setblon();
        }

        void opRESMP0(ushort addr, byte data)
        {
            if (RegW[RESMP0] != 0 && (data & 0x02) == 0)
            {
                var middle = 4;
                switch (RegW[NUSIZ0] & 0x07)
                {
                    case 0x05: middle <<= 1; break;  // double size
                    case 0x07: middle <<= 2; break;  // quad size
                }
                M0 = P0 - middle;
            }
            RegW[RESMP0] = (byte)(data & 0x02);
            m0on = RegW[ENAM0] != 0 && RegW[RESMP0] == 0;
        }

        void opRESMP1(ushort addr, byte data)
        {
            if (RegW[RESMP1] != 0 && (data & 0x02) == 0)
            {
                var middle = 4;
                switch (RegW[NUSIZ1] & 0x07)
                {
                    case 0x05: middle <<= 1; break;  // double size
                    case 0x07: middle <<= 2; break;  // quad size
                }
                M1 = P1 - middle;
            }
            RegW[RESMP1] = (byte)(data & 0x02);
            m1on = RegW[ENAM1] != 0 && RegW[RESMP1] == 0;
        }

        void opHMOVE(ushort addr, byte data)
        {
            P0suppress = 0;
            P1suppress = 0;
            StartHMOVEClock = Clock + 3;

            // Activision Spiderfighter Cheat (Score and Logo)
            // Delaying the start of the HMOVE here results in it completing on the next scanline (to have visible effect.)
            // HMOVEing during the visible scanline probably has extra consequences,
            // however, it seems not many carts try to do this.
            if (PokeOpHSync == 201) StartHMOVEClock++; // any increment >0 works
        }

        void opHMCLR(ushort addr, byte data)
        {
            SetHmr(HMP0, 0);
            SetHmr(HMP1, 0);
            SetHmr(HMM0, 0);
            SetHmr(HMM1, 0);
            SetHmr(HMBL, 0);
        }

        void opCXCLR(ushort addr, byte data)
        {
            Collisions = 0;
        }

        void BuildPokeOpTable()
        {
            PokeOp = new PokeOpTyp[64];
            for (var i = 0; i < PokeOp.Length; i++)
            {
                PokeOp[i] = opNULL;
            }
            PokeOp[VSYNC]  = opVSYNC;
            PokeOp[VBLANK] = opVBLANK;
            PokeOp[WSYNC]  = opWSYNC;
            PokeOp[RSYNC]  = opRSYNC;
            PokeOp[NUSIZ0] = opNUSIZ0;
            PokeOp[NUSIZ1] = opNUSIZ1;
            PokeOp[COLUP0] = opCOLUP0;
            PokeOp[COLUP1] = opCOLUP1;
            PokeOp[COLUPF] = opCOLUPF;
            PokeOp[COLUBK] = opCOLUBK;
            PokeOp[CTRLPF] = opCTRLPF;
            PokeOp[REFP0]  = opREFP0;
            PokeOp[REFP1]  = opREFP1;
            PokeOp[PF0]    = opPF;
            PokeOp[PF1]    = opPF;
            PokeOp[PF2]    = opPF;
            PokeOp[RESP0]  = opRESP0;
            PokeOp[RESP1]  = opRESP1;
            PokeOp[RESM0]  = opRESM0;
            PokeOp[RESM1]  = opRESM1;
            PokeOp[RESBL]  = opRESBL;
            PokeOp[AUDC0]  = opAUD;
            PokeOp[AUDC1]  = opAUD;
            PokeOp[AUDF0]  = opAUD;
            PokeOp[AUDF1]  = opAUD;
            PokeOp[AUDV0]  = opAUD;
            PokeOp[AUDV1]  = opAUD;
            PokeOp[GRP0]   = opGRP0;
            PokeOp[GRP1]   = opGRP1;
            PokeOp[ENAM0]  = opENAM0;
            PokeOp[ENAM1]  = opENAM1;
            PokeOp[ENABL]  = opENABL;
            PokeOp[HMP0]   = opHM;
            PokeOp[HMP1]   = opHM;
            PokeOp[HMM0]   = opHM;
            PokeOp[HMM1]   = opHM;
            PokeOp[HMBL]   = opHM;
            PokeOp[VDELP0] = opVDELP0;
            PokeOp[VDELP1] = opVDELP1;
            PokeOp[VDELBL] = opVDELBL;
            PokeOp[RESMP0] = opRESMP0;
            PokeOp[RESMP1] = opRESMP1;
            PokeOp[HMOVE]  = opHMOVE;
            PokeOp[HMCLR]  = opHMCLR;
            PokeOp[CXCLR]  = opCXCLR;
        }

        #endregion

        #region Input Helpers

        int SampleINPT(int inpt)
        {
            var mi = M.InputState;

            switch (inpt <= 1 ? mi.LeftControllerJack : mi.RightControllerJack)
            {
                case Controller.Paddles:
                    // playerno = inpt
                    return mi.SampleCapturedOhmState(inpt & 3);
                case Controller.ProLineJoystick:
                    // playerno = inpt/2
                    switch (inpt & 3)
                    {
                        case 0: return mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger)  ? 0 : Int32.MaxValue;
                        case 1: return mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger2) ? 0 : Int32.MaxValue;
                        case 2: return mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger)  ? 0 : Int32.MaxValue;
                        case 3: return mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger2) ? 0 : Int32.MaxValue;
                    }
                    break;
                case Controller.BoosterGrip:
                    // playerno = inpt
                    return mi.SampleCapturedControllerActionState(inpt & 3, ControllerAction.Trigger2) ? 0 : Int32.MaxValue;
                case Controller.Keypad:
                    return SampleKeypadStateDumped(inpt & 3);
            }
            return int.MaxValue;
        }

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

            switch (playerNo == 0 ? mi.LeftControllerJack : mi.RightControllerJack)
            {
                case Controller.Joystick:
                case Controller.ProLineJoystick:
                case Controller.Driving:
                case Controller.BoosterGrip:
                    return mi.SampleCapturedControllerActionState(playerNo, ControllerAction.Trigger);
                case Controller.Keypad:
                    return SampleKeypadStateLatched(playerNo);
                case Controller.Lightgun:
                    int sampledScanline, sampledHpos;
                    mi.SampleCapturedLightGunPosition(playerNo, out sampledScanline, out sampledHpos);
                    return ((scanline - 4) >= sampledScanline && (hpos - 23) >= sampledHpos);
            }
            return false;
        }

        bool SampleKeypadStateLatched(int deviceno)
        {
            ControllerAction action;

            if ((M.PIA.WrittenPortA & 0x01) == 0)
            {
                action = ControllerAction.Keypad3;
            }
            else if ((M.PIA.WrittenPortA & 0x02) == 0)
            {
                action = ControllerAction.Keypad6;
            }
            else if ((M.PIA.WrittenPortA & 0x04) == 0)
            {
                action = ControllerAction.Keypad9;
            }
            else if ((M.PIA.WrittenPortA & 0x08) == 0)
            {
                action = ControllerAction.KeypadP;
            }
            else
            {
                return false;
            }

            return M.InputState.SampleCapturedControllerActionState(deviceno, action);
        }

        int SampleKeypadStateDumped(int inpt)
        {
            ControllerAction action;

            if ((M.PIA.WrittenPortA & 0x01) == 0)
            {
                action = (inpt & 1) == 0 ? ControllerAction.Keypad1 : ControllerAction.Keypad2;
            }
            else if ((M.PIA.WrittenPortA & 0x02) == 0)
            {
                action = (inpt & 1) == 0 ? ControllerAction.Keypad4 : ControllerAction.Keypad5;
            }
            else if ((M.PIA.WrittenPortA & 0x04) == 0)
            {
                action = (inpt & 1) == 0 ? ControllerAction.Keypad7 : ControllerAction.Keypad8;
            }
            else if ((M.PIA.WrittenPortA & 0x08) == 0)
            {
                action = (inpt & 1) == 0 ? ControllerAction.KeypadA : ControllerAction.Keypad0;
            }
            else
            {
                return Int32.MaxValue;
            }

            // playerno = inpt/2
            return M.InputState.SampleCapturedControllerActionState(inpt >> 1, action) ? Int32.MaxValue : 0;
        }

        #endregion

        #region Serialization Members

        public TIA(DeserializationContext input, MachineBase m) : this()
        {
            if (input == null)
                throw new ArgumentNullException("input");
            if (m == null)
                throw new ArgumentNullException("m");

            M = m;
            TIASound = input.ReadTIASound(M, CPU_TICKS_PER_AUDIO_SAMPLE);

            input.CheckVersion(1);
            RegW = input.ReadExpectedBytes(0x40);
            HSync = input.ReadInt32();
            HMoveCounter = input.ReadInt32();
            ScanLine = input.ReadInt32();
            FrameBufferIndex = input.ReadInt32();
            FrameBufferElement = input.ReadBufferElement();
            StartHMOVEClock = input.ReadUInt64();
            HMoveLatch = input.ReadBoolean();
            StartClock = input.ReadUInt64();
            P0 = input.ReadInt32();
            P0mmr = input.ReadBoolean();
            EffGRP0 = input.ReadByte();
            OldGRP0 = input.ReadByte();
            P0type = input.ReadInt32();
            P0suppress = input.ReadInt32();
            P1 = input.ReadInt32();
            P1mmr = input.ReadBoolean();
            EffGRP1 = input.ReadByte();
            OldGRP1 = input.ReadByte();
            P1type = input.ReadInt32();
            P1suppress = input.ReadInt32();
            M0 = input.ReadInt32();
            M0mmr = input.ReadBoolean();
            M0type = input.ReadInt32();
            M0size = input.ReadInt32();
            m0on = input.ReadBoolean();
            M1 = input.ReadInt32();
            M1mmr = input.ReadBoolean();
            M1type = input.ReadInt32();
            M1size = input.ReadInt32();
            m1on = input.ReadBoolean();
            BL = input.ReadInt32();
            BLmmr = input.ReadBoolean();
            OldENABL = input.ReadBoolean();
            BLsize = input.ReadInt32();
            blon = input.ReadBoolean();
            PF210 = input.ReadUInt32();
            PFReflectionState = input.ReadInt32();
            colubk = input.ReadByte();
            colupf = input.ReadByte();
            colup0 = input.ReadByte();
            colup1 = input.ReadByte();
            vblankon = input.ReadBoolean();
            scoreon = input.ReadBoolean();
            pfpriority = input.ReadBoolean();
            DumpEnabled = input.ReadBoolean();
            DumpDisabledCycle = input.ReadUInt64();
            Collisions = (TIACxPairFlags)input.ReadInt32();
            WSYNCDelayClocks = input.ReadInt32();
            EndOfFrame = input.ReadBoolean();
        }

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

            output.Write(TIASound);

            output.WriteVersion(1);
            output.Write(RegW);
            output.Write(HSync);
            output.Write(HMoveCounter);
            output.Write(ScanLine);
            output.Write(FrameBufferIndex);
            output.Write(FrameBufferElement);
            output.Write(StartHMOVEClock);
            output.Write(HMoveLatch);
            output.Write(StartClock);
            output.Write(P0);
            output.Write(P0mmr);
            output.Write(EffGRP0);
            output.Write(OldGRP0);
            output.Write(P0type);
            output.Write(P0suppress);
            output.Write(P1);
            output.Write(P1mmr);
            output.Write(EffGRP1);
            output.Write(OldGRP1);
            output.Write(P1type);
            output.Write(P1suppress);
            output.Write(M0);
            output.Write(M0mmr);
            output.Write(M0type);
            output.Write(M0size);
            output.Write(m0on);
            output.Write(M1);
            output.Write(M1mmr);
            output.Write(M1type);
            output.Write(M1size);
            output.Write(m1on);
            output.Write(BL);
            output.Write(BLmmr);
            output.Write(OldENABL);
            output.Write(BLsize);
            output.Write(blon);
            output.Write(PF210);
            output.Write(PFReflectionState);
            output.Write(colubk);
            output.Write(colupf);
            output.Write(colup0);
            output.Write(colup1);
            output.Write(vblankon);
            output.Write(scoreon);
            output.Write(pfpriority);
            output.Write(DumpEnabled);
            output.Write(DumpDisabledCycle);
            output.Write((int)Collisions);
            output.Write(WSYNCDelayClocks);
            output.Write(EndOfFrame);
        }

        #endregion

        #region Helpers

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

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

        #endregion
    }
}