"""
Module managing the input events generated by the user on the graphical interface.
"""
import logging as log
import time
import sys
import threading

import pyautogui
from pynput.keyboard import Key, KeyCode, Controller

def get_subclasses(base_class):
    """ :return: A list of all the classes inheriting, directly or not from the base_class. """
    subclasses = [base_class]
    for c in base_class.__subclasses__():
        subclasses += get_subclasses(c)
    return subclasses

def format_modifiers(modifier_names):
    """ :type modifier_names: list of names to join """
    return '{}+'.format('+'.join(modifier_names)) if modifier_names else ''

def use_modifiers(executor):
    """
    Decorator handling the press of key modifiers while executing other
    types of events.
    It is meant to be used on the `execute` method of Event sub-classes.
    """
    def wrapper(self):
        self.ctrl = Controller()
        modifier_keys = tuple(Key[k] for k in self.modifiers)
        if self.modifiers == (Key.alt_gr.name,):
            if sys.platform in ALT_GR:
                modifier_keys = ALT_GR[sys.platform]
            else:
                log.warning('Unrecognized platform "%s"', sys.platform)

        for m in modifier_keys:
            self.ctrl.press(m)
            log.debug('»»»» %s pressed', m)

        executor(self)

        for m in modifier_keys:
            self.ctrl.release(m)
            log.debug('»»»» %s released', m)

        del self.ctrl
    return wrapper

class Event:
    """
    Base class for events. Only contains the time of occurrence (in ms).
    All event classes must derive from this class and -at least- reimplement
    the __str___ and execute methods.
    """
    DOUBLE_CLICK_THRESHOLD = 300
    MOUSE_MOVE_DURATION = .5
    MOUSE_MOVE_EASING = pyautogui.easeInQuad

    @staticmethod
    def create(class_name, from_event=None):
        """
        Creates a new event of the specified class, while preserving the values
        of the origin event (as much as possible).
        :param class_name: The name of the type of event to create
        :param from_event: The event from wich the common values will be copied
        """
        #pylint: disable=eval-used
        new_event = eval(class_name)()
        if from_event is not None:
            for key in new_event.__dict__.keys():
                # collecting old values while preserving new type
                if key in from_event.__dict__ and key != 'type':
                    new_event.__dict__[key] = from_event.__dict__[key]
        return new_event


    @staticmethod
    def get_subclasses():
        """ :return: A list of all the classes inheriting, directly or not from this class. """
        subclasses = get_subclasses(Event)
        subclasses.remove(Event)
        return subclasses

    def __init__(self):
        self.time = round(time.time()*1000)
        self.type = self.__class__.__name__
        self.block_events_on_exec = True
        log.debug(self)

    def __str__(self):
        return 'EVT: @{0}'.format(self.time)

    def execute(self):
        """ Replays the event """
        log.debug(str(self))

    def get_serializable(self):
        return self.__dict__


##     ##  #######  ##     ##  ######  ########    ######## ##     ## ######## ##    ## ########  ######
###   ### ##     ## ##     ## ##    ## ##          ##       ##     ## ##       ###   ##    ##    ##    ##
#### #### ##     ## ##     ## ##       ##          ##       ##     ## ##       ####  ##    ##    ##
## ### ## ##     ## ##     ##  ######  ######      ######   ##     ## ######   ## ## ##    ##     ######
##     ## ##     ## ##     ##       ## ##          ##        ##   ##  ##       ##  ####    ##          ##
##     ## ##     ## ##     ## ##    ## ##          ##         ## ##   ##       ##   ###    ##    ##    ##
##     ##  #######   #######   ######  ########    ########    ###    ######## ##    ##    ##     ######


class MouseImageClick(Event):
    """
    Class representing a single click with the main mouse button on a location
    specified by an image.
    """
    def __init__(self, image_path='', image_index=0):
        """
        :param image_path: The location of the capture of the area to clik on
        :param image_index: If multiple area on the screen exactly match the
                            capture, this is the 0-based index of the area to
                            click on. The numbering starts on the top-left
                            corner of the screen.
        """
        self.path = image_path
        self.index = image_index
        super().__init__()

    def __str__(self):
        return 'CLICK: [{0}#{1}]'.format(self.path, self.index)

    def execute(self):
        locations = pyautogui.locateAllOnScreen(self.path)
        if locations and len(locations) > self.index:
            pyautogui.click(pyautogui.center(locations[self.index]))
        else:
            log.warning('The area defined by "%s#%d" couldn\'t be found',
                        self.path, self.index)


class MouseClick(Event):
    """ Class representing a single click with the main mouse button """
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        self.modifiers = []
        super().__init__()

    def __str__(self):
        return '{2}CLICK: [x={0}, y={1}]'.format(
            self.x,
            self.y,
            format_modifiers(self.modifiers))

    @use_modifiers
    def execute(self):
        log.debug('»»» moving to (%d, %d)', self.x, self.y)
        pyautogui.moveTo(self.x, self.y, Event.MOUSE_MOVE_DURATION, Event.MOUSE_MOVE_EASING)
        log.debug('»»» click!')
        # down then up instead of pyautogui.click() to prevent Java missing event
        pyautogui.mouseDown()
        pyautogui.mouseUp()


class MouseDrag(MouseClick):
    """ Class representing a click-drag-release with the mouse """
    def __init__(self, x0=0, y0=0, x1=0, y1=0):
        """
        :parm start_location: The coordinates where the mouse clicked
        :type start_location: A two elements tuple
        :parm end_location: The coordinates where the mouse dropped the dragged element
        :type end_location: A two elements tuple
        """
        self.xx = x1
        self.yy = y1
        super().__init__(x0, y0)

    def __str__(self):
        return 'DRAG: [x0={0}, y0={1}] -> [x1={2}, y1={3}]'.format(
            self.x, self.y, self.xx, self.yy)

    @use_modifiers
    def execute(self):
        log.debug('»»» dragging from (%d,%d) to (%d,%d)', self.x, self.y, self.xx, self.yy)
        log.debug('»»»   moving to (%d, %d)', self.x, self.y)
        pyautogui.moveTo(self.x, self.y, Event.MOUSE_MOVE_DURATION)
        log.debug('»»»   pressing mouse button')
        pyautogui.mouseDown()
        log.debug('»»»   moving to (%d, %d)', self.xx, self.yy)
        pyautogui.moveTo(self.xx, self.yy, Event.MOUSE_MOVE_DURATION)
        log.debug('»»»   releasing mouse button')
        pyautogui.mouseUp()
        log.debug('»»» .')


class MouseAltClick(MouseClick):
    """ Class representing a single click with the secondary mouse button """

    def __str__(self):
        return '{2}ALT CLICK: [x={0}, y={1}]'.format(
            self.x,
            self.y,
            format_modifiers(self.modifiers))

    @use_modifiers
    def execute(self):
        log.debug('»»» moving to (%d, %d)', self.x, self.y)
        pyautogui.moveTo(self.x, self.y, Event.MOUSE_MOVE_DURATION, Event.MOUSE_MOVE_EASING)
        log.debug('»»» click (alt)')
        pyautogui.rightClick()
        log.debug('»»» .')


class MouseDoubleClick(MouseClick):
    """ Class representing a doube click with the main mouse button """

    def __str__(self):
        return '{2}DOUBLE CLICK: [x={0}, y={1}]'.format(
            self.x,
            self.y,
            format_modifiers(self.modifiers))

    def execute(self):
        log.debug('»»» moving to (%d, %d)', self.x, self.y)
        pyautogui.moveTo(self.x, self.y, Event.MOUSE_MOVE_DURATION, Event.MOUSE_MOVE_EASING)
        log.debug('»»» click, click!')
        pyautogui.doubleClick()
        log.debug('»»» .')


##    ## ######## ##    ##    ######## ##     ## ######## ##    ## ########  ######
##   ##  ##        ##  ##     ##       ##     ## ##       ###   ##    ##    ##    ##
##  ##   ##         ####      ##       ##     ## ##       ####  ##    ##    ##
#####    ######      ##       ######   ##     ## ######   ## ## ##    ##     ######
##  ##   ##          ##       ##        ##   ##  ##       ##  ####    ##          ##
##   ##  ##          ##       ##         ## ##   ##       ##   ###    ##    ##    ##
##    ## ########    ##       ########    ###    ######## ##    ##    ##     ######


MODIFIERS = (Key.ctrl, Key.ctrl_l, Key.ctrl_r,
             Key.alt, Key.alt_l, Key.alt_r, Key.alt_gr,
             Key.shift, Key.shift_l, Key.shift_r,
             KeyCode(65027))

class TextEntered(Event):
    """ Class representing a sequence of key strokes on the keyboard """
    def __init__(self, text=' '):
        """
        :parm text: The coordinates where the mouse clicked
        """
        self.text = str(text.char) if isinstance(text, KeyCode) else str(text)
        super().__init__()

    def __str__(self):
        txt = ''
        if self.text is not None:
            txt = ''.join(self.text)
        return 'TYPE: [txt={0}]'.format(txt)

    def execute(self):
        # DON'T USE PYAUTOGUI FOR THIS
        kbd = Controller()
        kbd.type(self.text)
        log.debug('»»» %s', self.text)


VK_TO_KEY = dict((k.value.vk, k.name) for k in Key)

class KeyPressed(Event):
    def __init__(self, key=Key.esc):
        """ :type key: Key or KeyCode or the name as a string """
        if isinstance(key, Key):
            self.key = key.name
        elif isinstance(key, KeyCode):
            log.warning('KeyPressed registered a KeyCode: %s', key)
            if key.char is not None:
                self.key = key.char
            elif key.vk in VK_TO_KEY:
                self.key = VK_TO_KEY[key.vk]
            else:
                log.warning('Not a known KeyCode: %s', key)
                self.key = key.vk
        elif isinstance(key, str):
            if key in Key.__members__:
                self.key = key
            else:
                log.error('Not a valid key name: %s', key)
                raise TypeError()
        else:
            log.error('Wrong type of key: %s', key)
            raise TypeError()
        super().__init__()

    def __str__(self):
        return 'KEY: [{0}]'.format(self.key)

    def execute(self):
        ctrl = Controller()
        if isinstance(self.key, str):
            k = Key[self.key]
        elif isinstance(self.key, int):
            k = KeyCode(self.key)
        else:
            raise TypeError()

        ctrl.press(k)
        log.debug('»»» %s pressed', self.key)
        ctrl.release(k)
        log.debug('»»» %s released', self.key)

ALT_GR = {
    'win32': (Key.ctrl_l, Key.alt_r),
    'linux': (KeyCode(65027),)
}

class Shortcut(KeyPressed):
    def __init__(self, key=Key.esc, modifiers=None):
        self.modifiers = tuple()
        if modifiers is not None:
            if tuple(modifiers) in ALT_GR.values():
                self.modifiers = (Key.alt_gr.name,)
            else:
                self.modifiers = tuple(k.name for k in modifiers)
        super().__init__(key)

    def __str__(self):
        return 'SHORTCUT: [{0}+{1}]'.format('+'.join(self.modifiers), self.key)

    @use_modifiers
    def execute(self):
        log.debug('»»» %s', self)
        if self.key in Key.__members__:
            k = Key[self.key]
        else:
            k = self.key

        # pylint: disable=no-member
        ctrl = self.ctrl if hasattr(self, 'ctrl') else Controller()

        ctrl.press(k)
        log.debug('»»»» %s pressed', k)
        ctrl.release(k)
        log.debug('»»»» %s released', k)


 #######  ######## ##     ## ######## ########     ######## ##     ## ######## ##    ## ########  ######
##     ##    ##    ##     ## ##       ##     ##    ##       ##     ## ##       ###   ##    ##    ##    ##
##     ##    ##    ##     ## ##       ##     ##    ##       ##     ## ##       ####  ##    ##    ##
##     ##    ##    ######### ######   ########     ######   ##     ## ######   ## ## ##    ##     ######
##     ##    ##    ##     ## ##       ##   ##      ##        ##   ##  ##       ##  ####    ##          ##
##     ##    ##    ##     ## ##       ##    ##     ##         ## ##   ##       ##   ###    ##    ##    ##
 #######     ##    ##     ## ######## ##     ##    ########    ###    ######## ##    ##    ##     ######


class Delay(Event):
    """ Class representing pause in the action (waiting for something to complete) """
    def __init__(self, duration=.5):
        """
        :type duration: int
        :param duration: Duration of the waiting time, in milliseconds
        """
        self.duration = duration
        self.block_events_on_exec = False
        self.exit = threading.Event()
        super().__init__()

    def __str__(self):
        return 'PAUSE: [dur={0}s]'.format(self.duration)

    def execute(self):
        log.debug('»»» waiting %fs', self.duration)
        self.exit.wait(self.duration)
        log.debug('»»» .')

    def kill(self):
        self.exit.set()

    def get_serializable(self):
        items = self.__dict__.copy()
        del items['exit']
        del items['block_events_on_exec']
        return items

class Comment(Event):
    """ Class for comment event (just does nothing on execution) """
    def __init__(self, text=''):
        self.text = text
        super().__init__()

    def __str__(self):
        return '/*** {} ***/'.format(self.text)

    def execute(self):
        log.debug('»»» /*** %s ***/', self.text)


class EventSequence(Event):
    """ A list of events"""
    DEFAULT_DELAY = 200

    def __init__(self, event_list=None):
        super().__init__()

        self.events = []
        if event_list is not None:
            self.events = event_list
        self.delay = EventSequence.DEFAULT_DELAY

    def __str__(self):
        return "{0} events in list".format(len(self.events))

    def execute(self):
        for e in self.events:
            e.execute()
