//! Replay viewer with transport controls and random-access snapshots.

use std::io;
use std::time::Instant;

use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, Borders, Clear, Gauge, Paragraph};
use ratatui::{DefaultTerminal, Frame};

use crate::config::Palette;
use crate::replay::Replay;
use crate::tui::board_view::render_board;
use crate::tui::panels::{hold_text, next_text, recent_inputs_text, stats_text};
use crate::tui::playback::PlaybackState;
use crate::tui::timeline::ReplayTimeline;

const VISIBLE_ROWS: usize = 23;
const NEXT_COUNT: usize = 5;
const RECENT_LIMIT: usize = 5;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ViewerAction {
    None,
    Back,
    Quit,
}

pub fn handle_key(
    code: KeyCode,
    pb: &mut PlaybackState,
    timeline: &ReplayTimeline,
    show_help: &mut bool,
) -> ViewerAction {
    match code {
        KeyCode::Char('q') => ViewerAction::Quit,
        KeyCode::Esc => ViewerAction::Back,
        KeyCode::Char(' ') => {
            pb.toggle_play();
            ViewerAction::None
        }
        KeyCode::Right => {
            pb.step(1);
            ViewerAction::None
        }
        KeyCode::Left => {
            pb.step(-1);
            ViewerAction::None
        }
        KeyCode::Char('.') => {
            pb.seek(timeline.next_lock(pb.index()));
            ViewerAction::None
        }
        KeyCode::Char(',') => {
            pb.seek(timeline.prev_lock(pb.index()));
            ViewerAction::None
        }
        KeyCode::Char('+') | KeyCode::Char('=') => {
            pb.faster();
            ViewerAction::None
        }
        KeyCode::Char('-') => {
            pb.slower();
            ViewerAction::None
        }
        KeyCode::Home => {
            pb.to_start();
            ViewerAction::None
        }
        KeyCode::End => {
            pb.to_end();
            ViewerAction::None
        }
        KeyCode::Char('?') => {
            *show_help = !*show_help;
            ViewerAction::None
        }
        _ => ViewerAction::None,
    }
}

const FOOTER: &str = "Space play/pause · ←/→ step · ,/. lock · -/+ speed · Home/End start/end · ? help · Esc back · q quit";

pub fn run(terminal: &mut DefaultTerminal, replay: &Replay, palette: &Palette) -> io::Result<bool> {
    let timeline = ReplayTimeline::build(replay);
    let mut pb = PlaybackState::new(timeline.len());
    let mut show_help = false;
    let mut last = Instant::now();

    loop {
        terminal.draw(|frame| draw(frame, replay, &timeline, &pb, show_help, palette))?;

        if event::poll(std::time::Duration::from_millis(8))? {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press {
                    match handle_key(key.code, &mut pb, &timeline, &mut show_help) {
                        ViewerAction::Back => return Ok(false),
                        ViewerAction::Quit => return Ok(true),
                        ViewerAction::None => {}
                    }
                }
            }
        }

        let now = Instant::now();
        let dt = now.duration_since(last).as_secs_f64();
        last = now;
        pb.advance(dt);
    }
}

fn draw(
    frame: &mut Frame,
    replay: &Replay,
    timeline: &ReplayTimeline,
    pb: &PlaybackState,
    show_help: bool,
    palette: &Palette,
) {
    let snap = timeline.snapshot(pb.index());
    let total_frames = timeline.snapshot(timeline.len() - 1).frame;

    let outer = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(1),
            Constraint::Length(3),
            Constraint::Length(1),
        ])
        .split(frame.area());

    let body = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Length(24), Constraint::Min(20)])
        .split(outer[0]);

    let board_block = Block::default().borders(Borders::ALL).title(" Field ");
    frame.render_widget(
        Paragraph::new(render_board(&snap.board, VISIBLE_ROWS, palette)).block(board_block),
        body[0],
    );

    let side = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(NEXT_COUNT as u16 + 2),
            Constraint::Min(8),
        ])
        .split(body[1]);

    let top = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Length(8),
            Constraint::Length(8),
            Constraint::Min(8),
        ])
        .split(side[0]);

    frame.render_widget(
        Paragraph::new(hold_text(&snap.board, palette))
            .block(Block::default().borders(Borders::ALL).title(" Hold ")),
        top[0],
    );
    frame.render_widget(
        Paragraph::new(next_text(&snap.board, NEXT_COUNT, palette))
            .block(Block::default().borders(Borders::ALL).title(" Next ")),
        top[1],
    );
    frame.render_widget(
        Paragraph::new(recent_inputs_text(
            replay,
            snap.frame,
            RECENT_LIMIT,
            palette,
        ))
        .block(Block::default().borders(Borders::ALL).title(" Inputs ")),
        top[2],
    );
    frame.render_widget(
        Paragraph::new(stats_text(
            replay,
            &snap.board,
            snap.frame,
            total_frames,
            palette,
        ))
        .block(Block::default().borders(Borders::ALL).title(" Stats ")),
        side[1],
    );

    let transport = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Min(20), Constraint::Length(16)])
        .split(outer[1]);

    let ratio = if timeline.len() > 1 {
        pb.index() as f64 / (timeline.len() - 1) as f64
    } else {
        0.0
    };
    let state = if pb.is_playing() { "▶" } else { "⏸" };
    let gauge = Gauge::default()
        .block(Block::default().borders(Borders::ALL))
        .gauge_style(Style::default().fg(palette.gauge))
        .ratio(ratio.clamp(0.0, 1.0))
        .label(format!("{state} {}/{}", pb.index(), timeline.len() - 1));
    frame.render_widget(gauge, transport[0]);

    let speed = Paragraph::new(Line::from(Span::styled(
        format!("{:.2}x", pb.speed()),
        Style::default()
            .fg(palette.highlight)
            .add_modifier(Modifier::BOLD),
    )))
    .block(Block::default().borders(Borders::ALL).title(" Speed "));
    frame.render_widget(speed, transport[1]);

    frame.render_widget(
        Paragraph::new(Line::from(Span::styled(
            FOOTER,
            Style::default().fg(palette.label),
        ))),
        outer[2],
    );

    if show_help {
        draw_help(frame);
    }
}

fn draw_help(frame: &mut Frame) {
    let area = centered(60, 40, frame.area());
    frame.render_widget(Clear, area);
    let lines = vec![
        Line::from(Span::styled(
            "Controls",
            Style::default().add_modifier(Modifier::BOLD),
        )),
        Line::from(""),
        Line::from("Space      play / pause"),
        Line::from("← / →      step one frame (pauses)"),
        Line::from(", / .      previous / next piece lock"),
        Line::from("- / +      slower / faster"),
        Line::from("Home/End   jump to start / end"),
        Line::from("?          toggle this help"),
        Line::from("Esc        back to menu"),
        Line::from("q          quit rsstacker"),
    ];
    frame.render_widget(
        Paragraph::new(Text::from(lines))
            .block(Block::default().borders(Borders::ALL).title(" Help ")),
        area,
    );
}

fn centered(pct_x: u16, pct_y: u16, area: Rect) -> Rect {
    let vert = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Percentage((100 - pct_y) / 2),
            Constraint::Percentage(pct_y),
            Constraint::Percentage((100 - pct_y) / 2),
        ])
        .split(area);
    Layout::default()
        .direction(Direction::Horizontal)
        .constraints([
            Constraint::Percentage((100 - pct_x) / 2),
            Constraint::Percentage(pct_x),
            Constraint::Percentage((100 - pct_x) / 2),
        ])
        .split(vert[1])[1]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::replay::load_recording;
    use crate::tui::playback::PlaybackState;
    use crate::tui::timeline::ReplayTimeline;
    use ratatui::crossterm::event::KeyCode;
    use std::path::Path;

    fn fixtures() -> (ReplayTimeline, PlaybackState) {
        let replay =
            load_recording(Path::new("tests/data/line_race/0266e1db49f5.recording")).unwrap();
        let tl = ReplayTimeline::build(&replay);
        let pb = PlaybackState::new(tl.len());
        (tl, pb)
    }

    #[test]
    fn space_toggles_play_q_quits_esc_backs() {
        let (tl, mut pb) = fixtures();
        let mut help = false;
        assert!(!pb.is_playing());
        let a = handle_key(KeyCode::Char(' '), &mut pb, &tl, &mut help);
        assert!(matches!(a, ViewerAction::None));
        assert!(pb.is_playing());
        assert!(matches!(
            handle_key(KeyCode::Char('q'), &mut pb, &tl, &mut help),
            ViewerAction::Quit
        ));
        assert!(matches!(
            handle_key(KeyCode::Esc, &mut pb, &tl, &mut help),
            ViewerAction::Back
        ));
    }

    #[test]
    fn arrows_step_and_pause_dot_jumps_lock() {
        let (tl, mut pb) = fixtures();
        let mut help = false;
        pb.toggle_play();
        handle_key(KeyCode::Right, &mut pb, &tl, &mut help);
        assert!(!pb.is_playing());
        assert_eq!(pb.index(), 1);
        handle_key(KeyCode::Left, &mut pb, &tl, &mut help);
        assert_eq!(pb.index(), 0);
        handle_key(KeyCode::Char('.'), &mut pb, &tl, &mut help);
        assert_eq!(pb.index(), tl.next_lock(0));
    }

    #[test]
    fn speed_keys_change_speed_and_question_toggles_help() {
        let (tl, mut pb) = fixtures();
        let mut help = false;
        handle_key(KeyCode::Char('+'), &mut pb, &tl, &mut help);
        assert_eq!(pb.speed(), 2.0);
        handle_key(KeyCode::Char('-'), &mut pb, &tl, &mut help);
        assert_eq!(pb.speed(), 1.0);
        handle_key(KeyCode::Char('='), &mut pb, &tl, &mut help);
        assert_eq!(pb.speed(), 2.0);
        handle_key(KeyCode::Char('?'), &mut pb, &tl, &mut help);
        assert!(help);
        handle_key(KeyCode::Char('?'), &mut pb, &tl, &mut help);
        assert!(!help);
    }
}
