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::{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};
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,
}

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

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

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

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

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

        match buf[0] {
            // Ctrl+C
            3 => {
                // clear line
                cursor::move_cursor(Direction::Right, se.line.distance_from_right_end());
                for _ in 0..se.line.len() {
                    write!(io::stdout(), "\x08 \x08").unwrap();
                }
                io::stdout().lock().flush().unwrap();
                se.line.clear();
            }

            // 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());
                    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();
                    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 = 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
                        }
                        b'B' => {
                            // 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'~' {
                                // delete
                                se.line.del_right();
                                se.line.display_post(b" ");
                            } 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.line.add(x);
                stdout.lock().write_all(&[x]).unwrap();
                se.line.display_post(b"");
            }
        }
    }

    se.raw.disable();
}

#[cfg(debug_assertions)]
fn reload_shell() {
    use std::{env, ffi::CString, ptr};

    // path to this executable
    let exe = env::current_exe().unwrap();
    let exe_c = CString::new(exe.as_os_str().as_bytes()).unwrap();

    // argv
    let args: Vec<CString> = env::args().map(|a| CString::new(a).unwrap()).collect();

    let mut argv: Vec<*const libc::c_char> = args.iter().map(|a| a.as_ptr()).collect();
    argv.push(ptr::null());

    // environment
    let env: Vec<CString> = env::vars()
        .map(|(k, v)| CString::new(format!("{}={}", k, v)).unwrap())
        .collect();

    let mut envp: Vec<*const libc::c_char> = env.iter().map(|e| e.as_ptr()).collect();
    envp.push(ptr::null());

    unsafe {
        libc::execve(exe_c.as_ptr(), argv.as_ptr(), envp.as_ptr());
    }

    eprintln!("exec failed");
}

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)]
                if run::RELOAD.load(std::sync::atomic::Ordering::SeqCst) {
                    reload_shell();
                    println!("failed to reload shell");
                }
            }
        }
    }

    println!("bye");
}
