// Copyright (c) 2018 The predicates-rs Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/license/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! Definition of `Predicate`s for comparisons of membership in a set.

use std::collections::HashSet;
use std::fmt;
use std::hash::Hash;
use std::iter::FromIterator;

use crate::reflection;
use crate::utils;
use crate::Predicate;

/// Predicate that returns `true` if `variable` is a member of the pre-defined
/// set, otherwise returns `false`.
///
/// Note that this implementation places the fewest restrictions on the
/// underlying `Item` type at the expense of having the least performant
/// implementation (linear search). If the type to be searched is `Hash + Eq`,
/// it is much more efficient to use `HashableInPredicate` and
/// `in_hash`. The implementation-specific predicates will be
/// deprecated when Rust supports trait specialization.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InPredicate<T>
where
    T: PartialEq + fmt::Debug,
{
    inner: utils::DebugAdapter<Vec<T>>,
}

impl<T> InPredicate<T>
where
    T: Ord + fmt::Debug,
{
    /// Creates a new predicate that will return `true` when the given `variable` is
    /// contained with the set of items provided.
    ///
    /// Note that this implementation requires `Item` to be `Ord`. The
    /// `InPredicate` uses a less efficient search algorithm but only
    /// requires `Item` implement `PartialEq`. The implementation-specific
    /// predicates will be deprecated when Rust supports trait specialization.
    ///
    /// # Examples
    ///
    /// ```
    /// use predicates::prelude::*;
    ///
    /// let predicate_fn = predicate::in_iter(vec![1, 3, 5]).sort();
    /// assert_eq!(true, predicate_fn.eval(&1));
    /// assert_eq!(false, predicate_fn.eval(&2));
    /// assert_eq!(true, predicate_fn.eval(&3));
    ///
    /// let predicate_fn = predicate::in_iter(vec!["a", "c", "e"]).sort();
    /// assert_eq!(true, predicate_fn.eval("a"));
    /// assert_eq!(false, predicate_fn.eval("b"));
    /// assert_eq!(true, predicate_fn.eval("c"));
    /// ```
    pub fn sort(self) -> OrdInPredicate<T> {
        let mut items = self.inner.debug;
        items.sort();
        OrdInPredicate {
            inner: utils::DebugAdapter::new(items),
        }
    }
}

impl<T> Predicate<T> for InPredicate<T>
where
    T: PartialEq + fmt::Debug,
{
    fn eval(&self, variable: &T) -> bool {
        self.inner.debug.contains(variable)
    }

    fn find_case<'a>(&'a self, expected: bool, variable: &T) -> Option<reflection::Case<'a>> {
        utils::default_find_case(self, expected, variable).map(|case| {
            case.add_product(reflection::Product::new(
                "var",
                utils::DebugAdapter::new(variable).to_string(),
            ))
        })
    }
}

impl<'a, T> Predicate<T> for InPredicate<&'a T>
where
    T: PartialEq + fmt::Debug + ?Sized,
{
    fn eval(&self, variable: &T) -> bool {
        self.inner.debug.contains(&variable)
    }

    fn find_case<'b>(&'b self, expected: bool, variable: &T) -> Option<reflection::Case<'b>> {
        utils::default_find_case(self, expected, variable).map(|case| {
            case.add_product(reflection::Product::new(
                "var",
                utils::DebugAdapter::new(variable).to_string(),
            ))
        })
    }
}

impl<T> reflection::PredicateReflection for InPredicate<T>
where
    T: PartialEq + fmt::Debug,
{
    fn parameters<'a>(&'a self) -> Box<dyn Iterator<Item = reflection::Parameter<'a>> + 'a> {
        let params = vec![reflection::Parameter::new("values", &self.inner)];
        Box::new(params.into_iter())
    }
}

impl<T> fmt::Display for InPredicate<T>
where
    T: PartialEq + fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let palette = crate::Palette::current();
        write!(
            f,
            "{} {} {}",
            palette.var.paint("var"),
            palette.description.paint("in"),
            palette.expected.paint("values")
        )
    }
}

/// Creates a new predicate that will return `true` when the given `variable` is
/// contained with the set of items provided.
///
/// Note that this implementation places the fewest restrictions on the
/// underlying `Item` type at the expense of having the least performant
/// implementation (linear search). If the type to be searched is `Hash + Eq`,
/// it is much more efficient to use `HashableInPredicate` and
/// `in_hash`. The implementation-specific predicates will be
/// deprecated when Rust supports trait specialization.
///
/// If you need to optimize this
/// - Type is `Ord`, call `sort()` on this predicate.
/// - Type is `Hash`, replace `in_iter` with `in_hash`.
///
/// # Examples
///
/// ```
/// use predicates::prelude::*;
///
/// let predicate_fn = predicate::in_iter(vec![1, 3, 5]);
/// assert_eq!(true, predicate_fn.eval(&1));
/// assert_eq!(false, predicate_fn.eval(&2));
/// assert_eq!(true, predicate_fn.eval(&3));
///
/// let predicate_fn = predicate::in_iter(vec!["a", "c", "e"]);
/// assert_eq!(true, predicate_fn.eval("a"));
/// assert_eq!(false, predicate_fn.eval("b"));
/// assert_eq!(true, predicate_fn.eval("c"));
/// ```
pub fn in_iter<I, T>(iter: I) -> InPredicate<T>
where
    T: PartialEq + fmt::Debug,
    I: IntoIterator<Item = T>,
{
    InPredicate {
        inner: utils::DebugAdapter::new(Vec::from_iter(iter)),
    }
}

/// Predicate that returns `true` if `variable` is a member of the pre-defined
/// set, otherwise returns `false`.
///
/// Note that this implementation requires `Item` to be `Ord`. The
/// `InPredicate` uses a less efficient search algorithm but only
/// requires `Item` implement `PartialEq`. The implementation-specific
/// predicates will be deprecated when Rust supports trait specialization.
///
/// This is created by the `predicate::in_iter(...).sort` function.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrdInPredicate<T>
where
    T: Ord + fmt::Debug,
{
    inner: utils::DebugAdapter<Vec<T>>,
}

impl<T> Predicate<T> for OrdInPredicate<T>
where
    T: Ord + fmt::Debug,
{
    fn eval(&self, variable: &T) -> bool {
        self.inner.debug.binary_search(variable).is_ok()
    }

    fn find_case<'a>(&'a self, expected: bool, variable: &T) -> Option<reflection::Case<'a>> {
        utils::default_find_case(self, expected, variable).map(|case| {
            case.add_product(reflection::Product::new(
                "var",
                utils::DebugAdapter::new(variable).to_string(),
            ))
        })
    }
}

impl<'a, T> Predicate<T> for OrdInPredicate<&'a T>
where
    T: Ord + fmt::Debug + ?Sized,
{
    fn eval(&self, variable: &T) -> bool {
        self.inner.debug.binary_search(&variable).is_ok()
    }

    fn find_case<'b>(&'b self, expected: bool, variable: &T) -> Option<reflection::Case<'b>> {
        utils::default_find_case(self, expected, variable).map(|case| {
            case.add_product(reflection::Product::new(
                "var",
                utils::DebugAdapter::new(variable).to_string(),
            ))
        })
    }
}

impl<T> reflection::PredicateReflection for OrdInPredicate<T>
where
    T: Ord + fmt::Debug,
{
    fn parameters<'a>(&'a self) -> Box<dyn Iterator<Item = reflection::Parameter<'a>> + 'a> {
        let params = vec![reflection::Parameter::new("values", &self.inner)];
        Box::new(params.into_iter())
    }
}

impl<T> fmt::Display for OrdInPredicate<T>
where
    T: Ord + fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let palette = crate::Palette::current();
        write!(
            f,
            "{} {} {}",
            palette.var.paint("var"),
            palette.description.paint("in"),
            palette.expected.paint("values")
        )
    }
}

/// Predicate that returns `true` if `variable` is a member of the pre-defined
/// `HashSet`, otherwise returns `false`.
///
/// Note that this implementation requires `Item` to be `Hash + Eq`. The
/// `InPredicate` uses a less efficient search algorithm but only
/// requires `Item` implement `PartialEq`. The implementation-specific
/// predicates will be deprecated when Rust supports trait specialization.
///
/// This is created by the `predicate::in_hash` function.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HashableInPredicate<T>
where
    T: Hash + Eq + fmt::Debug,
{
    inner: utils::DebugAdapter<HashSet<T>>,
}

impl<T> Predicate<T> for HashableInPredicate<T>
where
    T: Hash + Eq + fmt::Debug,
{
    fn eval(&self, variable: &T) -> bool {
        self.inner.debug.contains(variable)
    }

    fn find_case<'a>(&'a self, expected: bool, variable: &T) -> Option<reflection::Case<'a>> {
        utils::default_find_case(self, expected, variable).map(|case| {
            case.add_product(reflection::Product::new(
                "var",
                utils::DebugAdapter::new(variable).to_string(),
            ))
        })
    }
}

impl<'a, T> Predicate<T> for HashableInPredicate<&'a T>
where
    T: Hash + Eq + fmt::Debug + ?Sized,
{
    fn eval(&self, variable: &T) -> bool {
        self.inner.debug.contains(&variable)
    }

    fn find_case<'b>(&'b self, expected: bool, variable: &T) -> Option<reflection::Case<'b>> {
        utils::default_find_case(self, expected, variable).map(|case| {
            case.add_product(reflection::Product::new(
                "var",
                utils::DebugAdapter::new(variable).to_string(),
            ))
        })
    }
}

impl<T> reflection::PredicateReflection for HashableInPredicate<T>
where
    T: Hash + Eq + fmt::Debug,
{
    fn parameters<'a>(&'a self) -> Box<dyn Iterator<Item = reflection::Parameter<'a>> + 'a> {
        let params = vec![reflection::Parameter::new("values", &self.inner)];
        Box::new(params.into_iter())
    }
}

impl<T> fmt::Display for HashableInPredicate<T>
where
    T: Hash + Eq + fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let palette = crate::Palette::current();
        write!(
            f,
            "{} {} {}",
            palette.var.paint("var"),
            palette.description.paint("in"),
            palette.expected.paint("values")
        )
    }
}

/// Creates a new predicate that will return `true` when the given `variable` is
/// contained with the set of items provided.
///
/// Note that this implementation requires `Item` to be `Hash + Eq`. The
/// `InPredicate` uses a less efficient search algorithm but only
/// requires `Item` implement `PartialEq`. The implementation-specific
/// predicates will be deprecated when Rust supports trait specialization.
///
/// # Examples
///
/// ```
/// use predicates::prelude::*;
///
/// let predicate_fn = predicate::in_hash(vec![1, 3, 5]);
/// assert_eq!(true, predicate_fn.eval(&1));
/// assert_eq!(false, predicate_fn.eval(&2));
/// assert_eq!(true, predicate_fn.eval(&3));
///
/// let predicate_fn = predicate::in_hash(vec!["a", "c", "e"]);
/// assert_eq!(true, predicate_fn.eval("a"));
/// assert_eq!(false, predicate_fn.eval("b"));
/// assert_eq!(true, predicate_fn.eval("c"));
/// ```
pub fn in_hash<I, T>(iter: I) -> HashableInPredicate<T>
where
    T: Hash + Eq + fmt::Debug,
    I: IntoIterator<Item = T>,
{
    HashableInPredicate {
        inner: utils::DebugAdapter::new(HashSet::from_iter(iter)),
    }
}
