//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2019-2026 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 implementations of the member functions for the
// FroidurePin class.

namespace libsemigroups {

  ////////////////////////////////////////////////////////////////////////
  // FroidurePin - constructors + destructor - public
  ////////////////////////////////////////////////////////////////////////

  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>::FroidurePin()
      : detail::BruidhinnTraits<Element>(),
        FroidurePinBase(),
        _elements(),
        _gens(),
        _id(),
        _map(),
        _mtx(),
        _sorted(),
        _state(),
        _tmp_product() {
    report_prefix("FroidurePin");
    init();
  }

  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>& FroidurePin<Element, Traits>::init() {
    // Must free data first because FroidurePinBase resets _degree to UNDEFINED
    free_data();
    FroidurePinBase::init();
    _elements.clear();
    _gens.clear();
    _map.clear();
    _idempotents.clear();
    _sorted.clear();
    _state       = nullptr;
    _nr_products = 0;
    report_prefix("FroidurePin");
    return *this;
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  FroidurePin<Element, Traits>::FroidurePin(Iterator1 first, Iterator2 last)
      : FroidurePin() {
    init(first, last);
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  FroidurePin<Element, Traits>&
  FroidurePin<Element, Traits>::init(Iterator1 first, Iterator2 last) {
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator1>())>,
                       element_type>);
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator2>())>,
                       element_type>);

    std::shared_ptr<state_type> prev_state = _state;
    init();
    _state = prev_state;
    // If we want to change the state in a call to init, then this can be
    // achieved by init(state_type) followed by calls to add_generator(s).
    add_generators_before_start(first, last);
    return *this;
  }

  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>::FroidurePin(FroidurePin const& S)
      : FroidurePin() {
    *this = S;
  }

  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>& FroidurePin<Element, Traits>::operator=(
      FroidurePin<Element, Traits> const& S) {
    free_data();  // Must free data first because FroidurePinBase resets _degree
                  // to UNDEFINED
    FroidurePinBase::operator=(S);
    _elements.clear();
    _elements.reserve(_nr);
    _gens.clear();
    _idempotents = S._idempotents;
    _sorted.clear();
    _state = S._state;

    element_index_type i = 0;
    for (internal_const_reference x : S._elements) {
      auto y = this->internal_copy(x);
      _elements.push_back(y);
      _map.emplace(y, i++);
    }
    if (S.number_of_generators() != 0) {
      copy_generators_from_elements(S.number_of_generators());
      init_degree(this->to_external_const(_gens[0]));
    }
    report_prefix("FroidurePin");
    return *this;
  }

  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::free_data() {
    if (_degree != UNDEFINED) {
      this->internal_free(_tmp_product);
      this->internal_free(_id);
    }

    // delete those generators not in _elements, i.e. the duplicate ones
    for (auto& x : _duplicate_gens) {
      this->internal_free(_gens[x.first]);
    }
    for (auto& x : _elements) {
      this->internal_free(x);
    }
  }

  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>::~FroidurePin() {
    free_data();
  }

  ////////////////////////////////////////////////////////////////////////
  // FroidurePin - constructor - private
  ////////////////////////////////////////////////////////////////////////

  // Partial copy.
  // \p copy a semigroup
  // \p coll a collection of additional generators
  //
  // This is a constructor for a semigroup generated by the generators of
  // the FroidurePin copy and the (possibly) additional generators coll.
  //
  // The relevant parts of the data structure of copy are copied and
  // \c this will be corrupt unless add_generators or closure is called
  // subsequently. This is why this member function is private.
  //
  // The same effect can be obtained by copying copy using the copy
  // constructor and then calling add_generators or closure. However,
  // this constructor avoids copying those parts of the data structure of
  // copy that add_generators invalidates anyway. If copy has not been
  // enumerated at all, then these two routes for adding more generators are
  // equivalent.
  //
  // <add_generators> or <closure> should usually be called after this.
  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>::FroidurePin(FroidurePin const& S,
                                            const_reference    rep)
      : FroidurePin() {
    _idempotents = S._idempotents;
    _state       = S._state;
    LIBSEMIGROUPS_ASSERT(S._lenindex.size() > 1);
    LIBSEMIGROUPS_ASSERT(Degree()(rep) >= S.degree());
    partial_copy(S);

    _elements.reserve(S._nr);

    size_t deg_plus = Degree()(rep) - S.degree();
    if (deg_plus != 0) {
      _degree += deg_plus;
      _found_one = false;
      _pos_one   = 0;
    }

    _id          = this->to_internal(One()(rep));
    _tmp_product = this->internal_copy(_id);

    _map.reserve(S._nr);

    element_index_type i = 0;
    for (internal_const_reference x : S._elements) {
      auto y = this->internal_copy(x);
      if (deg_plus != 0) {
        IncreaseDegree()(this->to_external(y), deg_plus);
      }
      _elements.push_back(y);
      _map.emplace(y, i);
      is_one(y, i++);
    }
    // copy the old generators
    copy_generators_from_elements(S.number_of_generators());
    // Now this is ready to have add_generators or closure called on it
  }

  ////////////////////////////////////////////////////////////////////////
  // FroidurePin - member functions - public
  ////////////////////////////////////////////////////////////////////////

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  typename FroidurePin<Element, Traits>::const_reference
  FroidurePin<Element, Traits>::to_element_no_checks(Iterator1 first,
                                                     Iterator2 last) const {
    element_index_type pos = current_position_no_checks(first, last);
    if (pos != UNDEFINED) {
      return this->to_external_const(_elements[pos]);
    }

    // Consider the empty case separately to avoid the unnecessary
    // multiplication by the identity.
    if (first == last) {
      // The next line asserts we can't get here without _id being allocated.
      LIBSEMIGROUPS_ASSERT(degree() != UNDEFINED);
      // The next line asserts that _id actually is an element, if not, then
      // we shouldn't be calling this function with the empty word.
      LIBSEMIGROUPS_ASSERT(currently_contains_one());
      return this->to_external_const(_id);
    }

    // current_position is always known for generators (i.e. when w.size()
    // == 1), so w.size() > 1 should be true here
    LIBSEMIGROUPS_ASSERT(std::distance(first, last) > 1);

    element_type prod  // TODO(1) remove allocation here
        = this->external_copy(this->to_external_const(_id));

    auto* state_ptr = _state.get();
    for (auto it = first; it != last; ++it) {
      LIBSEMIGROUPS_ASSERT(static_cast<size_t>(*it) < number_of_generators());
      Swap()(this->to_external(_tmp_product), prod);
      internal_product(prod,
                       this->to_external_const(_tmp_product),
                       this->to_external_const(_gens[*it]),
                       state_ptr);
    }
    Swap()(this->to_external(_tmp_product), prod);
    this->external_free(prod);

    return this->to_external_const(_tmp_product);
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  typename FroidurePin<Element, Traits>::const_reference
  FroidurePin<Element, Traits>::to_element(Iterator1 first,
                                           Iterator2 last) const {
    // If !w.empty() && number_of_generators() == 0, then
    // throw_if_any_generator_index_out_of_range will throw . . .
    throw_if_any_generator_index_out_of_range(first, last);
    if (first == last) {
      if (number_of_generators() == 0) {
        // . . . hence this function throws if number_of_generators() == 0.
        LIBSEMIGROUPS_EXCEPTION("cannot convert the empty word to an element "
                                "when no generators are defined");
      }
      if (!currently_contains_one()) {
        LIBSEMIGROUPS_EXCEPTION(
            "cannot convert the empty word to an element, the identity is "
            "not {}an element of the semigroup",
            finished() ? "" : "known to be ");
      }
    }
    return to_element_no_checks(first, last);
  }

  template <typename Element, typename Traits>
  template <typename Iterator1,
            typename Iterator2,
            typename Iterator3,
            typename Iterator4>
  bool FroidurePin<Element, Traits>::equal_to_no_checks(Iterator1 first1,
                                                        Iterator2 last1,
                                                        Iterator3 first2,
                                                        Iterator4 last2) const {
    element_index_type u_pos = current_position_no_checks(first1, last1);
    element_index_type v_pos = current_position_no_checks(first2, last2);
    if (finished() || (u_pos != UNDEFINED && v_pos != UNDEFINED)) {
      return u_pos == v_pos;
    }
    if (std::equal(first1, last1, first2, last2)) {
      return true;
    }
    element_type uu = to_element_no_checks(first1, last1);
    // to_element_no_checks returns a reference to _tmp_product, so we must
    // copy it first time around, but not the second (next line)
    const_reference vv  = to_element_no_checks(first2, last2);
    auto            res = (uu == vv);
    this->external_free(uu);
    return res;
  }

  template <typename Element, typename Traits>
  size_t FroidurePin<Element, Traits>::number_of_generators() const noexcept {
    return _gens.size();
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_reference
  FroidurePin<Element, Traits>::generator_no_checks(
      generator_index_type pos) const {
    return this->to_external_const(_gens[pos]);
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_reference
  FroidurePin<Element, Traits>::generator(generator_index_type pos) const {
    throw_if_generator_index_out_of_range(pos);
    return generator_no_checks(pos);
  }

  template <typename Element, typename Traits>
  FroidurePinBase::element_index_type
  FroidurePin<Element, Traits>::current_position(const_reference x) const {
    if (Degree()(x) != _degree) {
      return UNDEFINED;
    }

    auto it = _map.find(this->to_internal_const(x));
    return (it == _map.end() ? UNDEFINED : it->second);
  }

  template <typename Element, typename Traits>
  FroidurePinBase::element_index_type
  FroidurePin<Element, Traits>::fast_product_no_checks(
      element_index_type i,
      element_index_type j) const {
    auto const n = 2 * Complexity()(this->to_external_const(_tmp_product));
    if (current_length(i) < n || current_length(j) < n) {
      return froidure_pin::product_by_reduction(*this, i, j);
    } else {
      internal_product(this->to_external(_tmp_product),
                       this->to_external_const(_elements[i]),
                       this->to_external_const(_elements[j]),
                       _state.get());
      return _map.find(_tmp_product)->second;
    }
  }

  template <typename Element, typename Traits>
  size_t FroidurePin<Element, Traits>::number_of_idempotents() {
    init_idempotents();
    return _idempotents.size();
  }

  template <typename Element, typename Traits>
  bool FroidurePin<Element, Traits>::is_idempotent_no_checks(
      element_index_type pos) {
    init_idempotents();
    return _is_idempotent[pos];
  }

  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>&
  FroidurePin<Element, Traits>::reserve(size_t n) {
    // Since the FroidurePin we are enumerating is bounded in size by the
    // maximum value of an element_index_t, we cast the argument here to
    // this integer type.
    element_index_type nn = static_cast<element_index_type>(n);
    _elements.reserve(nn);
    _final.reserve(nn);
    _first.reserve(nn);
    _enumerate_order.reserve(nn);
    _left.reserve(nn, _left.out_degree());
    _length.reserve(nn);
    _map.reserve(nn);
    _prefix.reserve(nn);
    _reduced.reserve(nn);
    _right.reserve(nn, _right.out_degree());
    _suffix.reserve(nn);
    return *this;
  }

  template <typename Element, typename Traits>
  bool FroidurePin<Element, Traits>::contains(const_reference x) {
    return (position(x) != UNDEFINED);
  }

  template <typename Element, typename Traits>
  FroidurePinBase::element_index_type
  FroidurePin<Element, Traits>::position(const_reference x) {
    if (Degree()(x) != _degree) {
      return UNDEFINED;
    }

    while (true) {
      auto it = _map.find(this->to_internal_const(x));
      if (it != _map.end()) {
        return it->second;
      }
      if (finished()) {
        return UNDEFINED;
      }
      enumerate(_nr + 1);
      // _nr + 1 means we run batch_size() more elements
    }
  }

  template <typename Element, typename Traits>
  FroidurePinBase::element_index_type
  FroidurePin<Element, Traits>::sorted_position(const_reference x) {
    return to_sorted_position(position(x));
  }

  template <typename Element, typename Traits>
  FroidurePinBase::element_index_type
  FroidurePin<Element, Traits>::to_sorted_position(element_index_type pos) {
    run();
    if (pos >= _nr) {
      return UNDEFINED;
    }
    init_sorted();
    return _sorted[pos].second;
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_reference
  FroidurePin<Element, Traits>::at(element_index_type pos) {
    enumerate(pos + 1);
    if (pos >= current_size()) {
      LIBSEMIGROUPS_EXCEPTION("element index out of range, expected value in "
                              "range [0, {}), got {}",
                              current_size(),
                              pos);
    }
    return this->to_external_const(_elements.at(pos));
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_reference
  FroidurePin<Element, Traits>::operator[](element_index_type pos) const {
    LIBSEMIGROUPS_ASSERT(pos < _elements.size());
    return this->to_external_const(_elements[pos]);
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_reference
  FroidurePin<Element, Traits>::sorted_at_no_checks(element_index_type pos) {
    init_sorted();
    return this->to_external_const(_sorted.at(pos).first);
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_reference
  FroidurePin<Element, Traits>::sorted_at(element_index_type pos) {
    if (pos >= size()) {
      LIBSEMIGROUPS_EXCEPTION("element index out of range, expected value in "
                              "range [0, {}), got {}",
                              size(),
                              pos);
    }
    return sorted_at_no_checks(pos);
  }

  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::report_progress() {
    using detail::group_digits;
    using detail::string_time;

    auto run_time = delta(start_time());

    auto num_elts        = fmt::format("{:<11}", group_digits(_nr));
    auto num_rules       = fmt::format("{:<9}", group_digits(_nr_rules));
    auto run_time_string = fmt::format("{:<7}", string_time(run_time));
    auto diam = fmt::format("\u2300 {:<4}", current_max_word_length());

    report_default("FroidurePin: {} (Cayley graph) | {} (elements) | "
                   "{} (rules) | {} (total) \n",
                   diam,
                   num_elts,
                   num_rules,
                   run_time_string);
  }

  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::run_impl() {
    std::lock_guard<std::mutex> lg(_mtx);
    if (_pos >= _nr) {
      return;
    }

    if (!running_until() && !running_for()) {
      report_default(
          "FroidurePin: enumerating until all elements are found . . .\n");
    }
    reset_start_time();
    auto tid = detail::this_threads_id();

    LIBSEMIGROUPS_ASSERT(_lenindex.size() > 1);

    auto ptr = _state.get();

    // product the generators by every generator
    if (_pos < _lenindex[1]) {
      report_progress();
      size_type number_of_shorter_elements = _nr;
      while (_pos < _lenindex[1]) {
        element_index_type i = _enumerate_order[_pos];
        for (generator_index_type j = 0; j != number_of_generators(); ++j) {
          internal_product(this->to_external(_tmp_product),
                           this->to_external_const(_elements[i]),
                           this->to_external_const(_gens[j]),
                           ptr,
                           tid);

          _nr_products++;
          auto it = _map.find(_tmp_product);

          if (it != _map.end()) {
            _right.target_no_checks(i, j, it->second);
            _nr_rules++;
          } else {
            is_one(_tmp_product, _nr);
            _elements.push_back(this->internal_copy(_tmp_product));
            _first.push_back(_first[i]);
            _final.push_back(j);
            _enumerate_order.push_back(_nr);
            _length.push_back(2);
            _map.emplace(_elements.back(), _nr);
            _prefix.push_back(i);
            _reduced.set(i, j, true);
            _right.target_no_checks(i, j, _nr);
            _suffix.push_back(_letter_to_pos[j]);
            _nr++;
          }
        }
        _pos++;
      }
      for (enumerate_index_type i = 0; i != _pos; ++i) {
        generator_index_type b = _final[_enumerate_order[i]];
        for (generator_index_type j = 0; j != number_of_generators(); ++j) {
          _left.target_no_checks(_enumerate_order[i],
                                 j,
                                 _right.target_no_checks(_letter_to_pos[j], b));
        }
      }
      _wordlen++;
      expand(_nr - number_of_shorter_elements);
      _lenindex.push_back(_enumerate_order.size());
      report_progress();
    }

    // Multiply the words of length > 1 by every generator
    while (_pos < _nr && !stopped()) {
      size_type number_of_shorter_elements = _nr;
      while (_pos != _lenindex[_wordlen + 1] && !stopped()) {
        element_index_type   i = _enumerate_order[_pos];
        generator_index_type b = _first[i];
        element_index_type   s = _suffix[i];
        for (generator_index_type j = 0; j != number_of_generators(); ++j) {
          if (!_reduced.get(s, j)) {
            element_index_type r = _right.target_no_checks(s, j);
            if (_found_one && r == _pos_one) {
              _right.target_no_checks(i, j, _letter_to_pos[b]);
            } else if (_prefix[r] != UNDEFINED) {  // r is not a generator
              _right.target_no_checks(
                  i,
                  j,
                  _right.target_no_checks(_left.target_no_checks(_prefix[r], b),
                                          _final[r]));
            } else {
              _right.target_no_checks(
                  i, j, _right.target_no_checks(_letter_to_pos[b], _final[r]));
            }
          } else {
            internal_product(this->to_external(_tmp_product),
                             this->to_external_const(_elements[i]),
                             this->to_external_const(_gens[j]),
                             ptr,
                             tid);
            _nr_products++;
            auto it = _map.find(_tmp_product);

            if (it != _map.end()) {
              _right.target_no_checks(i, j, it->second);
              _nr_rules++;
            } else {
              is_one(_tmp_product, _nr);
              _elements.push_back(this->internal_copy(_tmp_product));
              _first.push_back(b);
              _final.push_back(j);
              _length.push_back(_wordlen + 2);
              _map.emplace(_elements.back(), _nr);
              _prefix.push_back(i);
              _reduced.set(i, j, true);
              _right.target_no_checks(i, j, _nr);
              _suffix.push_back(_right.target_no_checks(s, j));
              _enumerate_order.push_back(_nr);
              _nr++;
            }
          }
        }  // finished applying gens to <_elements.at(_pos)>
        _pos++;
      }  // finished words of length <wordlen> + 1
      expand(_nr - number_of_shorter_elements);

      if (_pos > _nr || _pos == _lenindex[_wordlen + 1]) {
        for (enumerate_index_type i = _lenindex[_wordlen]; i != _pos; ++i) {
          element_index_type   p = _prefix[_enumerate_order[i]];
          generator_index_type b = _final[_enumerate_order[i]];
          for (generator_index_type j = 0; j != number_of_generators(); ++j) {
            _left.target_no_checks(
                _enumerate_order[i],
                j,
                _right.target_no_checks(_left.target_no_checks(p, j), b));
          }
        }
        _wordlen++;
        _lenindex.push_back(_enumerate_order.size());
      }
      if (_pos < _nr) {
        report_progress();
      }
    }
    report_why_we_stopped();

    auto const num_prods
        = fmt::format("{}", detail::group_digits(_nr_products));

    auto const poss_prods = fmt::format(
        "{}", detail::group_digits(current_size() * number_of_generators()));

    auto const percent_prods
        = fmt::format("{:.3f}",
                      static_cast<double>(100 * _nr_products)
                          / (current_size() * number_of_generators()));

    report_default("FroidurePin: number of products was  {} of {} ({}%)\n",
                   num_prods,
                   poss_prods,
                   percent_prods);
  }

  template <typename Element, typename Traits>
  bool FroidurePin<Element, Traits>::finished_impl() const {
    return !running() && _pos >= _nr;
  }

  template <typename Element, typename Traits>
  void
  FroidurePin<Element, Traits>::throw_if_bad_degree(const_reference x) const {
    size_t const n = Degree()(x);
    if (degree() != UNDEFINED && n != degree()) {
      LIBSEMIGROUPS_EXCEPTION(
          "invalid element degree, expected {}, but found {}", degree(), n);
    }
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  void FroidurePin<Element, Traits>::throw_if_bad_degree(Iterator1 first,
                                                         Iterator2 last) const {
    if (degree() == UNDEFINED && first != last) {
      throw_if_inconsistent_degree(first, last);
    } else {
      for (auto it = first; it != last; ++it) {
        throw_if_bad_degree(*it);
      }
    }
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  void FroidurePin<Element, Traits>::throw_if_degree_too_small(
      Iterator1 first,
      Iterator2 last) const {
    if (first != last) {
      for (auto it = first; it != last; ++it) {
        auto m = Degree()(*it);
        if (m < degree()) {
          LIBSEMIGROUPS_EXCEPTION(
              "invalid element degree, expected {}, but found {}", degree(), m);
        }
      }
    }
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  void
  FroidurePin<Element, Traits>::throw_if_inconsistent_degree(Iterator1 first,
                                                             Iterator2 last) {
    if (first != last) {
      auto n = Degree()(*first);
      for (auto it = first; it != last; ++it) {
        auto m = Degree()(*it);
        if (m != n) {
          LIBSEMIGROUPS_EXCEPTION(
              "invalid element degree, expected {}, but found {}", n, m);
        }
      }
    }
  }

  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::init_degree(const_reference x) {
    if (_degree == UNDEFINED) {
      _degree      = Degree()(x);
      _id          = this->to_internal(One()(x));
      _tmp_product = this->to_internal(One()(x));
    }
    LIBSEMIGROUPS_ASSERT(Degree()(x) == _degree);
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  void
  FroidurePin<Element, Traits>::add_generators_before_start(Iterator1 first,
                                                            Iterator2 last) {
    if (first != last) {
      init_degree(*first);
    }

    // TODO(1) if Iterator is std::move_iterator, then first and last are not
    // valid after the next line (the values they point at are moved
    // into std::distance for some reason).
    size_t const m                      = std::distance(first, last);
    size_t       number_of_new_elements = 0;

    for (auto it_coll = first; it_coll < last; ++it_coll) {
      auto it = _map.find(this->to_internal_const(*it_coll));
      if (it == _map.end()) {
        // new generator
        number_of_new_elements++;
        // The following does not do the correct thing when *it_coll is a
        // rvalue reference. The problem is that for the types where it matters
        // (non-trivial, big objects) to_internal_const just takes the address,
        // then internal_copy takes a copy by dereferencing the pointer
        // obtained from to_internal_const. That dereferenced pointer is then
        // forwarded as an lvalue reference, which means the copy constructor,
        // not the move constructor is called.
        _gens.push_back(this->internal_copy(this->to_internal_const(*it_coll)));
        generator_index_type const n = _gens.size() - 1;

        is_one(_gens.back(), _nr);
        _elements.push_back(_gens.back());
        _enumerate_order.push_back(_nr);
        // Note that every non-duplicate generator is *really* stored in
        // _elements, and so must be deleted from _elements but not _gens.
        _first.push_back(n);
        _final.push_back(n);
        _letter_to_pos.push_back(_nr);
        _length.push_back(1);
        _map.emplace(_elements.back(), _nr);
        _prefix.push_back(UNDEFINED);
        _suffix.push_back(UNDEFINED);
        _nr++;
        // TODO(1) _prefix.push_back(_nr) and get rid of _letter_to_pos,
        // and the extra clause in the run member function!
      } else if (!started()
                 || _letter_to_pos[_first[it->second]] == it->second) {
        // duplicate generator
        // i.e. _gens[i] = _gens[_first[it->second]]
        // _first maps from element_index_type -> generator_index_type :)
        _letter_to_pos.push_back(it->second);
        _nr_rules++;
        _duplicate_gens.emplace_back(_gens.size(), _first[it->second]);
        _gens.push_back(this->internal_copy(this->to_internal_const(*it_coll)));
      } else {
        // x is an old element that will now be a generator
        _gens.push_back(_elements[it->second]);
        _letter_to_pos.push_back(it->second);
        _enumerate_order.push_back(it->second);

        _first[it->second]  = _gens.size() - 1;
        _final[it->second]  = _gens.size() - 1;
        _prefix[it->second] = UNDEFINED;
        _suffix[it->second] = UNDEFINED;
        _length[it->second] = UNDEFINED;
      }
    }
    expand(number_of_new_elements);
    LIBSEMIGROUPS_ASSERT(_lenindex.size() > 1);
    _lenindex[1] += number_of_new_elements;
    _left.add_to_out_degree(m);
    _reduced.add_cols(m);
    _right.add_to_out_degree(m);
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  void
  FroidurePin<Element, Traits>::add_generators_after_start(Iterator1 first,
                                                           Iterator2 last) {
    reset_start_time();
    auto const tid = detail::this_threads_id();

    // get some parameters from the old semigroup
    generator_index_type const old_nrgens         = number_of_generators();
    size_type const            old_nr             = _nr;
    size_type                  number_of_old_left = _pos;

    // erase the old index
    _enumerate_order.erase(_enumerate_order.begin() + _lenindex[1],
                           _enumerate_order.end());

    add_generators_before_start(first, last);

    // old_new[i] indicates if we have seen _elements.at(i) yet in new.
    std::vector<bool> old_new(old_nr, false);

    for (generator_index_type i = 0; i < _letter_to_pos.size(); i++) {
      if (_letter_to_pos[i] < old_nr) {
        old_new[_letter_to_pos[i]] = true;
      } else {
        break;
      }
    }
    // reset the data structure
    _idempotents_found = false;
    _nr_rules          = _duplicate_gens.size();
    _pos               = 0;
    _wordlen           = 0;
    _lenindex.clear();
    _lenindex.push_back(0);
    _lenindex.push_back(number_of_generators() - _duplicate_gens.size());
    std::fill(_reduced.begin(), _reduced.end(), false);

    size_type number_of_shorter_elements;
    auto      ptr = _state.get();
    // Repeat until we have multiplied all of the elements of <old> up to
    // the old value of _pos by all of the (new and old) generators

    while (number_of_old_left > 0) {
      number_of_shorter_elements = _nr;
      while (_pos < _lenindex[_wordlen + 1] && number_of_old_left > 0) {
        element_index_type i = _enumerate_order[_pos];  // position in _elements
        generator_index_type b = _first[i];
        element_index_type   s = _suffix[i];
        if (_right.target_no_checks(i, 0) != UNDEFINED) {
          number_of_old_left--;
          // _elements[i] is in old semigroup, and its descendants are
          // known
          for (generator_index_type j = 0; j < old_nrgens; j++) {
            element_index_type k = _right.target_no_checks(i, j);
            if (!old_new[k]) {  // it's new!
              is_one(_elements[k], k);
              _first[k]  = _first[i];
              _final[k]  = j;
              _length[k] = _wordlen + 2;
              _prefix[k] = i;
              _reduced.set(i, j, true);
              if (_wordlen == 0) {
                _suffix[k] = _letter_to_pos[j];
              } else {
                _suffix[k] = _right.target_no_checks(s, j);
              }
              _enumerate_order.push_back(k);
              old_new[k] = true;
            } else if (s == UNDEFINED || _reduced.get(s, j)) {
              // this clause could be removed if _nr_rules wasn't necessary
              _nr_rules++;
            }
          }
          for (generator_index_type j = old_nrgens; j < number_of_generators();
               j++) {
            closure_update(i, j, b, s, old_nr, tid, old_new, ptr);
          }
        } else {
          // _elements[i] is either not in old, or it is in old but its
          // descendants are not known
          for (generator_index_type j = 0; j < number_of_generators(); j++) {
            closure_update(i, j, b, s, old_nr, tid, old_new, ptr);
          }
        }
        _pos++;
      }  // finished words of length <wordlen> + 1

      expand(_nr - number_of_shorter_elements);
      if (_pos > _nr || _pos == _lenindex[_wordlen + 1]) {
        if (_wordlen == 0) {
          for (enumerate_index_type i = 0; i < _pos; i++) {
            size_t b = _final[_enumerate_order[i]];
            for (generator_index_type j = 0; j < number_of_generators(); j++) {
              // TODO(2) reuse old info here!
              _left.target_no_checks(
                  _enumerate_order[i],
                  j,
                  _right.target_no_checks(_letter_to_pos[j], b));
            }
          }
        } else {
          for (enumerate_index_type i = _lenindex[_wordlen]; i < _pos; i++) {
            element_index_type   p = _prefix[_enumerate_order[i]];
            generator_index_type b = _final[_enumerate_order[i]];
            for (generator_index_type j = 0; j < number_of_generators(); j++) {
              // TODO(2) reuse old info here!
              _left.target_no_checks(
                  _enumerate_order[i],
                  j,
                  _right.target_no_checks(_left.target_no_checks(p, j), b));
            }
          }
        }
        _lenindex.push_back(_enumerate_order.size());
        _wordlen++;
      }
      report_progress();
    }
    report_why_we_stopped();
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  FroidurePin<Element, Traits>&
  FroidurePin<Element, Traits>::add_generators_no_checks(Iterator1 first,
                                                         Iterator2 last) {
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator1>())>,
                       element_type>);
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator2>())>,
                       element_type>);
    if (_pos == 0) {
      add_generators_before_start(first, last);
    } else {
      add_generators_after_start(first, last);
    }
    return *this;
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  FroidurePin<Element, Traits>&
  FroidurePin<Element, Traits>::add_generators(Iterator1 first,
                                               Iterator2 last) {
    throw_if_bad_degree(first, last);
    return add_generators_no_checks(first, last);
  }

  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>&
  FroidurePin<Element, Traits>::add_generator_no_checks(const_reference x) {
    return add_generators_no_checks(&x, &x + 1);
  }

  template <typename Element, typename Traits>
  FroidurePin<Element, Traits>&
  FroidurePin<Element, Traits>::add_generator(const_reference x) {
    return add_generators(&x, &x + 1);
  }

  // TODO(1) make this work
  // template <typename Element, typename Traits>
  // void FroidurePin<Element, Traits>::add_generator(rvalue_reference x) {
  //   auto first = std::make_move_iterator(&x);
  //   auto last  = std::make_move_iterator(&x + 1);
  //   add_generators(first, last);
  // }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  FroidurePin<Element, Traits>
  FroidurePin<Element, Traits>::copy_add_generators_no_checks(
      Iterator1 first,
      Iterator2 last) const {
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator1>())>,
                       element_type>);
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator2>())>,
                       element_type>);

    if (first == last) {
      return FroidurePin(*this);
    } else {
      // Partially copy
      FroidurePin out(*this, *first);
      // Replacing  the following with repeated calls to add_generator results
      // in a corrupted object. This is probably due to the incomplete data in
      // the FroidurePin "out" at this stage.
      out.add_generators_no_checks(first, last);
      return out;
    }
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  FroidurePin<Element, Traits>&
  FroidurePin<Element, Traits>::closure_no_checks(Iterator1 first,
                                                  Iterator2 last) {
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator1>())>,
                       element_type>);
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator2>())>,
                       element_type>);
    for (auto it = first; it != last; ++it) {
      if (!contains(*it)) {
        add_generator_no_checks(*it);
      }
    }
    return *this;
  }

  template <typename Element, typename Traits>
  template <typename Iterator1, typename Iterator2>
  FroidurePin<Element, Traits>
  FroidurePin<Element, Traits>::copy_closure_no_checks(Iterator1 first,
                                                       Iterator2 last) {
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator1>())>,
                       element_type>);
    static_assert(
        std::is_same_v<std::decay_t<decltype(*std::declval<Iterator2>())>,
                       element_type>);
    if (first == last) {
      return FroidurePin(*this);
    } else {
      // The next line is required so that when we call the closure member
      // function on out, the partial copy contains enough information to
      // do all membership testing without a call to run (which will fail
      // because the partial copy does not contain enough data to run run).
      this->run();
      // Partially copy
      FroidurePin out(*this, *first);
      out.closure_no_checks(first, last);
      return out;
    }
  }

  ////////////////////////////////////////////////////////////////////////
  // FroidurePin - enumeration member functions - private
  ////////////////////////////////////////////////////////////////////////

  // Expand the data structures in the semigroup with space for nr elements
  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::expand(size_type nr) {
    _left.add_nodes(nr);
    _right.add_nodes(nr);
    _reduced.add_rows(nr);
  }

  // Check if an element is the identity, x should be in the position pos
  // of _elements.
  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::
      is_one(internal_const_element_type x, element_index_type pos) noexcept(
          std::is_nothrow_default_constructible_v<InternalEqualTo>&& noexcept(
              std::declval<InternalEqualTo>()(x, x))) {
    if (!_found_one && InternalEqualTo()(x, _id)) {
      _pos_one   = pos;
      _found_one = true;
    }
  }

  // _duplicates_gens, _letter_to_pos, and _elements must all be
  // initialised for this to work, and _gens must be an empty vector.
  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::copy_generators_from_elements(size_t N) {
    LIBSEMIGROUPS_ASSERT(_gens.empty());
    if (N == 0) {
      return;
    }
    _gens.resize(N);
    std::vector<bool> seen(N, false);
    // really copy duplicate gens from _elements
    for (std::pair<generator_index_type, generator_index_type> const& x :
         _duplicate_gens) {
      // The degree of everything in _elements has already been increased
      // (if it needs to be at all), and so we do not need to increase the
      // degree in the copy below.
      _gens[x.first] = this->internal_copy(_elements[_letter_to_pos[x.second]]);
      seen[x.first]  = true;
    }
    // the non-duplicate gens are already in _elements, so don't really copy
    for (generator_index_type i = 0; i < N; i++) {
      if (!seen[i]) {
        _gens[i] = _elements[_letter_to_pos[i]];
      }
    }
  }

  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::closure_update(element_index_type   i,
                                                    generator_index_type j,
                                                    generator_index_type b,
                                                    element_index_type   s,
                                                    size_type            old_nr,
                                                    size_t const&        tid,
                                                    std::vector<bool>& old_new,
                                                    state_type*        ptr) {
    if (_wordlen != 0 && !_reduced.get(s, j)) {
      element_index_type r = _right.target_no_checks(s, j);
      if (_found_one && r == _pos_one) {
        _right.target_no_checks(i, j, _letter_to_pos[b]);
      } else if (_prefix[r] != UNDEFINED) {
        _right.target_no_checks(
            i,
            j,
            _right.target_no_checks(_left.target_no_checks(_prefix[r], b),
                                    _final[r]));
      } else {
        _right.target_no_checks(
            i, j, _right.target_no_checks(_letter_to_pos[b], _final[r]));
      }
    } else {
      internal_product(this->to_external(_tmp_product),
                       this->to_external_const(_elements[i]),
                       this->to_external_const(_gens[j]),
                       ptr,
                       tid);
      auto it = _map.find(_tmp_product);
      if (it == _map.end()) {  // it's new!
        is_one(_tmp_product, _nr);
        _elements.push_back(this->internal_copy(_tmp_product));
        _first.push_back(b);
        _final.push_back(j);
        _length.push_back(_wordlen + 2);
        _map.emplace(_elements.back(), _nr);
        _prefix.push_back(i);
        _reduced.set(i, j, true);
        _right.target_no_checks(i, j, _nr);
        if (_wordlen == 0) {
          _suffix.push_back(_letter_to_pos[j]);
        } else {
          _suffix.push_back(_right.target_no_checks(s, j));
        }
        _enumerate_order.push_back(_nr);
        _nr++;
      } else if (it->second < old_nr && !old_new[it->second]) {
        // we didn't process it yet!
        is_one(_tmp_product, it->second);
        _first[it->second]  = b;
        _final[it->second]  = j;
        _length[it->second] = _wordlen + 2;
        _prefix[it->second] = i;
        _reduced.set(i, j, true);
        _right.target_no_checks(i, j, it->second);
        if (_wordlen == 0) {
          _suffix[it->second] = _letter_to_pos[j];
        } else {
          _suffix[it->second] = _right.target_no_checks(s, j);
        }
        _enumerate_order.push_back(it->second);
        old_new[it->second] = true;
      } else {  // it->second >= old->_nr || old_new[it->second]
        // it's old
        _right.target_no_checks(i, j, it->second);
        _nr_rules++;
      }
    }
  }

  ////////////////////////////////////////////////////////////////////////
  // FroidurePin - initialisation member functions - private
  ////////////////////////////////////////////////////////////////////////

  // Initialise the data member _sorted. We store a list of pairs consisting
  // of an internal_element_type and element_index_type which is sorted on
  // the first entry using the operator< of the Element class. The second
  // component is then inverted (as a permutation) so that we can then find
  // the position of an element in the sorted list of elements.
  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::init_sorted() {
    if (_sorted.size() == size()) {
      return;
    }
    size_t n = size();
    _sorted.reserve(n);
    for (element_index_type i = 0; i < n; i++) {
      _sorted.emplace_back(_elements[i], i);
    }
    std::sort(
        _sorted.begin(),
        _sorted.end(),
        [this](std::pair<internal_element_type, element_index_type> const& x,
               std::pair<internal_element_type, element_index_type> const& y)
            -> bool {
          return Less()(this->to_external_const(x.first),
                        this->to_external_const(y.first));
        });

    // Invert the permutation in _sorted[*].second
    std::vector<element_index_type> tmp_inverter;
    tmp_inverter.resize(n);
    for (element_index_type i = 0; i < n; i++) {
      tmp_inverter[_sorted[i].second] = i;
    }
    for (element_index_type i = 0; i < n; i++) {
      _sorted[i].second = tmp_inverter[i];
    }
  }

  // Find the idempotents and store their pointers and positions in a
  // std::pair of type internal_idempotent_pair.
  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::init_idempotents() {
    if (_idempotents_found) {
      return;
    }
    report_default("FroidurePin: finding idempotents . . .\n");
    reset_start_time();
    _idempotents_found = true;
    run();
    _is_idempotent.resize(_nr, 0);

    // Find the threshold beyond which it is quicker to simply product
    // elements rather than follow a path in the Cayley graph. This is the
    // enumerate_index_t i for which length(i) >= complexity.
    size_t cmplxty = std::max(
        size_t{Complexity()(this->to_external_const(_tmp_product)) / 2},
        size_t{1});
    LIBSEMIGROUPS_ASSERT(_lenindex.size() > 1);
    // threshold_length = the min. length of a word which is >= complexity.
    // if a word has length strictly greater than threshold_length, then we
    // multiply, otherwise we trace in the Cayley graph.
    size_t threshold_length = std::min(cmplxty, current_max_word_length());
    LIBSEMIGROUPS_ASSERT(threshold_length < _lenindex.size());

    enumerate_index_type threshold_index = _lenindex.at(threshold_length);

    size_t total_load = 0;
    for (size_t i = 1; i <= threshold_length; ++i) {
      // _lenindex[i - 1] is the element_index_t where words of length i
      // begin so _lenindex[i] - _lenindex[i - 1]) is the number of words of
      // length i.
      total_load += i * (_lenindex[i] - _lenindex[i - 1]);
    }
    auto const cmplxy_str = fmt::format("{:>11}", cmplxty);

    auto const threshold_length_str = fmt::format("{:>11}", threshold_length);
    auto const threshold_index_str
        = fmt::format("{:>11}", detail::group_digits(threshold_index));
    auto const mean_path_len = fmt::format(
        "{:>11.3f}", static_cast<double>(total_load) / threshold_index);
    auto const num_prods
        = fmt::format("{:>11}", detail::group_digits(_nr - threshold_index));

    report_default("FroidurePin: complexity of multiplication {:>11} | {}\n",
                   "",
                   cmplxy_str);
    report_default("FroidurePin: multiple words longer than {:>13} | {}\n",
                   "",
                   threshold_length_str);
    report_default(
        "FroidurePin: number of paths followed in Cayley graph | {}\n",
        threshold_index_str);
    report_default(
        "FroidurePin: mean path length {:<23} | {}\n", "", mean_path_len);
    report_default(
        "FroidurePin: number of products {:21} | {}\n", "", num_prods);

    // _lenindex.at(threshold_length) is the element_index_type where words
    // of length (threshold_length + 1) begin
    LIBSEMIGROUPS_ASSERT(_nr >= _lenindex.at(threshold_length));
    total_load += cmplxty * (_nr - _lenindex.at(threshold_length));

    // Use only 1 thread
    idempotents(0, _nr, threshold_index, _idempotents);
    auto       run_time = detail::string_time(delta(start_time()));
    auto const num_idem = detail::group_digits(_idempotents.size());
    report_default(
        "FroidurePin: found {} idempotents in {}\n", num_idem, run_time);
  }

  // Find the idempotents in the range [first, last) and store
  // the corresponding std::pair of type internal_idempotent_pair in the 4th
  // parameter. The parameter threshold is the point, calculated in
  // init_idempotents, at which it is better to simply multiply elements
  // rather than trace in the left/right Cayley graph.
  template <typename Element, typename Traits>
  void FroidurePin<Element, Traits>::idempotents(
      enumerate_index_type                   first,
      enumerate_index_type                   last,
      enumerate_index_type                   threshold,
      std::vector<internal_idempotent_pair>& idempotents) {
    enumerate_index_type pos = first;

    for (; pos < std::min(threshold, last); pos++) {
      element_index_type k = _enumerate_order[pos];
      if (_is_idempotent[k] == 0) {
        // The following is product_by_reduction, don't have to consider
        // lengths because they are equal!!
        element_index_type i = k, j = k;
        while (j != UNDEFINED) {
          i = _right.target_no_checks(i, _first[j]);
          // TODO(1) improve this if R/L-classes are known to stop
          // performing the product if we fall out of the R/L-class of the
          // initial element.
          j = _suffix[j];
        }
        if (i == k) {
          idempotents.emplace_back(_elements[k], k);
          _is_idempotent[k] = 1;
        }
      }
    }

    if (pos >= last) {
      return;
    }

    auto tid = detail::this_threads_id();
    auto ptr = _state.get();

    for (; pos < last; pos++) {
      element_index_type k = _enumerate_order[pos];
      if (_is_idempotent[k] == 0) {
        internal_product(this->to_external(_tmp_product),
                         this->to_external(_elements[k]),
                         this->to_external(_elements[k]),
                         ptr,
                         tid);
        if (InternalEqualTo()(_tmp_product, _elements[k])) {
          idempotents.emplace_back(_elements[k], k);
          _is_idempotent[k] = 1;
        }
      }
    }
  }

  ////////////////////////////////////////////////////////////////////////
  // FroidurePin - iterators - public
  ////////////////////////////////////////////////////////////////////////

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_iterator
  FroidurePin<Element, Traits>::cbegin() const {
    return const_iterator(_elements.cbegin());
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_iterator
  FroidurePin<Element, Traits>::begin() const {
    return cbegin();
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_iterator
  FroidurePin<Element, Traits>::cend() const {
    return const_iterator(_elements.cend());
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_iterator
  FroidurePin<Element, Traits>::end() const {
    return cend();
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_iterator_sorted
  FroidurePin<Element, Traits>::cbegin_sorted() {
    init_sorted();
    return const_iterator_pair_first(_sorted.cbegin());
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_iterator_sorted
  FroidurePin<Element, Traits>::cend_sorted() {
    init_sorted();
    return const_iterator_pair_first(_sorted.cend());
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_iterator_idempotents
  FroidurePin<Element, Traits>::cbegin_idempotents() {
    init_idempotents();
    return const_iterator_pair_first(_idempotents.cbegin());
  }

  template <typename Element, typename Traits>
  typename FroidurePin<Element, Traits>::const_iterator_idempotents
  FroidurePin<Element, Traits>::cend_idempotents() {
    init_idempotents();
    return const_iterator_pair_first(_idempotents.cend());
  }

  ////////////////////////////////////////////////////////////////////////
  // FroidurePin - iterators - private
  ////////////////////////////////////////////////////////////////////////

  template <typename Element, typename Traits>
  struct FroidurePin<Element, Traits>::DerefPairFirst
      : detail::BruidhinnTraits<Element> {
    const_reference
    operator()(typename std::vector<
               std::pair<internal_element_type,
                         element_index_type>>::const_iterator const& it) const {
      return this->to_external_const((*it).first);
    }
  };

  template <typename Element, typename Traits>
  struct FroidurePin<Element, Traits>::AddressOfPairFirst
      : detail::BruidhinnTraits<Element> {
    const_pointer
    operator()(typename std::vector<
               std::pair<internal_element_type,
                         element_index_type>>::const_iterator const& it) const {
      return &(this->to_external_const((*it).first));
    }
  };

  template <typename Element, typename Traits>
  struct FroidurePin<Element, Traits>::IteratorPairFirstTraits
      : detail::ConstIteratorTraits<
            std::vector<std::pair<internal_element_type, element_index_type>>> {
    using value_type = typename FroidurePin<Element, Traits>::element_type;
    using const_reference =
        typename FroidurePin<Element, Traits>::const_reference;
    using const_pointer = typename FroidurePin<Element, Traits>::const_pointer;

    using Deref     = DerefPairFirst;
    using AddressOf = AddressOfPairFirst;
  };

  template <typename Element, typename Traits>
  std::string to_human_readable_repr(FroidurePin<Element, Traits> const& fp) {
    using detail::group_digits;
    return fmt::format("<{} enumerated FroidurePin with {} generator{}, {} "
                       "element{}, Cayley graph \u2300 {}, & {} rule{}>",
                       fp.finished() ? "fully" : "partially",
                       group_digits(fp.number_of_generators()),
                       fp.number_of_generators() == 1 ? "" : "s",
                       group_digits(fp.current_size()),
                       fp.current_size() == 1 ? "" : "s",
                       group_digits(fp.current_max_word_length()),
                       group_digits(fp.current_number_of_rules()),
                       fp.current_number_of_rules() == 1 ? "" : "s");
  }

  namespace froidure_pin {

    template <typename Element, typename Traits, typename Word>
    Word minimal_factorisation(
        FroidurePin<Element, Traits>&                          fp,
        typename FroidurePin<Element, Traits>::const_reference x) {
      Word w;
      minimal_factorisation(fp, w, x);
      return w;
    }

    template <typename Element, typename Traits, typename Word>
    void minimal_factorisation(
        FroidurePin<Element, Traits>&                          fp,
        Word&                                                  w,
        typename FroidurePin<Element, Traits>::const_reference x) {
      auto pos = fp.position(x);
      if (pos == UNDEFINED) {
        LIBSEMIGROUPS_EXCEPTION(
            "the argument is not an element of the semigroup");
      }
      w.clear();
      fp.current_minimal_factorisation_no_checks(std::back_inserter(w), pos);
    }

    template <typename Element, typename Traits, typename Word>
    Word
    factorisation(FroidurePin<Element, Traits>&                          fp,
                  typename FroidurePin<Element, Traits>::const_reference x) {
      return minimal_factorisation(fp, x);
    }

    template <typename Element, typename Traits, typename Word>
    void
    factorisation(FroidurePin<Element, Traits>&                          fp,
                  Word&                                                  w,
                  typename FroidurePin<Element, Traits>::const_reference x) {
      return minimal_factorisation(fp, w, x);
    }

  }  // namespace froidure_pin

}  // namespace libsemigroups
