//! Export utilities for rendered chart output.
//!
//! Provides helpers to strip ANSI escape sequences from rendered strings and
//! to write chart output to files, optionally as plain text.

/// Strips all ANSI escape sequences from a string, returning plain text.
///
/// Handles CSI sequences (`\x1b[...X` where `X` is a letter), including
/// SGR colour/style codes (`\x1b[...m`) and erase-to-end-of-line (`\x1b[K`).
///
/// Uses a simple state machine that recognises the ESC (`\x1b`) introducer,
/// then skips everything until the terminating byte (an ASCII letter).
///
/// # Examples
///
/// ```
/// use termichart_render::strip_ansi;
///
/// let styled = "\x1b[1m\x1b[38;5;196mHello\x1b[0m";
/// assert_eq!(strip_ansi(styled), "Hello");
/// ```
pub fn strip_ansi(input: &str) -> String {
    let mut out = String::with_capacity(input.len());

    // States for the tiny state machine.
    // Normal    -- copying characters to output.
    // SeenEsc   -- just saw `\x1b`, waiting for `[` or another char.
    // InCsi     -- inside a CSI sequence, waiting for the terminator (letter).
    #[derive(Clone, Copy, PartialEq)]
    enum State {
        Normal,
        SeenEsc,
        InCsi,
    }

    let mut state = State::Normal;

    for ch in input.chars() {
        match state {
            State::Normal => {
                if ch == '\x1b' {
                    state = State::SeenEsc;
                } else {
                    out.push(ch);
                }
            }
            State::SeenEsc => {
                if ch == '[' {
                    // CSI sequence -- skip until we see the terminator.
                    state = State::InCsi;
                } else {
                    // Not a CSI sequence (e.g. bare ESC). Drop both the ESC
                    // and this character, then return to normal.
                    state = State::Normal;
                }
            }
            State::InCsi => {
                // CSI parameter/intermediate bytes are in the range 0x20..=0x3F.
                // The final byte (terminator) is an ASCII letter in 0x40..=0x7E.
                if ch.is_ascii_alphabetic() || ch == '@' {
                    // Terminator reached -- discard it and return to normal.
                    state = State::Normal;
                }
                // Otherwise keep skipping (parameter digits, semicolons, etc.).
            }
        }
    }

    out
}

/// Exports a rendered chart string to a file.
///
/// If `strip_colors` is `true`, all ANSI escape sequences are removed so the
/// file contains plain text. Otherwise the raw ANSI output is written,
/// which can be viewed with tools like `less -R` or `cat`.
///
/// # Errors
///
/// Returns an [`std::io::Error`] if the file cannot be created or written.
///
/// # Examples
///
/// ```no_run
/// use termichart_render::export_to_file;
///
/// let rendered = "\x1b[1mChart Title\x1b[0m\nsome data\n";
/// export_to_file(rendered, "/tmp/chart.txt", true).unwrap();
/// ```
pub fn export_to_file(rendered: &str, path: &str, strip_colors: bool) -> std::io::Result<()> {
    let content = if strip_colors {
        strip_ansi(rendered)
    } else {
        rendered.to_string()
    };
    std::fs::write(path, content)
}

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

    #[test]
    fn strip_ansi_plain_text_unchanged() {
        assert_eq!(strip_ansi("hello world"), "hello world");
    }

    #[test]
    fn strip_ansi_empty_string() {
        assert_eq!(strip_ansi(""), "");
    }

    #[test]
    fn strip_ansi_removes_sgr_reset() {
        assert_eq!(strip_ansi("\x1b[0m"), "");
    }

    #[test]
    fn strip_ansi_removes_bold() {
        assert_eq!(strip_ansi("\x1b[1mBold\x1b[0m"), "Bold");
    }

    #[test]
    fn strip_ansi_removes_dim() {
        assert_eq!(strip_ansi("\x1b[2mDim\x1b[0m"), "Dim");
    }

    #[test]
    fn strip_ansi_removes_256_color_fg() {
        let input = "\x1b[38;5;196mRed\x1b[0m";
        assert_eq!(strip_ansi(input), "Red");
    }

    #[test]
    fn strip_ansi_removes_256_color_bg() {
        let input = "\x1b[48;5;21mBlue BG\x1b[0m";
        assert_eq!(strip_ansi(input), "Blue BG");
    }

    #[test]
    fn strip_ansi_removes_truecolor_fg() {
        let input = "\x1b[38;2;255;0;128mPink\x1b[0m";
        assert_eq!(strip_ansi(input), "Pink");
    }

    #[test]
    fn strip_ansi_removes_truecolor_bg() {
        let input = "\x1b[48;2;0;255;0mGreen BG\x1b[0m";
        assert_eq!(strip_ansi(input), "Green BG");
    }

    #[test]
    fn strip_ansi_removes_erase_to_eol() {
        let input = "text\x1b[K";
        assert_eq!(strip_ansi(input), "text");
    }

    #[test]
    fn strip_ansi_removes_default_fg_bg() {
        let input = "\x1b[39m\x1b[49mDefault";
        assert_eq!(strip_ansi(input), "Default");
    }

    #[test]
    fn strip_ansi_mixed_styled_and_plain() {
        let input = "AAA\x1b[1mBBB\x1b[0mCCC";
        assert_eq!(strip_ansi(input), "AAABBBCCC");
    }

    #[test]
    fn strip_ansi_multiple_style_changes() {
        // Simulates bold + fg colour + text + reset + erase, like render_to_string output.
        let input = "\x1b[1m\x1b[38;5;10mOK\x1b[0m\x1b[K\n\x1b[2m\x1b[48;2;30;30;30m--\x1b[0m\x1b[K";
        assert_eq!(strip_ansi(input), "OK\n--");
    }

    #[test]
    fn strip_ansi_preserves_newlines() {
        let input = "\x1b[0mline1\x1b[K\nline2\x1b[K\n";
        assert_eq!(strip_ansi(input), "line1\nline2\n");
    }

    #[test]
    fn strip_ansi_preserves_unicode() {
        let input = "\x1b[1m\u{2800}\u{28FF}\x1b[0m";
        assert_eq!(strip_ansi(input), "\u{2800}\u{28FF}");
    }

    #[test]
    fn export_to_file_plain_text() {
        let dir = std::env::temp_dir();
        let path = dir.join("termichart_test_plain.txt");
        let path_str = path.to_str().unwrap();

        let rendered = "\x1b[1mTitle\x1b[0m\x1b[K\ndata\x1b[K\n";
        export_to_file(rendered, path_str, true).unwrap();

        let contents = std::fs::read_to_string(&path).unwrap();
        assert_eq!(contents, "Title\ndata\n");

        // Clean up.
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn export_to_file_with_ansi() {
        let dir = std::env::temp_dir();
        let path = dir.join("termichart_test_ansi.txt");
        let path_str = path.to_str().unwrap();

        let rendered = "\x1b[1mTitle\x1b[0m\n";
        export_to_file(rendered, path_str, false).unwrap();

        let contents = std::fs::read_to_string(&path).unwrap();
        assert_eq!(contents, rendered);

        // Clean up.
        let _ = std::fs::remove_file(&path);
    }
}
