#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""Â© Ihor Mirzov, 2019-2023
Distributed under GNU General Public License v3.0

Classes for keycodes sending from master window to slave.
It is a layer between system libraries Xlib/ctypes and GUI.

mw = master window
sw = slave window
"""

# Standard modules
import os
import time
import math
import logging
if os.name == 'nt':
    import ctypes
    from ctypes import wintypes
if os.name == 'posix':
    import Xlib
    from Xlib import display, protocol, X, XK
    from Xlib.ext.xtest import fake_input

# External modules
from PyQt5 import QtWidgets


def align_master(w):
    """Align master window - w could be QMainWindow or QDialog.
    frameSize() is window size, geometry() is the central widget size,
    width and height - desirable window size.
    """
    ag = QtWidgets.QApplication.primaryScreen().availableGeometry()
    w.move(ag.left(), ag.top())
    size = QtWidgets.QApplication.primaryScreen().availableSize()
    width = math.floor(size.width() / 3)
    height = size.height()
    dw = w.frameSize().width() - w.geometry().width() # delta width
    dh = w.frameSize().height() - w.geometry().height() # delta height
    w.resize(width-dw, height-dh)


class WindowInfo:

    def __init__(self, wid, pid, wname):
        self.wid = wid # window identificator
        self.hex_wid = '0x{}'.format(hex(wid)[2:].zfill(8))
        self.pid = pid # process ID
        self.wname = wname # window title

    def to_string(self):
        # return '{}  {: 8d}  {}'\
        #     .format(self.hex_wid, self.pid, self.wname)
        return ', '.join([self.hex_wid, str(self.pid), self.wname])


def post_wrapper(wc):
    """Common wrapper for WindowConnection.post()."""
    def wrap(method):
        def fcn(wc, cmd):
            if wc.get_slave_process() is not None \
                and wc.get_slave_process().poll() is None\
                and wc.wid2 is not None:

                if len(cmd):
                    # Drop previously pressed keys
                    if 'CalculiX GraphiX' in wc.get_slave_title():
                        method(wc, '\n')

                    # Post command
                    method(wc, cmd + '\n')

                    # Flush CGX buffer to get continuous output
                    if 'CalculiX GraphiX' in wc.get_slave_title():
                        method(wc, ' ')

                    return
                else:
                    logging.warning('Empty command.')

            else:
                logging.warning('No slave window.')
        return fcn
    return wrap


class WindowConnection:
    """Pair master and slave windows together
    to align and enable keycodes sending.
    """

    def __init__(self, mw, sw):
        self.opened_windows = [] # list of WindowInfo objects
        self.w = 800
        self.h = 600
        screen = QtWidgets.QApplication.primaryScreen()
        if screen is not None:
            self.w = screen.availableSize().width()
            self.h = screen.availableSize().height()

        if mw is None or sw is None:
            return

        self.mw = mw # master window
        self.sw = sw # slave window
        self.wid1 = self.get_master_wid() # master window id
        self.wid2 = self.get_slave_wid() # slave window id

    def log_opened_windows(self):
        msg = 'Window list:'
        if not len(self.opened_windows):
            self.get_opened_windows()
        for wi in self.opened_windows:
            msg += wi.to_string()
        logging.debug(msg)

    def get_slave_title(self):
        try:
            return self.sw.info.wname
        except:
            logging.error('Can not get slave title.')
            return ""

    def get_slave_process(self):
        try:
            return self.sw.process
        except:
            logging.warning('Can not get slave process.')
            return None

    def get_slave_wid(self):
        try:
            return self.sw.info.wid
        except:
            logging.error('Can not get slave wid.')
            return None

    def get_master_wid(self):
        try:
            return self.mw.info.wid
        except:
            logging.error('Can not get master wid.')
            return None


class WindowConnectionLinux(WindowConnection):

    def __init__(self, mw, sw):
        super().__init__(mw, sw)

        self.d = Xlib.display.Display()
        self.screen = self.d.screen()
        self.root = self.screen.root

        # 0:lowercase, 1:shifted
        self.keyboardMapping = {
            '\t':('Tab', 0),
            '\n':('Return', 0),
            '\r':('Return', 0),
            '\b':('BackSpace', 0),
            ' ': ('space', 0),
            '!': ('exclam', 1),
            '#': ('numbersign', 1),
            '%': ('percent', 1),
            '$': ('dollar', 1),
            '&': ('ampersand', 1),
            '"': ('quotedbl', 1),
            "'": ('apostrophe', 0),
            '(': ('parenleft', 1),
            ')': ('parenright', 1),
            '*': ('asterisk', 1),
            '=': ('equal', 0),
            '+': ('plus', 1),
            ',': ('comma', 0),
            '-': ('minus', 0),
            '.': ('period', 0),
            '/': ('slash', 0),
            ':': ('colon', 1),
            ';': ('semicolon', 0),
            '<': ('less', 0),
            '>': ('greater', 1),
            '?': ('question', 1),
            '@': ('at', 1),
            '[': ('bracketleft', 0),
            ']': ('bracketright', 0),
            '^': ('asciicircum', 1),
            '_': ('underscore', 1),
            '`': ('grave', 0),
            '{': ('braceleft', 1),
            '\\':('backslash', 0),
            '|': ('bar', 1),
            '}': ('braceright', 1),
            '~': ('asciitilde', 1) }
        KEY_NAMES = [
            'Shift_L', 'Control_L', 'Alt_L', 'Pause', 'Caps_Lock',
            'Escape', 'Page_Up', 'Page_Down', 'End', 'Home',
            'Left', 'Up', 'Right', 'Down', 'Print',
            'Insert', 'Delete', 'Help', 'Super_L', 'Super_R',
            'KP_0', 'KP_1', 'KP_2', 'KP_3', 'KP_4', 'KP_5', 'KP_6',
            'KP_7', 'KP_8', 'KP_9', 'KP_Multiply', 'KP_Add',
            'KP_Separator', 'KP_Subtract', 'KP_Decimal', 'KP_Divide',
            'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
            'Num_Lock', 'Scroll_Lock', 'Shift_R', 'Control_R', 'Alt_R' ]
        for c in KEY_NAMES:
            self.keyboardMapping[c] = (c, 0)
        for c in '1234567890abcdefghijklmnopqrstuvwxyz':
            self.keyboardMapping[c] = (c, 0)
        for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
            self.keyboardMapping[c] = (c, 1)

    @post_wrapper(WindowConnection)
    def post(self, cmd):
        """Activate window, send message to slave, deactivate."""

        def event(win, e, keycode, case):
            """Create X event."""
            return e(
                time=Xlib.X.CurrentTime,
                root=self.root,
                window=win,
                same_screen=0, child=Xlib.X.NONE,
                root_x=0, root_y=0, event_x=0, event_y=0,
                state=case, # 0=lowercase, 1=uppercase
                detail=keycode)

        def sendkey(win, symbol):
            """Key press + release."""
            if symbol in self.keyboardMapping:
                c = self.keyboardMapping[symbol][0]
                keysym = XK.string_to_keysym(c)
                code = self.d.keysym_to_keycode(keysym)
                case = self.keyboardMapping[symbol][1]

                e = event(win, Xlib.protocol.event.KeyPress, code, case)
                win.send_event(e, propagate=True)
                e = event(win, Xlib.protocol.event.KeyRelease, code, case)
                win.send_event(e, propagate=True)
            else:
                msg = 'Symbol {} is not supported.'.format(symbol)
                logging.warning(msg)

        win = self.d.create_resource_object('window', self.wid2)
        for symbol in cmd:
            sendkey(win, symbol)
        self.d.sync()
        time.sleep(0.1)

    def get_opened_windows(self):
        """Get dictionary with windows id, pid and wname."""
        self.opened_windows.clear()
        ids = self.root.get_full_property(
            self.d.intern_atom('_NET_CLIENT_LIST'),
            Xlib.X.AnyPropertyType).value
        for _id_ in ids:
            w = self.d.create_resource_object('window', _id_)
            try:
                pid = w.get_full_property(
                    self.d.intern_atom('_NET_WM_PID'), Xlib.X.AnyPropertyType).value[0]
                wname = w.get_full_property(
                    self.d.intern_atom('_NET_WM_NAME'), Xlib.X.AnyPropertyType).value.decode()
            except:
                pid = 0
                wname = w.get_wm_name()
            wi = WindowInfo(_id_, pid, wname)
            self.opened_windows.append(wi)
        return self.opened_windows

    def send_hotkey(self, *keys):
        """Key press + release."""
        for key in keys:
            if key not in self.keyboardMapping:
                logging.warning('WARNING! Key {} is not supported.'.format(key))
                return
        for key in keys:
            c = self.keyboardMapping[key][0]
            keysym = XK.string_to_keysym(c)
            code = self.d.keysym_to_keycode(keysym)
            fake_input(self.d, Xlib.X.KeyPress, code)
        for key in reversed(keys):
            c = self.keyboardMapping[key][0]
            keysym = XK.string_to_keysym(c)
            code = self.d.keysym_to_keycode(keysym)
            fake_input(self.d, Xlib.X.KeyRelease, code)
        self.d.sync()
        time.sleep(0.3)

    def align_windows(self):
        """Align master and slave windows."""

        # Align master
        self.mw.setGeometry(0, 0, math.floor(self.w/3), self.h)

        # Align slave
        if self.wid2 is not None:
            win = self.d.create_resource_object('window', self.wid2)
            win.set_input_focus(Xlib.X.RevertToNone, Xlib.X.CurrentTime)
            self.send_hotkey('Super_L', 'Down') # restore window
            time.sleep(0.3)
            win.configure(x=math.ceil(self.w/3), y=0,
                width=math.floor(self.w*2/3), height=self.h)
        else:
            logging.error('Nothing to align - slave WID is None.')

        self.d.sync()


class WindowConnectionWindows(WindowConnection):

    def __init__(self, mw, sw):
        super().__init__(mw, sw)

        # 0:lowercase, 1:shifted
        self.keyboardMapping = {
            '\t':(9, 0),
            '\n':(13, 0),
            ' ': (32, 0),
            '!': (49, 1),
            '@': (50, 1),
            '#': (51, 1),
            '$': (52, 1),
            '%': (53, 1),
            '^': (54, 1),
            '&': (55, 1),
            '*': (56, 1),
            '(': (57, 1),
            ')': (48, 1),
            ';': (0xBA, 0),
            ':': (0xBA, 1),
            '=': (0xBB, 0),
            '+': (0xBB, 1),
            ',': (0xBC, 0),
            '<': (0xBC, 1),
            '-': (0xBD, 0),
            '_': (0xBD, 1),
            '.': (0xBE, 0),
            '>': (0xBE, 1),
            '/': (0xBF, 0),
            '?': (0xBF, 1),
            '`': (0xC0, 0),
            '~': (0xC0, 1),
            '[': (0xDB, 0),
            '{': (0xDB, 1),
            '\\':(0xDC, 0),
            '|': (0xDC, 1),
            ']': (0xDD, 0),
            '}': (0xDD, 1),
            '\'':(0xDE, 0),
            '\"':(0xDE, 1),
            'F4':(0x73, 0),
            'Alt_L': (0x12, 0),
            # 'VK_LWIN': (0x5B, 0),
            # 'VK_LEFT': (0x25, 0),
            # 'VK_UP':   (0x26, 0),
            # 'VK_RIGHT':(0x27, 0),
            # 'VK_DOWN': (0x28, 0),
            }
        for c in '0123456789':
            self.keyboardMapping[c] = (ord(c), 0)
        for c in 'abcdefghijklmnopqrstuvwxyz':
            self.keyboardMapping[c] = (ord(c)-32, 0)
        for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
            self.keyboardMapping[c] = (ord(c), 1)

    @post_wrapper(WindowConnection)
    def post(self, cmd):
        """Activate window, send message to slave, deactivate."""

        def sendkey(symbol):
            """Key press (0) + release (2)."""
            if symbol not in self.keyboardMapping:
                msg = 'Symbol {} is not supported.'.format(symbol)
                logging.warning(msg)
                return
            code = self.keyboardMapping[symbol][0]
            case = self.keyboardMapping[symbol][1]
            if case == 1: # press Shift
                ctypes.windll.user32.keybd_event(0x10, 0, 0, 0)
            ctypes.windll.user32.keybd_event(code, 0, 0, 0) # key press
            ctypes.windll.user32.keybd_event(code, 0, 2, 0) # key release
            if case == 1: # release Shift
                ctypes.windll.user32.keybd_event(0x10, 0, 2, 0)

        ctypes.windll.user32.SetForegroundWindow(self.wid2)
        for symbol in cmd:
            time.sleep(0.001) # BUG doesn't work otherwise
            sendkey(symbol)
        ctypes.windll.user32.SetForegroundWindow(self.wid1)
        time.sleep(0.1)

    def get_opened_windows(self):
        self.opened_windows.clear()
        WNDENUMPROC = ctypes.WINFUNCTYPE(
            wintypes.BOOL,
            wintypes.HWND,    # _In_ hwnd
            wintypes.LPARAM,) # _In_ lParam

        @WNDENUMPROC
        def enum_proc(hwnd, lParam):
            if ctypes.windll.user32.IsWindowVisible(hwnd):
                pid = wintypes.DWORD()
                length = ctypes.windll.user32.GetWindowTextLengthW(hwnd) + 1
                buff = ctypes.create_unicode_buffer(length)
                ctypes.windll.user32.GetWindowTextW(hwnd, buff, length)
                wi = WindowInfo(hwnd, int(str(pid)[8:-1]), buff.value)
                self.opened_windows.append(wi)
            return True

        ctypes.windll.user32.EnumWindows(enum_proc, 0)
        return self.opened_windows

    def send_hotkey(self, *keys):
        """Key press + release."""
        for key in keys:
            if key not in self.keyboardMapping:
                logging.warning('WARNING! Key {} is not supported.'.format(key))
                return
        for key in keys:
            code = self.keyboardMapping[key][0]
            ctypes.windll.user32.keybd_event(code, 0, 0, 0) # key press
        for key in reversed(keys):
            code = self.keyboardMapping[key][0]
            ctypes.windll.user32.keybd_event(code, 0, 2, 0) # key release
        time.sleep(0.3)

    def align_windows(self):
        """Align master and slave windows.
        NOTE CGX 'wpos' and 'wsize' works worse than 'ctypes'.
        TODO Split on align_master() and align_slave()
        TODO Move align_master() and align_slave to the Window
        """
        # Align master
        align_master(self.mw)

        # Align slave
        ag = QtWidgets.QApplication.primaryScreen().availableGeometry()
        size = QtWidgets.QApplication.primaryScreen().availableSize()
        width = math.floor(size.width() / 3)
        height = size.height()
        if self.wid2 is not None:
            ctypes.windll.user32.ShowWindow(self.wid2, 9) # restore window
            time.sleep(0.3)
            ctypes.windll.user32.MoveWindow(self.wid2,
                ag.left() + width + 1, ag.top(),
                size.width() - width, height, True)
            # ctypes.windll.user32.SetWindowPos(self.wid2, 0,
            #     ag.left() + width + 1, ag.top(),
            #     size.width() - width, height, 0x0040)
        else:
            logging.error('Nothing to align - slave WID is None.')
