#include %A_LineFile%\..\CLR.ahk

class AutoHotInterception {
	_contextManagers := Map()

	__New() {
		bitness := A_PtrSize == 8 ? "x64" : "x86"
		dllName := "interception.dll"
		if (A_IsCompiled) {
			dllFile := A_LineFile "\..\Lib\" bitness "\" dllName
			DirCreate("Lib")
			FileInstall("Lib\AutoHotInterception.dll", "Lib\AutoHotInterception.dll")
			if (bitness == "x86") {
				DirCreate("Lib\x86")
				FileInstall("Lib\x86\interception.dll", "Lib\x86\interception.dll")
			} else {
				DirCreate("Lib\x64")
				FileInstall("Lib\x64\interception.dll", "Lib\x64\interception.dll")
			}
		} else {
			dllFile := A_LineFile "\..\" bitness "\" dllName
		}
		if (!FileExist(dllFile)) {
			MsgBox("Unable to find " dllFile ", exiting...`nYou should extract both x86 and x64 folders from the library folder in interception.zip into AHI's lib folder.")
			ExitApp
		}

		hModule := DllCall("LoadLibrary", "Str", dllFile, "Ptr")
		if (hModule == 0) {
			this_bitness := A_PtrSize == 8 ? "64-bit" : "32-bit"
			other_bitness := A_PtrSize == 4 ? "64-bit" : "32-bit"
			MsgBox("Bitness of " dllName " does not match bitness of AHK.`nAHK is " this_bitness ", but " dllName " is " other_bitness ".")
			ExitApp
		}
		DllCall("FreeLibrary", "Ptr", hModule)

		dllName := "AutoHotInterception.dll"
		if (A_IsCompiled) {
			dllFile := A_LineFile "\..\Lib\" dllName
		} else {
			dllFile := A_LineFile "\..\" dllName
		}
		hintMessage := "Try right-clicking " dllFile ", select Properties, and if there is an 'Unblock' checkbox, tick it`nAlternatively, running Unblocker.ps1 in the lib folder (ideally as admin) can do this for you."
		if (!FileExist(dllFile)) {
			MsgBox("Unable to find " dllFile ", exiting...")
			ExitApp
		}

		asm := CLR_LoadLibrary(dllFile)
		try {
			this.Instance := asm.CreateInstance("AutoHotInterception.Manager")
		}
		catch {
			MsgBox(dllName " failed to load`n`n" hintMessage)
			ExitApp
		}
		if (this.Instance.OkCheck() != "OK") {
			MsgBox(dllName " loaded but check failed!`n`n" hintMessage)
			ExitApp
		}
	}

	GetInstance() {
		return this.Instance
	}

	; --------------- Input Synthesis ----------------
	SendKeyEvent(id, code, state) {
		this.Instance.SendKeyEvent(id, code, state)
	}

	SendMouseButtonEvent(id, btn, state) {
		this.Instance.SendMouseButtonEvent(id, btn, state)
	}

	SendMouseButtonEventAbsolute(id, btn, state, x, y) {
		this.Instance.SendMouseButtonEventAbsolute(id, btn, state, x, y)
	}

	SendMouseMove(id, x, y) {
		this.Instance.SendMouseMove(id, x, y)
	}

	SendMouseMoveRelative(id, x, y) {
		this.Instance.SendMouseMoveRelative(id, x, y)
	}

	SendMouseMoveAbsolute(id, x, y) {
		this.Instance.SendMouseMoveAbsolute(id, x, y)
	}

	SetState(state) {
		this.Instance.SetState(state)
	}

	MoveCursor(x, y, cm := "Screen", mouseId := -1) {
		if (mouseId == -1)
			mouseId := 11 ; Use 1st found mouse
		oldMode := A_CoordModeMouse
		CoordMode("Mouse", cm)
		Loop {
			MouseGetPos(&cx, &cy)
			dx := this.GetDirection(cx, x)
			dy := this.GetDirection(cy, y)
			if (dx == 0 && dy == 0)
				break
			this.SendMouseMove(mouseId, dx, dy)
		}
		CoordMode("Mouse", oldMode)
	}

	GetDirection(cp, dp) {
		d := dp - cp
		if (d > 0)
			return 1
		if (d < 0)
			return -1
		return 0
	}

	; --------------- Querying ------------------------
	GetDeviceId(IsMouse, VID, PID, instance := 1) {
		static devType := Map(0, "Keyboard", 1, "Mouse")
		dev := this.Instance.GetDeviceId(IsMouse, VID, PID, instance)
		if (dev == 0) {
			MsgBox("Could not get " devType[isMouse] " with VID " VID ", PID " PID ", Instance " instance)
			ExitApp
		}
		return dev
	}

	GetDeviceIdFromHandle(isMouse, handle, instance := 1) {
		static devType := Map(0, "Keyboard", 1, "Mouse")
		dev := this.Instance.GetDeviceIdFromHandle(IsMouse, handle, instance)
		if (dev == 0) {
			MsgBox("Could not get " devType[isMouse] " with Handle " handle ", Instance " instance)
			ExitApp
		}
		return dev
	}

	GetKeyboardId(VID, PID, instance := 1) {
		return this.GetDeviceId(false, VID, PID, instance)
	}

	GetMouseId(VID, PID, instance := 1) {
		return this.GetDeviceId(true, VID, PID, instance)
	}

	GetKeyboardIdFromHandle(handle, instance := 1) {
		return this.GetDeviceIdFromHandle(false, handle, instance)
	}

	GetMouseIdFromHandle(handle, instance := 1) {
		return this.GetDeviceIdFromHandle(true, handle, instance)
	}

	GetDeviceList() {
		DeviceList := Map()
		arr := this.Instance.GetDeviceList()
		for v in arr {
			; ToDo: Return a class, so code completion works?
			DeviceList[v.id] := { ID: v.id, VID: v.vid, PID: v.pid, IsMouse: v.IsMouse, Handle: v.Handle }
		}
		return DeviceList
	}

	; ---------------------- Subscription Mode ----------------------
	SubscribeKey(id, code, block, callback, concurrent := false) {
		this.Instance.SubscribeKey(id, code, block, callback, concurrent)
	}

	UnsubscribeKey(id, code) {
		this.Instance.UnsubscribeKey(id, code)
	}

	SubscribeKeyboard(id, block, callback, concurrent := false) {
		this.Instance.SubscribeKeyboard(id, block, callback, concurrent)
	}

	UnsubscribeKeyboard(id) {
		this.Instance.UnsubscribeKeyboard(id)
	}

	SubscribeMouseButton(id, btn, block, callback, concurrent := false) {
		this.Instance.SubscribeMouseButton(id, btn, block, callback, concurrent)
	}

	UnsubscribeMouseButton(id, btn) {
		this.Instance.UnsubscribeMouseButton(id, btn)
	}

	SubscribeMouseButtons(id, block, callback, concurrent := false) {
		this.Instance.SubscribeMouseButtons(id, block, callback, concurrent)
	}

	UnsubscribeMouseButtons(id) {
		this.Instance.UnsubscribeMouseButtons(id)
	}

	SubscribeMouseMove(id, block, callback, concurrent := false) {
		this.Instance.SubscribeMouseMove(id, block, callback, concurrent)
	}

	UnsubscribeMouseMove(id) {
		this.Instance.UnsubscribeMouseMove(id)
	}

	SubscribeMouseMoveRelative(id, block, callback, concurrent := false) {
		this.Instance.SubscribeMouseMoveRelative(id, block, callback, concurrent)
	}

	UnsubscribeMouseMoveRelative(id) {
		this.Instance.UnsubscribeMouseMoveRelative(id)
	}

	SubscribeMouseMoveAbsolute(id, block, callback, concurrent := false) {
		this.Instance.SubscribeMouseMoveAbsolute(id, block, callback, concurrent)
	}

	UnsubscribeMouseMoveAbsolute(id) {
		this.Instance.UnsubscribeMouseMoveAbsolute(id)
	}

	; ------------- Context Mode ----------------
	; Creates a context class to make it easy to turn on/off the hotkeys
	CreateContextManager(id) {
		if (this._contextManagers.Has(id)) {
			Msgbox("ID " id " already has a Context Manager")
			ExitApp
		}
		cm := AutoHotInterception.ContextManager(this, id)
		this._contextManagers[id] := cm
		return cm
	}

	RemoveContextManager(id) {
		if (!this._contextManagers.Has(id)) {
			Msgbox("ID " id " does not have a Context Manager")
			ExitApp
		}
		this._contextManagers[id].Remove()
		this._contextManagers.Delete(id)
	}

	; Helper class for dealing with context mode
	class ContextManager {
		IsActive := 0
		__New(parent, id) {
			this.parent := parent
			this.id := id
			result := this.parent.Instance.SetContextCallback(id, this.OnContextCallback.Bind(this))
		}

		OnContextCallback(state) {
			Sleep 0
			this.IsActive := state
		}

		Remove() {
			this.parent.Instance.RemoveContextCallback(this.id)
		}
	}
}