using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;

using BizHawk.Client.Common;
using BizHawk.Emulation.Cores.Nintendo.NES;

namespace BizHawk.Client.EmuHawk
{
	public partial class NESNameTableViewer : Form, IToolForm
	{
		// TODO:
		// Show Scroll Lines + UI Toggle
		private readonly NES.PPU.DebugCallback _callback = new NES.PPU.DebugCallback();
		private NES _nes;

		public NESNameTableViewer()
		{
			InitializeComponent();
			Closing += (o, e) =>
				{
					Global.Config.NesNameTableSettings.Wndx = Location.X;
					Global.Config.NesNameTableSettings.Wndx = Location.Y;
					Global.Config.NESNameTableRefreshRate = RefreshRate.Value;
				};
			TopMost = Global.Config.NesNameTableSettings.TopMost;
			_callback.Callback = () => Generate();
		}

		private void NESNameTableViewer_Load(object sender, EventArgs e)
		{
			if (Global.Config.NesNameTableSettings.UseWindowPosition)
			{
				Location = Global.Config.NesNameTableSettings.WindowPosition;
			}

			_nes = Global.Emulator as NES;
			RefreshRate.Value = Global.Config.NESNameTableRefreshRate;
			Generate(true);
		}

		#region Public API

		public bool AskSave() { return true; }
		public bool UpdateBefore { get { return true; } }

		public void Restart()
		{
			if (Global.Emulator is NES)
			{
				_nes = Global.Emulator as NES;
				Generate(true);
			}
			else
			{
				Close();
			}
		}

		public void UpdateValues()
		{
			if (Global.Emulator is NES)
			{
				(Global.Emulator as NES).ppu.NTViewCallback = _callback;
			}
			else
			{
				Close();
			}
		}

		#endregion

		private unsafe void DrawTile(int* dst, int pitch, byte* pal, byte* tile, int* finalpal)
		{
			dst += 7;
			int vinc = pitch + 8;
			for (int j = 0; j < 8; j++)
			{
				int lo = tile[0];
				int hi = tile[8] << 1;
				for (int i = 0; i < 8; i++)
				{
					*dst-- = finalpal[pal[lo & 1 | hi & 2]];
					lo >>= 1;
					hi >>= 1;
				}
				dst += vinc;
				tile++;
			}
		}

		private unsafe void GenerateExAttr(int* dst, int pitch, byte[] palram, byte[] ppumem, byte[] exram)
		{
			byte[] chr = _nes.GetBoard().VROM ?? _nes.GetBoard().VRAM;
			int chr_mask = chr.Length - 1;

			fixed (byte* chrptr = chr, palptr = palram, ppuptr = ppumem, exptr = exram)
			fixed (int* finalpal = _nes.GetCompiledPalette())
			{
				DrawExNT(dst, pitch, palptr, ppuptr + 0x2000, exptr, chrptr, chr_mask, finalpal);
				DrawExNT(dst + 256, pitch, palptr, ppuptr + 0x2400, exptr, chrptr, chr_mask, finalpal);
				dst += pitch * 240;
				DrawExNT(dst, pitch, palptr, ppuptr + 0x2800, exptr, chrptr, chr_mask, finalpal);
				DrawExNT(dst + 256, pitch, palptr, ppuptr + 0x2c00, exptr, chrptr, chr_mask, finalpal);
			}
		}

		private unsafe void GenerateAttr(int* dst, int pitch, byte[] palram, byte[] ppumem)
		{
			fixed (byte* palptr = palram, ppuptr = ppumem)
			fixed (int* finalpal = _nes.GetCompiledPalette())
			{
				byte* chrptr = ppuptr + (_nes.ppu.reg_2000.bg_pattern_hi ? 0x1000 : 0);
				DrawNT(dst, pitch, palptr, ppuptr + 0x2000, chrptr, finalpal);
				DrawNT(dst + 256, pitch, palptr, ppuptr + 0x2400, chrptr, finalpal);
				dst += pitch * 240;
				DrawNT(dst, pitch, palptr, ppuptr + 0x2800, chrptr, finalpal);
				DrawNT(dst + 256, pitch, palptr, ppuptr + 0x2c00, chrptr, finalpal);
			}
		}

		private unsafe void DrawNT(int* dst, int pitch, byte* palram, byte* nt, byte* chr, int* finalpal)
		{
			byte* at = nt + 0x3c0;

			for (int ty = 0; ty < 30; ty++)
			{
				for (int tx = 0; tx < 32; tx++)
				{
					byte t = *nt++;
					byte a = at[ty >> 2 << 3 | tx >> 2];
					a >>= tx & 2;
					a >>= (ty & 2) << 1;
					int palnum = a & 3;

					int tileaddr = t << 4;
					DrawTile(dst, pitch, palram + palnum * 4, chr + tileaddr, finalpal);
					dst += 8;
				}
				dst -= 256;
				dst += pitch * 8;
			}
		}

		private unsafe void DrawExNT(int* dst, int pitch, byte* palram, byte* nt, byte* exnt, byte* chr, int chr_mask, int* finalpal)
		{
			for (int ty = 0; ty < 30; ty++)
			{
				for (int tx = 0; tx < 32; tx++)
				{
					byte t = *nt++;
					byte ex = *exnt++;

					int tilenum = t | (ex & 0x3f) << 8;
					int palnum = ex >> 6;

					int tileaddr = tilenum << 4 & chr_mask;
					DrawTile(dst, pitch, palram + palnum * 4, chr + tileaddr, finalpal);
					dst += 8;
				}
				dst -= 256;
				dst += pitch * 8;
			}
		}

		private unsafe void Generate(bool now = false)
		{
			if (!IsHandleCreated || IsDisposed)
			{
				return;
			}

			if (_nes == null)
			{
				return;
			}

			if (now == false && Global.Emulator.Frame % RefreshRate.Value != 0)
			{
				return;
			}

			var bmpdata = NameTableView.Nametables.LockBits(
				new Rectangle(0, 0, 512, 480),
				ImageLockMode.WriteOnly,
				PixelFormat.Format32bppArgb);

			var dptr = (int*)bmpdata.Scan0.ToPointer();
			var pitch = bmpdata.Stride / 4;
			var pt_add = _nes.ppu.reg_2000.bg_pattern_hi ? 0x1000 : 0;

			// Buffer all the data from the ppu, because it will be read multiple times and that is slow
			var ppuBuffer = new byte[0x3000];
			for (var i = 0; i < 0x3000; i++)
			{
				ppuBuffer[i] = _nes.ppu.ppubus_peek(i);
			}

			var palram = new byte[0x20];
			for (var i = 0; i < 0x20; i++)
			{
				palram[i] = _nes.ppu.PALRAM[i];
			}

			var board = _nes.GetBoard();
			if (board is ExROM && (board as ExROM).ExAttrActive)
			{
				byte[] exram = new byte[1024];
				var md = _nes.MemoryDomains["ExRAM"];
				for (int i = 0; i < 1024; i++)
					exram[i] = md.PeekByte(i);

				GenerateExAttr(dptr, pitch, palram, ppuBuffer, exram);
			}
			else
			{
				GenerateAttr(dptr, pitch, palram, ppuBuffer);
			}

			NameTableView.Nametables.UnlockBits(bmpdata);
			NameTableView.Refresh();
		}

		private void RefreshFloatingWindowControl()
		{
			Owner = Global.Config.NesNameTableSettings.FloatingWindow ? null : GlobalWin.MainForm;
		}

		#region Events

		#region Menu and Context Menu

		private void ScreenshotMenuItem_Click(object sender, EventArgs e)
		{
			NameTableView.Screenshot();
		}

		private void ScreenshotToClipboardMenuItem_Click(object sender, EventArgs e)
		{
			NameTableView.ScreenshotToClipboard();
		}

		private void ExitMenuItem_Click(object sender, EventArgs e)
		{
			Close();
		}

		private void OptionsSubMenu_DropDownOpened(object sender, EventArgs e)
		{
			AutoloadMenuItem.Checked = Global.Config.AutoLoadNESNameTable;
			SaveWindowPositionMenuItem.Checked = Global.Config.NesNameTableSettings.SaveWindowPosition;
			AlwaysOnTopMenuItem.Checked = Global.Config.NesNameTableSettings.TopMost;
			FloatingWindowMenuItem.Checked = Global.Config.NesNameTableSettings.FloatingWindow;
		}

		private void AutoloadMenuItem_Click(object sender, EventArgs e)
		{
			Global.Config.AutoLoadNESNameTable ^= true;
		}

		private void SaveWindowPositionMenuItem_Click(object sender, EventArgs e)
		{
			Global.Config.NesNameTableSettings.SaveWindowPosition ^= true;
		}

		private void AlwaysOnTopMenuItem_Click(object sender, EventArgs e)
		{
			Global.Config.NesNameTableSettings.TopMost ^= true;
			TopMost = Global.Config.NesNameTableSettings.TopMost;
		}

		private void FloatingWindowMenuItem_Click(object sender, EventArgs e)
		{
			Global.Config.NesNameTableSettings.FloatingWindow ^= true;
			RefreshFloatingWindowControl();
		}

		private void RefreshImageContextMenuItem_Click(object sender, EventArgs e)
		{
			UpdateValues();
			NameTableView.Refresh();
		}

		#endregion

		#region Dialog and Controls

		private void NesNameTableViewer_KeyDown(object sender, KeyEventArgs e)
		{
			switch (e.KeyCode)
			{
				case Keys.C:
					if (e.Modifiers == Keys.Control)
					{
						NameTableView.ScreenshotToClipboard();
					}

					break;
			}
		}

		protected override void OnShown(EventArgs e)
		{
			RefreshFloatingWindowControl();
			base.OnShown(e);
		}

		private void NESNameTableViewer_FormClosed(object sender, FormClosedEventArgs e)
		{
			if (_nes != null && _nes.ppu.NTViewCallback == _callback)
			{
				_nes.ppu.NTViewCallback = null;
			}
		}

		private void ScanlineTextbox_TextChanged(object sender, EventArgs e)
		{
			int temp;
			if (int.TryParse(txtScanline.Text, out temp))
			{
				_callback.Scanline = temp;
			}
		}

		private void NametableRadio_CheckedChanged(object sender, EventArgs e)
		{
			if (rbNametableNW.Checked)
			{
				NameTableView.Which = NameTableViewer.WhichNametable.NT_2000;
			}

			if (rbNametableNE.Checked)
			{
				NameTableView.Which = NameTableViewer.WhichNametable.NT_2400;
			}

			if (rbNametableSW.Checked)
			{
				NameTableView.Which = NameTableViewer.WhichNametable.NT_2800;
			}

			if (rbNametableSE.Checked)
			{
				NameTableView.Which = NameTableViewer.WhichNametable.NT_2C00;
			}

			if (rbNametableAll.Checked)
			{
				NameTableView.Which = NameTableViewer.WhichNametable.NT_ALL;
			}
		}

		private void NameTableView_MouseMove(object sender, MouseEventArgs e)
		{
			int TileX, TileY, NameTable;
			if (NameTableView.Which == NameTableViewer.WhichNametable.NT_ALL)
			{
				TileX = e.X / 8;
				TileY = e.Y / 8;
				NameTable = (TileX / 32) + ((TileY / 30) * 2);
			}
			else
			{
				switch (NameTableView.Which)
				{
					default:
					case NameTableViewer.WhichNametable.NT_2000:
						NameTable = 0;
						break;
					case NameTableViewer.WhichNametable.NT_2400:
						NameTable = 1;
						break;
					case NameTableViewer.WhichNametable.NT_2800:
						NameTable = 2;
						break;
					case NameTableViewer.WhichNametable.NT_2C00:
						NameTable = 3;
						break;
				}

				TileX = e.X / 16;
				TileY = e.Y / 16;
			}

			XYLabel.Text = TileX + " : " + TileY;
			int PPUAddress = 0x2000 + (NameTable * 0x400) + ((TileY % 30) * 32) + (TileX % 32);
			PPUAddressLabel.Text = string.Format("{0:X4}", PPUAddress);
			int TileID = _nes.ppu.ppubus_read(PPUAddress, true);
			TileIDLabel.Text = string.Format("{0:X2}", TileID);
			TableLabel.Text = NameTable.ToString();

			int ytable = 0, yline = 0;
			if (e.Y >= 240)
			{
				ytable += 2;
				yline = 240;
			}
			int table = (e.X >> 8) + ytable;
			int ntaddr = (table << 10);
			int px = e.X & 255;
			int py = e.Y - yline;
			int tx = px >> 3;
			int ty = py >> 3;
			int atbyte_ptr = ntaddr + 0x3C0 + ((ty >> 2) << 3) + (tx >> 2);
			int at = _nes.ppu.ppubus_peek(atbyte_ptr + 0x2000);
			if ((ty & 2) != 0) at >>= 4;
			if ((tx & 2) != 0) at >>= 2;
			at &= 0x03;
			PaletteLabel.Text = at.ToString();
		}

		private void NameTableView_MouseLeave(object sender, EventArgs e)
		{
			XYLabel.Text = string.Empty;
			PPUAddressLabel.Text = string.Empty;
			TileIDLabel.Text = string.Empty;
			TableLabel.Text = string.Empty;
			PaletteLabel.Text = string.Empty;
		}

		#endregion

		#endregion
	}
}
