"""
Pygame display backend - renders to a native application window.
"""

import time

from engine.display.renderer import parse_ansi


class PygameDisplay:
    """Pygame display backend - renders to native window.

    Supports reuse mode - when reuse=True, skips SDL initialization
    and reuses the existing pygame window from a previous instance.
    """

    width: int = 80
    window_width: int = 800
    window_height: int = 600

    def __init__(
        self,
        cell_width: int = 10,
        cell_height: int = 18,
        window_width: int = 800,
        window_height: int = 600,
        target_fps: float = 30.0,
    ):
        self.width = 80
        self.height = 24
        self.cell_width = cell_width
        self.cell_height = cell_height
        self.window_width = window_width
        self.window_height = window_height
        self.target_fps = target_fps
        self._initialized = False
        self._pygame = None
        self._screen = None
        self._font = None
        self._resized = False
        self._quit_requested = False
        self._last_frame_time = 0.0
        self._frame_period = 1.0 / target_fps if target_fps > 0 else 0
        self._glyph_cache = {}

    def _get_font_path(self) -> str | None:
        """Get font path for rendering."""
        import os
        import sys
        from pathlib import Path

        env_font = os.environ.get("MAINLINE_PYGAME_FONT")
        if env_font and os.path.exists(env_font):
            return env_font

        def search_dir(base_path: str) -> str | None:
            if not os.path.exists(base_path):
                return None
            if os.path.isfile(base_path):
                return base_path
            for font_file in Path(base_path).rglob("*"):
                if font_file.suffix.lower() in (".ttf", ".otf", ".ttc"):
                    name = font_file.stem.lower()
                    if "geist" in name and ("nerd" in name or "mono" in name):
                        return str(font_file)
            return None

        search_dirs = []
        if sys.platform == "darwin":
            search_dirs.append(os.path.expanduser("~/Library/Fonts/"))
        elif sys.platform == "win32":
            search_dirs.append(
                os.path.expanduser("~\\AppData\\Local\\Microsoft\\Windows\\Fonts\\")
            )
        else:
            search_dirs.extend(
                [
                    os.path.expanduser("~/.local/share/fonts/"),
                    os.path.expanduser("~/.fonts/"),
                    "/usr/share/fonts/",
                ]
            )

        for search_dir_path in search_dirs:
            found = search_dir(search_dir_path)
            if found:
                return found

        return None

    def init(self, width: int, height: int, reuse: bool = False) -> None:
        """Initialize display with dimensions.

        Args:
            width: Terminal width in characters
            height: Terminal height in rows
            reuse: If True, attach to existing pygame window instead of creating new
        """
        self.width = width
        self.height = height

        try:
            import pygame
        except ImportError:
            return

        if reuse and PygameDisplay._pygame_initialized:
            self._pygame = pygame
            self._initialized = True
            return

        pygame.init()
        pygame.display.set_caption("Mainline")

        self._screen = pygame.display.set_mode(
            (self.window_width, self.window_height),
            pygame.RESIZABLE,
        )
        self._pygame = pygame
        PygameDisplay._pygame_initialized = True

        # Calculate character dimensions from actual window size
        self.width = max(1, self.window_width // self.cell_width)
        self.height = max(1, self.window_height // self.cell_height)

        font_path = self._get_font_path()
        if font_path:
            try:
                self._font = pygame.font.Font(font_path, self.cell_height - 2)
            except Exception:
                self._font = pygame.font.SysFont("monospace", self.cell_height - 2)
        else:
            self._font = pygame.font.SysFont("monospace", self.cell_height - 2)

        # Check if font supports box-drawing characters; if not, try to find one
        self._use_fallback_border = False
        if self._font:
            try:
                # Test rendering some key box-drawing characters
                test_chars = ["┌", "─", "┐", "│", "└", "┘"]
                for ch in test_chars:
                    surf = self._font.render(ch, True, (255, 255, 255))
                    # If surface is empty (width=0 or all black), font lacks glyph
                    if surf.get_width() == 0:
                        raise ValueError("Missing glyph")
            except Exception:
                # Font doesn't support box-drawing, will use line drawing fallback
                self._use_fallback_border = True

        self._initialized = True

    def show(self, buffer: list[str], border: bool = False) -> None:
        if not self._initialized or not self._pygame:
            return

        t0 = time.perf_counter()

        for event in self._pygame.event.get():
            if event.type == self._pygame.QUIT:
                self._quit_requested = True
            elif event.type == self._pygame.KEYDOWN:
                if event.key in (self._pygame.K_ESCAPE, self._pygame.K_c):
                    if event.key == self._pygame.K_c and not (
                        event.mod & self._pygame.KMOD_LCTRL
                        or event.mod & self._pygame.KMOD_RCTRL
                    ):
                        continue
                    self._quit_requested = True
            elif event.type == self._pygame.VIDEORESIZE:
                self.window_width = event.w
                self.window_height = event.h
                self.width = max(1, self.window_width // self.cell_width)
                self.height = max(1, self.window_height // self.cell_height)
                self._resized = True

        # FPS limiting - skip frame if we're going too fast
        if self._frame_period > 0:
            now = time.perf_counter()
            elapsed = now - self._last_frame_time
            if elapsed < self._frame_period:
                return  # Skip this frame
            self._last_frame_time = now

        # Get metrics for border display
        fps = 0.0
        frame_time = 0.0
        from engine.display import get_monitor

        monitor = get_monitor()
        if monitor:
            stats = monitor.get_stats()
            avg_ms = stats.get("pipeline", {}).get("avg_ms", 0) if stats else 0
            frame_count = stats.get("frame_count", 0) if stats else 0
            if avg_ms and frame_count > 0:
                fps = 1000.0 / avg_ms
                frame_time = avg_ms

        self._screen.fill((0, 0, 0))

        # If border requested but font lacks box-drawing glyphs, use graphical fallback
        if border and self._use_fallback_border:
            self._draw_fallback_border(fps, frame_time)
            # Adjust content area to fit inside border
            content_offset_x = self.cell_width
            content_offset_y = self.cell_height
            self.window_width - 2 * self.cell_width
            self.window_height - 2 * self.cell_height
        else:
            # Normal rendering (with or without text border)
            content_offset_x = 0
            content_offset_y = 0

            if border:
                from engine.display import render_border

                buffer = render_border(buffer, self.width, self.height, fps, frame_time)

        blit_list = []

        for row_idx, line in enumerate(buffer[: self.height]):
            if row_idx >= self.height:
                break

            tokens = parse_ansi(line)
            x_pos = content_offset_x

            for text, fg, bg, _bold in tokens:
                if not text:
                    continue

                # Use None as key for no background
                bg_key = bg if bg != (0, 0, 0) else None
                cache_key = (text, fg, bg_key)

                if cache_key not in self._glyph_cache:
                    # Render and cache
                    if bg_key is not None:
                        self._glyph_cache[cache_key] = self._font.render(
                            text, True, fg, bg_key
                        )
                    else:
                        self._glyph_cache[cache_key] = self._font.render(text, True, fg)

                surface = self._glyph_cache[cache_key]
                blit_list.append(
                    (surface, (x_pos, content_offset_y + row_idx * self.cell_height))
                )
                x_pos += self._font.size(text)[0]

        self._screen.blits(blit_list)

        # Draw fallback border using graphics if needed
        if border and self._use_fallback_border:
            self._draw_fallback_border(fps, frame_time)

        self._pygame.display.flip()

        elapsed_ms = (time.perf_counter() - t0) * 1000

        if monitor:
            chars_in = sum(len(line) for line in buffer)
            monitor.record_effect("pygame_display", elapsed_ms, chars_in, chars_in)

    def _draw_fallback_border(self, fps: float, frame_time: float) -> None:
        """Draw border using pygame graphics primitives instead of text."""
        if not self._screen or not self._pygame:
            return

        # Colors
        border_color = (0, 255, 0)  # Green (like terminal border)
        text_color = (255, 255, 255)

        # Calculate dimensions
        x1 = 0
        y1 = 0
        x2 = self.window_width - 1
        y2 = self.window_height - 1

        # Draw outer rectangle
        self._pygame.draw.rect(
            self._screen, border_color, (x1, y1, x2 - x1 + 1, y2 - y1 + 1), 1
        )

        # Draw top border with FPS
        if fps > 0:
            fps_text = f" FPS:{fps:.0f}"
        else:
            fps_text = ""
        # We need to render this text with a fallback font that has basic ASCII
        # Use system font which should have these characters
        try:
            font = self._font  # May not have box chars but should have alphanumeric
            text_surf = font.render(fps_text, True, text_color, (0, 0, 0))
            text_rect = text_surf.get_rect()
            # Position on top border, right-aligned
            text_x = x2 - text_rect.width - 5
            text_y = y1 + 2
            self._screen.blit(text_surf, (text_x, text_y))
        except Exception:
            pass

        # Draw bottom border with frame time
        if frame_time > 0:
            ft_text = f" {frame_time:.1f}ms"
            try:
                ft_surf = self._font.render(ft_text, True, text_color, (0, 0, 0))
                ft_rect = ft_surf.get_rect()
                ft_x = x2 - ft_rect.width - 5
                ft_y = y2 - ft_rect.height - 2
                self._screen.blit(ft_surf, (ft_x, ft_y))
            except Exception:
                pass

    def clear(self) -> None:
        if self._screen and self._pygame:
            self._screen.fill((0, 0, 0))
            self._pygame.display.flip()

    def get_dimensions(self) -> tuple[int, int]:
        """Get current terminal dimensions based on window size.

        Returns:
            (width, height) in character cells
        """
        # Query actual window size and recalculate character cells
        if self._screen and self._pygame:
            try:
                w, h = self._screen.get_size()
                if w != self.window_width or h != self.window_height:
                    self.window_width = w
                    self.window_height = h
                    self.width = max(1, w // self.cell_width)
                    self.height = max(1, h // self.cell_height)
            except Exception:
                pass
        return self.width, self.height

    def cleanup(self, quit_pygame: bool = True) -> None:
        """Cleanup display resources.

        Args:
            quit_pygame: If True, quit pygame entirely. Set to False when
                        reusing the display to avoid closing shared window.
        """
        if quit_pygame and self._pygame:
            self._pygame.quit()
            PygameDisplay._pygame_initialized = False

    @classmethod
    def reset_state(cls) -> None:
        """Reset pygame state - useful for testing."""
        cls._pygame_initialized = False

    def is_quit_requested(self) -> bool:
        """Check if user requested quit (Ctrl+C, Ctrl+Q, or Escape).

        Returns True if the user pressed Ctrl+C, Ctrl+Q, or Escape.
        The main loop should check this and raise KeyboardInterrupt.
        """
        return self._quit_requested

    def clear_quit_request(self) -> bool:
        """Clear the quit request flag after handling.

        Returns the previous quit request state.
        """
        was_requested = self._quit_requested
        self._quit_requested = False
        return was_requested
