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

pub mod cursor;
pub mod linebuf;
pub mod panic;
pub mod parse;
pub mod raw;

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

use crate::cursor::{Direction, move_cursor};
use crate::parse::Ast;

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

const PROMPT: &str = "> ";

fn run_command(raw: &ScopedRawMode, line: Vec<u8>) {
    let parsed = parse::do_parse(&line);

    let parsed = match parsed {
        Ok(p) => p,
        Err(err) => {
            print!("{err:?}\r\n{PROMPT}");
            return;
        }
    };

    let Ast::Pipes(pipes) = parsed else {
        todo!("can only handle pipes");
    };

    // simple command that can probably be builtin
    // TODO: handle builtins uniformly instead of big if case
    if pipes.cmds.len() == 1 {
        let c = &pipes.cmds[0];
        match &c.cmd[..] {
            b"cd" => {
                let target: &Path = match c.args.get(0).map(|v| &v[..]) {
                    Some(b"-") => todo!("prev"),
                    Some(path) => OsStr::from_bytes(path).as_ref(),
                    None => todo!("homedir"),
                };

                if let Err(_) = std::env::set_current_dir(target) {
                    print!("ERR {PROMPT}");
                } else {
                    print!("{PROMPT}");
                }

                return;
            }
            b"from" => todo!("read from file"),
            b"to" | b"into" => todo!("write into file"),
            b"append" => todo!("append to file"),
            _ => (),
        }
    }

    let mut children: Vec<Child> = Vec::new();
    let mut prev_stdout = None;
    let mut spawn_error = false;

    raw.disable();

    for (i, cmd) in pipes.cmds.iter().enumerate() {
        let mut command = Command::new(OsStr::from_bytes(&cmd.cmd));
        for arg in cmd.args.iter() {
            command.arg(OsStr::from_bytes(arg));
        }

        if let Some(stdout) = prev_stdout.take() {
            command.stdin(Stdio::from(stdout));
        }

        if i < pipes.cmds.len() - 1 {
            command.stdout(Stdio::piped());
        }

        let mut child = match command.spawn() {
            Ok(c) => c,
            Err(e) => {
                println!("failed to spawn {:?} - {e:?}", OsStr::from_bytes(&cmd.cmd));
                spawn_error = true;
                break;
            }
        };

        prev_stdout = child.stdout.take();

        children.push(child);
    }

    let status_string;

    if spawn_error {
        for child in children.iter_mut() {
            if let Err(e) = child.kill() {
                println!("failed to kill child - {e:?}");
            }
        }
        status_string = "ERR".into();
    } else {
        let mut code = 0;
        for mut child in children {
            match child.wait() {
                Ok(ec) => {
                    if let Some(c) = ec.code() {
                        code = c;
                    }
                }
                Err(e) => {
                    println!("failed to wait for child - {e:?}")
                }
            }
        }
        if code == 0 {
            status_string = String::new();
        } else {
            status_string = format!("{code}");
        }
    }

    raw.enable();

    print!("\r{status_string}{PROMPT}");
}

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 stdin = stdin.lock();
    let mut stdout = stdout.lock();

    let mut buffer = [0u8; 1];
    let mut line = LineBuf::new();
    let mut history = Vec::new();

    print!("{PROMPT}");

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

        match buffer[0] {
            // EOF
            4 => {
                break;
            }

            // Enter
            b'\r' => {
                let line = line.dump();
                if !line.is_empty() {
                    print!("\r\n");
                    history.push(line.clone());
                    run_command(&raw, line);
                }
            }

            // Backspace (127 on most systems)
            127 => {
                if line.is_empty() && !line.is_dirty() && !history.is_empty() {
                    // take previous command for editing
                    let cmd = history[history.len() - 1].clone();
                    io::stdout().write_all(&cmd).unwrap();
                    io::stdout().flush().unwrap();
                    line.set_content(cmd);
                } else if line.del_left().is_some() {
                    print!("\x08 \x08");
                }
            }

            b'\t' => {
                todo!()
            }

            // Escape sequence
            27 => {
                let mut seq = [0u8; 2];
                stdin.read_exact(&mut seq).unwrap();

                if seq[0] == b'[' {
                    match seq[1] {
                        b'A' => {
                            // up
                        }
                        b'B' => {
                            // down
                        }
                        b'C' => {
                            move_cursor(Direction::Right, 1);
                            line.right();
                        }
                        b'D' => {
                            move_cursor(Direction::Left, 1);
                            line.left();
                        }
                        x => todo!("escape character {x}"),
                    }
                }
            }

            b'|' if line.is_empty() && !history.is_empty() => {
                let mut cmd = history[history.len() - 1].clone();
                cmd.extend_from_slice(b" | ");
                io::stdout().write_all(&cmd).unwrap();
                io::stdout().flush().unwrap();
                line.set_content(cmd);
            }

            // Normal character
            x => {
                line.add(x);
                stdout.write_all(&[x]).unwrap();
                line.display_post();
            }
        }
    }

    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(_) => continue,
        }
    }

    println!("bye");
}
