/*
 * PIA.cs
 *
 * The Peripheral Interface Adapter (6532) device.
 * a.k.a. RIOT (RAM I/O Timer?)
 *
 * Copyright © 2003, 2004, 2012 Mike Murphy
 *
 */
using System;

namespace EMU7800.Core
{
    public sealed class PIA : IDevice
    {
        readonly MachineBase M;

        readonly byte[] RAM = new byte[0x80];

        ulong TimerTarget;
        int TimerShift;
        bool IRQEnabled, IRQTriggered;

        public byte DDRA { get; private set; }
        public byte DDRB { get; private set; }

        public byte WrittenPortA { get; private set; }
        public byte WrittenPortB { get; private set; }

        #region IDevice Members

        public void Reset()
        {
            // Some games will loop/hang on $0284 if these are initialized to zero
            TimerShift = 10;
            TimerTarget = M.CPU.Clock + (ulong)(0xff << TimerShift);

            IRQEnabled = false;
            IRQTriggered = false;

            DDRA = 0;

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

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

        #endregion

        public override string ToString()
        {
            return "PIA/RIOT M6532";
        }

        #region Constructors

        public PIA(MachineBase m)
        {
            if (m == null)
                throw new ArgumentNullException("m");
            M = m;
        }

        #endregion

        byte peek(ushort addr)
        {
            if ((addr & 0x200) == 0)
            {
                return RAM[addr & 0x7f];
            }

            switch ((byte)(addr & 7))
            {
                case 0:  // SWCHA: Controllers
                    return ReadPortA();
                case 1:  // SWCHA DDR: 0=input, 1=output
                    return DDRA;
                case 2:  // SWCHB: Console switches (on 7800, PB2 & PB4 are used)
                    return ReadPortB();
                case 3:  // SWCHB DDR: 0=input, 1=output
                    return 0;
                case 4:  // INTIM
                case 6:
                    return ReadTimerRegister();
                case 5:  // INTFLG
                case 7:
                    return ReadInterruptFlag();
                default:
                    LogDebug("PIA: Unhandled peek ${0:x4}, PC=${1:x4}", addr, M.CPU.PC);
                    return 0;
            }
        }

        void poke(ushort addr, byte data)
        {
            if ((addr & 0x200) == 0)
            {
                RAM[addr & 0x7f] = data;
                return;
            }

            // A2 Distinguishes I/O registers from the Timer
            if ((addr & 0x04) != 0)
            {
                if ((addr & 0x10) != 0)
                {
                    IRQEnabled = (addr & 0x08) != 0;
                    SetTimerRegister(data, addr & 3);
                }
                else
                {
                    LogDebug("PIA: Timer: Unhandled poke ${0:x4} w/${1:x2}, PC=${2:x4}", addr, data, M.CPU.PC);
                }
            }
            else
            {
                switch ((byte)(addr & 3))
                {
                    case 0:  // SWCHA:  Port A
                        WritePortA(data);
                        break;
                    case 1:  // SWACNT: Port A DDR
                        DDRA = data;
                        break;
                    case 2:  // SWCHB:  Port B
                        WritePortB(data);
                        break;
                    case 3:  // SWBCNT: Port B DDR
                        DDRB = data;
                        break;
                }
            }
        }

        // 0: TIM1T:  set    1 clock interval (  838 nsec/interval)
        // 1: TIM8T:  set    8 clock interval (  6.7 usec/interval)
        // 2: TIM64T: set   64 clock interval ( 53.6 usec/interval)
        // 3: T1024T: set 1024 clock interval (858.2 usec/interval)
        void SetTimerRegister(byte data, int interval)
        {
            IRQTriggered = false;
            TimerShift = new[] { 0, 3, 6, 10 }[interval];
            TimerTarget = M.CPU.Clock + (ulong)(data << TimerShift);
        }

        byte ReadTimerRegister()
        {
            IRQTriggered = false;
            var delta = (int)(TimerTarget - M.CPU.Clock);
            if (delta >= 0)
            {
                return (byte)(delta >> TimerShift);
            }
            if (delta != -1)
            {
                IRQTriggered = true;
            }
            return (byte)(delta >= -256 ? delta : 0);
        }

        byte ReadInterruptFlag()
        {
            var delta = (int)(TimerTarget - M.CPU.Clock);
            return (byte)((delta >= 0 || IRQEnabled && IRQTriggered) ? 0x00 : 0x80);
        }

        // PortA: Controller Jacks
        //
        //            Left Jack                Right Jack
        //          -------------             -------------
        //          \ 1 2 3 4 5 /             \ 1 2 3 4 5 /
        //           \ 6 7 8 9 /               \ 6 7 8 9 /
        //            ---------                 ---------
        //
        // pin 1   D4 PIA SWCHA            D0 PIA SWCHA
        // pin 2   D5 PIA SWCHA            D1 PIA SWCHA
        // pin 3   D6 PIA SWCHA            D2 PIA SWCHA
        // pin 4   D7 PIA SWCHA            D3 PIA SWCHA
        // pin 5   D7 TIA INPT1 (Dumped)   D7 TIA INPT3 (Dumped)   7800: Right Fire
        // pin 6   D7 TIA INPT4 (Latched)  D7 TIA INPT5 (Latched)  2600: Fire
        // pin 7   +5                      +5
        // pin 8   GND                     GND
        // pin 9   D7 TIA INPT0 (Dumped)   D7 TIA INPT2 (Dumped)   7800: Left Fire
        //
        byte ReadPortA()
        {
            var porta = 0;
            var mi = M.InputState;

            switch (mi.LeftControllerJack)
            {
                case Controller.Joystick:
                case Controller.ProLineJoystick:
                case Controller.BoosterGrip:
                    porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Up)    ? 0 : (1 << 4);
                    porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Down)  ? 0 : (1 << 5);
                    porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Left)  ? 0 : (1 << 6);
                    porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Right) ? 0 : (1 << 7);
                    break;
                case Controller.Driving:
                    porta |= mi.SampleCapturedDrivingState(0) << 4;
                    break;
                case Controller.Paddles:
                    porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger) ? 0 : (1 << 7);
                    porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger) ? 0 : (1 << 6);
                    break;
                case Controller.Lightgun:
                    porta |= mi.SampleCapturedControllerActionState(0, ControllerAction.Trigger) ? (1 << 4) : 0;
                    break;
            }

            switch (mi.RightControllerJack)
            {
                case Controller.Joystick:
                case Controller.ProLineJoystick:
                case Controller.BoosterGrip:
                    porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Up)    ? 0 : (1 << 0);
                    porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Down)  ? 0 : (1 << 1);
                    porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Left)  ? 0 : (1 << 2);
                    porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Right) ? 0 : (1 << 3);
                    break;
                case Controller.Driving:
                    porta |= mi.SampleCapturedDrivingState(1);
                    break;
                case Controller.Paddles:
                    porta |= mi.SampleCapturedControllerActionState(2, ControllerAction.Trigger) ? 0 : (1 << 3);
                    porta |= mi.SampleCapturedControllerActionState(3, ControllerAction.Trigger) ? 0 : (1 << 2);
                    break;
                case Controller.Lightgun:
                    porta |= mi.SampleCapturedControllerActionState(1, ControllerAction.Trigger) ? (1 << 0) : 0;
                    break;
            }

            return (byte)porta;
        }

        void WritePortA(byte porta)
        {
            WrittenPortA = (byte)((porta & DDRA) | (WrittenPortA & (~DDRA)));
        }

        void WritePortB(byte portb)
        {
            WrittenPortB = (byte)((portb & DDRB) | (WrittenPortB & (~DDRB)));
        }

        // PortB: Console Switches
        //
        // D0 Game Reset  0=on
        // D1 Game Select 0=on
        // D2 (used on 7800)
        // D3 Console Color 1=Color, 0=B/W
        // D4 (used on 7800)
        // D5 (unused)
        // D6 Left  Difficulty A 1=A (pro), 0=B (novice)
        // D7 Right Difficulty A 1=A (pro), 0=B (novice)
        //
        byte ReadPortB()
        {
            var portb = 0;
            var mi = M.InputState;

            portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.GameReset)        ? 0 : (1 << 0);
            portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.GameSelect)       ? 0 : (1 << 1);
            portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.GameBW)           ? 0 : (1 << 3);
            portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.LeftDifficultyA)  ? (1 << 6) : 0;
            portb |= mi.SampleCapturedConsoleSwitchState(ConsoleSwitch.RightDifficultyA) ? (1 << 7) : 0;

            return (byte)portb;
        }

        #region Serialization Members

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

            var version = input.CheckVersion(1, 2);
            RAM = input.ReadExpectedBytes(0x80);
            TimerTarget = input.ReadUInt64();
            TimerShift = input.ReadInt32();
            IRQEnabled = input.ReadBoolean();
            IRQTriggered = input.ReadBoolean();
            DDRA = input.ReadByte();
            WrittenPortA = input.ReadByte();
            if (version > 1)
            {
                DDRB = input.ReadByte();
                WrittenPortB = input.ReadByte();
            }
        }

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

            output.WriteVersion(2);
            output.Write(RAM);
            output.Write(TimerTarget);
            output.Write(TimerShift);
            output.Write(IRQEnabled);
            output.Write(IRQTriggered);
            output.Write(DDRA);
            output.Write(WrittenPortA);
            output.Write(DDRB);
            output.Write(WrittenPortB);
        }

        #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
    }
}