#![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::*;

#[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))
}

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

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
pub struct re;
impl Builtin for re {
    fn name(&self) -> &str {
        "re"
    }

    fn special(&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(())
    }
}

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

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

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 {
        for arg in args {
            let kind = super::get_command_kind(&session.lock().unwrap(), &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(())
    }
}

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

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

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

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

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