/*
 * SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
 * SPDX-License-Identifier: BSD-2-Clause
 */
//! Common definitions for the feature-check crate's modules.

use std::collections::HashMap;
use std::fmt::Debug;

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

use crate::version::Version;

/// The default option to pass to a program to obtain the list of features.
pub const DEFAULT_OPTION_NAME: &str = "--features";

/// The default prefix to look for in the lines output by the program.
pub const DEFAULT_PREFIX: &str = "Features: ";

/// The result of evaluating either a single term or the whole expression.
#[derive(Debug)]
#[non_exhaustive]
pub enum CalcResult {
    /// No value, e.g. the queried feature is not present.
    Null,
    /// A boolean value, usually for the whole expression.
    Bool(bool),
    /// A feature's obtained version.
    Version(Version),
}

/// An object that may be evaluated and provide a result.
pub trait Calculable: Debug {
    /// Get the value of the evaluated term or expression as applied to
    /// the list of features obtained for the program.
    ///
    /// # Errors
    ///
    /// Will propagate errors from parsing strings as [`crate::version::Version`] objects.
    /// Will also return an error if the expression contains comparisons of
    /// objects of incompatible types (e.g. a version string and a comparison result).
    fn get_value(&self, features: &HashMap<String, Version>) -> Result<CalcResult, ParseError>;
}

/// The feature-check operating mode, usually "List".
#[derive(Debug)]
#[non_exhaustive]
pub enum Mode {
    /// Obtain the list of the program's features.
    List,
    /// Obtain the value of a single feature.
    Single(Box<dyn Calculable + 'static>),
    /// Evaluate a "feature op version" expression.
    Simple(Box<dyn Calculable + 'static>),
}

/// Runtime configuration settings for
/// the [`obtain_features`][crate::obtain::obtain_features] function.
#[derive(Debug)]
#[non_exhaustive]
pub struct Config {
    /// The option to pass to the program to query for supported features.
    pub option_name: String,
    /// The prefix to look for in the lines output by the program.
    pub prefix: String,
    /// The name or full path of the program to execute.
    pub program: String,
    /// The feature-check tool's operating mode.
    pub mode: Mode,
}

impl Default for Config {
    #[inline]
    fn default() -> Self {
        Self {
            option_name: DEFAULT_OPTION_NAME.to_owned(),
            prefix: DEFAULT_PREFIX.to_owned(),
            program: String::new(),
            mode: Mode::List,
        }
    }
}

// https://github.com/rust-lang/rust-clippy/issues/4979
#[allow(clippy::missing_const_for_fn)]
impl Config {
    /// Replace the option name to query.
    #[inline]
    #[must_use]
    pub fn with_option_name(self, option_name: String) -> Self {
        Self {
            option_name,
            ..self
        }
    }
    /// Replace the prefix to look for in the program output.
    #[inline]
    #[must_use]
    pub fn with_prefix(self, prefix: String) -> Self {
        Self { prefix, ..self }
    }
    /// Replace the name of the program to execute.
    #[inline]
    #[must_use]
    pub fn with_program(self, program: String) -> Self {
        Self { program, ..self }
    }
    /// Replace the query mode.
    #[inline]
    #[must_use]
    pub fn with_mode(self, mode: Mode) -> Self {
        Self { mode, ..self }
    }
}

/// Errors that can occur during parsing a test expression or the features line.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ParseError {
    /// The arguments to a comparison operator are of incompatible types.
    #[error("Cannot compare {0} to {1}")]
    CannotCompare(String, String),

    /// An unrecognized comparison operator was specified.
    #[error("Invalid comparison operator '{0}'")]
    InvalidComparisonOperator(String),

    /// The arguments to a comparison operator are of unexpected types.
    #[error("Don't know how to compare {0} to anything, including {1}")]
    Uncomparable(String, String),

    /// A parser failed.
    #[error("Could not parse '{0}' as a valid expression")]
    ParseFailure(String, #[source] AnyError),

    /// A parser left some bytes out.
    #[error("Could not parse '{0}' as a valid expression: {1} bytes left over")]
    ParseLeftovers(String, usize),
}

/// Errors that can occur during querying a program's features.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ObtainError {
    /// The program's output was not a valid UTF-8 string.
    #[error("Could not decode the {0} program's output as valid UTF-8")]
    DecodeOutput(String, #[source] AnyError),

    /// The program could not be executed.
    #[error("Could not execute the {0} program")]
    RunProgram(String, #[source] AnyError),

    /// A user-supplied expression could not be parsed.
    #[error("Parse error")]
    Parse(#[source] ParseError),
}

/// The result of querying a program for its supported features.
#[derive(Debug)]
#[non_exhaustive]
pub enum Obtained {
    /// The program could not be executed at all, or its output could
    /// not be parsed as a reasonable string.
    Failed(ObtainError),
    /// The program does not support being queried for features.
    NotSupported,
    /// The program's supported features were successfully parsed.
    Features(HashMap<String, Version>),
}
