// e gui emo wrapper for the hoardom tui
// spawns hoardom --tui in a pty and renders it in its own window
// so it shows up with its own icon in the dock (mac) or taskbar (linux) for people that want that
//
// built with: cargo build --features gui

use eframe::egui::{self, Color32, FontId, Rect, Sense};
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
use vte::{Params, Perform};

use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

// thx gemma for the formated section comments i didnt want... thats not what I wanted when i said sanetize my comments from swearing and german language.
// i mean it is prettier than my usual way, will check otherfiles to see if can atleast make it somewhat consistant.

// ---- constants ----

const FONT_SIZE: f32 = 14.0;
const DEFAULT_COLS: u16 = 120;
const DEFAULT_ROWS: u16 = 35;

const DEFAULT_FG: Color32 = Color32::from_rgb(204, 204, 204);
const DEFAULT_BG: Color32 = Color32::from_rgb(24, 24, 24);

// ----- terminal colors -----

#[derive(Clone, Copy, PartialEq)]
enum TermColor {
    Default,
    Indexed(u8),
    Rgb(u8, u8, u8),
}


// ai made this fn because i gave up on trying.
fn ansi_color(idx: u8) -> Color32 {
    match idx {
        0 => Color32::from_rgb(0, 0, 0),
        1 => Color32::from_rgb(170, 0, 0),
        2 => Color32::from_rgb(0, 170, 0),
        3 => Color32::from_rgb(170, 85, 0),
        4 => Color32::from_rgb(0, 0, 170),
        5 => Color32::from_rgb(170, 0, 170),
        6 => Color32::from_rgb(0, 170, 170),
        7 => Color32::from_rgb(170, 170, 170),
        8 => Color32::from_rgb(85, 85, 85),
        9 => Color32::from_rgb(255, 85, 85),
        10 => Color32::from_rgb(85, 255, 85),
        11 => Color32::from_rgb(255, 255, 85),
        12 => Color32::from_rgb(85, 85, 255),
        13 => Color32::from_rgb(255, 85, 255),
        14 => Color32::from_rgb(85, 255, 255),
        15 => Color32::from_rgb(255, 255, 255),
        // 6x6x6 color cube
        16..=231 => {
            let idx = (idx - 16) as u16;
            let ri = idx / 36;
            let gi = (idx % 36) / 6;
            let bi = idx % 6;
            let v = |i: u16| -> u8 {
                if i == 0 {
                    0
                } else {
                    55 + i as u8 * 40
                }
            };
            Color32::from_rgb(v(ri), v(gi), v(bi))
        }
        // grayscale ramp
        232..=255 => {
            let g = 8 + (idx - 232) * 10;
            Color32::from_rgb(g, g, g)
        }
    }
}

fn resolve_color(c: TermColor, is_fg: bool) -> Color32 {
    match c {
        TermColor::Default => {
            if is_fg {
                DEFAULT_FG
            } else {
                DEFAULT_BG
            }
        }
        TermColor::Indexed(i) => ansi_color(i),
        TermColor::Rgb(r, g, b) => Color32::from_rgb(r, g, b),
    }
}

// ----- terminal cell -----

#[derive(Clone, Copy)]
struct Cell {
    ch: char,
    fg: TermColor,
    bg: TermColor,
    bold: bool,
    reverse: bool,
}

impl Default for Cell {
    fn default() -> Self {
        Cell {
            ch: ' ',
            fg: TermColor::Default,
            bg: TermColor::Default,
            bold: false,
            reverse: false,
        }
    }
}

impl Cell {
    fn resolved_fg(&self) -> Color32 {
        if self.reverse {
            resolve_color(self.bg, false)
        } else {
            let c = resolve_color(self.fg, true);
            if self.bold {
                // brighten bold text a bit
                let [r, g, b, a] = c.to_array();
                Color32::from_rgba_premultiplied(
                    r.saturating_add(40),
                    g.saturating_add(40),
                    b.saturating_add(40),
                    a,
                )
            } else {
                c
            }
        }
    }

    fn resolved_bg(&self) -> Color32 {
        if self.reverse {
            resolve_color(self.fg, true)
        } else {
            resolve_color(self.bg, false)
        }
    }
}

// ----- terminal grid -----


// contains quiet a few ai solved bugfixes they seem ... fineish... to me and work
struct TermGrid {
    cells: Vec<Vec<Cell>>,
    rows: usize,
    cols: usize,
    cursor_row: usize,
    cursor_col: usize,
    cursor_visible: bool,
    scroll_top: usize,
    scroll_bottom: usize,

    // current drawing attributes
    attr_fg: TermColor,
    attr_bg: TermColor,
    attr_bold: bool,
    attr_reverse: bool,

    // saved cursor
    saved_cursor: Option<(usize, usize)>,

    // alternate screen buffer
    alt_saved: Option<(Vec<Vec<Cell>>, usize, usize)>,

    // mouse tracking modes
    mouse_normal: bool, // ?1000 - normal tracking (clicks)
    mouse_button: bool, // ?1002 - button-event tracking (drag)
    mouse_any: bool,    // ?1003 - any-event tracking (all motion)
    mouse_sgr: bool,    // ?1006 - SGR extended coordinates
}

// i partially stole this from somewhere i forgot from where though.
impl TermGrid {
    fn new(rows: usize, cols: usize) -> Self {
        TermGrid {
            cells: vec![vec![Cell::default(); cols]; rows],
            rows,
            cols,
            cursor_row: 0,
            cursor_col: 0,
            cursor_visible: true,
            scroll_top: 0,
            scroll_bottom: rows,
            attr_fg: TermColor::Default,
            attr_bg: TermColor::Default,
            attr_bold: false,
            attr_reverse: false,
            saved_cursor: None,
            alt_saved: None,
            mouse_normal: false,
            mouse_button: false,
            mouse_any: false,
            mouse_sgr: false,
        }
    }

    fn mouse_enabled(&self) -> bool {
        self.mouse_normal || self.mouse_button || self.mouse_any
    }

    fn resize(&mut self, new_rows: usize, new_cols: usize) {
        if new_rows == self.rows && new_cols == self.cols {
            return;
        }
        for row in &mut self.cells {
            row.resize(new_cols, Cell::default());
        }
        while self.cells.len() < new_rows {
            self.cells.push(vec![Cell::default(); new_cols]);
        }
        self.cells.truncate(new_rows);
        self.rows = new_rows;
        self.cols = new_cols;
        self.scroll_top = 0;
        self.scroll_bottom = new_rows;
        self.cursor_row = self.cursor_row.min(new_rows.saturating_sub(1));
        self.cursor_col = self.cursor_col.min(new_cols.saturating_sub(1));
    }

    fn reset_attrs(&mut self) {
        self.attr_fg = TermColor::Default;
        self.attr_bg = TermColor::Default;
        self.attr_bold = false;
        self.attr_reverse = false;
    }

    fn put_char(&mut self, c: char) {
        if self.cursor_col >= self.cols {
            self.cursor_col = 0;
            self.line_feed();
        }
        if self.cursor_row < self.rows && self.cursor_col < self.cols {
            self.cells[self.cursor_row][self.cursor_col] = Cell {
                ch: c,
                fg: self.attr_fg,
                bg: self.attr_bg,
                bold: self.attr_bold,
                reverse: self.attr_reverse,
            };
        }
        self.cursor_col += 1;
    }

    fn line_feed(&mut self) {
        if self.cursor_row + 1 >= self.scroll_bottom {
            self.scroll_up();
        } else {
            self.cursor_row += 1;
        }
    }

    fn scroll_up(&mut self) {
        if self.scroll_top < self.scroll_bottom && self.scroll_bottom <= self.rows {
            self.cells.remove(self.scroll_top);
            self.cells
                .insert(self.scroll_bottom - 1, vec![Cell::default(); self.cols]);
        }
    }

    fn scroll_down(&mut self) {
        if self.scroll_top < self.scroll_bottom && self.scroll_bottom <= self.rows {
            self.cells.remove(self.scroll_bottom - 1);
            self.cells
                .insert(self.scroll_top, vec![Cell::default(); self.cols]);
        }
    }

    fn erase_display(&mut self, mode: u16) {
        match mode {
            0 => {
                // cursor to end
                for c in self.cursor_col..self.cols {
                    self.cells[self.cursor_row][c] = Cell::default();
                }
                for r in (self.cursor_row + 1)..self.rows {
                    for c in 0..self.cols {
                        self.cells[r][c] = Cell::default();
                    }
                }
            }
            1 => {
                // start to cursor
                for r in 0..self.cursor_row {
                    for c in 0..self.cols {
                        self.cells[r][c] = Cell::default();
                    }
                }
                for c in 0..=self.cursor_col.min(self.cols.saturating_sub(1)) {
                    self.cells[self.cursor_row][c] = Cell::default();
                }
            }
            2 | 3 => {
                // whole screen
                for r in 0..self.rows {
                    for c in 0..self.cols {
                        self.cells[r][c] = Cell::default();
                    }
                }
            }
            _ => {}
        }
    }

    fn erase_line(&mut self, mode: u16) {
        let row = self.cursor_row;
        if row >= self.rows {
            return;
        }
        match mode {
            0 => {
                for c in self.cursor_col..self.cols {
                    self.cells[row][c] = Cell::default();
                }
            }
            1 => {
                for c in 0..=self.cursor_col.min(self.cols.saturating_sub(1)) {
                    self.cells[row][c] = Cell::default();
                }
            }
            2 => {
                for c in 0..self.cols {
                    self.cells[row][c] = Cell::default();
                }
            }
            _ => {}
        }
    }

    fn erase_chars(&mut self, n: usize) {
        let row = self.cursor_row;
        if row >= self.rows {
            return;
        }
        for i in 0..n {
            let c = self.cursor_col + i;
            if c < self.cols {
                self.cells[row][c] = Cell::default();
            }
        }
    }

    fn delete_chars(&mut self, n: usize) {
        let row = self.cursor_row;
        if row >= self.rows {
            return;
        }
        for _ in 0..n {
            if self.cursor_col < self.cols {
                self.cells[row].remove(self.cursor_col);
                self.cells[row].push(Cell::default());
            }
        }
    }

    fn insert_chars(&mut self, n: usize) {
        let row = self.cursor_row;
        if row >= self.rows {
            return;
        }
        for _ in 0..n {
            if self.cursor_col < self.cols {
                self.cells[row].insert(self.cursor_col, Cell::default());
                self.cells[row].truncate(self.cols);
            }
        }
    }

    fn insert_lines(&mut self, n: usize) {
        for _ in 0..n {
            if self.cursor_row < self.scroll_bottom {
                if self.scroll_bottom <= self.rows {
                    self.cells.remove(self.scroll_bottom - 1);
                }
                self.cells
                    .insert(self.cursor_row, vec![Cell::default(); self.cols]);
            }
        }
    }

    fn delete_lines(&mut self, n: usize) {
        for _ in 0..n {
            if self.cursor_row < self.scroll_bottom && self.cursor_row < self.rows {
                self.cells.remove(self.cursor_row);
                let insert_at = (self.scroll_bottom - 1).min(self.cells.len());
                self.cells
                    .insert(insert_at, vec![Cell::default(); self.cols]);
            }
        }
    }

    fn enter_alt_screen(&mut self) {
        self.alt_saved = Some((self.cells.clone(), self.cursor_row, self.cursor_col));
        self.erase_display(2);
        self.cursor_row = 0;
        self.cursor_col = 0;
    }

    fn leave_alt_screen(&mut self) {
        if let Some((cells, row, col)) = self.alt_saved.take() {
            self.cells = cells;
            self.cursor_row = row;
            self.cursor_col = col;
        }
    }

    // slopfix : SGR , set graphics rendition (colors and attributes)
    fn sgr(&mut self, params: &[u16]) {
        if params.is_empty() {
            self.reset_attrs();
            return;
        }
        let mut i = 0;
        while i < params.len() {
            match params[i] {
                0 => self.reset_attrs(),
                1 => self.attr_bold = true,
                7 => self.attr_reverse = true,
                22 => self.attr_bold = false,
                27 => self.attr_reverse = false,
                30..=37 => self.attr_fg = TermColor::Indexed(params[i] as u8 - 30),
                38 => {
                    // extended fg color
                    if i + 2 < params.len() && params[i + 1] == 5 {
                        self.attr_fg = TermColor::Indexed(params[i + 2] as u8);
                        i += 2;
                    } else if i + 4 < params.len() && params[i + 1] == 2 {
                        self.attr_fg = TermColor::Rgb(
                            params[i + 2] as u8,
                            params[i + 3] as u8,
                            params[i + 4] as u8,
                        );
                        i += 4;
                    }
                }
                39 => self.attr_fg = TermColor::Default,
                40..=47 => self.attr_bg = TermColor::Indexed(params[i] as u8 - 40),
                48 => {
                    // extended bg color
                    if i + 2 < params.len() && params[i + 1] == 5 {
                        self.attr_bg = TermColor::Indexed(params[i + 2] as u8);
                        i += 2;
                    } else if i + 4 < params.len() && params[i + 1] == 2 {
                        self.attr_bg = TermColor::Rgb(
                            params[i + 2] as u8,
                            params[i + 3] as u8,
                            params[i + 4] as u8,
                        );
                        i += 4;
                    }
                }
                49 => self.attr_bg = TermColor::Default,
                90..=97 => self.attr_fg = TermColor::Indexed(params[i] as u8 - 90 + 8),
                100..=107 => self.attr_bg = TermColor::Indexed(params[i] as u8 - 100 + 8),
                _ => {}
            }
            i += 1;
        }
    }

    fn handle_csi(&mut self, params: &[u16], intermediates: &[u8], action: char) {
        // helper: get param with default
        let p = |i: usize, def: u16| -> u16 {
            params.get(i).copied().filter(|&v| v > 0).unwrap_or(def)
        };

        let private = intermediates.contains(&b'?');

        match action {
            'A' => {
                let n = p(0, 1) as usize;
                self.cursor_row = self.cursor_row.saturating_sub(n);
            }
            'B' => {
                let n = p(0, 1) as usize;
                self.cursor_row = (self.cursor_row + n).min(self.rows.saturating_sub(1));
            }
            'C' => {
                let n = p(0, 1) as usize;
                self.cursor_col = (self.cursor_col + n).min(self.cols.saturating_sub(1));
            }
            'D' => {
                let n = p(0, 1) as usize;
                self.cursor_col = self.cursor_col.saturating_sub(n);
            }
            'H' | 'f' => {
                // cursor position (1-based)
                let row = p(0, 1) as usize;
                let col = p(1, 1) as usize;
                self.cursor_row = row.saturating_sub(1).min(self.rows.saturating_sub(1));
                self.cursor_col = col.saturating_sub(1).min(self.cols.saturating_sub(1));
            }
            'J' => self.erase_display(p(0, 0)),
            'K' => self.erase_line(p(0, 0)),
            'L' => self.insert_lines(p(0, 1) as usize),
            'M' => self.delete_lines(p(0, 1) as usize),
            'P' => self.delete_chars(p(0, 1) as usize),
            'X' => self.erase_chars(p(0, 1) as usize),
            '@' => self.insert_chars(p(0, 1) as usize),
            'G' | '`' => {
                let col = p(0, 1) as usize;
                self.cursor_col = col.saturating_sub(1).min(self.cols.saturating_sub(1));
            }
            'd' => {
                let row = p(0, 1) as usize;
                self.cursor_row = row.saturating_sub(1).min(self.rows.saturating_sub(1));
            }
            'S' => {
                for _ in 0..p(0, 1) {
                    self.scroll_up();
                }
            }
            'T' => {
                for _ in 0..p(0, 1) {
                    self.scroll_down();
                }
            }
            'm' => {
                if params.is_empty() {
                    self.sgr(&[0]);
                } else {
                    self.sgr(params);
                }
            }
            'r' => {
                let top = p(0, 1) as usize;
                let bottom = p(1, self.rows as u16) as usize;
                self.scroll_top = top.saturating_sub(1);
                self.scroll_bottom = bottom.min(self.rows);
            }
            's' => {
                self.saved_cursor = Some((self.cursor_row, self.cursor_col));
            }
            'u' => {
                if let Some((r, c)) = self.saved_cursor {
                    self.cursor_row = r.min(self.rows.saturating_sub(1));
                    self.cursor_col = c.min(self.cols.saturating_sub(1));
                }
            }
            'h' if private => {
                for &param in params {
                    match param {
                        25 => self.cursor_visible = true,
                        1000 => self.mouse_normal = true,
                        1002 => self.mouse_button = true,
                        1003 => self.mouse_any = true,
                        1006 => self.mouse_sgr = true,
                        1049 => self.enter_alt_screen(),
                        _ => {}
                    }
                }
            }
            'l' if private => {
                for &param in params {
                    match param {
                        25 => self.cursor_visible = false,
                        1000 => self.mouse_normal = false,
                        1002 => self.mouse_button = false,
                        1003 => self.mouse_any = false,
                        1006 => self.mouse_sgr = false,
                        1049 => self.leave_alt_screen(),
                        _ => {}
                    }
                }
            }
            _ => {}
        }
    }
}

// ----- vte perform implementation -----

impl Perform for TermGrid {
    fn print(&mut self, c: char) {
        self.put_char(c);
    }

    fn execute(&mut self, byte: u8) {
        match byte {
            0x08 => {
                // backspace
                self.cursor_col = self.cursor_col.saturating_sub(1);
            }
            0x09 => {
                // tab - next tab stop (every 8 cols)
                self.cursor_col = ((self.cursor_col / 8) + 1) * 8;
                if self.cursor_col >= self.cols {
                    self.cursor_col = self.cols.saturating_sub(1);
                }
            }
            0x0A | 0x0B | 0x0C => {
                // line feed
                self.line_feed();
            }
            0x0D => {
                // carriage return
                self.cursor_col = 0;
            }
            _ => {}
        }
    }

    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) {
        if ignore {
            return;
        }
        let flat: Vec<u16> = params.iter().map(|sub| sub[0]).collect();
        self.handle_csi(&flat, intermediates, action);
    }

    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
        if !intermediates.is_empty() {
            return;
        }
        match byte {
            b'7' => {
                self.saved_cursor = Some((self.cursor_row, self.cursor_col));
            }
            b'8' => {
                if let Some((r, c)) = self.saved_cursor {
                    self.cursor_row = r.min(self.rows.saturating_sub(1));
                    self.cursor_col = c.min(self.cols.saturating_sub(1));
                }
            }
            b'D' => self.line_feed(),
            b'M' => {
                // reverse index
                if self.cursor_row == self.scroll_top {
                    self.scroll_down();
                } else {
                    self.cursor_row = self.cursor_row.saturating_sub(1);
                }
            }
            _ => {}
        }
    }

    fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {}
    fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {}
    fn put(&mut self, _byte: u8) {}
    fn unhook(&mut self) {}
}

// ----- keyboard input mapping -----

// map egui keys to terminal escape sequences
// yk because maybe i want to recycle this file for some other project in the future hence why i implemented it
fn special_key_bytes(key: &egui::Key, modifiers: &egui::Modifiers) -> Option<Vec<u8>> {
    use egui::Key;
    match key {
        Key::ArrowUp => Some(b"\x1b[A".to_vec()),
        Key::ArrowDown => Some(b"\x1b[B".to_vec()),
        Key::ArrowRight => Some(b"\x1b[C".to_vec()),
        Key::ArrowLeft => Some(b"\x1b[D".to_vec()),
        Key::Home => Some(b"\x1b[H".to_vec()),
        Key::End => Some(b"\x1b[F".to_vec()),
        Key::PageUp => Some(b"\x1b[5~".to_vec()),
        Key::PageDown => Some(b"\x1b[6~".to_vec()),
        Key::Insert => Some(b"\x1b[2~".to_vec()),
        Key::Delete => Some(b"\x1b[3~".to_vec()),
        Key::Escape => Some(b"\x1b".to_vec()),
        Key::Tab => {
            if modifiers.shift {
                Some(b"\x1b[Z".to_vec())
            } else {
                Some(b"\x09".to_vec())
            }
        }
        Key::Backspace => Some(b"\x7f".to_vec()),
        Key::Enter => Some(b"\x0d".to_vec()),
        Key::F1 => Some(b"\x1bOP".to_vec()),
        Key::F2 => Some(b"\x1bOQ".to_vec()),
        Key::F3 => Some(b"\x1bOR".to_vec()),
        Key::F4 => Some(b"\x1bOS".to_vec()),
        Key::F5 => Some(b"\x1b[15~".to_vec()),
        Key::F6 => Some(b"\x1b[17~".to_vec()),
        Key::F7 => Some(b"\x1b[18~".to_vec()),
        Key::F8 => Some(b"\x1b[19~".to_vec()),
        Key::F9 => Some(b"\x1b[20~".to_vec()),
        Key::F10 => Some(b"\x1b[21~".to_vec()),
        Key::F11 => Some(b"\x1b[23~".to_vec()),
        Key::F12 => Some(b"\x1b[24~".to_vec()),
        _ => None,
    }
}

// ctrl+letter -> control character byte
fn ctrl_key_byte(key: &egui::Key) -> Option<u8> {
    use egui::Key;
    match key {
        Key::A => Some(0x01),
        Key::B => Some(0x02),
        Key::C => Some(0x03),
        Key::D => Some(0x04),
        Key::E => Some(0x05),
        Key::F => Some(0x06),
        Key::G => Some(0x07),
        Key::H => Some(0x08),
        Key::I => Some(0x09),
        Key::J => Some(0x0A),
        Key::K => Some(0x0B),
        Key::L => Some(0x0C),
        Key::M => Some(0x0D),
        Key::N => Some(0x0E),
        Key::O => Some(0x0F),
        Key::P => Some(0x10),
        Key::Q => Some(0x11),
        Key::R => Some(0x12),
        Key::S => Some(0x13),
        Key::T => Some(0x14),
        Key::U => Some(0x15),
        Key::V => Some(0x16),
        Key::W => Some(0x17),
        Key::X => Some(0x18),
        Key::Y => Some(0x19),
        Key::Z => Some(0x1A),
        _ => None,
    }
}

// ----- the egui app -----

struct HoardomApp {
    grid: Arc<Mutex<TermGrid>>,
    pty_writer: Mutex<Box<dyn Write + Send>>,
    pty_master: Box<dyn portable_pty::MasterPty + Send>,
    child_exited: Arc<AtomicBool>,
    cell_width: f32,
    cell_height: f32,
    current_cols: u16,
    current_rows: u16,
    last_mouse_button: Option<u8>, // track held mouse button for drag/release
}

impl eframe::App for HoardomApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        // bail if the child process is gone
        if self.child_exited.load(Ordering::Relaxed) {
            ctx.send_viewport_cmd(egui::ViewportCommand::Close);
            return;
        }

        // measure cell dimensions on first frame (cant do it in creation callback)
        if self.cell_width == 0.0 {
            let (cw, ch) = ctx.fonts(|f| {
                let fid = FontId::monospace(FONT_SIZE);
                let galley = f.layout_no_wrap("M".into(), fid.clone(), DEFAULT_FG);
                let row_h = f.row_height(&fid);
                (galley.rect.width(), row_h)
            });
            self.cell_width = cw;
            self.cell_height = ch;
        }

        // handle keyboard input
        ctx.input(|input| {
            for event in &input.events {
                match event {
                    egui::Event::Text(text) => {
                        // only pass printable chars (specials handled via Key events)
                        let filtered: String = text.chars().filter(|c| !c.is_control()).collect();
                        if !filtered.is_empty() {
                            if let Ok(mut w) = self.pty_writer.lock() {
                                let _ = w.write_all(filtered.as_bytes());
                            }
                        }
                    }
                    egui::Event::Key {
                        key,
                        pressed: true,
                        modifiers,
                        ..
                    } => {
                        if modifiers.ctrl || modifiers.mac_cmd {
                            if let Some(byte) = ctrl_key_byte(key) {
                                if let Ok(mut w) = self.pty_writer.lock() {
                                    let _ = w.write_all(&[byte]);
                                }
                            }
                        } else if let Some(bytes) = special_key_bytes(key, modifiers) {
                            if let Ok(mut w) = self.pty_writer.lock() {
                                let _ = w.write_all(&bytes);
                            }
                        }
                    }
                    _ => {}
                }
            }
        });

        // handle mouse input
        self.handle_mouse(ctx);

        // check if window was resized, update pty dimensions
        let avail = ctx.available_rect();
        if self.cell_width > 0.0 && self.cell_height > 0.0 {
            let new_cols = (avail.width() / self.cell_width).floor() as u16;
            let new_rows = (avail.height() / self.cell_height).floor() as u16;
            let new_cols = new_cols.max(20);
            let new_rows = new_rows.max(10);

            if new_cols != self.current_cols || new_rows != self.current_rows {
                self.current_cols = new_cols;
                self.current_rows = new_rows;
                let _ = self.pty_master.resize(PtySize {
                    rows: new_rows,
                    cols: new_cols,
                    pixel_width: 0,
                    pixel_height: 0,
                });
                if let Ok(mut grid) = self.grid.lock() {
                    grid.resize(new_rows as usize, new_cols as usize);
                }
            }
        }

        // render the terminal grid
        egui::CentralPanel::default()
            .frame(egui::Frame::default().fill(DEFAULT_BG))
            .show(ctx, |ui| {
                self.render_grid(ui);
            });

        ctx.request_repaint_after(Duration::from_millis(16));
    }
}

impl HoardomApp {
    // translate egui pointer events to terminal mouse sequences
    fn handle_mouse(&mut self, ctx: &egui::Context) {
        let (mouse_enabled, use_sgr) = {
            match self.grid.lock() {
                Ok(g) => (g.mouse_enabled(), g.mouse_sgr),
                Err(_) => return,
            }
        };
        if !mouse_enabled {
            return;
        }

        let cw = self.cell_width;
        let ch = self.cell_height;
        if cw <= 0.0 || ch <= 0.0 {
            return;
        }

        let avail = ctx.available_rect();

        ctx.input(|input| {
            if let Some(pos) = input.pointer.latest_pos() {
                let col = ((pos.x - avail.min.x) / cw).floor() as i32;
                let row = ((pos.y - avail.min.y) / ch).floor() as i32;
                let col = col.max(0) as u16;
                let row = row.max(0) as u16;

                // scroll events
                let scroll_y = input.raw_scroll_delta.y;
                if scroll_y != 0.0 {
                    let button: u8 = if scroll_y > 0.0 { 64 } else { 65 };
                    let seq = if use_sgr {
                        format!("\x1b[<{};{};{}M", button, col + 1, row + 1)
                    } else {
                        let cb = (button + 32) as char;
                        let cx = (col + 33).min(255) as u8 as char;
                        let cy = (row + 33).min(255) as u8 as char;
                        format!("\x1b[M{}{}{}", cb, cx, cy)
                    };
                    if let Ok(mut w) = self.pty_writer.lock() {
                        let _ = w.write_all(seq.as_bytes());
                    }
                }

                // button press
                if input.pointer.any_pressed() {
                    let button: u8 = if input.pointer.button_pressed(egui::PointerButton::Primary) {
                        0
                    } else if input.pointer.button_pressed(egui::PointerButton::Middle) {
                        1
                    } else if input.pointer.button_pressed(egui::PointerButton::Secondary) {
                        2
                    } else {
                        0
                    };
                    self.last_mouse_button = Some(button);
                    let seq = if use_sgr {
                        format!("\x1b[<{};{};{}M", button, col + 1, row + 1)
                    } else {
                        let cb = (button + 32) as char;
                        let cx = (col + 33).min(255) as u8 as char;
                        let cy = (row + 33).min(255) as u8 as char;
                        format!("\x1b[M{}{}{}", cb, cx, cy)
                    };
                    if let Ok(mut w) = self.pty_writer.lock() {
                        let _ = w.write_all(seq.as_bytes());
                    }
                }

                // button release
                if input.pointer.any_released() {
                    let button = self.last_mouse_button.unwrap_or(0);
                    self.last_mouse_button = None;
                    let seq = if use_sgr {
                        format!("\x1b[<{};{};{}m", button, col + 1, row + 1)
                    } else {
                        let cb = (3u8 + 32) as char; // release = button 3 in normal mode
                        let cx = (col + 33).min(255) as u8 as char;
                        let cy = (row + 33).min(255) as u8 as char;
                        format!("\x1b[M{}{}{}", cb, cx, cy)
                    };
                    if let Ok(mut w) = self.pty_writer.lock() {
                        let _ = w.write_all(seq.as_bytes());
                    }
                }

                // drag / motion
                if input.pointer.is_moving() && self.last_mouse_button.is_some() {
                    let button = self.last_mouse_button.unwrap_or(0) + 32; // motion flag
                    let seq = if use_sgr {
                        format!("\x1b[<{};{};{}M", button, col + 1, row + 1)
                    } else {
                        let cb = (button + 32) as char;
                        let cx = (col + 33).min(255) as u8 as char;
                        let cy = (row + 33).min(255) as u8 as char;
                        format!("\x1b[M{}{}{}", cb, cx, cy)
                    };
                    if let Ok(mut w) = self.pty_writer.lock() {
                        let _ = w.write_all(seq.as_bytes());
                    }
                }
            }
        });
    }

    fn render_grid(&self, ui: &mut egui::Ui) {
        let grid = match self.grid.lock() {
            Ok(g) => g,
            Err(_) => return,
        };

        let painter = ui.painter();
        let rect = ui.available_rect_before_wrap();
        let cw = self.cell_width;
        let ch = self.cell_height;

        // draw each row - render character by character at exact cell positions
        // to keep backgrounds and text perfectly aligned
        for row in 0..grid.rows {
            let y = rect.min.y + row as f32 * ch;

            // draw background spans (batch consecutive same-bg cells)
            let mut bg_start = 0usize;
            let mut current_bg = grid.cells[row][0].resolved_bg();

            for col in 0..=grid.cols {
                let cell_bg = if col < grid.cols {
                    grid.cells[row][col].resolved_bg()
                } else {
                    Color32::TRANSPARENT // sentinel to flush last span
                };

                if cell_bg != current_bg || col == grid.cols {
                    // draw the background span
                    if current_bg != DEFAULT_BG {
                        let x0 = rect.min.x + bg_start as f32 * cw;
                        let x1 = rect.min.x + col as f32 * cw;
                        painter.rect_filled(
                            Rect::from_min_max(egui::pos2(x0, y), egui::pos2(x1, y + ch)),
                            0.0,
                            current_bg,
                        );
                    }
                    bg_start = col;
                    current_bg = cell_bg;
                }
            }

            // draw text - render each cell at its exact x position
            // this prevents sub-pixel drift that causes bg/text misalignment
            for col in 0..grid.cols {
                let cell = &grid.cells[row][col];
                if cell.ch == ' ' || cell.ch == '\0' {
                    continue;
                }
                let x = rect.min.x + col as f32 * cw;
                let fg = cell.resolved_fg();
                let mut buf = [0u8; 4];
                let s = cell.ch.encode_utf8(&mut buf);
                painter.text(
                    egui::pos2(x, y),
                    egui::Align2::LEFT_TOP,
                    s,
                    FontId::monospace(FONT_SIZE),
                    fg,
                );
            }
        }

        // draw cursor
        if grid.cursor_visible && grid.cursor_row < grid.rows && grid.cursor_col < grid.cols {
            let cx = rect.min.x + grid.cursor_col as f32 * cw;
            let cy = rect.min.y + grid.cursor_row as f32 * ch;
            painter.rect_filled(
                Rect::from_min_size(egui::pos2(cx, cy), egui::vec2(cw, ch)),
                0.0,
                Color32::from_rgba_premultiplied(180, 180, 180, 100),
            );
        }

        // reserve the space so egui knows we used it
        ui.allocate_exact_size(
            egui::vec2(grid.cols as f32 * cw, grid.rows as f32 * ch),
            Sense::hover(),
        );
    }
}

// ----- find the hoardom binary -----

fn find_hoardom() -> PathBuf {
    // check same directory as ourselves
    if let Ok(exe) = std::env::current_exe() {
        if let Some(dir) = exe.parent() {
            // check for hoardom next to us
            let candidate = dir.join("hoardom");
            if candidate.exists() && candidate != exe {
                return candidate;
            }
            // in a mac .app bundle the binary might be named differently
            let candidate = dir.join("hoardom-bin");
            if candidate.exists() {
                return candidate;
            }
        }
    }
    // fall back to PATH
    PathBuf::from("hoardom")
}

// ----- load app icon -----

fn load_icon() -> egui::IconData {
    let png_bytes = include_bytes!("../dist/AppIcon.png");
    let img = image::load_from_memory_with_format(png_bytes, image::ImageFormat::Png)
        .expect("failed to decode embedded icon")
        .into_rgba8();
    let (w, h) = img.dimensions();
    egui::IconData {
        rgba: img.into_raw(),
        width: w,
        height: h,
    }
}

// ----- main -----

fn main() -> eframe::Result<()> {
    let hoardom_bin = find_hoardom();

    // setup pty
    let pty_system = native_pty_system();
    let pair = pty_system
        .openpty(PtySize {
            rows: DEFAULT_ROWS,
            cols: DEFAULT_COLS,
            pixel_width: 0,
            pixel_height: 0,
        })
        .expect("failed to open pty");

    // spawn hoardom --tui in the pty
    let mut cmd = CommandBuilder::new(&hoardom_bin);
    cmd.arg("--tui");
    cmd.env("TERM", "xterm-256color");

    let mut child = pair
        .slave
        .spawn_command(cmd)
        .unwrap_or_else(|e| panic!("failed to spawn {:?}: {}", hoardom_bin, e));

    // close the slave end in the parent so pty gets proper eof
    drop(pair.slave);

    let reader = pair
        .master
        .try_clone_reader()
        .expect("failed to clone pty reader");
    let writer = pair
        .master
        .take_writer()
        .expect("failed to take pty writer");

    let grid = Arc::new(Mutex::new(TermGrid::new(
        DEFAULT_ROWS as usize,
        DEFAULT_COLS as usize,
    )));
    let child_exited = Arc::new(AtomicBool::new(false));

    // egui context holder so the reader thread can request repaints
    let ctx_holder: Arc<Mutex<Option<egui::Context>>> = Arc::new(Mutex::new(None));

    // reader thread: reads pty output and feeds it through the vt parser
    let grid_clone = grid.clone();
    let exited_clone = child_exited.clone();
    let ctx_clone = ctx_holder.clone();
    thread::spawn(move || {
        let mut parser = vte::Parser::new();
        let mut reader = reader;
        let mut buf = [0u8; 8192];
        loop {
            match reader.read(&mut buf) {
                Ok(0) | Err(_) => {
                    exited_clone.store(true, Ordering::Relaxed);
                    if let Ok(lock) = ctx_clone.lock() {
                        if let Some(ctx) = lock.as_ref() {
                            ctx.request_repaint();
                        }
                    }
                    break;
                }
                Ok(n) => {
                    if let Ok(mut g) = grid_clone.lock() {
                        parser.advance(&mut *g, &buf[..n]);
                    }
                    if let Ok(lock) = ctx_clone.lock() {
                        if let Some(ctx) = lock.as_ref() {
                            ctx.request_repaint();
                        }
                    }
                }
            }
        }
    });

    // child reaper thread
    let exited_clone2 = child_exited.clone();
    thread::spawn(move || {
        let _ = child.wait();
        exited_clone2.store(true, Ordering::Relaxed);
    });

    // calculate initial window size from cell dimensions
    // (rough estimate, refined on first frame)
    let est_width = DEFAULT_COLS as f32 * 8.5 + 20.0;
    let est_height = DEFAULT_ROWS as f32 * 18.0 + 20.0;

    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_title("hoardom")
            .with_inner_size([est_width, est_height])
            .with_min_inner_size([300.0, 200.0])
            .with_icon(load_icon()),
        ..Default::default()
    };

    eframe::run_native(
        "hoardom",
        options,
        Box::new(move |cc| {
            // store the egui context for the reader thread
            if let Ok(mut holder) = ctx_holder.lock() {
                *holder = Some(cc.egui_ctx.clone());
            }

            cc.egui_ctx.set_visuals(egui::Visuals::dark());

            // font fallback chain for monospace: Hack (default) -> NotoSansMono -> NotoSansSymbols2
            // Hack is missing box drawing, block elements, ellipsis
            // NotoSansMono covers those but is missing dingbats (symbols)
            // NotoSansSymbols2-subset has just those two glyphs from which only the necessary ones are extracted afaik
            let mut fonts = egui::FontDefinitions::default();
            fonts.font_data.insert(
                "NotoSansMono".to_owned(),
                std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
                    "../dist/NotoSansMono-Regular.ttf"
                ))),
            );
            fonts.font_data.insert(
                "NotoSansSymbols2".to_owned(),
                std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
                    "../dist/NotoSansSymbols2-subset.ttf"
                ))),
            );
            let mono = fonts
                .families
                .entry(egui::FontFamily::Monospace)
                .or_default();
            mono.push("NotoSansMono".to_owned());
            mono.push("NotoSansSymbols2".to_owned());
            cc.egui_ctx.set_fonts(fonts);

            Ok(Box::new(HoardomApp {
                grid,
                pty_writer: Mutex::new(writer),
                pty_master: pair.master,
                child_exited,
                cell_width: 0.0, // measured on first frame
                cell_height: 0.0,
                current_cols: DEFAULT_COLS,
                current_rows: DEFAULT_ROWS,
                last_mouse_button: None,
            }))
        }),
    )
}
