using System;
using BizHawk.Common;
using BizHawk.Common.NumberExtensions;

namespace BizHawk.Emulation.Cores.Nintendo.NES
{
	// Mapper 83 seems to be a hacky mess that represents 3 different Cony cartridges
	// http://problemkaputt.de/everynes.htm#mapper83cony
	internal sealed class ConyA : NesBoardBase
	{
		private byte[] prg_regs = new byte[4];
		private byte[] _low = new byte[4]; // some kind of security feature?
		private byte[] chr_regs = new byte[8];

		private int prg_bank_mask_16k, prg_bank_mask_8k, chr_bank_mask_2k;
		private int IRQCount;
		private bool IRQa;
		private byte bank, mode;
		private bool is_2k_bank, is_not_2k_bank;

		public override bool Configure(EDetectionOrigin origin)
		{
			switch (Cart.BoardType)
			{
				case "MAPPER083":
					if (Cart.PrgSize == 128)
					{
						prg_bank_mask_8k = Cart.PrgSize / 8 - 1;
						prg_bank_mask_16k = Cart.PrgSize / 16 - 1;
						chr_bank_mask_2k = 127;
						//prg_regs[0] = 0xC;
						//prg_regs[1] = 0xB;
						//prg_regs[2] = 0xE;
						//prg_regs[3] = 0xF;
						return true;
					}
					return false;
				default:
					return false;
			}
		}

		public override void SyncState(Serializer ser)
		{
			base.SyncState(ser);
			ser.Sync(nameof(prg_regs), ref prg_regs, false);
			ser.Sync(nameof(chr_regs), ref chr_regs, false);
			ser.Sync(nameof(IRQCount), ref IRQCount);
			ser.Sync(nameof(IRQa), ref IRQa);
			ser.Sync(nameof(bank), ref bank);
			ser.Sync(nameof(mode), ref mode);
			ser.Sync(nameof(is_2k_bank), ref is_2k_bank);
			ser.Sync(nameof(is_not_2k_bank), ref is_not_2k_bank);
			ser.Sync(nameof(_low), ref _low, false);
		}

		public void Mirroring()
		{
			switch (mode & 3)
			{
				case 0: SetMirrorType(EMirrorType.Vertical); break;
				case 1: SetMirrorType(EMirrorType.Horizontal); break;
				case 2: SetMirrorType(EMirrorType.OneScreenA); break;
				case 3: SetMirrorType(EMirrorType.OneScreenB); break;
			}
		}
		public override void WritePrg(int addr, byte value)
		{
			switch (addr)
			{
				case 0x0000: is_2k_bank = true; bank = value; mode |= 0x40; break;
				case 0x3000:
				case 0x30FF:
				case 0x31FF:
					bank = value;
					mode |= 0x40;
					break;

				case 0x0100: mode = (byte)(value | (mode & 0x40)); break;

				case 0x0300: prg_regs[0] = value; mode &= 0xBF; break;
				case 0x0301: prg_regs[1] = value; mode &= 0xBF; break;
				case 0x0302: prg_regs[2] = value; mode &= 0xBF; break;

				// used in 1k CHR bank switching
				case 0x0312: chr_regs[2] = value; is_not_2k_bank = true; break;
				case 0x0313: chr_regs[3] = value; is_not_2k_bank = true; break;
				case 0x0314: chr_regs[4] = value; is_not_2k_bank = true; break;
				case 0x0315: chr_regs[5] = value; is_not_2k_bank = true; break;

				// used in 1k and 2k CHR bank switching
				case 0x0310: chr_regs[0] = value; break;
				case 0x0311: chr_regs[1] = value; break;
				case 0x0316: chr_regs[6] = value; break;
				case 0x0317: chr_regs[7] = value; break;

				case 0x0200:
					IRQCount &= 0xFF00; IRQCount |= value;
					IrqSignal = false;
					break;
				case 0x0201:
					IRQCount &= 0xFF;
					IRQCount |= value << 8;
					IRQa = mode.Bit(7);
					break;
			}

			Mirroring();
		}

		public override byte ReadPpu(int addr)
		{
			if (addr < 0x2000)
			{
				if (is_2k_bank && !is_not_2k_bank)
				{
					int index = (addr >> 11) & 0x3;
					int bank = chr_regs[index];

					// indexes are numbered oddly for different bank switching schemes
					if (index == 2)
						bank = chr_regs[6];
					if (index == 3)
						bank = chr_regs[7];

					bank &= chr_bank_mask_2k;
					return Vrom[(bank << 11) + (addr & 0x7FF)];
				}
				else
				{
					int index = (addr >> 10) & 0x7;
					int bank = chr_regs[index];
					bank |= ((bank & 0x30) << 4);
					bank &= 0xFF;
					return Vrom[(bank << 10) + (addr & 0x3FF)];
				}

			}

			return base.ReadPpu(addr);
		}

		public override byte ReadPrg(int addr)
		{
			if ((mode & 0x40) > 0)
			{
				if (addr < 0x4000)
				{
					return Rom[(((bank & 0x7) & 0x3F) << 14) + (addr & 0x3FFF)];
				}
				return Rom[((((bank & 0x7) & 0x30) | 0x7) << 14) + (addr & 0x3FFF)];
			}
			else
			{
				int index = (addr >> 13) & 0x3;
				int bank = prg_regs[index];
				// last bank is fixed
				if (index == 3)
					bank = prg_bank_mask_8k;

				return Rom[(bank << 13) + (addr & 0x1FFF)];
			}
		}

		public override void ClockCpu()
		{
			if (IRQa)
			{
				IRQCount--;
				if (IRQCount == 0)
				{
					IRQCount = 0xFFFF;
					IrqSignal = true;
					IRQa = false;
				}
			}
		}

		public override void WriteExp(int addr, byte value)
		{
			if (addr >= 0x1100 && addr <= 0x1103)
				_low[addr & 0x3] = value;
			else
				base.WriteExp(addr, value);
		}

		public override byte ReadExp(int addr)
		{
			if (addr == 0x1000)
				return (byte)((NES.DB & 0xFC) | 0);
			else if (addr >= 0x1100 && addr <= 0x1103)
				return _low[addr & 0x3];
			else
				return base.ReadExp(addr);

		}
	}

	internal sealed class ConyB : NesBoardBase
	{
		private byte[] prg_regs = new byte[4];
		private byte[] _low = new byte[4]; // some kind of security feature?
		private byte[] chr_regs = new byte[8];

		private int prg_bank_mask_16k, prg_bank_mask_8k, chr_bank_mask_2k;
		private int IRQCount;
		private bool IRQa;
		private byte bank, mode;
		private bool is_2k_bank, is_not_2k_bank;

		public override bool Configure(EDetectionOrigin origin)
		{
			switch (Cart.BoardType)
			{
				case "MAPPER083":
					if (Cart.PrgSize == 256)
					{
						prg_bank_mask_16k = Cart.PrgSize / 16 - 1;
						prg_bank_mask_8k = Cart.PrgSize / 8 - 1;
						chr_bank_mask_2k = Cart.PrgSize / 2 - 1;

						//prg_regs[1] = (byte)prg_bank_mask_16k;
						//is_2k_bank = true;
						return true;
					}
					return false;
				default:
					return false;
			}
		}

		public override void SyncState(Serializer ser)
		{
			base.SyncState(ser);
			ser.Sync(nameof(prg_regs), ref prg_regs, false);
			ser.Sync(nameof(chr_regs), ref chr_regs, false);
			ser.Sync(nameof(IRQCount), ref IRQCount);
			ser.Sync(nameof(IRQa), ref IRQa);
			ser.Sync(nameof(bank), ref bank);
			ser.Sync(nameof(mode), ref mode);
			ser.Sync(nameof(is_2k_bank), ref is_2k_bank);
			ser.Sync(nameof(is_not_2k_bank), ref is_not_2k_bank);
			ser.Sync(nameof(_low), ref _low, false);
		}

		public void Mirroring()
		{
			switch (mode & 3)
			{
				case 0: SetMirrorType(EMirrorType.Vertical); break;
				case 1: SetMirrorType(EMirrorType.Horizontal); break;
				case 2: SetMirrorType(EMirrorType.OneScreenA); break;
				case 3: SetMirrorType(EMirrorType.OneScreenB); break;
			}
		}
		public override void WritePrg(int addr, byte value)
		{
			switch (addr)
			{
				case 0x0000: is_2k_bank = true; bank = value; mode |= 0x40; break;
				case 0x3000:  
				case 0x30FF: 
				case 0x31FF:
					bank = value;
					mode |= 0x40;
					break;

				case 0x0100: mode = (byte)(value | (mode & 0x40)); break;

				case 0x0300: prg_regs[0] = value; mode &= 0xBF; break;
				case 0x0301: prg_regs[1] = value; mode &= 0xBF; break;
				case 0x0302: prg_regs[2] = value; mode &= 0xBF; break;

				// used in 1k CHR bank switching
				case 0x0312: chr_regs[2] = value; is_not_2k_bank = true; break;
				case 0x0313: chr_regs[3] = value; is_not_2k_bank = true; break;
				case 0x0314: chr_regs[4] = value; is_not_2k_bank = true; break;
				case 0x0315: chr_regs[5] = value; is_not_2k_bank = true; break;

				// used in 1k and 2k CHR bank switching
				case 0x0310: chr_regs[0] = value; break;
				case 0x0311: chr_regs[1] = value; break;
				case 0x0316: chr_regs[6] = value; break;
				case 0x0317: chr_regs[7] = value; break;

				case 0x0200:
					IRQCount &= 0xFF00; IRQCount |= value; 
					IrqSignal = false;
					break;
				case 0x0201:
					IRQCount &= 0xFF;
					IRQCount |= value << 8;
					IRQa = mode.Bit(7);
					break;
			}

			Mirroring();
		}

		public override byte ReadPpu(int addr)
		{
			if (addr < 0x2000)
			{
				if (is_2k_bank && !is_not_2k_bank)
				{
					int index = (addr >> 11) & 0x3;
					int bank = chr_regs[index];

					// indexes are numbered oddly for different bank switching schemes
					if (index == 2)
						bank = chr_regs[6];
					if (index == 3)
						bank = chr_regs[7];

					return Vrom[(bank << 11) + (addr & 0x7FF)];
				} else
				{
					int index = (addr >> 10) & 0x7;
					int bank = chr_regs[index];
					bank |= ((bank & 0x30) << 4);
					return Vrom[(bank << 10) + (addr & 0x3FF)];
				}
				
			}

			return base.ReadPpu(addr);
		}

		public override byte ReadPrg(int addr)
		{
			if ((mode & 0x40)>0)
			{
				if (addr < 0x4000)
				{
					return Rom[((bank&0x3F) << 14) + (addr & 0x3FFF)];
				}

				return Rom[(((bank & 0x30) | 0xF) << 14) + (addr & 0x3FFF)];
			} else
			{
				int index = (addr >> 13) & 0x3;
				int bank = prg_regs[index];

				// last bank is fixed
				if (index == 3)
					bank = prg_bank_mask_8k;

				return Rom[(bank << 13) + (addr & 0x1FFF)];


			}
			
		}

		public override void ClockCpu()
		{
			if (IRQa)
			{
				IRQCount--;
				if (IRQCount==0)
				{
					IRQCount = 0xFFFF;
					IrqSignal = true;
					IRQa = false;
				}
			}
		}

		public override void WriteExp(int addr, byte value)
		{
			if (addr >= 0x1100 && addr <= 0x1103)
				_low[addr & 0x3] = value;
			else
				base.WriteExp(addr, value);
		}

		public override byte ReadExp(int addr)
		{
			if (addr == 0x1000)
				return (byte)((NES.DB & 0xFC) | 0);
			else if (addr >= 0x1100 && addr <= 0x1103)
				return _low[addr & 0x3];
			else
				return base.ReadExp(addr);

		}
	}

	internal sealed class ConyC : NesBoardBase
	{
		private byte[] prg_regs = new byte[2];
		private byte[] chr_regs = new byte[8];

		private int prg_bank_mask_16k;
		private int _irqCount;
		private bool _irqA, _irqEnable;

		public override bool Configure(EDetectionOrigin origin)
		{
			switch (Cart.BoardType)
			{
				case "MAPPER083":
					// We need one of the Cony boards to throw an error on an unexpected cart size, so we picked this one
					if (Cart.PrgSize != 128 && Cart.PrgSize != 256 && Cart.PrgSize != 1024)
					{
						throw new InvalidOperationException("Unexpected prg size of " + Cart.PrgSize + " for Mapper 83");
					}

					if (Cart.PrgSize == 1024)
					{
						prg_bank_mask_16k = Cart.PrgSize / 16 - 1;

						prg_regs[1] = (byte)prg_bank_mask_16k;
						return true;
					}
					return false;
				default:
					return false;
			}
		}

		public override void SyncState(Serializer ser)
		{
			base.SyncState(ser);
			ser.Sync(nameof(chr_regs), ref chr_regs, false);
			ser.Sync(nameof(prg_regs), ref prg_regs, false);
			ser.Sync(nameof(_irqCount), ref _irqCount);
			ser.Sync(nameof(_irqA), ref _irqA);
			ser.Sync(nameof(_irqEnable), ref _irqEnable);
		}

		public override void WritePrg(int addr, byte value)
		{
			if (addr == 0)
			{
				prg_regs[0] = (byte)(value & prg_bank_mask_16k);
			}

			else if (addr >= 0x310 && addr < 0x318)
			{
				chr_regs[addr & 0x7] = value;
			}

			else if (addr == 0x200)
			{
				_irqCount &= 0xFF00; _irqCount |= value;
				IrqSignal = false;
			}

			else if (addr == 0x201)
			{
				_irqCount &= 0xFF;
				_irqCount |= value << 8;
				_irqA = true;
			}
			else if (addr == 0x0100)
			{
				_irqEnable = value.Bit(7);
			}
		}

		public override byte ReadPpu(int addr)
		{
			if (addr < 0x2000)
			{
				int index = (addr >> 10) & 0x7;
				int bank = chr_regs[index];
				return Vrom[(bank << 10) + (addr & 0x3FF)];
			}

			return base.ReadPpu(addr);
		}

		public override byte ReadPrg(int addr)
		{
			if (addr < 0x4000)
			{
				return Rom[(prg_regs[0] << 14) + (addr & 0x3FFF)];
			}

			return Rom[(prg_regs[1] << 14) + (addr & 0x3FFF)];
		}

		public override void ClockCpu()
		{
			if (_irqA)
			{
				_irqCount--;
				if (_irqCount == 0)
				{
					_irqCount = 0xFFFF;
					IrqSignal = _irqEnable;
				}
			}
		}
	}
}
