//! confget - a command-line configuration file parser.
//!
//! Extract the specified variables from a configuration file and
//! format their values as requested.
/*
 * Copyright (c) 2021, 2022  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.
 */
// Activate most of the clippy::restriction lints that we have come across...
#![warn(clippy::missing_docs_in_private_items)]
// ...except for these ones.
#![allow(clippy::implicit_return)]

use std::collections::HashMap;
use std::process::{ExitCode, Termination};

use clap::Parser;
use expect_exit::{Expected, ExpectedWithError};

use confget::defs::{BackendKind, Config, FEATURES};
use confget::format;

/// The exit code from the program itself.
#[derive(Debug)]
enum MainResult {
    /// Everything is fine.
    OK,
    /// An error in the command-line arguments, or a non-match in "check only" mode.
    Failure,
}

impl Termination for MainResult {
    fn report(self) -> ExitCode {
        ExitCode::from(match self {
            Self::OK => 0,
            Self::Failure => 1,
        })
    }
}

/// The requested mode of operation for the confget tool.
#[derive(Debug)]
enum Mode {
    /// Either help/version/features were requested, or the supplied
    /// command-line options were inconsistent.
    Handled(MainResult),
    /// Query for the specified variables with the specified options.
    Query(Config),
    /// Only check whether a variable is defined in the file.
    Check(Config, String),
    /// Only display the section names parsed from the config file.
    Sections(Config),
}

/// The command-line option definitions.
#[derive(Debug, Parser)]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
    /// check if the variable is defined in the file
    #[clap(short)]
    check_only: bool,

    /// the configuration file to read from
    #[clap(short)]
    filename: Option<String>,

    /// display usage information and exit
    #[clap(short = 'h', long = "help")]
    show_help: bool,

    /// specify which variables to display
    #[clap(short = 'L')]
    match_var_names: bool,

    /// list all variables in the specified section
    #[clap(short)]
    list_all: bool,

    /// only display values that match the specified pattern
    #[clap(short)]
    match_var_values: Option<String>,

    /// always display the variable name
    #[clap(short = 'N')]
    show_var_name: bool,

    /// never display the variable name
    #[clap(short = 'n')]
    never_show_var_name: bool,

    /// allow variables in the specified section to override those placed before any section definitions
    #[clap(short = 'O')]
    section_override: bool,

    /// display this string after the variable name
    #[clap(short = 'P')]
    name_suffix: Option<String>,

    /// display this string before the variable name
    #[clap(short = 'p')]
    name_prefix: Option<String>,

    /// query for a specific type of information
    #[clap(short)]
    query: Option<String>,

    /// quote the values suitably for the Bourne shell
    #[clap(short = 'S')]
    shell_escape: bool,

    /// the configuration section to read
    #[clap(short = 's')]
    section: Option<String>,

    /// the configuration file type
    #[clap(short = 't')]
    backend_name: Option<String>,

    /// display program version information and exit
    #[clap(short = 'V', long = "version")]
    show_version: bool,

    /// treat the match patterns as regular expressions
    #[clap(short = 'x')]
    match_regex: bool,

    /// the names of the variables to display
    varnames: Vec<String>,
}

/// The usage (help) message for the confget tool.
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";

/// Process the command-line options, check for conflicts or inconsistencies.
/// If the `--version`, `--help`, or `-q features/feature` options were specified,
/// display the requested information and exit.
#[allow(clippy::print_stdout)]
fn validate_options(opts: &Cli, config: Config) -> Mode {
    #[allow(clippy::integer_arithmetic)]
    (u32::from(opts.query.is_some())
        + u32::from(opts.match_var_names)
        + u32::from(opts.list_all)
        + u32::from(
            !(config.varnames.is_empty()
                || opts.match_var_names
                || opts
                    .query
                    .as_ref()
                    .map_or(false, |value| value == "feature")),
        )
        < 2)
    .or_exit_("Only a single query at a time, please!");

    if opts.show_version {
        let &(_, version) = FEATURES
            .iter()
            .find(|&&(name, _)| name == "BASE")
            .or_exit(|| format!("Internal error: no 'BASE' in {:?}", FEATURES));
        println!("confget {}", version);
    }
    if opts.show_help {
        println!("{}", USAGE);
    }
    if opts.show_help || opts.show_version {
        return Mode::Handled(MainResult::OK);
    }

    if let Some(ref query) = opts.query {
        return match query.as_str() {
            "sections" => {
                (config.backend == BackendKind::Ini).or_exit_(
                "The query for sections is only supported for the 'ini' backend for the present",
            );
                Mode::Sections(config)
            }
            "features" => {
                config
                    .varnames
                    .is_empty()
                    .or_exit_("No arguments to -q features");
                let features: Vec<String> = FEATURES
                    .iter()
                    .map(|&(name, version)| format!("{}={}", name, version))
                    .collect();
                println!("{}", features.join(" "));
                Mode::Handled(MainResult::OK)
            }
            "feature" => {
                let (feature_name, rest) = config
                    .varnames
                    .split_first()
                    .or_exit_("Expected a single feature name");
                rest.is_empty()
                    .or_exit_("Only a single feature name expected");
                match FEATURES.iter().find(|&&(name, _)| name == feature_name) {
                    Some(&(_, value)) => {
                        println!("{}", value);
                        Mode::Handled(MainResult::OK)
                    }
                    None => Mode::Handled(MainResult::Failure),
                }
            }
            other => expect_exit::exit(&format!("Unrecognized query '{}'", other)),
        };
    }

    if opts.list_all {
        config
            .varnames
            .is_empty()
            .or_exit_("Only a single query at a time, please!");
    } else if opts.match_var_names {
        (!config.varnames.is_empty()).or_exit_("No patterns to match against");
    } else {
        (!config.varnames.is_empty()).or_exit_("No variables specified to query");
    }

    if opts.check_only {
        let (varname, rest) = opts
            .varnames
            .split_first()
            .or_exit_("Internal error: check_only with no config.varnames");
        rest.is_empty()
            .or_exit_("Only a single query at a time, please!");
        Mode::Check(config, varname.clone())
    } else {
        Mode::Query(config)
    }
}

/// Parse the command-line arguments, process informational queries.
fn parse_args() -> Mode {
    let opts = Cli::parse();

    let config = Config {
        backend: opts
            .backend_name
            .as_ref()
            .map_or(BackendKind::Ini, |value| {
                value.parse().or_exit_e_("Invalid backend specified")
            }),
        filename: opts.filename.clone(),
        list_all: opts.list_all,
        match_regex: opts.match_regex,
        match_var_names: opts.match_var_names,
        match_var_values: opts.match_var_values.clone(),
        name_prefix: match opts.name_prefix {
            Some(ref value) => value.clone(),
            None => String::new(),
        },
        name_suffix: match opts.name_suffix {
            Some(ref value) => value.clone(),
            None => String::new(),
        },
        section: match opts.section {
            Some(ref value) => value.clone(),
            None => String::new(),
        },
        section_override: opts.section_override,
        section_specified: opts.section.is_some(),
        shell_escape: opts.shell_escape,
        show_var_name: opts.show_var_name
            || ((opts.match_var_names || opts.list_all || opts.varnames.len() > 1)
                && !opts.never_show_var_name),
        varnames: opts.varnames.clone(),
    };
    validate_options(&opts, config)
}

/// Display the processed variable names and/or values.
#[allow(clippy::print_stdout)]
fn output_vars(
    config: &Config,
    data: &HashMap<String, HashMap<String, String>>,
    section: &str,
) -> MainResult {
    for var in format::filter_vars(config, data, section)
        .or_exit_e_("Could not select the variables to output")
    {
        println!("{}", var.output_full);
    }
    MainResult::OK
}

/// Exit with status 0 if the specified variable is present in the config file.
fn output_check_only(
    _config: &Config,
    data: &HashMap<String, HashMap<String, String>>,
    section: &str,
    varname: &str,
) -> MainResult {
    match data.get(section) {
        Some(sect_data) => {
            if sect_data.contains_key(varname) {
                MainResult::OK
            } else {
                MainResult::Failure
            }
        }
        None => MainResult::Failure,
    }
}

/// Output the names of the sections parsed from the config file.
#[allow(clippy::print_stdout)]
fn output_sections(data: &HashMap<String, HashMap<String, String>>) -> MainResult {
    let mut sections: Vec<&String> = data.keys().filter(|value| !value.is_empty()).collect();
    sections.sort();
    for name in sections {
        println!("{}", name);
    }
    MainResult::OK
}

/// Parse the INI-style file, return the data and the queried section name.
fn parse_config(config: &Config) -> (HashMap<String, HashMap<String, String>>, String) {
    let (data, first_section) =
        confget::read_ini_file(config).or_exit_e_("Could not initialize the confget parser");
    let section = if config.section.is_empty() {
        first_section
    } else {
        config.section.clone()
    };
    (data, section)
}

fn main() -> MainResult {
    match parse_args() {
        Mode::Handled(res) => res,
        Mode::Check(config, varname) => {
            let (data, section) = parse_config(&config);
            output_check_only(&config, &data, &section, &varname)
        }
        Mode::Sections(config) => {
            let (data, _) = parse_config(&config);
            output_sections(&data)
        }
        Mode::Query(config) => {
            let (data, section) = parse_config(&config);
            output_vars(&config, &data, &section)
        }
    }
}
