import importlib.util
import json
import os
import random
import sys
import time
from ctypes import c_long, c_longlong, windll

import pythoncom
import win32com.client

try:
    import pydirectinput
except Exception as exc:
    pydirectinput = None
    _PYDIRECTINPUT_IMPORT_ERROR = exc
else:
    _PYDIRECTINPUT_IMPORT_ERROR = None
    pydirectinput.FAILSAFE = False
    pydirectinput.PAUSE = 0


MAIN_CONFIG_FILE = "wow_multikey_qt.json"
BACKEND_TIANYA = "tianya"
BACKEND_GHOST = "ghost"
BACKEND_DIRECTINPUT = "directinput"
TIANYA_DLL_PATH = "ddl/wyhkm.dll"
GHOSTBOX_MODULE_PATH = "Ghostbox_ddl/ghostbox.py"


class _TianyaBackend:
    name = BACKEND_TIANYA

    def __init__(self, controller, dll_path=TIANYA_DLL_PATH):
        self._controller = controller
        self._dll_path_setting = dll_path
        self._wyhkm = None
        self.dll_path = None

    def configure(self, dll_path=None):
        if dll_path:
            self._dll_path_setting = dll_path

    def label(self):
        return self.name if self.is_available() else f"{self.name}_unavailable"

    def is_available(self):
        if self._wyhkm:
            try:
                if self._wyhkm.IsOpen(0):
                    return True
            except Exception:
                self._wyhkm = None

        self.dll_path = self._controller._resolve_resource_path(self._dll_path_setting)
        if not self.dll_path:
            self._controller._log(f">>> [tianya_control] DLL not found: {self._dll_path_setting}")
            self._wyhkm = None
            return False

        try:
            hkmdll = windll.LoadLibrary(self.dll_path)
            hkmdll.DllInstall.argtypes = (c_long, c_longlong)

            if hkmdll.DllInstall(1, 2) < 0:
                self._controller._log(">>> [tianya_control] DllInstall failed")
                self._wyhkm = None
                return False

            pythoncom.CoInitialize()
            self._wyhkm = win32com.client.Dispatch("wyp.hkm")

            dev_id = self._wyhkm.SearchDevice(0x2612, 0x1701, 0)
            if dev_id == -1:
                self._controller._log(">>> [tianya_control] Device not found (Index 0)")
                self._wyhkm = None
                return False

            if not self._wyhkm.Open(dev_id, 0):
                self._controller._log(">>> [tianya_control] Open device failed")
                self._wyhkm = None
                return False

            self._wyhkm.SetMode(1, 1)
            self._wyhkm.SetMode(2, 1)
            self._wyhkm.SetKeyInterval(30, 50)
            self._wyhkm.SetMouseInterval(30, 50)

            self._controller._log(f">>> [tianya_control] Device opened and initialized: {dev_id}")
            return True
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] Init failed: {exc}")
            self._wyhkm = None
            return False

    def _normalize_key(self, key_str):
        return str(key_str).strip().upper()

    def delay_rnd(self, min_ms, max_ms):
        if not self.is_available():
            return False
        try:
            self._wyhkm.DelayRnd(int(min_ms), int(max_ms))
            return True
        except Exception:
            return False

    def key_down(self, key_str):
        if not self.is_available():
            return False
        try:
            self._wyhkm.KeyDown(self._normalize_key(key_str))
            return True
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] KeyDown failed ({key_str}): {exc}")
            return False

    def key_up(self, key_str):
        if not self.is_available():
            return False
        try:
            self._wyhkm.KeyUp(self._normalize_key(key_str))
            return True
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] KeyUp failed ({key_str}): {exc}")
            return False

    def key_press(self, key_str):
        if not self.is_available():
            return False
        try:
            self._wyhkm.KeyPress(self._normalize_key(key_str))
            return True
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] KeyPress failed ({key_str}): {exc}")
            return False

    def move_to(self, x, y):
        if not self.is_available():
            return False
        try:
            return bool(self._wyhkm.MoveTo(int(x), int(y)))
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] MoveTo failed ({x}, {y}): {exc}")
            return False

    def move_r(self, dx, dy):
        if not self.is_available():
            return False
        try:
            return bool(self._wyhkm.MoveR(int(dx), int(dy)))
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] MoveR failed ({dx}, {dy}): {exc}")
            return False

    def left_click(self):
        if not self.is_available():
            return False
        try:
            return bool(self._wyhkm.LeftClick())
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] LeftClick failed: {exc}")
            return False

    def right_click(self):
        if not self.is_available():
            return False
        try:
            return bool(self._wyhkm.RightClick())
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] RightClick failed: {exc}")
            return False

    def left_down(self):
        if not self.is_available():
            return False
        try:
            return bool(self._wyhkm.LeftDown())
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] LeftDown failed: {exc}")
            return False

    def left_up(self):
        if not self.is_available():
            return False
        try:
            return bool(self._wyhkm.LeftUp())
        except Exception as exc:
            self._controller._log(f">>> [tianya_control] LeftUp failed: {exc}")
            return False


class _GhostboxBackend:
    name = BACKEND_GHOST
    LEFT_BUTTON = 1
    RIGHT_BUTTON = 2

    def __init__(self, controller, module_path=GHOSTBOX_MODULE_PATH):
        self._controller = controller
        self._module_path_setting = module_path
        self._module = None
        self.module_path = None

    def configure(self, module_path=None):
        if module_path and module_path != self._module_path_setting:
            self._module_path_setting = module_path
            self._module = None

    def label(self):
        return self.name if self.is_available() else f"{self.name}_unavailable"

    def _load_module(self):
        if self._module is not None:
            return self._module

        self.module_path = self._controller._resolve_resource_path(self._module_path_setting)
        if not self.module_path:
            self._controller._log(f">>> [ghost_control] module not found: {self._module_path_setting}")
            return None

        try:
            spec = importlib.util.spec_from_file_location("ghostbox_runtime", self.module_path)
            if spec is None or spec.loader is None:
                self._controller._log(f">>> [ghost_control] load spec failed: {self.module_path}")
                return None
            module = importlib.util.module_from_spec(spec)
            sys.modules[spec.name] = module
            spec.loader.exec_module(module)
            self._module = module
            return module
        except Exception as exc:
            self._controller._log(f">>> [ghost_control] load failed: {exc}")
            self._module = None
            return None

    def is_available(self):
        gb = self._load_module()
        if gb is None:
            return False

        try:
            if gb.isconnected():
                return True
        except Exception:
            pass

        try:
            result = gb.opendevice()
            connected = bool(gb.isconnected())
            if not connected:
                self._controller._log(f">>> [ghost_control] Open device failed: result={result}")
                return False

            try:
                gb.setpresskeydelay(30, 50)
                gb.setpressmousebuttondelay(30, 50)
                gb.setmousemovementdelay(3, 5)
            except Exception:
                pass

            self._controller._log(">>> [ghost_control] Device opened and initialized")
            return True
        except Exception as exc:
            self._controller._log(f">>> [ghost_control] Init failed: {exc}")
            return False

    def _normalize_key(self, key_str):
        return str(key_str).strip().lower()

    def _call(self, action, func, *args):
        if not self.is_available():
            return False
        try:
            func(*args)
            return True
        except Exception as exc:
            self._controller._log(f">>> [ghost_control] {action} failed: {exc}")
            return False

    def delay_rnd(self, min_ms, max_ms):
        low = min(int(min_ms), int(max_ms))
        high = max(int(min_ms), int(max_ms))
        time.sleep(random.uniform(low, high) / 1000.0)
        return True

    def key_down(self, key_str):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call(f"KeyDown({key_str})", gb.presskeybyname, self._normalize_key(key_str))

    def key_up(self, key_str):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call(f"KeyUp({key_str})", gb.releasekeybyname, self._normalize_key(key_str))

    def key_press(self, key_str):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call(f"KeyPress({key_str})", gb.pressandreleasekeybyname, self._normalize_key(key_str))

    def move_to(self, x, y):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call(f"MoveTo({x}, {y})", gb.movemouseto, int(x), int(y))

    def move_r(self, dx, dy):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call(f"MoveR({dx}, {dy})", gb.movemouserelative, int(dx), int(dy))

    def left_click(self):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call("LeftClick", gb.pressandreleasemousebutton, self.LEFT_BUTTON)

    def right_click(self):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call("RightClick", gb.pressandreleasemousebutton, self.RIGHT_BUTTON)

    def left_down(self):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call("LeftDown", gb.pressmousebutton, self.LEFT_BUTTON)

    def left_up(self):
        gb = self._load_module()
        if gb is None:
            return False
        return self._call("LeftUp", gb.releasemousebutton, self.LEFT_BUTTON)


class _DirectInputBackend:
    name = BACKEND_DIRECTINPUT

    def __init__(self, controller):
        self._controller = controller
        self._last_error = None

    def label(self):
        return self.name if self.is_available() else f"{self.name}_unavailable"

    def is_available(self):
        if pydirectinput is not None:
            return True
        if self._last_error != _PYDIRECTINPUT_IMPORT_ERROR:
            self._last_error = _PYDIRECTINPUT_IMPORT_ERROR
            self._controller._log(f">>> [input_control] pydirectinput unavailable: {_PYDIRECTINPUT_IMPORT_ERROR}")
        return False

    def _normalize_key(self, key_str):
        return str(key_str).strip().lower()

    def delay_rnd(self, min_ms, max_ms):
        if not self.is_available():
            return False
        low = min(int(min_ms), int(max_ms))
        high = max(int(min_ms), int(max_ms))
        time.sleep(random.uniform(low, high) / 1000.0)
        return True

    def key_down(self, key_str):
        if not self.is_available():
            return False
        try:
            pydirectinput.keyDown(self._normalize_key(key_str))
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] keyDown failed ({key_str}): {exc}")
            return False

    def key_up(self, key_str):
        if not self.is_available():
            return False
        try:
            pydirectinput.keyUp(self._normalize_key(key_str))
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] keyUp failed ({key_str}): {exc}")
            return False

    def key_press(self, key_str):
        if not self.is_available():
            return False
        try:
            pydirectinput.press(self._normalize_key(key_str))
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] press failed ({key_str}): {exc}")
            return False

    def move_to(self, x, y):
        if not self.is_available():
            return False
        try:
            pydirectinput.moveTo(int(x), int(y))
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] moveTo failed ({x}, {y}): {exc}")
            return False

    def move_r(self, dx, dy):
        if not self.is_available():
            return False
        try:
            pydirectinput.moveRel(int(dx), int(dy))
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] moveRel failed ({dx}, {dy}): {exc}")
            return False

    def left_click(self):
        if not self.is_available():
            return False
        try:
            pydirectinput.click(button="left")
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] left click failed: {exc}")
            return False

    def right_click(self):
        if not self.is_available():
            return False
        try:
            pydirectinput.click(button="right")
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] right click failed: {exc}")
            return False

    def left_down(self):
        if not self.is_available():
            return False
        try:
            pydirectinput.mouseDown(button="left")
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] left mouseDown failed: {exc}")
            return False

    def left_up(self):
        if not self.is_available():
            return False
        try:
            pydirectinput.mouseUp(button="left")
            return True
        except Exception as exc:
            self._controller._log(f">>> [directinput] left mouseUp failed: {exc}")
            return False


class HardwareController:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(HardwareController, cls).__new__(cls)
        return cls._instance

    def __init__(
        self,
        dll_path=TIANYA_DLL_PATH,
        use_hardware_input=None,
        use_tianya_box=None,
        use_ghost_box=None,
        ghostbox_module_path=GHOSTBOX_MODULE_PATH,
        backend=None,
    ):
        if getattr(self, "_bootstrapped", False):
            self._tianya_backend.configure(dll_path=dll_path)
            self._ghost_backend.configure(module_path=ghostbox_module_path)
            if backend is not None or use_hardware_input is not None or use_tianya_box is not None or use_ghost_box is not None:
                self.configure(
                    use_hardware_input=use_hardware_input,
                    use_tianya_box=use_tianya_box,
                    use_ghost_box=use_ghost_box,
                    dll_path=dll_path,
                    ghostbox_module_path=ghostbox_module_path,
                    backend=backend,
                )
            return

        self._bootstrapped = True
        self._backend = BACKEND_TIANYA
        self._last_backend_log = None
        self._tianya_backend = _TianyaBackend(self, dll_path)
        self._ghost_backend = _GhostboxBackend(self, ghostbox_module_path)
        self._directinput_backend = _DirectInputBackend(self)

        if backend is None and use_hardware_input is None and use_tianya_box is None and use_ghost_box is None:
            use_tianya_box, use_ghost_box = self._load_backend_preference(default_tianya=True)
        self.configure(
            use_hardware_input=use_hardware_input,
            use_tianya_box=use_tianya_box,
            use_ghost_box=use_ghost_box,
            backend=backend,
        )

    def _runtime_base_dir(self):
        if getattr(sys, "frozen", False):
            return os.path.dirname(sys.executable)
        return os.path.dirname(os.path.abspath(__file__))

    def _resolve_resource_path(self, relative_path):
        if os.path.isabs(relative_path):
            return relative_path if os.path.exists(relative_path) else None

        candidates = []
        if getattr(sys, "frozen", False):
            exe_dir = os.path.dirname(sys.executable)
            if exe_dir:
                candidates.append(os.path.join(exe_dir, relative_path))
            meipass = getattr(sys, "_MEIPASS", "")
            if meipass:
                candidates.append(os.path.join(meipass, relative_path))

        module_dir = os.path.dirname(os.path.abspath(__file__))
        candidates.append(os.path.join(module_dir, relative_path))
        candidates.append(os.path.abspath(relative_path))

        seen = set()
        for candidate in candidates:
            candidate = os.path.abspath(candidate)
            if candidate in seen:
                continue
            seen.add(candidate)
            if os.path.exists(candidate):
                return candidate
        return None

    def _config_path(self):
        candidates = [os.path.join(self._runtime_base_dir(), MAIN_CONFIG_FILE)]
        if getattr(sys, "frozen", False):
            meipass = getattr(sys, "_MEIPASS", "")
            if meipass:
                candidates.append(os.path.join(meipass, MAIN_CONFIG_FILE))
        candidates.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), MAIN_CONFIG_FILE))

        seen = set()
        for candidate in candidates:
            candidate = os.path.abspath(candidate)
            if candidate in seen:
                continue
            seen.add(candidate)
            if os.path.exists(candidate):
                return candidate
        return os.path.join(self._runtime_base_dir(), MAIN_CONFIG_FILE)

    def _load_backend_preference(self, default_tianya=True):
        config_path = self._config_path()
        if not os.path.exists(config_path):
            return default_tianya, False
        try:
            with open(config_path, "r", encoding="utf-8") as f:
                cfg = json.load(f)
            bot_cfg = (cfg or {}).get("bot") or {}
            has_new_flags = "use_tianya_box" in bot_cfg or "use_ghost_box" in bot_cfg
            if has_new_flags:
                return bool(bot_cfg.get("use_tianya_box", False)), bool(bot_cfg.get("use_ghost_box", False))
            return bool(bot_cfg.get("use_hardware_input", default_tianya)), False
        except Exception as exc:
            self._log(f">>> [input_control] load config failed: {exc}")
            return default_tianya, False

    def _log(self, message):
        print(message)
        try:
            log_path = os.path.join(self._runtime_base_dir(), "hardware_control.log")
            with open(log_path, "a", encoding="utf-8") as f:
                f.write(message + "\n")
        except Exception:
            pass

    def _normalize_backend(self, backend):
        if backend in (BACKEND_TIANYA, "hardware"):
            return BACKEND_TIANYA
        if backend == BACKEND_GHOST:
            return BACKEND_GHOST
        if backend == BACKEND_DIRECTINPUT:
            return BACKEND_DIRECTINPUT
        return BACKEND_DIRECTINPUT

    def _select_backend(self, use_tianya_box, use_ghost_box):
        if bool(use_tianya_box):
            return BACKEND_TIANYA
        if bool(use_ghost_box):
            return BACKEND_GHOST
        return BACKEND_DIRECTINPUT

    def _current_backend(self):
        if self._backend == BACKEND_TIANYA:
            return self._tianya_backend
        if self._backend == BACKEND_GHOST:
            return self._ghost_backend
        return self._directinput_backend

    def _log_backend(self):
        label = self.backend_label()
        if label != self._last_backend_log:
            self._last_backend_log = label
            self._log(f">>> [input_control] backend={label}")

    def configure(
        self,
        use_hardware_input=None,
        use_tianya_box=None,
        use_ghost_box=None,
        dll_path=None,
        ghostbox_module_path=None,
        backend=None,
    ):
        self._tianya_backend.configure(dll_path=dll_path)
        self._ghost_backend.configure(module_path=ghostbox_module_path)

        if backend is not None:
            self._backend = self._normalize_backend(backend)
        else:
            if use_tianya_box is None and use_ghost_box is None:
                if use_hardware_input is None:
                    use_tianya_box, use_ghost_box = self._load_backend_preference(default_tianya=True)
                else:
                    use_tianya_box, use_ghost_box = bool(use_hardware_input), False
            else:
                use_tianya_box = bool(use_tianya_box)
                use_ghost_box = bool(use_ghost_box)
                if use_hardware_input is not None and not bool(use_hardware_input):
                    use_tianya_box, use_ghost_box = False, False
            self._backend = self._select_backend(use_tianya_box, use_ghost_box)

        self._log_backend()
        return self._backend

    def uses_hardware_input(self):
        return self._backend in (BACKEND_TIANYA, BACKEND_GHOST)

    def uses_tianya_box(self):
        return self._backend == BACKEND_TIANYA

    def uses_ghost_box(self):
        return self._backend == BACKEND_GHOST

    def backend_label(self):
        return self._current_backend().label()

    def is_available(self):
        return self._current_backend().is_available()

    def delay_rnd(self, min_ms, max_ms):
        return self._current_backend().delay_rnd(min_ms, max_ms)

    def key_down(self, key_str):
        return self._current_backend().key_down(key_str)

    def key_up(self, key_str):
        return self._current_backend().key_up(key_str)

    def key_press(self, key_str):
        return self._current_backend().key_press(key_str)

    def keyDown(self, key_str):
        return self.key_down(key_str)

    def keyUp(self, key_str):
        return self.key_up(key_str)

    def press(self, key_str):
        return self.key_press(key_str)

    def move_to(self, x, y):
        return self._current_backend().move_to(x, y)

    def move_r(self, dx, dy):
        return self._current_backend().move_r(dx, dy)

    def MoveR(self, dx, dy):
        return self.move_r(dx, dy)

    def left_click(self):
        return self._current_backend().left_click()

    def right_click(self):
        return self._current_backend().right_click()

    def left_down(self):
        return self._current_backend().left_down()

    def left_up(self):
        return self._current_backend().left_up()


hw_ctrl = HardwareController()
