using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

using BizHawk.Common;
using BizHawk.Common.IOExtensions;
using BizHawk.Bizware.BizwareGL;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Common.IEmulatorExtensions;
using BizHawk.Emulation.Cores.Nintendo.NES;
using BizHawk.Client.Common;
using BizHawk.Client.MultiHawk.ToolExtensions;

namespace BizHawk.Client.MultiHawk
{
	public partial class Mainform : Form
	{
		static Mainform()
		{
			// If this isnt here, then our assemblyresolving hacks wont work due to the check for MainForm.INTERIM
			// its.. weird. dont ask.
		}

		public Mainform(string[] args)
		{
			BizHawk.Client.Common.GLManager.CreateInstance();

			InitializeComponent();
			_throttle = new BizHawk.Client.EmuHawk.Throttle();
			_inputManager = new InputManager(this);
			Global.Config = ConfigService.Load<Config>(PathManager.DefaultIniPath);
			Global.Config.DispFixAspectRatio = false; // TODO: don't hardcode this
			Global.Config.ResolveDefaults();
			GlobalWin.MainForm = this;

			Global.ControllerInputCoalescer = new ControllerInputCoalescer();
			Global.FirmwareManager = new FirmwareManager();
			Global.MovieSession = new MovieSession
			{
				Movie = MovieService.DefaultInstance,
				MovieControllerAdapter = MovieService.DefaultInstance.LogGeneratorInstance().MovieControllerAdapter,
				MessageCallback = AddMessage,

				AskYesNoCallback = StateErrorAskUser,
				PauseCallback = PauseEmulator,
				ModeChangedCallback = SetMainformMovieInfo
			};

			new AutoResetEvent(false);
			// TODO
			//Icon = Properties.Resources.logo;
			Global.Game = GameInfo.NullInstance;

			// In order to allow late construction of this database, we hook up a delegate here to dearchive the data and provide it on demand
			// we could background thread this later instead if we wanted to be real clever
			NES.BootGodDB.GetDatabaseBytes = () =>
			{
				using (var NesCartFile =
						new HawkFile(Path.Combine(PathManager.GetExeDirectoryAbsolute(), "gamedb", "NesCarts.7z")).BindFirst())
				{
					return NesCartFile
						.GetStream()
						.ReadAllBytes();
				}
			};

			Database.LoadDatabase(Path.Combine(PathManager.GetExeDirectoryAbsolute(), "gamedb", "gamedb.txt"));

			Input.Initialize(this.Handle);
			InitControls();

			// TODO
			//CoreFileProvider.SyncCoreCommInputSignals();

			Global.ActiveController = new Controller(NullEmulator.NullController);
			Global.AutoFireController = Global.AutofireNullControls;
			Global.AutofireStickyXORAdapter.SetOnOffPatternFromConfig();

			Closing += (o, e) =>
			{
				Global.MovieSession.Movie.Stop();

				foreach (var ew in EmulatorWindows.ToList())
				{
					ew.ShutDown();
				}

				SaveConfig();
			};

			if (Global.Config.MainWndx != -1 && Global.Config.MainWndy != -1 && Global.Config.SaveWindowPosition)
			{
				Location = new Point(Global.Config.MainWndx, Global.Config.MainWndy);
			}
		}

		private static bool StateErrorAskUser(string title, string message)
		{
			var result = MessageBox.Show(
				message,
				title,
				MessageBoxButtons.YesNo,
				MessageBoxIcon.Question
			);

			return result == DialogResult.Yes;
		}

		private void Mainform_Load(object sender, EventArgs e)
		{
			SetMainformMovieInfo();

			if (Global.Config.RecentRomSessions.AutoLoad)
			{
				LoadRomSessionFromRecent(Global.Config.RecentRomSessions.MostRecent);

				if (Global.Config.RecentMovies.AutoLoad && !Global.Config.RecentMovies.Empty)
				{
					LoadMoviesFromRecent(Global.Config.RecentMovies.MostRecent);
				}
			}

			if (Global.Config.SaveWindowPosition && Global.Config.MainWidth > 0 && Global.Config.MainHeight > 0)
			{
				this.Size = new Size(Global.Config.MainWidth, Global.Config.MainHeight);
				
			}
		}

		public EmulatorWindowList EmulatorWindows = new EmulatorWindowList();

		private bool _exit;

		protected override void OnClosed(EventArgs e)
		{
			_exit = true;
			base.OnClosed(e);
		}

		private void SaveConfig()
		{
			if (Global.Config.SaveWindowPosition)
			{
				if (Global.Config.MainWndx != -32000) // When minimized location is -32000, don't save this into the config file!
				{
					Global.Config.MainWndx = Location.X;
				}

				if (Global.Config.MainWndy != -32000)
				{
					Global.Config.MainWndy = Location.Y;
				}
			}
			else
			{
				Global.Config.MainWndx = -1;
				Global.Config.MainWndy = -1;
			}

			Global.Config.MainWidth = this.Width;
			Global.Config.MainHeight = this.Height;

			ConfigService.Save(PathManager.DefaultIniPath, Global.Config);
		}

		private static void InitControls()
		{
			var controls = new Controller(
				new ControllerDefinition
				{
					Name = "Emulator Frontend Controls",
					BoolButtons = Global.Config.HotkeyBindings.Select(x => x.DisplayName).ToList()
				});

			foreach (var b in Global.Config.HotkeyBindings)
			{
				controls.BindMulti(b.DisplayName, b.Bindings);
			}

			Global.ClientControls = controls;
			Global.AutofireNullControls = new AutofireController(NullEmulator.NullController, Global.Emulator);
		}

		private void OpenRomMenuItem_Click(object sender, EventArgs e)
		{
			var ofd = new OpenFileDialog
			{
				InitialDirectory = PathManager.GetExeDirectoryAbsolute()
			};

			ofd.Filter = FormatFilter(
					"Rom Files", "*.nes;*.fds;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.m3u;*.cue;*.ccd;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.col;.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;%ARCH%",
					"Music Files", "*.psf;*.sid",
					"Disc Images", "*.cue;*.ccd;*.m3u",
					"NES", "*.nes;*.fds;%ARCH%",
					"Super NES", "*.smc;*.sfc;*.xml;%ARCH%",
					"Master System", "*.sms;*.gg;*.sg;%ARCH%",
					"PC Engine", "*.pce;*.sgx;*.cue;*.ccd;%ARCH%",
					"TI-83", "*.rom;%ARCH%",
					"Archive Files", "%ARCH%",
					"Savestate", "*.state",
					"Atari 2600", "*.a26;*.bin;%ARCH%",
					"Atari 7800", "*.a78;*.bin;%ARCH%",
					"Atari Lynx", "*.lnx;%ARCH%",
					"Genesis", "*.gen;*.smd;*.bin;*.md;*.cue;*.ccd;%ARCH%",
					"Gameboy", "*.gb;*.gbc;*.sgb;%ARCH%",
					"Gameboy Advance", "*.gba;%ARCH%",
					"Colecovision", "*.col;%ARCH%",
					"Intellivision (very experimental)", "*.int;*.bin;*.rom;%ARCH%",
					"PSX Executables (experimental)", "*.exe",
					"PSF Playstation Sound File", "*.psf;*.minipsf",
					"Commodore 64 (experimental)", "*.prg; *.d64, *.g64; *.crt;%ARCH%",
					"SID Commodore 64 Music File", "*.sid;%ARCH%",
					"Nintendo 64", "*.z64;*.v64;*.n64",
					"WonderSwan", "*.ws;*.wsc;%ARCH%",
					"All Files", "*.*");

			var result = ofd.ShowDialog();
			if (result != DialogResult.OK)
			{
				return;
			}

			LoadRom(ofd.FileName);
		}

		private readonly InputManager _inputManager;

		private bool ReloadRom(EmulatorWindow ew)
		{
			bool deterministic = ew.Emulator.DeterministicEmulation;
			string path = ew.CurrentRomPath;

			var loader = new RomLoader
			{
				ChooseArchive = LoadArhiveChooser,
				ChoosePlatform = ChoosePlatformForRom,
				Deterministic = deterministic,
				MessageCallback = AddMessage
			};


			loader.OnLoadError += ShowLoadError;
			loader.OnLoadSettings += CoreSettings;
			loader.OnLoadSyncSettings += CoreSyncSettings;

			var nextComm = new CoreComm(ShowMessageCoreComm, AddMessage);

			var result = loader.LoadRom(path, nextComm);

			if (result)
			{
				ew.SaveRam();
				ew.Emulator.Dispose();
				ew.Emulator = loader.LoadedEmulator;
				ew.CoreComm = nextComm;

				_inputManager.SyncControls();

				if (EmulatorWindows.First() == ew)
				{
					Global.Emulator = ew.Emulator;
				}

				return true;
			}
			else
			{
				return false;
			}
		}

		private string StripArchivePath(string path)
		{
			if (path.Contains("|"))
			{
				return path.Split('|').Last();
			}

			return path;
		}

		private bool LoadRom(string path)
		{
			bool deterministic = true;

			var loader = new RomLoader
			{
				ChooseArchive = LoadArhiveChooser,
				ChoosePlatform = ChoosePlatformForRom,
				Deterministic = deterministic,
				MessageCallback = AddMessage
			};


			loader.OnLoadError += ShowLoadError;
			loader.OnLoadSettings += CoreSettings;
			loader.OnLoadSyncSettings += CoreSyncSettings;

			var nextComm = new CoreComm(ShowMessageCoreComm, AddMessage);

			var result = loader.LoadRom(path, nextComm);

			if (result)
			{
				var ew = new EmulatorWindow(this)
				{
					TopLevel = false,
					Text = Path.GetFileNameWithoutExtension(StripArchivePath(path)),
					Emulator = loader.LoadedEmulator,

					GL = new Bizware.BizwareGL.Drivers.OpenTK.IGL_TK(),
					GLManager = BizHawk.Client.Common.GLManager.Instance,
					Game = loader.Game,
					CurrentRomPath = loader.CanonicalFullPath
				};

				nextComm.RequestGLContext = () => ew.GLManager.CreateGLContext();
				nextComm.ActivateGLContext = (gl) => ew.GLManager.Activate((GLManager.ContextRef)ew.GL);
				nextComm.DeactivateGLContext = () => ew.GLManager.Deactivate();

				ew.CoreComm = nextComm;
				ew.Init();

				if (EmulatorWindows.Any())
				{
					// Attempt to open the window is a smart location
					var last = EmulatorWindows.Last();

					int x = last.Location.X + last.Width + 5;
					int y = last.Location.Y;
					if (x + (last.Width / 2) > Width) // If it will go too far off screen
					{
						y += last.Height + 5;
						x = EmulatorWindows.First().Location.X;
					}

					ew.Location = new Point(x, y);
				}
				

				EmulatorWindows.Add(ew);

				WorkspacePanel.Controls.Add(ew);
				ew.Show();

				Global.Config.RecentRoms.Add(loader.CanonicalFullPath);

				if (EmulatorWindows.Count == 1)
				{
					Global.Emulator = ew.Emulator;
				}

				_inputManager.SyncControls();

				return true;
			}
			else
			{
				return false;
			}
		}

		/// <summary>
		/// Controls whether the app generates input events. should be turned off for most modal dialogs
		/// </summary>
		public bool AllowInput
		{
			get
			{
				// the main form gets input
				if (ActiveForm == this)
				{
					return true;
				}

				// modals that need to capture input for binding purposes get input, of course
				if (ActiveForm is BizHawk.Client.EmuHawk.HotkeyConfig
					|| ActiveForm is BizHawk.Client.EmuHawk.ControllerConfig
					//|| ActiveForm is TAStudio
					//|| ActiveForm is VirtualpadTool
				)
				{
					return true;
				}

				return false;
			}
		}

		private static string FormatFilter(params string[] args)
		{
			var sb = new StringBuilder();
			if (args.Length % 2 != 0)
			{
				throw new ArgumentException();
			}

			var num = args.Length / 2;
			for (int i = 0; i < num; i++)
			{
				sb.AppendFormat("{0} ({1})|{1}", args[i * 2], args[i * 2 + 1]);
				if (i != num - 1)
				{
					sb.Append('|');
				}
			}

			var str = sb.ToString().Replace("%ARCH%", "*.zip;*.rar;*.7z;*.gz");
			str = str.Replace(";", "; ");
			return str;
		}

		public void AddMessage(string message)
		{
			StatusBarMessageLabel.Text = message;
		}

		private string ChoosePlatformForRom(RomGame rom)
		{
			// TODO
			return null;
		}

		private int? LoadArhiveChooser(HawkFile file)
		{
			var ac = new BizHawk.Client.EmuHawk.ArchiveChooser(file);
			if (ac.ShowDialog(this) == DialogResult.OK)
			{
				return ac.SelectedMemberIndex;
			}
			else
			{
				return null;
			}
		}

		private void ShowLoadError(object sender, RomLoader.RomErrorArgs e)
		{
			string title = "load error";
			if (e.AttemptedCoreLoad != null)
			{
				title = e.AttemptedCoreLoad + " load error";
			}

			MessageBox.Show(this, e.Message, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
		}

		private static void CoreSettings(object sender, RomLoader.SettingsLoadArgs e)
		{
			e.Settings = Global.Config.GetCoreSettings(e.Core);
		}

		private void CoreSyncSettings(object sender, RomLoader.SettingsLoadArgs e)
		{
			if (Global.MovieSession.QueuedMovie != null)
			{
				if (!string.IsNullOrWhiteSpace(Global.MovieSession.QueuedMovie.SyncSettingsJson))
				{
					e.Settings = ConfigService.LoadWithType(Global.MovieSession.QueuedMovie.SyncSettingsJson);
				}
				else
				{
					MessageBox.Show(
						"No sync settings found, using currently configured settings for this core.",
						"No sync settings found",
						MessageBoxButtons.OK,
						MessageBoxIcon.Warning
						);

					e.Settings = Global.Config.GetCoreSyncSettings(e.Core);
				}
			}
			else
			{
				e.Settings = Global.Config.GetCoreSyncSettings(e.Core);
			}

		}

		public void FlagNeedsReboot()
		{
			// TODO
		}

		private void ShowMessageCoreComm(string message)
		{
			MessageBox.Show(this, message, "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
		}

		private static void CheckMessages()
		{
			Application.DoEvents();
			if (ActiveForm != null)
			{
				BizHawk.Client.EmuHawk.ScreenSaver.ResetTimerPeriodically();
			}
		}

		// sends an alt+mnemonic combination
		private void SendAltKeyChar(char c)
		{
			typeof(ToolStrip).InvokeMember("ProcessMnemonicInternal", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.InvokeMethod | System.Reflection.BindingFlags.Instance, null, MainformMenu, new object[] { c });
		}

		// sends a simulation of a plain alt key keystroke
		private void SendPlainAltKey(int lparam)
		{
			var m = new Message { WParam = new IntPtr(0xF100), LParam = new IntPtr(lparam), Msg = 0x0112, HWnd = Handle };
			base.WndProc(ref m);
		}

		public void ProcessInput()
		{
			ControllerInputCoalescer conInput = Global.ControllerInputCoalescer as ControllerInputCoalescer;

			for (; ; )
			{

				// loop through all available events
				var ie = Input.Instance.DequeueEvent();
				if (ie == null) { break; }

				// look for hotkey bindings for this key
				var triggers = Global.ClientControls.SearchBindings(ie.LogicalButton.ToString());
				if (triggers.Count == 0)
				{
					// Maybe it is a system alt-key which hasnt been overridden
					if (ie.EventType == Input.InputEventType.Press)
					{
						if (ie.LogicalButton.Alt && ie.LogicalButton.Button.Length == 1)
						{
							var c = ie.LogicalButton.Button.ToLower()[0];
							if ((c >= 'a' && c <= 'z') || c == ' ')
							{
								SendAltKeyChar(c);
							}
						}
						if (ie.LogicalButton.Alt && ie.LogicalButton.Button == "Space")
						{
							SendPlainAltKey(32);
						}
					}
				}

				bool handled;
				switch (Global.Config.Input_Hotkey_OverrideOptions)
				{
					default:
					case 0: // Both allowed
						conInput.Receive(ie);

						handled = false;
						if (ie.EventType == Input.InputEventType.Press)
						{
							handled = triggers.Aggregate(handled, (current, trigger) => current | CheckHotkey(trigger));
						}

						// hotkeys which arent handled as actions get coalesced as pollable virtual client buttons
						if (!handled)
						{
							GlobalWin.HotkeyCoalescer.Receive(ie);
						}

						break;
					case 1: // Input overrides Hokeys
						conInput.Receive(ie);
						if (!Global.ActiveController.HasBinding(ie.LogicalButton.ToString()))
						{
							handled = false;
							if (ie.EventType == Input.InputEventType.Press)
							{
								handled = triggers.Aggregate(handled, (current, trigger) => current | CheckHotkey(trigger));
							}

							// hotkeys which arent handled as actions get coalesced as pollable virtual client buttons
							if (!handled)
							{
								GlobalWin.HotkeyCoalescer.Receive(ie);
							}
						}
						break;
					case 2: // Hotkeys override Input
						handled = false;
						if (ie.EventType == Input.InputEventType.Press)
						{
							handled = triggers.Aggregate(handled, (current, trigger) => current | CheckHotkey(trigger));
						}

						// hotkeys which arent handled as actions get coalesced as pollable virtual client buttons
						if (!handled)
						{
							GlobalWin.HotkeyCoalescer.Receive(ie);
							conInput.Receive(ie);
						}
						break;
				}
			}
		}

		public void ProgramRunLoop()
		{			
			CheckMessages();

			for (; ; )
			{
				Input.Instance.Update();

				// handle events and dispatch as a hotkey action, or a hotkey button, or an input button
				ProcessInput();
				Global.ClientControls.LatchFromPhysical(GlobalWin.HotkeyCoalescer);

				Global.ActiveController.LatchFromPhysical(Global.ControllerInputCoalescer);

				// TODO
				//Global.ActiveController.ApplyAxisConstraints(
				//	(Global.Emulator is N64 && Global.Config.N64UseCircularAnalogConstraint) ? "Natural Circle" : null);

				Global.ActiveController.OR_FromLogical(Global.ClickyVirtualPadController);
				Global.AutoFireController.LatchFromPhysical(Global.ControllerInputCoalescer);

				if (Global.ClientControls["Autohold"])
				{
					Global.StickyXORAdapter.MassToggleStickyState(Global.ActiveController.PressedButtons);
					Global.AutofireStickyXORAdapter.MassToggleStickyState(Global.AutoFireController.PressedButtons);
				}
				else if (Global.ClientControls["Autofire"])
				{
					Global.AutofireStickyXORAdapter.MassToggleStickyState(Global.ActiveController.PressedButtons);
				}

				// autohold/autofire must not be affected by the following inputs
				Global.ActiveController.Overrides(Global.LuaAndAdaptor);

				if (EmulatorWindows.Any())
				{
					StepRunLoop_Core();
					StepRunLoop_Throttle();

					foreach (var window in EmulatorWindows)
					{
						window.Render();
					}
				}

				CheckMessages();

				if (_exit)
				{
					break;
				}

				Thread.Sleep(0);
			}

			Shutdown();
		}

		private void Shutdown()
		{
			//TODO
			//if (_currAviWriter != null)
			//{
			//	_currAviWriter.CloseFile();
			//	_currAviWriter = null;
			//}
		}

		public void LoadQuickSave(string quickSlotName)
		{
			try
			{
				foreach (var window in EmulatorWindows)
				{
					window.LoadQuickSave(quickSlotName);
				}
			}
			catch
			{
				MessageBox.Show("Could not load " + quickSlotName);
			}
		}

		public void SaveQuickSave(string quickSlotName)
		{
			try
			{
				foreach (var window in EmulatorWindows)
				{
					window.SaveQuickSave(quickSlotName);
				}
			}
			catch
			{
				MessageBox.Show("Could not save " + quickSlotName);
			}
		}

		private void SelectSlot(int num)
		{
			Global.Config.SaveSlot = num;
			UpdateStatusSlots();
		}

		private void UpdateStatusSlots()
		{
			// TODO
			SaveSlotSelectedMessage();
			UpdateAfterFrameChanged();
		}

		private void SaveSlotSelectedMessage()
		{
			AddMessage("Slot " + Global.Config.SaveSlot + " selected.");
		}

		public void TogglePause()
		{
			EmulatorPaused ^= true;
			//SetPauseStatusbarIcon(); // TODO
		}
		private bool CheckHotkey(string trigger)
		{
			switch (trigger)
			{
				default:
					return false;

				case "Pause":
					TogglePause();
					break;
				case "Reboot Core":
					RebootCoresMenuItem_Click(null, null);
					break;
				case "Quick Load":
					LoadQuickSave("QuickSave" + Global.Config.SaveSlot);
					break;
				case "Quick Save":
					SaveQuickSave("QuickSave" + Global.Config.SaveSlot);
					break;

				// Save States
				case "Save State 0":
					SaveQuickSave("QuickSave0");
					Global.Config.SaveSlot = 0;
					UpdateStatusSlots();
					break;
				case "Save State 1":
					SaveQuickSave("QuickSave1");
					Global.Config.SaveSlot = 1;
					UpdateStatusSlots();
					break;
				case "Save State 2":
					SaveQuickSave("QuickSave2");
					Global.Config.SaveSlot = 2;
					UpdateStatusSlots();
					break;
				case "Save State 3":
					SaveQuickSave("QuickSave3");
					Global.Config.SaveSlot = 3;
					UpdateStatusSlots();
					break;
				case "Save State 4":
					SaveQuickSave("QuickSave4");
					Global.Config.SaveSlot = 4;
					UpdateStatusSlots();
					break;
				case "Save State 5":
					SaveQuickSave("QuickSave5");
					Global.Config.SaveSlot = 5;
					UpdateStatusSlots();
					break;
				case "Save State 6":
					SaveQuickSave("QuickSave6");
					Global.Config.SaveSlot = 6;
					UpdateStatusSlots();
					break;
				case "Save State 7":
					SaveQuickSave("QuickSave7");
					Global.Config.SaveSlot = 7;
					UpdateStatusSlots();
					break;
				case "Save State 8":
					SaveQuickSave("QuickSave8");
					Global.Config.SaveSlot = 8;
					UpdateStatusSlots();
					break;
				case "Save State 9":
					SaveQuickSave("QuickSave9");
					Global.Config.SaveSlot = 9;
					//UpdateStatusSlots();
					break;
				case "Load State 0":
					LoadQuickSave("QuickSave0");
					Global.Config.SaveSlot = 0;
					UpdateStatusSlots();
					break;
				case "Load State 1":
					LoadQuickSave("QuickSave1");
					Global.Config.SaveSlot = 1;
					UpdateStatusSlots();
					break;
				case "Load State 2":
					LoadQuickSave("QuickSave2");
					Global.Config.SaveSlot = 2;
					UpdateStatusSlots();
					break;
				case "Load State 3":
					LoadQuickSave("QuickSave3");
					Global.Config.SaveSlot = 3;
					UpdateStatusSlots();
					break;
				case "Load State 4":
					LoadQuickSave("QuickSave4");
					Global.Config.SaveSlot = 4;
					UpdateStatusSlots();
					break;
				case "Load State 5":
					LoadQuickSave("QuickSave5");
					Global.Config.SaveSlot = 5;
					UpdateStatusSlots();
					break;
				case "Load State 6":
					LoadQuickSave("QuickSave6");
					Global.Config.SaveSlot = 6;
					UpdateStatusSlots();
					break;
				case "Load State 7":
					LoadQuickSave("QuickSave7");
					Global.Config.SaveSlot = 7;
					break;
				case "Load State 8":
					LoadQuickSave("QuickSave8");
					Global.Config.SaveSlot = 8;
					UpdateStatusSlots();
					break;
				case "Load State 9":
					LoadQuickSave("QuickSave9");
					Global.Config.SaveSlot = 9;
					UpdateStatusSlots();
					break;

				case "Select State 0":
					SelectSlot(0);
					break;
				case "Select State 1":
					SelectSlot(1);
					break;
				case "Select State 2":
					SelectSlot(2);
					break;
				case "Select State 3":
					SelectSlot(3);
					break;
				case "Select State 4":
					SelectSlot(4);
					break;
				case "Select State 5":
					SelectSlot(5);
					break;
				case "Select State 6":
					SelectSlot(6);
					break;
				case "Select State 7":
					SelectSlot(7);
					break;
				case "Select State 8":
					SelectSlot(8);
					break;
				case "Select State 9":
					SelectSlot(9);
					break;

				// Movie
				case "Stop Movie":
					StopMovieMenuItem_Click(null, null);
					break;
				case "Toggle read-only":
					ToggleReadonlyMenuItem_Click(null, null);
					break;
				// TODO
				//case "Play from beginning":
				//	RestartMovie();
				//	break;
				// TODO
				//case "Save Movie":
				//	SaveMovie();
				//	break;
			}

			return true;
		}

		public bool PressFrameAdvance = false;
		public bool PressRewind = false;
		public bool FastForward = false;
		public bool TurboFastForward = false;
		public bool EmulatorPaused = true;
		private readonly BizHawk.Client.EmuHawk.Throttle _throttle;
		//private bool _unthrottled; // TODO
		private bool _runloopFrameadvance;
		private bool _runloopFrameProgress;
		private long _frameAdvanceTimestamp;
		private bool _runloopLastFf;
		private int _runloopFps;
		private long _runloopSecond;
		private int _runloopLastFps;

		public bool IsTurboing
		{
			get
			{
				return Global.ClientControls["Turbo"];
			}
		}

		private void SyncThrottle()
		{
			// "unthrottled" = throttle was turned off with "Toggle Throttle" hotkey
			// "turbo" = throttle is off due to the "Turbo" hotkey being held
			// They are basically the same thing but one is a toggle and the other requires a
			// hotkey to be held. There is however slightly different behavior in that turbo
			// skips outputting the audio. There's also a third way which is when no throttle
			// method is selected, but the clock throttle determines that by itself and
			// everything appears normal here.

			var fastForward = Global.ClientControls["Fast Forward"] || FastForward;
			var turbo = IsTurboing;

			int speedPercent = fastForward ? Global.Config.SpeedPercentAlternate : Global.Config.SpeedPercent;

			Global.DisableSecondaryThrottling = /*_unthrottled || TODO */ turbo || fastForward;

			// realtime throttle is never going to be so exact that using a double here is wrong
			_throttle.SetCoreFps(EmulatorWindows.Master.Emulator.CoreComm.VsyncRate);
			_throttle.signal_paused = EmulatorPaused;
			_throttle.signal_unthrottle = /*_unthrottled || TODO */ turbo;
			_throttle.signal_overrideSecondaryThrottle = fastForward && (Global.Config.SoundThrottle || Global.Config.VSyncThrottle || Global.Config.VSync);
			_throttle.SetSpeedPercent(speedPercent);
		}

		private void StepRunLoop_Throttle()
		{
			SyncThrottle();
			_throttle.signal_frameAdvance = _runloopFrameadvance;
			_throttle.signal_continuousframeAdvancing = _runloopFrameProgress;

			_throttle.Step(true, -1);
		}

		public void PauseEmulator()
		{
			EmulatorPaused = true;
			//SetPauseStatusbarIcon(); // TODO
		}

		public void UnpauseEmulator()
		{
			EmulatorPaused = false;
			//SetPauseStatusbarIcon(); // TODO
		}

		private void StepRunLoop_Core()
		{
			var runFrame = false;
			_runloopFrameadvance = false;
			var currentTimestamp = Stopwatch.GetTimestamp();
			double frameAdvanceTimestampDeltaMs = (double)(currentTimestamp - _frameAdvanceTimestamp) / Stopwatch.Frequency * 1000.0;
			bool frameProgressTimeElapsed = frameAdvanceTimestampDeltaMs >= Global.Config.FrameProgressDelayMs;

			if (Global.ClientControls["Frame Advance"])
			{
				// handle the initial trigger of a frame advance
				if (_frameAdvanceTimestamp == 0)
				{
					PauseEmulator();
					runFrame = true;
					_runloopFrameadvance = true;
					_frameAdvanceTimestamp = currentTimestamp;
				}
				else
				{
					// handle the timed transition from countdown to FrameProgress
					if (frameProgressTimeElapsed)
					{
						runFrame = true;
						_runloopFrameProgress = true;
						UnpauseEmulator();
					}
				}
			}
			else
			{
				// handle release of frame advance: do we need to deactivate FrameProgress?
				if (_runloopFrameProgress)
				{
					_runloopFrameProgress = false;
					PauseEmulator();
				}

				_frameAdvanceTimestamp = 0;
			}

			if (!EmulatorPaused)
			{
				runFrame = true;
			}

			if (runFrame)
			{
				var isFastForwarding = Global.ClientControls["Fast Forward"] || IsTurboing;
				var updateFpsString = _runloopLastFf != isFastForwarding;
				_runloopLastFf = isFastForwarding;
				_runloopFps++;

				if ((double)(currentTimestamp - _runloopSecond) / Stopwatch.Frequency >= 1.0)
				{
					_runloopLastFps = _runloopFps;
					_runloopSecond = currentTimestamp;
					_runloopFps = 0;
					updateFpsString = true;
				}

				if (updateFpsString)
				{
					var fps_string = _runloopLastFps + " fps";
					if (IsTurboing)
					{
						fps_string += " >>>>";
					}
					else if (isFastForwarding)
					{
						fps_string += " >>";
					}

					//GlobalWin.OSD.FPS = fps_string; // TODO
				}

				Global.MovieSession.HandleMovieOnFrameLoop();

				foreach (var window in EmulatorWindows)
				{
					window.FrameAdvance();
				}

				PressFrameAdvance = false;

				UpdateAfterFrameChanged();
			}
		}

		private void UpdateAfterFrameChanged()
		{
			if (EmulatorWindows.Any())
			{
				string frame = EmulatorWindows.Master.Emulator.Frame.ToString();

				if (Global.MovieSession.Movie.IsActive)
				{
					if (Global.MovieSession.Movie.IsFinished)
					{
						frame += string.Format(" / {0} (finished)", Global.MovieSession.Movie.FrameCount);
					}
					else if (Global.MovieSession.Movie.IsPlaying)
					{
						frame += string.Format(" / {0}", Global.MovieSession.Movie.FrameCount);
					}
				}

				FameStatusBarLabel.Text = frame;
			}
		}

		private void FileSubMenu_DropDownOpened(object sender, EventArgs e)
		{
			SaveSessionMenuItem.Enabled = !string.IsNullOrWhiteSpace(EmulatorWindows.SessionName);
			SaveSessionAsMenuItem.Enabled = EmulatorWindows.Any();
		}

		private void LoadRomFromRecent(string rom)
		{
			if (!LoadRom(rom))
			{
				Global.Config.RecentRoms.HandleLoadError(rom);
			}
		}

		private void MovieSubMenu_DropDownOpened(object sender, EventArgs e)
		{
			PlayMovieMenuItem.Enabled =
				RecordMovieMenuItem.Enabled =
				EmulatorWindows.Any();

			StopMovieMenuItem.Enabled =
				RestartMovieMenuItem.Enabled =
				Global.MovieSession.Movie.IsActive;
		}

		private void RecordMovieMenuItem_Click(object sender, EventArgs e)
		{
			new RecordMovie().ShowDialog();
			UpdateMainText();
			UpdateAfterFrameChanged();
		}

		private void PlayMovieMenuItem_Click(object sender, EventArgs e)
		{
			new PlayMovie().ShowDialog();
			UpdateMainText();
			UpdateAfterFrameChanged();
		}

		private void StopMovieMenuItem_Click(object sender, EventArgs e)
		{
			Global.MovieSession.StopMovie(true);
			SetMainformMovieInfo();
			UpdateMainText();
			UpdateAfterFrameChanged();
			//UpdateStatusSlots(); // TODO
		}

		private void ToggleReadonlyMenuItem_Click(object sender, EventArgs e)
		{
			Global.MovieSession.ReadOnly ^= true;
			if (Global.MovieSession.ReadOnly)
			{
				AddMessage("Movie is now Read-only");
			}
			else
			{
				AddMessage("Movie is now read+write");
			}
		}

		private void LoadMoviesFromRecent(string path)
		{
			if (File.Exists(path))
			{
				var movie = MovieService.Get(path);
				Global.MovieSession.ReadOnly = true;
				StartNewMovie(movie, false);
			}
			else
			{
				Global.Config.RecentMovies.HandleLoadError(path);
			}
		}

		public void SetMainformMovieInfo()
		{
			if (Global.MovieSession.Movie.IsPlaying)
			{
				PlayRecordStatusButton.Image = Properties.Resources.Play;
				PlayRecordStatusButton.ToolTipText = "Movie is in playback mode";
				PlayRecordStatusButton.Visible = true;
			}
			else if (Global.MovieSession.Movie.IsRecording)
			{
				PlayRecordStatusButton.Image = Properties.Resources.RecordHS;
				PlayRecordStatusButton.ToolTipText = "Movie is in record mode";
				PlayRecordStatusButton.Visible = true;
			}
			else if (!Global.MovieSession.Movie.IsActive)
			{
				PlayRecordStatusButton.Image = Properties.Resources.Blank;
				PlayRecordStatusButton.ToolTipText = "No movie is active";
				PlayRecordStatusButton.Visible = false;
			}
		}

		public bool StartNewMovie(IMovie movie, bool record)
		{
			if (movie.IsActive)
			{
				movie.Save();
			}

			try
			{
				Global.MovieSession.QueueNewMovie(movie, record, Global.Emulator);
			}
			catch (MoviePlatformMismatchException ex)
			{
				MessageBox.Show(this, ex.Message, "Movie/Platform Mismatch", MessageBoxButtons.OK, MessageBoxIcon.Error);
				return false;
			}

			RebootCoresMenuItem_Click(null, null);

			Global.Emulator = EmulatorWindows.Master.Emulator;

			if (Global.MovieSession.PreviousNES_InQuickNES.HasValue)
			{
				Global.Config.NES_InQuickNES = Global.MovieSession.PreviousNES_InQuickNES.Value;
				Global.MovieSession.PreviousNES_InQuickNES = null;
			}

			if (Global.MovieSession.PreviousSNES_InSnes9x.HasValue)
			{
				Global.Config.SNES_InSnes9x = Global.MovieSession.PreviousSNES_InSnes9x.Value;
				Global.MovieSession.PreviousSNES_InSnes9x = null;
			}

			if (Global.MovieSession.PreviousGBA_UsemGBA.HasValue)
			{
				Global.Config.GBA_UsemGBA = Global.MovieSession.PreviousGBA_UsemGBA.Value;
				Global.MovieSession.PreviousGBA_UsemGBA = null;
			}

			Global.Config.RecentMovies.Add(movie.Filename);

			if (EmulatorWindows.Master.Emulator.HasSavestates() && movie.StartsFromSavestate)
			{
				if (movie.TextSavestate != null)
				{
					EmulatorWindows.Master.Emulator.AsStatable().LoadStateText(new StringReader(movie.TextSavestate));
				}
				else
				{
					EmulatorWindows.Master.Emulator.AsStatable().LoadStateBinary(new BinaryReader(new MemoryStream(movie.BinarySavestate, false)));
				}

				foreach (var ew in EmulatorWindows)
				{
					ew.Emulator.ResetCounters();
				}
			}

			Global.MovieSession.RunQueuedMovie(record);

			SetMainformMovieInfo();
			UpdateAfterFrameChanged();
			return true;
		}

		private void hotkeyConfigToolStripMenuItem_Click(object sender, EventArgs e)
		{
			if (new BizHawk.Client.EmuHawk.HotkeyConfig().ShowDialog() == DialogResult.OK)
			{
				InitControls();
				_inputManager.SyncControls();
			}
		}

		private void controllerConfigToolStripMenuItem_Click(object sender, EventArgs e)
		{
			var controller = new BizHawk.Client.EmuHawk.ControllerConfig(EmulatorWindows.Master.Emulator.ControllerDefinition);
			if (controller.ShowDialog() == DialogResult.OK)
			{
				InitControls();
				_inputManager.SyncControls();
			}
		}

		public void EmulatorWindowClosed(EmulatorWindow ew)
		{
			EmulatorWindows.Remove(ew);
			WorkspacePanel.Controls.Remove(ew);

			if (ew.Emulator == Global.Emulator)
			{
				if (EmulatorWindows.Any())
				{
					Global.Emulator = EmulatorWindows.Master.Emulator;
				}
				else
				{
					Global.Emulator = null;
				}
			}
		}

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

		private void saveConfigToolStripMenuItem_Click(object sender, EventArgs e)
		{
			SaveConfig();
			AddMessage("Saved settings");
		}

		private void RebootCoresMenuItem_Click(object sender, EventArgs e)
		{
			foreach (var ew in EmulatorWindows)
			{
				ReloadRom(ew);
			}

			AddMessage("Rebooted all cores");
		}

		private void SaveSessionMenuItem_Click(object sender, EventArgs e)
		{
			if (!string.IsNullOrWhiteSpace(EmulatorWindows.SessionName))
			{
				File.WriteAllText(EmulatorWindows.SessionName, EmulatorWindows.SessionJson);
				AddMessage("Session saved.");
			}
		}

		private void SaveSessionAsMenuItem_Click(object sender, EventArgs e)
		{
			if (EmulatorWindows.Any())
			{
				var file = GetSaveFileFromUser();
				if (file != null)
				{
					EmulatorWindows.SessionName = file.FullName;
					Global.Config.RecentRomSessions.Add(file.FullName);
					SaveSessionMenuItem_Click(sender, e);
					UpdateMainText();
				}
			}
		}

		private FileInfo GetSaveFileFromUser()
		{
			var sfd = new SaveFileDialog();
			if (!string.IsNullOrWhiteSpace(EmulatorWindows.SessionName))
			{
				sfd.FileName = Path.GetFileNameWithoutExtension(EmulatorWindows.SessionName);
				sfd.InitialDirectory = Path.GetDirectoryName(EmulatorWindows.SessionName);
			}
			else if (EmulatorWindows.Master != null)
			{
				sfd.FileName = PathManager.FilesystemSafeName(EmulatorWindows.Master.Game);
				sfd.InitialDirectory = PathManager.GetRomsPath("Global");
			}
			else
			{
				sfd.FileName = "NULL";
				sfd.InitialDirectory = PathManager.GetRomsPath("Global");
			}

			sfd.Filter = "Rom Session Files (*.romses)|*.romses|All Files|*.*";
			sfd.RestoreDirectory = true;
			var result = sfd.ShowDialog();
			if (result != DialogResult.OK)
			{
				return null;
			}

			return new FileInfo(sfd.FileName);
		}

		private void OpenSessionMenuItem_Click(object sender, EventArgs e)
		{
			var file = GetFileFromUser("Rom Session Files (*.romses)|*.romses|All Files|*.*");
			if (file != null)
			{
				NewSessionMenuItem_Click(null, null);
				var json = File.ReadAllText(file.FullName);
				EmulatorWindows.SessionName = file.FullName;
				LoadRomSession(EmulatorWindowList.FromJson(json));
				Global.Config.RecentRomSessions.Add(file.FullName);
				UpdateMainText();
			}
		}

		private void UpdateMainText()
		{
			string text = "MultiHawk";

			if (!string.IsNullOrWhiteSpace(EmulatorWindows.SessionName))
			{
				text += " - " + Path.GetFileNameWithoutExtension(EmulatorWindows.SessionName);
			}

			if (Global.MovieSession.Movie.IsActive)
			{
				text += " - " + Path.GetFileNameWithoutExtension(Global.MovieSession.Movie.Filename);
			}

			Text = text;
		}

		private static FileInfo GetFileFromUser(string filter)
		{
			var ofd = new OpenFileDialog
			{
				InitialDirectory = PathManager.GetRomsPath("Global"),
				Filter = filter,
				RestoreDirectory = true
			};

			if (!Directory.Exists(ofd.InitialDirectory))
			{
				Directory.CreateDirectory(ofd.InitialDirectory);
			}

			var result = ofd.ShowDialog();
			return result == DialogResult.OK ? new FileInfo(ofd.FileName) : null;
		}

		private void CloseAllWindows()
		{
			foreach (var ew in EmulatorWindows.ToList())
			{
				ew.Close();
			}

			EmulatorWindows.Clear();
		}

		private void NewSessionMenuItem_Click(object sender, EventArgs e)
		{
			foreach (var ew in EmulatorWindows.ToList())
			{
				ew.Close();
			}

			EmulatorWindows.Clear();
			UpdateMainText();
		}

		private void LoadRomSession(IEnumerable<EmulatorWindowList.RomSessionEntry> entries)
		{
			foreach (var entry in entries)
			{
				LoadRom(entry.RomName);
				EmulatorWindows.Last().Location = new Point(entry.Wndx, entry.Wndy);
				UpdateMainText();
			}
		}

		private void LoadRomSessionFromRecent(string path)
		{
			var file = new FileInfo(path);
			if (file.Exists)
			{
				NewSessionMenuItem_Click(null, null);
				var json = File.ReadAllText(file.FullName);
				EmulatorWindows.SessionName = file.FullName;
				LoadRomSession(EmulatorWindowList.FromJson(json));
				Global.Config.RecentRomSessions.Add(file.FullName);
				UpdateMainText();
			}
			else
			{
				Global.Config.RecentRomSessions.HandleLoadError(path);
			}
		}

		private void RecentSessionSubMenu_DropDownOpened(object sender, EventArgs e)
		{
			RecentSessionSubMenu.DropDownItems.Clear();
			RecentSessionSubMenu.DropDownItems.AddRange(
				Global.Config.RecentRomSessions.RecentMenu(LoadRomSessionFromRecent, autoload: true));
		}

		private void RecentRomSubMenu_DropDownOpened(object sender, EventArgs e)
		{
			RecentRomSubMenu.DropDownItems.Clear();
			RecentRomSubMenu.DropDownItems.AddRange(
				Global.Config.RecentRoms.RecentMenu(LoadRomFromRecent, autoload: false));
		}

		private void ViewSubMenu_DropDownOpened(object sender, EventArgs e)
		{
			_1xMenuItem.Checked = Global.Config.TargetZoomFactor == 1;
			_2xMenuItem.Checked = Global.Config.TargetZoomFactor == 2;
			_3xMenuItem.Checked = Global.Config.TargetZoomFactor == 3;
			_4xMenuItem.Checked = Global.Config.TargetZoomFactor == 4;
		}

		private void _1xMenuItem_Click(object sender, EventArgs e)
		{
			Global.Config.TargetZoomFactor = 1;
			ReRenderAllWindows();
		}

		private void _2xMenuItem_Click(object sender, EventArgs e)
		{
			Global.Config.TargetZoomFactor = 2;
			ReRenderAllWindows();
		}

		private void _3xMenuItem_Click(object sender, EventArgs e)
		{
			Global.Config.TargetZoomFactor = 3;
			ReRenderAllWindows();
		}

		private void _4xMenuItem_Click(object sender, EventArgs e)
		{
			Global.Config.TargetZoomFactor = 4;
			ReRenderAllWindows();
		}

		private void ReRenderAllWindows()
		{
			foreach (var ew in EmulatorWindows)
			{
				ew.FrameBufferResized();
				ew.Render();
			}
		}

		private void LoadLastMovieMenuItem_Click(object sender, EventArgs e)
		{
			LoadMoviesFromRecent(Global.Config.RecentMovies.MostRecent);
		}

		private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
		{
			LoadLastMovieContextMenuItem.Visible = !Global.Config.RecentMovies.Empty;
			PlayMovieContextMenuItem.Visible =
				RecordMovieContextMenuItem.Visible =
				!Global.MovieSession.Movie.IsActive;

			StopMovieContextMenuItem.Visible =
				RestartMovieContextMenuItem.Visible =
				Global.MovieSession.Movie.IsActive;

		}

		private void RecentMovieSubMenu_DropDownOpened(object sender, EventArgs e)
		{
			RecentMovieSubMenu.DropDownItems.Clear();
			RecentMovieSubMenu.DropDownItems.AddRange(
				Global.Config.RecentMovies.RecentMenu(LoadMoviesFromRecent, autoload: true));
		}

		private void RestartMovieMenuItem_Click(object sender, EventArgs e)
		{
			if (Global.MovieSession.Movie.IsActive)
			{
				StartNewMovie(Global.MovieSession.Movie, false);
				AddMessage("Replaying movie file in read-only mode");
			}
		}
	}
}
