use std::borrow::Cow;
use std::error::Error;
use std::fmt;

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TypeError {
    type_name: Cow<'static, str>,
    reason: Cow<'static, str>,
    suggestion: Cow<'static, str>,
}

impl TypeError {
    pub fn new(
        type_name: impl Into<Cow<'static, str>>,
        reason: impl Into<Cow<'static, str>>,
        suggestion: impl Into<Cow<'static, str>>,
    ) -> Self {
        Self {
            type_name: type_name.into(),
            reason: reason.into(),
            suggestion: suggestion.into(),
        }
    }

    pub fn empty(type_name: impl Into<Cow<'static, str>>) -> Self {
        Self::new(
            type_name,
            "value must not be empty",
            "provide a non-empty value",
        )
    }

    pub fn parse(
        type_name: impl Into<Cow<'static, str>>,
        expected: impl Into<Cow<'static, str>>,
    ) -> Self {
        Self::new(
            type_name,
            expected,
            "check the type definition and provide a matching value",
        )
    }

    pub fn range(
        type_name: impl Into<Cow<'static, str>>,
        rule: impl Into<Cow<'static, str>>,
    ) -> Self {
        Self::new(type_name, rule, "provide a value inside the declared range")
    }

    pub fn type_name(&self) -> &str {
        &self.type_name
    }

    pub fn reason(&self) -> &str {
        &self.reason
    }

    pub fn suggestion(&self) -> &str {
        &self.suggestion
    }
}

impl fmt::Display for TypeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "invalid value for {}\nreason: {}\nplease try: {}",
            self.type_name, self.reason, self.suggestion
        )
    }
}

impl Error for TypeError {}

#[cfg(feature = "errors")]
impl miette::Diagnostic for TypeError {
    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
        Some(Box::new("types::invalid_value"))
    }

    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
        Some(Box::new(self.suggestion()))
    }
}
