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

use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::{self, File};
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 ansi;
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, ExpString, Parse, PostExpansion};

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::BuiltinClone>,
    vars: HashMap<BString, BString>,
    funs: HashMap<BString, Block>,
    aliases: run::Aliases,
    socket_running: Option<export_fun::SocketRunning>,
    path_cache: PathCache,
    ctrlc: CtrlC,

    /// terminfo identifier to command invocation
    ti_keybinds: HashMap<BString, parse::Command<PostExpansion>>,
    /// byte literals to command invocation
    ascii_keybinds: HashMap<BString, parse::Command<PostExpansion>>,

    debug_keystrokes: bool,
    loud: bool,

    /// 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())
    }

    // TODO: prompt should be BString as well
    fn prompt(&self) -> String {
        #[cfg(debug_assertions)]
        let dev = "dev ";
        #[cfg(not(debug_assertions))]
        let dev = "";
        format!("{dev}[{}]# ", 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 prompt_clear(&mut self) {
        self.clear_prompt();
        self.history_visit = 0;
    }

    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();
        }
    }

    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 del_left_or_previous(&mut self) {
        if self.line.is_empty() && !self.line.is_dirty() && !self.history.is_empty() {
            // take previous command for editing
            let cmd = self.history[self.history.len() - 1].cmd.clone();
            self.type_bytes(&cmd);
        } else {
            self.del_left();
        }
    }

    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 del_left_word(&mut self) {
        todo!()
    }

    fn del_right_word(&mut self) {
        let mut del = 0;
        while let Some(x) = self.line.get_right()
            && x == b' '
        {
            self.line.del_right();
            del += 1;
        }
        while let Some(x) = self.line.get_right()
            && x != b' '
        {
            self.line.del_right();
            del += 1;
        }
        self.line.display_post(&vec![b' '; del]);
    }

    fn cursor_left(&mut self) {
        if self.line.left() {
            move_cursor(Direction::Left, 1);
            io::stdout().lock().flush().unwrap();
        }
    }

    fn cursor_right(&mut self) {
        if self.line.right() {
            move_cursor(Direction::Right, 1);
            io::stdout().lock().flush().unwrap();
        }
    }

    // move to next
    fn cursor_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 cursor_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 complete(session: Arc<Mutex<Session>>) {
        let cmd = session.lock().unwrap().line.pre().to_vec();

        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();
        }
    }

    fn try_submit_command(session: Arc<Mutex<Session>>) {
        let mut se = session.lock().unwrap();
        let line = se.line.into_bytes();

        if !line.is_empty() {
            let parsed = match parse::do_parse(&line) {
                Ok(p) => p,
                Err(_) => {
                    se.line.add(b'\n');
                    print!("\r\n> ");
                    return;
                }
            };
            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);
        }
    }

    fn screen_clear(&mut self) {
        clear_screen();
        self.reprint_prompt();
    }
}

fn exec_rc_file(se: Arc<Mutex<Session>>) {
    let _ = run::source(
        se,
        basedir::config_dir().join(".pishrc").as_os_str().as_bytes(),
    );
}

fn event_loop() {
    history::setup();
    ansi::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(),
        aliases: run::Aliases::new(),
        path_cache: Default::default(),
        ctrlc: Default::default(),
        debug_keystrokes: false,
        loud: false,
        ti_keybinds: HashMap::new(),
        ascii_keybinds: HashMap::new(),
    };

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

    session.lock().unwrap().loud = true;

    print!("{}", session.lock().unwrap().prompt());

    completion::populate_path_cache(session.clone());

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

    'repl: loop {
        let mut se = session.lock().unwrap();

        let Some(key) = ansi::read(se.debug_keystrokes) else {
            break;
        };

        if se.debug_keystrokes {
            println!("{key:?}");
        }

        if let Some(cmd) = se.ascii_keybinds.get(key.as_bytes()) {
            let cmd = cmd.clone();
            drop(se);
            // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code.
            let _ = run::run_quiet(session.clone(), cmd);
            continue 'repl;
        }

        match key {
            ansi::KbInput::Key([x]) => se.type_byte(x),
            ansi::KbInput::Escape(escape) => {
                for terminfo_key in escape.keys.iter() {
                    if let Some(cmd) = se.ti_keybinds.get(terminfo_key.as_bytes()) {
                        let cmd = cmd.clone();
                        drop(se);
                        // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code.
                        let _ = run::run_quiet(session.clone(), cmd);
                        continue 'repl;
                    }
                }
            }
            ansi::KbInput::InvalidEscape(_) => continue,
        }
    }

    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");
}
