// m4-rs frozen module — frozen state save/reload.
//
// WHO:   infinityabundance. Original frozen file format by GNU m4 authors.
// WHAT:  Implements -F (freeze state to file) and -R (reload state from file).
//        Saves macro definitions, pushdef stacks, quote/comment settings,
//        and diversion contents in a text-based format.
// WHEN:  Called at exit (-F) or startup (-R) by the CLI.
// WHERE: crates/m4-rs-core/src/frozen.rs
// WHY:   Frozen files avoid re-parsing large standard libraries (e.g., Autoconf).
//        They snapshot internal state for fast reload.
// HOW:   Text-based format matching GNU m4 1.4.20 §15.2:
//        V1              — version (GNU m4 1.4.20 uses V1)
//        Q`,             — quote open delimiter
//        '               — quote close delimiter (on next line)
//        C#,             — comment start delimiter
//                         — comment end delimiter (newline, shown as blank)
//        F1,1            — feature flags
//        T12,5:define>define  — macro (name_len,body_len:name>body)
//        D1,10           — diversion 1, 10 bytes
//        <bytes>         — diversion content
//        #               — end of diversion marker

use crate::expansion::ExpansionEngine;
use crate::macro_table::MacroDef;
use std::io::{BufRead, BufReader, Write};
use std::path::Path;

/// Save engine state to a frozen file.
pub fn save_state(engine: &ExpansionEngine, path: &Path) -> std::io::Result<()> {
    let mut f = std::fs::File::create(path)?;

    writeln!(f, "# Frozen state generated by m4-rs")?;
    // GNU m4 1.4.20 uses version 1 (see manual §15.2).
    writeln!(f, "V1")?;

    // Quote delimiters — two-line format: Q<olen>,<clen> then <open><close>
    // Skip if at defaults (matching GNU m4 behavior).
    let qo = &engine.quote_config.open;
    let qc = &engine.quote_config.close;
    if qo != "`" || qc != "'" {
        writeln!(f, "Q{},{}", qo.len(), qc.len())?;
        write!(f, "{}", qo)?;
        writeln!(f, "{}", qc)?;
    }

    // Comment delimiters — two-line format: C<slen>,<elen> then <start><end>
    // Skip if at defaults (matching GNU m4 behavior).
    let co = &engine.quote_config.comment_open;
    let cc = &engine.quote_config.comment_close;
    if co != "#" || cc != "\n" {
        writeln!(f, "C{},{}", co.len(), cc.len())?;
        write!(f, "{}", co)?;
        write!(f, "{}", cc)?;
    }

    // No feature flags line needed (m4-rs doesn't use GNU m4's feature flags).
    // Note: GNU m4 writes F1,1 here but we skip it to avoid format incompatibility.

    // Macro definitions (user-defined, two-line format matching GNU m4)
    for (name, stack) in &engine.macro_table.table {
        for def in &stack.defs {
            if def.is_builtin {
                // Builtins: write as F lines (GNU m4 two-line format)
                let n = name.len();
                writeln!(f, "F{},{}", n, n)?;
                write!(f, "{}", String::from_utf8_lossy(name))?;
                write!(f, "{}", String::from_utf8_lossy(name))?;
                writeln!(f)?;
            } else {
                // User macros: T<namelen>,<bodylen> then <name><body>
                let n = name.len();
                let b = def.text.len();
                writeln!(f, "T{},{}", n, b)?;
                write!(f, "{}", String::from_utf8_lossy(name))?;
                write!(f, "{}", String::from_utf8_lossy(&def.text))?;
                writeln!(f)?;
            }
        }
    }

    // Diversions
    for (&num, buf) in &engine.diversions {
        if num == 0 || buf.is_empty() {
            continue;
        }
        writeln!(f, "D{},{}", num, buf.len())?;
        f.write_all(buf)?;
        writeln!(f)?;
        writeln!(f, "#")?;
    }

    f.flush()?;
    Ok(())
}

/// Load frozen state into an engine (before processing input).
pub fn load_state(engine: &mut ExpansionEngine, path: &Path) -> std::io::Result<()> {
    let file = std::fs::File::open(path)?;
    let reader = BufReader::new(file);
    let mut lines = reader.lines();

    while let Some(Ok(line)) = lines.next() {
        let line = line.trim().to_string();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        match line.chars().next() {
            Some('V') => {
                // Version marker — verify compatibility with GNU m4 1.4.20 format.
                // GNU m4 1.4.20 uses V1 only (see manual §15.2).
                if line != "V1" {
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::InvalidData,
                        format!("unsupported frozen file version: {}", line),
                    ));
                }
            }
            Some('Q') => {
                // GNU m4 frozen format: Q<olen>,<clen> + next line with <open><close>
                if let Some(rest) = line.strip_prefix('Q') {
                    let comma_pos = rest.find(',').unwrap_or(0);
                    let olen: usize = rest[..comma_pos].parse().unwrap_or(0);
                    let clen: usize = rest[comma_pos + 1..].parse().unwrap_or(0);
                    if let Some(Ok(data_line)) = lines.next() {
                        let data = data_line.trim();
                        if data.len() >= olen + clen {
                            let open = &data[..olen];
                            let close = &data[olen..olen + clen];
                            engine.quote_config.change_quote(Some(open), Some(close));
                        }
                    }
                }
            }
            Some('C') => {
                // GNU m4 frozen format: C<slen>,<elen> + next line with <start><end>
                if let Some(rest) = line.strip_prefix('C') {
                    let comma_pos = rest.find(',').unwrap_or(0);
                    let slen: usize = rest[..comma_pos].parse().unwrap_or(0);
                    let elen: usize = rest[comma_pos + 1..].parse().unwrap_or(0);
                    if let Some(Ok(data_line)) = lines.next() {
                        let data = data_line.trim();
                        if data.len() >= slen + elen {
                            let start = &data[..slen];
                            let end = &data[slen..slen + elen];
                            engine.quote_config.change_comment(Some(start), Some(end));
                        }
                    }
                }
            }
            Some('F') => {
                // GNU m4 frozen format: F<len1>,<len2> + next line with <name><name>
                // These are builtin macro names (is_builtin=true).
                // The name appears twice: once for the bare name and once for
                // the m4_-prefixed form. We register the bare name as a builtin.
                if let Some(rest) = line.strip_prefix('F') {
                    let comma_pos = rest.find(',').unwrap_or(0);
                    let namelen: usize = rest[..comma_pos].parse().unwrap_or(0);
                    if namelen > 0 {
                        if let Some(Ok(data_line)) = lines.next() {
                            let data = data_line.trim();
                            let db = data.as_bytes();
                            if db.len() >= namelen {
                                let name = &db[..namelen];
                                let def = MacroDef::builtin(false);
                                engine
                                    .macro_table
                                    .table
                                    .entry(name.to_vec())
                                    .or_insert_with(|| {
                                        crate::macro_table::MacroStack::new(def.clone())
                                    })
                                    .replace_top(def);
                            }
                        }
                    }
                }
            }
            Some('T') => {
                // GNU m4 frozen format: T<namelen>,<bodylen> + next line with <name><body>
                // These are user-defined macros (is_builtin=false).
                if let Some(rest) = line.strip_prefix('T') {
                    let comma_pos = rest.find(',').unwrap_or(0);
                    let namelen: usize = rest[..comma_pos].parse().unwrap_or(0);
                    let bodylen: usize = rest[comma_pos + 1..].parse().unwrap_or(0);
                    if namelen > 0 || bodylen > 0 {
                        if let Some(Ok(data_line)) = lines.next() {
                            let data_bytes = data_line.as_bytes();
                            let total = namelen + bodylen;
                            if data_bytes.len() >= total {
                                let name = &data_bytes[..namelen];
                                let body = &data_bytes[namelen..total];
                                engine.macro_table.pushdef(name, body);
                            }
                        }
                    }
                }
            }
            Some('D') => {
                // Diversion: D<num>,<size>\n<bytes>\n#
                // Use the declared size to know exactly how many bytes to read.
                // The size includes the trailing newline consumed by lines().
                if let Some(rest) = line.strip_prefix('D') {
                    let comma_pos = rest.find(',').unwrap_or(0);
                    let divnum: i32 = rest[..comma_pos].parse().unwrap_or(0);
                    let size: usize = rest[comma_pos + 1..].parse().unwrap_or(0);
                    if size > 0 {
                        if let Some(Ok(data_line)) = lines.next() {
                            let mut data = data_line.as_bytes().to_vec();
                            // lines() strips the trailing \n which the size count includes.
                            // Add it back so the content matches exactly.
                            data.push(b'\n');
                            let end = size.min(data.len());
                            if end > 0 {
                                engine.diversions.insert(divnum, data[..end].to_vec());
                            }
                        }
                    }
                    // Consume the trailing # or # End... line
                    if let Some(Ok(terminator)) = lines.next() {
                        let t = terminator.trim();
                        if !t.starts_with('#') {
                            // Not a valid terminator — ignore
                        }
                    }
                }
            }
            _ => {
                // Unknown directive — skip
            }
        }
    }

    // Sync the relexer's quote_config so expand_user_macro doesn't need
    // to clone on every expansion. This must happen after loading because
    // load_state may change quote/comment delimiters.
    engine.relexer.quote_config = engine.quote_config.clone();

    Ok(())
}

/// Escape a delimiter string for frozen file format.
/// Newlines become \n, commas and backslashes are escaped.
#[allow(dead_code)]
fn escape_delim(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('\n', "\\n")
        .replace(',', "\\,")
}

/// Unescape a delimiter string from frozen file format.
#[allow(dead_code)]
fn unescape_delim(s: &str) -> String {
    let mut result = String::new();
    let mut chars = s.chars();
    while let Some(c) = chars.next() {
        if c == '\\' {
            match chars.next() {
                Some('n') => result.push('\n'),
                Some('\\') => result.push('\\'),
                Some(',') => result.push(','),
                Some(other) => {
                    result.push('\\');
                    result.push(other);
                }
                None => result.push('\\'),
            }
        } else {
            result.push(c);
        }
    }
    result
}

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

    #[test]
    fn test_escape_roundtrip() {
        let original = "hello\nworld\\,test";
        let escaped = escape_delim(original);
        let unescaped = unescape_delim(&escaped);
        assert_eq!(unescaped, original);
    }

    #[test]
    fn test_save_and_load_basic() {
        let mut engine = ExpansionEngine::new();
        engine.register_builtins();
        engine.macro_table.define(b"foo", b"bar");
        engine.macro_table.define(b"hello", b"world");

        let tmp = std::env::temp_dir().join("m4_test_frozen.m4f");
        save_state(&engine, &tmp).expect("save");

        let mut engine2 = ExpansionEngine::new();
        engine2.register_builtins();
        load_state(&mut engine2, &tmp).expect("load");

        assert_eq!(engine2.macro_table.lookup(b"foo").unwrap().text, b"bar");
        assert_eq!(engine2.macro_table.lookup(b"hello").unwrap().text, b"world");

        let _ = std::fs::remove_file(&tmp);
    }

    #[test]
    fn test_save_and_load_quote_config() {
        let mut engine = ExpansionEngine::new();
        engine.quote_config.change_quote(Some("["), Some("]"));

        let tmp = std::env::temp_dir().join("m4_test_frozen2.m4f");
        save_state(&engine, &tmp).expect("save");

        let mut engine2 = ExpansionEngine::new();
        engine2.register_builtins();
        load_state(&mut engine2, &tmp).expect("load");

        assert_eq!(engine2.quote_config.open, "[");
        assert_eq!(engine2.quote_config.close, "]");

        let _ = std::fs::remove_file(&tmp);
    }

    #[test]
    fn test_save_and_load_pushdef_stack() {
        let mut engine = ExpansionEngine::new();
        engine.register_builtins();
        // pushdef creates a stack: inner-first lookup
        engine.macro_table.pushdef(b"test", b"outer");
        engine.macro_table.pushdef(b"test", b"inner");

        let tmp = std::env::temp_dir().join("m4_test_frozen3.m4f");
        save_state(&engine, &tmp).expect("save");

        let mut engine2 = ExpansionEngine::new();
        engine2.register_builtins();
        load_state(&mut engine2, &tmp).expect("load");

        // Top of stack (inner) should be restored first
        let def = engine2.macro_table.lookup(b"test").expect("should exist");
        assert_eq!(def.text, b"inner");

        // pop one level
        engine2.macro_table.popdef(b"test");
        let def2 = engine2
            .macro_table
            .lookup(b"test")
            .expect("should still exist");
        assert_eq!(def2.text, b"outer");

        let _ = std::fs::remove_file(&tmp);
    }

    #[test]
    fn test_save_and_load_diversions() {
        let mut engine = ExpansionEngine::new();
        engine.register_builtins();
        // Store some data in diversion 1
        engine.diversions.insert(1, b"diverted content".to_vec());

        let tmp = std::env::temp_dir().join("m4_test_frozen4.m4f");
        save_state(&engine, &tmp).expect("save");

        let mut engine2 = ExpansionEngine::new();
        engine2.register_builtins();
        load_state(&mut engine2, &tmp).expect("load");

        assert_eq!(
            engine2.diversions.get(&1),
            Some(&b"diverted content".to_vec())
        );

        let _ = std::fs::remove_file(&tmp);
    }

    #[test]
    fn test_save_and_load_special_characters() {
        let mut engine = ExpansionEngine::new();
        engine.register_builtins();
        // Use printable ASCII special characters that survive text-based format.
        // The frozen file format is text-based; non-UTF-8 bytes and NUL cannot
        // roundtrip through String::from_utf8_lossy. This matches GNU m4 behavior
        // where the frozen format restricts character ranges.
        engine
            .macro_table
            .define(b"test_name", b"body with spaces and symbols!@#$%");

        let tmp = std::env::temp_dir().join("m4_test_frozen5.m4f");
        save_state(&engine, &tmp).expect("save");

        let mut engine2 = ExpansionEngine::new();
        engine2.register_builtins();
        load_state(&mut engine2, &tmp).expect("load");

        let def = engine2
            .macro_table
            .lookup(b"test_name")
            .expect("should exist");
        assert_eq!(def.text, b"body with spaces and symbols!@#$%");

        let _ = std::fs::remove_file(&tmp);
    }

    #[test]
    fn test_save_and_load_comment_delimiters() {
        let mut engine = ExpansionEngine::new();
        // Use a non-newline close delimiter for frozen roundtrip.
        // Newlines in delimiters cannot survive the line-oriented frozen
        // file format (GNU m4 has the same limitation).
        engine.quote_config.change_comment(Some("/*"), Some("*/"));

        let tmp = std::env::temp_dir().join("m4_test_frozen6.m4f");
        save_state(&engine, &tmp).expect("save");

        let mut engine2 = ExpansionEngine::new();
        engine2.register_builtins();
        load_state(&mut engine2, &tmp).expect("load");

        assert_eq!(engine2.quote_config.comment_open, "/*");
        assert_eq!(engine2.quote_config.comment_close, "*/");

        let _ = std::fs::remove_file(&tmp);
    }

    #[test]
    fn test_version_mismatch_error() {
        let mut engine = ExpansionEngine::new();
        let tmp = std::env::temp_dir().join("m4_test_frozen_badver.m4f");

        // Write a file with bogus version
        std::fs::write(&tmp, b"V9\n").expect("write");

        let result = load_state(&mut engine, &tmp);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(
            err.to_string().contains("unsupported frozen file version"),
            "expected version error, got: {}",
            err
        );

        let _ = std::fs::remove_file(&tmp);
    }

    #[test]
    fn test_empty_macro_body() {
        let mut engine = ExpansionEngine::new();
        engine.register_builtins();
        engine.macro_table.define(b"empty", b"");

        let tmp = std::env::temp_dir().join("m4_test_frozen_empty.m4f");
        save_state(&engine, &tmp).expect("save");

        let mut engine2 = ExpansionEngine::new();
        engine2.register_builtins();
        load_state(&mut engine2, &tmp).expect("load");

        let def = engine2.macro_table.lookup(b"empty").expect("should exist");
        assert!(def.text.is_empty());

        let _ = std::fs::remove_file(&tmp);
    }

    #[test]
    fn test_multiple_diversions_roundtrip() {
        let mut engine = ExpansionEngine::new();
        engine.register_builtins();
        engine.diversions.insert(1, b"first diversion".to_vec());
        engine.diversions.insert(3, b"third diversion".to_vec());
        engine.diversions.insert(5, b"fifth diversion".to_vec());
        // Diversion 0 should be skipped
        engine.diversions.insert(0, b"should not be saved".to_vec());

        let tmp = std::env::temp_dir().join("m4_test_frozen_multi.m4f");
        save_state(&engine, &tmp).expect("save");

        let mut engine2 = ExpansionEngine::new();
        engine2.register_builtins();
        load_state(&mut engine2, &tmp).expect("load");

        assert_eq!(
            engine2.diversions.get(&1),
            Some(&b"first diversion".to_vec())
        );
        assert_eq!(
            engine2.diversions.get(&3),
            Some(&b"third diversion".to_vec())
        );
        assert_eq!(
            engine2.diversions.get(&5),
            Some(&b"fifth diversion".to_vec())
        );
        // Diversion 0 should not be loaded (it's skipped during save)

        let _ = std::fs::remove_file(&tmp);
    }
}
