use std::env;
use std::ffi::OsString;
use std::path::PathBuf;

use structopt::StructOpt;

#[derive(StructOpt, Debug, PartialEq, Eq)]
#[structopt(author, about)]
pub struct Args {
    /// Cause environment variables, including those with null values, to override macro
    /// assignments within makefiles.
    #[structopt(short, long)]
    environment_overrides: bool,

    /// Specify a different makefile (or '-' for standard input).
    ///
    /// The argument makefile is a pathname of a description file, which is also referred
    /// to as the makefile. A pathname of '-' shall denote the standard input. There can
    /// be multiple instances of this option, and they shall be processed in the order
    /// specified. The effect of specifying the same option-argument more than once is
    /// unspecified.
    #[structopt(short = "f", long = "file", visible_alias = "makefile", number_of_values = 1, parse(from_os_str))]
    makefile: Vec<PathBuf>,

    /// Ignore error codes returned by invoked commands.
    ///
    /// This mode is the same as if the special target .IGNORE were specified without
    /// prerequisites.
    #[structopt(short, long)]
    ignore_errors: bool,

    /// Continue to update other targets that do not depend on the current target if a
    /// non-ignored error occurs while executing the commands to bring a target
    /// up-to-date.
    #[structopt(short, long)]
    keep_going: bool,

    /// Write commands that would be executed on standard output, but do not execute them
    /// (but execute lines starting with '+').
    ///
    /// However, lines with a <plus-sign> ( '+' ) prefix shall be executed. In this mode,
    /// lines with an at-sign ( '@' ) character prefix shall be written to standard
    /// output.
    #[structopt(short = "n", long, visible_alias = "just-print", visible_alias = "recon")]
    dry_run: bool,

    /// Write to standard output the complete set of macro definitions and target
    /// descriptions.
    ///
    /// The output format is unspecified.
    #[structopt(short, long, visible_alias = "print-data-base")]
    print_everything: bool,

    /// Return a zero exit value if the target file is up-to-date; otherwise, return an
    /// exit value of 1.
    ///
    /// Targets shall not be updated if this option is specified. However, a makefile
    /// command line (associated with the targets) with a <plus-sign> ( '+' ) prefix
    /// shall be executed.
    #[structopt(short, long)]
    question: bool,

    /// Clear the suffix list and do not use the built-in rules.
    #[structopt(short = "r", long)]
    no_builtin_rules: bool,

    /// Terminate make if an error occurs while executing the commands to bring a target
    /// up-to-date (default behavior, required by POSIX to be also a flag for some
    /// reason).
    ///
    /// This shall be the default and the opposite of -k.
    #[structopt(short = "S", long, visible_alias = "stop", hidden = true, overrides_with="keep-going")]
    no_keep_going: bool,

    /// Do not write makefile command lines or touch messages to standard output before
    /// executing.
    ///
    /// This mode shall be the same as if the special target .SILENT were specified
    /// without prerequisites.
    #[structopt(short, long, visible_alias = "quiet")]
    silent: bool,

    /// Update the modification time of each target as though a touch target had been
    /// executed.
    ///
    /// Targets that have prerequisites but no commands, or that are already up-to-date,
    /// shall not be touched in this manner. Write messages to standard output for each
    /// target file indicating the name of the file and that it was touched. Normally,
    /// the makefile command lines associated with each target are not executed. However,
    /// a command line with a <plus-sign> ( '+' ) prefix shall be executed.
    #[structopt(short, long)]
    touch: bool,

    /// Target names or macro definitions.
    ///
    /// If no target is specified, while make is processing the makefiles, the first
    /// target that make encounters that is not a special target or an inference rule
    /// shall be used.
    targets_or_macros: Vec<String>,
}

impl Args {
    pub fn from_env_and_args() -> Args {
        // POSIX spec says "Any options specified in the MAKEFLAGS environment variable
        // shall be evaluated before any options specified on the make utility command
        // line."
        // TODO make sure only flags show up in MAKEFLAGS (i.e. not -f or other stuff)
        // TODO accept "option letters without the leading <hyphen-minus> characters"
        let env_makeflags = env::var("MAKEFLAGS").unwrap_or_default();
        let env_makeflags = env_makeflags.split_whitespace()
            .map(|x| OsString::from(x));
        let mut args = env::args_os();
        // per the structopt docs, the first argument will be used as the binary name,
        // so we need to make sure it goes in before MAKEFLAGS
        let arg0 = args.next().unwrap_or_else(|| env!("CARGO_PKG_NAME").into());

        let args = ::std::iter::once(arg0)
            .chain(env_makeflags.into_iter())
            .chain(args);
        Args::from_iter(args)
    }
}

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

    #[test]
    fn no_args() {
        let args: Vec<OsString> = vec!["makers".into()];
        let args = Args::from_iter(args.into_iter());
        assert_eq!(args, Args {
            environment_overrides: false,
            makefile: vec![],
            ignore_errors: false,
            keep_going: false,
            dry_run: false,
            print_everything: false,
            question: false,
            no_builtin_rules: false,
            no_keep_going: false,
            silent: false,
            touch: false,
            targets_or_macros: vec![],
        });
    }

    #[test]
    fn kitchen_sink_args() {
        let args = "makers -eiknpqrstf foo -f bruh bar baz=yeet";
        let args = Args::from_iter(args.split_whitespace());
        assert_eq!(args, Args {
            environment_overrides: true,
            makefile: vec!["foo".into(), "bruh".into()],
            ignore_errors: true,
            keep_going: true,
            dry_run: true,
            print_everything: true,
            question: true,
            no_builtin_rules: true,
            no_keep_going: false,
            silent: true,
            touch: true,
            targets_or_macros: vec!["bar".into(), "baz=yeet".into()],
        });
    }

    #[test]
    fn keep_going_wrestling() {
        let args = "makers -kSkSkSSSkSkkSk -k -S -k -k -S -S -k";
        let args = Args::from_iter(args.split_whitespace());
        assert_eq!(args, Args {
            environment_overrides: false,
            makefile: vec![],
            ignore_errors: false,
            keep_going: true,
            dry_run: false,
            print_everything: false,
            question: false,
            no_builtin_rules: false,
            no_keep_going: false,
            silent: false,
            touch: false,
            targets_or_macros: vec![],
        });
    }

    // TODO test MAKEFLAGS
}