//! A PTY-backed terminal: runs an interactive program (a shell, a REPL, an
//! AI-agent CLI like `claude`) inside the editor.
//!
//! This is the interactive counterpart to the command [console](crate::console).
//! The console pipes a one-shot command's output; a real terminal needs a
//! **pseudo-terminal**: programs detect a tty and switch to full-screen/TUI mode,
//! read raw keystrokes from stdin, and drive the screen with ANSI/VT escape
//! sequences. So this module:
//!
//! - opens a PTY with [`portable-pty`] (cross-platform — Linux now, Windows
//!   later) and spawns the program attached to it;
//! - feeds the program's byte stream into a [`vt100`] parser on a reader thread,
//!   which maintains the on-screen grid (cells, colours, cursor);
//! - exposes the grid for the egui panel to render, and [`send_input`] to write
//!   keystrokes back to the program.
//!
//! [`send_input`]: PtyTerminal::send_input
//!
//! The two pure pieces — encoding an egui key press into the bytes a terminal
//! expects ([`encode_key`]) and mapping a [`vt100`] colour to an egui colour
//! ([`vt_color`]) — are unit-tested; the rendering/input loop itself is the
//! eye-checked part.

use std::io::{Read, Write};
use std::sync::{Arc, Mutex};

use egui::{Key, Modifiers};
use portable_pty::{Child, CommandBuilder, MasterPty, PtySize};

/// A live terminal session: the spawned child, its PTY, and the parsed screen.
pub struct PtyTerminal {
    /// A short label for the session (e.g. the program name) shown on the tab.
    pub title: String,
    /// The parsed terminal screen, updated by the reader thread.
    parser: Arc<Mutex<vt100::Parser>>,
    /// The PTY master — kept for resizing.
    master: Box<dyn MasterPty + Send>,
    /// Writes keystrokes to the program (the PTY input side).
    writer: Box<dyn Write + Send>,
    /// The spawned child — killed on drop so closing the panel ends the program.
    child: Box<dyn Child + Send + Sync>,
    /// Current grid size, so we only resize on an actual change.
    rows: u16,
    cols: u16,
}

impl PtyTerminal {
    /// Spawns `program` (with `args`) attached to a fresh PTY of `rows`×`cols`,
    /// running in `cwd`. `title` labels the session.
    pub fn spawn(
        title: impl Into<String>,
        program: &str,
        args: &[&str],
        cwd: &std::path::Path,
        rows: u16,
        cols: u16,
    ) -> std::io::Result<Self> {
        let pty_system = portable_pty::native_pty_system();
        let pair = pty_system
            .openpty(PtySize {
                rows,
                cols,
                pixel_width: 0,
                pixel_height: 0,
            })
            .map_err(to_io)?;

        let mut cmd = CommandBuilder::new(program);
        cmd.args(args);
        cmd.cwd(cwd);
        // Advertise a capable terminal so programs emit colour + use full-screen
        // mode; without this many tools fall back to dumb output.
        cmd.env("TERM", "xterm-256color");

        let child = pair.slave.spawn_command(cmd).map_err(to_io)?;
        // Drop the slave handle so the master sees EOF when the child exits.
        drop(pair.slave);

        let reader = pair.master.try_clone_reader().map_err(to_io)?;
        let writer = pair.master.take_writer().map_err(to_io)?;

        let parser = Arc::new(Mutex::new(vt100::Parser::new(rows, cols, 0)));
        spawn_reader(reader, parser.clone());

        Ok(Self {
            title: title.into(),
            parser,
            master: pair.master,
            writer,
            child,
            rows,
            cols,
        })
    }

    /// Borrows the parsed screen state for rendering (locks the parser).
    pub fn with_screen<R>(&self, f: impl FnOnce(&vt100::Screen) -> R) -> R {
        let parser = self.parser.lock().unwrap();
        f(parser.screen())
    }

    /// The current grid size in (rows, cols).
    pub fn size(&self) -> (u16, u16) {
        (self.rows, self.cols)
    }

    /// Writes raw bytes (already terminal-encoded) to the program's input.
    pub fn send_input(&mut self, bytes: &[u8]) {
        let _ = self.writer.write_all(bytes);
        let _ = self.writer.flush();
    }

    /// Resizes the PTY and parser to `rows`×`cols` (no-op if unchanged). Programs
    /// receive `SIGWINCH` and redraw to the new size.
    pub fn resize(&mut self, rows: u16, cols: u16) {
        if rows == 0 || cols == 0 || (rows == self.rows && cols == self.cols) {
            return;
        }
        self.rows = rows;
        self.cols = cols;
        let _ = self.master.resize(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        });
        self.parser
            .lock()
            .unwrap()
            .screen_mut()
            .set_size(rows, cols);
    }

    /// Whether the child program has exited.
    pub fn has_exited(&mut self) -> bool {
        matches!(self.child.try_wait(), Ok(Some(_)))
    }
}

impl Drop for PtyTerminal {
    fn drop(&mut self) {
        // End the program when the panel/session goes away.
        let _ = self.child.kill();
    }
}

/// Spawns the reader thread: pumps the PTY's output into the `vt100` parser until
/// EOF (the child exited / the master closed).
fn spawn_reader(mut reader: Box<dyn Read + Send>, parser: Arc<Mutex<vt100::Parser>>) {
    std::thread::spawn(move || {
        let mut buf = [0u8; 8192];
        loop {
            match reader.read(&mut buf) {
                Ok(0) | Err(_) => break,
                Ok(n) => parser.lock().unwrap().process(&buf[..n]),
            }
        }
    });
}

/// Adapts a `portable_pty` error into `std::io::Error`.
fn to_io(err: impl std::fmt::Display) -> std::io::Error {
    std::io::Error::other(err.to_string())
}

/// Encodes an egui [`Key`] press (with modifiers) into the byte sequence a
/// terminal program expects on stdin, or `None` for keys we don't translate
/// (printable characters arrive separately as text input events).
///
/// Covers the control keys a TUI needs: Enter, Backspace, Tab, Esc, the arrows
/// and navigation keys (as ANSI CSI sequences), and `Ctrl`+letter (which maps to
/// control codes 0x01–0x1A — e.g. `Ctrl+C` → `0x03`).
pub fn encode_key(key: Key, mods: Modifiers) -> Option<Vec<u8>> {
    // Ctrl + A..Z -> 0x01..0x1A (Ctrl+C = ETX = 0x03, etc.).
    if mods.ctrl && !mods.alt {
        if let Some(letter) = letter_index(key) {
            return Some(vec![letter + 1]); // 'a' -> 1
        }
    }
    let bytes: &[u8] = match key {
        Key::Enter => b"\r",
        Key::Backspace => b"\x7f",
        Key::Tab => b"\t",
        Key::Escape => b"\x1b",
        Key::ArrowUp => b"\x1b[A",
        Key::ArrowDown => b"\x1b[B",
        Key::ArrowRight => b"\x1b[C",
        Key::ArrowLeft => b"\x1b[D",
        Key::Home => b"\x1b[H",
        Key::End => b"\x1b[F",
        Key::PageUp => b"\x1b[5~",
        Key::PageDown => b"\x1b[6~",
        Key::Delete => b"\x1b[3~",
        Key::Insert => b"\x1b[2~",
        _ => return None,
    };
    Some(bytes.to_vec())
}

/// The 0-based index (`a`=0 … `z`=25) of an alphabetic [`Key`], else `None`.
/// Used to map `Ctrl`+letter to its control code.
fn letter_index(key: Key) -> Option<u8> {
    let name = key.name(); // "A".."Z" for letter keys
    let bytes = name.as_bytes();
    if bytes.len() == 1 && bytes[0].is_ascii_uppercase() {
        Some(bytes[0] - b'A')
    } else {
        None
    }
}

/// Maps a [`vt100`] colour to an egui colour, given the default foreground to use
/// for [`vt100::Color::Default`].
pub fn vt_color(color: vt100::Color, default: egui::Color32) -> egui::Color32 {
    match color {
        vt100::Color::Default => default,
        vt100::Color::Rgb(r, g, b) => egui::Color32::from_rgb(r, g, b),
        vt100::Color::Idx(i) => ansi_indexed(i),
    }
}

/// The RGB for an ANSI 256-colour palette index: the 16 base colours, the
/// 6×6×6 colour cube, and the 24-step grey ramp.
fn ansi_indexed(i: u8) -> egui::Color32 {
    match i {
        // Standard + bright 16-colour palette.
        0 => egui::Color32::from_rgb(0x00, 0x00, 0x00),
        1 => egui::Color32::from_rgb(0xCD, 0x00, 0x00),
        2 => egui::Color32::from_rgb(0x00, 0xCD, 0x00),
        3 => egui::Color32::from_rgb(0xCD, 0xCD, 0x00),
        4 => egui::Color32::from_rgb(0x00, 0x00, 0xEE),
        5 => egui::Color32::from_rgb(0xCD, 0x00, 0xCD),
        6 => egui::Color32::from_rgb(0x00, 0xCD, 0xCD),
        7 => egui::Color32::from_rgb(0xE5, 0xE5, 0xE5),
        8 => egui::Color32::from_rgb(0x7F, 0x7F, 0x7F),
        9 => egui::Color32::from_rgb(0xFF, 0x00, 0x00),
        10 => egui::Color32::from_rgb(0x00, 0xFF, 0x00),
        11 => egui::Color32::from_rgb(0xFF, 0xFF, 0x00),
        12 => egui::Color32::from_rgb(0x5C, 0x5C, 0xFF),
        13 => egui::Color32::from_rgb(0xFF, 0x00, 0xFF),
        14 => egui::Color32::from_rgb(0x00, 0xFF, 0xFF),
        15 => egui::Color32::from_rgb(0xFF, 0xFF, 0xFF),
        // 6×6×6 colour cube (indices 16..=231).
        16..=231 => {
            let i = i - 16;
            let steps = [0u8, 95, 135, 175, 215, 255];
            let r = steps[(i / 36) as usize];
            let g = steps[((i / 6) % 6) as usize];
            let b = steps[(i % 6) as usize];
            egui::Color32::from_rgb(r, g, b)
        }
        // 24-step grey ramp (indices 232..=255).
        _ => {
            let level = 8 + (i - 232) * 10;
            egui::Color32::from_gray(level)
        }
    }
}

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

    #[test]
    fn ctrl_c_encodes_to_etx() {
        assert_eq!(encode_key(Key::C, Modifiers::CTRL), Some(vec![0x03]));
        assert_eq!(encode_key(Key::A, Modifiers::CTRL), Some(vec![0x01]));
    }

    #[test]
    fn control_keys_encode_to_their_sequences() {
        assert_eq!(
            encode_key(Key::Enter, Modifiers::NONE),
            Some(b"\r".to_vec())
        );
        assert_eq!(
            encode_key(Key::Backspace, Modifiers::NONE),
            Some(b"\x7f".to_vec())
        );
        assert_eq!(
            encode_key(Key::ArrowUp, Modifiers::NONE),
            Some(b"\x1b[A".to_vec())
        );
    }

    #[test]
    fn plain_letters_are_not_encoded_here() {
        // Printable text comes through egui text-input events, not key encoding.
        assert_eq!(encode_key(Key::A, Modifiers::NONE), None);
    }

    #[test]
    fn vt_default_color_uses_the_supplied_default() {
        let dflt = egui::Color32::from_rgb(1, 2, 3);
        assert_eq!(vt_color(vt100::Color::Default, dflt), dflt);
        assert_eq!(
            vt_color(vt100::Color::Rgb(10, 20, 30), dflt),
            egui::Color32::from_rgb(10, 20, 30)
        );
    }

    #[test]
    fn ansi_cube_and_grey_indices_map_in_range() {
        // Index 16 is the bottom of the cube = black; 231 is white.
        assert_eq!(ansi_indexed(16), egui::Color32::from_rgb(0, 0, 0));
        assert_eq!(ansi_indexed(231), egui::Color32::from_rgb(255, 255, 255));
        // Greyscale ramp stays grey (r == g == b).
        let g = ansi_indexed(240);
        assert_eq!(g.r(), g.g());
        assert_eq!(g.g(), g.b());
    }
}
