import ctypes
import gc
import os
import sys
import threading
import time
from functools import wraps
from typing import Any, Callable

import numpy as np

from mower.utils import config
from mower.utils import typealias as tp
from mower.utils.csleep import MowerExit
from mower.utils.device import swipe_update
from mower.utils.device.emulator import restart_emulator
from mower.utils.device.exception import EmulatorError, GameError
from mower.utils.device.method.mumumanager import MuMuManager
from mower.utils.log import logger
from mower.utils.vector import sm, va


class BufferPool:
    def __init__(self, max_buffers=5):
        self.max_buffers = max_buffers
        self.buffers = {}

    def get_buffer(self):
        self.cleanup_buffers()
        for buffer in self.buffers.values():
            refcount = sys.getrefcount(buffer)
            if refcount <= 3:
                return buffer
        # 如果没有可复用的缓冲区，分配新的内存
        new_buffer = (ctypes.c_ubyte * 8294400)()
        self.buffers[id(new_buffer)] = new_buffer
        return new_buffer

    def cleanup_buffers(self):
        """
        超过最大数量时清理未被使用的缓冲区
        """
        if len(self.buffers) <= self.max_buffers:
            return
        elif len(self.buffers) >= 30:
            self.buffers.clear()
            gc.collect()
            return
        for id in list(self.buffers.keys()):
            refcount = sys.getrefcount(self.buffers[id])
            if refcount <= 2:
                del self.buffers[id]
                if len(self.buffers) <= self.max_buffers:
                    return


class NemuIpcIncompatible(Exception):
    pass


def retry_mumuipc(func):
    @wraps(func)
    def retry_wrapper(self, *args, **kwargs):
        for _ in range(3):
            try:
                return func(self, *args, **kwargs)
            except MowerExit:
                raise
            except GameError as e:
                logger.debug_exception(e)
                config.device.check_current_focus()
            except EmulatorError as e:
                logger.debug_exception(e)
                restart_emulator()
            except Exception as e:
                logger.debug_exception(e)

    return retry_wrapper


class MuMu12IPC:
    def __init__(self):
        self.emulator_folder = config.conf.emulator.emulator_folder
        self.instanse_index = int(config.conf.emulator.index)
        self.connection = 0
        self.display_id = -1
        self.buffer_pool = BufferPool(10)
        # 加载动态链接库
        dll_path = os.path.join(
            self.emulator_folder, "sdk", "external_renderer_ipc.dll"
        )
        try:
            self.external_renderer = ctypes.CDLL(dll_path)
        except OSError as e:
            logger.error(e)
            # OSError: [WinError 126] 找不到指定的模块。
            if not os.path.exists(dll_path):
                raise NemuIpcIncompatible(
                    f"文件不存在: {dll_path}, 请检查模拟器文件夹是否正确"
                    f"NemuIpc requires MuMu12 version >= 3.8.13, please check your version"
                )
            else:
                raise NemuIpcIncompatible(
                    f"ipc_dll={dll_path} exists, but cannot be loaded"
                )

        # 定义函数原型
        self.external_renderer.nemu_connect.argtypes = [ctypes.c_wchar_p, ctypes.c_int]
        self.external_renderer.nemu_connect.restype = ctypes.c_int

        self.external_renderer.nemu_disconnect.argtypes = [ctypes.c_int]
        self.external_renderer.nemu_disconnect.restype = None

        self.external_renderer.nemu_get_display_id.argtypes = [
            ctypes.c_int,
            ctypes.c_char_p,
            ctypes.c_int,
        ]
        self.external_renderer.nemu_get_display_id.restype = ctypes.c_int

        self.external_renderer.nemu_capture_display.argtypes = [
            ctypes.c_int,
            ctypes.c_uint,
            ctypes.c_int,
            ctypes.POINTER(ctypes.c_int),
            ctypes.POINTER(ctypes.c_int),
            ctypes.POINTER(ctypes.c_ubyte),
        ]
        self.external_renderer.nemu_capture_display.restype = ctypes.c_int
        self.external_renderer.nemu_input_event_touch_down.argtypes = [
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
        ]
        self.external_renderer.nemu_input_event_touch_down.restype = ctypes.c_int

        self.external_renderer.nemu_input_event_touch_up.argtypes = [
            ctypes.c_int,
            ctypes.c_int,
        ]
        self.external_renderer.nemu_input_event_touch_up.restype = ctypes.c_int

        self.external_renderer.nemu_input_event_finger_touch_down.argtypes = [
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
        ]
        self.external_renderer.nemu_input_event_finger_touch_down.restype = ctypes.c_int

        self.external_renderer.nemu_input_event_finger_touch_up.argtypes = [
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
        ]
        self.external_renderer.nemu_input_event_finger_touch_up.restype = ctypes.c_int

        self.external_renderer.nemu_input_event_key_down.argtypes = [
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
        ]
        self.external_renderer.nemu_input_event_key_down.restype = ctypes.c_int

        self.external_renderer.nemu_input_event_key_up.argtypes = [
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_int,
        ]
        self.external_renderer.nemu_input_event_key_up.restype = ctypes.c_int

    @property
    def mumumanager(self) -> MuMuManager:
        return config.device.get_service("mumumanager")

    def connect(self):
        "连接到 emulator"
        if self.mumumanager.emulator_status() != "running":
            raise EmulatorError("模拟器未启动，请启动模拟器")
        self.connection = self.external_renderer.nemu_connect(
            ctypes.c_wchar_p(os.path.dirname(self.emulator_folder)),
            self.instanse_index,
        )
        if self.connection == 0:
            raise EmulatorError("连接模拟器失败，请启动模拟器")
        logger.info("连接模拟器成功")

    def get_display_id(self) -> int:
        config.device.check_current_focus()
        pkg_name = config.conf.APPNAME.encode("utf-8")  # 替换为实际包名
        self.display_id = self.external_renderer.nemu_get_display_id(
            self.connection, pkg_name, config.conf.mumu_multi_app.app_index
        )
        if self.display_id < 0:
            logger.error(f"获取Display ID失败: {self.display_id}")
            raise GameError("获取Display ID失败,请先打开游戏")
        logger.info(f"获取Display ID成功: {self.display_id}")

    @retry_mumuipc
    def check_status(self):
        if self.connection == 0:
            self.connect()
        if self.display_id < 0:
            self.get_display_id()

    def capture_display(self) -> np.ndarray:
        self.check_status()
        pixels = self.buffer_pool.get_buffer()
        result = self.external_renderer.nemu_capture_display(
            self.connection,
            self.display_id,
            8294400,
            ctypes.byref(ctypes.c_int(1920)),
            ctypes.byref(ctypes.c_int(1080)),
            pixels,
        )
        if result != 0:
            logger.error(f"获取截图失败: {result}")
            self.connection = 0
            self.display_id = -1
            config.device.exit()  # 可能是游戏卡死
            return np.zeros((1080, 1920, 3), dtype=np.uint8)
        image = np.frombuffer(pixels, dtype=np.uint8).reshape((1080, 1920, 4))[:, :, :3]
        image = np.flipud(image)  # 翻转
        return image

    def key_down(self, key_code: int):
        """按下键盘按键"""
        self.check_status()
        result = self.external_renderer.nemu_input_event_key_down(
            self.connection, self.display_id, key_code
        )
        if result != 0:
            self.connection = 0
            self.display_id = -1
            config.device.exit()  # 可能是游戏卡死

    def key_up(self, key_code: int):
        """释放键盘按键"""
        self.check_status()
        result = self.external_renderer.nemu_input_event_key_up(
            self.connection, self.display_id, key_code
        )
        if result != 0:
            self.connection = 0
            self.display_id = -1
            config.device.exit()  # 可能是游戏卡死

    # 单点触控
    def touch_down(self, x: int, y: int):
        self.check_status()
        result = self.external_renderer.nemu_input_event_touch_down(
            self.connection, self.display_id, int(1080 - y), int(x)
        )
        if result != 0:
            self.connection = 0
            self.display_id = -1
            config.device.exit()  # 可能是游戏卡死

    def touch_up(self):
        self.check_status()
        result = self.external_renderer.nemu_input_event_touch_up(
            self.connection, self.display_id
        )
        if result != 0:
            self.connection = 0
            self.display_id = -1
            config.device.exit()  # 可能是游戏卡死

    # 多点触控
    def finger_touch_down(self, finger_id: int, x: int, y: int):
        self.check_status()
        result = self.external_renderer.nemu_input_event_finger_touch_down(
            self.connection, self.display_id, finger_id, int(1080 - y), int(x)
        )
        if result != 0:
            self.connection = 0
            self.display_id = -1
            config.device.exit()  # 可能是游戏卡死

    def finger_touch_up(self, finger_id: int):
        self.check_status()
        result = self.external_renderer.nemu_input_event_finger_touch_up(
            self.connection, self.display_id, finger_id
        )
        if result != 0:
            self.connection = 0
            self.display_id = -1
            config.device.exit()  # 可能是游戏卡死

    def tap(self, x, y, hold_time: float = 0.07) -> None:
        """
        Tap on screen
        Args:
            x: horizontal position
            y: vertical position
            hold_time: hold time
        """
        self.touch_down(x, y)
        time.sleep(hold_time)
        self.touch_up()

    def send_keyevent(self, key_code: int, hold_time: float = 0.1):
        self.key_down(key_code)
        time.sleep(hold_time)
        self.key_up(key_code)

    def back(self):
        self.send_keyevent(1)

    def swipe(
        self,
        x0: int,
        y0: int,
        x1: int,
        y1: int,
        duration: float = 1.0,
        fall: bool = True,
        lift: bool = True,
        update: bool = False,
        interval: float = 0,
        func: Callable[[np.ndarray], Any] = lambda _: None,
    ):
        """
        模拟滑动
        Args:
            x0 (int): 起点横坐标
            y0 (int): 起点纵坐标
            x1 (int): 终点横坐标
            y1 (int): 终点纵坐标
            duration (float): 滑动持续时间，单位为秒
            fall (bool): 是否按下，默认 True
            lift (bool): 是否抬起，默认 True
            update (bool): 滑动前是否截图，默认 False
            interval (float): 滑动完成后的等待时间，单位为秒，默认 0
            func (Callable[[np.ndarray], Any]): 截图后处理的函数，默认空函数
        """
        frame_time = 1 / 30

        start_time = time.perf_counter()
        fall and self.touch_down(x0, y0)

        if update and config.recog:
            thread = threading.Thread(target=swipe_update.start, args=(func,))
            thread.start()

        if (down_time := time.perf_counter()) < start_time + frame_time:
            time.sleep(start_time + frame_time - down_time)
            down_time = start_time + frame_time

        while (progress := (time.perf_counter() - down_time) / duration) <= 1:
            x, y = va(sm(1 - progress, (x0, y0)), sm(progress, (x1, y1)))
            start_time = time.perf_counter()
            self.touch_down(x, y)
            if (move_time := time.perf_counter() - start_time) < frame_time:
                time.sleep(frame_time - move_time)
        self.touch_down(x1, y1)

        lift and self.touch_up()

        if update and config.recog:
            start_time = time.perf_counter()
            thread.join()
            if (stop_time := time.perf_counter()) < start_time + interval:
                time.sleep(start_time + interval - stop_time)
            return swipe_update.result

        if interval > 0:
            time.sleep(interval)

    def swipe_ext(
        self,
        points: list[tp.Coordinate],
        durations: list[int],
        update: bool = False,
        interval: float = 0,
        func: Callable[[tp.Image], Any] = lambda _: None,
    ):
        total = len(durations)
        for idx, (S, E, D) in enumerate(zip(points[:-1], points[1:], durations)):
            first = idx == 0
            last = idx == total - 1
            result = self.swipe(
                x0=S[0],
                y0=S[1],
                x1=E[0],
                y1=E[1],
                duration=D / 1000,
                fall=first,
                lift=last,
                update=last and update,
                interval=interval if last else 0,
                func=func,
            )
        return result

    def kill_server(self):
        if self.connection != 0:
            self.external_renderer.nemu_disconnect(self.connection)
            logger.info(f"Disconnected from emulator: handle={self.connection}")
            self.connection = 0
            self.display_id = -1
            self.buffer_pool.buffers.clear()
        else:
            logger.warning("No active connection to disconnect.")

    def reset_when_exit(self):
        self.display_id = -1
