import ctypes
import math
import os
import random
import time
import tkinter as tk
from tkinter import messagebox

DOT_DIAMETER = 40
MIN_SPEED = 300.0
MAX_SPEED = 800.0
FRAME_MS = 16
MAX_DT = 0.05
EXIT_SIZE = 48
EXIT_MARGIN = 12
EXIT_PADDING = 10
CROSS_PADDING = 15
JITTER_MIN = 1.2
JITTER_MAX = 2.4
JITTER_ANGLE = 0.6
HINT_MS = 6000
VIBRATION_MS = 50
VIBRATION_SPEED = 65535

ES_CONTINUOUS = 0x80000000
ES_SYSTEM_REQUIRED = 0x00000001
ES_DISPLAY_REQUIRED = 0x00000002


class XINPUT_VIBRATION(ctypes.Structure):
    _fields_ = [
        ("wLeftMotorSpeed", ctypes.c_ushort),
        ("wRightMotorSpeed", ctypes.c_ushort),
    ]


class LaserCatApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Лазер для кота")
        self.root.configure(bg="#000000")
        self.root.attributes("-fullscreen", True)
        self.root.protocol("WM_DELETE_WINDOW", self.request_exit)

        self.canvas = tk.Canvas(self.root, bg="#000000", highlightthickness=0, cursor="none")
        self.canvas.pack(fill="both", expand=True)
        self.canvas.focus_set()
        self.canvas.bind("<Button-1>", self.on_click)
        self.canvas.bind("<Configure>", self.on_resize)

        self.root.bind("<Escape>", self.request_exit)
        self.root.bind("<F11>", self.toggle_fullscreen)
        self.root.bind("<FocusOut>", self.on_focus_out)
        self.root.bind("<FocusIn>", self.on_focus_in)
        self.root.bind("<Unmap>", self.on_focus_out)
        self.root.bind("<Map>", self.on_focus_in)

        self.width = 0
        self.height = 0
        self.x = DOT_DIAMETER / 2
        self.y = DOT_DIAMETER / 2
        self.speed = random.uniform(MIN_SPEED, MAX_SPEED)
        angle = random.uniform(0.0, math.tau)
        self.vx = math.cos(angle) * self.speed
        self.vy = math.sin(angle) * self.speed

        self.dot = None
        self.exit_rect = None
        self.exit_cross = ()
        self.exit_box = (0, 0, 0, 0)
        self.hint = None
        self.hits_label = None

        self.catches = 0
        self.paused = False
        self.dialog_open = False
        self.started = False
        self.last_time = time.perf_counter()
        self.next_jitter = self.last_time + random.uniform(JITTER_MIN, JITTER_MAX)
        self.xinput = self.load_xinput()

        self.keep_display_awake()

    def load_xinput(self):
        if os.name != "nt":
            return None
        try:
            return ctypes.WinDLL("xinput1_4")
        except OSError:
            return None

    def keep_display_awake(self):
        if os.name != "nt":
            return
        try:
            ctypes.windll.kernel32.SetThreadExecutionState(
                ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
            )
        except (AttributeError, OSError) as exc:
            messagebox.showerror("Лазер для кота", "Не удалось отключить спящий режим: " + str(exc))

    def release_display_awake(self):
        if os.name != "nt":
            return
        try:
            ctypes.windll.kernel32.SetThreadExecutionState(ES_CONTINUOUS)
        except (AttributeError, OSError) as exc:
            messagebox.showerror("Лазер для кота", "Не удалось вернуть спящий режим: " + str(exc))

    def on_resize(self, event):
        self.width = event.width
        self.height = event.height
        if self.dot is None:
            self.create_items()
        else:
            self.layout_items()
        if not self.started and self.width > 1 and self.height > 1:
            self.started = True
            self.last_time = time.perf_counter()
            self.next_jitter = self.last_time + random.uniform(JITTER_MIN, JITTER_MAX)
            self.root.after(FRAME_MS, self.tick)

    def create_items(self):
        radius = DOT_DIAMETER / 2
        self.x = max(radius, self.width / 2)
        self.y = max(radius, self.height / 2)

        self.dot = self.canvas.create_oval(0, 0, 0, 0, fill="#ff0000", outline="")

        self.exit_rect = self.canvas.create_rectangle(
            0, 0, 0, 0, outline="#ffffff", width=2, stipple="gray50"
        )
        cross_one = self.canvas.create_line(
            0, 0, 0, 0, fill="#ffffff", width=3, capstyle=tk.ROUND, stipple="gray50"
        )
        cross_two = self.canvas.create_line(
            0, 0, 0, 0, fill="#ffffff", width=3, capstyle=tk.ROUND, stipple="gray50"
        )
        self.exit_cross = (cross_one, cross_two)

        self.hint = self.canvas.create_text(
            0, 0,
            text="Клик мышью — новая траектория    Esc — выход",
            fill="#787878",
            font=("Segoe UI", 14),
        )
        self.hits_label = self.canvas.create_text(
            20, 0,
            text="Поймано: 0",
            fill="#606060",
            font=("Segoe UI", 12),
            anchor="w",
        )

        self.layout_items()
        self.root.after(HINT_MS, self.hide_hint)

    def layout_items(self):
        radius = DOT_DIAMETER / 2
        self.x = min(max(self.x, radius), max(radius, self.width - radius))
        self.y = min(max(self.y, radius), max(radius, self.height - radius))
        self.place_dot()

        x0 = max(0, self.width - EXIT_MARGIN - EXIT_SIZE)
        y0 = EXIT_MARGIN
        x1 = x0 + EXIT_SIZE
        y1 = y0 + EXIT_SIZE
        self.exit_box = (x0 - EXIT_PADDING, y0 - EXIT_PADDING, x1 + EXIT_PADDING, y1 + EXIT_PADDING)
        self.canvas.coords(self.exit_rect, x0, y0, x1, y1)
        self.canvas.coords(
            self.exit_cross[0],
            x0 + CROSS_PADDING, y0 + CROSS_PADDING,
            x1 - CROSS_PADDING, y1 - CROSS_PADDING,
        )
        self.canvas.coords(
            self.exit_cross[1],
            x0 + CROSS_PADDING, y1 - CROSS_PADDING,
            x1 - CROSS_PADDING, y0 + CROSS_PADDING,
        )

        if self.hint is not None:
            self.canvas.coords(self.hint, self.width / 2, 44)
        self.canvas.coords(self.hits_label, 20, max(20, self.height - 28))

    def place_dot(self):
        radius = DOT_DIAMETER / 2
        self.canvas.coords(
            self.dot,
            self.x - radius, self.y - radius,
            self.x + radius, self.y + radius,
        )

    def hide_hint(self):
        if self.hint is not None:
            self.canvas.delete(self.hint)
            self.hint = None

    def tick(self):
        now = time.perf_counter()
        dt = now - self.last_time
        self.last_time = now
        if dt > MAX_DT:
            dt = MAX_DT
        if not self.paused:
            self.advance(dt)
            if now >= self.next_jitter:
                self.jitter()
                self.next_jitter = now + random.uniform(JITTER_MIN, JITTER_MAX)
        self.root.after(FRAME_MS, self.tick)

    def advance(self, dt):
        radius = DOT_DIAMETER / 2
        left = radius
        top = radius
        right = max(radius, self.width - radius)
        bottom = max(radius, self.height - radius)

        self.x += self.vx * dt
        self.y += self.vy * dt

        if self.x <= left:
            self.x = left
            self.vx = abs(self.vx) * random.uniform(0.85, 1.15)
        elif self.x >= right:
            self.x = right
            self.vx = -abs(self.vx) * random.uniform(0.85, 1.15)

        if self.y <= top:
            self.y = top
            self.vy = abs(self.vy) * random.uniform(0.85, 1.15)
        elif self.y >= bottom:
            self.y = bottom
            self.vy = -abs(self.vy) * random.uniform(0.85, 1.15)

        current = math.hypot(self.vx, self.vy)
        self.speed = min(max(current, MIN_SPEED), MAX_SPEED)
        if current > 0.0:
            factor = self.speed / current
            self.vx *= factor
            self.vy *= factor

        self.place_dot()

    def jitter(self):
        angle = math.atan2(self.vy, self.vx) + random.uniform(-JITTER_ANGLE, JITTER_ANGLE)
        self.speed = min(max(self.speed, MIN_SPEED), MAX_SPEED)
        self.vx = math.cos(angle) * self.speed
        self.vy = math.sin(angle) * self.speed

    def on_click(self, event):
        if self.paused:
            return
        x0, y0, x1, y1 = self.exit_box
        if x0 <= event.x <= x1 and y0 <= event.y <= y1:
            self.request_exit()
            return

        dx = self.x - event.x
        dy = self.y - event.y
        if abs(dx) < 1.0 and abs(dy) < 1.0:
            angle = random.uniform(0.0, math.tau)
        else:
            angle = math.atan2(dy, dx)

        self.speed = random.uniform(MIN_SPEED, MAX_SPEED)
        self.vx = math.cos(angle) * self.speed
        self.vy = math.sin(angle) * self.speed
        self.catches += 1
        self.canvas.itemconfigure(self.hits_label, text="Поймано: " + str(self.catches))
        self.vibrate()

        self.last_time = time.perf_counter()
        self.next_jitter = self.last_time + random.uniform(JITTER_MIN, JITTER_MAX)

    def vibrate(self):
        if self.xinput is not None:
            self.xinput.XInputSetState(
                0, ctypes.byref(XINPUT_VIBRATION(VIBRATION_SPEED, VIBRATION_SPEED))
            )
            self.root.after(VIBRATION_MS, self.stop_vibration)
        self.flash_dot()

    def stop_vibration(self):
        if self.xinput is not None:
            self.xinput.XInputSetState(0, ctypes.byref(XINPUT_VIBRATION(0, 0)))

    def flash_dot(self):
        if self.dot is None:
            return
        self.canvas.itemconfigure(self.dot, fill="#ff8080")
        self.root.after(VIBRATION_MS, self.restore_glow)

    def restore_glow(self):
        if self.dot is not None:
            self.canvas.itemconfigure(self.dot, fill="#ff0000")

    def on_focus_out(self, event=None):
        if not self.dialog_open:
            self.paused = True

    def on_focus_in(self, event=None):
        if not self.dialog_open:
            self.paused = False
            self.last_time = time.perf_counter()

    def toggle_fullscreen(self, event=None):
        current = bool(self.root.attributes("-fullscreen"))
        self.root.attributes("-fullscreen", not current)

    def request_exit(self, event=None):
        if self.dialog_open:
            return
        self.dialog_open = True
        self.paused = True
        answer = messagebox.askyesno("Лазер для кота", "Закрыть приложение?")
        self.dialog_open = False
        self.paused = False
        self.last_time = time.perf_counter()
        if answer:
            self.release_display_awake()
            self.root.destroy()


def main():
    root = tk.Tk()
    LaserCatApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()