/*
 * SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
 * SPDX-License-Identifier: BSD-2-Clause
 */
//! Common definitions used by the various `confget` modules.
//!
//! This module mainly defines the [`Config`] struct that provides
//! configuration settings for pretty much all of the functions in
//! the `confget` library. The easiest way to use it is to call
//! the [`Config::default`] method and override some settings as
//! needed:
//!
//! ```
//! let config = confget::Config {
//!     filename: Some("/etc/config.ini".to_string()),
//!     section: "client".to_string(),
//!     ..confget::Config::default()
//! };
//! ```

use std::collections::HashMap;
use std::str::FromStr;

use anyhow::Error as AnyError;
use regex::Error as RegexError;
use thiserror::Error;

/// An error that occurred during processing the input data.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConfgetError {
    /// Invalid configuration, probably command-line.
    #[error("Invalid configuration specified: {0}")]
    Config(String),

    /// Invalid data read from an input file.
    #[error("Could not parse the '{0}' file")]
    FileFormat(String, #[source] AnyError),

    /// Invalid glob/fnmatch pattern.
    #[error("Could not parse the '{0}' glob pattern")]
    Glob(String, #[source] AnyError),

    /// Something went really wrong somewhere.
    #[error("Internal confget error: {0}")]
    Internal(String),

    /// Could not read the input data.
    #[error("Could not read the input data")]
    ReadData(#[source] AnyError),

    /// Invalid regular expression.
    #[error("Could not compile the '{0}' regular expression")]
    Regex(String, #[source] RegexError),

    /// Invalid backend type.
    #[error("Unrecognized backend type '{0}'")]
    UnknownBackend(String),
}

/// The configuration backend to use for parsing the input data.
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum BackendKind {
    #[cfg(feature = "ini-nom")]
    /// Parse INI-style files and their sections using the Nom-based backend.
    IniNom,

    #[cfg(feature = "ini-regex")]
    /// Parse INI-style files and their sections using the legacy regex-based backend.
    IniRE,
}

impl BackendKind {
    #[cfg(feature = "ini-nom")]
    /// The name of the Nom-based INI-style file backend.
    pub const INI_NOM: &'static str = "ini-nom";

    #[cfg(feature = "ini-regex")]
    /// The name of the legacy regex-based INI-style file backend.
    pub const INI_RE: &'static str = "ini-regex";

    /// Return the most suitable backend type for INI-style files.
    #[inline]
    #[must_use]
    pub const fn get_preferred_ini_backend() -> Self {
        #[cfg(feature = "ini-nom")]
        return Self::IniNom;

        #[cfg(not(feature = "ini-nom"))]
        #[cfg(feature = "ini-regex")]
        return Self::IniRE;
    }

    /// Return the name of the most suitable backend type for INI-style files.
    #[inline]
    #[must_use]
    pub const fn get_preferred_ini_backend_name() -> &'static str {
        #[cfg(feature = "ini-nom")]
        return Self::INI_NOM;

        #[cfg(not(feature = "ini-nom"))]
        #[cfg(feature = "ini-regex")]
        return Self::INI_RE;
    }
}

impl FromStr for BackendKind {
    type Err = ConfgetError;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            #[cfg(feature = "ini-nom")]
            Self::INI_NOM => Ok(Self::IniNom),

            #[cfg(feature = "ini-regex")]
            Self::INI_RE => Ok(Self::IniRE),

            other => Err(ConfgetError::UnknownBackend(other.to_owned())),
        }
    }
}

impl AsRef<str> for BackendKind {
    #[inline]
    fn as_ref(&self) -> &str {
        match *self {
            #[cfg(feature = "ini-nom")]
            Self::IniNom => Self::INI_NOM,

            #[cfg(feature = "ini-regex")]
            Self::IniRE => Self::INI_RE,
        }
    }
}

/// Configuration settings for the `confget` functions.
///
/// This is the main way to control the behavior of a backend's
/// [`read_file`][`crate::backend::Backend::read_file`] method,
/// the [`read_ini_file`][`crate::read_ini_file`] function, and
/// the [`format::filter_vars`][`crate::format::filter_vars`] function:
/// specify what file to read, what variables to extract from it, and
/// how to format them.
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)]
#[allow(clippy::struct_excessive_bools)]
pub struct Config {
    /// The configuration backend to use.
    pub backend: BackendKind,
    /// The encoding the input file is in, or "" for the locale default.
    pub encoding: String,
    /// The (backend-specific) filename to read data from.
    pub filename: Option<String>,
    /// Formatting: select all the variables in the specified section.
    pub list_all: bool,
    /// Formatting: treat the variable match patterns as regular expressions
    /// instead of glob ones.
    pub match_regex: bool,
    /// Formatting: treat `varnames` as a list of patterns, not exact
    /// variable names.
    pub match_var_names: bool,
    /// Formatting: only select variables with values that match a pattern.
    pub match_var_values: Option<String>,
    /// Formatting: specify a string to prepend to the variable name.
    pub name_prefix: String,
    /// Formatting: specify a string to append to the variable name.
    pub name_suffix: String,
    /// Formatting: select variables from the specified section.
    pub section: String,
    /// Formatting: read variables from the initial section (""), then
    /// override their values with variables from the one specified by
    /// the `section` field.
    pub section_override: bool,
    /// Formatting: if `section` is an empty string and there are no
    /// variables in the initial section, do not select the first section
    /// defined in the file.
    pub section_specified: bool,
    /// Formatting: make the output values suitable for parsing by
    /// Bourne-like shells.
    pub shell_escape: bool,
    /// Formatting: always display the variable name.
    pub show_var_name: bool,
    /// Formatting: select variable names or patterns to display.
    pub varnames: Vec<String>,
}

impl Default for Config {
    /// Initialize a [`Config`] object with default values.
    ///
    /// This is the recommended way to use the [`Config`] struct:
    /// only override the settings that must be overridden.
    #[inline]
    fn default() -> Self {
        Self {
            backend: BackendKind::get_preferred_ini_backend(),
            encoding: String::new(),
            filename: None,
            list_all: false,
            match_regex: false,
            match_var_names: false,
            match_var_values: None,
            name_prefix: String::new(),
            name_suffix: String::new(),
            section: String::new(),
            section_override: false,
            section_specified: false,
            shell_escape: false,
            show_var_name: false,
            varnames: Vec::new(),
        }
    }
}

/// Variable/value pairs from a single section.
pub type SectionData = HashMap<String, String>;

/// Section/variable/value information read from a file.
pub type FileData = HashMap<String, SectionData>;
