use std::ffi::OsStr;
use std::io::{self, IsTerminal, Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::process::{Child, Command, Stdio};

pub mod cursor;
pub mod linebuf;
pub mod panic;
pub mod parse;
pub mod raw;
pub mod run;

use linebuf::LineBuf;
use raw::*;

use crate::cursor::{Direction, move_cursor};

macro_rules! print {
    ($($x:tt)*) => {{
        write!(io::stdout(), $($x)*).unwrap();
        io::stdout().flush().unwrap();
    }}
}

macro_rules! println {
    () => {{
        println!("")
    }};
    ($($x:tt)*) => {{
        write!(io::stdout(), $($x)*).unwrap();
        write!(io::stdout(), "\r\n").unwrap();
        io::stdout().flush().unwrap();
    }};
}

const PROMPT: &str = "> ";

fn completely_clear_screen() {
    print!("\x1B[2J\x1B[1;1H");
}

fn clear_screen() {
    completely_clear_screen();
    print!("{PROMPT}")
}

type BString = Vec<u8>;
#[allow(non_camel_case_types)]
type bstr = [u8];

pub struct Session {
    raw: ScopedRawMode,
    line: LineBuf,
    history: Vec<BString>,
}

fn event_loop() {
    let stdin = io::stdin();
    let stdout = io::stdout();

    let fd = stdin.as_raw_fd();
    let raw = ScopedRawMode::on_fd(fd);
    raw.enable();

    let mut se = Session {
        raw,
        line: LineBuf::new(),
        history: Vec::new(),
    };

    print!("{PROMPT}");

    loop {
        let mut buf = [0u8; 1];

        let Ok(_) = stdin.lock().read_exact(&mut buf) else {
            break;
        };

        match buf[0] {
            // EOF
            4 => {
                break;
            }

            // Ctrl+L
            12 => clear_screen(),

            // Ctrl+R
            18 => {}

            // Enter
            b'\r' => {
                let line = se.line.dump();
                if !line.is_empty() {
                    print!("\r\n");
                    se.history.push(line.clone());
                    run::run(&se, line);
                }
            }

            // Backspace (127 on most systems)
            127 => {
                if se.line.is_empty() && !se.line.is_dirty() && !se.history.is_empty() {
                    // take previous command for editing
                    let cmd = se.history[se.history.len() - 1].clone();
                    io::stdout().write_all(&cmd).unwrap();
                    io::stdout().flush().unwrap();
                    se.line.set_content(cmd);
                } else if se.line.del_left().is_some() {
                    print!("\x08 \x08");
                }
            }

            b'\t' => {
                todo!()
            }

            // Escape sequence
            27 => {
                let mut seq = [0u8; 2];
                stdin.lock().read_exact(&mut seq).unwrap();

                if seq[0] == b'[' {
                    match seq[1] {
                        b'A' => {
                            // up
                        }
                        b'B' => {
                            // down
                        }
                        b'C' => {
                            move_cursor(Direction::Right, 1);
                            se.line.right();
                        }
                        b'D' => {
                            move_cursor(Direction::Left, 1);
                            se.line.left();
                        }
                        x => todo!("escape character {x}"),
                    }
                }
            }

            b'|' if se.line.is_empty() && !se.history.is_empty() => {
                let mut cmd = se.history[se.history.len() - 1].clone();
                cmd.extend_from_slice(b" | ");
                io::stdout().write_all(&cmd).unwrap();
                io::stdout().flush().unwrap();
                se.line.set_content(cmd);
            }

            // Normal character
            x => {
                se.line.add(x);
                stdout.lock().write_all(&[x]).unwrap();
                se.line.display_post();
            }
        }
    }

    se.raw.disable();
}

fn main() {
    if !io::stdin().is_terminal() {
        println!("need to run in a tty");
        return;
    }

    crate::panic::hook();

    // it is quite annoying when the terminal window closes due to a crash, so let's just catch all panics
    loop {
        let res = std::panic::catch_unwind(event_loop);
        match res {
            Ok(_) => break,
            Err(_) => continue,
        }
    }

    println!("bye");
}
