#![allow(non_camel_case_types)]

use std::sync::{Arc, Mutex};
use std::{env::*, fs::OpenOptions, path::PathBuf};

use pish_derive::FromArgs;

use super::{Builtin, BuiltinError as Error, BuiltinResult as Result};
use crate::parse::CmdDisplay;
use crate::run::AliasAge;
use crate::*;

#[allow(unused)]
pub enum ArgParseError<'a> {
    LeftoverArg(&'a [u8]),
    MissingArg(&'static str),
    MissingArgValue(&'static str),
    ArgValueParseError(&'static str, String),
}

pub trait ArgParse: Sized {
    fn parse<'a>(args: &'a [BString]) -> std::result::Result<Self, ArgParseError<'a>>;
}

fn read_args<T: ArgParse>(args: &[BString], w: &mut dyn Write) -> std::result::Result<T, Error> {
    let err = match T::parse(args) {
        Ok(t) => return Ok(t),
        Err(e) => e,
    };

    match err {
        ArgParseError::LeftoverArg(items) => {
            w.write_all(b"leftover argument: ")?;
            w.write_all(items)?;
            w.write_all(b"\n")?;
        }
        ArgParseError::MissingArg(arg) => {
            writeln!(w, "argument `{arg}` is missing")?;
        }
        ArgParseError::MissingArgValue(arg) => {
            writeln!(w, "argument `{arg}` is missing its value")?;
        }
        ArgParseError::ArgValueParseError(arg, err) => {
            writeln!(w, "failed to parse value of `{arg}`: {err}")?;
        }
    }

    Err(Error::Exit(-2))
}

#[derive(Copy, Clone)]
pub struct cd;
impl Builtin for cd {
    fn name(&self) -> &str {
        "cd"
    }
    fn io(
        &self,
        se: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        let mut dir = match current_dir() {
            Ok(path) => path.as_os_str().as_bytes().to_vec(),
            Err(_) => vec![b'.'],
        };

        std::mem::swap(&mut dir, &mut se.lock().unwrap().prev_path);

        let target_path: BString = match args.first().map(|v| &v[..]) {
            Some(b"-") => dir,
            Some(path) => path.to_vec(),
            None => {
                if let Some(home) = std::env::var_os("HOME") {
                    home.into_encoded_bytes()
                } else {
                    writeln!(stdout, "$HOME not set")?;
                    return Err(Error::Exit(-1));
                }
            }
        };

        if set_current_dir(OsStr::from_bytes(&target_path)).is_err() {
            write!(stdout, "failed to cd into ")?;
            stdout.write_all(&target_path)?;
            writeln!(stdout, "\n")?;
            Err(Error::Exit(1))
        } else {
            Ok(())
        }
    }
}

#[derive(Copy, Clone)]
pub struct clear;
impl Builtin for clear {
    fn name(&self) -> &str {
        "clear"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        _args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        stdout.write_all(b"\x1B[2J\x1B[1;1H")?;
        Ok(())
    }
}

/// restart shell
#[derive(Copy, Clone)]
pub struct re;
impl Builtin for re {
    fn name(&self) -> &str {
        "re"
    }

    fn special(&mut self, session: Arc<Mutex<Session>>, _args: &[BString]) {
        session.lock().unwrap().raw_disable();
        crate::reload::begin_reload();
        session.lock().unwrap().raw_enable(); // something went wrong, let's restore raw mode
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        _args: &[BString],
        _stdin: &mut dyn Read,
        _stdout: &mut dyn Write,
    ) -> Result {
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct Sink {
    name: &'static str,
    append: bool,
}

impl Builtin for Sink {
    fn name(&self) -> &str {
        self.name
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        args: &[BString],
        stdin: &mut dyn Read,
        _stdout: &mut dyn Write,
    ) -> Result {
        let Some(path) = args.first() else {
            return Err(Error::Exit(1));
        };
        let path = PathBuf::from(OsStr::from_bytes(path));
        let mut file = OpenOptions::new()
            .write(true)
            .create(true)
            .append(self.append)
            .open(path)?;
        std::io::copy(stdin, &mut file)?;
        Ok(())
    }
}

pub const fn sink(name: &'static str, append: bool) -> Sink {
    Sink { name, append }
}

#[derive(Copy, Clone)]
pub struct from;
impl Builtin for from {
    fn name(&self) -> &str {
        "from"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        let Some(path) = args.first() else {
            return Err(Error::Exit(1));
        };
        let path = PathBuf::from(OsStr::from_bytes(path));
        let mut file = OpenOptions::new().read(true).open(path)?;
        std::io::copy(&mut file, stdout)?;
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct _type;
impl Builtin for _type {
    fn name(&self) -> &str {
        "type"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        let session = session.lock().unwrap();
        for arg in args {
            if session.aliases.get(&arg, AliasAge::MAX).is_some() {
                // TODO: tell what it is aliased to
                writeln!(stdout, "{} is an alias", String::from_utf8_lossy(arg))?;
                continue;
            }

            let kind = super::get_command_kind(&session, &arg[..]);

            let kind_str = match kind {
                run::CommandKind::Builtin(_) => "builtin",
                run::CommandKind::Fun(_) => "function",
                run::CommandKind::Path(_) => "command (if it exists)",
            };

            writeln!(stdout, "{} is {}", String::from_utf8_lossy(arg), kind_str)?;
        }

        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct builtins;
impl Builtin for builtins {
    fn name(&self) -> &str {
        "builtins"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        _args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        let mut bs = super::BUILTINS.to_vec();
        bs.sort_by_key(|b| b.name());
        for b in bs.into_iter() {
            write!(stdout, "{} ", b.name())?;
        }
        writeln!(stdout)?;
        Ok(())
    }
}

#[derive(FromArgs, Debug)]
struct HistoryArgs {
    /// displays only local shell session history
    local: bool,

    /// displays only history of current directory
    here: bool,

    /// only shows if cwd perfectly matches
    strict: bool,

    at: Option<PathBuf>,
    // TODO: temporal control, i.e. before & after
}

fn display_history<T: Iterator<Item = HistoryEntry>>(it: T, stdout: &mut dyn Write) -> Result {
    let now = crate::date::DateTime::now();
    for entry in it {
        let delta = now.relative_to(&entry.time);
        for _ in 0..crate::date::DateTime::longest_reasonable_delta() - delta.len() {
            stdout.write_all(b" ")?;
        }
        stdout.write_all(delta.as_bytes())?;
        stdout.write_all(b"  ")?;
        stdout.write_all(&entry.cmd)?;
        stdout.write_all(b"\n")?;
    }
    Ok(())
}

#[derive(Copy, Clone)]
pub struct history;
impl Builtin for history {
    fn name(&self) -> &str {
        "history"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        let args: HistoryArgs = read_args(args, stdout)?;

        let path_prefix = if args.here {
            Some(current_dir()?.as_os_str().as_bytes().to_vec())
        } else {
            args.at.map(|path| path.as_os_str().as_bytes().to_vec())
        };

        let min_time = None;
        let max_time = None;

        if args.local {
            let hist = session.lock().unwrap().history.clone();
            display_history(
                crate::history::local_history_filter(
                    hist,
                    min_time,
                    max_time,
                    path_prefix.as_deref(),
                    args.strict,
                ),
                stdout,
            )
        } else {
            let Ok(hist) = crate::history::HistoryQueryer::new() else {
                writeln!(stdout, "error opening global history file")?;
                return Err(Error::Exit(-1));
            };

            let Ok(it) = hist.query(min_time, max_time, path_prefix.as_deref(), args.strict) else {
                writeln!(stdout, "error querying global history")?;
                return Err(Error::Exit(-1));
            };

            display_history(it, stdout)
        }
    }
}

#[derive(Copy, Clone)]
pub struct escape;
impl Builtin for escape {
    fn name(&self) -> &str {
        "escape"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        for arg in args.iter() {
            let escaped = arg.escape_ascii().to_string();
            stdout.write_all(escaped.as_bytes())?;
            stdout.write_all(b" ")?;
        }
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct parse;
impl Builtin for parse {
    fn name(&self) -> &str {
        "parse"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        for arg in args {
            match crate::parse::do_parse(arg) {
                Ok(parsed) => {
                    write!(stdout, "ok ")?;
                    parsed.cdisplay(stdout)?;
                    writeln!(stdout)?;
                }
                Err(err) => {
                    writeln!(stdout, "err {:?} {}", err.0, err.1.escape_ascii())?;
                }
            }
        }
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct completion;
impl Builtin for completion {
    fn name(&self) -> &str {
        "completion"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        for arg in args {
            let c = crate::completion(session.clone(), &arg);
            write!(stdout, "{:?} ", c.kind)?;
            stdout.write_all(&c.shared_prefix)?;
            for s in c.suggestions {
                stdout.write_all(b" ")?;
                stdout.write_all(&s.display)?;
            }
            stdout.write_all(b"\n")?;
        }
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct null;
impl Builtin for null {
    fn name(&self) -> &str {
        "null"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        _args: &[BString],
        stdin: &mut dyn Read,
        _stdout: &mut dyn Write,
    ) -> Result {
        io::copy(stdin, &mut io::sink())?;
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct var;
impl Builtin for var {
    fn name(&self) -> &str {
        "$"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        if args.is_empty() {
            return Err(Error::Exit(-1));
        } else if args.len() > 1 {
            return Err(Error::Exit(-2));
        }
        let se = session.lock().unwrap();
        if let Some(val) = se.vars.get(&args[0]) {
            stdout.write_all(val.as_slice())?;
            Ok(())
        } else {
            Err(Error::Exit(1))
        }
    }
}

#[derive(Copy, Clone)]
pub struct alias;
impl Builtin for alias {
    fn name(&self) -> &str {
        "alias"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        mut args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        let Some(alias_name) = args.first() else {
            writeln!(stdout, "alias: expected alias name")?;
            return Err(Error::Exit(-1));
        };

        args = &args[1..];
        if let Some(b"=") = args.first().map(|x| x.as_slice()) {
            args = &args[1..];
        }

        if args.is_empty() {
            writeln!(stdout, "alias: expected alias definition")?;
            return Err(Error::Exit(-1));
        };

        let mut parse_fail = false;
        let mut alias_args = Vec::new();
        for arg in args {
            match <Vec<crate::parse::ExpString> as crate::parse::Parse>::parse_from_bytes(&arg[..])
            {
                Ok(mut parsed) => {
                    alias_args.append(&mut parsed);
                }
                Err(err) => {
                    writeln!(
                        stdout,
                        "alias: unparseable argument ({err:?}): {}",
                        arg.escape_ascii(),
                    )?;
                    parse_fail = true;
                }
            }
        }

        if parse_fail {
            return Err(Error::Exit(-1));
        }

        session
            .lock()
            .unwrap()
            .aliases
            .insert(alias_name.clone(), alias_args);

        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct unalias;
impl Builtin for unalias {
    fn name(&self) -> &str {
        "unalias"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        if args.len() != 1 {
            writeln!(stdout, "unalias: expecting exactly one argument")?;
            return Err(Error::Exit(-1));
        }

        session.lock().unwrap().aliases.remove(&args[0]);

        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct debug;
impl Builtin for debug {
    fn name(&self) -> &str {
        "debug"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        let mut se = session.lock().unwrap();
        for arg in args {
            match &arg[..] {
                b"keys" | b"keystrokes" => se.debug_keystrokes = !se.debug_keystrokes,
                _ => writeln!(stdout, "debug: unknown option {}", arg.escape_ascii())?,
            }
        }
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct terminfo;
impl Builtin for terminfo {
    fn name(&self) -> &str {
        "terminfo"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        _args: &[BString],
        _stdin: &mut dyn Read,
        f: &mut dyn Write,
    ) -> Result {
        let ti = crate::ansi::ti();

        writeln!(f, "# Booleans")?;
        for k in ti.booleans.iter() {
            writeln!(f, "{k}")?;
        }
        writeln!(f)?;

        writeln!(f, "# Numbers")?;
        for (k, v) in ti.numbers.iter() {
            writeln!(f, "{k} {v}")?;
        }
        writeln!(f)?;

        writeln!(f, "# Strings")?;
        for (k, v) in ti.strings.iter() {
            writeln!(f, "{k} {}", v.escape_ascii())?;
        }
        writeln!(f)?;

        Ok(())
    }
}

#[derive(Clone)]
pub struct bind {
    key: Option<crate::ansi::KbInput<'static>>,
}

impl bind {
    pub const fn new() -> Self {
        Self { key: None }
    }

    fn is_interactive(args: &[BString]) -> bool {
        matches!(
            args.get(0).map(|x| &x[..]),
            Some(b"i" | b"interactive" | b"del" | b"delete")
        )
    }
}

impl Builtin for bind {
    fn name(&self) -> &str {
        "bind"
    }

    fn special(&mut self, _session: Arc<Mutex<Session>>, args: &[BString]) {
        if Self::is_interactive(args) {
            let raw = crate::raw::ScopedRawMode::on_fd(0);
            raw.enable();
            self.key = crate::ansi::read(false);
            raw.disable();
        }
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        let mut usage = || {
            writeln!(
                stdout,
                "usage: bind | bind ti NAMED_KEYBIND COMMAND | bind key KEY COMMAND | bind [i|interactive] | bind [del|delete]"
            )?;
            Err(Error::Exit(1))
        };

        if args.len() == 0 {
            let mut dump = |map: &HashMap<BString, crate::parse::Command<PostExpansion>>,
                            category: &str|
             -> std::io::Result<()> {
                let mut entries: Vec<_> = map.iter().collect();
                entries.sort_by_key(|x| x.0);
                for (key, cmd) in entries.iter() {
                    write!(stdout, "bind {category} {} ", key.escape_ascii())?;
                    stdout.write_all(&cmd.cmd)?;
                    for arg in cmd.args.iter() {
                        stdout.write_all(b" ")?;
                        stdout.write_all(arg)?;
                    }
                    writeln!(stdout)?;
                }
                Ok(())
            };
            let se = session.lock().unwrap();
            dump(&se.ti_keybinds, "ti ")?;
            dump(&se.ascii_keybinds, "key")?;
            return Ok(());
        }

        if args.len() < 1 {
            return usage();
        }

        let kind = &args[0];

        let mut se = session.lock().unwrap();

        let bind0 = |map: &mut HashMap<BString, _>, key: &[u8], cmd: &[BString]| {
            map.insert(
                key.to_vec(),
                crate::parse::Command {
                    cmd: cmd[0].clone(),
                    args: cmd[1..].to_vec(),
                },
            );
        };

        let bind1 = |map: &mut HashMap<_, _>| {
            bind0(map, &args[1], &args[2..]);
        };

        if Self::is_interactive(args) {
            let Some(key) = self.key.as_ref() else {
                writeln!(stdout, "no input available.")?;
                return Err(Error::Exit(-1));
            };

            if kind.starts_with(b"del") {
                match key {
                    ansi::KbInput::Escape(e) => {
                        se.ti_keybinds.remove(e.keys[0].as_bytes());
                        writeln!(
                            stdout,
                            "deleting keybind ti {}",
                            e.keys[0].as_bytes().escape_ascii()
                        )?;
                    }
                    _ => {
                        se.ascii_keybinds.remove(key.as_bytes());
                        writeln!(
                            stdout,
                            "deleting keybind key {}",
                            key.as_bytes().escape_ascii()
                        )?;
                    }
                }
            } else {
                let x = match key {
                    ansi::KbInput::Escape(e) => {
                        bind0(&mut se.ti_keybinds, e.keys[0].as_bytes(), &args[1..]);
                        format!("ti {}", e.keys[0])
                    }
                    _ => {
                        bind0(&mut se.ascii_keybinds, key.as_bytes(), &args[1..]);
                        format!("key {}", key.as_bytes().escape_ascii())
                    }
                };

                // print what gets bound
                write!(stdout, "bind {x} ")?;
                for arg in args.iter().skip(1) {
                    stdout.write(&arg[..])?;
                    write!(stdout, " ")?;
                }
                writeln!(stdout)?;
            }

            return Ok(());
        }

        match &kind[..] {
            b"ti" => bind1(&mut se.ti_keybinds),
            b"key" => bind1(&mut se.ascii_keybinds),
            _ => return usage(),
        }

        Ok(())
    }
}

#[derive(Clone)]
pub struct exit;
impl Builtin for exit {
    fn name(&self) -> &str {
        "exit"
    }

    fn special(&mut self, _session: Arc<Mutex<Session>>, args: &[BString]) {
        let exit_code: i32 = loop {
            let Some(arg) = args.get(0) else {
                break 0;
            };
            let Ok(arg) = String::from_utf8(arg.clone()) else {
                break 1;
            };
            let Ok(num) = arg.parse() else {
                break 1;
            };
            break num;
        };

        println!("bye!\r");

        std::process::exit(exit_code);
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        _args: &[BString],
        _stdin: &mut dyn Read,
        _stdout: &mut dyn Write,
    ) -> Result {
        Ok(())
    }
}

/// control terminal
#[derive(Copy, Clone)]
pub struct ct;
impl Builtin for ct {
    fn name(&self) -> &str {
        "ct"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        _stdout: &mut dyn Write,
    ) -> Result {
        let Some(arg) = args.get(0) else {
            return Err(Error::Exit(-1));
        };

        let mut se = session.lock().unwrap();

        match &arg[..] {
            b"cursor_begin" => se.move_to_begin(),
            b"cursor_end" => se.move_to_end(),
            b"cursor_right" => se.cursor_right(),
            b"cursor_left" => se.cursor_left(),
            b"cursor_right_word" => se.cursor_right_word(),
            b"cursor_left_word" => se.cursor_left_word(),
            b"prompt_clear" => se.prompt_clear(),
            b"screen_clear" => {
                drop(se);
                Session::screen_clear(session);
            },
            b"history_previous" => se.history_up(),
            b"history_next" => se.history_down(),
            b"prompt_del_left" => se.del_left(),
            b"prompt_del_right" => se.del_right(),
            b"prompt_del_left_word" => se.del_left_word(),
            b"prompt_del_right_word" => se.del_right_word(),
            b"prompt_del_left_or_previous" => se.del_left_or_previous(),
            b"complete" => {
                drop(se);
                Session::complete(session)
            }
            b"try_submit_command" => {
                drop(se);
                Session::try_submit_command(session);
            }
            _ => return Err(Error::Exit(-2)),
        }

        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct source;

impl Builtin for source {
    fn name(&self) -> &str {
        "source"
    }

    fn io(
        &self,
        session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        _stdout: &mut dyn Write,
    ) -> Result {
        for file in args.iter() {
            // TODO: useful error propagation (?)
            super::source(session.clone(), file).map_err(|_| Error::Exit(-1))?;
        }
        Ok(())
    }
}

#[derive(Copy, Clone)]
pub struct Break;

impl Builtin for Break {
    fn name(&self) -> &str {
        "break"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        _args: &[BString],
        _stdin: &mut dyn Read,
        _stdout: &mut dyn Write,
    ) -> Result {
        Err(Error::Break)
    }
}

#[derive(Copy, Clone)]
pub struct Continue;

impl Builtin for Continue {
    fn name(&self) -> &str {
        "continue"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        _args: &[BString],
        _stdin: &mut dyn Read,
        _stdout: &mut dyn Write,
    ) -> Result {
        Err(Error::Continue)
    }
}

#[derive(Copy, Clone)]
pub struct Here;

impl Builtin for Here {
    fn name(&self) -> &str {
        "here"
    }

    fn io(
        &self,
        _session: Arc<Mutex<Session>>,
        args: &[BString],
        _stdin: &mut dyn Read,
        stdout: &mut dyn Write,
    ) -> Result {
        if args.is_empty() {
            return Err(Error::Exit(1));
        }
        stdout.write_all(&args[0])?;
        Ok(())
    }
}
