// 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::error::Error;
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};

use anyhow::Error as AnyError;

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,
        }
    }
}

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)]
#[non_exhaustive]
pub enum ParseError {
    /// The arguments to a comparison operator are of incompatible types.
    CannotCompare(String, String),

    /// An unrecognized comparison operator was specified.
    InvalidComparisonOperator(String),

    /// The arguments to a comparison operator are of unexpected types.
    Uncomparable(String, String),

    /// A parser failed.
    ParseFailure(String, AnyError),

    /// A parser left some bytes out.
    ParseLeftovers(String, usize),
}

impl Display for ParseError {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        match *self {
            Self::CannotCompare(ref left, ref right) => {
                write!(f, "Cannot compare {left} to {right}")
            }
            Self::InvalidComparisonOperator(ref value) => {
                write!(f, "Invalid comparison operator '{value}'")
            }
            Self::Uncomparable(ref left, ref right) => write!(
                f,
                "Don't know how to compare {left} to anything, including {right}"
            ),
            Self::ParseFailure(ref value, _) => {
                write!(f, "Could not parse '{value}' as a valid expression")
            }
            Self::ParseLeftovers(ref value, ref count) => {
                write!(
                    f,
                    "Could not parse '{value}' as a valid expression: {count} bytes left over"
                )
            }
        }
    }
}

impl Error for ParseError {
    #[inline]
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match *self {
            Self::CannotCompare(_, _)
            | Self::InvalidComparisonOperator(_)
            | Self::Uncomparable(_, _)
            | Self::ParseLeftovers(_, _) => None,
            Self::ParseFailure(_, ref err) => Some(err.as_ref()),
        }
    }
}

/// Errors that can occur during querying a program's features.
#[derive(Debug)]
#[non_exhaustive]
pub enum ObtainError {
    /// The program's output was not a valid UTF-8 string.
    DecodeOutput(String, AnyError),

    /// The program could not be executed.
    RunProgram(String, AnyError),

    /// A user-supplied expression could not be parsed.
    Parse(ParseError),
}

impl Display for ObtainError {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        match *self {
            Self::DecodeOutput(ref prog, _) => write!(
                f,
                "Could not decode the {prog} program's output as valid UTF-8"
            ),
            Self::RunProgram(ref prog, _) => write!(f, "Could not execute the {prog} program"),
            Self::Parse(_) => write!(f, "Parse error"),
        }
    }
}

impl Error for ObtainError {
    #[inline]
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match *self {
            Self::DecodeOutput(_, ref err) | Self::RunProgram(_, ref err) => Some(err.as_ref()),
            Self::Parse(ref err) => Some(err),
        }
    }
}

/// 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>),
}
