//! Text builders for side panels used by live play and replay screens.

use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span, Text};

use crate::config::Palette;
use crate::engine::board::Board;
use crate::replay::{KeyKind, Replay};
use crate::score_attack::score_board_locks;

use super::board_view::piece_color;

fn key_label(key: &str) -> &str {
    match key {
        "moveLeft" => "L",
        "moveRight" => "R",
        "rotateCW" => "CW",
        "rotateCCW" => "CCW",
        "rotate180" => "180",
        "softDrop" => "SD",
        "hardDrop" => "HD",
        "hold" => "HOLD",
        other => other,
    }
}

fn display_mode(mode: &str) -> &str {
    match mode {
        "40l" => "Line Race",
        "blitz" => "Score Attack",
        other => other,
    }
}

fn piece_span(piece: char, palette: &Palette) -> Span<'static> {
    Span::styled(
        format!("{piece} "),
        Style::default()
            .fg(piece_color(piece, palette))
            .add_modifier(Modifier::BOLD),
    )
}

pub fn hold_text(board: &Board, palette: &Palette) -> Text<'static> {
    let line = match board.hold {
        Some(p) => Line::from(piece_span(p, palette)),
        None => Line::from(Span::styled("-", Style::default().fg(palette.label))),
    };
    Text::from(vec![line])
}

pub fn next_text(board: &Board, count: usize, palette: &Palette) -> Text<'static> {
    let mut lines: Vec<Line<'static>> = Vec::new();
    for &p in board.queue.iter().take(count) {
        lines.push(Line::from(piece_span(p, palette)));
    }
    if lines.is_empty() {
        lines.push(Line::from(Span::styled(
            "-",
            Style::default().fg(palette.label),
        )));
    }
    Text::from(lines)
}

pub fn stats_text(
    replay: &Replay,
    board: &Board,
    frame: i32,
    total_frames: i32,
    palette: &Palette,
) -> Text<'static> {
    let secs = frame as f64 / 60.0;
    let h = &replay.handling;
    let mut lines = vec![
        Line::from(Span::styled(
            format!("Mode    {}", display_mode(&replay.gamemode)),
            Style::default().fg(palette.label),
        )),
        Line::from(Span::styled(
            format!("Player  {}", replay.username),
            Style::default().fg(palette.label),
        )),
        Line::from(Span::styled(
            format!("Time    {secs:.2}s"),
            Style::default().fg(palette.label),
        )),
        Line::from(Span::styled(
            format!("Frame   {frame}/{total_frames}"),
            Style::default().fg(palette.label),
        )),
        Line::from(Span::styled(
            format!("Pieces  {}", board.pieces_placed),
            Style::default().fg(palette.label),
        )),
    ];
    match replay.gamemode.as_str() {
        "blitz" => {
            let score = score_board_locks(board).score;
            lines.push(Line::from(Span::styled(
                format!("Score  {score}"),
                Style::default()
                    .fg(palette.highlight)
                    .add_modifier(Modifier::BOLD),
            )));
            lines.push(Line::from(Span::styled(
                format!("Lines   {}", board.lines_cleared),
                Style::default().fg(palette.label),
            )));
        }
        "40l" => {
            lines.push(Line::from(Span::styled(
                format!("Lines  {}/40", board.lines_cleared),
                Style::default().fg(palette.label),
            )));
        }
        _ => {}
    }
    lines.push(Line::from(""));
    lines.push(Line::from(Span::styled(
        "Handling",
        Style::default()
            .fg(palette.label)
            .add_modifier(Modifier::BOLD),
    )));
    lines.push(Line::from(Span::styled(
        format!("ARR {}  DAS {}", h.arr, h.das),
        Style::default().fg(palette.label),
    )));
    lines.push(Line::from(Span::styled(
        format!("DCD {}  SDF {}", h.dcd, h.sdf),
        Style::default().fg(palette.label),
    )));
    lines.push(Line::from(Span::styled(
        format!("Seed    {}", replay.seed),
        Style::default().fg(palette.label),
    )));
    Text::from(lines)
}

pub fn recent_inputs(replay: &Replay, frame: i32, limit: usize) -> Vec<String> {
    let mut downs: Vec<&crate::replay::KeyEvent> = replay
        .key_events
        .iter()
        .filter(|e| matches!(e.kind, KeyKind::KeyDown) && e.frame <= frame)
        .collect();
    downs.sort_by(|a, b| (a.frame, a.subframe.to_bits()).cmp(&(b.frame, b.subframe.to_bits())));
    let start = downs.len().saturating_sub(limit);
    downs[start..]
        .iter()
        .map(|e| key_label(&e.key).to_string())
        .collect()
}

pub fn recent_inputs_text(
    replay: &Replay,
    frame: i32,
    limit: usize,
    palette: &Palette,
) -> Text<'static> {
    let labels = recent_inputs(replay, frame, limit);
    if labels.is_empty() {
        return Text::from(vec![Line::from(Span::styled(
            "-",
            Style::default().fg(palette.label),
        ))]);
    }
    let lines = labels
        .into_iter()
        .map(|l| Line::from(Span::styled(l, Style::default().fg(palette.input))))
        .collect::<Vec<_>>();
    Text::from(lines)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::Config;
    use crate::replay::{Handling, KeyEvent, KeyKind, Replay};
    use std::collections::BTreeMap;

    fn sample_replay() -> Replay {
        Replay {
            replay_id: "id".into(),
            gamemode: "40l".into(),
            timestamp: "t".into(),
            username: "Player".into(),
            seed: 1,
            handling: Handling {
                arr: 0.0,
                das: 4.6,
                dcd: 0,
                sdf: 41,
                safelock: false,
                cancel: false,
                may20g: true,
                irs: "tap".into(),
                ihs: "tap".into(),
            },
            total_frames: 100,
            key_events: vec![
                KeyEvent {
                    frame: 1,
                    subframe: 0.0,
                    kind: KeyKind::KeyDown,
                    key: "moveLeft".into(),
                    hoisted: false,
                },
                KeyEvent {
                    frame: 2,
                    subframe: 0.0,
                    kind: KeyKind::KeyUp,
                    key: "moveLeft".into(),
                    hoisted: false,
                },
                KeyEvent {
                    frame: 3,
                    subframe: 0.0,
                    kind: KeyKind::KeyDown,
                    key: "hardDrop".into(),
                    hoisted: false,
                },
                KeyEvent {
                    frame: 50,
                    subframe: 0.0,
                    kind: KeyKind::KeyDown,
                    key: "rotateCW".into(),
                    hoisted: false,
                },
            ],
            pieces_placed: 0,
            lines_cleared: 0,
            clear_stats: BTreeMap::new(),
            final_time_ms: 0,
            inputs: 0,
            allow_180: false,
            nolockout: false,
            pro: false,
            stride: false,
            no_szo: false,
            levels: false,
            levelgbase: 0.0,
            levelgspeed: 0.007,
            gravity_may20g: true,
        }
    }

    fn replay_with_mode(mode: &str) -> Replay {
        let mut r = sample_replay();
        r.gamemode = mode.to_string();
        r
    }

    fn text_contains_line(text: &ratatui::text::Text, needle: &str) -> bool {
        text.lines.iter().any(|line| {
            let joined: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
            joined.contains(needle)
        })
    }

    #[test]
    fn stats_show_mode_and_line_race_progress() {
        let palette = Config::default().palette;
        let r = replay_with_mode("40l");
        let mut board = Board::new(vec!['I', 'O', 'T', 'S', 'Z', 'J', 'L']);
        board.lines_cleared = 12;
        let text = stats_text(&r, &board, 600, 800, &palette);
        assert!(text_contains_line(&text, "Mode"));
        assert!(text_contains_line(&text, "Line Race"));
        assert!(text_contains_line(&text, "Lines  12/40"));
        assert!(!text_contains_line(&text, "Score"));
    }

    #[test]
    fn stats_show_live_score_for_score_attack() {
        let palette = Config::default().palette;
        let r = replay_with_mode("blitz");
        let board = Board::new(vec!['I', 'O', 'T', 'S', 'Z', 'J', 'L']);
        let text = stats_text(&r, &board, 600, 800, &palette);
        assert!(text_contains_line(&text, "Mode"));
        assert!(text_contains_line(&text, "Score Attack"));
        assert!(text_contains_line(&text, "Score  0"));
        assert!(!text_contains_line(&text, "/40"));
    }

    #[test]
    fn stats_show_handling_configs() {
        let palette = Config::default().palette;
        let r = replay_with_mode("40l");
        let board = Board::new(vec!['I', 'O', 'T', 'S', 'Z', 'J', 'L']);
        let text = stats_text(&r, &board, 0, 800, &palette);
        assert!(text_contains_line(&text, "Handling"));
        assert!(text_contains_line(&text, "ARR"));
        assert!(text_contains_line(&text, "DAS"));
        assert!(text_contains_line(&text, "SDF"));
    }

    #[test]
    fn stats_unknown_mode_shows_no_metric_line() {
        let palette = Config::default().palette;
        let r = replay_with_mode("endless");
        let board = Board::new(vec!['I', 'O', 'T', 'S', 'Z', 'J', 'L']);
        let text = stats_text(&r, &board, 0, 800, &palette);
        assert!(!text_contains_line(&text, "/40"));
        assert!(!text_contains_line(&text, "Score"));
        assert!(text_contains_line(&text, "Mode"));
        assert!(text_contains_line(&text, "Handling"));
    }

    #[test]
    fn recent_inputs_only_keydowns_up_to_frame_most_recent_last() {
        let r = sample_replay();
        let got = recent_inputs(&r, 10, 5);
        assert_eq!(got, vec!["L".to_string(), "HD".to_string()]);
        assert!(!got.contains(&"CW".to_string()));
    }

    #[test]
    fn recent_inputs_respects_limit() {
        let r = sample_replay();
        let got = recent_inputs(&r, 100, 1);
        assert_eq!(got, vec!["CW".to_string()]);
    }
}
