from talon import Module, Context, ui
from talon.screen import Screen
from talon.canvas import Canvas, MouseEvent
from skia import Canvas as SkiaCanvas
from talon.types import Rect, Point2d

mod = Module()
ctx = Context()
canvas: Canvas | None = None
last_mouse_pos: Point2d | None = None

mod.tag("gamepad_tester", "Gamepad tester gui is showing")

buttons = {
    "dpad_up": False,
    "dpad_right": False,
    "dpad_down": False,
    "dpad_left": False,
    "north": False,
    "east": False,
    "south": False,
    "west": False,
    "select": False,
    "start": False,
    "l1": False,
    "r1": False,
    "l3": False,
    "r3": False,
}

triggers = {
    "l2": 0.0,
    "r2": 0.0,
}

sticks = {
    "left": (0.0, 0.0),
    "right": (0.0, 0.0),
}

BACKGROUND_COLOR = "fffafa"  # Snow
BORDER_COLOR = "000000"  # Black
FONT_SIZE = 16
WIDTH = 900
HEIGHT = 800
CIRCLE_RADIUS = 100
BUTTON_OFFSET = CIRCLE_RADIUS / 2
BUTTON_RADIUS = BUTTON_OFFSET / 2
ROW_OFFSET = CIRCLE_RADIUS * 1.25
BUTTON_FLAT_WIDTH = BUTTON_RADIUS * 3
BUTTON_FLAT_HEIGHT = BUTTON_RADIUS
TRIGGER_HEIGHT = CIRCLE_RADIUS
BUTTON_OFFSETS = [(0, -1), (1, 0), (0, 1), (-1, 0)]


def render_round_button(c: SkiaCanvas, x: float, y: float, is_pressed: bool):
    c.paint.style = c.paint.Style.FILL if is_pressed else c.paint.Style.STROKE
    c.draw_circle(x, y, BUTTON_RADIUS)


def render_square_button(c: SkiaCanvas, x: float, y: float, is_pressed: bool):
    c.paint.style = c.paint.Style.FILL if is_pressed else c.paint.Style.STROKE
    c.draw_rect(
        Rect(
            x - BUTTON_RADIUS,
            y - BUTTON_RADIUS,
            BUTTON_RADIUS * 2,
            BUTTON_RADIUS * 2,
        )
    )


def render_flat_button(c: SkiaCanvas, x: float, y: float, is_pressed: bool):
    c.paint.style = c.paint.Style.FILL if is_pressed else c.paint.Style.STROKE
    c.draw_rect(
        Rect(
            x - BUTTON_FLAT_WIDTH / 2,
            y - BUTTON_FLAT_HEIGHT / 2,
            BUTTON_FLAT_WIDTH,
            BUTTON_FLAT_HEIGHT,
        )
    )


def render_buttons(
    c: SkiaCanvas, x: float, y: float, useCircle: bool, buttons_ids: list[str]
):
    c.paint.style = c.paint.Style.STROKE
    c.draw_circle(x, y, CIRCLE_RADIUS)
    for i, button_id in enumerate(buttons_ids):
        (offset_x, offset_y) = BUTTON_OFFSETS[i]
        button_x = x + offset_x * BUTTON_OFFSET
        button_y = y + offset_y * BUTTON_OFFSET
        is_pressed = buttons[button_id]
        if useCircle:
            render_round_button(c, button_x, button_y, is_pressed)
        else:
            render_square_button(c, button_x, button_y, is_pressed)


def render_trigger(c: SkiaCanvas, x: float, y: float, value: float):
    # Render button outline
    c.paint.style = c.paint.Style.STROKE
    c.draw_rect(
        Rect(
            x - BUTTON_FLAT_WIDTH / 2,
            y - TRIGGER_HEIGHT,
            BUTTON_FLAT_WIDTH,
            TRIGGER_HEIGHT,
        )
    )
    # Render button value
    c.paint.style = c.paint.Style.FILL
    height = value * TRIGGER_HEIGHT
    c.draw_rect(
        Rect(
            x - BUTTON_FLAT_WIDTH / 2,
            y - height,
            BUTTON_FLAT_WIDTH,
            height,
        )
    )
    # Render value text
    text = str(round(value * 100))
    text_rect = c.paint.measure_text(text)[1]
    c.draw_text(
        text,
        x - text_rect.x - text_rect.width / 2,
        y - TRIGGER_HEIGHT - text_rect.height,
    )


def render_stick(
    c: SkiaCanvas, x: float, y: float, is_pressed: bool, value_x: float, value_y: float
):
    # Stick click
    if is_pressed:
        c.paint.style = c.paint.Style.FILL
        c.draw_circle(x, y, CIRCLE_RADIUS)
        return

    c.paint.style = c.paint.Style.STROKE
    # Draw outer circle
    c.draw_circle(x, y, CIRCLE_RADIUS)
    # Draw cross
    c.draw_line(x - CIRCLE_RADIUS, y, x + CIRCLE_RADIUS, y)
    c.draw_line(x, y - CIRCLE_RADIUS, x, y + CIRCLE_RADIUS)
    dot_x = x + value_x * CIRCLE_RADIUS
    dot_y = y + value_y * CIRCLE_RADIUS
    # Draw line to dot
    c.draw_line(x, y, dot_x, dot_y)
    # Draw center dot
    c.paint.style = c.paint.Style.FILL
    c.draw_circle(dot_x, dot_y, 5)
    # Render value texts
    text = f"{round(value_x * 100)}, {round(value_y * 100)}"
    text_rect = c.paint.measure_text(text)[1]
    c.draw_text(
        text,
        x - text_rect.x - text_rect.width / 2,
        y - CIRCLE_RADIUS - text_rect.height,
    )


def render_close_text(c: SkiaCanvas, x: float, y: float):
    text = 'Say "gamepad tester" to close'
    text_rect = c.paint.measure_text(text)[1]
    c.draw_text(
        text,
        x - text_rect.x - text_rect.width / 2,
        y - 2 * text_rect.height,
    )


def on_draw(c: SkiaCanvas):
    c.paint.textsize = FONT_SIZE

    # Render background
    c.paint.style = c.paint.Style.FILL
    c.paint.color = BACKGROUND_COLOR
    c.draw_rect(c.rect)

    c.paint.color = BORDER_COLOR
    y_center = c.rect.center.y + ROW_OFFSET * 0.75

    offset = CIRCLE_RADIUS * 2.5

    # Draw trigger buttons
    y = y_center - ROW_OFFSET * 2.5
    render_trigger(c, c.rect.center.x - offset, y, triggers["l2"])
    render_trigger(c, c.rect.center.x + offset, y, triggers["r2"])

    # Draw bumper buttons
    y = y_center - ROW_OFFSET * 2.15
    render_flat_button(c, c.rect.center.x - offset, y, buttons["l1"])
    render_flat_button(c, c.rect.center.x + offset, y, buttons["r1"])

    y = y_center - ROW_OFFSET

    # Draw D-pad buttons
    render_buttons(
        c,
        c.rect.center.x - offset,
        y,
        False,
        ["dpad_up", "dpad_right", "dpad_down", "dpad_left"],
    )

    # Draw compass buttons
    render_buttons(
        c,
        c.rect.center.x + offset,
        y,
        True,
        ["north", "east", "south", "west"],
    )

    # Draw select/start buttons
    offset = CIRCLE_RADIUS * 0.75
    y = y_center - ROW_OFFSET / 3
    render_round_button(c, c.rect.center.x - offset, y, buttons["select"])
    render_round_button(c, c.rect.center.x + offset, y, buttons["start"])

    # Draw sticks
    offset = CIRCLE_RADIUS * 1.5
    y = y_center + ROW_OFFSET
    render_stick(c, c.rect.center.x - offset, y, buttons["l3"], *sticks["left"])
    render_stick(c, c.rect.center.x + offset, y, buttons["r3"], *sticks["right"])

    # Draw close text
    render_close_text(c, c.rect.center.x, c.rect.bot)


def on_mouse(e: MouseEvent):
    global last_mouse_pos
    if e.event == "mousedown" and e.button == 0:
        last_mouse_pos = e.gpos
    elif e.event == "mousemove" and last_mouse_pos:
        dx = e.gpos.x - last_mouse_pos.x
        dy = e.gpos.y - last_mouse_pos.y
        last_mouse_pos = e.gpos
        if canvas is not None:
            canvas.move(canvas.rect.x + dx, canvas.rect.y + dy)
    elif e.event == "mouseup" and e.button == 0:
        last_mouse_pos = None


def show():
    global canvas
    screen: Screen = ui.main_screen()
    x = screen.rect.center.x
    y = screen.rect.center.y
    canvas = Canvas.from_rect(Rect(x - WIDTH / 2, y - HEIGHT / 2, WIDTH, HEIGHT))
    canvas.draggable = True
    canvas.blocks_mouse = True
    canvas.register("draw", on_draw)
    canvas.register("mouse", on_mouse)
    ctx.tags = ["user.gamepad_tester"]


def hide():
    global canvas
    if canvas is not None:
        canvas.unregister("draw", on_draw)
        canvas.unregister("mouse", on_mouse)
        canvas.close()
        canvas = None
    ctx.tags = []


@mod.action_class
class Actions:
    def gamepad_tester_toggle():
        """Toggle visibility of gamepad tester gui"""
        if canvas is None:
            show()
        else:
            hide()

    def gamepad_tester_button(id: str, is_pressed: bool):
        """Indicates that a gamepad button has changed state"""
        buttons[id] = is_pressed

    def gamepad_tester_trigger(id: str, value: float):
        """Indicates that a gamepad trigger has changed state"""
        triggers[id] = value

    def gamepad_tester_stick(id: str, x: float, y: float):
        """Indicates that a gamepad stick has changed state"""
        sticks[id] = (x, y)
