using System;
using System.IO;
using System.Globalization;

using BizHawk.Common;
using BizHawk.Common.BufferExtensions;
using BizHawk.Common.IOExtensions;
using BizHawk.Emulation.Common;

namespace BizHawk.Emulation.Cores.Sega.Genesis
{
	public sealed partial class GenVDP : IVideoProvider
	{
		// Memory
		public byte[] VRAM = new byte[0x10000];
		public ushort[] CRAM = new ushort[64];
		public ushort[] VSRAM = new ushort[40];
		public byte[] Registers = new byte[0x20];

		public byte[] PatternBuffer = new byte[0x20000];
		public int[] Palette = new int[64];
		public int[] FrameBuffer = new int[320 * 224];
		public int FrameWidth = 320;
		public int FrameHeight = 224;

		public int ScanLine;
		public int HIntLineCounter;

		public bool HInterruptsEnabled { get { return (Registers[0] & 0x10) != 0; } }
		public bool DisplayEnabled { get { return (Registers[1] & 0x40) != 0; } }
		public bool VInterruptEnabled { get { return (Registers[1] & 0x20) != 0; } }
		public bool DmaEnabled { get { return (Registers[1] & 0x10) != 0; } }
		public bool CellBasedVertScroll { get { return (Registers[11] & 0x04) != 0; } }

		public bool InDisplayPeriod { get { return ScanLine < 224 && DisplayEnabled; } }

		ushort NameTableAddrA;
		ushort NameTableAddrB;
		ushort NameTableAddrWindow;
		ushort SpriteAttributeTableAddr;
		ushort HScrollTableAddr;
		int NameTableWidth = 32;
		int NameTableHeight = 32;

		int DisplayWidth;
		int SpriteLimit;
		int SpritePerLineLimit;
		int DotsPerLineLimit;

		bool ControlWordPending;
		ushort VdpDataAddr;
		byte VdpDataCode;

		const int CommandVramRead = 0;
		const int CommandVramWrite = 1;
		const int CommandCramWrite = 3;
		const int CommandVsramRead = 4;
		const int CommandVsramWrite = 5;
		const int CommandCramRead = 8;

		public ushort VdpStatusWord = 0x3400;
		public const int StatusHorizBlanking = 0x04;
		public const int StatusVerticalBlanking = 0x08;
		public const int StatusOddFrame = 0x10;
		public const int StatusSpriteCollision = 0x20;
		public const int StatusSpriteOverflow = 0x40;
		public const int StatusVerticalInterruptPending = 0x80;

		public bool VdpDebug = false;

		public Func<int> GetPC;

		public GenVDP()
		{
			WriteVdpRegister(00, 0x04);
			WriteVdpRegister(01, 0x04);
			WriteVdpRegister(02, 0x30);
			WriteVdpRegister(03, 0x3C);
			WriteVdpRegister(04, 0x07);
			WriteVdpRegister(05, 0x67);
			WriteVdpRegister(10, 0xFF);
			WriteVdpRegister(12, 0x81);
			WriteVdpRegister(15, 0x02);
			Log.Note("VDP", "VDP init routine complete");
		}

		public ushort ReadVdp(int addr)
		{
			switch (addr)
			{
				case 0:
				case 2:
					return ReadVdpData();
				case 4:
				case 6:
					return ReadVdpControl();
				default:
					return ReadHVCounter();
			}
		}

		public void WriteVdp(int addr, ushort data)
		{
			switch (addr)
			{
				case 0:
				case 2:
					WriteVdpData(data);
					return;
				case 4:
				case 6:
					WriteVdpControl(data);
					return;
			}
		}

		public void WriteVdpControl(ushort data)
		{
			Log.Note("VDP", "Control Write {0:X4} (PC={1:X6})", data, GetPC());

			if (ControlWordPending == false)
			{
				if ((data & 0xC000) == 0x8000)
				{
					int reg = (data >> 8) & 0x1F;
					byte value = (byte)(data & 0xFF);
					WriteVdpRegister(reg, value);
					VdpDataCode = 0;
				}
				else
				{
					ControlWordPending = true;
					VdpDataAddr &= 0xC000;
					VdpDataAddr |= (ushort)(data & 0x3FFF);
					VdpDataCode &= 0x3C;
					VdpDataCode |= (byte)(data >> 14);
					//Console.WriteLine("Address = {0:X4}", VdpDataAddr);
					//Console.WriteLine("Code = {0:X2}", VdpDataCode);
				}
			}
			else
			{
				ControlWordPending = false;

				// Update data address and code
				VdpDataAddr &= 0x3FFF;
				VdpDataAddr |= (ushort)((data & 0x03) << 14);
				//Console.WriteLine("Address = {0:X4}", VdpDataAddr);
				VdpDataCode &= 0x03;
				VdpDataCode |= (byte)((data >> 2) & 0x3C);
				//Log.Note("VDP", "Code = {0:X2}", VdpDataCode);

				if ((VdpDataCode & 0x20) != 0 && DmaEnabled) // DMA triggered
				{
					//Console.WriteLine("DMA TIME!");

					// what type of DMA?
					switch (Registers[23] >> 6)
					{
						case 2:
							Log.Note("VDP", "VRAM FILL");
							DmaFillModePending = true;
							break;
						case 3:
							Log.Error("VDP", "VRAM COPY *");
							ExecuteVramVramCopy();
							break;
						default:
							Execute68000VramCopy();
							break;
					}
				}
			}
		}

		public ushort ReadVdpControl()
		{
			VdpStatusWord |= 0x0200; // Fifo empty // TODO kill this, emulating the damn FIFO.
			ControlWordPending = false; // Hmm.. if this happens in an interrupt between 1st and 2nd word..

			// sprite overflow flag should clear.
			// sprite collision flag should clear.

			return VdpStatusWord;
		}

		public void WriteVdpData(ushort data)
		{
			Log.Note("VDP", "Data port write: {0:X4} (PC={1:X6})", data, GetPC());
			ControlWordPending = false;

			// byte-swap incoming data when A0 is set
			if ((VdpDataAddr & 1) != 0)
			{
				data = (ushort)((data >> 8) | (data << 8));
				Log.Error("VDP", "VRAM byte-swap is happening because A0 is not 0. [{0:X4}] = {1:X4}", VdpDataAddr, data);
			}

			switch (VdpDataCode & 0xF)
			{
				case CommandVramWrite: // VRAM Write
					VRAM[VdpDataAddr & 0xFFFE] = (byte)data;
					VRAM[(VdpDataAddr & 0xFFFE) + 1] = (byte)(data >> 8);
					//if (VdpDebug)
					Log.Note("VDP", "VRAM[{0:X4}] = {1:X4}", VdpDataAddr, data);
					UpdatePatternBuffer(VdpDataAddr & 0xFFFE);
					UpdatePatternBuffer((VdpDataAddr & 0xFFFE) + 1);
					VdpDataAddr += Registers[0x0F];
					break;
				case CommandCramWrite: // CRAM write
					CRAM[(VdpDataAddr / 2) % 64] = data;
					//if (VdpDebug)
					Log.Note("VDP", "CRAM[{0:X2}] = {1:X4}", (VdpDataAddr / 2) % 64, data);
					ProcessPalette((VdpDataAddr / 2) % 64);
					VdpDataAddr += Registers[0x0F];
					break;
				case CommandVsramWrite: // VSRAM write
					VSRAM[(VdpDataAddr / 2) % 40] = data;
					//if (VdpDebug)
					Log.Note("VDP", "VSRAM[{0:X2}] = {1:X4}", (VdpDataAddr / 2) % 40, data);
					VdpDataAddr += Registers[0x0F];
					break;
				default:
					Log.Error("VPD", "VDP DATA WRITE WITH UNHANDLED CODE!!! {0}", VdpDataCode & 7);
					break;
			}

			if (DmaFillModePending)
			{
				ExecuteVramFill(data);
			}
		}

		public ushort ReadVdpData()
		{
			int orig_addr = VdpDataAddr;
			ushort retval = 0xBEEF;
			switch (VdpDataCode & 0x0F)
			{
				case CommandVramRead:
					//if ((VdpDataAddr & 1) != 0) throw new Exception("VRAM read is not word-aligned. what do?");
					retval = VRAM[VdpDataAddr & 0xFFFE];
					retval |= (ushort)(VRAM[(VdpDataAddr & 0xFFFE) + 1] << 8);
					VdpDataAddr += Registers[0x0F];
					break;
				case CommandVsramRead:
					retval = VSRAM[(VdpDataAddr / 2) % 40];
					VdpDataAddr += Registers[0x0F];
					return retval;
				case CommandCramRead:
					retval = CRAM[(VdpDataAddr / 2) % 64];
					VdpDataAddr += Registers[0x0F];
					return retval;
				default:
					throw new Exception("VRAM read with unexpected code!!! " + (VdpDataCode & 0x0F));
			}

			Log.Note("VDP", "VDP Data Read from {0:X4} returning {1:X4}", orig_addr, retval);
			return retval;
		}

		ushort ReadHVCounter()
		{
			int vcounter = ScanLine;
			if (vcounter > 0xEA)
				vcounter -= 7;
			// TODO generalize this across multiple video modes and stuff.

			// TODO dont tie this to musashi cycle count.
			// Figure out a "clean" way to get cycle counter information available to VDP.
			// Oh screw that. The VDP and the cpu cycle counters are going to be intertwined pretty tightly.
			int hcounter = (488 - Native68000.Musashi.GetCyclesRemaining()) * 255 / 488;
			// FIXME: totally utterly wrong.

			ushort res = (ushort)((vcounter << 8) | (hcounter & 0xFF));
			//Console.WriteLine("READ HVC: V={0:X2} H={1:X2}  ret={2:X4}", vcounter, hcounter, res);

			return res;
		}

		public void WriteVdpRegister(int register, byte data)
		{
			//if (VdpDebug)
			Log.Note("VDP", "Register {0}: {1:X2}", register, data);
			switch (register)
			{
				case 0x00: // Mode Set Register 1
					Registers[register] = data;
					//if (VdpDebug)
					//Log.Note("VDP", "HINT enabled: " + HInterruptsEnabled);
					break;

				case 0x01: // Mode Set Register 2
					//if (VdpDebug)
					//{
					//    Registers[register] = data;
					//    Log.Note("VDP", "DisplayEnabled: " + DisplayEnabled);
					//    Log.Note("VDP", "DmaEnabled: " + DmaEnabled);
					//    Log.Note("VDP", "VINT enabled: " + VInterruptEnabled);
					//}
					break;

				case 0x02: // Name Table Address for Layer A
					NameTableAddrA = (ushort)((data & 0x38) << 10);
					//if (VdpDebug)
					//Log.Note("VDP", "SET NTa A = {0:X4}", NameTableAddrA);
					break;

				case 0x03: // Name Table Address for Window
					NameTableAddrWindow = (ushort)((data & 0x3E) << 10);
					//if (VdpDebug)
					//Log.Note("VDP", "SET NTa W = {0:X4}", NameTableAddrWindow);
					break;

				case 0x04: // Name Table Address for Layer B
					NameTableAddrB = (ushort)(data << 13);
					//if (VdpDebug)
					//Log.Note("VDP", "SET NTa B = {0:X4}", NameTableAddrB);
					break;

				case 0x05: // Sprite Attribute Table Address
					SpriteAttributeTableAddr = (ushort)(data << 9);
					//if (VdpDebug)
					//Log.Note("VDP", "SET SAT attr = {0:X4}", SpriteAttributeTableAddr);
					break;

				case 0x0A: // H Interrupt Register
					//if (VdpDebug)
					//Log.Note("VDP", "HInt occurs every {0} lines.", data);
					break;

				case 0x0B: // VScroll/HScroll modes
					//if (VdpDebug)
					//{
					//    if ((data & 4) != 0)
					//        Log.Note("VDP", "VSCroll Every 2 Cells Enabled");
					//    else
					//        Log.Note("VDP", "Full Screen VScroll");

					//    int hscrollmode = data & 3;
					//    switch (hscrollmode)
					//    {
					//        case 0: Log.Note("VDP", "Full Screen HScroll"); break;
					//        case 1: Log.Note("VDP", "Prohibited HSCROLL mode!!!  But it'll work."); break;
					//        case 2: Log.Note("VDP", "HScroll every 1 cell"); break;
					//        case 3: Log.Note("VDP", "HScroll every line"); break;
					//    }
					//}
					break;

				case 0x0C: // Mode Set #4
					// TODO interlaced modes
					if ((data & 0x81) == 0)
					{
						// Display is 32 cells wide
						if (DisplayWidth != 32)
						{
							FrameBuffer = new int[256 * 224];
							FrameWidth = 256;
							DisplayWidth = 32;
							SpriteLimit = 64;
							SpritePerLineLimit = 16;
							DotsPerLineLimit = 256;
						}
					}
					else
					{
						// Display is 40 cells wide
						if (DisplayWidth != 40)
						{
							FrameBuffer = new int[320 * 224];
							FrameWidth = 320;
							DisplayWidth = 40;
							SpriteLimit = 80;
							SpritePerLineLimit = 20;
							DotsPerLineLimit = 320;
						}
					}
					break;

				case 0x0D: // H Scroll Table Address
					HScrollTableAddr = (ushort)(data << 10);
					//if (VdpDebug)
					//Log.Note("VDP", "SET HScrollTab attr = {0:X4}", HScrollTableAddr);
					break;

				case 0x0F: // Auto Address Register Increment
					//if (VdpDebug)
					//Log.Note("VDP", "Set Data Increment to " + data);
					break;

				case 0x10: // Nametable Dimensions
					switch (data & 0x03)
					{
						case 0: NameTableWidth = 32; break;
						case 1: NameTableWidth = 64; break;
						case 2: NameTableWidth = 32; break; // invalid setting
						case 3: NameTableWidth = 128; break;
					}
					switch ((data >> 4) & 0x03)
					{
						case 0: NameTableHeight = 32; break;
						case 1: NameTableHeight = 64; break;
						case 2: NameTableHeight = 32; break; // invalid setting
						case 3: NameTableHeight = 128; break;
					}
					break;

				case 0x11: // Window H Position
					int whp = data & 31;
					bool fromright = (data & 0x80) != 0;
					//if (VdpDebug)
					//Log.Note("VDP", "Window H is {0} units from {1}", whp, fromright ? "right" : "left");
					break;

				case 0x12: // Window V
					whp = data & 31;
					fromright = (data & 0x80) != 0;
					//if (VdpDebug)
					//Log.Note("VDP", "Window V is {0} units from {1}", whp, fromright ? "lower" : "upper");
					break;

				case 0x13: // DMA Length Low
					Registers[register] = data;
					//Log.Note("VDP", "DMA Length = {0:X4}", DmaLength);
					break;

				case 0x14: // DMA Length High
					Registers[register] = data;
					//Log.Note("VDP", "DMA Length = {0:X4}", DmaLength);
					break;

				case 0x15: // DMA Source Low
					Registers[register] = data;
					//Log.Note("VDP", "DMA Source = {0:X6}", DmaSource);
					break;
				case 0x16: // DMA Source Mid
					Registers[register] = data;
					//Log.Note("VDP", "DMA Source = {0:X6}", DmaSource);
					break;
				case 0x17: // DMA Source High
					Registers[register] = data;
					//Log.Note("VDP", "DMA Source = {0:X6}", DmaSource);
					break;

			}
			Registers[register] = data;
		}

		void ProcessPalette(int slot)
		{
			byte r = PalXlatTable[(CRAM[slot] & 0x000F) >> 0];
			byte g = PalXlatTable[(CRAM[slot] & 0x00F0) >> 4];
			byte b = PalXlatTable[(CRAM[slot] & 0x0F00) >> 8];
			Palette[slot] = Colors.ARGB(r, g, b);
		}

		void UpdatePatternBuffer(int addr)
		{
			PatternBuffer[(addr * 2) + 1] = (byte)(VRAM[addr ^ 1] & 0x0F);
			PatternBuffer[(addr * 2) + 0] = (byte)(VRAM[addr ^ 1] >> 4);
		}

		public int[] GetVideoBuffer()
		{
			return FrameBuffer;
		}

		public int VirtualWidth { get { return 320; } }
		public int VirtualHeight { get { return FrameHeight; } }

		public int BufferWidth
		{
			get { return FrameWidth; }
		}

		public int BufferHeight
		{
			get { return FrameHeight; }
		}

		public int BackgroundColor
		{
			get { return Palette[Registers[7] & 0x3F]; }
		}

		#region State Save/Load Code

		public void SaveStateText(TextWriter writer)
		{
			writer.WriteLine("[VDP]");

			writer.Write("VRAM ");
			VRAM.SaveAsHex(writer);
			writer.Write("CRAM ");
			CRAM.SaveAsHex(writer);
			writer.Write("VSRAM ");
			VSRAM.SaveAsHex(writer);
			writer.Write("Registers ");
			Registers.SaveAsHex(writer);

			writer.WriteLine("ControlWordPending {0}", ControlWordPending);
			writer.WriteLine("DmaFillModePending {0}", DmaFillModePending);
			writer.WriteLine("VdpDataAddr {0:X4}", VdpDataAddr);
			writer.WriteLine("VdpDataCode {0}", VdpDataCode);

			writer.WriteLine("[/VDP]");
		}

		public void LoadStateText(TextReader reader)
		{
			while (true)
			{
				string[] args = reader.ReadLine().Split(' ');
				if (args[0].Trim() == "") continue;
				if (args[0] == "[/VDP]") break;
				else if (args[0] == "VRAM") VRAM.ReadFromHex(args[1]);
				else if (args[0] == "CRAM") CRAM.ReadFromHex(args[1]);
				else if (args[0] == "VSRAM") VSRAM.ReadFromHex(args[1]);
				else if (args[0] == "Registers") Registers.ReadFromHex(args[1]);
				else if (args[0] == "ControlWordPending") ControlWordPending = bool.Parse(args[1]);
				else if (args[0] == "DmaFillModePending") DmaFillModePending = bool.Parse(args[1]);
				else if (args[0] == "VdpDataAddr") VdpDataAddr = ushort.Parse(args[1], NumberStyles.HexNumber);
				else if (args[0] == "VdpDataCode") VdpDataCode = byte.Parse(args[1]);
				else
					Console.WriteLine("Skipping unrecognized identifier " + args[0]);
			}

			for (int i = 0; i < CRAM.Length; i++)
				ProcessPalette(i);
			for (int i = 0; i < VRAM.Length; i++)
				UpdatePatternBuffer(i);
			for (int i = 0; i < Registers.Length; i++)
				WriteVdpRegister(i, Registers[i]);
		}

		public void SaveStateBinary(BinaryWriter writer)
		{
			writer.Write(VRAM);
			writer.Write(CRAM);
			writer.Write(VSRAM);
			writer.Write(Registers);

			writer.Write(ControlWordPending);
			writer.Write(DmaFillModePending);
			writer.Write(VdpDataAddr);
			writer.Write(VdpDataCode);
		}

		public void LoadStateBinary(BinaryReader reader)
		{
			VRAM = reader.ReadBytes(VRAM.Length);
			CRAM = reader.ReadUInt16s(CRAM.Length);
			VSRAM = reader.ReadUInt16s(VSRAM.Length);
			Registers = reader.ReadBytes(Registers.Length);

			ControlWordPending = reader.ReadBoolean();
			DmaFillModePending = reader.ReadBoolean();
			VdpDataAddr = reader.ReadUInt16();
			VdpDataCode = reader.ReadByte();

			for (int i = 0; i < CRAM.Length; i++)
				ProcessPalette(i);
			for (int i = 0; i < VRAM.Length; i++)
				UpdatePatternBuffer(i);
			for (int i = 0; i < Registers.Length; i++)
				WriteVdpRegister(i, Registers[i]);
		}

		#endregion
	}
}