using BizHawk.Common;
using BizHawk.Emulation.Cores.Components.Z80A;

namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
	/// <summary>
	/// An intermediary class that manages cycling the ULA and CPU
	/// along with inherent Port and Memory contention
	/// </summary>
	public class CPUMonitor
	{
		private readonly SpectrumBase _machine;
		private readonly Z80A _cpu;
		public MachineType machineType = MachineType.ZXSpectrum48;

		/// <summary>
		/// CPU total executes t-states
		/// </summary>
		public long TotalExecutedCycles => _cpu.TotalExecutedCycles;

		/// <summary>
		/// Current BUSRQ line array
		/// </summary>
		public ushort BUSRQ
		{
			get
			{
				switch (machineType)
				{
					case MachineType.ZXSpectrum128Plus2a:
					case MachineType.ZXSpectrum128Plus3:
						return _cpu.MEMRQ[_cpu.bus_pntr];
					default:
						return _cpu.BUSRQ[_cpu.mem_pntr];
				}
			}
		}

		public CPUMonitor(SpectrumBase machine)
		{
			_machine = machine;
			_cpu = _machine.CPU;
		}

		/// <summary>
		/// The last 16-bit port address that was detected
		/// </summary>
		public ushort lastPortAddr;

		/// <summary>
		/// If true, the next read memory operation has been contended
		/// </summary>
		public bool NextMemReadContended;

		/// <summary>
		/// Handles the ULA and CPU cycle clocks, along with any memory and port contention
		/// </summary>
		public void ExecuteCycle()
		{
			// simulate the ULA clock cycle before the CPU cycle
			_machine.ULADevice.CycleClock(TotalExecutedCycles);

			// is the next CPU cycle causing a BUSRQ or IORQ?
			if (BUSRQ > 0)
			{
				// check for IORQ
				if (!CheckIO())
				{
					// is the memory address of the BUSRQ potentially contended?
					if (_machine.IsContended(AscertainBUSRQAddress()))
					{
						var cont = _machine.ULADevice.GetContentionValue((int)_machine.CurrentFrameCycle);
						if (cont > 0)
						{
							_cpu.TotalExecutedCycles += cont;
							NextMemReadContended = true;
						}
					}
				}
			}

			_cpu.ExecuteOne();
		}

		/// <summary>
		/// Looks up the current BUSRQ address that is about to be signalled on the upcoming cycle
		/// </summary>
		private ushort AscertainBUSRQAddress()
		{
			ushort addr = 0;
			switch (BUSRQ)
			{
				// PCh
				case 1:
					addr = (ushort)(_cpu.Regs[_cpu.PCl] | _cpu.Regs[_cpu.PCh] << 8);
					break;
				// SPh
				case 3:
					addr = (ushort)(_cpu.Regs[_cpu.SPl] | _cpu.Regs[_cpu.SPh] << 8);
					break;
				// A
				case 4:
					addr = (ushort)(_cpu.Regs[_cpu.F] | _cpu.Regs[_cpu.A] << 8);
					break;
				// B
				case 6:
					addr = (ushort)(_cpu.Regs[_cpu.C] | _cpu.Regs[_cpu.B] << 8);
					break;
				// D
				case 8:
					addr = (ushort)(_cpu.Regs[_cpu.E] | _cpu.Regs[_cpu.D] << 8);
					break;
				// H
				case 10:
					addr = (ushort)(_cpu.Regs[_cpu.L] | _cpu.Regs[_cpu.H] << 8);
					break;
				// W
				case 12:
					addr = (ushort)(_cpu.Regs[_cpu.Z] | _cpu.Regs[_cpu.W] << 8);
					break;
				// Ixh
				case 16:
					addr = (ushort)(_cpu.Regs[_cpu.Ixl] | _cpu.Regs[_cpu.Ixh] << 8);
					break;
				// Iyh
				case 18:
					addr = (ushort)(_cpu.Regs[_cpu.Iyl] | _cpu.Regs[_cpu.Iyh] << 8);
					break;
				// I
				case 21:
					addr = (ushort)(_cpu.Regs[_cpu.R] | _cpu.Regs[_cpu.I] << 8);
					break;
				// BC
				case Z80A.BIO1:
				case Z80A.BIO2:
				case Z80A.BIO3:
				case Z80A.BIO4:
					addr = (ushort)(_cpu.Regs[_cpu.C] | _cpu.Regs[_cpu.B] << 8);
					break;
				// WZ
				case Z80A.WIO1:
				case Z80A.WIO2:
				case Z80A.WIO3:
				case Z80A.WIO4:
					addr = (ushort)(_cpu.Regs[_cpu.Z] | _cpu.Regs[_cpu.W] << 8);
					break;
			}

			return addr;
		}

		/// <summary>
		/// Running every cycle, this determines whether the upcoming BUSRQ is for an IO operation
		/// Also processes any contention
		/// </summary>
		private bool CheckIO()
		{
			bool isIO = false;

			switch (BUSRQ)
			{
				// BC: T1
				case Z80A.BIO1:
					lastPortAddr = AscertainBUSRQAddress();
					isIO = true;
					if (IsIOCycleContended(1))
						_cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle);
					break;
				// BC: T2
				case Z80A.BIO2:
					lastPortAddr = AscertainBUSRQAddress();
					isIO = true;
					if (IsIOCycleContended(2))
						_cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle);
					break;
				// BC: T3
				case Z80A.BIO3:
					lastPortAddr = AscertainBUSRQAddress();
					isIO = true;
					if (IsIOCycleContended(3))
						_cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle);
					break;
				// BC: T4
				case Z80A.BIO4:
					lastPortAddr = AscertainBUSRQAddress();
					isIO = true;
					if (IsIOCycleContended(4))
						_cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle);
					break;

				// WZ: T1
				case Z80A.WIO1:
					lastPortAddr = AscertainBUSRQAddress();
					isIO = true;
					if (IsIOCycleContended(1))
						_cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle);
					break;
				// WZ: T2
				case Z80A.WIO2:
					lastPortAddr = AscertainBUSRQAddress();
					isIO = true;
					if (IsIOCycleContended(2))
						_cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle);
					break;
				// WZ: T3
				case Z80A.WIO3:
					lastPortAddr = AscertainBUSRQAddress();
					isIO = true;
					if (IsIOCycleContended(3))
						_cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle);
					break;
				// WZ: T4
				case Z80A.WIO4:
					lastPortAddr = AscertainBUSRQAddress();
					isIO = true;
					if (IsIOCycleContended(4))
						_cpu.TotalExecutedCycles += _machine.ULADevice.GetPortContentionValue((int)_machine.CurrentFrameCycle);
					break;
			}

			return isIO;
		}

		/// <summary>
		/// Returns TRUE if the supplied T-cycle within an IO operation has the possibility of being contended
		/// This can be different based on the emulated ZX Spectrum model
		/// </summary>
		private bool IsIOCycleContended(int T)
		{
			bool lowBitSet = (lastPortAddr & 0x0001) != 0;
			bool highByte407f = false;

			switch (machineType)
			{
				case MachineType.ZXSpectrum16:
				case MachineType.ZXSpectrum48:

					if ((lastPortAddr & 0xc000) == 0x4000)
						highByte407f = true;

					if (highByte407f)
					{
						// high byte 40-7f
						if (lowBitSet)
						{
							// high byte 40-7f
							// low bit set
							// C:1, C:1, C:1, C:1
							switch (T)
							{
								case 1:
								case 2:
								case 3:
								case 4:
									return true;
							}
						}
						else
						{
							// high byte 40-7f
							// low bit reset
							// C:1, C:3
							switch (T)
							{
								case 1:
								case 2:
									return true;
							}
						}
					}
					else
					{
						// high byte not 40-7f
						if (lowBitSet)
						{
							// high byte not 40-7f
							// low bit set
							// N:4                            
						}
						else
						{
							// high byte not 40-7f
							// low bit reset
							// N:1, C:3
							switch (T)
							{
								case 2:
									return true;
							}
						}
					}
					break;

				case MachineType.ZXSpectrum128:
				case MachineType.ZXSpectrum128Plus2:
					if ((lastPortAddr & 0xc000) == 0x4000 || (lastPortAddr & 0xc000) == 0xc000 && _machine.ContendedBankPaged())
						highByte407f = true;

					if (highByte407f)
					{
						// high byte 40-7f
						if (lowBitSet)
						{
							// high byte 40-7f
							// low bit set
							// C:1, C:1, C:1, C:1
							switch (T)
							{
								case 1:
								case 2:
								case 3:
								case 4:
									return true;
							}
						}
						else
						{
							// high byte 40-7f
							// low bit reset
							// C:1, C:3
							switch (T)
							{
								case 1:
								case 2:
									return true;
							}
						}
					}
					else
					{
						// high byte not 40-7f
						if (lowBitSet)
						{
							// high byte not 40-7f
							// low bit set
							// N:4                            
						}
						else
						{
							// high byte not 40-7f
							// low bit reset
							// N:1, C:3
							switch (T)
							{
								case 2:
									return true;
							}
						}
					}
					break;

				case MachineType.ZXSpectrum128Plus2a:
				case MachineType.ZXSpectrum128Plus3:
					// No contention occurs as the ULA only applies contention when the Z80 MREQ line is active
					// (which is not during an IO operation)
					break;
			}

			return false;
		}

		/// <summary>
		/// Called when the first byte of an instruction is fetched
		/// </summary>
		public void OnExecFetch(ushort firstByte)
		{
			// fetch instruction without incrementing pc
			//_cpu.FetchInstruction(_cpu.FetchMemory(firstByte));

		}

		public void SyncState(Serializer ser)
		{
			ser.BeginSection(nameof(CPUMonitor));
			ser.Sync(nameof(lastPortAddr), ref lastPortAddr);
			ser.Sync(nameof(NextMemReadContended), ref NextMemReadContended);
			ser.EndSection();
		}
	}
}
