//! Recording parser for plain JSON and gzip-compressed JSON.

use std::collections::BTreeMap;
use std::path::Path;

use serde_json::Value;

use super::types::{Handling, KeyEvent, KeyKind, Replay};

fn as_f64(v: &Value, default: f64) -> f64 {
    v.as_f64().unwrap_or(default)
}

fn as_i64(v: &Value, default: i64) -> i64 {
    // Input files may store integer-like values as either JSON integers or floats.
    if let Some(i) = v.as_i64() {
        i
    } else if let Some(f) = v.as_f64() {
        f as i64
    } else {
        default
    }
}

fn as_bool(v: &Value, default: bool) -> bool {
    v.as_bool().unwrap_or(default)
}

fn as_str(v: &Value, default: &str) -> String {
    v.as_str().unwrap_or(default).to_string()
}

pub fn load_recording(path: &Path) -> Result<Replay, String> {
    let bytes = std::fs::read(path).map_err(|e| format!("read {path:?}: {e}"))?;

    // Plain JSON is tried first; gzip is accepted as a compact storage form.
    let raw: Value = match serde_json::from_slice(&bytes) {
        Ok(v) => v,
        Err(_) => {
            use std::io::Read;
            let mut decoder = flate2::read::GzDecoder::new(&bytes[..]);
            let mut text = String::new();
            decoder
                .read_to_string(&mut text)
                .map_err(|e| format!("gunzip {path:?}: {e}"))?;
            serde_json::from_str(&text).map_err(|e| format!("parse {path:?}: {e}"))?
        }
    };

    let replay = &raw["replay"];
    let opts = &replay["options"];
    let h = &opts["handling"];
    let handling = Handling {
        arr: as_f64(&h["arr"], 0.0),
        das: as_f64(&h["das"], 0.0),
        dcd: as_i64(&h["dcd"], 0) as i32,
        sdf: as_i64(&h["sdf"], 0) as i32,
        safelock: as_bool(&h["safelock"], false),
        cancel: as_bool(&h["cancel"], false),
        may20g: as_bool(&h["may20g"], false),
        irs: as_str(&h["irs"], "tap"),
        ihs: as_str(&h["ihs"], "tap"),
    };

    let mut key_events: Vec<KeyEvent> = Vec::new();
    if let Some(events) = replay["events"].as_array() {
        for event in events {
            let event_type = event["type"].as_str().unwrap_or("");
            if event_type == "keydown" || event_type == "keyup" {
                let data = &event["data"];
                key_events.push(KeyEvent {
                    frame: as_i64(&event["frame"], 0) as i32,
                    subframe: as_f64(&data["subframe"], 0.0),
                    kind: if event_type == "keydown" {
                        KeyKind::KeyDown
                    } else {
                        KeyKind::KeyUp
                    },
                    key: as_str(&data["key"], ""),
                    hoisted: as_bool(&data["hoisted"], false),
                });
            }
        }
    }

    let stats = &replay["results"]["stats"];
    let mut clear_stats: BTreeMap<String, i64> = BTreeMap::new();
    if let Some(clears) = stats["clears"].as_object() {
        for (k, v) in clears {
            clear_stats.insert(k.clone(), as_i64(v, 0));
        }
    }

    let username = raw["users"]
        .get(0)
        .map(|u| as_str(&u["username"], ""))
        .unwrap_or_default();

    Ok(Replay {
        replay_id: as_str(&raw["id"], ""),
        gamemode: as_str(&raw["gamemode"], ""),
        timestamp: as_str(&raw["ts"], ""),
        username,
        seed: as_i64(&opts["seed"], 0),
        handling,
        total_frames: as_i64(&replay["frames"], 0) as i32,
        key_events,
        pieces_placed: as_i64(&stats["piecesplaced"], 0) as u32,
        lines_cleared: as_i64(&stats["lines"], 0) as u32,
        clear_stats,
        final_time_ms: as_i64(&stats["finaltime"], 0),
        inputs: as_i64(&stats["inputs"], 0),
        allow_180: as_bool(&opts["allow180"], false),
        nolockout: as_bool(&opts["nolockout"], false),
        pro: as_bool(&opts["pro"], false),
        stride: as_bool(&opts["stride"], false),
        no_szo: as_bool(&opts["no_szo"], false),
        levels: as_bool(&opts["levels"], false),
        levelgbase: as_f64(&opts["levelgbase"], 0.0),
        levelgspeed: as_f64(&opts["levelgspeed"], 0.007),
        gravity_may20g: as_bool(&opts["gravitymay20g"], true),
    })
}
