//! Format the variable names and values as specified by the configuration.
//!
//! The [`filter_vars`] function is mainly used by the `confget`
//! command-line tool, but e.g. the "format safely for Bourne shell use"
//! handling may also be useful to other consumers.

/*
 * 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.
 */
use std::collections::HashMap;
use std::error::Error;
use std::hash::BuildHasher;

use expect_exit::ExpectedResult;
use regex::Regex;

use crate::defs::Config;

/// The name and value of a variable, formatted according to the configuration.
///
/// This is the structure returned by the [`filter_vars`] function.
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)]
#[allow(clippy::module_name_repetitions)]
pub struct FormatOutput {
    /// The name of the variable as found in the input.
    pub name: String,
    /// The value of the variable as found in the input.
    pub value: String,
    /// The name of the variable, formatted according to the configuration.
    pub output_name: String,
    /// The value of the variable, formatted according to the configuration.
    pub output_value: String,
    /// The combination of `output_name` and `output_value` as defined by
    /// the configuration (e.g.
    /// [show_var_name][`crate::defs::Config::show_var_name`]).
    pub output_full: String,
}

/// Convert the a user-supplied fnmatch glob pattern to a regular expression string.
///
/// # Errors
/// The conversion resulted in an invalid regular expression pattern.
fn compile_fnmatch(glob_pattern: &str) -> Result<Regex, Box<dyn Error>> {
    let pattern_vec: Vec<String> = glob_pattern
        .chars()
        .map(|chr| match chr {
            '?' => ".".to_owned(),
            '*' => ".*".to_owned(),
            '.' | '+' | '(' | ')' | '[' | ']' => format!("\\{}", chr),
            _ => chr.to_string(),
        })
        .collect();
    let pattern = format!("^{}$", pattern_vec.iter().cloned().collect::<String>());
    Regex::new(&pattern).expect_result(|| {
        format!(
            "Could not parse the '{}' glob pattern (regex pattern: '{}')",
            glob_pattern, pattern
        )
    })
}

/// Process a user-supplied regular expression pattern.
///
/// # Errors
/// Invalid regular expression pattern.
fn compile_regex(pattern: &str) -> Result<Regex, Box<dyn Error>> {
    Regex::new(pattern)
        .expect_result(|| format!("Could not parse the '{}' regular expression", pattern))
}

/// Process the variable names read from the INI-style file.
///
/// # Errors
/// Invalid fnmatch/glob or regex pattern.
fn get_varnames_only<'data>(
    config: &'data Config,
    sect_data: &HashMap<&'data String, &String>,
) -> Result<Vec<&'data String>, Box<dyn Error>> {
    let mut res: Vec<&String> = if config.list_all {
        sect_data.keys().copied().collect()
    } else if config.match_var_names {
        let regexes: Vec<Regex> = config
            .varnames
            .iter()
            .map(|pat| {
                if config.match_regex {
                    compile_regex(pat)
                } else {
                    compile_fnmatch(pat)
                }
            })
            .collect::<Result<Vec<Regex>, Box<dyn Error>>>()?;
        sect_data
            .keys()
            .filter(|value| regexes.iter().any(|re| re.is_match(value)))
            .copied()
            .collect()
    } else {
        config
            .varnames
            .iter()
            .filter(|name| sect_data.contains_key(name))
            .collect()
    };
    res.sort();
    Ok(res)
}

/// Process the variable names and/or values as requested.
///
/// # Errors
/// Invalid fnmatch/glob or regex pattern.
fn get_varnames<'data>(
    config: &'data Config,
    sect_data: &HashMap<&'data String, &String>,
) -> Result<Vec<&'data String>, Box<dyn Error>> {
    let varnames = get_varnames_only(config, sect_data)?;
    match config.match_var_values {
        None => Ok(varnames),
        Some(ref pattern) => {
            let re = if config.match_regex {
                compile_regex(pattern)?
            } else {
                compile_fnmatch(pattern)?
            };
            Ok(varnames
                .iter()
                .copied()
                .filter(|name| match sect_data.get(name) {
                    Some(value) => re.is_match(value),
                    None => false,
                })
                .collect())
        }
    }
}

/// Select only the variables requested by the configuration.
///
/// This function is mainly used by the `confget` command-line tool.
/// It examines the [Config][`crate::defs::Config`] object and
/// returns the variables that have been selected by its settings
/// (`varname`, `list_all`, `match_var_names`, etc.) from
/// the appropriate sections (`section`, `section_override`, etc.).
///
/// # Errors
/// Invalid fnmatch/glob or regex pattern specified.
#[inline]
pub fn filter_vars<S: BuildHasher + Default>(
    config: &Config,
    data: &HashMap<String, HashMap<String, String, S>, S>,
    section: &str,
) -> Result<Vec<FormatOutput>, Box<dyn Error>> {
    let empty: HashMap<String, String, S> = HashMap::default();
    let sect_iter_first = if config.section_override {
        match data.get("") {
            Some(sect_data) => sect_data.iter(),
            None => empty.iter(),
        }
    } else {
        empty.iter()
    };
    let sect_iter_second = match data.get(section) {
        Some(sect_data) => sect_data.iter(),
        None => empty.iter(),
    };
    let sect_data: HashMap<&String, &String> = sect_iter_first.chain(sect_iter_second).collect();
    Ok::<Result<_, Box<dyn Error>>, Box<dyn Error>>(
        get_varnames(config, &sect_data)?
            .iter()
            .map(|name| -> Result<FormatOutput, Box<dyn Error>> {
                let value = sect_data.get(name).expect_result(|| {
                    format!(
                        "Internal error: the '{}' variable should have been present in {:?}",
                        name, sect_data
                    )
                })?;
                let output_name = format!("{}{}{}", config.name_prefix, name, config.name_suffix);
                let output_value = if config.shell_escape {
                    format!(
                        "'{}'",
                        value.split('\'').collect::<Vec<_>>().join("'\"'\"'")
                    )
                } else {
                    (*value).to_string()
                };
                let output_full = if config.show_var_name {
                    format!("{}={}", output_name, output_value)
                } else {
                    output_value.clone()
                };
                Ok(FormatOutput {
                    name: (*name).to_string(),
                    value: (*value).to_string(),
                    output_name,
                    output_value,
                    output_full,
                })
            })
            .collect::<Result<_, Box<dyn Error>>>(),
    )?
}
