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

namespace AxibugEmuOnline.Client.UNES.Mapper
{
    [MapperDef(1)]
    public class MMC1 : BaseMapper
    {
        // TODO: are MMC1 and MMC1A even different chip types?
        public enum ChipType { MMC1, MMC1A, MMC1B, MMC1C }
        public enum CHRBankingMode { Single, Double }
        public enum PRGBankingMode { Switch32Kb, Switch16KbFixFirst, Switch16KbFixLast }

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

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

        private uint _serialData;
        private int _serialPos;

        private uint _control;

        private readonly uint[] _chrBankOffsets = new uint[2];
        private readonly uint[] _chrBanks = new uint[2];

        private readonly uint[] _prgBankOffsets = new uint[2];
        private uint _prgBank;

        private bool _prgRAMEnabled;

        private uint? _lastWritePC;

        public MMC1(Emulator emulator) : this(emulator, ChipType.MMC1B)
        {

        }

        public MMC1(Emulator emulator, ChipType chipType) : base(emulator)
        {
            _type = chipType;
            if (chipType == ChipType.MMC1B) _prgRAMEnabled = true;
            UpdateControl(0x0F);
            _emulator.Cartridge.MirroringMode = Horizontal;
        }

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

            cpu.MapWriteHandler(0x6000, 0x7FFF, (address, val) =>
            {
                // PRG RAM is always enabled on MMC1A
                if (_type == ChipType.MMC1A || _prgRAMEnabled)
                    _prgRAM[address - 0x6000] = val;
            });

            cpu.MapWriteHandler(0x8000, 0xFFFF, (address, val) =>
            {
                // Explicitly ignore the second write happening on consecutive cycles
                // of an RMW instruction
                var cycle = _emulator.CPU.PC;
                if (cycle == _lastWritePC)
                    return;
                _lastWritePC = cycle;

                if ((val & 0x80) > 0)
                {
                    _serialData = 0;
                    _serialPos = 0;
                    UpdateControl(_control | 0x0C);
                }
                else
                {
                    _serialData |= (uint)((val & 0x1) << _serialPos);
                    _serialPos++;

                    if (_serialPos == 5)
                    {
                        // Address is incompletely decoded
                        address &= 0x6000;
                        if (address == 0x0000)
                            UpdateControl(_serialData);
                        else if (address == 0x2000)
                            UpdateCHRBank(0, _serialData);
                        else if (address == 0x4000)
                            UpdateCHRBank(1, _serialData);
                        else if (address == 0x6000)
                            UpdatePRGBank(_serialData);

                        _serialData = 0;
                        _serialPos = 0;
                    }
                }
            });
        }

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

        private void UpdateControl(uint value)
        {
            _control = value;

            _emulator.Cartridge.MirroringMode = _mirroringModes[value & 0x3];

            _chrBankingMode = (CHRBankingMode)((value >> 4) & 0x1);

            var prgMode = (value >> 2) & 0x3;
            // Both 0 and 1 are 32Kb switch
            if (prgMode == 0) prgMode = 1;
            _prgBankingMode = (PRGBankingMode)(prgMode - 1);

            UpdateCHRBank(1, _chrBanks[1]);
            UpdateCHRBank(0, _chrBanks[0]);
            UpdatePRGBank(_prgBank);
        }

        private void UpdatePRGBank(uint value)
        {
            _prgBank = value;

            _prgRAMEnabled = (value & 0x10) == 0;
            value &= 0xF;

            switch (_prgBankingMode)
            {
                case PRGBankingMode.Switch32Kb:
                    value >>= 1;
                    value *= 0x4000;
                    _prgBankOffsets[0] = value;
                    _prgBankOffsets[1] = value + 0x4000;
                    break;
                case PRGBankingMode.Switch16KbFixFirst:
                    _prgBankOffsets[0] = 0;
                    _prgBankOffsets[1] = value * 0x4000;
                    break;
                case PRGBankingMode.Switch16KbFixLast:
                    _prgBankOffsets[0] = value * 0x4000;
                    _prgBankOffsets[1] = _lastBankOffset;
                    break;
            }
        }

        private void UpdateCHRBank(uint bank, uint value)
        {
            _chrBanks[bank] = value;

            // TODO FIXME: I feel like this branch should only be taken
            // when bank == 0, but this breaks Final Fantasy
            // When can banking mode change without UpdateCHRBank being called?
            if (_chrBankingMode == CHRBankingMode.Single)
            {
                value = _chrBanks[0];
                value >>= 1;
                value *= 0x1000;
                _chrBankOffsets[0] = value;
                _chrBankOffsets[1] = value + 0x1000;
            }
            else
            {
                _chrBankOffsets[bank] = value * 0x1000;
            }
        }
    }
}