///////////////////////////////////////////////////////////////////////////////
// GroupDecomposer.hh
//
// This file decomposes the given symmetry group in a way that makes it easy to
// compute the canonical representative of an equivalence class.
///////////////////////////////////////////////////////////////////////////////
#ifndef GROUPDECOMPOSER_HH
#define GROUPDECOMPOSER_HH

#include "TriangNode.hh"
#include "Symmetry.hh"
#include "GKZ.hh"
#include "mptopcom_typedefs.hh"
#include "CerrWrappers.hh"

#include <map>
#include <list>

#include <polymake/Map.h>
#include <polymake/Set.h>
#include <polymake/Series.h>

// Trying to deal with permlib
#include <polymake/group/permlib.h>

class GroupDecomposer;

pm::Array<int> symmetry2array(const Symmetry& s){
   pm::Array<int> result(s.n());
   for(int i=0; i<s.n(); i++){
      result[i] = s[i];
   }
   return result;
}

Symmetry array2symmetry(const pm::Array<int>& s){
   Symmetry result(s.size());
   for(int i=0; i<s.size(); i++){
      result[i] = s[i];
   }
   return result;
}

// Compute inverse permutation
Symmetry inverse(const Symmetry& g){
   // FIXME
   // Is this right? Can it be done better?
   Symmetry result(g);
   // std::cout << "Maxindex: " << g.maxindex() << std::endl;
   for(int i=0; i<g.maxindex(); i++){
      result[g[i]] = i;
   }
   return result;
}

// Check whether the signature 'sig1' refines the signature 'sig2', i.e. for
// every set of 'sig1' there is a set in 'sig2' containing it.
bool refines(const gkz_vector_type& sig1, const gkz_vector_type& sig2){
   for(int i=0; i<sig1.dim(); i++){
      if(sig2[i] != sig2[sig1[i]]){
         return false;
      }
   }
   return true;
}

// Check whether a permutation (non-)fixes entry n(=i).
struct non_fixed {
   int i;
   non_fixed(int n): i(n){}
   bool operator() (const Symmetry& s) { return s[i] != i; }
};

// Get signature of a permutation, i.e. point orbits.
const gkz_vector_type signature(const Symmetry& g) {
   gkz_vector_type result(g.n());
   pm::Set<int> all(pm::range<int>(0,g.n()-1));
   while(all.size() > 0){
      int current = all.front();
      all -= current;
      result[current] = current;
      int next = g[current];
      while(next != current){
         result[next] = current;
         all -= next;
         next = g[next];
      }
   }
   return result;
}

// Save group elements as pairs of g and the inverse to avoid recomputing.
typedef Symmetry gp_elem;

class GroupDecomposer {
   private:
      mutable int bound;
      const gkz_type& gkz;
      const SymmetryGroup& group;
      gp_elem identity;
      std::list<gp_elem> group_elems;
      std::map<int, std::list<gp_elem> > fixed_pt_filter;
      std::map<int, std::map<int, gp_elem>> switch_table;
      std::map<int, pm::Set<int>> supports;
      
      std::map<int, std::map<int, std::map<gkz_vector_type, gp_elem>>> triply_filtered_group;
      const pm::Map<basis_type, int>& basis2int;
      const pm::Map<int, basis_type>& int2basis;
      int depth;

      bool fixes(int i, const gp_elem& g){
         const basis_type& b = int2basis[i];
         const basis_type image = g(b);
         return b == image;
      }

      int nFixedPts(const gp_elem& g){
         int result = 0;
         while(fixes(result, g)){
            result++;
         }
         return result;
      }

      bool insert_element(const Symmetry& g){
         int i = nFixedPts(g);
         if(i>depth){
            depth = i;
         }
         int j = i;
         while(basis2int[g(int2basis[j])] != i){
            j = basis2int[g(int2basis[j])];
         };
         gkz_vector_type sig(signature(g));
         if(triply_filtered_group.count(i) == 0){
            triply_filtered_group[i] = std::map<int, std::map<gkz_vector_type, gp_elem>>();
         }
         if(triply_filtered_group[i].count(j) == 0){
            (triply_filtered_group[i])[j] = std::map<gkz_vector_type, gp_elem>();
            ((triply_filtered_group[i])[j])[sig] = g;
         } else {
            if(((triply_filtered_group[i])[j]).count(sig)==0){
               bool toadd = true;
               for(const auto c:(triply_filtered_group[i])[j]){
                  if(refines(c.first, sig)){
                     toadd = false;
                     return false;
                  }
               }
               std::map<gkz_vector_type, gp_elem>::iterator iter = ((triply_filtered_group[i])[j]).begin();
               std::map<gkz_vector_type, gp_elem>::iterator endIter = ((triply_filtered_group[i])[j]).end();
               for(; iter != endIter;)
               {
                  if (refines(sig, iter->first) ){
                     ((triply_filtered_group[i])[j]).erase(iter++);
                  } else {
                     ++iter;
                  }
               }
               if(toadd){
                  ((triply_filtered_group[i])[j])[sig] = g;
                  return true;
               }
            }
         }
         return false;
      }

      void init_double_filter(){
         int i;
         for(int j=0; j<fixed_pt_filter.size(); j++){
            switch_table[j] = std::map<int, gp_elem>();
            supports[j] = pm::Set<int>();
            for(const auto& g : fixed_pt_filter[j]){
               i = j;
               while(g[i] != j){
                  i++;
               }
               if(i != j){
                  if(switch_table[j].count(i) == 0){
                     (switch_table[j])[i] = g;
                     supports[j] += i;
                  }
               }
            }
         }
         if(CommandlineOptions::verbose()){
            cerrFirstWorker << "GKZ level filtration:" << std::endl;
            for(int j=0; j<fixed_pt_filter.size(); j++){
               cerrFirstWorker << "Level " << j << ": ";
               cerrFirstWorker << supports[j].size() << " phi_" << j << " = ";
               cerrFirstWorker << (supports[j].size()+1) << std::endl;
            }
         }
      }

      void init(){
         int k=0;
         // Filter group by signature.
         // Populate first stage of filter by fixed pts.
         for(SymmetryGroup::const_iterator sym_iter = group.begin();
               sym_iter != group.end();
               ++sym_iter){
            Symmetry g(*sym_iter);
            insert_element(g);
            group_elems.push_back(g);
            k++;
            fixed_pt_filter[0].push_back(g);
         }

         // Make identity element.
         identity = gp_elem(fixed_pt_filter[0].front());
         for(int i=0; i<identity.maxindex(); i++){
            identity[i] = i;
         }
         int i = 0;
         // Filter by fixed pts
         while(fixed_pt_filter[i].size() != 0){
            fixed_pt_filter[i+1] = fixed_pt_filter[i];
            fixed_pt_filter[i+1].remove_if(non_fixed(i));
            i++;
         }
         // We save this bound to prune search tree later on.
         bound = i+1;
         // Populate double_filter, this is (probably) the same as the strong
         // generators from permlib.
         init_double_filter();
      }


   public:
      GroupDecomposer(const SymmetryGroup& group_in,
            const gkz_type& gkz_in):
         group(group_in), gkz(gkz_in), bound(0), depth(0),
         basis2int(gkz_in.get_basis2int()),
         int2basis(gkz_in.get_int2basis()) {
            if(group.size() == 0){return;}
            init();
            if(CommandlineOptions::verbose()){
               cerrFirstWorker << "Final depth: " << depth << std::endl;
               for(int i=0; i<=depth; i++){
                  int n = triply_filtered_group[i].size();
                  int total = 0;
                  for(const auto& s : triply_filtered_group[i]){
                     total += (s.second).size();
                  }
                  cerrFirstWorker << "Level: " << i << " - signatures: " << n << " elements: " << total << std::endl;
               }
            }
         }

      GroupDecomposer(const GroupDecomposer&) = delete;
      
      void get_stabilizer(std::map<int, std::map<int, const gp_elem*>>& result, const gkz_vector_type& sig)const{
         // std::map<int, std::map<int, std::map<gkz_vector_type, gp_elem>>>
         int i,j;
         for(const auto& level : triply_filtered_group){
            for(const auto& sublevel : level.second){
               for(const auto& element : sublevel.second){
                  if(refines(element.first, sig)){
                     i = level.first;
                     j = sublevel.first;
                     if(result.count(i) == 0){
                        result[i] = std::map<int, const gp_elem*>();
                     }
                     (result[i])[j] = &(element.second);
                     goto loopexit;
                  }
               }
            }
            loopexit:;
         }
         /*
         std::cout << "Stabilizer result:" << std::endl;
         for(int i=0; i<=depth; i++){
            int n = result[i].size() + 1;
            cerrFirstWorker << "Level: " << i << " - " << n << std::endl;
         }
         */
      }

      int get_depth() const {
         return depth;
      }

      // Get maximum depth of search tree via strong basis.
      int get_bound() const{
         return bound;
      }

      void print_signature(const Symmetry& g) const{
         std::cout << gkz.stringify(signature(g)) << std::endl;
      }
      
      // Check whether there are any switches that potentially improve a
      // vector. We require these group elements to fix the first 'fixed'
      // elements. The second requirement is that an element of the set
      // 'desired' is permuted to position 'fixed'. I.e. we want elements that
      // switch 'fixed' with any element from 'desired'. If there are any
      // elements, they are stored in the list 'switches'.
      bool has_switches(int fixed, const pm::Set<int>& desired, std::list<const gp_elem*>& switches) const{
         bool result = false;
         if(supports.count(fixed) > 0){
            const pm::Set<int>& support(supports.at(fixed));
            if(!(support * desired).empty()){
               const pm::Set<int> intersection(support * desired);
               for(const auto& j : intersection){
                  switches.push_back(&(switch_table.at(fixed).at(j)));
               }
               result = true;
            }
         }
         if(desired.contains(fixed)){
            switches.push_back(&identity);
            result = true;
         }
         return result;
      }

};



#endif
