import asyncio
from collections import namedtuple
from typing import Union
import warnings
from ahk.script import ScriptEngine, AsyncScriptEngine
from ahk.utils import make_logger
import ast

logger = make_logger(__name__)


_BUTTONS = {
    1: 'L',
    2: 'R',
    3: 'M',
    'left': 'L',
    'right': 'R',
    'middle': 'M',
    'wheelup': 'WU',
    'wheeldown': 'WD',
    'wheelleft': 'WL',
    'wheelright': 'WR',
}


def resolve_button(button):
    """
    Resolve a string of a button name to a canonical name used for AHK script

    :param button:
    :type button: str
    :return:
    """
    if isinstance(button, str):
        button = button.lower()

    if button in _BUTTONS:
        button = _BUTTONS.get(button)
    elif isinstance(button, int) and button > 3:
        #  for addtional mouse buttons
        button = f'X{button-3}'
    return button


class MouseMixin(ScriptEngine):
    """
    Provides mouse functionality for the AHK class
    """

    def __init__(self, mouse_speed=2, mode=None, **kwargs):
        """

        :param mouse_speed: default mouse speed
        :param mode:
        :param kwargs:
        """
        if mode is None:
            mode = 'Screen'
        self.mode = mode
        self._mouse_speed = mouse_speed
        super().__init__(**kwargs)

    @property
    def mouse_speed(self):
        if callable(self._mouse_speed):
            return self._mouse_speed()
        else:
            return self._mouse_speed

    @mouse_speed.setter
    def mouse_speed(self, value):
        self._mouse_speed = value

    def _mouse_position(self, mode=None):
        if mode is None:
            mode = self.mode
        return self.render_template('mouse/mouse_position.ahk', mode=mode)

    @property
    def mouse_position(self):
        return self.get_mouse_position()

    @mouse_position.setter
    def mouse_position(self, position):
        x, y = position
        self.mouse_move(x=x, y=y, speed=0, relative=False)

    def get_mouse_position(self, mode=None):
        script = self._mouse_position(mode=mode)
        response = self.run_script(script)
        return ast.literal_eval(response)

    def _mouse_move(self, x=None, y=None, speed=None, relative=False, mode=None, blocking=True):
        if x is None and y is None:
            raise ValueError('Position argument(s) missing. Must provide x and/or y coordinates')
        if speed is None:
            speed = self.mouse_speed
        if callable(speed):
            speed = speed()
        if mode is None:
            mode = self.mode
        if relative and (x is None or y is None):
            x = x or 0
            y = y or 0
        elif not relative and (x is None or y is None):
            posx, posy = self.mouse_position
            x = x or posx
            y = y or posy

        return self.render_template(
            'mouse/mouse_move.ahk', x=x, y=y, speed=speed, relative=relative, mode=mode, blocking=blocking
        )

    def mouse_move(self, *args, **kwargs):
        """
        REF: https://www.autohotkey.com/docs/commands/MouseMove.htm

        :param x: the x coordinate to move to. If omitted, current position is used
        :param y: the y coordinate to move to. If omitted, current position is used
        :param speed: 0 (fastest) to 100 (slowest). Can be a callable or string AHK expression
        :param relative: Move the mouse realtive to current position rather than absolute x,y coordinates
        :param mode:
        :param blocking:
        :return:

        """
        blocking = kwargs.get('blocking', True)
        script = self._mouse_move(*args, **kwargs)
        return self.run_script(script, blocking=blocking) or None

    def _click(self, x=None, y=None, *, button=None, n=None, direction=None, relative=None, blocking=True, mode=None):
        if x or y:
            if y is None and not isinstance(x, int) and len(x) == 2:
                #  alow position to be specified by a two-sequence
                x, y = x
            assert x is not None and y is not None, 'If provided, position must be specified by x AND y'

        button = resolve_button(button)

        if relative:
            relative = 'Rel'
        args = [arg for arg in (x, y, button, n, direction, relative) if arg is not None]
        if mode is None:
            mode = self.mode
        script = self.render_template('mouse/click.ahk', args=args, mode=mode, blocking=blocking)
        return script

    def click(self, *args, **kwargs):
        """
        Click mouse button at a specified position. REF: https://www.autohotkey.com/docs/commands/Click.htm

        :param x:
        :param y:
        :param button:
        :param n: number of times to click the button
        :param direction:
        :param relative:
        :param blocking:
        :param mode:
        :return:
        """
        blocking = kwargs.get('blocking', True)
        script = self._click(*args, **kwargs)
        return self.run_script(script, blocking=blocking) or None

    def double_click(self, *args, **kwargs):
        """
        Convenience function to double click, equivalent to ``click`` with ``n=2``

        :param args:
        :param kwargs:
        :return:
        """
        n = kwargs.get('n', 1)
        kwargs['n'] = n * 2
        return self.click(*args, **kwargs) or None

    def right_click(self, *args, **kwargs):
        """
        Convenience function clicking right mouse button. Equivalent to ``click`` with ``button='R'``

        :param args:
        :param kwargs:
        :return:
        """
        kwargs['button'] = 2
        return self.click(*args, **kwargs) or None

    def mouse_wheel(self, direction, *args, **kwargs):
        """
        Convenience function for 'clicking' the mouse wheel in a given direction.

        :param direction: the string 'up' or 'down'
        :param args: args passed to ``click``
        :param kwargs: keyword args passed to ``click``
        :return:
        """
        assert direction in ('up', 'down')
        kwargs['button'] = f'Wheel{direction}'
        return self.click(*args, **kwargs) or None

    def wheel_up(self, *args, **kwargs):
        """
        Convenience function for ``click`` with wheel up button

        :param args:
        :param kwargs:
        :return:
        """
        return self.mouse_wheel('up', *args, **kwargs) or None

    def wheel_down(self, *args, **kwargs):
        """
        Convenience function for ``click`` with wheel down button

        :param args:
        :param kwargs:
        :return:
        """
        return self.mouse_wheel('down', *args, **kwargs) or None

    def _mouse_drag(
        self,
        x,
        y=None,
        *,
        from_position=None,
        speed=None,
        button: Union[str, int] = 1,
        relative=None,
        blocking=True,
        mode=None,
    ):
        if from_position is None:
            x1, y1 = self.mouse_position
        else:
            x1, y1 = from_position

        if y is None:
            x2, y2 = x
        else:
            x2 = x
            y2 = y

        if relative:
            x1, y1 = (0, 0)

        button = resolve_button(button)

        if speed is None:
            speed = self.mouse_speed

        if mode is None:
            mode = self.mode

        script = self.render_template(
            'mouse/mouse_drag.ahk',
            button=button,
            x1=x1,
            y1=y1,
            x2=x2,
            y2=y2,
            speed=speed,
            relative=relative,
            blocking=blocking,
            mode=mode,
        )

        return script

    def mouse_drag(self, *args, **kwargs):
        """
        Click and drag the mouse

        :param x:
        :param y:
        :param from_position: (x,y) tuple of an optional starting position. Current position is used if omitted
        :param speed:
        :param button: The button the click and drag; defaults to left mouse button
        :param relative: click and drag to a relative position rather than an absolute position
        :param blocking:
        :param mode:
        :return:
        """
        blocking = kwargs.get('blocking', True)
        script = self._mouse_drag(*args, **kwargs)
        return self.run_script(script, blocking=blocking) or None


class AsyncMouseMixin(AsyncScriptEngine, MouseMixin):
    async def get_mouse_position(self, mode=None):
        script = self._mouse_position(mode=mode)
        response = await self.a_run_script(script)
        return ast.literal_eval(response)

    @MouseMixin.mouse_position.setter
    def mouse_position(self, position):
        warnings.warn('mouse_position setter only schedules coroutine. use mouse_move() (with speed=0) instead')
        x, y = position
        coro = self.mouse_move(x=x, y=y, speed=0, relative=False)
        asyncio.create_task(coro)
