//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2020 James D. Mitchell
// Copyright (C) 2020 Reinis Cirpons
//
// 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 a helper class for checking whether or not a congruence
// defined by generating pairs or finitely presented semigroup is obviously
// infinite. Currently, all that is checked is that:
//
// 1. For every generator there is at least one side of one relation that
//    consists solely of that generator. If this condition is not met, then
//    there is a generator of infinite order.
//
// 2. The number of occurrences of every generator is not preserved by the
//    relations. Otherwise, it is not possible to use the relations to reduce
//    the number of occurrences of a generator in a word, and so there are
//    infinitely many distinct words.
//
// 3. The number of generators on the left hand side of a relation is not the
//    same as the number of generators on the right hand side for at least
//    one generator. Otherwise the relations preserve the length of any word
//    and so there are infinitely many distinct words.
//
// 4. There are at least as many relations as there are generators. Otherwise
//    we can find a surjective homomorphism onto an infinite subsemigroup of
//    the rationals under addition.
//
// 5. The checks 2., 3. and 4. are a special case of a more general matrix based
//    condition. We construct a matrix whose columns correspond to generators
//    and rows correspond to relations. The (i, j)-th entry is the number of
//    occurrences of the j-th generator in the left hand side of the i-th
//    relation minus the number of occurrences of it on the right hand side.
//    If this matrix has a non-trivial kernel, then we can construct a
//    surjective homomorphism onto an infinite subsemigroup of the rationals
//    under addition. So we check that the matrix is full rank.
//
// 6. The presentation is not that of a free product. To do this we consider
//    a graph whose vertices are generators and an edge connects two generators
//    if they occur on either side of the same relation. If this graph is
//    disconnected then the presentation is a free product and is therefore
//    infinite. Note that we currently do not consider the case where the
//    identity occurs in the presentation.

#ifndef LIBSEMIGROUPS_OBVINF_HPP_
#define LIBSEMIGROUPS_OBVINF_HPP_

#include <cstddef>  // for size_t
#include <numeric>  // for accumulate
#include <string>   // for string
#include <utility>  // for pair
#include <vector>   // for vector

#include "config.hpp"  // for LIBSEMIGROUPS_EIGEN_ENABLED
#include "types.hpp"   // for word_type etc
#include "uf.hpp"      // for Duf

#ifdef LIBSEMIGROUPS_EIGEN_ENABLED
#include <Eigen/Core>
#endif

namespace libsemigroups {
  namespace detail {
    class IsObviouslyInfinite final {
      using const_iterator_word_type =
          typename std::vector<word_type>::const_iterator;
      using const_iterator_pair_string = typename std::vector<
          std::pair<std::string, std::string>>::const_iterator;

     public:
      explicit IsObviouslyInfinite(size_t);

      explicit IsObviouslyInfinite(std::string const& lphbt)
          : IsObviouslyInfinite(lphbt.size()) {}

      IsObviouslyInfinite(IsObviouslyInfinite const&)            = delete;
      IsObviouslyInfinite(IsObviouslyInfinite&&)                 = delete;
      IsObviouslyInfinite& operator=(IsObviouslyInfinite const&) = delete;
      IsObviouslyInfinite& operator=(IsObviouslyInfinite&&)      = delete;

      ~IsObviouslyInfinite();

      void add_rules(const_iterator_word_type first,
                     const_iterator_word_type last);

      void add_rules(std::string const&         lphbt,
                     const_iterator_pair_string first,
                     const_iterator_pair_string last);

      bool result() const;

     private:
      void private_add_rule(size_t const, word_type const&, word_type const&);

      inline void letters_in_word(size_t row, word_type const& w, int64_t adv) {
        for (size_t const& x : w) {
          matrix(row, x) += adv;
          _seen[x] = true;
        }
      }

      inline void plus_letters_in_word(size_t row, word_type const& w) {
        letters_in_word(row, w, 1);
      }

      inline void minus_letters_in_word(size_t row, word_type const& w) {
        letters_in_word(row, w, -1);
      }

      inline int64_t& matrix(size_t row, size_t col) {
#ifdef LIBSEMIGROUPS_EIGEN_ENABLED
        return _matrix(row, col);
#else
        (void) row;
        return _matrix[col];
#endif
      }

      inline bool matrix_row_sums_to_0(size_t row) {
#ifdef LIBSEMIGROUPS_EIGEN_ENABLED
        return _matrix.row(row).sum() == 0;
#else
        (void) row;
        return std::accumulate(_matrix.cbegin(), _matrix.cend(), 0) == 0;
#endif
      }

      // letter_type i belongs to "preserve" if there exists a relation where
      // the number of occurrences of i is not the same on both sides of the
      // relation letter_type i belongs to "unique" if there is a relation
      // where one side consists solely of i.
      bool              _empty_word;
      detail::Duf<>     _letter_components;
      size_t            _nr_gens;
      size_t            _nr_letter_components;
      size_t            _nr_relations;
      bool              _preserve_length;
      std::vector<bool> _preserve;
      std::vector<bool> _seen;
      std::vector<bool> _unique;

#ifdef LIBSEMIGROUPS_EIGEN_ENABLED
      Eigen::Matrix<int64_t, Eigen::Dynamic, Eigen::Dynamic> _matrix;
#else
      std::vector<int64_t> _matrix;
#endif
    };
  }  // namespace detail
}  // namespace libsemigroups
#endif  // LIBSEMIGROUPS_OBVINF_HPP_
