"""SDL2-based frame implementation."""

from __future__ import annotations

import platform
from ctypes import byref, c_int
from typing import TYPE_CHECKING, Final

import sdl2 as sdl
import skia
import zengl

from castella.frame.base import BaseFrame
from castella.models.geometry import Point, Size
from castella.models.events import (
    InputCharEvent,
    InputKeyEvent,
    KeyAction,
    KeyCode,
    MouseEvent,
    WheelEvent,
)

if TYPE_CHECKING:
    from castella.protocols.painter import BasePainter

if platform.system() == "Windows":
    import ctypes

    user32 = ctypes.windll.user32
    user32.SetProcessDPIAware()


def _rgba_masks() -> tuple[int, int, int, int]:
    """Get RGBA masks based on platform byte order."""
    if platform.system() == "Windows":
        return (0, 0, 0, 0)

    if sdl.SDL_BYTEORDER == sdl.SDL_BIG_ENDIAN:
        return (0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF)
    else:
        return (0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000)


class Frame(BaseFrame):
    """SDL2 window frame with Skia rendering."""

    UPDATE_EVENT_TYPE: Final = sdl.SDL_RegisterEvents(1)

    PIXEL_DEPTH = 32
    PIXEL_PITCH_FACTOR = 4

    def __init__(
        self, title: str = "castella", width: float = 500, height: float = 500
    ):
        super().__init__(title, width, height)

        sdl.SDL_Init(sdl.SDL_INIT_EVENTS)
        self._rgba_masks = _rgba_masks()
        window = sdl.SDL_CreateWindow(
            bytes(title, "utf8"),
            sdl.SDL_WINDOWPOS_CENTERED,
            sdl.SDL_WINDOWPOS_CENTERED,
            int(width),
            int(height),
            sdl.SDL_WINDOW_SHOWN
            | sdl.SDL_WINDOW_RESIZABLE
            | sdl.SDL_WINDOW_ALLOW_HIGHDPI,
        )

        self._window = window
        zengl.init(zengl.loader(headless=True))
        self._update_surface_and_painter()

    def _update_surface_and_painter(self) -> None:
        """Create/recreate the Skia surface for the current size."""
        from castella import skia_painter as painter

        info = skia.ImageInfo.MakeN32Premul(
            int(self._size.width), int(self._size.height)
        )
        surface = skia.Surface.MakeRenderTarget(
            skia.GrDirectContext.MakeGL(), skia.Budgeted.kNo, info
        )

        self._surface = surface
        self._painter = painter.Painter(self, self._surface)

    def _signal_main_thread(self) -> None:
        """Signal main thread via SDL custom event."""
        sdl_event = sdl.SDL_Event()
        sdl_event.type = Frame.UPDATE_EVENT_TYPE
        sdl.SDL_PushEvent(sdl_event)

    # ========== Abstract Method Implementations ==========

    def get_painter(self) -> "BasePainter":
        return self._painter

    def get_size(self) -> Size:
        return self._size

    def flush(self) -> None:
        skia_image = self._surface.makeImageSnapshot()
        skia_bytes = skia_image.tobytes()

        size = self._size
        width = int(size.width)
        height = int(size.height)
        sdl_surface = sdl.SDL_CreateRGBSurfaceFrom(
            skia_bytes,
            width,
            height,
            self.PIXEL_DEPTH,
            self.PIXEL_PITCH_FACTOR * width,
            *self._rgba_masks,
        )

        rect = sdl.SDL_Rect(0, 0, width, height)
        window_surface = sdl.SDL_GetWindowSurface(self._window)
        sdl.SDL_BlitSurface(sdl_surface, rect, window_surface, rect)
        sdl.SDL_UpdateWindowSurface(self._window)

    def clear(self) -> None:
        self._surface.getCanvas().clear(0)

    def run(self) -> None:
        self._ensure_main_thread()

        event = sdl.SDL_Event()
        while True:
            ok = sdl.SDL_WaitEvent(byref(event))
            if not ok:
                sdl.SDL_Quit()
                break

            match event.type:
                case sdl.SDL_QUIT:
                    sdl.SDL_Quit()
                    break
                case Frame.UPDATE_EVENT_TYPE:
                    self._process_pending_updates()
                case sdl.SDL_WINDOWEVENT:
                    if event.window.event == sdl.SDL_WINDOWEVENT_RESIZED:
                        width = event.window.data1
                        height = event.window.data2
                        self._on_resize(width, height)
                case sdl.SDL_MOUSEBUTTONDOWN:
                    self._callback_on_mouse_down(
                        MouseEvent(pos=Point(x=event.button.x, y=event.button.y))
                    )
                case sdl.SDL_MOUSEBUTTONUP:
                    self._callback_on_mouse_up(
                        MouseEvent(pos=Point(x=event.button.x, y=event.button.y))
                    )
                case sdl.SDL_MOUSEWHEEL:
                    x, y = c_int(0), c_int(0)
                    sdl.SDL_GetMouseState(byref(x), byref(y))
                    self._callback_on_mouse_wheel(
                        WheelEvent(
                            pos=Point(x=x.value, y=y.value),
                            x_offset=+event.wheel.x * 20,
                            y_offset=-event.wheel.y * 20,
                        )
                    )
                case sdl.SDL_MOUSEMOTION:
                    self._callback_on_cursor_pos(
                        MouseEvent(pos=Point(x=event.motion.x, y=event.motion.y))
                    )
                case sdl.SDL_KEYDOWN:
                    self._callback_on_input_key(
                        InputKeyEvent(
                            key=_convert_to_key_code(event.key.keysym.sym),
                            scancode=0,
                            action=KeyAction.PRESS,
                            mods=_get_key_mods(),
                        )
                    )
                case sdl.SDL_TEXTINPUT:
                    self._callback_on_input_char(
                        InputCharEvent(char=event.text.text.decode("utf-8"))
                    )

    def _on_resize(self, w: int, h: int) -> None:
        """Handle window resize."""
        self._size = Size(width=w, height=h)
        self._update_surface_and_painter()
        self._callback_on_redraw(self._painter, True)

    # ========== Clipboard ==========

    def get_clipboard_text(self) -> str:
        return sdl.SDL_GetClipboardText().decode("utf-8")

    def set_clipboard_text(self, text: str) -> None:
        sdl.SDL_SetClipboardText(text.encode("utf-8"))


# ========== Key Code Converter ==========


def _convert_to_key_code(keysym: int) -> KeyCode:
    match keysym:
        case sdl.SDLK_BACKSPACE:
            return KeyCode.BACKSPACE
        case sdl.SDLK_LEFT:
            return KeyCode.LEFT
        case sdl.SDLK_RIGHT:
            return KeyCode.RIGHT
        case sdl.SDLK_UP:
            return KeyCode.UP
        case sdl.SDLK_DOWN:
            return KeyCode.DOWN
        case sdl.SDLK_PAGEUP:
            return KeyCode.PAGE_UP
        case sdl.SDLK_PAGEDOWN:
            return KeyCode.PAGE_DOWN
        case sdl.SDLK_DELETE:
            return KeyCode.DELETE
        case sdl.SDLK_a:
            return KeyCode.A
        case sdl.SDLK_c:
            return KeyCode.C
        case sdl.SDLK_v:
            return KeyCode.V
        case sdl.SDLK_x:
            return KeyCode.X
        case _:
            return KeyCode.UNKNOWN


def _get_key_mods() -> int:
    """Get current keyboard modifier state in GLFW-compatible format."""
    key_mods = sdl.SDL_GetModState()
    mods = 0
    if key_mods & (sdl.KMOD_LSHIFT | sdl.KMOD_RSHIFT):
        mods |= 0x0001  # SHIFT
    if key_mods & (sdl.KMOD_LCTRL | sdl.KMOD_RCTRL):
        mods |= 0x0002  # CTRL
    if key_mods & (sdl.KMOD_LALT | sdl.KMOD_RALT):
        mods |= 0x0004  # ALT
    if key_mods & (sdl.KMOD_LGUI | sdl.KMOD_RGUI):
        mods |= 0x0008  # SUPER/CMD
    return mods
