//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2019 James D. Mitchell
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

// This file contains the declaration of the PBR class.

#ifndef LIBSEMIGROUPS_PBR_HPP_
#define LIBSEMIGROUPS_PBR_HPP_

#include <cstddef>           // for size_t
#include <cstdint>           // for uint32_t, int32_t
#include <initializer_list>  // for initializer_list
#include <iosfwd>            // for ostream, ostringstream
#include <type_traits>       // for forward
#include <vector>            // for vector, operator<, operator==, allocator

#include "adapters.hpp"  // for Hash

namespace libsemigroups {

  //! Class for partitioned binary relations (PBR).
  //!
  //! Partitioned binary relations (PBRs) are a generalisation of bipartitions,
  //! which were introduced by
  //! [Martin and Mazorchuk](https://arxiv.org/abs/1102.0862).
  class PBR {
    friend void validate(PBR const& x);

   public:
    //! Type of constructor argument.
    template <typename T>
    using vector_type = std::vector<std::vector<T>> const&;

    //! Type of constructor argument.
    template <typename T>
    using initializer_list_type = std::initializer_list<std::vector<T>> const&;

    //! Deleted.
    PBR() = delete;

    //! Default copy constructor.
    PBR(PBR const&) = default;

    //! Default move constructor.
    PBR(PBR&&) = default;

    //! Default copy assignment operator.
    PBR& operator=(PBR const&) = default;

    //! Default move assignment operator.
    PBR& operator=(PBR&&) = default;

    //! Construct from adjacencies \c 0 to `2n - 1`.
    //!
    //! The parameter \p x must be a container of vectors of
    //! \c uint32_t with size \f$2n\f$ for some integer
    //! \f$n\f$, the vector in position \f$i\f$ is the list of points adjacent
    //! to \f$i\f$ in the PBR constructed.
    //!
    //! \param  x the container of vectors of adjacencies.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \warning
    //! No checks whatsoever on the validity of \p x are performed.
    //!
    //! \sa \ref libsemigroups::validate(PBR const&)
    explicit PBR(vector_type<uint32_t> x);

    //! \copydoc PBR(vector_type<uint32_t>)
    explicit PBR(initializer_list_type<uint32_t> x);

    //! Construct empty PBR of given \ref degree.
    //!
    //! \param n the degree
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    explicit PBR(size_t n);

    //! Construct from adjacencies \c 1 to \c n and \c -1 to \c
    //! -n.
    //!
    //! The parameters \p left and \p right should be containers of
    //! \f$n\f$ vectors of integer values, so that
    //! the vector in position \f$i\f$ of \p left is the list of points
    //! adjacent to \f$i\f$ in the PBR, and the vector in position \f$i\f$
    //! of \p right is the list of points adjacent to \f$n + i\f$ in the PBR.
    //! A negative value \f$i\f$ corresponds to \f$n - i\f$.
    //!
    //! \param left container of adjacencies of \c 1 to \c n
    //! \param right container of adjacencies of \c n + 1 to \c 2n.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \warning
    //! No checks whatsoever on the validity of \p left or \p right are
    //! performed.
    //!
    //! \sa libsemigroups::validate(PBR const&) and
    //! make(initializer_list_type<int32_t>, initializer_list_type<int32_t>)
    PBR(initializer_list_type<int32_t> left,
        initializer_list_type<int32_t> right);

    // clang-format off
    //! \copydoc PBR(initializer_list_type<int32_t>, initializer_list_type<int32_t>) <!-- NOLINT(whitespace/line_length) -->
    // clang-format on
    PBR(vector_type<int32_t> left, vector_type<int32_t> right);

    //! Construct and validate.
    //!
    //! \tparam T the types of the arguments
    //!
    //! \param args the arguments to forward to the constructor.
    //!
    //! \returns
    //! A PBR constructed from \p args and validated.
    //!
    //! \throws LibsemigroupsException if libsemigroups::validate(PBR const&)
    //! throws when called with the constructed PBR.
    template <typename... T>
    static PBR make(T... args) {
      // TODO(later) validate_args
      PBR result(std::forward<T>(args)...);
      validate(result);
      return result;
    }

    //! Construct and validate.
    //!
    //! \param args the arguments to forward to the constructor.
    //!
    //! \returns
    //! A PBR constructed from \p args and validated.
    //!
    //! \throws LibsemigroupsException if libsemigroups::validate(PBR const&)
    //! throws when called with the constructed PBR.
    static PBR make(initializer_list_type<uint32_t> args) {
      return make<decltype(args)>(args);
    }

    //! Construct and validate.
    //!
    //! \param left the 1st argument to forward to the constructor.
    //! \param right the 2nd argument to forward to the constructor.
    //!
    //! \returns
    //! A PBR constructed from \p args and validated.
    //!
    //! \throws LibsemigroupsException if libsemigroups::validate(PBR const&)
    //! throws when called with the constructed PBR.
    static PBR make(initializer_list_type<int32_t> left,
                    initializer_list_type<int32_t> right) {
      return make<decltype(left), decltype(right)>(left, right);
    }

    //! Returns the degree of a PBR.
    //!
    //! The *degree* of a PBR is half the number of points in the PBR.
    //!
    //! \parameters
    //! (None)
    //!
    //! \returns
    //! A value of type \c size_t.
    //!
    //! \exceptions
    //! \noexcept
    size_t degree() const noexcept;

    //! Returns the identity PBR with degree degree().
    //!
    //! This member function returns a new PBR with degree equal to the degree
    //! of \c this where every value is adjacent to its negative. Equivalently,
    //! \f$i\f$ is adjacent \f$i + n\f$ and vice versa for every \f$i\f$ less
    //! than the degree \f$n\f$.
    //!
    //! \parameters
    //! (None)
    //!
    //! \returns
    //! A PBR.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    PBR identity() const;

    //! Returns the identity PBR with specified degree.
    //!
    //! This function returns a new PBR with degree equal to \p n where every
    //! value is adjacent to its negative. Equivalently, \f$i\f$ is adjacent
    //! \f$i + n\f$ and vice versa for every \f$i\f$ less than the degree
    //! \f$n\f$.
    //!
    //! \param n the degree.
    //!
    //! \returns
    //! A PBR.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    static PBR identity(size_t n);

    //! Multiply two PBR objects and store the product in \c this.
    //!
    //! Replaces the contents of \c this by the product of \p x and \p y.
    //!
    //! The parameter \p thread_id is required since some temporary storage is
    //! required to find the product of \p x and \p y.  Note that if different
    //! threads call this member function with the same value of \p thread_id
    //! then bad things will happen.
    //!
    //! \param x a PBR.
    //! \param y a PBR.
    //! \param thread_id the index of the calling thread (defaults to \c 0).
    //!
    //! \returns
    //! (None)
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \warning
    //! No checks are made on whether or not the parameters are compatible. If
    //! \p x and \p y have different degrees, then bad things will happen.
    void product_inplace(PBR const& x, PBR const& y, size_t thread_id = 0);

    //! Check equality.
    //!
    //! \param that a PBR
    //!
    //! \returns \c true if \c this equals \p that, and \c false otherwise.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst linear in degree().
    bool operator==(PBR const& that) const {
      return _vector == that._vector;
    }

    //! Compare.
    //!
    //! \param that a PBR object
    //!
    //! \returns \c true if \c this is less than \p that, and \c false
    //! otherwise.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst linear in degree().
    bool operator<(PBR const& that) const {
      return _vector < that._vector;
    }

    //! Returns a reference to the points adjacent to a given point.
    //!
    //! \param i the point.
    //!
    //! \returns A value reference to a `std::vector<uint32_t>`.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    std::vector<uint32_t>& operator[](size_t i) {
      return _vector[i];
    }

    //! Returns a const reference to the points adjacent to a given point.
    //!
    //! \param i the point.
    //!
    //! \returns A value const reference to a `std::vector<uint32_t>`.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    std::vector<uint32_t> const& operator[](size_t i) const {
      return _vector[i];
    }

    //! Returns a hash value for a PBR.
    //!
    //! This value is recomputed every time this function is called.
    //!
    //! \returns A hash value for a \c this.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Linear in `degree()`.
    //!
    //! \parameters
    //! (None)
    // not noexcept because Hash<T>::operator() isn't
    size_t hash_value() const {
      return Hash<std::vector<std::vector<uint32_t>>>()(_vector);
    }

    //! Insertion operator
    //!
    //! This member function allows PBR objects to be inserted into an
    //! \ostringstream
    friend std::ostringstream& operator<<(std::ostringstream&, PBR const&);

    //! Insertion operator
    //!
    //! This member function allows PBR objects to be inserted into an \ostream.
    friend std::ostream& operator<<(std::ostream&, PBR const&);

   private:
    std::vector<std::vector<uint32_t>> _vector;
  };

  //! Validate a PBR.
  //!
  //! This function throws a LibsemigroupsException if
  //! the argument \p x is not valid.
  //!
  //! \param x the PBR to validate.
  //!
  //! \returns
  //! (None)
  //!
  //! \throws LibsemigroupsException if any of the following hold:
  //! * \p x does not describe a binary relation on an even number of points;
  //! * \p x has a point related to a point that is greater than degree()
  //! * a list of points related to a point is not sorted.
  //!
  //! \complexity
  //! Linear in the PBR::degree of \p x.
  void validate(PBR const& x);

  //! Multiply two PBRs.
  //!
  //! Returns a newly constructed PBR equal to the product of \p x and \p y.
  //!
  //! \param x a PBR
  //! \param y a PBR
  //!
  //! \returns
  //! A value of type \c PBR
  //!
  //! \exceptions
  //! \no_libsemigroups_except
  //!
  //! \complexity
  //! Cubic in degree().
  PBR operator*(PBR const& x, PBR const& y);

  //! Check PBRs for inequality.
  //!
  //! \param x a PBR
  //! \param y a PBR
  //!
  //! \returns
  //! A value of type \c bool.
  //!
  //! \exceptions
  //! \no_libsemigroups_except
  //!
  //! \complexity
  //! At worst quadratic in the degree of \p x and \p y.
  inline bool operator!=(PBR const& x, PBR const& y) {
    return !(x == y);
  }

  //! Convenience function that just calls ``operator<``.
  inline bool operator>(PBR const& x, PBR const& y) {
    return y < x;
  }

  //! Convenience function that just calls ``operator<`` and ``operator==``.
  inline bool operator<=(PBR const& x, PBR const& y) {
    return x < y || x == y;
  }

  //! Convenience function that just calls ``operator<=``.
  inline bool operator>=(PBR const& x, PBR const& y) {
    return y <= x;
  }

  namespace detail {

    template <typename T>
    struct IsPBRHelper : std::false_type {};

    template <>
    struct IsPBRHelper<PBR> : std::true_type {};

  }  // namespace detail

  //! Helper variable template.
  //!
  //! The value of this variable is \c true if the template parameter \p T is
  //! \ref PBR.
  //!
  //! \tparam T a type.
  template <typename T>
  static constexpr bool IsPBR = detail::IsPBRHelper<T>::value;

  ////////////////////////////////////////////////////////////////////////
  // Adapters
  ////////////////////////////////////////////////////////////////////////

  //! Returns the approximate time complexity of multiplying PBRs.
  //!
  //! The approximate time complexity of multiplying PBRs is \f$2n ^ 3\f$
  //! where \f$n\f$ is the degree.
  template <>
  struct Complexity<PBR> {
    //! Call operator.
    //!
    //! \param x a PBR.
    //!
    //! \returns
    //! A value of type `size_t` representing the complexity of multiplying the
    //! parameter \p x by another PBR of the same degree.
    //!
    //! \exceptions
    //! \noexcept
    //!
    //! \complexity
    //! Constant.
    size_t operator()(PBR const& x) const noexcept {
      return 8 * x.degree() * x.degree() * x.degree();
    }
  };

  template <>
  struct Degree<PBR> {
    size_t operator()(PBR const& x) const noexcept {
      return x.degree();
    }
  };

  template <>
  struct Hash<PBR> {
    size_t operator()(PBR const& x) const {
      return x.hash_value();
    }
  };

  template <>
  struct One<PBR> {
    PBR operator()(PBR const& x) const {
      return (*this)(x.degree());
    }

    PBR operator()(size_t N = 0) const {
      return PBR::identity(N);
    }
  };

  template <>
  struct Product<PBR> {
    void operator()(PBR& xy, PBR const& x, PBR const& y, size_t thread_id = 0) {
      xy.product_inplace(x, y, thread_id);
    }
  };

  template <>
  struct IncreaseDegree<PBR> {
    void operator()(PBR&, size_t) {}
  };

}  // namespace libsemigroups
#endif  // LIBSEMIGROUPS_PBR_HPP_
