"""SDL2 window, event loop, rendering and input handling."""

from __future__ import annotations

import ctypes
import logging
import os
import queue
import time
from enum import Enum, auto
from typing import Any

import sdl2

try:
    import sdl2.sdlttf as ttf
except ImportError:
    ttf = None  # type: ignore[assignment]

try:
    import sdl2.sdlimage as sdl_img
except ImportError:
    sdl_img = None  # type: ignore[assignment]

from r36s_dlna_browser.dlna.browser_state import BrowserState
from r36s_dlna_browser.dlna.models import ItemType, MediaItem
from r36s_dlna_browser.player.backend import PlayerBackend
from r36s_dlna_browser.platform.controls import Action, map_key
from r36s_dlna_browser.ui import theme
from r36s_dlna_browser.ui import screens

log = logging.getLogger(__name__)


class Screen(Enum):
    SERVERS = auto()
    BROWSE = auto()
    PLAYBACK = auto()
    ERROR = auto()


class SDLApp:
    """Owns the SDL2 window and drives the main loop."""

    def __init__(
        self,
        ui_queue: queue.Queue[Any],
        cmd_queue: queue.Queue[Any],
        browser_state: BrowserState,
        player: PlayerBackend,
    ) -> None:
        self._ui_q = ui_queue
        self._cmd_q = cmd_queue
        self._state = browser_state
        self._player = player
        self._screen = Screen.SERVERS
        self._running = False
        self._window = None
        self._renderer = None
        self._font = None
        self._playback_font = None
        self._icons: dict = {}
        self._layout = theme.DEFAULT_LAYOUT
        self._playback_clear_once = False
        self._needs_redraw = True
        self._last_playback_draw_at = 0.0
        self._last_playback_snapshot: tuple[Any, ...] | None = None
        self._last_playback_interaction_at = 0.0

    # ── lifecycle ────────────────────────────────────────────────

    def run(self) -> None:
        self._init_sdl()
        self._running = True
        try:
            while self._running:
                self._poll_events()
                self._process_ui_queue()
                self._refresh_playback_hud_visibility()
                if self._should_draw():
                    self._draw()
                sdl2.SDL_Delay(theme.FRAME_DELAY_MS)
        finally:
            self._cleanup()

    def _init_sdl(self) -> None:
        sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO | sdl2.SDL_INIT_GAMECONTROLLER)
        if ttf:
            ttf.TTF_Init()

        window_w, window_h = self._preferred_window_size()
        self._layout = theme.get_layout(window_w, window_h)

        self._window = sdl2.SDL_CreateWindow(
            b"R36S DLNA Browser",
            sdl2.SDL_WINDOWPOS_CENTERED,
            sdl2.SDL_WINDOWPOS_CENTERED,
            window_w,
            window_h,
            sdl2.SDL_WINDOW_SHOWN,
        )
        self._renderer = sdl2.SDL_CreateRenderer(
            self._window, -1,
            sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC,
        )

        # Open first available gamecontroller
        for i in range(sdl2.SDL_NumJoysticks()):
            if sdl2.SDL_IsGameController(i):
                sdl2.SDL_GameControllerOpen(i)
                break

        self._reload_fonts()
        if not self._font:
            log.error("Could not load any font – UI text will be missing.")
        self._icons = self._load_icons()
        self._player.attach_window(self._window)
        self._sync_player_viewport()

    def _load_icons(self) -> dict:
        icons: dict = {}
        if not sdl_img:
            return icons
        sdl_img.IMG_Init(sdl_img.IMG_INIT_PNG)
        mapping = [
            ("folder", ItemType.CONTAINER),
            ("audio",  ItemType.AUDIO),
            ("video",  ItemType.VIDEO),
            ("image",  ItemType.IMAGE),
            ("playing", "playing"),
            ("server",  "server"),
            ("hud-play", "hud-play"),
            ("hud-pause", "hud-pause"),
            ("hud-stop", "hud-stop"),
            ("hud-seek", "hud-seek"),
            ("hud-volume", "hud-volume"),
            ("hud-display", "hud-display"),
        ]
        for fname, key in mapping:
            path = theme.ICONS_DIR / f"{fname}.png"
            if path.exists():
                surf = sdl_img.IMG_Load(str(path).encode())
                if surf:
                    tex = sdl2.SDL_CreateTextureFromSurface(self._renderer, surf)
                    sdl2.SDL_FreeSurface(surf)
                    if tex:
                        icons[key] = tex
                        log.debug("Loaded icon: %s", fname)
        log.info("Loaded %d icon(s).", len(icons))
        return icons

    def _load_font(self, size: int):
        if not ttf:
            log.warning("SDL2_ttf not available – text rendering disabled.")
            return None
        for path in theme.FONT_SEARCH_PATHS:
            expanded = os.path.expanduser(path)
            if os.path.isfile(expanded):
                f = ttf.TTF_OpenFont(expanded.encode(), size)
                if f:
                    log.info("Loaded font: %s", expanded)
                    return f
        return None

    def _reload_fonts(self) -> None:
        if self._font and ttf:
            ttf.TTF_CloseFont(self._font)
        if self._playback_font and ttf:
            ttf.TTF_CloseFont(self._playback_font)
        self._font = self._load_font(self._layout.font_size)
        self._playback_font = self._load_font(self._layout.playback_font_size)

    def _preferred_window_size(self) -> tuple[int, int]:
        mode = sdl2.SDL_DisplayMode()
        if sdl2.SDL_GetCurrentDisplayMode(0, ctypes.byref(mode)) == 0:
            width = int(mode.w)
            height = int(mode.h)
            if 320 <= width <= 960 and 320 <= height <= 960:
                return width, height
        return theme.SCREEN_W, theme.SCREEN_H

    def _update_layout(self, width: int, height: int) -> None:
        prev_font_size = self._layout.font_size
        prev_playback_font_size = self._layout.playback_font_size
        self._layout = theme.get_layout(width, height)
        if (
            self._layout.font_size != prev_font_size
            or self._layout.playback_font_size != prev_playback_font_size
        ):
            self._reload_fonts()

    def _cleanup(self) -> None:
        for tex in self._icons.values():
            sdl2.SDL_DestroyTexture(tex)
        self._icons = {}
        if sdl_img:
            sdl_img.IMG_Quit()
        if self._font and ttf:
            ttf.TTF_CloseFont(self._font)
        if self._playback_font and ttf:
            ttf.TTF_CloseFont(self._playback_font)
        if self._renderer:
            sdl2.SDL_DestroyRenderer(self._renderer)
        if self._window:
            sdl2.SDL_DestroyWindow(self._window)
        if ttf:
            ttf.TTF_Quit()
        sdl2.SDL_Quit()

    # ── events ───────────────────────────────────────────────────

    def _poll_events(self) -> None:
        event = sdl2.SDL_Event()
        while sdl2.SDL_PollEvent(ctypes.byref(event)):
            if event.type == sdl2.SDL_QUIT:
                self._running = False
                return
            if event.type == sdl2.SDL_WINDOWEVENT and event.window.event in (
                sdl2.SDL_WINDOWEVENT_MOVED,
                sdl2.SDL_WINDOWEVENT_SIZE_CHANGED,
                sdl2.SDL_WINDOWEVENT_RESIZED,
            ):
                if self._window and event.window.windowID == sdl2.SDL_GetWindowID(self._window):
                    self._sync_player_viewport()
                    self._mark_dirty()
            action = map_key(event)
            if action is not None:
                self._handle_action(action)

    def _process_ui_queue(self) -> None:
        while True:
            try:
                msg = self._ui_q.get_nowait()
            except queue.Empty:
                break
            self._handle_ui_message(msg)

    def _handle_ui_message(self, msg: tuple) -> None:
        kind = msg[0]
        if kind == "servers_updated":
            self._state.loading = False
            self._screen = Screen.SERVERS
        elif kind == "items_updated":
            self._state.loading = False
            self._screen = Screen.BROWSE
        elif kind == "playback_started":
            self._state.playback_title = msg[1] if len(msg) > 1 else ""
            self._state.playback_paused = False
            self._show_playback_hud(force=True)
            self._playback_clear_once = True
            self._screen = Screen.PLAYBACK
        elif kind == "playback_paused":
            self._state.playback_paused = True
            self._show_playback_hud(force=True)
        elif kind == "playback_resumed":
            self._state.playback_paused = False
            self._show_playback_hud(force=True)
        elif kind == "playback_stopped":
            self._state.playback_paused = False
            self._screen = Screen.BROWSE if self._state.in_browse_mode else Screen.SERVERS
        elif kind == "error":
            self._state.error = msg[1] if len(msg) > 1 else "Unknown error"
            self._state.loading = False
            self._screen = Screen.ERROR
        self._mark_dirty()

    # ── input dispatch ───────────────────────────────────────────

    def _handle_action(self, action: Action) -> None:
        if action == Action.QUIT:
            self._running = False
            return

        if self._screen == Screen.ERROR:
            if action in (Action.CONFIRM, Action.BACK):
                self._state.error = ""
                self._screen = Screen.BROWSE if self._state.in_browse_mode else Screen.SERVERS
                self._mark_dirty()
            return

        if self._screen == Screen.PLAYBACK:
            if action == Action.BACK:
                self._cmd_q.put(("stop",))
            elif action == Action.CONFIRM:
                self._cmd_q.put(("toggle_pause",))
            elif action == Action.UP:
                self._cmd_q.put(("volume", 5))
            elif action == Action.DOWN:
                self._cmd_q.put(("volume", -5))
            elif action == Action.PAGE_UP:
                self._cmd_q.put(("seek", -10))
            elif action == Action.PAGE_DOWN:
                self._cmd_q.put(("seek", 10))
            elif action == Action.HUD_MODE:
                self._cycle_playback_hud_mode()
            self._mark_dirty()
            self._show_playback_hud(force=action != Action.HUD_MODE)
            return

        if self._screen == Screen.SERVERS:
            self._handle_server_action(action)
        elif self._screen == Screen.BROWSE:
            self._handle_browse_action(action)

    def _handle_server_action(self, action: Action) -> None:
        n = len(self._state.servers)
        if action == Action.UP and n:
            self._state.server_cursor = max(0, self._state.server_cursor - 1)
            self._adjust_server_scroll()
            self._mark_dirty()
        elif action == Action.DOWN and n:
            self._state.server_cursor = min(n - 1, self._state.server_cursor + 1)
            self._adjust_server_scroll()
            self._mark_dirty()
        elif action == Action.CONFIRM:
            srv = self._state.selected_server
            if srv:
                obj_id = self._state.enter_server(srv)
                self._state.loading = True
                self._cmd_q.put(("browse", srv.location, obj_id))
            else:
                # No server selected – refresh
                self._state.loading = True
                self._cmd_q.put(("discover",))
            self._mark_dirty()
        elif action == Action.BACK:
            # Refresh server list
            self._state.loading = True
            self._cmd_q.put(("discover",))
            self._mark_dirty()

    def _handle_browse_action(self, action: Action) -> None:
        items = self._state.current_items
        n = len(items)
        if action == Action.UP and n:
            self._state.cursor -= 1
            self._adjust_browse_scroll()
            self._mark_dirty()
        elif action == Action.DOWN and n:
            self._state.cursor += 1
            self._adjust_browse_scroll()
            self._mark_dirty()
        elif action == Action.PAGE_UP and n:
            self._state.cursor = max(0, self._state.cursor - theme.PAGE_SIZE)
            self._adjust_browse_scroll()
            self._mark_dirty()
        elif action == Action.PAGE_DOWN and n:
            self._state.cursor = min(n - 1, self._state.cursor + theme.PAGE_SIZE)
            self._adjust_browse_scroll()
            self._mark_dirty()
        elif action == Action.CONFIRM:
            item = self._state.selected_item()
            if item:
                if item.is_container:
                    self._state.loading = True
                    lv = self._state.current_level
                    loc = lv.server_location if lv else ""
                    self._cmd_q.put(("browse", loc, item.object_id))
                elif item.resource_url:
                    self._cmd_q.put(("play", item.resource_url, item.title))
                self._mark_dirty()
        elif action == Action.BACK:
            if not self._state.go_back():
                self._screen = Screen.SERVERS
            else:
                self._screen = Screen.BROWSE
            self._mark_dirty()

    def _adjust_server_scroll(self) -> None:
        vis = self._layout.visible_items
        cur = self._state.server_cursor
        off = self._state.server_scroll_offset
        if cur < off:
            self._state.server_scroll_offset = cur
        elif cur >= off + vis:
            self._state.server_scroll_offset = cur - vis + 1

    def _adjust_browse_scroll(self) -> None:
        vis = self._layout.visible_items
        cur = self._state.cursor
        off = self._state.scroll_offset
        if cur < off:
            self._state.scroll_offset = cur
        elif cur >= off + vis:
            self._state.scroll_offset = cur - vis + 1

    # ── drawing ──────────────────────────────────────────────────

    def _draw(self) -> None:
        r = self._renderer
        if self._screen == Screen.PLAYBACK:
            sdl2.SDL_SetRenderDrawColor(r, *theme.PLAYBACK_BG_COLOR)
        else:
            sdl2.SDL_SetRenderDrawColor(r, *theme.BG_COLOR)
        sdl2.SDL_RenderClear(r)
        self._playback_clear_once = False

        f = self._font
        if self._screen == Screen.SERVERS:
            screens.draw_server_list(r, f, self._state, self._layout, self._icons)
        elif self._screen == Screen.BROWSE:
            screens.draw_browse_list(r, f, self._state, self._layout, self._icons)
        elif self._screen == Screen.PLAYBACK:
            self._player.render(r)
            screens.draw_playback(r, self._playback_font or f, self._state, self._layout, self._icons)
        elif self._screen == Screen.ERROR:
            screens.draw_error(r, f, self._state, self._layout)

        sdl2.SDL_RenderPresent(r)
        self._needs_redraw = False
        if self._screen == Screen.PLAYBACK:
            self._last_playback_draw_at = time.monotonic()
            self._last_playback_snapshot = self._playback_snapshot()

    def _mark_dirty(self) -> None:
        self._needs_redraw = True

    def _playback_snapshot(self) -> tuple[Any, ...]:
        return (
            self._state.playback_title,
            self._state.playback_paused,
            round(self._state.playback_position, 1),
            round(self._state.playback_duration, 1),
            self._state.playback_volume,
            self._state.playback_buffer_percent,
            self._state.playback_resolution,
            self._state.playback_backend,
            self._state.playback_hud_visible,
            self._state.playback_hud_mode,
        )

    def _refresh_playback_hud_visibility(self) -> None:
        if self._screen != Screen.PLAYBACK:
            return
        if self._state.playback_hud_mode == theme.PLAYBACK_HUD_HIDDEN:
            return
        if self._state.playback_hud_mode == theme.PLAYBACK_HUD_PINNED or self._state.playback_paused:
            if not self._state.playback_hud_visible:
                self._state.playback_hud_visible = True
                self._mark_dirty()
            return
        visible = (time.monotonic() - self._last_playback_interaction_at) < (theme.PLAYBACK_HUD_AUTOHIDE_MS / 1000.0)
        if visible != self._state.playback_hud_visible:
            self._state.playback_hud_visible = visible
            self._mark_dirty()

    def _show_playback_hud(self, force: bool = False) -> None:
        self._last_playback_interaction_at = time.monotonic()
        if force or self._state.playback_hud_mode != theme.PLAYBACK_HUD_HIDDEN:
            if not self._state.playback_hud_visible:
                self._state.playback_hud_visible = True
                self._mark_dirty()

    def _cycle_playback_hud_mode(self) -> None:
        next_mode = {
            theme.PLAYBACK_HUD_AUTO: theme.PLAYBACK_HUD_PINNED,
            theme.PLAYBACK_HUD_PINNED: theme.PLAYBACK_HUD_HIDDEN,
            theme.PLAYBACK_HUD_HIDDEN: theme.PLAYBACK_HUD_AUTO,
        }
        self._state.playback_hud_mode = next_mode[self._state.playback_hud_mode]
        self._state.playback_hud_visible = self._state.playback_hud_mode != theme.PLAYBACK_HUD_HIDDEN
        self._last_playback_interaction_at = time.monotonic()

    def _should_draw(self) -> bool:
        if self._needs_redraw:
            return True
        if self._screen != Screen.PLAYBACK:
            return False
        if self._player.has_new_frame():
            return True

        now = time.monotonic()
        if now - self._last_playback_draw_at < (theme.PLAYBACK_HUD_REFRESH_MS / 1000.0):
            return False
        return self._playback_snapshot() != self._last_playback_snapshot

    def _sync_player_viewport(self) -> None:
        width = ctypes.c_int()
        height = ctypes.c_int()
        if self._window:
            sdl2.SDL_GetWindowSize(self._window, ctypes.byref(width), ctypes.byref(height))
        if width.value <= 0 or height.value <= 0:
            width.value = theme.SCREEN_W
            height.value = theme.SCREEN_H

        self._update_layout(width.value, height.value)

        self._player.set_viewport(
            width.value,
            height.value,
            self._layout.playback_hud_top,
            height.value - self._layout.playback_video_bottom,
            self._layout.playback_video_left,
            max(0, width.value - self._layout.playback_video_right),
        )
