use crate::ConfigRange;
use core::fmt;
use std::{convert::Infallible, error::Error as StdError};

pub type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(Debug)]
pub struct Error {
    kind: ErrorKind,
    source: Box<dyn StdError + Send + Sync>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ErrorKind {
    NegotiationFailed,
    Other,
}

impl Error {
    pub fn new<E: StdError + Send + Sync + 'static>(kind: ErrorKind, error: E) -> Self {
        Self {
            kind,
            source: Box::new(error),
        }
    }

    pub fn negotiation_failed<A: ConfigRange, B: ConfigRange>(a: Vec<A>, b: Vec<B>) -> Self {
        Self {
            kind: ErrorKind::NegotiationFailed,
            source: Box::new(IncompatibleConfig(a, b)),
        }
    }

    pub fn msg(msg: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Other,
            source: Box::<dyn StdError + Send + Sync>::from(msg.into()),
        }
    }

    pub fn other<E: StdError + Send + Sync + 'static>(error: E) -> Self {
        Self {
            kind: ErrorKind::Other,
            source: Box::new(error),
        }
    }

    pub fn kind(&self) -> ErrorKind {
        self.kind
    }
}

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        Some(&*self.source)
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            let mut prev: &dyn StdError = &*self.source;

            writeln!(f, "{}", prev)?;

            while let Some(source) = prev.source() {
                writeln!(f, "  {}", source)?;
                prev = source;
            }

            Ok(())
        } else {
            self.source.fmt(f)
        }
    }
}

#[derive(Debug)]
pub struct IncompatibleConfig<A, B>(Vec<A>, Vec<B>);

impl<A: ConfigRange, B: ConfigRange> fmt::Display for IncompatibleConfig<A, B> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Incompatible configs: {:?} {:?}", self.0, self.1)
    }
}

impl<A: ConfigRange, B: ConfigRange> StdError for IncompatibleConfig<A, B> {}

impl From<Infallible> for Error {
    fn from(value: Infallible) -> Self {
        match value {}
    }
}
