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, Stdio};
use std::thread::sleep;
use std::time::Duration;

mod basedir;
mod completion;
mod cursor;
mod history;
mod linebuf;
mod panic;
mod parse;
mod raw;
mod reload;
mod run;

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

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

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

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<BString>,
    dispatch: CommandDispatch,
    prev_path: BString,

    /// 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.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() {
            write!(io::stdout(), "\x08 \x08").unwrap();
        }
        io::stdout().lock().flush().unwrap();
        self.line.clear();
    }

    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].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 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() {
    let stdin = io::stdin();
    let stdout = io::stdout();

    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 mut se = Session {
        raw,
        line: LineBuf::new(),
        history: Vec::new(),
        dispatch: CommandDispatch::new(),
        prev_path: vec![b'.'],
        history_visit: 0,
    };

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

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

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

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

            // Ctrl+L
            12 => {
                clear_screen();
                write!(io::stdout(), "{}", se.prompt()).unwrap();
                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.dump();
                if !line.is_empty() {
                    print!("\r\n");
                    se.history.push(line.clone());
                    se.history_visit = 0;
                    run::run(&mut 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();
                    se.type_bytes(&cmd);
                } else {
                    se.del_left();
                }
            }

            b'\t' => {
                let cmd = se.line.into_bytes();
                let comp = parse::completion_context(&cmd);
                match comp.kind {
                    parse::CompletionKind::Command => todo!(),
                    parse::CompletionKind::Argument => {
                        let suggestions = completion::path_completion(comp.partial);
                        if suggestions.len() == 0 {
                            continue;
                        }

                        if suggestions.len() == 1 {
                            // apply suggestion
                            se.type_bytes(&suggestions[0].delta);
                            continue;
                        }

                        cursor::save();

                        // one line below
                        print!("\r\n");
                        for s in suggestions {
                            io::stdout().lock().write_all(&s.display).unwrap();
                            println!();
                        }

                        cursor::restore();
                        stdout.lock().flush().unwrap();
                    }
                    parse::CompletionKind::None => {
                        for _ in 0..4 {
                            se.line.add(b' ');
                            print!(" ")
                        }
                    }
                }
            }

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

                if seq[0] == b'[' {
                    // still more
                    while {
                        let last = seq[seq.len() - 1];
                        last < 0x40 || last > 0x7E || 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:?}");
                            }
                        }
                        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.type_byte(x);
            }
        }
    }

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

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

    println!("bye");
}
