/**
 * Copyright (c) 2021  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
use std::collections::HashMap;
use std::env;
use std::process;

use expect_exit::ExpectedWithError;

use confget::defs;
use confget::format;

#[derive(Debug)]
struct Config {
    config: defs::Config,
    check_only: bool,
    query_sections: bool,
}

const USAGE: &str = "Usage:
confget [-cOSx] [-N | -n] [-f filename] [-m pattern] [-P postfix] [-p prefix]
        [-s section] [-t type] var...

confget [-OSx] [-N | -n] [-f filename] [-m pattern] [-P postfix] [-p prefix]
        [-s section] [-t type] -L pattern...

confget [-OSx] [-N | -n] [-f filename] [-m pattern] [-P postfix] [-p prefix]
        [-s section] [-t type] -l
confget [-f filename] -q sections [-t type]

confget -q features
confget -q feature NAME";

fn validate_options(opts: &getopts::Matches, usage: &str) {
    let total = (opts.opt_present("q") as i32)
        + (opts.opt_present("L") as i32)
        + (opts.opt_present("l") as i32)
        + ((!(opts.free.is_empty()
            || opts.opt_present("L")
            || opts.opt_get_default("q", "".to_string()).unwrap() == "feature")) as i32);
    if total > 1 {
        expect_exit::exit("Only a single query at a time, please!");
    }

    if opts.opt_present("V") {
        let (_, version) = defs::FEATURES
            .iter()
            .find(|(name, _)| *name == "BASE")
            .unwrap();
        println!("confget {}", version);
    }
    if opts.opt_present("h") {
        println!("{}", usage);
    }
    if opts.opt_present("h") || opts.opt_present("V") {
        process::exit(0);
    }

    if opts.opt_present("q") {
        let query = opts.opt_str("q").unwrap();
        if query == "sections" {
            if opts
                .opt_get_default("t", defs::BackendKind::INI.to_string())
                .unwrap()
                != defs::BackendKind::INI
            {
                expect_exit::exit("The query for sections is only supported for the 'ini' backend for the present");
            }
        } else if query == "features" {
            if !opts.free.is_empty() {
                expect_exit::exit("No arguments to -q features");
            }
            let features: Vec<String> = defs::FEATURES
                .iter()
                .map(|(name, version)| format!("{}={}", name, version))
                .collect();
            println!("{}", features.join(" "));
            process::exit(0);
        } else if query == "feature" {
            if opts.free.len() != 1 {
                expect_exit::exit("Only a single feature name expected");
            }
            let feature_name = &opts.free[0];
            match defs::FEATURES
                .iter()
                .find(|(name, _)| *name == feature_name)
            {
                Some((_, value)) => {
                    println!("{}", value);
                    process::exit(0);
                }
                None => process::exit(1),
            };
        } else {
            expect_exit::exit(&format!("Unrecognized query '{}'", query));
        }
    }

    if opts.opt_present("l") || opts.opt_get_default("q", "".to_string()).unwrap() == "sections" {
        if !opts.free.is_empty() {
            expect_exit::exit("Only a single query at a time, please!");
        }
    } else if opts.opt_present("L") {
        if opts.free.is_empty() {
            expect_exit::exit("No patterns to match against");
        }
    } else if opts.opt_present("c") && opts.free.len() > 1 {
        expect_exit::exit("Only a single query at a time, please!");
    } else if opts.free.is_empty() {
        expect_exit::exit("No variables specified to query");
    }
}

fn parse_args() -> Config {
    let args: Vec<String> = env::args().collect();
    let mut optargs = getopts::Options::new();

    optargs.optflag("c", "", "check if the variable is defined in the file");
    optargs.optopt("f", "", "the configuration file to read from", "FILENAME");
    optargs.optflag("h", "help", "display usage information and exit");
    optargs.optflag("L", "", "specify which variables to display");
    optargs.optflag("l", "", "list all variables in the specified section");
    optargs.optopt(
        "m",
        "",
        "only display values that match the specified pattern",
        "VALUE_PATTERN",
    );
    optargs.optflag("N", "", "always display the variable name");
    optargs.optflag("n", "", "never display the variable name");
    optargs.optflag(
        "O",
        "",
        "allow variables in the specified section to override \
                    those placed before any section definitions",
    );
    optargs.optopt(
        "P",
        "",
        "display this string after the variable name",
        "NAME_SUFFIX",
    );
    optargs.optopt(
        "p",
        "",
        "display this string before the variable name",
        "NAME_PREFIX",
    );
    optargs.optopt("q", "", "query for a specific type of information", "QUERY");
    optargs.optflag("S", "", "quote the values suitably for the Bourne shell");
    optargs.optopt("s", "", "the configuration section to read", "SECTION");
    optargs.optopt("t", "", "the configuration file type", "BACKEND");
    optargs.optflag(
        "V",
        "version",
        "display program version information and exit",
    );
    optargs.optflag("x", "", "treat the match patterns as regular expressions");

    let opts = optargs
        .parse(&args[1..])
        .or_exit_e_("Could not parse the command-line options");

    validate_options(&opts, &optargs.usage(USAGE));

    Config {
        config: defs::Config {
            backend: (opts
                .opt_get_default("t", defs::BackendKind::INI.to_string())
                .unwrap())
            .parse()
            .or_exit_e_("Invalid backend specified"),
            filename: opts.opt_str("f"),
            list_all: opts.opt_present("l"),
            match_regex: opts.opt_present("x"),
            match_var_names: opts.opt_present("L"),
            match_var_values: opts.opt_str("m"),
            name_prefix: opts.opt_get_default("p", "".to_string()).unwrap(),
            name_suffix: opts.opt_get_default("P", "".to_string()).unwrap(),
            section: opts.opt_get_default("s", "".to_string()).unwrap(),
            section_override: opts.opt_present("O"),
            section_specified: opts.opt_present("s"),
            shell_escape: opts.opt_present("S"),
            show_var_name: opts.opt_present("N")
                || ((opts.opt_present("L") || opts.opt_present("l") || opts.free.len() > 1)
                    && !opts.opt_present("n")),
            varnames: opts.free.clone(),
        },
        check_only: opts.opt_present("c"),
        query_sections: opts.opt_get_default("q", "".to_string()).unwrap() == "sections",
    }
}

fn output_vars(
    config: &defs::Config,
    data: &HashMap<String, HashMap<String, String>>,
    section: &str,
) {
    for var in format::filter_vars(config, data, section)
        .or_exit_e_("Could not select the variables to output")
    {
        println!("{}", var.output_full);
    }
}

fn output_check_only(
    config: &defs::Config,
    data: &HashMap<String, HashMap<String, String>>,
    section: &str,
) {
    match data.get(section) {
        Some(sect_data) => match sect_data.contains_key(&config.varnames[0]) {
            true => process::exit(0),
            false => process::exit(1),
        },
        None => process::exit(1),
    }
}

fn output_sections(data: &HashMap<String, HashMap<String, String>>) {
    let mut sections: Vec<&String> = data.keys().filter(|value| !value.is_empty()).collect();
    sections.sort();
    for name in sections.iter() {
        println!("{}", name);
    }
}

fn main() {
    let config = parse_args();
    let (data, first_section) = confget::read_ini_file(&config.config)
        .or_exit_e_("Could not initialize the confget parser");
    let section = match config.config.section.is_empty() {
        true => first_section,
        false => config.config.section.clone(),
    };
    if config.check_only {
        output_check_only(&config.config, &data, &section);
    } else if config.query_sections {
        output_sections(&data);
    } else {
        output_vars(&config.config, &data, &section);
    }
}
