use eyre::{bail, Result};

use super::pattern::r#match;
use super::r#macro::{Set as MacroSet, Source as MacroSource};
use super::token::TokenString;

pub fn expand_call(name: &str, args: &[TokenString], macros: &MacroSet) -> Result<String> {
    match name {
        "filter" => {
            assert_eq!(args.len(), 2);
            text::filter(macros, &args[0], &args[1])
        }
        "filter-out" => {
            assert_eq!(args.len(), 2);
            text::filter_out(macros, &args[0], &args[1])
        }
        "sort" => {
            assert_eq!(args.len(), 1);
            text::sort(macros, &args[0])
        }

        "notdir" => {
            assert_eq!(args.len(), 1);
            file_name::notdir(macros, &args[0])
        }
        "basename" => {
            assert_eq!(args.len(), 1);
            file_name::basename(macros, &args[0])
        }
        "addprefix" => {
            assert_eq!(args.len(), 2);
            file_name::addprefix(macros, &args[0], &args[1])
        }
        "wildcard" => {
            assert_eq!(args.len(), 1);
            file_name::wildcard(macros, &args[0])
        }

        "if" => {
            assert!(args.len() == 2 || args.len() == 3);
            conditional::r#if(macros, &args[0], &args[1], args.get(2))
        }

        // foreach
        "foreach" => {
            assert_eq!(args.len(), 3);
            foreach::foreach(macros, &args[0], &args[1], &args[2])
        }

        // call
        "call" => {
            assert!(!args.is_empty());
            call::call(macros, args.iter())
        }

        // eval
        "eval" => todo!(),

        "origin" => {
            assert_eq!(args.len(), 1);
            origin::origin(macros, &args[0])
        }

        // shell
        "shell" => {
            assert_eq!(args.len(), 1);
            shell::shell(macros, &args[0])
        }

        // fallback
        _ => bail!("function not implemented: {}", name),
    }
}

// Text Functions
mod text {
    use super::*;

    pub fn filter(macros: &MacroSet, patterns: &TokenString, text: &TokenString) -> Result<String> {
        let patterns = macros.expand(patterns)?;
        let patterns = patterns.split_whitespace().collect::<Vec<_>>();
        let text = macros.expand(text)?;
        let text = text.split_whitespace();
        let mut result_pieces = vec![];
        for word in text {
            if patterns
                .iter()
                .any(|pattern| r#match(pattern, word).map_or(false, |x| x.is_some()))
            {
                result_pieces.push(word);
            }
        }
        Ok(result_pieces.join(" "))
    }

    pub fn filter_out(
        macros: &MacroSet,
        patterns: &TokenString,
        text: &TokenString,
    ) -> Result<String> {
        let patterns = macros.expand(patterns)?;
        let patterns = patterns.split_whitespace().collect::<Vec<_>>();
        let text = macros.expand(text)?;
        let text = text.split_whitespace();
        let mut result_pieces = vec![];
        for word in text {
            if patterns
                .iter()
                .all(|pattern| r#match(pattern, word).map_or(false, |x| x.is_none()))
            {
                result_pieces.push(word);
            }
        }
        Ok(result_pieces.join(" "))
    }

    pub fn sort(macros: &MacroSet, words: &TokenString) -> Result<String> {
        let words = macros.expand(words)?;
        let mut words = words.split_whitespace().collect::<Vec<_>>();
        words.sort_unstable();
        words.dedup();
        Ok(words.join(" "))
    }
}

// File Name Functions
mod file_name {
    use std::env;
    use std::ffi::OsStr;
    use std::path::Path;

    use eyre::WrapErr;

    use super::*;

    pub fn notdir(macros: &MacroSet, words: &TokenString) -> Result<String> {
        let words = macros.expand(words)?;
        let words = words
            .split_whitespace()
            .map(|word| {
                Path::new(word)
                    .file_name()
                    .and_then(OsStr::to_str)
                    .unwrap_or("")
            })
            .collect::<Vec<_>>();
        Ok(words.join(" "))
    }

    pub fn basename(macros: &MacroSet, words: &TokenString) -> Result<String> {
        let words = macros.expand(words)?;
        let words = words
            .split_whitespace()
            .map(|word| {
                Path::new(word)
                    .with_extension("")
                    .to_str()
                    .map_or_else(String::new, ToString::to_string)
            })
            .collect::<Vec<_>>();
        Ok(words.join(" "))
    }

    pub fn addprefix(
        macros: &MacroSet,
        prefix: &TokenString,
        targets: &TokenString,
    ) -> Result<String> {
        let prefix = macros.expand(prefix)?;
        let targets = macros.expand(targets)?;
        let results = targets
            .split_whitespace()
            .map(|t| format!("{}{}", prefix, t))
            .collect::<Vec<_>>();
        Ok(results.join(" "))
    }

    pub fn wildcard(macros: &MacroSet, pattern: &TokenString) -> Result<String> {
        let pattern = macros.expand(pattern)?;
        let home_dir = env::var("HOME")
            .ok()
            .or_else(|| dirs::home_dir().and_then(|p| p.to_str().map(String::from)));
        let pattern = if let Some(home_dir) = home_dir {
            pattern.replace('~', &home_dir)
        } else {
            pattern
        };
        let results = glob::glob(&pattern)
            .context("invalid glob pattern!")?
            .filter_map(|path| {
                path.ok()
                    .map(|x| x.to_str().map(ToString::to_string).unwrap_or_default())
            })
            .collect::<Vec<_>>();
        Ok(results.join(" "))
    }
}

// Functions for Conditionals
mod conditional {
    use super::*;

    pub fn r#if(
        macros: &MacroSet,
        condition: &TokenString,
        if_true: &TokenString,
        if_false: Option<&TokenString>,
    ) -> Result<String> {
        let mut condition = condition.clone();
        condition.trim_start();
        condition.trim_end();
        let condition = macros.expand(&condition)?;
        if condition.is_empty() {
            if let Some(if_false) = if_false {
                macros.expand(if_false)
            } else {
                Ok(String::new())
            }
        } else {
            macros.expand(if_true)
        }
    }
}

// foreach
mod foreach {
    use super::*;

    pub fn foreach(
        macros: &MacroSet,
        var: &TokenString,
        list: &TokenString,
        text: &TokenString,
    ) -> Result<String> {
        let var = macros.expand(var)?;
        let list = macros.expand(list)?;
        let words = list.split_whitespace();

        let mut macros = macros.with_overlay();
        let results = words
            .map(|word| {
                macros.set(var.clone(), MacroSource::File, TokenString::text(word));
                macros.expand(text)
            })
            .collect::<Result<Vec<_>, _>>()?;
        Ok(results.join(" "))
    }
}

// call
mod call {
    use super::*;

    pub fn call<'a>(
        macros: &MacroSet,
        args: impl Iterator<Item = &'a TokenString>,
    ) -> Result<String> {
        let args = args
            .map(|arg| macros.expand(arg))
            .collect::<Result<Vec<_>, _>>()?;
        let function = args[0].clone();

        let mut macros = macros.with_overlay();
        for (i, x) in args.into_iter().enumerate() {
            macros.set(i.to_string(), MacroSource::File, TokenString::text(x));
        }
        macros.expand(&TokenString::r#macro(function))
    }
}

mod origin {
    use super::*;

    pub fn origin(macros: &MacroSet, variable: &TokenString) -> Result<String> {
        let variable = macros.expand(variable)?;
        Ok(macros.origin(&variable).to_owned())
    }
}

mod shell {
    use super::*;

    use std::env;
    use std::process::{Command, Stdio};

    pub fn shell(macros: &MacroSet, command: &TokenString) -> Result<String> {
        // TODO bring this in from command_line
        let command = macros.expand(command)?;
        let (program, args) = if cfg!(windows) {
            let cmd = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into());
            let args = vec!["/c", &command];
            (cmd, args)
        } else {
            let sh = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into());
            let args = vec!["-e", "-c", &command];
            (sh, args)
        };
        let result = Command::new(program)
            .args(args)
            .stderr(Stdio::inherit())
            .output()?;
        let status = result.status;
        // TODO set .SHELLSTATUS
        Ok(String::from_utf8(result.stdout)?
            .replace("\r\n", "\n")
            .trim_end_matches('\n')
            .replace("\n", " "))
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use crate::makefile::r#macro::{Set as MacroSet, Source as MacroSource};

    type R = Result<()>;

    fn call(name: &str, args: &[TokenString], macros: &MacroSet) -> Result<String> {
        super::expand_call(name, args, macros)
    }

    macro_rules! call {
        ($func:literal $($arg:literal),+) => {
            call($func, &[$(TokenString::text($arg)),+], &MacroSet::new())?
        };
        ($func:ident $($arg:literal),+) => {
            call(stringify!($func), &[$(TokenString::text($arg)),+], &MacroSet::new())?
        };
    }

    #[test]
    fn filter() -> R {
        let result = call!(filter "word", "this contains a word inside it");
        assert_eq!(result, "word");

        let result = call!(filter "%.c %.s", "foo.c bar.c baz.s ugh.h");
        assert_eq!(result, "foo.c bar.c baz.s");
        Ok(())
    }

    #[test]
    fn filter_out() -> R {
        let result = call!("filter-out" "main1.o main2.o", "main1.o foo.o main2.o bar.o");
        assert_eq!(result, "foo.o bar.o");
        Ok(())
    }

    #[test]
    fn sort() -> R {
        let result = call!(sort "foo bar lose foo");
        assert_eq!(result, "bar foo lose");
        Ok(())
    }

    #[test]
    fn notdir() -> R {
        let result = call!(notdir "src/foo.c hacks");
        assert_eq!(result, "foo.c hacks");
        Ok(())
    }

    #[test]
    fn basename() -> R {
        let result = call!(basename "src/foo.c src-1.0/bar hacks");
        assert_eq!(result, "src/foo src-1.0/bar hacks");
        Ok(())
    }

    #[test]
    fn addprefix() -> R {
        let result = call!(addprefix "src/", "foo bar");
        assert_eq!(result, "src/foo src/bar");
        Ok(())
    }

    #[test]
    fn wildcard() -> R {
        use std::env::{set_current_dir, set_var};
        use std::fs::write;
        use std::path::MAIN_SEPARATOR;

        let tempdir = tempfile::tempdir()?;

        write(tempdir.path().join("foo.c"), "")?;
        write(tempdir.path().join("bar.h"), "")?;
        write(tempdir.path().join("baz.txt"), "")?;
        write(tempdir.path().join("acab.c"), "ACAB")?;
        write(tempdir.path().join("based.txt"), "☭")?;

        set_current_dir(tempdir.path())?;
        set_var("HOME", tempdir.path());
        let sort = |x: String| call("sort", &[TokenString::text(&x)], &MacroSet::new());
        assert_eq!(sort(call!(wildcard "*.c"))?, "acab.c foo.c");
        assert_eq!(
            sort(call!(wildcard "~/ba?.*"))?,
            format!(
                "{0}{1}bar.h {0}{1}baz.txt",
                tempdir.path().display(),
                MAIN_SEPARATOR
            )
        );
        Ok(())
    }

    #[test]
    fn foreach() -> R {
        let mut macros = MacroSet::new();
        macros.set(
            "test".to_owned(),
            MacroSource::File,
            "worked for $(item).".parse()?,
        );
        assert_eq!(
            call(
                "foreach",
                &[
                    TokenString::text("item"),
                    TokenString::text("a b c d"),
                    TokenString::r#macro("test")
                ],
                &macros,
            )?,
            "worked for a. worked for b. worked for c. worked for d."
        );
        Ok(())
    }

    #[test]
    fn call_test() -> R {
        let mut macros = MacroSet::new();
        macros.set(
            "reverse".to_owned(),
            MacroSource::File,
            "$(2) $(1)".parse()?,
        );
        assert_eq!(
            call(
                "call",
                &[
                    TokenString::text("reverse"),
                    TokenString::text("a"),
                    TokenString::text("b")
                ],
                &macros,
            )?,
            "b a"
        );
        Ok(())
    }
}