using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Reflection;

namespace BizHawk.Emulation.Cores
{
	/// <summary>
	/// libretro related shims
	/// </summary>
	public class LibRetro : IDisposable
	{
		public const int RETRO_API_VERSION = 1;

		public enum RETRO_DEVICE
		{
			NONE = 0,
			JOYPAD = 1,
			MOUSE = 2,
			KEYBOARD = 3,
			LIGHTGUN = 4,
			ANALOG = 5,
			POINTER = 6,
			SENSOR_ACCELEROMETER = 7
		};

		public enum RETRO_DEVICE_ID_JOYPAD
		{
			B = 0,
			Y = 1,
			SELECT = 2,
			START = 3,
			UP = 4,
			DOWN = 5,
			LEFT = 6,
			RIGHT = 7,
			A = 8,
			X = 9,
			L = 10,
			R = 11,
			L2 = 12,
			R2 = 13,
			L3 = 14,
			R3 = 15
		};

		public enum RETRO_DEVICE_ID_ANALOG
		{
			// LEFT / RIGHT?
			X = 0,
			Y = 1
		};

		public enum RETRO_DEVICE_ID_MOUSE
		{
			X = 0,
			Y = 1,
			LEFT = 2,
			RIGHT = 3
		};

		public enum RETRO_DEVICE_ID_LIGHTGUN
		{
			X = 0,
			Y = 1,
			TRIGGER = 2,
			CURSOR = 3,
			TURBO = 4,
			PAUSE = 5,
			START = 6
		};

		public enum RETRO_DEVICE_ID_POINTER
		{
			X = 0,
			Y = 1,
			PRESSED = 2
		};

		public enum RETRO_DEVICE_ID_SENSOR_ACCELEROMETER
		{
			X = 0,
			Y = 1,
			Z = 2
		};

		public enum RETRO_REGION
		{
			NTSC = 0,
			PAL = 1
		};

		public enum RETRO_MEMORY
		{
			SAVE_RAM = 0,
			RTC = 1,
			SYSTEM_RAM = 2,
			VIDEO_RAM = 3,
		};

		public enum RETRO_KEY
		{
			   UNKNOWN = 0,
   FIRST = 0,
   BACKSPACE = 8,
   TAB = 9,
   CLEAR = 12,
   RETURN = 13,
   PAUSE = 19,
   ESCAPE = 27,
   SPACE = 32,
   EXCLAIM = 33,
   QUOTEDBL = 34,
   HASH = 35,
   DOLLAR = 36,
   AMPERSAND = 38,
   QUOTE = 39,
   LEFTPAREN = 40,
   RIGHTPAREN = 41,
   ASTERISK = 42,
   PLUS = 43,
   COMMA = 44,
   MINUS = 45,
   PERIOD = 46,
   SLASH = 47,
   _0 = 48,
   _1 = 49,
   _2 = 50,
   _3 = 51,
   _4 = 52,
   _5 = 53,
   _6 = 54,
   _7 = 55,
   _8 = 56,
   _9 = 57,
   COLON = 58,
   SEMICOLON = 59,
   LESS = 60,
   EQUALS = 61,
   GREATER = 62,
   QUESTION = 63,
   AT = 64,
   LEFTBRACKET = 91,
   BACKSLASH = 92,
   RIGHTBRACKET = 93,
   CARET = 94,
   UNDERSCORE = 95,
   BACKQUOTE = 96,
   a = 97,
   b = 98,
   c = 99,
   d = 100,
   e = 101,
   f = 102,
   g = 103,
   h = 104,
   i = 105,
   j = 106,
   k = 107,
   l = 108,
   m = 109,
   n = 110,
   o = 111,
   p = 112,
   q = 113,
   r = 114,
   s = 115,
   t = 116,
   u = 117,
   v = 118,
   w = 119,
   x = 120,
   y = 121,
   z = 122,
   DELETE = 127,

   KP0 = 256,
   KP1 = 257,
   KP2 = 258,
   KP3 = 259,
   KP4 = 260,
   KP5 = 261,
   KP6 = 262,
   KP7 = 263,
   KP8 = 264,
   KP9 = 265,
   KP_PERIOD = 266,
   KP_DIVIDE = 267,
   KP_MULTIPLY = 268,
   KP_MINUS = 269,
   KP_PLUS = 270,
   KP_ENTER = 271,
   KP_EQUALS = 272,

   UP = 273,
   DOWN = 274,
   RIGHT = 275,
   LEFT = 276,
   INSERT = 277,
   HOME = 278,
   END = 279,
   PAGEUP = 280,
   PAGEDOWN = 281,

   F1 = 282,
   F2 = 283,
   F3 = 284,
   F4 = 285,
   F5 = 286,
   F6 = 287,
   F7 = 288,
   F8 = 289,
   F9 = 290,
   F10 = 291,
   F11 = 292,
   F12 = 293,
   F13 = 294,
   F14 = 295,
   F15 = 296,

   NUMLOCK = 300,
   CAPSLOCK = 301,
   SCROLLOCK = 302,
   RSHIFT = 303,
   LSHIFT = 304,
   RCTRL = 305,
   LCTRL = 306,
   RALT = 307,
   LALT = 308,
   RMETA = 309,
   LMETA = 310,
   LSUPER = 311,
   RSUPER = 312,
   MODE = 313,
   COMPOSE = 314,

   HELP = 315,
   PRINT = 316,
   SYSREQ = 317,
   BREAK = 318,
   MENU = 319,
   POWER = 320,
   EURO = 321,
   UNDO = 322,

   LAST
		};

		[Flags]
		public enum RETRO_MOD
		{
			NONE = 0,
			SHIFT = 1,
			CTRL = 2,
			ALT = 4,
			META = 8,
			NUMLOCK = 16,
			CAPSLOCK = 32,
			SCROLLLOCK = 64
		};

		public enum RETRO_ENVIRONMENT
		{
			SET_ROTATION = 1,
			GET_OVERSCAN = 2,
			GET_CAN_DUPE = 3,
			SET_MESSAGE = 6,
			SHUTDOWN = 7,
			SET_PERFORMANCE_LEVEL = 8,
			GET_SYSTEM_DIRECTORY = 9,
			SET_PIXEL_FORMAT = 10,
			SET_INPUT_DESCRIPTORS = 11,
			SET_KEYBOARD_CALLBACK = 12,
			SET_DISK_CONTROL_INTERFACE = 13,
			SET_HW_RENDER = 14,
			GET_VARIABLE = 15,
			SET_VARIABLES = 16,
			GET_VARIABLE_UPDATE = 17,
			SET_SUPPORT_NO_GAME = 18,
			GET_LIBRETRO_PATH = 19,
			SET_AUDIO_CALLBACK = 22,
			SET_FRAME_TIME_CALLBACK = 21,
			GET_RUMBLE_INTERFACE = 23,
			GET_INPUT_DEVICE_CAPABILITIES = 24,
		};

		public enum RETRO_PIXEL_FORMAT
		{
			XRGB1555 = 0,
			XRGB8888 = 1,
			RGB565 = 2
		};

		public struct retro_message
		{
			public string msg;
			public uint frames;
		}

		public struct retro_input_descriptor
		{
			public uint port;
			public uint device;
			public uint index;
			public uint id;
		}

		public struct retro_system_info
		{
			public string library_name;
			public string library_version;
			public string valid_extensions;
			[MarshalAs(UnmanagedType.U1)]
			public bool need_fullpath;
			[MarshalAs(UnmanagedType.U1)]
			public bool block_extract;
		}

		public struct retro_game_geometry
		{
			public uint base_width;
			public uint base_height;
			public uint max_width;
			public uint max_height;
			public float aspect_ratio;
		}

		public struct retro_system_timing
		{
			public double fps;
			public double sample_rate;
		}

		public struct retro_system_av_info
		{
			public retro_game_geometry geometry;
			public retro_system_timing timing;
		}

		public struct retro_variable
		{
			public string key;
			public string value;
		}

		public struct retro_game_info
		{
			public string path;
			public IntPtr data;
			public uint size;
			public string meta;
		}

		#region callback prototypes
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		[return:MarshalAs(UnmanagedType.U1)]
		public delegate bool retro_environment_t(RETRO_ENVIRONMENT cmd, IntPtr data);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void retro_video_refresh_t(IntPtr data, uint width, uint height, uint pitch);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void retro_audio_sample_t(short left, short right);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate uint retro_audio_sample_batch_t(IntPtr data, uint frames);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void retro_input_poll_t();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate short retro_input_state_t(uint port, uint device, uint index, uint id);
		#endregion

		#region entry point prototypes
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_set_environment(retro_environment_t cb);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_set_video_refresh(retro_video_refresh_t cb);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_set_audio_sample(retro_audio_sample_t cb);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_set_audio_sample_batch(retro_audio_sample_batch_t cb);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_set_input_poll(retro_input_poll_t cb);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_set_input_state(retro_input_state_t cb);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_init();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_deinit();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate uint epretro_api_version();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_get_system_info(ref retro_system_info info);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_get_system_av_info(ref retro_system_av_info info);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_set_controller_port_device(uint port, uint device);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_reset();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_run();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate uint epretro_serialize_size();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		[return: MarshalAs(UnmanagedType.U1)]
		public delegate bool epretro_serialize(IntPtr data, uint size);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		[return: MarshalAs(UnmanagedType.U1)]
		public delegate bool epretro_unserialize(IntPtr data, uint size);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_cheat_reset();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_cheat_set(uint index, [MarshalAs(UnmanagedType.U1)]bool enabled, string code);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		[return: MarshalAs(UnmanagedType.U1)]
		public delegate bool epretro_load_game(ref retro_game_info game);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		[return: MarshalAs(UnmanagedType.U1)]
		public delegate bool epretro_load_game_special(uint game_type, ref retro_game_info info, uint num_info);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate void epretro_unload_game();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate uint epretro_get_region();
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate IntPtr epretro_get_memory_data(RETRO_MEMORY id);
		[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
		public delegate uint epretro_get_memory_size(RETRO_MEMORY id);
		#endregion
		#region entry points
		// these are all hooked up by reflection on dll load
		public epretro_set_environment retro_set_environment;
		public epretro_set_video_refresh retro_set_video_refresh;
		public epretro_set_audio_sample retro_set_audio_sample;
		public epretro_set_audio_sample_batch retro_set_audio_sample_batch;
		public epretro_set_input_poll retro_set_input_poll;
		public epretro_set_input_state retro_set_input_state;
		public epretro_init retro_init;
		/// <summary>
		/// Dispose() calls this, so you shouldn't
		/// </summary>
		public epretro_deinit retro_deinit;
		public epretro_api_version retro_api_version;
		public epretro_get_system_info retro_get_system_info;
		public epretro_get_system_av_info retro_get_system_av_info;
		public epretro_set_controller_port_device retro_set_controller_port_device;
		public epretro_reset retro_reset;
		public epretro_run retro_run;
		public epretro_serialize_size retro_serialize_size;
		public epretro_serialize retro_serialize;
		public epretro_unserialize retro_unserialize;
		public epretro_cheat_reset retro_cheat_reset;
		public epretro_cheat_set retro_cheat_set;
		public epretro_load_game retro_load_game;
		public epretro_load_game_special retro_load_game_special;
		public epretro_unload_game retro_unload_game;
		public epretro_get_region retro_get_region;
		public epretro_get_memory_data retro_get_memory_data;
		public epretro_get_memory_size retro_get_memory_size;
		#endregion

		private static Dictionary<IntPtr, LibRetro> AttachedCores = new Dictionary<IntPtr, LibRetro>();
		private IntPtr hModule = IntPtr.Zero;

		public void Dispose()
		{
			// like many other emu cores, libretros are in general single instance, so we track some things
			lock (AttachedCores)
			{
				if (hModule != IntPtr.Zero)
				{
					retro_deinit();
					ClearAllEntryPoints();
					Win32.FreeLibrary(hModule);
					AttachedCores.Remove(hModule);
					hModule = IntPtr.Zero;
				}
			}
		}

		public LibRetro(string modulename)
		{
			// like many other emu cores, libretros are in general single instance, so we track some things
			lock (AttachedCores)
			{
				IntPtr newmodule = Win32.LoadLibrary(modulename);
				if (newmodule == IntPtr.Zero)
					throw new Exception(string.Format("LoadLibrary(\"{0}\") returned NULL", modulename));

				if (AttachedCores.ContainsKey(newmodule))
				{
					// this core is already loaded, so we must detatch the old instance
					LibRetro martyr = AttachedCores[newmodule];
					martyr.retro_deinit();
					martyr.ClearAllEntryPoints();
					martyr.hModule = IntPtr.Zero;
					Win32.FreeLibrary(newmodule); // decrease ref count by 1
				}
				AttachedCores[newmodule] = this;
				hModule = newmodule;
				if (!ConnectAllEntryPoints())
				{
					ClearAllEntryPoints();
					Win32.FreeLibrary(hModule);
					hModule = IntPtr.Zero;
					throw new Exception("ConnectAllEntryPoints() failed.  The console may contain more details.");
				}
			}
		}

		private static class Win32
		{
			[DllImport("kernel32.dll")]
			public static extern IntPtr LoadLibrary(string dllToLoad);
			[DllImport("kernel32.dll")]
			public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
			[DllImport("kernel32.dll")]
			public static extern bool FreeLibrary(IntPtr hModule);
		}

		private static IEnumerable<FieldInfo> GetAllEntryPoints()
		{
			return typeof(LibRetro).GetFields().Where((field) => field.FieldType.Name.StartsWith("epretro"));
		}

		private void ClearAllEntryPoints()
		{
			foreach (var field in GetAllEntryPoints())
			{
				field.SetValue(this, null);
			}
		}

		private bool ConnectAllEntryPoints()
		{
			bool succeed = true;
			foreach (var field in GetAllEntryPoints())
			{
				string fieldname = field.Name;
				IntPtr entry = Win32.GetProcAddress(hModule, fieldname);
				if (entry != IntPtr.Zero)
				{
					field.SetValue(this, Marshal.GetDelegateForFunctionPointer(entry, field.FieldType));
				}
				else
				{
					Console.WriteLine("Couldn't bind libretro entry point {0}", fieldname);
					succeed = false;
				}
			}
			return succeed;
		}
	}
}
