#![feature(unix_socket_ancillary_data, peer_credentials_unix_socket)]
#![allow(clippy::needless_range_loop)]

use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
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::Command;
use std::sync::{Arc, Mutex};
use std::thread::sleep;
use std::time::Duration;

pub mod basedir;
pub mod completion;
pub mod ctrlc;
pub mod cursor;
pub mod date;
pub mod defer;
pub mod export_fun;
pub mod history;
pub mod linebuf;
pub mod panic;
pub mod parse;
pub mod raw;
pub mod reload;
pub mod run;
pub mod rw;
pub mod serialization;
pub mod wait;

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

use crate::completion::{PathCache, completion};
use crate::ctrlc::CtrlC;
use crate::cursor::{Direction, move_cursor};
use crate::history::HistoryEntry;
use crate::parse::Block;

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

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

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

fn clear_screen() {
    completely_clear_screen();
}

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

pub struct Session {
    raw: ScopedRawMode,
    line: LineBuf,
    history: Vec<HistoryEntry>,
    prev_path: BString,
    builtins: HashMap<BString, &'static dyn run::Builtin>,
    vars: HashMap<BString, BString>,
    funs: HashMap<BString, Block>,
    socket_running: Option<export_fun::SocketRunning>,
    path_cache: PathCache,
    ctrlc: CtrlC,

    /// n before end of history.len()
    /// 0 == not checking history
    history_visit: usize,
}

/// relative path -- in case it is a proper subpath the result starts with a slash `/`
fn relative_path(root: &Path, target: &Path) -> Option<String> {
    let root = root.to_string_lossy();
    let mut target = target.to_string_lossy().to_string();
    if !target.ends_with("/") {
        target += "/";
    }
    if let Some(("", leaf)) = target.split_once(&*root) {
        Some(leaf.into())
    } else {
        None
    }
}

impl Session {
    fn pretty_cwd_res(&self) -> io::Result<String> {
        let dir = std::env::current_dir()?;
        let mut s = if let Some(home_dir) = std::env::home_dir() {
            if let Some(rela) = relative_path(&home_dir, &dir) {
                format!("~{rela}")
            } else {
                dir.to_string_lossy().to_string()
            }
        } else {
            dir.to_string_lossy().to_string()
        };
        while s.ends_with("/") && s.len() > 1 {
            s.remove(s.len() - 1);
        }
        Ok(s)
    }

    fn pretty_cwd(&self) -> String {
        self.pretty_cwd_res().unwrap_or_else(|_| String::new())
    }

    fn prompt(&self) -> String {
        format!("[{}]# ", self.pretty_cwd())
    }

    fn clear_prompt(&mut self) {
        cursor::move_cursor(Direction::Right, self.line.distance_from_right_end());
        for _ in 0..self.line.len() {
            print!("\x08 \x08");
        }
        io::stdout().lock().flush().unwrap();
        self.line.clear();
    }

    fn reprint_prompt(&self) {
        print!("{}", self.prompt());
        self.line.display_pre();
        self.line.display_post(b"");
    }

    fn display_historic_entry(&mut self) {
        self.clear_prompt();
        let new = if self.history_visit == 0 {
            Vec::new()
        } else {
            self.history[self.history.len() - self.history_visit]
                .cmd
                .clone()
        };
        io::stdout().write_all(&new).unwrap();
        io::stdout().flush().unwrap();
        self.line.set_content(new);
    }

    fn history_up(&mut self) {
        if self.history_visit < self.history.len() {
            self.history_visit += 1;
            self.display_historic_entry();
        }
    }

    fn history_down(&mut self) {
        if self.history_visit > 0 {
            self.history_visit -= 1;
            self.display_historic_entry();
        }
    }

    // move to next
    fn move_left_word(&mut self) {
        let mut i = 0;

        // find word
        while let Some(b' ') = self.line.get_left() {
            self.line.left();
            i += 1;
        }

        // skip it
        while let Some(x) = self.line.get_left()
            && !x.is_ascii_whitespace()
        {
            self.line.left();
            i += 1;
        }

        cursor::move_cursor(Direction::Left, i);
        io::stdout().flush().unwrap();
    }

    fn move_right_word(&mut self) {
        let mut i = 0;

        // find word
        while let Some(b' ') = self.line.get_right() {
            self.line.right();
            i += 1;
        }

        // skip it
        while let Some(x) = self.line.get_right()
            && !x.is_ascii_whitespace()
        {
            self.line.right();
            i += 1;
        }

        cursor::move_cursor(Direction::Right, i);
        io::stdout().flush().unwrap();
    }

    fn type_byte(&mut self, b: u8) {
        self.line.add(b);
        io::stdout().lock().write_all(&[b]).unwrap();
        self.line.display_post(b"");
    }

    fn type_bytes(&mut self, bs: &[u8]) {
        for b in bs.iter() {
            self.type_byte(*b);
        }
    }

    fn del_left(&mut self) {
        if self.line.del_left().is_some() {
            cursor::move_cursor(Direction::Left, 1);
            self.line.display_post(b" ");
        }
    }

    fn del_right(&mut self) {
        self.line.del_right();
        self.line.display_post(b" ");
    }

    fn move_to_begin(&mut self) {
        cursor::move_cursor(Direction::Left, self.line.all_left());
        io::stdout().flush().unwrap();
    }

    fn move_to_end(&mut self) {
        cursor::move_cursor(Direction::Right, self.line.all_right());
        io::stdout().flush().unwrap();
    }
}

fn read1() -> u8 {
    let mut buf = [0];
    io::stdin().lock().read_exact(&mut buf).unwrap();
    buf[0]
}

fn event_loop() {
    history::setup();

    let stdin = io::stdin();

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

    fs::create_dir_all(basedir::config_dir()).unwrap();
    fs::create_dir_all(basedir::data_dir()).unwrap();

    let se = Session {
        raw,
        line: LineBuf::new(),
        history: Vec::new(),
        builtins: run::builtin_map(),
        prev_path: vec![b'.'],
        history_visit: 0,
        socket_running: None,
        vars: HashMap::new(),
        funs: HashMap::new(),
        path_cache: Default::default(),
        ctrlc: Default::default(),
    };

    print!("{}", se.prompt());

    let session = Arc::new(Mutex::new(se));
    completion::populate_path_cache(session.clone());

    let _sock_dropper = export_fun::listen(session.clone());
    let _ctrlc = ctrlc::setup(session.clone());

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

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

        let mut se = session.lock().unwrap();

        match buf[0] {
            // Ctrl+A
            1 => {
                se.move_to_begin();
            }

            // Ctrl+E
            5 => {
                se.move_to_end();
            }

            // Ctrl+C
            3 => {
                se.clear_prompt();
                se.history_visit = 0;
            }

            // EOF
            4 => {
                break;
            }

            // apparently another backspace, don't do anything.
            8 => {}

            // Ctrl+L
            12 => {
                clear_screen();
                print!("{}", se.prompt());
                io::stdout().write_all(&se.line.into_bytes()).unwrap();
                cursor::move_cursor(Direction::Left, se.line.distance_from_right_end());
                io::stdout().lock().flush().unwrap();
            }

            // Ctrl+R
            18 => {}

            // Enter
            b'\r' => {
                let line = se.line.into_bytes();

                if !line.is_empty() {
                    let parsed = match parse::do_parse(&line) {
                        Ok(p) => p,
                        Err((crate::parse::ParseError::Eof, _)) => {
                            se.line.add(b'\n');
                            print!("\r\n> ");
                            continue
                        },
                        Err(e) => {
                            println!("{e:?}\n{}", se.prompt());
                            continue;
                        }
                    };
                    print!("\r\n");
                    let entry = HistoryEntry::new(line.clone());
                    history::persist(&entry);
                    se.history.push(entry);
                    se.history_visit = 0;
                    se.line.dump();
                    drop(se);
                    run::run(session.clone(), parsed);
                }
            }

            // 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].cmd.clone();
                    se.type_bytes(&cmd);
                } else {
                    se.del_left();
                }
            }

            b'\t' => {
                let cmd = se.line.pre().to_vec();
                drop(se);

                let comp = completion(session.clone(), &cmd);

                let mut se = session.lock().unwrap();

                se.type_bytes(&comp.shared_prefix);

                if comp.suggestions.len() > 1 {
                    print!("\r\n");
                    for s in comp.suggestions {
                        io::stdout().lock().write_all(&s.display).unwrap();
                        println!();
                    }
                    se.reprint_prompt();
                }
            }

            // Escape sequence
            27 => {
                let mut seq = vec![read1()];

                if seq[0] == b'[' {
                    // still more
                    while {
                        let last = seq[seq.len() - 1];
                        !(0x40..=0x7E).contains(&last) || seq.len() == 1
                    } {
                        seq.push(read1());
                    }

                    match seq[1] {
                        b'A' => {
                            // up
                            se.history_up();
                        }
                        b'B' => {
                            // down
                            se.history_down();
                        }
                        b'C' => {
                            if se.line.right() {
                                move_cursor(Direction::Right, 1);
                                io::stdout().lock().flush().unwrap();
                            }
                        }
                        b'D' => {
                            if se.line.left() {
                                move_cursor(Direction::Left, 1);
                                io::stdout().lock().flush().unwrap();
                            }
                        }
                        b'3' => {
                            if seq.len() > 2 && seq[2] == b'~' {
                                se.del_right();
                            } else {
                                todo!("unhandled: {}", seq.escape_ascii());
                            }
                        }

                        // HOME button
                        b'H' => {
                            se.move_to_begin();
                        }

                        // END button
                        b'F' => {
                            se.move_to_end();
                        }

                        // Ctrl Arrow
                        b'1' => {
                            if seq[1..].starts_with(b"1;5") {
                                if seq.len() == 4 {
                                    todo!("idk what this is.");
                                }
                                match seq[4] {
                                    b'A' => {
                                        println!("Ctrl+Up");
                                        se.reprint_prompt();
                                        continue;
                                    }
                                    b'B' => {
                                        println!("Ctrl+Down");
                                        se.reprint_prompt();
                                        continue;
                                    }
                                    b'C' => {
                                        se.move_right_word();
                                    }
                                    b'D' => {
                                        se.move_left_word();
                                    }
                                    _ => todo!("unhandled {}", seq.escape_ascii()),
                                }
                                continue;
                            }
                            todo!("unhandled {}", seq[1..].escape_ascii())
                        }

                        _ => todo!("escape characters {}", seq[1..].escape_ascii()),
                    }
                }
            }

            b'|' if se.line.is_empty() && !se.history.is_empty() => {
                let mut cmd = se.history[se.history.len() - 1].cmd.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.type_byte(x);
            }
        }
    }

    session.lock().unwrap().raw.disable();
}

fn main() {
    export_fun::maybe_run_defined_function();
    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(_) => {
                #[cfg(debug_assertions)]
                unsafe {
                    reload::continue_reload()
                }
            }
        }

        // prevent incredibly fast panic loops
        sleep(Duration::from_secs(1));
    }

    println!("bye");
}
