using static AxibugEmuOnline.Client.UNES.Cartridge.VRAMMirroringMode;

namespace AxibugEmuOnline.Client.UNES.Mapper
{
    [MapperDef(4)]
    public class MMC3 : BaseMapper
    {
        // Different PRG RAM write/enable controls
        public enum ChipType { MMC3, MMC6 }
        public enum CHRBankingMode { TwoFour, FourTwo }
        public enum PRGBankingMode { SwitchFix, FixSwitch }

        private readonly Cartridge.VRAMMirroringMode[] _mirroringModes = { Vertical, Horizontal };

        private readonly ChipType _type;
        protected CHRBankingMode _chrBankingMode;
        protected PRGBankingMode _prgBankingMode;


        protected readonly uint[] _chrBankOffsets = new uint[8];
        protected uint[] _prgBankOffsets;
        protected readonly uint[] _banks = new uint[8];
        protected uint _currentBank;

        private uint _irqReloadValue;
        private uint _irqCounter;
        protected bool _irqEnabled;

        private bool _prgRAMEnabled;

        public MMC3(Emulator emulator) : this(emulator, ChipType.MMC3)
        {

        }

        public MMC3(Emulator emulator, ChipType chipType) : base(emulator)
        {
            _type = chipType;
            _prgBankOffsets = new uint[] { 0, 0x2000, _lastBankOffset, _lastBankOffset + 0x2000 };
        }

        public override void InitializeMemoryMap(CPU cpu)
        {
            cpu.MapReadHandler(0x6000, 0x7FFF, address => _prgRAM[address - 0x6000]);
            cpu.MapReadHandler(0x8000, 0xFFFF, address => _prgROM[_prgBankOffsets[(address - 0x8000) / 0x2000] + address % 0x2000]);

            cpu.MapWriteHandler(0x6000, 0xFFFF, WriteByte);
        }

        public override void InitializeMemoryMap(PPU ppu)
        {
            ppu.MapReadHandler(0x0000, 0x1FFF, address => _chrROM[_chrBankOffsets[address / 0x400] + address % 0x400]);
            ppu.MapWriteHandler(0x0000, 0x1FFF, (address, val) => _chrROM[_chrBankOffsets[address / 0x400] + address % 0x400] = val);
        }

        public override void ProcessCycle(int scanLine, int cycle)
        {
            if (_emulator.PPU.F.RenderingEnabled && cycle == 260 && (0 <= scanLine && scanLine < 240 || scanLine == -1))
            {
                if (_irqCounter == 0)
                {
                    _irqCounter = _irqReloadValue;
                }
                else
                {
                    _irqCounter--;
                    if (_irqEnabled && _irqCounter == 0)
                    {
                        _emulator.CPU.TriggerInterrupt(CPU.InterruptType.IRQ);
                    }
                }
            }
        }

        protected void WriteByte(uint addr, byte value)
        {
            bool even = (addr & 0x1) == 0;

            if (addr < 0x8000)
            {
                if (_prgRAMEnabled)
                {
                    _prgRAM[addr - 0x6000] = value;
                }
            }
            else if (addr < 0xA000)
            {
                if (even)
                {
                    _currentBank = value & 0x7u;
                    _prgBankingMode = (PRGBankingMode)((value >> 6) & 0x1);
                    _chrBankingMode = (CHRBankingMode)((value >> 7) & 0x1);
                }
                else
                {
                    _banks[_currentBank] = value;
                }

                UpdateOffsets();
            }
            else if (addr < 0xC000)
            {
                if (even)
                {
                    _emulator.Cartridge.MirroringMode = _mirroringModes[value & 0x1];
                }
                else
                {
                    _prgRAMEnabled = (value & 0xC0) == 0x80;
                }
            }
            else if (addr < 0xE000)
            {
                if (even)
                {
                    _irqReloadValue = value;
                }
                else
                {
                    _irqCounter = 0;
                }
            }
            else
            {
                _irqEnabled = !even;
            }
        }

        protected void UpdateOffsets()
        {
            switch (_prgBankingMode)
            {
                case PRGBankingMode.SwitchFix:
                    _prgBankOffsets[0] = _banks[6] * 0x2000;
                    _prgBankOffsets[1] = _banks[7] * 0x2000;
                    _prgBankOffsets[2] = _lastBankOffset;
                    _prgBankOffsets[3] = _lastBankOffset + 0x2000;
                    break;
                case PRGBankingMode.FixSwitch:
                    _prgBankOffsets[0] = _lastBankOffset;
                    _prgBankOffsets[1] = _banks[7] * 0x2000;
                    _prgBankOffsets[2] = _banks[6] * 0x2000;
                    _prgBankOffsets[3] = _lastBankOffset + 0x2000;
                    break;
            }

            switch (_chrBankingMode)
            {
                case CHRBankingMode.TwoFour:
                    _chrBankOffsets[0] = _banks[0] & 0xFE;
                    _chrBankOffsets[1] = _banks[0] | 0x01;
                    _chrBankOffsets[2] = _banks[1] & 0xFE;
                    _chrBankOffsets[3] = _banks[1] | 0x01;
                    _chrBankOffsets[4] = _banks[2];
                    _chrBankOffsets[5] = _banks[3];
                    _chrBankOffsets[6] = _banks[4];
                    _chrBankOffsets[7] = _banks[5];
                    break;
                case CHRBankingMode.FourTwo:
                    _chrBankOffsets[0] = _banks[2];
                    _chrBankOffsets[1] = _banks[3];
                    _chrBankOffsets[2] = _banks[4];
                    _chrBankOffsets[3] = _banks[5];
                    _chrBankOffsets[4] = _banks[0] & 0xFE;
                    _chrBankOffsets[5] = _banks[0] | 0x01;
                    _chrBankOffsets[6] = _banks[1] & 0xFE;
                    _chrBankOffsets[7] = _banks[1] | 0x01;
                    break;
            }

            for (var i = 0; i < _prgBankOffsets.Length; i++)
            {
                _prgBankOffsets[i] %= (uint)_prgROM.Length;
            }

            for (var i = 0; i < _chrBankOffsets.Length; i++)
            {
                _chrBankOffsets[i] = (uint) (_chrBankOffsets[i] * 0x400 % _chrROM.Length);
            }
        }
    }
}