from __future__ import annotations

"""Staged progressive slide fade-in: BG, then frame+blocking, then digits."""

import time
from typing import Callable, Optional, Tuple

import pygame

from reusable_odometer.render.widget import OdometerWidget

PHASE1_END = 85
PHASE2_END = 170
DEFAULT_FADE_STEP = 8
FADE_DURATION_SEC = 1.0
# OHD game-to-game snapshot crossfade (not the staged BG/frame/digits fade-in).
CROSSFADE_DURATION_SEC = 0.32
CROSSFADE_STEP = 8


def _fade_step_sleep(step: int = DEFAULT_FADE_STEP) -> float:
    steps = max(1, len(range(0, 256, max(1, step))))
    return FADE_DURATION_SEC / steps


def crossfade_step_sleep(step: int = CROSSFADE_STEP) -> float:
    steps = max(1, len(range(0, 256, max(1, step))))
    return CROSSFADE_DURATION_SEC / steps


def _phase_alphas(step_value: int) -> Tuple[int, int, int]:
    """Return (background, frame_blocking, digits) alpha for step_value in 0..255."""
    if step_value < PHASE1_END:
        bg = min(255, int(255 * step_value / PHASE1_END))
        return bg, 0, 0
    if step_value < PHASE2_END:
        t = (step_value - PHASE1_END) / (PHASE2_END - PHASE1_END)
        return 255, min(255, int(255 * t)), 0
    t = (step_value - PHASE2_END) / max(1, (256 - PHASE2_END))
    return 255, 255, min(255, int(255 * t))


def _blit_alpha(
    target: pygame.Surface, source: pygame.Surface, pos: Tuple[int, int], alpha: int
) -> None:
    if alpha <= 0:
        return
    if alpha >= 255:
        target.blit(source, pos)
        return
    temp = source.copy()
    temp.set_alpha(alpha)
    target.blit(temp, pos)


def run_staged_progressive_fade_in(
    win: pygame.Surface,
    widget: OdometerWidget,
    output_size: Tuple[int, int],
    *,
    under: Optional[pygame.Surface] = None,
    step: int = DEFAULT_FADE_STEP,
    step_sleep: float | None = None,
    tick_background: Optional[Callable[[float], None]] = None,
    should_abort: Optional[Callable[[], bool]] = None,
    on_keydown: Optional[Callable[[str], None]] = None,
) -> bool:
    """
    Fade in progressive slide in three beats:
      1. background to full brightness
      2. blocking (if any) + frame together (frame on top)
      3. digits
    """
    bg_layer = widget.render_background_layer(output_size).convert_alpha()
    frame_layer = widget.render_frame_blocking_layer(output_size).convert_alpha()
    digits_layer = widget.render_digits_layer(output_size).convert_alpha()

    if step_sleep is None:
        step_sleep = _fade_step_sleep(step)

    under_pre = None
    if under is not None:
        try:
            under_pre = under.convert_alpha()
        except pygame.error:
            under_pre = under.convert()

    last_tick = time.perf_counter()
    t0 = last_tick

    while True:
        if should_abort is not None and should_abort():
            return False

        now = time.perf_counter()
        elapsed = now - t0
        if elapsed >= FADE_DURATION_SEC:
            step_value = 255
        else:
            step_value = int(255 * (elapsed / FADE_DURATION_SEC))

        if tick_background is not None:
            tick_background(max(0.0, now - last_tick))
        last_tick = now

        bg_alpha, frame_alpha, digits_alpha = _phase_alphas(step_value)
        if digits_alpha > 0:
            digits_layer = widget.render_digits_layer(output_size).convert_alpha()

        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN and on_keydown is not None:
                on_keydown(pygame.key.name(event.key))

        win.fill((0, 0, 0))
        if under_pre is not None and bg_alpha < 255:
            under_pre.set_alpha(max(0, 255 - bg_alpha))
            win.blit(under_pre, (0, 0))
        if bg_alpha >= 255:
            bg_layer.set_alpha(None)
            win.blit(bg_layer, (0, 0))
        elif bg_alpha > 0:
            bg_layer.set_alpha(bg_alpha)
            win.blit(bg_layer, (0, 0))
        if frame_alpha >= 255:
            frame_layer.set_alpha(None)
            win.blit(frame_layer, (0, 0))
        elif frame_alpha > 0:
            frame_layer.set_alpha(frame_alpha)
            win.blit(frame_layer, (0, 0))
        if digits_alpha >= 255:
            digits_layer.set_alpha(None)
            win.blit(digits_layer, (0, 0))
        elif digits_alpha > 0:
            digits_layer.set_alpha(digits_alpha)
            win.blit(digits_layer, (0, 0))
        pygame.display.update()

        if elapsed >= FADE_DURATION_SEC:
            break
        remain = step_sleep - (time.perf_counter() - now)
        if remain > 0.001:
            time.sleep(remain)

    return True
