/*
 *  This file is a part of TiledArray.
 *  Copyright (C) 2015  Virginia Tech
 *
 *  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/>.
 *
 *  Edward F Valeev, Justus Calvin
 *  Department of Chemistry, Virginia Tech
 *
 *  permutation_group.h
 *  May 13, 2015
 *
 */

#ifndef TILEDARRAY_SYMM_PERMUTATION_GROUP_H__INCLUDED
#define TILEDARRAY_SYMM_PERMUTATION_GROUP_H__INCLUDED

#include <numeric>
#include <algorithm>
#include <cassert>

#include <TiledArray/symm/permutation.h>

namespace TiledArray {

  namespace symmetry {

    /**
     * \addtogroup symmetry
     * @{
     */

    /// Permutation group

    /// PermutationGroup is a group of permutations. A permutation group is specified compactly by
    /// a generating set (set of permutations that can multiplicatively generate the entire group).
    class PermutationGroup {
    public:
      using Permutation = TiledArray::symmetry::Permutation;
      using element_type = Permutation;

    protected:

      /// Group generators
      std::vector<Permutation> generators_;
      /// Group elements
      std::vector<Permutation> elements_;

    public:

      // Compiler generated functions
      PermutationGroup(const PermutationGroup&) = default;
      PermutationGroup(PermutationGroup&&) = default;
      PermutationGroup& operator=(const PermutationGroup&) = default;
      PermutationGroup& operator=(PermutationGroup&&) = default;

      /// General constructor

      /// This constructs a permutation group from a set of generators.
      /// The order of generators does not matter, and repeated generators
      /// will be ignored (internally generators are stored as a sorted sequence).
      /// \param degree The number of elements in the set whose symmetry this group describes
      /// \param generators The generating set that defines this group
      PermutationGroup(std::vector<Permutation> generators) :
        generators_(std::move(generators))
      {
        init(generators_, elements_);
      }

      /// Group order accessor

      /// The order of the group is the number of elements in the group.
      /// For symmetric group \c G the order is factorial of \c G->degree()
      /// \return The order of the group
      unsigned int order() const { return elements_.size(); }

      /// Idenity element accessor

      /// \return the Identity element
      static Permutation identity() { return Permutation(); }

      /// Group element accessor

      /// \note Elements are ordered lexicograhically.
      /// \param i Index of the group element to be returned, \c 0<=i&&i<order()
      /// \return A const reference to the i-th group element
      const Permutation& operator[](unsigned int i) const {
        TA_ASSERT(i < elements_.size());
        return elements_[i];
      }

      /// Elements vector accessor

      /// \note Elements appear in lexicograhical order.
      /// \return A const reference to the vector of elements
      const std::vector<Permutation>& elements() const { return elements_; }

      /// Generators vector accessor

      /// \note Generators appear in lexicograhical order.
      /// \return A const reference to the vector of generators
      const std::vector<Permutation>& generators() const { return generators_; }

      /// @name Iterator accessors

      /// PermutationGroup iterators dereference to group elements, i.e. Permutation objects.
      /// Iterators can be used to iterate over group elements in lexicographical order. \sa operator[]
      /// @{

      /// forward iterator over the group elements pointing to the first element

      /// \return a std::vector<Permutation>::const_iterator object that points to the first element in the group
      std::vector<Permutation>::const_iterator begin() const {
        return elements_.begin();
      }

      /// forward iterator over the group elements pointing to the first element

      /// \return a std::vector<Permutation>::const_iterator object that points to the first element in the group
      std::vector<Permutation>::const_iterator cbegin() const {
        return elements_.cbegin();
      }

      /// forward iterator over the group elements pointing past the last element

      /// \return a std::vector<Permutation>::const_iterator object that points past the last element in the group
      std::vector<Permutation>::const_iterator end() const {
        return elements_.end();
      }

      /// forward iterator over the group elements pointing past the last element

      /// \return a std::vector<Permutation>::const_iterator object that points past the last element in the group
      std::vector<Permutation>::const_iterator cend() const {
        return elements_.cend();
      }

      /// @}

      /// Computes the domain of this group

      /// \tparam Set a container type in which the result will be returned (e.g. \c std::set )
      /// \return the domain of this permutation, as a sorted sequence
      template <typename Set>
      Set domain() const {
        Set result;
        // sufficient to loop over generators
        for(const auto& e: generators_) {
          const auto e_domain = e.domain<Set>();
          result.insert(e_domain.begin(), e_domain.end());
        }
        return result;
      }


    protected:

      PermutationGroup() {} // makes uninitialized group, all initialization is left to the derived class

      /// computes \c elements from \c generators
      /// \param[in,out] generators sequence set of generators; duplicate generators will be removed, generators will be resorted
      /// \param[out] elements resulting elements
      static void init(std::vector<Permutation>& generators,
                       std::vector<Permutation>& elements) {

        sort(generators.begin(), generators.end());
        auto unique_last = std::unique(generators.begin(), generators.end());
        generators.erase(unique_last, generators.end());
        { // eliminate identity from the generator list
          auto I_iter = std::find(generators.begin(), generators.end(), Permutation());
          if (I_iter != generators.end())
            generators.erase(I_iter);
        }

        // add the identity element first
        elements.emplace_back();

        /// add generators to the elements
        for(const auto& g: generators) {
          elements.push_back(g);
        }

        // Generate the remaining elements in the group by multiplying by generators
        for(unsigned int g = 1u; g < elements.size(); ++g) {
          for(const auto& G: generators) {
            Permutation e = elements[g] * G;
            if(std::find(elements.cbegin(), elements.cend(), e) == elements.cend()) {
              elements.emplace_back(std::move(e));
            }
          }
        }

        sort(elements.begin(), elements.end());
      }

    }; // class PermutationGroup

    /// PermutationGroup equality operator

    /// \param p1 The left-hand permutation group to be compared
    /// \param p2 The right-hand permutation group to be compared
    /// \return \c true if all elements of \c p1 and \c p2 are equal, otherwise \c false.
    inline bool operator==(const PermutationGroup& p1, const PermutationGroup& p2) {
      return (p1.order() == p2.order())
             && p1.elements() == p2.elements();
    }

    /// PermutationGroup inequality operator

    /// \param p1 The left-hand permutation group to be compared
    /// \param p2 The right-hand permutation group to be compared
    /// \return \c true if any element of \c p1 is not equal to that of \c p2,
    /// otherwise \c false.
    inline bool operator!=(const PermutationGroup& p1, const PermutationGroup& p2) {
      return ! operator==(p1, p2);
    }

    /// PermutationGroup less-than operator

    /// \param p1 The left-hand permutation group to be compared
    /// \param p2 The right-hand permutation group to be compared
    /// \return \c true if the elements of \c p1 are lexicographically less than
    /// that of \c p2, otherwise \c false.
    inline bool operator<(const PermutationGroup& p1, const PermutationGroup& p2) {
      return std::lexicographical_compare(p1.cbegin(), p1.cend(),
          p2.cbegin(), p2.cend());
    }

    /// Add permutation group to an output stream

    /// \param[out] output The output stream
    /// \param[in] p The permutation group to be added to the output stream
    /// \return The output stream
    inline std::ostream& operator<<(std::ostream& output, const PermutationGroup& p) {
      output << "{";
      for (auto i=p.cbegin(); i!=p.cend();) {
        output << *i;
        if (++i != p.cend())
          output << ", ";
      }
      output << "}";
      return output;
    }


    /// Symmetric group

    /// Symmetric group of degree \f$ n \f$ is a group of \em all permutations of set \f$ \{x_0, x_1, \dots x_{n-1}\} \f$
    /// where \f$ x_i \f$ are nonnegative integers.
    class SymmetricGroup final: public PermutationGroup {
      public:
        using index_type = Permutation::index_type;

        // Compiler generated functions
        SymmetricGroup() = delete;
        SymmetricGroup(const SymmetricGroup&) = default;
        SymmetricGroup(SymmetricGroup&&) = default;
        SymmetricGroup& operator=(const SymmetricGroup&) = default;
        SymmetricGroup& operator=(SymmetricGroup&&) = default;

        /// Construct symmetric group on domain \f$ \{0, 1, \dots n-1\} \f$, where \f$ n \f$ = \c degree
        /// \param degree the degree of this group
        SymmetricGroup(unsigned int degree) :
          SymmetricGroup(SymmetricGroup::iota_vector(degree))
        {
        }

        /// Construct symmetric group on domain \c [begin,end)
        /// \tparam InputIterator an input iterator type
        /// \param begin iterator pointing to the beginning of the range
        /// \param end iterator pointing to past the end of the range
        template <typename InputIterator,
                  typename std::enable_if< ::TiledArray::detail::is_input_iterator<InputIterator>::value>::type* = nullptr>
        SymmetricGroup(InputIterator begin, InputIterator end) :
          PermutationGroup(), domain_(begin, end)
        {
          for(auto iter=begin; iter!=end; ++iter) {
            TA_ASSERT(*iter >= 0);
          }

          const auto degree = domain_.size();

          // Add generators to the list of elements
          if(degree > 2u) {
            for(unsigned int i = 0u; i < degree; ++i) {
              // Construct the generator and add to the list
              unsigned int i1 = (i + 1u) % degree;
              generators_.emplace_back(Permutation::Map{{domain_[i],domain_[i1]},{domain_[i1],domain_[i]}});
            }
          } else if(degree == 2u) {
            // Construct the generator
            generators_.emplace_back(Permutation::Map{{domain_[0], domain_[1]}, {domain_[1], domain_[0]}});
          }

          init(generators_, elements_);
        }

        /// Construct symmetric group using domain as an initializer list

        /// \tparam Integer an integral type
        /// \param list An initializer list of Integer
        template <typename Integer,
                  typename std::enable_if<std::is_integral<Integer>::value>::type* = nullptr>
        explicit SymmetricGroup(std::initializer_list<Integer> list) :
            SymmetricGroup(list.begin(), list.end())
        {
        }

        /// Degree accessor

        /// The degree of the group is the number of elements in the set on which the group members act
        /// \return The degree of the group
        unsigned int degree() const { return domain_.size(); }

      private:
        std::vector<index_type> domain_;

        /// make vector {0, 1, ... n-1}
        static std::vector<index_type> iota_vector(size_t n) {
          std::vector<index_type> result(n);
          std::iota(result.begin(), result.end(), 0);
          return result;
        }

        SymmetricGroup(const std::vector<index_type>& domain) :
          SymmetricGroup(domain.begin(), domain.end())
        {
        }


    };

    /// determines whether a given MultiIndex is lexicographically smallest
    /// among all indices generated by the action of \c pg.
    /// \tparam MultiIndex a sequence type that is directly addressable, i.e. has a fast \c operator[]
    /// \param idx an Index object
    /// \param pg the PermutationGroup
    /// \return \c false if action of a permutation in \c pg can produce
    ///            an Index that is lexicographically smaller than \c idx (i.e. there exists
    ///            \c i such that \c pg[i]*idx is lexicographically less than \c idx), \c true otherwise
    template <typename MultiIndex>
    bool is_lexicographically_smallest(const MultiIndex& idx,
                                       const PermutationGroup& pg) {
      const auto idx_size = idx.size();
      for(const auto& p: pg) {
        for(size_t i=0; i!=idx_size; ++i) {
          auto idx_i = idx[i];
          auto idx_p_i = idx[p[i]];
          if (idx_p_i < idx_i)
            return false;
          if (idx_p_i > idx_i)
            break;
        }
      }
      return true;
    }

    /// Computes conjugate permutation group obtained by the action of a permutation

    /// Conjugate of group \f$ G \f$ under the action of element \f$ h \f$ is a group
    /// \f$ \{ a: a = h g h^{-1}, \forall g \in G \} \f$ .
    /// \note Since all permutation groups are subgroups of the symmetric group on the "infinite"
    /// set \f$ \{ 0, 1, \dots \} \f$, this can be used to "shift" the domain of \c G by action of
    /// \c h that permutes elements in the domain of \c G with those outside its domain, e.g.
    /// if \f$ G = S_2 \f$ on domain \f$ \{0,1\} \f$, conjugation with \f$ h = {1->2, 2->1} \f$
    /// will produce \f$ S_2 \f$ on domain \f$ \{0,2\} \f$.
    /// \param G input PermutationGroup
    /// \param h input Permutation
    /// \return conjugate PermutationGroup
    inline PermutationGroup conjugate(const PermutationGroup& G, const PermutationGroup::Permutation& h) {
      std::vector<PermutationGroup::Permutation> conjugate_generators;
      const auto h_inv = h.inv();
      for(const auto& generator: G.generators()) {
        conjugate_generators.emplace_back(h * generator * h_inv);
      }
      return PermutationGroup{std::move(conjugate_generators)};
    }

    /// Computes intersect of 2 PermutationGroups
    /// \note The intersect is guaranteed to be a subgroup for both groups, hence this is the largest common subgroup.
    /// \param G1 PermutationGroup
    /// \param G2 PermutationGroup
    /// \return PermutationGroup
    inline PermutationGroup intersect(const PermutationGroup& G1, const PermutationGroup& G2) {
      std::vector<PermutationGroup::Permutation> intersect_elements;
      std::set_intersection(G1.begin(), G1.end(),
                            G2.begin(), G2.end(),
                            std::back_inserter(intersect_elements));
      return PermutationGroup{std::move(intersect_elements)};
    }

    /// Computes the largest subgroup of a permutation group that leaves the given set of indices fixed.

    /// \tparam Set a set of indices
    /// \param G input PermutationGroup
    /// \param f input Set
    /// \return the fixed set subgroup of \c G
    template <typename Set>
    inline PermutationGroup
    stabilizer(const PermutationGroup& G, const Set& f) {
      std::vector<PermutationGroup::Permutation> stabilizer_generators;
      for(const auto& generator: G.generators()) {
        bool fixes_set = true;
        for (const auto& i: f) {
          if (generator.is_in_domain(i)) {
            fixes_set = false;
            break;
          }
        }
        if (fixes_set)
          stabilizer_generators.push_back(generator);
      }
      return PermutationGroup{std::move(stabilizer_generators)};
    }

    /** @}*/

  } // namespace symmetry
} // namespace TiledArray

#endif // TILEDARRAY_SYMM_PERMUTATION_GROUP_H__INCLUDED
