#!/usr/bin/env python3
import socket
import keyboard  # pip install keyboard
import mouse     # pip install mouse
import threading
import traceback
import sys
import time
import ctypes
from ctypes import wintypes

from PyQt5 import QtCore, QtGui, QtWidgets

SERVER_IP = "192.168.1.17"
SERVER_PORT = 65432

# ----------------------------- Windows helpers for no-activate + click-through -----------------------------
def _make_clickthrough_noactivate(winid: int):
    """
    On Windows, set WS_EX_NOACTIVATE and WS_EX_TRANSPARENT so the overlay never steals focus
    and is click-through. Safe to call multiple times.
    """
    if sys.platform != "win32":
        return
    hwnd = wintypes.HWND(winid)
    GWL_EXSTYLE = -20
    WS_EX_TRANSPARENT = 0x00000020
    WS_EX_LAYERED = 0x00080000
    WS_EX_NOACTIVATE = 0x08000000
    WS_EX_TOOLWINDOW = 0x00000080  # keeps it out of Alt-Tab

    user32 = ctypes.windll.user32
    getlong = user32.GetWindowLongW
    setlong = user32.SetWindowLongW

    ex = getlong(hwnd, GWL_EXSTYLE)
    ex |= (WS_EX_TRANSPARENT | WS_EX_NOACTIVATE | WS_EX_LAYERED | WS_EX_TOOLWINDOW)
    setlong(hwnd, GWL_EXSTYLE, ex)

# ----------------------------- Qt Overlay -----------------------------
class Overlay(QtWidgets.QWidget):
    """Static, borderless, always-on-top overlay that is click-through and non-activating."""
    def __init__(self, initial_text="macro: OFF"):
        super().__init__()
        self.setWindowFlags(
            QtCore.Qt.FramelessWindowHint
            | QtCore.Qt.WindowStaysOnTopHint
            | QtCore.Qt.Tool  # hides from taskbar/alt-tab
        )

        # Do not accept focus or mouse
        self.setFocusPolicy(QtCore.Qt.NoFocus)
        if hasattr(QtCore.Qt, "WindowDoesNotAcceptFocus"):
            self.setWindowFlag(QtCore.Qt.WindowDoesNotAcceptFocus, True)
        self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating, True)
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
        self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, True)  # clicks pass through

        self._text = initial_text
        self._bg_opacity = 0.85
        self._padding = 10
        self._radius = 10
        self._font = QtGui.QFont("Consolas", 10)

        # Fixed size/pos (static)
        self.resize(130, 50)
        self.move(5, 580)

    @QtCore.pyqtSlot(str)
    def set_text(self, s: str):
        self._text = s
        self.update()

    def showEvent(self, e: QtGui.QShowEvent):
        super().showEvent(e)
        # Apply Windows click-through + no-activate at OS level
        try:
            _make_clickthrough_noactivate(int(self.winId()))
        except Exception:
            pass

    def paintEvent(self, _evt):
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)

        rect = self.rect().adjusted(0, 0, -1, -1)
        bg = QtGui.QColor(10, 10, 10)
        bg.setAlphaF(self._bg_opacity)

        painter.setBrush(bg)
        painter.setPen(QtCore.Qt.NoPen)
        painter.drawRoundedRect(rect, self._radius, self._radius)

        painter.setPen(QtGui.QPen(QtGui.QColor(180, 255, 180)))
        painter.setFont(self._font)

        text_rect = self.rect().adjusted(self._padding, self._padding, -self._padding, -self._padding)
        painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop, self._text)


class UiBus(QtCore.QObject):
    set_label = QtCore.pyqtSignal(str)
    quit_app = QtCore.pyqtSignal()

# ----------------------------- Worker / Client -----------------------------
def client_worker(ui: UiBus, stop_evt: threading.Event):
    hooks = {"keyboard": [], "mouse": []}

    state = {
        "macro1": False,  # toggled by 'L'
        "macro2": False,  # toggled by '='
        "macro3": False,  # toggled by 'home'
    }

    def overlay_text():
        if state["macro1"]:
            return "macro: #1"
        if state["macro2"]:
            return "macro: #2"
        if state["macro3"]:
            return "macro: #3"
        return "macro: OFF"

    def push_overlay():
        ui.set_label.emit(overlay_text())

    def log(msg: str):
        print(msg, flush=True)

    def send_cmd(sock, cmd: bytes):
        try:
            sock.sendall(cmd)
            sock.recv(1024)
        except Exception as e:
            log(f"[!] send_cmd failed ({cmd!r}): {e}")

    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((SERVER_IP, SERVER_PORT))
            log(f"Connected {SERVER_IP}:{SERVER_PORT}")
            push_overlay()

            mouse_pressed = False

            def set_mode(mode: str):
                """mode: 'off' | 'macro1' | 'macro2' | 'macro3'"""
                send_cmd(s, f"mode {mode}".encode())

            def activate_macro1():
                # enable macro1, disable others
                state["macro1"] = True
                state["macro2"] = False
                state["macro3"] = False
                set_mode("macro1")
                push_overlay()

            def activate_macro2():
                state["macro2"] = True
                state["macro1"] = False
                state["macro3"] = False
                set_mode("macro2")
                push_overlay()

            def activate_macro3():
                state["macro3"] = True
                state["macro1"] = False
                state["macro2"] = False
                set_mode("macro3")
                push_overlay()

            def deactivate_all():
                state["macro1"] = False
                state["macro2"] = False
                state["macro3"] = False
                set_mode("off")
                push_overlay()

            def on_toggle(event):
                # 'L' -> macro1, '=' -> macro2, 'home' -> macro3; mutually exclusive
                if event.event_type != keyboard.KEY_DOWN:
                    return
                name = event.name
                if name == "l":
                    if state["macro1"]:
                        deactivate_all()
                    else:
                        activate_macro1()
                elif name in ("=", "equal"):
                    if state["macro2"]:
                        deactivate_all()
                    else:
                        activate_macro2()
                elif name == "home":
                    if state["macro3"]:
                        deactivate_all()
                    else:
                        activate_macro3()

            def on_mouse_event(event):
                nonlocal mouse_pressed
                try:
                    active = state["macro1"] or state["macro2"] or state["macro3"]
                    if active and isinstance(event, mouse.ButtonEvent):
                        if event.event_type == mouse.DOWN and event.button == mouse.LEFT and not mouse_pressed:
                            send_cmd(s, b"mouse left down")
                            mouse_pressed = True
                        elif event.event_type == mouse.UP and event.button == mouse.LEFT and mouse_pressed:
                            send_cmd(s, b"mouse left up")
                            mouse_pressed = False
                except Exception as e:
                    log(f"[!] Mouse send failed: {e}")

            # Hooks (overlay is click-through, so it won't steal focus)
            keyboard.hook(on_toggle);            hooks["keyboard"].append(on_toggle)
            mouse.hook(on_mouse_event);          hooks["mouse"].append(on_mouse_event)

            push_overlay()

            # Run until '-' is pressed
            while not stop_evt.is_set():
                if keyboard.is_pressed('-'):
                    log("[*] Exit requested by '-'")
                    ui.quit_app.emit()
                    break
                time.sleep(0.05)

    except Exception as e:
        push_overlay()
        log("An error occurred:\n" + "".join(traceback.format_exception(type(e), e, e.__traceback__)))
    finally:
        # tell server we're off
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s2:
                s2.settimeout(0.5)
                s2.connect((SERVER_IP, SERVER_PORT))
                s2.sendall(b"mode off")
                _ = s2.recv(1024)
        except Exception:
            pass

        state["macro1"] = False
        state["macro2"] = False
        state["macro3"] = False
        push_overlay()
        try:
            for cb in hooks["keyboard"]:
                keyboard.unhook(cb)
            for cb in hooks["mouse"]:
                mouse.unhook(cb)
        except Exception:
            pass

# ----------------------------- Main -----------------------------
def main():
    app = QtWidgets.QApplication(sys.argv)

    bus = UiBus()
    overlay = Overlay("macro: OFF")
    bus.set_label.connect(overlay.set_text)
    bus.quit_app.connect(app.quit)
    overlay.show()

    stop_evt = threading.Event()
    t = threading.Thread(target=client_worker, args=(bus, stop_evt), daemon=True)
    t.start()

    def on_about_to_quit():
        stop_evt.set()
        try:
            t.join(timeout=2.0)
        except:
            pass

    app.aboutToQuit.connect(on_about_to_quit)
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
