"""
credits.py — Credits screen for Powernet.

Displays scrolling, markdown-like formatted credits content with:
- Auto-scroll from bottom to top (~30 px/s)
- Manual mouse-wheel scrolling
- Clickable hyperlinks (opens in browser)
- Bold / highlighted inline text
- Horizontal dividers and vertical gaps
- Top fade-out gradient overlay
- Header panel matching world_manager style

Returns from update():
  "back"  → go back to main menu / caller
  None    → no transition
"""

from __future__ import annotations

import re
import webbrowser
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

import pygame

from helpers.gui import GUIHandler, theme
from helpers.i18n import t
from helpers.icons import get_icon, ICO_BACK

# ---------------------------------------------------------------------------
# Credits content (markdown-like source string)
# ---------------------------------------------------------------------------

def _build_credits_source() -> str:
    year = datetime.now().year
    return f"""\
# POWERNET

## Created by

Arne Kock · André Weiß

---

## Icons

Lucid icon set created by **Midhil [Leo Red]**

---

## Publisher

**GMRZE** · [gmrze.net](https://gmrze.net)

Contact: [powernet@gmrze.net](mailto:powernet@gmrze.net)

---

## Music

**Abstraction Music Loop Bundle**

Licensed under **Public Domain (CC-0)**

[abstractionmusic.com](https://abstractionmusic.com) · [abstractionmusic.bandcamp.com](https://abstractionmusic.bandcamp.com) · [patreon.com/ben_burnes](https://patreon.com/ben_burnes)

Although permitted within the license terms, Abstraction and Tallbeard Studios
do not endorse the use of these assets in any projects relating to NFTs,
AI/Machine Learning, or direct resale of unmodified assets.

---

## UI Icons

**IconsShadow-32** · CC BY 4.0

[crusenho.itch.io](https://crusenho.itch.io)

---

## Font

**Ithaca** by GGBotNet

[ggbot.net](https://ggbot.net) · SIL Open Font License 1.1

---

© {year} GMRZE — All rights reserved.\
"""

# ---------------------------------------------------------------------------
# Auto-scroll speed and layout constants
# ---------------------------------------------------------------------------

_SCROLL_SPEED   = 30.0   # px per second (auto-scroll upward)
_CONTENT_WIDTH  = 700    # max width of the content column
_GAP_HEIGHT     = 20     # px for empty lines
_FADE_HEIGHT    = 80     # px — how tall the top fade gradient is
_HEADER_H       = 64     # px — height of the top chrome panel
_CONTENT_PAD_X  = 40     # horizontal padding inside content area
_TRAIL_SPACE    = 120    # px of blank space appended below the last segment

# Link colour / hover colour
_LINK_COLOR     = (80,  160, 240)
_LINK_HOV_COLOR = (140, 200, 255)

# Highlighted "bold" text colour (brighter than TEXT)
_BOLD_COLOR     = (235, 245, 255)

# ---------------------------------------------------------------------------
# Inline parsing helpers
# ---------------------------------------------------------------------------


@dataclass
class _Span:
    """A single inline text span (plain, bold, or link)."""
    text:  str
    bold:  bool  = False
    url:   Optional[str] = None   # if not None → clickable link


def _parse_inline(line: str) -> list[_Span]:
    """
    Break a text line into a list of _Span objects by detecting
    **bold** and [label](url) patterns.
    """
    # Tokenise by finding all special spans and the plain text between them.
    spans: list[_Span] = []

    # Combine both patterns into one alternating regex so we handle them in
    # left-to-right order without nested conflicts.
    combined = re.compile(r"(\*\*(.+?)\*\*|\[([^\]]+)\]\(([^)]+)\))")
    cursor = 0
    for m in combined.finditer(line):
        if m.start() > cursor:
            spans.append(_Span(line[cursor:m.start()]))
        full = m.group(0)
        if full.startswith("**"):
            spans.append(_Span(m.group(2), bold=True))
        else:
            # link: [label](url)
            spans.append(_Span(m.group(3), url=m.group(4)))
        cursor = m.end()
    if cursor < len(line):
        spans.append(_Span(line[cursor:]))
    return spans


# ---------------------------------------------------------------------------
# Rendered segment
# ---------------------------------------------------------------------------


@dataclass
class _Segment:
    """A pre-rendered line/block ready to blit."""
    surf: pygame.Surface
    y:    int = 0                         # absolute y in the virtual canvas
    url:  Optional[str] = None            # if set → this segment is a link
    # rect within the virtual canvas used for click / hover detection
    link_rect: Optional[pygame.Rect] = None


# ---------------------------------------------------------------------------
# Credits parser & renderer
# ---------------------------------------------------------------------------


def _build_segments(content_width: int) -> list[_Segment]:
    """
    Parse _CREDITS_SOURCE and produce a flat list of _Segment objects.
    Each segment carries a surface and an optional url for click handling.
    """
    segments: list[_Segment] = []
    y_cursor = 0

    def _add(surf: pygame.Surface, url: Optional[str] = None) -> None:
        nonlocal y_cursor
        seg = _Segment(surf=surf, y=y_cursor, url=url)
        if url is not None:
            seg.link_rect = pygame.Rect(0, y_cursor, surf.get_width(), surf.get_height())
        segments.append(seg)
        y_cursor += surf.get_height() + 6   # 6 px inter-line gap

    for raw_line in _build_credits_source().splitlines():
        line = raw_line.rstrip()

        # ---- h1: # Text ----
        if line.startswith("# ") and not line.startswith("## "):
            text = line[2:].strip()
            surf = theme.font(40).render(text, True, theme.ACCENT)
            centered = pygame.Surface((content_width, surf.get_height()), pygame.SRCALPHA)
            centered.blit(surf, ((content_width - surf.get_width()) // 2, 0))
            y_cursor += 10
            _add(centered)
            y_cursor += 10
            continue

        # ---- h2: ## Text ----
        if line.startswith("## "):
            text = line[3:].strip()
            surf = theme.font(26).render(text, True, theme.TEXT)
            centered = pygame.Surface((content_width, surf.get_height()), pygame.SRCALPHA)
            centered.blit(surf, ((content_width - surf.get_width()) // 2, 0))
            y_cursor += 6
            _add(centered)
            y_cursor += 4
            continue

        # ---- horizontal divider: --- ----
        if line.strip() == "---":
            div_h = 2
            div_surf = pygame.Surface((content_width, div_h + 8), pygame.SRCALPHA)
            pygame.draw.line(
                div_surf, (*theme.BORDER, 160),
                (0, 4), (content_width, 4), div_h,
            )
            y_cursor += 6
            _add(div_surf)
            y_cursor += 6
            continue

        # ---- empty line → gap ----
        if line.strip() == "":
            gap = pygame.Surface((content_width, _GAP_HEIGHT), pygame.SRCALPHA)
            segments.append(_Segment(surf=gap, y=y_cursor))
            y_cursor += _GAP_HEIGHT
            continue

        # ---- text line (may contain **bold** and [link](url) spans) ----
        spans = _parse_inline(line)
        _render_text_line(spans, content_width, segments, y_cursor)
        # _render_text_line updates y_cursor via its own logic; we need the
        # height of the tallest span to advance y_cursor.
        if spans:
            line_height = theme.font(17).size("Ay")[1]
            y_cursor += line_height + 6

    return segments


def _render_text_line(
    spans: list[_Span],
    content_width: int,
    segments: list[_Segment],
    y_cursor: int,
) -> None:
    """
    Render one text line of mixed spans (plain / bold / link) into segments.
    Wraps at content_width. Each visual line becomes one _Segment.
    Link spans get their url stored so they're clickable.
    """
    font_plain = theme.font(17)
    font_bold  = theme.font(17)   # same font; bold is colour-differentiated

    # We build a list of (rendered_surf, url) tuples and paste them together.
    parts: list[tuple[pygame.Surface, Optional[str]]] = []
    total_w = 0

    for span in spans:
        if not span.text:
            continue
        if span.url is not None:
            color = _LINK_COLOR
        elif span.bold:
            color = _BOLD_COLOR
        else:
            color = theme.TEXT_DIM

        font = font_bold if span.bold else font_plain
        surf = font.render(span.text, True, color)

        if span.url:
            # Draw underline
            underline_surf = pygame.Surface(
                (surf.get_width(), surf.get_height() + 2), pygame.SRCALPHA
            )
            underline_surf.blit(surf, (0, 0))
            pygame.draw.line(
                underline_surf, (*_LINK_COLOR, 180),
                (0, surf.get_height() - 1),
                (surf.get_width(), surf.get_height() - 1),
                1,
            )
            surf = underline_surf

        parts.append((surf, span.url))
        total_w += surf.get_width()

    if not parts:
        return

    # Compose all spans into one wide surface, track per-span x offsets for
    # link hit-testing.
    max_h = max(s.get_height() for s, _ in parts)
    composed_w = total_w
    composed = pygame.Surface((max(composed_w, 1), max_h), pygame.SRCALPHA)

    x_off = 0
    link_parts: list[tuple[int, int, str]] = []   # (x, w, url)
    for surf, url in parts:
        composed.blit(surf, (x_off, max_h - surf.get_height()))
        if url:
            link_parts.append((x_off, surf.get_width(), url))
        x_off += surf.get_width()

    # Center horizontally
    blit_x = (content_width - composed_w) // 2
    blit_x = max(0, blit_x)

    # One segment per link span so each has its own url / rect.
    # Non-link text and link text live on the same composed surface but we
    # need separate segments for link hit areas. Strategy: emit the whole
    # composed surface as a no-url segment first, then emit invisible
    # hit-rect segments for each link.

    base_seg = _Segment(
        surf=composed,
        y=y_cursor,
        url=None,
    )
    # Store blit_x in surf for later drawing (we use a wider canvas trick)
    canvas = pygame.Surface((content_width, max_h), pygame.SRCALPHA)
    canvas.blit(composed, (blit_x, 0))
    base_seg.surf = canvas
    segments.append(base_seg)

    # Emit link hit-rect segments (zero-height surf, just for click detection)
    for lx, lw, url in link_parts:
        hit_surf = pygame.Surface((lw, max_h), pygame.SRCALPHA)
        hit_seg = _Segment(
            surf=hit_surf,
            y=y_cursor,
            url=url,
        )
        hit_seg.link_rect = pygame.Rect(blit_x + lx, y_cursor, lw, max_h)
        segments.append(hit_seg)


# ---------------------------------------------------------------------------
# Credits scene
# ---------------------------------------------------------------------------


class Credits:
    """
    Scene: scrolling credits with auto-scroll and clickable links.

    update() return values
    ----------------------
    "back"   Main menu / calling scene
    None     No transition
    """

    def __init__(self, screen: pygame.Surface) -> None:
        self._pending: Optional[str] = None

        self._gui = GUIHandler()
        self._last_size = (0, 0)

        # Scroll state: scroll_y is the y-offset into the virtual canvas.
        # Starts at a negative value (showing blank below content), scrolls up.
        self._scroll_y: float = 0.0
        self._total_h:  int   = 0
        self._segments: list[_Segment] = []

        # Fade gradient surface (rebuilt on resize)
        self._fade_surf: Optional[pygame.Surface] = None

        # Per-frame hover tracking for link underline colour change
        self._hovered_url: Optional[str] = None

        self._build(screen.get_size())

    # ------------------------------------------------------------------
    # Build / rebuild on resize
    # ------------------------------------------------------------------

    def _build(self, size: tuple[int, int]) -> None:
        w, h = size
        self._last_size = size
        cx = w // 2
        pad = 20

        # Content area geometry
        content_width = min(_CONTENT_WIDTH, w - 2 * _CONTENT_PAD_X)
        self._content_x = cx - content_width // 2
        self._content_width = content_width
        self._scroll_top = _HEADER_H + 8
        self._scroll_bot = h
        self._screen_w = w
        self._screen_h = h

        # Parse and render segments
        self._segments = _build_segments(content_width)
        if self._segments:
            last = self._segments[-1]
            self._total_h = last.y + last.surf.get_height() + _TRAIL_SPACE
        else:
            self._total_h = h

        # Start scroll at bottom (blank) so the text scrolls in from below.
        scroll_area_h = self._scroll_bot - self._scroll_top
        self._scroll_y = float(-scroll_area_h)

        # Build fade gradient (opaque BG at top → transparent below)
        self._fade_surf = self._build_fade(w, _FADE_HEIGHT)

        # ---- GUI chrome ----
        self._gui.clear()

        # Header panel
        self._gui.panel(cx, 0, w, _HEADER_H, color=theme.PANEL, alpha=220,
                        border_color=None, anchor="midtop")
        self._gui.panel(cx, _HEADER_H, w, 2, color=theme.ACCENT, alpha=180,
                        border_color=None, border_width=0, anchor="midtop")

        # Title
        self._gui.label(cx, _HEADER_H // 2, t("credits"),
                        font_size=26, color=theme.TEXT, anchor="center")

        # Back button — top left
        self._gui.small_button(
            pad, _HEADER_H // 2, t("back"),
            font_size=16, anchor="midleft",
            color=theme.TEXT_DIM, hover_color=theme.ACCENT,
            icon_surf=get_icon(ICO_BACK, 16),
            on_click=self._on_back, name="btn_back",
        )

    @staticmethod
    def _build_fade(width: int, height: int) -> pygame.Surface:
        """Build a vertical gradient: opaque BG at top → transparent at bottom."""
        surf = pygame.Surface((width, height), pygame.SRCALPHA)
        bg = theme.BG
        for y in range(height):
            alpha = int(255 * (1.0 - y / height))
            pygame.draw.line(surf, (*bg, alpha), (0, y), (width, y))
        return surf

    # ------------------------------------------------------------------
    # Callbacks
    # ------------------------------------------------------------------

    def _on_back(self) -> None:
        self._pending = "back"

    # ------------------------------------------------------------------
    # Scene interface
    # ------------------------------------------------------------------

    def update(self, dt: float, events: list[pygame.event.Event]) -> Optional[str]:
        # Resize handling
        current_size = pygame.display.get_surface().get_size()
        if current_size != self._last_size:
            self._build(current_size)

        scroll_area_h = self._scroll_bot - self._scroll_top
        mouse_pos = pygame.mouse.get_pos()

        # Detect hovered link for visual feedback
        self._hovered_url = None
        for seg in self._segments:
            if seg.url and seg.link_rect:
                screen_rect = pygame.Rect(
                    self._content_x + seg.link_rect.x,
                    self._scroll_top + seg.link_rect.y - int(self._scroll_y),
                    seg.link_rect.width,
                    seg.link_rect.height,
                )
                if screen_rect.collidepoint(mouse_pos):
                    self._hovered_url = seg.url
                    break

        for event in events:
            if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                self._pending = "back"

            # Manual scroll with mouse wheel
            if event.type == pygame.MOUSEWHEEL:
                self._scroll_y += event.y * -40

            # Click detection for links
            if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                mx, my = event.pos
                for seg in self._segments:
                    if seg.url and seg.link_rect:
                        screen_rect = pygame.Rect(
                            self._content_x + seg.link_rect.x,
                            self._scroll_top + seg.link_rect.y - int(self._scroll_y),
                            seg.link_rect.width,
                            seg.link_rect.height,
                        )
                        if screen_rect.collidepoint(mx, my):
                            webbrowser.open(seg.url)
                            break

        # Auto-scroll upward
        self._scroll_y += _SCROLL_SPEED * dt

        # Clamp: don't scroll beyond the end of the content; allow trailing space
        max_scroll = float(self._total_h - scroll_area_h)
        if self._scroll_y > max_scroll:
            self._scroll_y = max_scroll
        # Don't allow scrolling above the blank start position
        min_scroll = float(-scroll_area_h)
        if self._scroll_y < min_scroll:
            self._scroll_y = min_scroll

        self._gui.handle_events(events)
        self._gui.update(dt)

        result, self._pending = self._pending, None
        return result

    def draw(self, surface: pygame.Surface) -> None:
        if surface.get_size() != self._last_size:
            self._build(surface.get_size())

        # Background
        surface.fill(theme.BG)

        # Subtle dot grid (matches world_manager aesthetic)
        step = 48
        for gx in range(0, surface.get_width(), step):
            for gy in range(0, surface.get_height(), step):
                pygame.draw.circle(surface, (18, 32, 52), (gx, gy), 1)

        # Clip to scroll area
        scroll_area_h = self._scroll_bot - self._scroll_top
        clip_rect = pygame.Rect(0, self._scroll_top, self._screen_w, scroll_area_h)
        old_clip = surface.get_clip()
        surface.set_clip(clip_rect)

        scroll_int = int(self._scroll_y)

        for seg in self._segments:
            seg_screen_y = self._scroll_top + seg.y - scroll_int
            # Skip segments fully outside the viewport
            if seg_screen_y + seg.surf.get_height() < self._scroll_top:
                continue
            if seg_screen_y > self._scroll_bot:
                continue

            dest_x = self._content_x
            # Link hover: tint the segment surface for hover feedback
            if seg.url and seg.link_rect and seg.url == self._hovered_url:
                hover_surf = seg.surf.copy()
                hover_surf.fill(_LINK_HOV_COLOR + (0,), special_flags=pygame.BLEND_RGBA_ADD)
                surface.blit(hover_surf, (dest_x, seg_screen_y))
            else:
                surface.blit(seg.surf, (dest_x, seg_screen_y))

        surface.set_clip(old_clip)

        # Top fade overlay (covers the scroll area near the header)
        if self._fade_surf:
            surface.blit(self._fade_surf, (0, self._scroll_top))

        # GUI (header panel + back button)
        self._gui.draw(surface)
