#define MUSASHI

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

using BizHawk.Common.BufferExtensions;

using BizHawk.Emulation.Common;
using BizHawk.Emulation.Common.Components;
using BizHawk.Emulation.Cores.Components.M68000;
using BizHawk.Emulation.Cores.Components.Z80;
using Native68000;

namespace BizHawk.Emulation.Cores.Sega.Genesis
{
	[CoreAttributes(
		"GenesisHawk",
		"Vecna",
		isPorted: false,
		isReleased: false
		)]
	public sealed partial class Genesis : IEmulator, ISaveRam, IStatable, IInputPollable
	{
		private int _lagcount = 0;
		private bool lagged = true;
		private bool islag = false;

		// ROM
		public byte[] RomData;

		// Machine stuff
		public MC68000 MainCPU;
		public Z80A SoundCPU;
		public GenVDP VDP;
		public SN76489 PSG;
		public YM2612 YM2612;
		public byte[] Ram = new byte[0x10000];
		public byte[] Z80Ram = new byte[0x2000];

		private bool M68000HasZ80Bus = false;
		private bool Z80Reset = false;
		private bool Z80Runnable { get { return (Z80Reset == false && M68000HasZ80Bus == false); } }

		private SoundMixer SoundMixer;

		[FeatureNotImplemented]
		public IInputCallbackSystem InputCallbacks { get { throw new NotImplementedException(); } }

		public void ResetCounters()
		{
			Frame = 0;
			_lagcount = 0;
			islag = false;
		}

		// Genesis timings:
		// 53,693,175   Machine clocks / sec
		//  7,670,454   Main 68000 cycles / sec (7 mclk divisor)
		//  3,579,545   Z80 cycles / sec (15 mclk divisor)

		// At 59.92 FPS:
		//    896,081   mclks / frame
		//    128,011   Main 68000 cycles / frame
		//     59,738   Z80 cycles / frame

		// At 262 lines/frame: 
		//       3420   mclks / line
		//      ~ 488.5 Main 68000 cycles / line
		//        228   Z80 cycles / line

		// Video characteristics:
		// 224 lines are active display. The remaining 38 lines are vertical blanking.
		// In H40 mode, the dot clock is 480 pixels per line. 
		// 320 are active display, the remaining 160 are horizontal blanking.
		// A total of 3420 mclks per line, but 2560 mclks are active display and 860 mclks are blanking.

#if MUSASHI
		VdpCallback _vdp;
		ReadCallback read8;
		ReadCallback read16;
		ReadCallback read32;
		WriteCallback write8;
		WriteCallback write16;
		WriteCallback write32;
#endif

		public Genesis(CoreComm comm, GameInfo game, byte[] rom)
		{
			ServiceProvider = new BasicServiceProvider(this);
			CoreComm = comm;
			MainCPU = new MC68000();
			SoundCPU = new Z80A();
			YM2612 = new YM2612() { MaxVolume = 23405 };
			PSG = new SN76489() { MaxVolume = 4681 };
			VDP = new GenVDP();
			VDP.DmaReadFrom68000 = ReadWord;
			(ServiceProvider as BasicServiceProvider).Register<IVideoProvider>(VDP);
			SoundMixer = new SoundMixer(YM2612, PSG);

			MainCPU.ReadByte = ReadByte;
			MainCPU.ReadWord = ReadWord;
			MainCPU.ReadLong = ReadLong;
			MainCPU.WriteByte = WriteByte;
			MainCPU.WriteWord = WriteWord;
			MainCPU.WriteLong = WriteLong;
			MainCPU.IrqCallback = InterruptCallback;

			// ---------------------- musashi -----------------------
#if MUSASHI
			_vdp = vdpcallback;
			read8 = Read8;
			read16 = Read16;
			read32 = Read32;
			write8 = Write8;
			write16 = Write16;
			write32 = Write32;

			Musashi.RegisterVdpCallback(Marshal.GetFunctionPointerForDelegate(_vdp));
			Musashi.RegisterRead8(Marshal.GetFunctionPointerForDelegate(read8));
			Musashi.RegisterRead16(Marshal.GetFunctionPointerForDelegate(read16));
			Musashi.RegisterRead32(Marshal.GetFunctionPointerForDelegate(read32));
			Musashi.RegisterWrite8(Marshal.GetFunctionPointerForDelegate(write8));
			Musashi.RegisterWrite16(Marshal.GetFunctionPointerForDelegate(write16));
			Musashi.RegisterWrite32(Marshal.GetFunctionPointerForDelegate(write32));
#endif
			// ---------------------- musashi -----------------------

			SoundCPU.ReadMemory = ReadMemoryZ80;
			SoundCPU.WriteMemory = WriteMemoryZ80;
			SoundCPU.WriteHardware = (a, v) => { Console.WriteLine("Z80: Attempt I/O Write {0:X2}:{1:X2}", a, v); };
			SoundCPU.ReadHardware = x => 0xFF;
			SoundCPU.IRQCallback = () => SoundCPU.Interrupt = false;
			Z80Reset = true;
			RomData = new byte[0x400000];
			for (int i = 0; i < rom.Length; i++)
				RomData[i] = rom[i];

			SetupMemoryDomains();
#if MUSASHI
			Musashi.Init();
			Musashi.Reset();
			VDP.GetPC = () => Musashi.PC;
#else
			MainCPU.Reset();
			VDP.GetPC = () => MainCPU.PC;
#endif
			InitializeCartHardware(game);
		}

		public IEmulatorServiceProvider ServiceProvider { get; private set; }

		void InitializeCartHardware(GameInfo game)
		{
			LogCartInfo();
			InitializeEeprom(game);
			InitializeSaveRam(game);
		}

		public void FrameAdvance(bool render, bool rendersound)
		{
			lagged = true;
			Frame++;
			PSG.BeginFrame(SoundCPU.TotalExecutedCycles);
			YM2612.BeginFrame(SoundCPU.TotalExecutedCycles);

			// Do start-of-frame events
			VDP.HIntLineCounter = VDP.Registers[10];
			//VDP.VdpStatusWord &= 
			unchecked { VDP.VdpStatusWord &= (ushort)~GenVDP.StatusVerticalBlanking; }

			for (VDP.ScanLine = 0; VDP.ScanLine < 262; VDP.ScanLine++)
			{
				//Log.Error("VDP","FRAME {0}, SCANLINE {1}", Frame, VDP.ScanLine);

				if (VDP.ScanLine < VDP.FrameHeight)
					VDP.RenderLine();

				Exec68k(365);
				RunZ80(171);

				// H-Int now?

				VDP.HIntLineCounter--;
				if (VDP.HIntLineCounter < 0 && VDP.ScanLine < 224) // FIXME
				{
					VDP.HIntLineCounter = VDP.Registers[10];
					VDP.VdpStatusWord |= GenVDP.StatusHorizBlanking;

					if (VDP.HInterruptsEnabled)
					{
						Set68kIrq(4);
						//Console.WriteLine("Fire hint!");
					}

				}

				Exec68k(488 - 365);
				RunZ80(228 - 171);

				if (VDP.ScanLine == 224)
				{
					VDP.VdpStatusWord |= GenVDP.StatusVerticalInterruptPending;
					VDP.VdpStatusWord |= GenVDP.StatusVerticalBlanking;
					Exec68k(16); // this is stupidly wrong.
					// End-frame stuff
					if (VDP.VInterruptEnabled)
						Set68kIrq(6);

					SoundCPU.Interrupt = true;
					//The INT output is asserted every frame for exactly one scanline, and it can't be disabled. A very short Z80 interrupt routine would be triggered multiple times if it finishes within 228 Z80 clock cycles. I think (but cannot recall the specifics) that some games have delay loops in the interrupt handler for this very reason. 
				}
			}
			PSG.EndFrame(SoundCPU.TotalExecutedCycles);
			YM2612.EndFrame(SoundCPU.TotalExecutedCycles);



			if (lagged)
			{
				_lagcount++;
				islag = true;
			}
			else
				islag = false;
		}

		void Exec68k(int cycles)
		{
#if MUSASHI
			Musashi.Execute(cycles);
#else
			MainCPU.ExecuteCycles(cycles);
#endif
		}

		void RunZ80(int cycles)
		{
			// I emulate the YM2612 synced to Z80 clock, for better or worse.
			// So we still need to keep the Z80 cycle count accurate even if the Z80 isn't running.

			if (Z80Runnable)
				SoundCPU.ExecuteCycles(cycles);
			else
				SoundCPU.TotalExecutedCycles += cycles;
		}

		void Set68kIrq(int irq)
		{
#if MUSASHI
			Musashi.SetIRQ(irq);
#else
			MainCPU.Interrupt = irq;
#endif
		}

		public IDictionary<string, RegisterValue> GetCpuFlagsAndRegisters()
		{
			return new Dictionary<string, RegisterValue>
			{
				{ "A-0", MainCPU.A[0].s32 },
				{ "A-1", MainCPU.A[1].s32 },
				{ "A-2", MainCPU.A[2].s32 },
				{ "A-3", MainCPU.A[3].s32 },
				{ "A-4", MainCPU.A[4].s32 },
				{ "A-5", MainCPU.A[5].s32 },
				{ "A-6", MainCPU.A[6].s32 },
				{ "A-7", MainCPU.A[7].s32 },

				{ "D-0", MainCPU.D[0].s32 },
				{ "D-1", MainCPU.D[1].s32 },
				{ "D-2", MainCPU.D[2].s32 },
				{ "D-3", MainCPU.D[3].s32 },
				{ "D-4", MainCPU.D[4].s32 },
				{ "D-5", MainCPU.D[5].s32 },
				{ "D-6", MainCPU.D[6].s32 },
				{ "D-7", MainCPU.D[7].s32 },

				{ "SR", MainCPU.SR },

				{ "Flag X", MainCPU.X },
				{ "Flag N", MainCPU.N },
				{ "Flag Z", MainCPU.Z },
				{ "Flag V", MainCPU.V },
				{ "Flag C", MainCPU.C }
			};
		}

		int vdpcallback(int level) // Musashi handler
		{
			InterruptCallback(level);
			return -1;
		}

		void InterruptCallback(int level)
		{
			unchecked { VDP.VdpStatusWord &= (ushort)~GenVDP.StatusVerticalInterruptPending; }
		}

		public CoreComm CoreComm { get; private set; }

		public ISoundProvider SoundProvider
		{
			get { return SoundMixer; }
		}
		public ISyncSoundProvider SyncSoundProvider { get { return new FakeSyncSound(SoundMixer, 735); } }
		public bool StartAsyncSound() { return true; }
		public void EndAsyncSound() { }

		public int Frame { get; set; }
		public int LagCount { get { return _lagcount; } set { _lagcount = value; } }
		public bool IsLagFrame { get { return islag; } }
		public bool DeterministicEmulation { get { return true; } }
		public string SystemId { get { return "GEN"; } }

		public string BoardName { get { return null; } }

		public void SaveStateText(TextWriter writer)
		{
			var buf = new byte[141501 + SaveRAM.Length];
			var stream = new MemoryStream(buf);
			var bwriter = new BinaryWriter(stream);
			SaveStateBinary(bwriter);

			writer.WriteLine("Version 1");
			writer.Write("BigFatBlob ");
			buf.SaveAsHex(writer);

			/*writer.WriteLine("[MegaDrive]");
			MainCPU.SaveStateText(writer, "Main68K");
			SoundCPU.SaveStateText(writer);
			PSG.SaveStateText(writer);
			VDP.SaveStateText(writer);
			writer.WriteLine("Frame {0}", Frame);
			writer.WriteLine("Lag {0}", _lagcount);
			writer.WriteLine("IsLag {0}", islag);
			writer.Write("MainRAM ");
			Ram.SaveAsHex(writer);
			writer.Write("Z80RAM ");
			Z80Ram.SaveAsHex(writer);
			writer.WriteLine("[/MegaDrive]");*/
		}

		public void LoadStateText(TextReader reader)
		{
			var buf = new byte[141501 + SaveRAM.Length];
			var version = reader.ReadLine();
			if (version != "Version 1")
				throw new Exception("Not a valid state vesrion! sorry! your state is bad! Robust states will be added later!");
			var omgstate = reader.ReadLine().Split(' ')[1];
			buf.ReadFromHex(omgstate);
			LoadStateBinary(new BinaryReader(new MemoryStream(buf)));

			/*while (true)
			{
				string[] args = reader.ReadLine().Split(' ');
				if (args[0].Trim() == "") continue;
				if (args[0] == "[MegaDrive]") continue;
				if (args[0] == "[/MegaDrive]") break;
				if (args[0] == "MainRAM")
					Ram.ReadFromHex(args[1]);
				else if (args[0] == "Z80RAM")
					Z80Ram.ReadFromHex(args[1]);
				else if (args[0] == "[Main68K]")
					MainCPU.LoadStateText(reader, "Main68K");
				else if (args[0] == "[Z80]")
					SoundCPU.LoadStateText(reader);
				else if (args[0] == "Frame")
					Frame = int.Parse(args[1]);
				else if (args[0] == "Lag")
					_lagcount = int.Parse(args[1]);
				else if (args[0] == "IsLag")
					islag = bool.Parse(args[1]);
				else if (args[0] == "[PSG]")
					PSG.LoadStateText(reader);
				else if (args[0] == "[VDP]")
					VDP.LoadStateText(reader);
				else
					Console.WriteLine("Skipping unrecognized identifier " + args[0]);
			}*/
		}

		public void SaveStateBinary(BinaryWriter writer)
		{
			Musashi.SaveStateBinary(writer);    // 124  
			//SoundCPU.SaveStateBinary(writer);   // 46     TODO fix this, there is no way to invoke this core from the UI for testing anyway.
			//PSG.SaveStateBinary(writer);        // 15
			VDP.SaveStateBinary(writer);        // 65781
			YM2612.SaveStateBinary(writer);     // 1785

			writer.Write(Ram);                  // 65535
			writer.Write(Z80Ram);               // 8192

			writer.Write(Frame);                // 4
			writer.Write(M68000HasZ80Bus);      // 1
			writer.Write(Z80Reset);             // 1
			writer.Write(BankRegion);           // 4

			for (int i = 0; i < 3; i++)
			{
				writer.Write(IOPorts[i].Data);
				writer.Write(IOPorts[i].TxData);
				writer.Write(IOPorts[i].RxData);
				writer.Write(IOPorts[i].SCtrl);
			}

			if (SaveRAM.Length > 0)
				writer.Write(SaveRAM);

			// TODO: EEPROM/cart HW state
			// TODO: lag counter crap
		}

		public void LoadStateBinary(BinaryReader reader)
		{
			Musashi.LoadStateBinary(reader);
			//SoundCPU.LoadStateBinary(reader);
			//PSG.LoadStateBinary(reader); 
			VDP.LoadStateBinary(reader);
			YM2612.LoadStateBinary(reader);

			Ram = reader.ReadBytes(Ram.Length);
			Z80Ram = reader.ReadBytes(Z80Ram.Length);

			Frame = reader.ReadInt32();
			M68000HasZ80Bus = reader.ReadBoolean();
			Z80Reset = reader.ReadBoolean();
			BankRegion = reader.ReadInt32();

			for (int i = 0; i < 3; i++)
			{
				IOPorts[i].Data = reader.ReadByte();
				IOPorts[i].TxData = reader.ReadByte();
				IOPorts[i].RxData = reader.ReadByte();
				IOPorts[i].SCtrl = reader.ReadByte();
			}

			if (SaveRAM.Length > 0)
				SaveRAM = reader.ReadBytes(SaveRAM.Length);
		}

		public byte[] SaveStateBinary()
		{
			var buf = new byte[141501 + SaveRAM.Length];
			var stream = new MemoryStream(buf);
			var writer = new BinaryWriter(stream);
			SaveStateBinary(writer);
			//Console.WriteLine("buf len = {0}", stream.Position);
			writer.Close();
			return buf;
		}

		public bool BinarySaveStatesPreferred { get { return false; } }

		MemoryDomainList memoryDomains;

		void SetupMemoryDomains()
		{
			var domains = new List<MemoryDomain>(3);
			var MainMemoryDomain = new MemoryDomain("Main RAM", Ram.Length, MemoryDomain.Endian.Big,
				addr => Ram[addr & 0xFFFF],
				(addr, value) => Ram[addr & 0xFFFF] = value);
			var Z80Domain = new MemoryDomain("Z80 RAM", Z80Ram.Length, MemoryDomain.Endian.Little,
				addr => Z80Ram[addr & 0x1FFF],
				(addr, value) => { Z80Ram[addr & 0x1FFF] = value; });

			var VRamDomain = new MemoryDomain("Video RAM", VDP.VRAM.Length, MemoryDomain.Endian.Big,
				addr => VDP.VRAM[addr & 0xFFFF],
				(addr, value) => VDP.VRAM[addr & 0xFFFF] = value);

			var RomDomain = new MemoryDomain("MD CART", RomData.Length, MemoryDomain.Endian.Big,
				addr => RomData[addr], //adelikat: For speed considerations, I didn't mask this, every tool that uses memory domains is smart enough not to overflow, if I'm wrong let me know!
				(addr, value) => RomData[addr & (RomData.Length - 1)] = value);

			var SystemBusDomain = new MemoryDomain("System Bus", 0x1000000, MemoryDomain.Endian.Big,
				addr => (byte)ReadByte((int)addr),
				(addr, value) => Write8((uint)addr, (uint)value));

			domains.Add(MainMemoryDomain);
			domains.Add(Z80Domain);
			domains.Add(VRamDomain);
			domains.Add(RomDomain);
			domains.Add(SystemBusDomain);
			memoryDomains = new MemoryDomainList(domains);
			(ServiceProvider as BasicServiceProvider).Register<IMemoryDomains>(memoryDomains);
		}

		public void Dispose() { }
	}
}
