//! Error types for function binding and application.

use std::error::Error;
use std::fmt::{Display, Formatter};

use anyhow::anyhow;
use thiserror::Error;

use crate::types::Type;

#[derive(Error, Debug)]
#[error("This function def cannot bind its arguments: {0}")]
pub struct MatchTestFailure(pub Box<dyn Error + Send + Sync + 'static>);

impl MatchTestFailure {
    pub fn msg(msg: &str) -> Self {
        Self(anyhow!(msg.to_string()).into())
    }

    pub fn wrap(e: impl Error + Send + Sync + 'static) -> Self {
        Self(Box::new(e))
    }
}

#[derive(Error, Debug)]
pub enum FunctionParameterBindingFailure {
    #[error("Attempted to bind a named parameter not present in the function def: {0}")]
    UnknownNamedParameter(String),

    #[error("Attempted to bind {got} positional arguments; expected only {expected}")]
    TooManyPositionalArguments { expected: usize, got: usize },

    #[error("Missing required argument: {0}")]
    MissingRequiredArgument(String),

    #[error("Type mismatch for {name}: expected {expected}, got {got}")]
    TypeCheckFailure {
        name: String,
        expected: String,
        got: Type,
    },

    #[error("Type mismatch for varargs: expected {expected}, got {got}")]
    TypeCheckFailureForVarargs { expected: String, got: Type },

    #[error("{0}")]
    DoesNotMatch(Box<dyn Error + Send + Sync>),
}

#[derive(Error, Debug)]
pub struct FunctionParameterBindingFailures(pub Vec<FunctionParameterBindingFailure>);

impl Display for FunctionParameterBindingFailures {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        for failure in &self.0 {
            write!(f, "  - {}\n", failure)?;
        }
        Ok(())
    }
}

#[derive(Error, Debug)]
pub enum FunctionBindFailure {
    #[error(transparent)]
    ParameterBindingFailures(FunctionParameterBindingFailures),

    #[error("Fatal failure during type matching: {0}")]
    Fatal(Box<dyn Error + Send + Sync + 'static>),
}

#[derive(Error, Debug)]
pub enum FunctionTranslationFailure {
    #[error("Fatal failure during type matching: {0}")]
    Fatal(Box<dyn Error + Send + Sync + 'static>),
}

#[derive(Error, Debug)]
#[error("Attempted {function_def}\n{binding_failures}")]
pub struct ApplyAttempt {
    pub function_def: String,
    pub binding_failures: FunctionParameterBindingFailures,
}

#[derive(Error, Debug)]
pub struct ApplyFailure(pub Vec<ApplyAttempt>);

impl Display for ApplyFailure {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        for attempt in self.0.iter() {
            write!(f, "{}\n", attempt)?;
        }
        Ok(())
    }
}

#[derive(Error, Debug)]
#[error(
    r#"No binding for parameter {name}

This is likely a bug in a FunctionDef implementation. When accessing this class in a lambda
passed to `return_type`, you should only access the parameters defined in `parameters()`.
"#
)]
pub struct NoBindingError {
    pub name: String,
}
