//! TOML configuration for handling, keybindings, and display colors.

pub mod keymap;

use std::path::PathBuf;

use ratatui::style::Color;
use serde::{Deserialize, Serialize};

use crate::config::keymap::KeyMap;
use crate::replay::Handling;
use crate::sim::input::Action;

#[derive(Serialize, Deserialize)]
#[serde(default)]
struct HandlingToml {
    arr: f64,
    das: f64,
    dcd: i32,
    sdf: i32,
    allow_180: bool,
}

impl Default for HandlingToml {
    fn default() -> Self {
        HandlingToml {
            arr: 0.0,
            das: 9.0,
            dcd: 0,
            sdf: 41,
            allow_180: true,
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(default)]
struct KeybindsToml {
    move_left: Vec<String>,
    move_right: Vec<String>,
    soft_drop: Vec<String>,
    hard_drop: Vec<String>,
    rotate_cw: Vec<String>,
    rotate_ccw: Vec<String>,
    rotate_180: Vec<String>,
    hold: Vec<String>,
}

impl Default for KeybindsToml {
    fn default() -> Self {
        KeybindsToml {
            move_left: vec!["Left".into()],
            move_right: vec!["Right".into()],
            soft_drop: vec!["Down".into()],
            hard_drop: vec!["Space".into()],
            rotate_cw: vec!["X".into(), "Up".into()],
            rotate_ccw: vec!["Z".into()],
            rotate_180: vec!["A".into()],
            hold: vec!["C".into()],
        }
    }
}

const DEFAULT_PIECE_I: &str = "#00ff66";
const DEFAULT_PIECE_O: &str = "#18f276";
const DEFAULT_PIECE_T: &str = "#2dea80";
const DEFAULT_PIECE_S: &str = "#39df77";
const DEFAULT_PIECE_Z: &str = "#4ed56f";
const DEFAULT_PIECE_J: &str = "#63cc76";
const DEFAULT_PIECE_L: &str = "#78c980";
const DEFAULT_GHOST: &str = "#2e7d4f";
const DEFAULT_EMPTY: &str = "#12351f";
const DEFAULT_FALLBACK: &str = "#7bd88f";
const DEFAULT_LABEL: &str = "#9be8aa";
const DEFAULT_MUTED: &str = "#5fa66d";
const DEFAULT_TEXT: &str = "#d8ffe0";
const DEFAULT_TITLE: &str = "#effff2";
const DEFAULT_HIGHLIGHT: &str = "#a7ff5a";
const DEFAULT_STATUS: &str = "#4cff76";
const DEFAULT_INPUT: &str = "#b8ff80";
const DEFAULT_GAUGE: &str = "#32d46b";

#[derive(Serialize, Deserialize)]
#[serde(default)]
struct PaletteToml {
    piece_i: String,
    piece_o: String,
    piece_t: String,
    piece_s: String,
    piece_z: String,
    piece_j: String,
    piece_l: String,
    ghost: String,
    empty: String,
    fallback: String,
    label: String,
    muted: String,
    text: String,
    title: String,
    highlight: String,
    status: String,
    input: String,
    gauge: String,
}

impl Default for PaletteToml {
    fn default() -> Self {
        PaletteToml {
            piece_i: DEFAULT_PIECE_I.into(),
            piece_o: DEFAULT_PIECE_O.into(),
            piece_t: DEFAULT_PIECE_T.into(),
            piece_s: DEFAULT_PIECE_S.into(),
            piece_z: DEFAULT_PIECE_Z.into(),
            piece_j: DEFAULT_PIECE_J.into(),
            piece_l: DEFAULT_PIECE_L.into(),
            ghost: DEFAULT_GHOST.into(),
            empty: DEFAULT_EMPTY.into(),
            fallback: DEFAULT_FALLBACK.into(),
            label: DEFAULT_LABEL.into(),
            muted: DEFAULT_MUTED.into(),
            text: DEFAULT_TEXT.into(),
            title: DEFAULT_TITLE.into(),
            highlight: DEFAULT_HIGHLIGHT.into(),
            status: DEFAULT_STATUS.into(),
            input: DEFAULT_INPUT.into(),
            gauge: DEFAULT_GAUGE.into(),
        }
    }
}

#[derive(Serialize, Deserialize, Default)]
struct ConfigFile {
    #[serde(default)]
    handling: HandlingToml,
    #[serde(default)]
    keybinds: KeybindsToml,
    #[serde(default)]
    palette: PaletteToml,
}

#[derive(Clone, Debug)]
pub struct Palette {
    pub piece_i: Color,
    pub piece_o: Color,
    pub piece_t: Color,
    pub piece_s: Color,
    pub piece_z: Color,
    pub piece_j: Color,
    pub piece_l: Color,
    pub ghost: Color,
    pub empty: Color,
    pub fallback: Color,
    pub label: Color,
    pub muted: Color,
    pub text: Color,
    pub title: Color,
    pub highlight: Color,
    pub status: Color,
    pub input: Color,
    pub gauge: Color,
}

impl Palette {
    fn from_toml(p: PaletteToml) -> Palette {
        Palette {
            piece_i: parse_color(&p.piece_i, DEFAULT_PIECE_I),
            piece_o: parse_color(&p.piece_o, DEFAULT_PIECE_O),
            piece_t: parse_color(&p.piece_t, DEFAULT_PIECE_T),
            piece_s: parse_color(&p.piece_s, DEFAULT_PIECE_S),
            piece_z: parse_color(&p.piece_z, DEFAULT_PIECE_Z),
            piece_j: parse_color(&p.piece_j, DEFAULT_PIECE_J),
            piece_l: parse_color(&p.piece_l, DEFAULT_PIECE_L),
            ghost: parse_color(&p.ghost, DEFAULT_GHOST),
            empty: parse_color(&p.empty, DEFAULT_EMPTY),
            fallback: parse_color(&p.fallback, DEFAULT_FALLBACK),
            label: parse_color(&p.label, DEFAULT_LABEL),
            muted: parse_color(&p.muted, DEFAULT_MUTED),
            text: parse_color(&p.text, DEFAULT_TEXT),
            title: parse_color(&p.title, DEFAULT_TITLE),
            highlight: parse_color(&p.highlight, DEFAULT_HIGHLIGHT),
            status: parse_color(&p.status, DEFAULT_STATUS),
            input: parse_color(&p.input, DEFAULT_INPUT),
            gauge: parse_color(&p.gauge, DEFAULT_GAUGE),
        }
    }

    pub fn piece(&self, piece: char) -> Color {
        match piece {
            'I' => self.piece_i,
            'O' => self.piece_o,
            'T' => self.piece_t,
            'S' => self.piece_s,
            'Z' => self.piece_z,
            'J' => self.piece_j,
            'L' => self.piece_l,
            _ => self.fallback,
        }
    }
}

pub struct Config {
    pub handling: Handling,
    pub allow_180: bool,
    pub keybinds: KeyMap,
    pub palette: Palette,
}

impl Config {
    pub fn default() -> Config {
        Config::from_file(ConfigFile::default())
    }

    fn from_file(f: ConfigFile) -> Config {
        let handling = Handling {
            arr: f.handling.arr,
            das: f.handling.das,
            dcd: f.handling.dcd,
            sdf: f.handling.sdf,
            safelock: false,
            cancel: false,
            may20g: true,
            irs: "tap".to_string(),
            ihs: "tap".to_string(),
        };
        let bindings = vec![
            (Action::MoveLeft, f.keybinds.move_left),
            (Action::MoveRight, f.keybinds.move_right),
            (Action::SoftDrop, f.keybinds.soft_drop),
            (Action::HardDrop, f.keybinds.hard_drop),
            (Action::RotateCW, f.keybinds.rotate_cw),
            (Action::RotateCCW, f.keybinds.rotate_ccw),
            (Action::Rotate180, f.keybinds.rotate_180),
            (Action::Hold, f.keybinds.hold),
        ];
        Config {
            handling,
            allow_180: f.handling.allow_180,
            keybinds: KeyMap::from_bindings(&bindings),
            palette: Palette::from_toml(f.palette),
        }
    }

    pub fn from_toml_str(text: &str) -> Config {
        match toml::from_str::<ConfigFile>(text) {
            Ok(f) => Config::from_file(f),
            Err(e) => {
                eprintln!("rsstacker: config parse error ({e}); using defaults");
                Config::default()
            }
        }
    }

    /// Loads the config file, creating it with defaults on first run.
    pub fn load() -> Config {
        let Some(path) = config_path() else {
            return Config::default();
        };
        if path.exists() {
            match std::fs::read_to_string(&path) {
                Ok(text) => Config::from_toml_str(&text),
                Err(e) => {
                    eprintln!("rsstacker: could not read {path:?} ({e}); using defaults");
                    Config::default()
                }
            }
        } else {
            if let Some(parent) = path.parent() {
                let _ = std::fs::create_dir_all(parent);
            }
            let _ = std::fs::write(&path, default_toml());
            Config::default()
        }
    }
}

pub fn config_path() -> Option<PathBuf> {
    dirs::config_dir().map(|d| d.join("rsstacker").join("config.toml"))
}

pub fn default_toml() -> String {
    toml::to_string_pretty(&ConfigFile::default()).unwrap_or_default()
}

fn parse_color(value: &str, fallback: &str) -> Color {
    parse_hex_color(value)
        .or_else(|| parse_hex_color(fallback))
        .unwrap_or(Color::Green)
}

fn parse_hex_color(value: &str) -> Option<Color> {
    let hex = value.trim().trim_start_matches('#');
    if hex.len() != 6 {
        return None;
    }
    let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
    let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
    let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
    Some(Color::Rgb(r, g, b))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_config_has_expected_handling_and_palette() {
        let c = Config::default();
        assert_eq!(c.handling.arr, 0.0);
        assert_eq!(c.handling.das, 9.0);
        assert_eq!(c.handling.sdf, 41);
        assert!(c.allow_180);
        assert_eq!(c.handling.irs, "tap");
        assert_eq!(c.palette.piece('I'), Color::Rgb(0x00, 0xff, 0x66));
    }

    #[test]
    fn default_toml_round_trips_to_default_config() {
        let toml_text = default_toml();
        let c = Config::from_toml_str(&toml_text);
        assert_eq!(c.handling.das, 9.0);
        assert!(c.allow_180);
        assert!(toml_text.contains("[palette]"));
        use crate::sim::input::Action;
        use ratatui::crossterm::event::KeyCode;
        assert_eq!(c.keybinds.action(KeyCode::Left), Some(Action::MoveLeft));
    }

    #[test]
    fn malformed_toml_falls_back_to_defaults() {
        let c = Config::from_toml_str("this is not valid toml === [[[\n");
        assert_eq!(c.handling.das, 9.0);
    }

    #[test]
    fn custom_toml_overrides_handling_and_palette() {
        let text = "[handling]\narr=0.0\ndas=4.0\ndcd=0\nsdf=41\nallow_180=false\n\
                    [keybinds]\nmove_left=[\"Left\"]\nmove_right=[\"Right\"]\nsoft_drop=[\"Down\"]\n\
                    hard_drop=[\"Space\"]\nrotate_cw=[\"X\"]\nrotate_ccw=[\"Z\"]\nrotate_180=[\"A\"]\nhold=[\"C\"]\n\
                    [palette]\npiece_i=\"#00aa00\"\n";
        let c = Config::from_toml_str(text);
        assert_eq!(c.handling.das, 4.0);
        assert_eq!(c.handling.sdf, 41);
        assert!(!c.allow_180);
        assert_eq!(c.palette.piece('I'), Color::Rgb(0x00, 0xaa, 0x00));
    }

    #[test]
    fn partial_handling_section_merges_with_defaults() {
        let c = Config::from_toml_str("[handling]\ndas = 4.0\n");
        assert_eq!(c.handling.das, 4.0);
        assert_eq!(c.handling.arr, 0.0);
        assert_eq!(c.handling.sdf, 41);
        assert!(c.allow_180);
    }

    #[test]
    fn invalid_palette_entry_uses_green_default() {
        let c = Config::from_toml_str("[palette]\npiece_i = \"not-a-color\"\n");
        assert_eq!(c.palette.piece('I'), Color::Rgb(0x00, 0xff, 0x66));
    }
}
