Documentation
use super::super::{error::*, implementation::*, problem::*, receiver::*};

use std::{any::*, collections::*, error::Error, fmt, slice::*};

/// Prefix for [Problems] [Display](fmt::Display).
pub const PROBLEMS_DISPLAY_PREFIX: &str = "";

//
// Problems
//

/// A simple wrapper for a vector of [Problem].
///
/// Useful for accumulating problems, as it implements [ProblemReceiver]. By default it will
/// swallow all problems into its vector. However, it can also be configured to fail fast on
/// specific error types by calling [handle_type_as_critical](Problems::handle_type_as_critical)
/// before use.
///
/// After use, you may want to call [check](Problems::check) to fail if there were problems.
///
/// Note that this type implements [Error] and as such can be used in the causation chain of a
/// [Problem]. This can be thought of as "packing" many problems into one. It can then be
/// "unpacked" via [Problem::into_problems].
#[derive(Debug, Default)]
pub struct Problems {
    /// Problems.
    pub problems: Vector<Problem>,

    /// Critical error type IDs.
    pub critical_error_types: HashSet<TypeId>,
}

impl Problems {
    /// Constructor.
    pub fn with_capacity(capacity: usize) -> Self {
        Vector::with_capacity(capacity).into()
    }

    /// True if there are no problems.
    pub fn is_empty(&self) -> bool {
        self.problems.is_empty()
    }

    /// The number of problems.
    pub fn count(&self) -> usize {
        self.problems.len()
    }

    /// Iterate problems.
    pub fn iter(&self) -> Iter<'_, Problem> {
        self.into_iter()
    }

    /// Marks a top error type as critical.
    pub fn handle_type_as_critical<ErrorT>(&mut self)
    where
        ErrorT: Any + Error,
    {
        self.critical_error_types.insert(TypeId::of::<ErrorT>());
    }

    /// True if the problem's top error is critical.
    pub fn is_critical(&self, problem: &Problem) -> bool {
        problem
            .top()
            .map(|cause| self.is_error_critical(&cause.error))
            .unwrap_or(false)
    }

    /// True if the error is critical.
    pub fn is_error_critical(&self, error: &CapturedError) -> bool {
        self.critical_error_types.contains(&error.type_id())
    }

    /// Add a problem.
    pub fn add<ProblemT>(&mut self, problem: ProblemT)
    where
        ProblemT: Into<Problem>,
    {
        self.problems.push(problem.into())
    }

    /// Fails with self if there is at least one problem.
    ///
    /// Other returns self.
    pub fn check(self) -> Result<Self, Self> {
        if self.is_empty() { Ok(self) } else { Err(self) }
    }

    /// Into unique problems.
    ///
    /// Equality is tested for using the first [CauseEquality](super::super::cause::CauseEquality) or
    /// [CauseComparison](super::super::cause::CauseComparison) found in each problem.
    pub fn into_unique(self) -> Self {
        Self::from_iter(self.iter_unique())
    }

    /// Iterate unique problems.
    ///
    /// Equality is tested for using the first [CauseEquality](super::super::cause::CauseEquality) or
    /// [CauseComparison](super::super::cause::CauseComparison) found in each problem.
    pub fn iter_unique(self) -> impl Iterator<Item = Problem> {
        iter_unique_problems(self.problems)
    }

    /// Iterate unique problems.
    ///
    /// Equality is tested for using the first [CauseEquality](super::super::cause::CauseEquality) or
    /// [CauseComparison](super::super::cause::CauseComparison) found in each problem.
    pub fn iter_unique_refs<'this>(&'this self) -> impl Iterator<Item = &'this Problem> {
        iter_unique_problem_refs(&self.problems)
    }
}

impl ProblemReceiver for Problems {
    fn give(&mut self, problem: Problem) -> Result<(), Problem> {
        // Fail fast if critical
        match self.is_critical(&problem) {
            // Fail fast if critical
            true => Err(problem),

            // Otherwise, swallow
            false => {
                self.add(problem);
                Ok(())
            }
        }
    }
}

impl fmt::Display for Problems {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        let mut iterator = self.into_iter().peekable();
        while let Some(problem) = iterator.next() {
            write!(formatter, "{}{}", PROBLEMS_DISPLAY_PREFIX, problem)?;
            if iterator.peek().is_some() {
                writeln!(formatter)?;
            }
        }
        Ok(())
    }
}

impl Error for Problems {}

impl From<Vector<Problem>> for Problems {
    fn from(problems: Vector<Problem>) -> Self {
        Self {
            problems,
            critical_error_types: Default::default(),
        }
    }
}

impl From<Problem> for Problems {
    fn from(problem: Problem) -> Self {
        let has_problems = |problem: &Problem| {
            for cause in problem {
                if cause.error.downcast_ref::<Problems>().is_some() {
                    return true;
                }
            }
            false
        };

        if has_problems(&problem) {
            for cause in problem {
                if let Ok(problems) = cause.error.downcast::<Problems>() {
                    return *problems;
                }
            }

            panic!("we should have found a Problems");
        }

        let mut problems = Problems::with_capacity(1);
        problems.add(problem);
        problems
    }
}

impl<'this> IntoIterator for &'this Problems {
    type Item = &'this Problem;
    type IntoIter = Iter<'this, Problem>;

    fn into_iter(self) -> Self::IntoIter {
        self.problems.iter()
    }
}

impl FromIterator<Problem> for Problems {
    fn from_iter<IntoIteratorT>(iterator: IntoIteratorT) -> Self
    where
        IntoIteratorT: IntoIterator<Item = Problem>,
    {
        iterator.into_iter().collect::<Vector<_>>().into()
    }
}

impl<ErrorT> FromIterator<ErrorT> for Problems
where
    ErrorT: 'static + Error + Send + Sync,
{
    fn from_iter<IntoIteratorT>(iterator: IntoIteratorT) -> Self
    where
        IntoIteratorT: IntoIterator<Item = ErrorT>,
    {
        Self::from_iter(iterator.into_iter().map(Problem::from))
    }
}