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

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

pub mod ansi;
pub mod basedir;
pub mod completion;
pub mod consts;
pub mod ctrlc;
pub mod cursor;
pub mod date;
pub mod defer;
pub mod export_fun;
pub mod history;
pub mod line;
pub mod panic;
pub mod parse;
pub mod ps1;
pub mod raw;
pub mod reload;
pub mod run;
pub mod rw;
pub mod serialization;
pub mod syntax_highlighting;
pub mod variants;
pub mod wait;
pub mod regex;
mod bitset;

use raw::*;

use crate::completion::{PathCache, completion};
use crate::ctrlc::CtrlC;
use crate::history::HistoryEntry;
use crate::line::Line;
use crate::parse::{Block, ExpString, Parse, PostExpansion};
use crate::ps1::Prompt;

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];

trait PushAll {
    fn push_all(&mut self, other: &bstr);
}

impl PushAll for BString {
    fn push_all(&mut self, other: &bstr) {
        for &c in other {
            self.push(c);
        }
    }
}

pub struct Session {
    raw: Option<ScopedRawMode>,
    line: Line,
    history: Vec<HistoryEntry>,
    prev_path: BString,
    builtins: HashMap<BString, &'static dyn run::BuiltinClone>,
    vars: run::Vars,
    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>>,

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

    highlighter: syntax_highlighting::Highlighter,
    prompt: Prompt,

    terminal_input: Option<ansi::TerminalInput>,
}

impl Session {
    pub fn new_noninteractive() -> Self {
        let mut vars = run::Vars::default();
        let prompt = Prompt::new(&mut vars);

        Self {
            raw: None,
            line: Line::new(),
            history: Vec::new(),
            prev_path: b".".into(),
            builtins: HashMap::new(),
            funs: HashMap::new(),
            aliases: run::Aliases::new(),
            socket_running: None,
            path_cache: Default::default(),
            ctrlc: Default::default(),
            ti_keybinds: HashMap::new(),
            ascii_keybinds: HashMap::new(),
            history_visit: 0,
            highlighter: syntax_highlighting::Highlighter::new(),
            prompt,
            vars,
            terminal_input: None,
        }
    }
}

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

fn pretty_cwd_res() -> 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() -> BString {
    pretty_cwd_res()
        .unwrap_or_else(|_| String::new())
        .into_bytes()
}

impl Session {
    fn prompt_clear(&mut self) {
        self.line.clear_prompt().unwrap();
        self.history_visit = 0;
    }

    fn reprint_prompt(this: Arc<Mutex<Self>>) {
        let prompt = this.lock().unwrap().prompt.prompt().to_vec();
        io::stdout().write_all(&prompt).unwrap();
        let mut this = this.lock().unwrap();
        if let Some(ti) = this.terminal_input.as_mut()
            && let Some(pos) = ti.query_cursor_position()
        {
            this.line.set_begin_of_line(pos);
        }
        this.line.mark_dirty();
        this.cohere().unwrap();
    }

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

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

    fn prompt_pipe_previous(&mut self) {
        if self.line.is_empty()
            && let Some(prev) = self.history.last()
        {
            let mut cmd = prev.cmd.clone();
            cmd.push_all(b" | ");
            self.line.type_bytes(&cmd).unwrap();
        } else {
            self.line.type_byte(b'|').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.line.type_bytes(&comp.shared_prefix).unwrap();

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

    fn reevaluate_prompt_inner(&mut self) -> bool {
        if self.prompt.requires_update(&self.vars) {
            self.prompt.load_prompt(&mut self.vars);
            true
        } else {
            false
        }
    }

    fn reevaluate_prompt(se: Arc<Mutex<Session>>) {
        if se.lock().unwrap().reevaluate_prompt_inner() {
            let mut prompt = se.lock().unwrap().prompt.clone();
            prompt.eval_prompt(se.clone());
            se.lock().unwrap().prompt = 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.type_byte(b'\n').unwrap();
                    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);

            let status_string = run::run(session.clone(), parsed);
            if !status_string.is_empty() {
                println!("{status_string}");
            }
            Self::reevaluate_prompt(session.clone());
            Self::reprint_prompt(session);
            let _ = std::io::stdout().lock().flush();
        }
    }

    fn screen_clear(this: Arc<Mutex<Self>>) {
        clear_screen();
        Self::reprint_prompt(this);
    }

    fn raw_enable(&self) {
        if let Some(r) = &self.raw {
            r.enable();
        }
    }

    fn raw_disable(&self) {
        if let Some(r) = &self.raw {
            r.disable();
        }
    }

    /// do cleanup actions after a bunch of mutations.
    fn cohere(&mut self) -> io::Result<()> {
        self.line.highlight_syntax(&mut self.highlighter)?;
        Ok(())
    }
}

const DEFAULT_PROFILE: &[u8] = include_bytes!("profile");

fn exec_rc_file(se: Arc<Mutex<Session>>) {
    let rcfile = basedir::config_dir().join(".pishrc");
    if let Ok(true) = fs::exists(&rcfile) {
        let _ = run::source(se, rcfile.as_os_str().as_bytes());
    } else {
        println!("{rcfile:?} does not exist. running default profile.");
        let script = parse::Script::parse_from_bytes(DEFAULT_PROFILE).unwrap();
        run::run_script(se, script);
    }
}

#[test]
fn default_profile_should_parse() {
    parse::Script::parse_from_bytes(DEFAULT_PROFILE).expect("default profile does not parse.");
}

pub fn event_loop() {
    fs::create_dir_all(basedir::config_dir()).unwrap();
    fs::create_dir_all(basedir::data_dir()).unwrap();
    fs::create_dir_all(basedir::state_dir()).unwrap();

    history::setup();
    ansi::setup();

    let stdin = io::stdin();

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

    let se = Session {
        raw: Some(raw),
        line: Line::new(),
        builtins: run::builtin_map(),
        prev_path: vec![b'.'],
        history_visit: 0,
        socket_running: None,
        path_cache: Default::default(),
        ctrlc: Default::default(),
        terminal_input: Some(ansi::TerminalInput::new()),
        ..Session::new_noninteractive()
    };

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

    Session::reevaluate_prompt(session.clone());

    Session::reprint_prompt(session.clone());

    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 _ = se.cohere();

        let Some(key) = se.terminal_input.as_mut().unwrap().read() else {
            break;
        };

        if se.terminal_input.as_mut().unwrap().debug {
            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.line.type_byte(x).unwrap(),
            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();
}

pub fn icon() -> BString {
    const DATA: &[u8] = include_bytes!("icon.txt");
    const COLOR0: &[u8] = b"\x1b[100m";
    const COLOR1: &[u8] = b"\x1b[47m";
    const COLOR_RESET: &[u8] = b"\x1b[0m";
    let mut color = COLOR0;
    let mut buf = BString::new();

    for line in DATA.split(|x| *x == b'\n') {
        if line.starts_with(b"-") {
            color = COLOR1;
            continue;
        }

        let mut colored = false;
        for &b in line {
            if b == b'#' {
                if !colored {
                    buf.push_all(color);
                    colored = true;
                }
            } else if colored {
                buf.push_all(COLOR_RESET);
                colored = false;
            }
            buf.push_all(b"  ");
        }
        if colored {
            buf.push_all(COLOR_RESET);
        }
        buf.push_all(b"\r\n");
    }

    buf
}
