/*
 * @author Valentin Simonov / http://va.lent.in/
 * @author Valentin Frolov
 * @author Andrew David Griffiths
 * @author Cryville:Lime
 */
using AOT;
using System;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;

namespace Cryville.Common.Unity.Input {
	public class WindowsPointerHandler : InputHandler {
		static WindowsPointerHandler Instance;

		const string PRESS_AND_HOLD_ATOM = "MicrosoftTabletPenServiceProperty";
		delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

		public const int MONITOR_DEFAULTTONEAREST = 2;
		readonly IntPtr hMainWindow;
		IntPtr oldWndProcPtr;
		IntPtr newWndProcPtr;
		WndProcDelegate newWndProc;
		ushort pressAndHoldAtomID;

		readonly int touchInputSize;
		readonly long freq;
		static bool usePointerMessage;

		public WindowsPointerHandler() {
			if (Instance != null)
				throw new InvalidOperationException("WindowsPointerHandler already created");
			Instance = this;
			usePointerMessage = true;

			hMainWindow = GetUnityWindowHandle();
			if (hMainWindow == IntPtr.Zero) {
				throw new InvalidOperationException("Cannot find the game window");
			}
			NativeMethods.QueryPerformanceFrequency(out freq);
		retry:
			if (usePointerMessage) {
				try {
					NativeMethods.EnableMouseInPointer(true);
					Logger.Log("main", 0, "Input", "Touch input frequency: {0}", freq);
				}
				catch (TypeLoadException) {
					usePointerMessage = false;
					Logger.Log("main", 2, "Input", "Advanced touch handling not supported");
					goto retry;
				}
			}
			else {
				touchInputSize = Marshal.SizeOf(typeof(NativeMethods.TOUCHINPUT));
			}
		}

		public const int TABLET_DISABLE_PRESSANDHOLD = 0x00000001;
		public const int TABLET_DISABLE_PENTAPFEEDBACK = 0x00000008;
		public const int TABLET_DISABLE_PENBARRELFEEDBACK = 0x00000010;
		public const int TABLET_DISABLE_FLICKS = 0x00010000;

		protected override void ActivateImpl() {
			newWndProc = WndProc;
			newWndProcPtr = Marshal.GetFunctionPointerForDelegate(newWndProc);
			oldWndProcPtr = SetWindowLongPtr(hMainWindow, -4, newWndProcPtr);
			if (!NativeMethods.RegisterTouchWindow(hMainWindow, NativeMethods.TOUCH_WINDOW_FLAGS.TWF_WANTPALM)) {
				throw new InvalidOperationException("Failed to register touch window");
			}
			pressAndHoldAtomID = NativeMethods.GlobalAddAtom(PRESS_AND_HOLD_ATOM);
			NativeMethods.SetProp(hMainWindow, PRESS_AND_HOLD_ATOM,
				TABLET_DISABLE_PRESSANDHOLD | // disables press and hold (right-click) gesture
				TABLET_DISABLE_PENTAPFEEDBACK | // disables UI feedback on pen up (waves)
				TABLET_DISABLE_PENBARRELFEEDBACK | // disables UI feedback on pen button down (circle)
				TABLET_DISABLE_FLICKS // disables pen flicks (back, forward, drag down, drag up);
			);
		}

		protected override void DeactivateImpl() {
			if (pressAndHoldAtomID != 0) {
				NativeMethods.RemoveProp(hMainWindow, PRESS_AND_HOLD_ATOM);
				NativeMethods.GlobalDeleteAtom(pressAndHoldAtomID);
			}
			if (!NativeMethods.UnregisterTouchWindow(hMainWindow)) {
				throw new InvalidOperationException("Failed to unregister touch window");
			}
			SetWindowLongPtr(hMainWindow, -4, oldWndProcPtr);
			newWndProcPtr = IntPtr.Zero;
			newWndProc = null;
		}

		const string UnityWindowClassName = "UnityWndClass";

		static IntPtr _windowHandle;
		public static IntPtr GetUnityWindowHandle() {
			uint threadId = NativeMethods.GetCurrentThreadId();
			NativeMethods.EnumThreadWindows(
				threadId,
				UnityWindowFilter,
				IntPtr.Zero
			);
			return _windowHandle;
		}

		[MonoPInvokeCallback(typeof(NativeMethods.EnumWindowsProc))]
		public static bool UnityWindowFilter(IntPtr hWnd, IntPtr lParam) {
			var classText = new StringBuilder(UnityWindowClassName.Length + 1);
			NativeMethods.GetClassName(hWnd, classText, classText.Capacity);
			if (classText.ToString() == UnityWindowClassName) {
				_windowHandle = hWnd;
				return false;
			}
			return true;
		}

		public override bool IsNullable(int type) {
			return true;
		}

		public override byte GetDimension(int type) {
			return 2;
		}

		public override string GetTypeName(int type) {
			switch (type) {
				case 0: return "Windows Pointer Unknown";
				case 1: return "Windows Pointer Pointer";
				case 2: return "Windows Pointer Touch";
				case 3: return "Windows Pointer Pen";
				case 4: return "Windows Pointer Mouse";
				case 5: return "Windows Pointer Touch Pad";
				case 255: return "Windows Touch";
				default: throw new ArgumentOutOfRangeException(nameof(type));
			}
		}

		public override double GetCurrentTimestamp() {
			long tick;
			NativeMethods.QueryPerformanceCounter(out tick);
			return (double)tick / freq;
		}

		public override void Dispose(bool disposing) {
			DeactivateImpl();
			if (usePointerMessage)
				NativeMethods.EnableMouseInPointer(false);
			Instance = null;
		}

		[MonoPInvokeCallback(typeof(WndProcDelegate))]
		static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) {
			try {
				var m = (WindowMessages)msg;
				/*if (m >= WindowMessages.WM_MOUSEFIRST && m <= WindowMessages.WM_MOUSELAST) {
					if (!usePointerMessage)
						Instance.handleMouseMsg(msg, wParam, lParam);
				}
				else*/
				switch (m) {
					case WindowMessages.WM_TOUCH:
						if (usePointerMessage)
							NativeMethods.CloseTouchInputHandle(lParam);
						else
							Instance.HandleTouchMsg(wParam, lParam);
						break;
					case WindowMessages.WM_POINTERDOWN:
					case WindowMessages.WM_POINTERUP:
					case WindowMessages.WM_POINTERUPDATE:
						if (usePointerMessage)
							Instance.HandlePointerMsg(msg, wParam, lParam);
						break;
					case WindowMessages.WM_CLOSE:
						Instance.Dispose();
						// Calling oldWndProcPtr here would crash the application
						Application.Quit();
						return IntPtr.Zero;
				}
				return NativeMethods.CallWindowProc(Instance.oldWndProcPtr, hWnd, msg, wParam, lParam);
			}
			catch (Exception ex) {
				Logger.Log("main", 4, "Input", "An error occured while handling pointer:\n{0}", ex);
				return IntPtr.Zero;
			}
		}

		void HandlePointerMsg(uint msg, IntPtr wParam, IntPtr lParam) {
			int id = LOWORD(wParam.ToInt32());

			var rawpinfo = new NativeMethods.POINTER_INFO();
			if (!NativeMethods.GetPointerInfo(id, ref rawpinfo))
				throw new InvalidOperationException("Failed to get pointer info");

			/*
			Vector2? _cs = null;
			uint? orientation = null;
			uint? pressure = null;
			if (pointerInfo.pointerType == NativeMethods.POINTER_INPUT_TYPE.PT_TOUCH) {
				var pointerTouchInfo = new NativeMethods.POINTER_TOUCH_INFO();
				NativeMethods.GetPointerTouchInfo(pointerId, ref pointerTouchInfo);
				if (pointerTouchInfo.touchMask.HasFlag(NativeMethods.TOUCH_MASK.TOUCH_MASK_CONTACTAREA)) {
					Vector2 cs;
					cs.x = pointerTouchInfo.rcContact.Width;
					cs.y = pointerTouchInfo.rcContact.Height;
					_cs = cs;
				}
				if (pointerTouchInfo.touchMask.HasFlag(NativeMethods.TOUCH_MASK.TOUCH_MASK_ORIENTATION)) {
					orientation = pointerTouchInfo.orientation;
				}
				if (pointerTouchInfo.touchMask.HasFlag(NativeMethods.TOUCH_MASK.TOUCH_MASK_PRESSURE)) {
					pressure = pointerTouchInfo.pressure;
				}
			}*/

			NativeMethods.POINT p = rawpinfo.ptPixelLocation;
			NativeMethods.ScreenToClient(hMainWindow, ref p);
			Vector2 _p = UnityCameraUtils.ScreenToWorldPoint(new Vector2(p.X, Screen.height - p.Y));

			double time = (double)rawpinfo.PerformanceCount / freq;

			int type;
			switch (rawpinfo.pointerType) {
				case NativeMethods.POINTER_INPUT_TYPE.PT_POINTER : type = 1; break;
				case NativeMethods.POINTER_INPUT_TYPE.PT_TOUCH   : type = 2; break;
				case NativeMethods.POINTER_INPUT_TYPE.PT_PEN     : type = 3; break;
				case NativeMethods.POINTER_INPUT_TYPE.PT_MOUSE   : type = 4; break;
				case NativeMethods.POINTER_INPUT_TYPE.PT_TOUCHPAD: type = 5; break;
				default: type = 0; break;
			}
			if (rawpinfo.pointerFlags.HasFlag(NativeMethods.POINTER_FLAGS.POINTER_FLAG_CANCELED)) {
				Feed(type, id, new InputVector(time));
				return;
			}

			InputVector vec = new InputVector(time, _p);

			switch ((WindowMessages)msg) {
				case WindowMessages.WM_POINTERDOWN:
				case WindowMessages.WM_POINTERUPDATE:
					Feed(type, id, vec);
					break;
				case WindowMessages.WM_POINTERUP:
					Feed(type, id, vec);
					Feed(type, id, new InputVector(time));
					break;
			}
		}

		/*void handleMouseMsg(uint msg, IntPtr wParam, IntPtr lParam) {
			int id = mouseId;
			double procTime = diag::Stopwatch.GetTimestamp()
				/ (double)diag::Stopwatch.Frequency;
			Vector2 _p;
			_p.x = ((int)(short)LOWORD(lParam.ToInt32()));
			_p.y = ((int)(short)HIWORD(lParam.ToInt32()));
			PointerPhase phase;
			switch ((WindowMessages)msg) {
				case WindowMessages.WM_LBUTTONDOWN:
				case WindowMessages.WM_LBUTTONDBLCLK:
					phase = PointerPhase.Begin;
					id = mouseId = newIdCallback();
					break;
				case WindowMessages.WM_MOUSEMOVE:
					if (mouseId == 0)
						return;
					phase = PointerPhase.Update;
					break;
				case WindowMessages.WM_LBUTTONUP:
					phase = PointerPhase.End;
					mouseId = 0;
					break;
				case WindowMessages.WM_MOUSELEAVE:
					phase = PointerPhase.Cancel;
					mouseId = 0;
					break;
				default:
					return;
			}
			callback(id, new PointerInfo() {
				Id = id,
				ProcessTime = procTime,
				Phase = phase,
				Type = PointerType.Mouse,
				Position = _p,
			});
		}*/

		void HandleTouchMsg(IntPtr wParam, IntPtr lParam) {
			int inputCount = LOWORD(wParam.ToInt32());
			NativeMethods.TOUCHINPUT[] inputs = new NativeMethods.TOUCHINPUT[inputCount];

			if (!NativeMethods.GetTouchInputInfo(lParam, inputCount, inputs, touchInputSize))
				throw new InvalidOperationException("Failed to get touch input info");

			for (int i = 0; i < inputCount; i++) {
				NativeMethods.TOUCHINPUT touch = inputs[i];

				NativeMethods.POINT p = new NativeMethods.POINT {
					X = touch.x / 100,
					Y = touch.y / 100
				};
				NativeMethods.ScreenToClient(hMainWindow, ref p);
				Vector2 _p = UnityCameraUtils.ScreenToWorldPoint(new Vector2(p.X, Screen.height - p.Y));

				/*Vector2? _cs = null;
				if (touch.dwMask.HasFlag(NativeMethods.TOUCHINPUT_Mask.TOUCHINPUTMASKF_CONTACTAREA)) {
					Vector2 cs;
					cs.x = touch.cxContact / 100;
					cs.y = touch.cyContact / 100;
					_cs = cs;
				}*/

				int id = touch.dwID;
				double time = touch.dwTime / 1000.0;
				InputVector vec = new InputVector(time, _p);

				if (touch.dwFlags.HasFlag(NativeMethods.TOUCHINPUT_Flags.TOUCHEVENTF_MOVE) ||
					touch.dwFlags.HasFlag(NativeMethods.TOUCHINPUT_Flags.TOUCHEVENTF_DOWN)) {
					Feed(255, id, vec);
				}
				else if (touch.dwFlags.HasFlag(NativeMethods.TOUCHINPUT_Flags.TOUCHEVENTF_UP)) {
					Feed(255, id, vec);
					Feed(255, id, new InputVector(time));
				}
			}

			NativeMethods.CloseTouchInputHandle(lParam);
		}

		public static int LOWORD(int value) {
			return value & 0xffff;
		}

		public static int HIWORD(int value) {
			return (value & (0xffff << 16)) >> 16;
		}

		public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) {
			if (IntPtr.Size == 8)
				return NativeMethods.SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
			return new IntPtr(NativeMethods.SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32()));
		}
	}
}
