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

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


namespace BizHawk.Emulation.Cores.Sega.MasterSystem
{
	public enum VdpMode { SMS, GameGear }

	// Emulates the Texas Instruments TMS9918 VDP.
	public sealed partial class VDP : IVideoProvider
	{
		// VDP State
		public byte[] VRAM = new byte[0x4000]; //16kb video RAM
		public byte[] CRAM; // SMS = 32 bytes, GG = 64 bytes CRAM
		public byte[] Registers = new byte[] { 0x06, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xF0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00 };
		public byte StatusByte;

		const int Command_VramRead = 0x00;
		const int Command_VramWrite = 0x40;
		const int Command_RegisterWrite = 0x80;
		const int Command_CramWrite = 0xC0;

		bool VdpWaitingForLatchByte = true;
		byte VdpLatch;
		byte VdpBuffer;
		ushort VdpAddress;
		byte VdpCommand;
		int TmsMode = 4;

		bool VIntPending;
		bool HIntPending;
		int lineIntLinesRemaining;

		SMS Sms;
		VdpMode mode;
		DisplayType DisplayType = DisplayType.NTSC;
		Z80A Cpu;

		public bool SpriteLimit;
		public int IPeriod = 228;
		public VdpMode VdpMode { get { return mode; } }

		public int FrameHeight = 192;
		public int ScanLine;
		public int[] FrameBuffer = new int[256 * 192];
		public int[] GameGearFrameBuffer = new int[160 * 144];

		public bool Mode1Bit { get { return (Registers[1] & 16) > 0; } }
		public bool Mode2Bit { get { return (Registers[0] & 2) > 0; } }
		public bool Mode3Bit { get { return (Registers[1] & 8) > 0; } }
		public bool Mode4Bit { get { return (Registers[0] & 4) > 0; } }
		public bool ShiftSpritesLeft8Pixels { get { return (Registers[0] & 8) > 0; } }
		public bool EnableLineInterrupts { get { return (Registers[0] & 16) > 0; } }
		public bool LeftBlanking { get { return (Registers[0] & 32) > 0; } }
		public bool HorizScrollLock { get { return (Registers[0] & 64) > 0; } }
		public bool VerticalScrollLock { get { return (Registers[0] & 128) > 0; } }
		public bool EnableDoubledSprites { get { return (Registers[1] & 1) > 0; } }
		public bool EnableLargeSprites { get { return (Registers[1] & 2) > 0; } }
		public bool EnableFrameInterrupts { get { return (Registers[1] & 32) > 0; } }
		public bool DisplayOn { get { return (Registers[1] & 64) > 0; } }
		public int SpriteAttributeTableBase { get { return ((Registers[5] >> 1) << 8) & 0x3FFF; } }
		public int SpriteTileBase { get { return (Registers[6] & 4) > 0 ? 256 : 0; } }
		public byte BackdropColor { get { return (byte)(16 + (Registers[7] & 15)); } }

		int NameTableBase;
		int ColorTableBase;
		int PatternGeneratorBase;
		int SpritePatternGeneratorBase;
		int TmsPatternNameTableBase;
		int TmsSpriteAttributeBase;

		// preprocessed state assist stuff.
		public int[] Palette = new int[32];
		public byte[] PatternBuffer = new byte[0x8000];

		byte[] ScanlinePriorityBuffer = new byte[256];
		byte[] SpriteCollisionBuffer = new byte[256];

		static readonly byte[] SMSPalXlatTable = { 0, 85, 170, 255 };
		static readonly byte[] GGPalXlatTable = { 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255 };

		public VDP(SMS sms, Z80A cpu, VdpMode mode, DisplayType displayType)
		{
			Sms = sms;
			Cpu = cpu;
			this.mode = mode;
			if (mode == VdpMode.SMS) CRAM = new byte[32];
			if (mode == VdpMode.GameGear) CRAM = new byte[64];
			DisplayType = displayType;
			NameTableBase = CalcNameTableBase();
		}

		public byte ReadData()
		{
			VdpWaitingForLatchByte = true;
			byte value = VdpBuffer;
			VdpBuffer = VRAM[VdpAddress & 0x3FFF];
			VdpAddress++;
			return value;
		}

		public byte ReadVdpStatus()
		{
			VdpWaitingForLatchByte = true;
			byte returnValue = StatusByte;
			StatusByte &= 0x1F;
			HIntPending = false;
			VIntPending = false;
			Cpu.Interrupt = false;
			return returnValue;
		}

		public byte ReadVLineCounter()
		{
			if (DisplayType == DisplayType.NTSC)
			{
				if (FrameHeight == 192)
					return VLineCounterTableNTSC192[ScanLine];
				if (FrameHeight == 224)
					return VLineCounterTableNTSC224[ScanLine];
				return VLineCounterTableNTSC240[ScanLine];
			}
			else
			{ // PAL
				if (FrameHeight == 192)
					return VLineCounterTablePAL192[ScanLine];
				if (FrameHeight == 224)
					return VLineCounterTablePAL224[ScanLine];
				return VLineCounterTablePAL240[ScanLine];
			}
		}

		public void WriteVdpControl(byte value)
		{
			if (VdpWaitingForLatchByte)
			{
				VdpLatch = value;
				VdpWaitingForLatchByte = false;
				VdpAddress = (ushort)((VdpAddress & 0xFF00) | value);
				return;
			}

			VdpWaitingForLatchByte = true;
			VdpAddress = (ushort)(((value & 63) << 8) | VdpLatch);
			switch (value & 0xC0)
			{
				case 0x00: // read VRAM
					VdpCommand = Command_VramRead;
					VdpBuffer = VRAM[VdpAddress & 0x3FFF];
					VdpAddress++;
					break;
				case 0x40: // write VRAM
					VdpCommand = Command_VramWrite;
					break;
				case 0x80: // VDP register write
					VdpCommand = Command_RegisterWrite;
					int reg = value & 0x0F;
					WriteRegister(reg, VdpLatch);
					break;
				case 0xC0: // write CRAM / modify palette
					VdpCommand = Command_CramWrite;
					break;
			}
		}

		public void WriteVdpData(byte value)
		{
			VdpWaitingForLatchByte = true;
			VdpBuffer = value;
			if (VdpCommand == Command_CramWrite)
			{
				// Write Palette / CRAM
				int mask = VdpMode == VdpMode.SMS ? 0x1F : 0x3F;
				CRAM[VdpAddress & mask] = value;
				UpdatePrecomputedPalette();
			}
			else
			{
				// Write VRAM and update pre-computed pattern buffer. 
				UpdatePatternBuffer((ushort)(VdpAddress & 0x3FFF), value);
				VRAM[VdpAddress & 0x3FFF] = value;
			}
			VdpAddress++;
		}

		public void UpdatePrecomputedPalette()
		{
			if (mode == VdpMode.SMS)
			{
				for (int i = 0; i < 32; i++)
				{
					byte value = CRAM[i];
					byte r = SMSPalXlatTable[(value & 0x03)];
					byte g = SMSPalXlatTable[(value & 0x0C) >> 2];
					byte b = SMSPalXlatTable[(value & 0x30) >> 4];
					Palette[i] = Colors.ARGB(r, g, b);
				}
			}
			else
			{ // GameGear
				for (int i = 0; i < 32; i++)
				{
					ushort value = (ushort)((CRAM[(i * 2) + 1] << 8) | CRAM[(i * 2) + 0]);
					byte r = GGPalXlatTable[(value & 0x000F)];
					byte g = GGPalXlatTable[(value & 0x00F0) >> 4];
					byte b = GGPalXlatTable[(value & 0x0F00) >> 8];
					Palette[i] = Colors.ARGB(r, g, b);
				}
			}
		}

		public int CalcNameTableBase()
		{
			if (FrameHeight == 192)
				return 1024 * (Registers[2] & 0x0E);
			return (1024 * (Registers[2] & 0x0C)) + 0x0700;
		}

		void CheckVideoMode()
		{
			if (Mode4Bit == false) // check old TMS modes
			{
				if (Mode1Bit) TmsMode = 1;
				else if (Mode2Bit) TmsMode = 2;
				else if (Mode3Bit) TmsMode = 3;
				else TmsMode = 0;
			}

			else if (Mode4Bit && Mode2Bit) // if Mode4 and Mode2 set, then check extension modes
			{
				TmsMode = 4;
				switch (Registers[1] & 0x18)
				{
					case 0x00:
					case 0x18: // 192-line mode
						if (FrameHeight != 192)
						{
							FrameHeight = 192;
							FrameBuffer = new int[256 * 192];
							NameTableBase = CalcNameTableBase();
						}
						break;
					case 0x10: // 224-line mode
						if (FrameHeight != 224)
						{
							FrameHeight = 224;
							FrameBuffer = new int[256 * 224];
							NameTableBase = CalcNameTableBase();
						}
						break;
					case 0x08: // 240-line mode
						if (FrameHeight != 240)
						{
							FrameHeight = 240;
							FrameBuffer = new int[256 * 240];
							NameTableBase = CalcNameTableBase();
						}
						break;
				}
			}

			else
			{ // default to standard 192-line mode4
				TmsMode = 4;
				if (FrameHeight != 192)
				{
					FrameHeight = 192;
					FrameBuffer = new int[256 * 192];
					NameTableBase = CalcNameTableBase();
				}
			}
		}

		void WriteRegister(int reg, byte data)
		{
			Registers[reg] = data;
			switch (reg)
			{
				case 0: // Mode Control Register 1
					CheckVideoMode();
					Cpu.Interrupt = (EnableLineInterrupts && HIntPending);
					break;
				case 1: // Mode Control Register 2
					CheckVideoMode();
					Cpu.Interrupt = (EnableFrameInterrupts && VIntPending);
					break;
				case 2: // Name Table Base Address
					NameTableBase = CalcNameTableBase();
					TmsPatternNameTableBase = (Registers[2] << 10) & 0x3C00;
					break;
				case 3: // Color Table Base Address
					ColorTableBase = (Registers[3] << 6) & 0x3FC0;
					break;
				case 4: // Pattern Generator Base Address
					PatternGeneratorBase = (Registers[4] << 11) & 0x3800;
					break;
				case 5: // Sprite Attribute Table Base Address
					// ??? should I move from my property to precalculated?
					TmsSpriteAttributeBase = (Registers[5] << 7) & 0x3F80;
					break;
				case 6: // Sprite Pattern Generator Base Adderss 
					SpritePatternGeneratorBase = (Registers[6] << 11) & 0x3800;
					break;
			}
		}

		static readonly byte[] pow2 = { 1, 2, 4, 8, 16, 32, 64, 128 };

		void UpdatePatternBuffer(ushort address, byte value)
		{
			// writing one byte affects 8 pixels due to stupid planar storage.
			for (int i = 0; i < 8; i++)
			{
				byte colorBit = pow2[address % 4];
				byte sourceBit = pow2[7 - i];
				ushort dest = (ushort)(((address & 0xFFFC) * 2) + i);
				if ((value & sourceBit) > 0) // setting bit
					PatternBuffer[dest] |= colorBit;
				else // clearing bit
					PatternBuffer[dest] &= (byte)~colorBit;
			}
		}

		void ProcessFrameInterrupt()
		{
			if (ScanLine == FrameHeight + 1)
			{
				StatusByte |= 0x80;
				VIntPending = true;
			}

			if (VIntPending && EnableFrameInterrupts)
				Cpu.Interrupt = true;
		}

		void ProcessLineInterrupt()
		{
			if (ScanLine <= FrameHeight)
			{
				if (lineIntLinesRemaining-- <= 0)
				{
					HIntPending = true;
					if (EnableLineInterrupts)
						Cpu.Interrupt = true;
					lineIntLinesRemaining = Registers[0x0A];
				}
				return;
			}
			// else we're outside the active display period
			lineIntLinesRemaining = Registers[0x0A];
		}

		public void ExecFrame(bool render)
		{
			int scanlinesPerFrame = DisplayType == DisplayType.NTSC ? 262 : 313;
			SpriteLimit = Sms.Settings.SpriteLimit;
			for (ScanLine = 0; ScanLine < scanlinesPerFrame; ScanLine++)
			{
				RenderCurrentScanline(render);

				ProcessFrameInterrupt();
				ProcessLineInterrupt();

				Cpu.ExecuteCycles(IPeriod);

				if (ScanLine == scanlinesPerFrame - 1)
					ProcessGGScreen();
			}
		}

		internal void RenderCurrentScanline(bool render)
		{
			// only mode 4 supports frameskip. deal with it
			if (TmsMode == 4)
			{
				if (render)
					RenderBackgroundCurrentLine(Sms.Settings.DispBG);

				if (EnableDoubledSprites)
					RenderSpritesCurrentLineDoubleSize(Sms.Settings.DispOBJ & render);
				else
					RenderSpritesCurrentLine(Sms.Settings.DispOBJ & render);

				RenderLineBlanking(render);
			}
			else if (TmsMode == 2)
			{
				RenderBackgroundM2(Sms.Settings.DispBG);
				RenderTmsSprites(Sms.Settings.DispOBJ);
			}
			else if (TmsMode == 0)
			{
				RenderBackgroundM0(Sms.Settings.DispBG);
				RenderTmsSprites(Sms.Settings.DispOBJ);
			}
		}

		public void SyncState(Serializer ser)
		{
			ser.BeginSection("VDP");
			ser.Sync("StatusByte", ref StatusByte);
			ser.Sync("WaitingForLatchByte", ref VdpWaitingForLatchByte);
			ser.Sync("Latch", ref VdpLatch);
			ser.Sync("ReadBuffer", ref VdpBuffer);
			ser.Sync("VdpAddress", ref VdpAddress);
			ser.Sync("Command", ref VdpCommand);
			ser.Sync("HIntPending", ref HIntPending);
			ser.Sync("VIntPending", ref VIntPending);
			ser.Sync("LineIntLinesRemaining", ref lineIntLinesRemaining);
			ser.Sync("Registers", ref Registers, false);
			ser.Sync("CRAM", ref CRAM, false);
			ser.Sync("VRAM", ref VRAM, false);
			ser.EndSection();

			if (ser.IsReader)
			{
				for (int i = 0; i < Registers.Length; i++)
					WriteRegister(i, Registers[i]);
				for (ushort i = 0; i < VRAM.Length; i++)
					UpdatePatternBuffer(i, VRAM[i]);
				UpdatePrecomputedPalette();
			}
		}

		public int[] GetVideoBuffer()
		{
			if (mode == VdpMode.SMS || Sms.Settings.ShowClippedRegions)
				return FrameBuffer;
			return GameGearFrameBuffer;
		}

		public int VirtualWidth
		{ 
			get
			{
				if (mode == MasterSystem.VdpMode.SMS)
					return 293;
				else if (Sms.Settings.ShowClippedRegions)
					return 256;
				else
					return 160;
			}
		}
		public int VirtualHeight { get { return BufferHeight; } }
		public int BufferWidth
		{
			get
			{
				if (mode == VdpMode.SMS || Sms.Settings.ShowClippedRegions)
					return 256;
				return 160; // GameGear
			}
		}

		public int BufferHeight
		{
			get
			{
				if (mode == VdpMode.SMS || Sms.Settings.ShowClippedRegions)
					return FrameHeight;
				return 144; // GameGear
			}
		}

		public int BackgroundColor
		{
			get { return Palette[BackdropColor]; }
		}
	}
}
