#ifndef __ADT_PRED_HPP__
#define __ADT_PRED_HPP__

#pragma once

#include "adt/function_traits.hpp"

#include <memory>
#include <functional>

namespace pred {

template<typename T>
class TypedPredicate {
 public:
  typedef T checked_type;
  
  template<typename P>
  TypedPredicate(P p)
      : self_(std::make_shared<TypedPredicateModel<P> >(std::move(p))) {}

  bool operator()(T x) const {
    return self_->operator()(x);
  }

 private:
  struct TypedPredicateConcept {
    virtual ~TypedPredicateConcept() {};
    virtual bool operator()(T x) const = 0;
  };

  template<class P>
  struct TypedPredicateModel : TypedPredicateConcept {
    TypedPredicateModel(P p)
        : data_(std::move(p)) {}

    virtual bool operator()(T x) const override {
      return data_(x);
    }

    P data_;
  };

  std::shared_ptr<const TypedPredicateConcept> self_;
};

template<typename T>
class AlwaysTrueOperator {
 public:
  typedef T checked_type;

  bool operator()(T) const {
    return true;
  }
};

template<typename T>
class AlwaysFalseOperator {
  typedef T checked_type;

 public:
  bool operator()(T) const {
    return false;
  }
};

template<typename T>
class AndOperator {
 public:
  typedef T checked_type;

  AndOperator(TypedPredicate<T> lhs, TypedPredicate<T> rhs)
      : lhs_(std::move(lhs)),
        rhs_(std::move(rhs)) { }

  bool operator()(T x) const {
    return lhs_(x) && rhs_(x);
  }

 private:
  const TypedPredicate<T> lhs_, rhs_;
};

template<typename T>
class OrOperator {
 public:
  typedef T checked_type;

  OrOperator(TypedPredicate<T> lhs, TypedPredicate<T> rhs)
      : lhs_(std::move(lhs)), rhs_(std::move(rhs)) { }

  bool operator()(T x) const {
    return lhs_(x) || rhs_(x);
  }

 private:
  const TypedPredicate<T> lhs_, rhs_;
};

template<typename T>
class NotOperator {
 public:
  typedef T checked_type;

  NotOperator(const TypedPredicate<T> p)
      : p_(std::move(p)) {}

  bool operator()(T x) const {
    return !p_(x);
  }

 private:
  const TypedPredicate<T> p_;
};

template<class P,
         bool = adt::function_traits<P>::arity == 1 &&
                std::is_same<typename adt::function_traits<P>::return_type, bool>::value>
struct is_predicate : public std::true_type {};

template<class P>
struct is_predicate<P, false> : public std::false_type {};

template<class TP1, class TP2,
         typename _T1 = typename adt::function_traits<TP1>::template arg<0>::type,
         typename _T2 = typename adt::function_traits<TP2>::template arg<0>::type,
         typename =
           typename std::enable_if<std::is_same<_T1, _T2>::value &&
                                   is_predicate<TP1>::value && is_predicate<TP2>::value
                                   >::type>
TypedPredicate<_T1> And(TP1 lhs, TP2 rhs) {
  return AndOperator<_T1>(lhs, rhs);
}

template<class TP1, class TP2,
         typename _T1 = typename adt::function_traits<TP1>::template arg<0>::type,
         typename _T2 = typename adt::function_traits<TP2>::template arg<0>::type,
         typename =
           typename std::enable_if<std::is_same<_T1, _T2>::value &&
                                   is_predicate<TP1>::value && is_predicate<TP2>::value
                                   >::type>
TypedPredicate<_T1> Or(TP1 lhs, TP2 rhs) {
  return OrOperator<_T1>(lhs, rhs);
}

template<class TP,
         typename _T = typename adt::function_traits<TP>::template arg<0>::type,
         typename =
           typename std::enable_if<is_predicate<TP>::value>::type>
TypedPredicate<_T> Not(TP p) {
  return NotOperator<_T>(p);
}

template<class T>
TypedPredicate<T> AlwaysTrue() {
  return AlwaysTrueOperator<T>();
}
template<class T>
TypedPredicate<T> AlwaysFalse() {
  return AlwaysFalseOperator<T>();
}

} // namespace pred

#endif // __ADT_PRED_HPP__
