//
// 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/>.
//

#ifndef LIBSEMIGROUPS_FROIDURE_PIN_BASE_HPP_
#define LIBSEMIGROUPS_FROIDURE_PIN_BASE_HPP_

#include <array>             // for array
#include <cstddef>           // for size_t
#include <cstdint>           // for uint32_t
#include <initializer_list>  // for initializer_list
#include <iterator>          // for forward_iterator_tag
#include <utility>           // for swap
#include <vector>            // for vector, allocator

#include "constants.hpp"           // for UNDEFINED
#include "ranges.hpp"              // for iterator_range
#include "runner.hpp"              // for Runner
#include "types.hpp"               // for word_type, generator_index_type, tril
#include "word-graph-helpers.hpp"  // for word_graph
#include "word-graph.hpp"          // for WordGraph

#include "detail/containers.hpp"  // for DynamicArray2

namespace libsemigroups {

  //! \defgroup froidure_pin_group Froidure-Pin
  //!
  //! This page contains documentation related to the implementation of the
  //! Froidure-Pin algorithm \cite Froidure1997aa in `libsemigroups`.
  //!
  //! The purpose of the Froidure-Pin algorithm is to determine the elements
  //! and structure of a semigroup or monoid generated by a set of generators
  //! whose multiplication and comparison for equality is decidable (such as
  //! matrices, transformations, and so on.)

  //! \ingroup froidure_pin_group
  //!
  //! \brief Base class for FroidurePin containing non-element specific data
  //! and member functions.
  //!
  //! Defined in `froidure-pin-base.hpp`.
  //!
  //! FroidurePinBase is an abstract base class for the class template
  //! FroidurePin.
  //!
  //! FroidurePinBase allows a polymorphic interface to instances of
  //! FroidurePin and its member functions reflect those member functions of
  //! FroidurePin that do not depend on the template parameter `Element`
  //! (the type of the elements of the semigroup represented).
  //!
  //! \sa FroidurePin and FroidurePinTraits.
  class FroidurePinBase : public Runner {
    template <typename Element, typename Traits>
    friend class FroidurePin;

   public:
    //! Alias for the type of the size of the enumerated semigroup.
    // It should be possible to change this type and everything will just work,
    // provided the size of the semigroup is less than the maximum value of
    // this type of integer.
    using size_type = uint32_t;

    //! \brief Alias for the index of a generator.
    //!
    //! Alias for \ref size_type used to indicate that a value should be the
    //! index of a generator.
    using generator_index_type = size_type;

    //! \brief Alias for the index of an element.
    //!
    //! The size of the semigroup being enumerated must be at most
    //! \c std::numeric_limits<element_index_type>::max().
    using element_index_type = size_type;

    //! \brief Alias for the types of the left and right Cayley graphs.
    using cayley_graph_type = WordGraph<element_index_type>;

   private:
    // See comments in FroidurePin
    using enumerate_index_type = size_type;

    ////////////////////////////////////////////////////////////////////////
    // FroidurePin - data members - private
    ////////////////////////////////////////////////////////////////////////

#ifndef LIBSEMIGROUPS_PARSED_BY_DOXYGEN
    // This now only contains a single data member, which might make it a bit
    // redundant.
    struct Settings {
      Settings() noexcept : _batch_size(8'192) {}
      Settings(Settings const&) noexcept            = default;
      Settings(Settings&&) noexcept                 = default;
      Settings& operator=(Settings const&) noexcept = default;
      Settings& operator=(Settings&&) noexcept      = default;
      ~Settings()                                   = default;

      size_t _batch_size;
    } _settings;
#endif

    size_t _degree;
    std::vector<std::pair<generator_index_type, generator_index_type>>
                                      _duplicate_gens;
    std::vector<element_index_type>   _enumerate_order;
    std::vector<generator_index_type> _final;
    std::vector<generator_index_type> _first;
    bool                              _found_one;
    bool                              _idempotents_found;
    std::vector<int>                  _is_idempotent;
    cayley_graph_type                 _left;
    std::vector<size_type>            _length;
    std::vector<enumerate_index_type> _lenindex;
    std::vector<element_index_type>   _letter_to_pos;
    size_type                         _nr;
    size_t                            _nr_products;
    size_t                            _nr_rules;
    enumerate_index_type              _pos;
    element_index_type                _pos_one;
    std::vector<element_index_type>   _prefix;
    detail::DynamicArray2<bool>       _reduced;
    cayley_graph_type                 _right;
    std::vector<element_index_type>   _suffix;
    size_t                            _wordlen;

   public:
    ////////////////////////////////////////////////////////////////////////
    // FroidurePinBase - constructors - public
    ////////////////////////////////////////////////////////////////////////

    //! \brief Default constructor.
    //!
    //! Default constructor.
    FroidurePinBase();

    //! \brief Reinitialize a FroidurePinBase object.
    //!
    //! This function re-initializes a FroidurePinBase object so that it is in
    //! the same state as if it had just been default constructed.
    //!
    //! \returns A reference to `*this`.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    FroidurePinBase& init();

    //! \brief Copy constructor.
    //!
    //! Copy constructor.
    FroidurePinBase(FroidurePinBase const& S);

    //! \brief Default move constructor.
    //!
    //! Default move constructor.
    FroidurePinBase(FroidurePinBase&& other) = default;

    //! \brief Copy assignment operator.
    //!
    //! Copy assignment operator.
    FroidurePinBase& operator=(FroidurePinBase const&);

    //! \brief Move assignment operator.
    //!
    //! Move assignment operator.
    FroidurePinBase& operator=(FroidurePinBase&&) = default;

    virtual ~FroidurePinBase();

    ////////////////////////////////////////////////////////////////////////
    // FroidurePinBase - settings - public
    ////////////////////////////////////////////////////////////////////////

    //! \brief Set a new value for the batch size.
    //!
    //! The *batch size* is the number of new elements to be found by any call
    //! to \ref run. This is used by, for example, FroidurePin::position so
    //! that it is possible to find the position of an element after only
    //! partially enumerating the elements of a FrodiurePin instance.
    //!
    //! The default value of the batch size is `8192`.
    //!
    //! \param batch_size the new value for the batch size.
    //!
    //! \returns A reference to `*this`.
    //!
    //! \exceptions
    //! \noexcept
    //!
    //! \complexity
    //! Constant.
    //!
    //! \sa \ref batch_size.
    FroidurePinBase& batch_size(size_t batch_size) noexcept {
      _settings._batch_size = batch_size;
      return *this;
    }

    //! \brief Returns the current value of the batch size.
    //!
    //! This function returns the minimum number of elements enumerated in any
    //! call to \ref run.
    //!
    //! \returns The current value of the batch size.
    //!
    //! \exceptions
    //! \noexcept
    //!
    //! \complexity
    //! Constant.
    //!
    //! \sa \ref batch_size(size_t).
    [[nodiscard]] size_t batch_size() const noexcept {
      return _settings._batch_size;
    }

#ifndef LIBSEMIGROUPS_PARSED_BY_DOXYGEN
    ////////////////////////////////////////////////////////////////////////
    // FroidurePinBase - pure virtual member functions - public
    ////////////////////////////////////////////////////////////////////////

    [[nodiscard]] virtual size_t number_of_generators() const = 0;
    [[nodiscard]] virtual tril   is_finite() const            = 0;

#endif  // LIBSEMIGROUPS_PARSED_BY_DOXYGEN not defined

    ////////////////////////////////////////////////////////////////////////
    // FroidurePinBase - member functions - public
    ////////////////////////////////////////////////////////////////////////

    //! \brief Returns the position corresponding to a word.
    //!
    //! This function returns the position in a FroidurePin instance
    //! corresponding to the word in the generators given by the iterators
    //! \p first and \p last. No enumeration is performed, and \ref UNDEFINED is
    //! returned if the word does not currently correspond to an element.
    //!
    //! \tparam Iterator1 type of the first argument.
    //! \tparam Iterator2 type of the second argument.
    //!
    //! \param first iterator pointing at the first letter in the word.
    //! \param last iterator pointing one beyond the last letter in the word.
    //!
    //! \returns
    //! The index of the element corresponding to the word given by \p first
    //! and \p last, or \ref UNDEFINED if there is no such element.
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the length of the distance from \p first to
    //! \p last.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check its argument is valid. In
    //! particular, if any of the letters (pointed at by iterators in the range
    //! from \p first to \p last) is out of range, then bad things will happen.
    //!
    //! \sa FroidurePin::to_element
    //! \sa FroidurePin::current_position
    //! \sa FroidurePin::position_no_checks
    //! \sa FroidurePin::position
    template <typename Iterator1, typename Iterator2>
    [[nodiscard]] element_index_type
    current_position_no_checks(Iterator1 first, Iterator2 last) const;

    //! \brief Returns the position corresponding to a word.
    //!
    //! This function returns the position in a FroidurePin instance
    //! corresponding to the word in the generators given by \p first and
    //! \p last. No enumeration is performed, and \ref UNDEFINED is returned if
    //! the word does not currently correspond to an element.
    //!
    //! \tparam Iterator1 type of the first argument.
    //! \tparam Iterator2 type of the second argument.
    //!
    //! \param first iterator pointing at the first letter in the word.
    //! \param last iterator pointing one beyond the last letter in the word.
    //!
    //! \returns
    //! The index of the element corresponding to the word given by \p first
    //! and \p last, or \ref UNDEFINED if there is no such element.
    //!
    //! \throws LibsemigroupsException if any of the letters (pointed at by
    //! iterators in the range from \p first to \p last) is out of range
    //! (exceeds FroidurePin::number_of_generators).
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the length of the distance from \p first to
    //! \p last.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \sa FroidurePin::to_element
    //! \sa FroidurePin::current_position_no_checks
    //! \sa FroidurePin::position_no_checks
    //! \sa FroidurePin::position
    template <typename Iterator1, typename Iterator2>
    [[nodiscard]] element_index_type current_position(Iterator1 first,
                                                      Iterator2 last) const {
      throw_if_any_generator_index_out_of_range(first, last);
      return current_position_no_checks(first, last);
    }

    //! \brief Returns the position corresponding to a word.
    //!
    //! This function returns the position in a FroidurePin instance
    //! corresponding to the word in the generators given by the iterators
    //! \p first and \p last. This function triggers a full enumeration.
    //!
    //! \tparam Iterator1 type of the first argument.
    //! \tparam Iterator2 type of the second argument.
    //!
    //! \param first iterator pointing at the first letter in the word.
    //! \param last iterator pointing one beyond the last letter in the word.
    //!
    //! \returns
    //! The index of the element corresponding to the word given by \p first
    //! and \p last.
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the length of the distance from \p first to
    //! \p last.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \warning This function does not check its argument is valid. In
    //! particular, if any of the letters (pointed at by iterators in the range
    //! from \p first to \p last) is out of range, then bad things will happen.
    //!
    //! \sa FroidurePin::to_element
    //! \sa FroidurePin::current_position_no_checks
    //! \sa FroidurePin::current_position
    //! \sa FroidurePin::position
    template <typename Iterator1, typename Iterator2>
    [[nodiscard]] element_index_type position_no_checks(Iterator1 first,
                                                        Iterator2 last) {
      run();
      return current_position_no_checks(first, last);
    }

    //! \brief Returns the position corresponding to a word.
    //!
    //! This function returns the position in a FroidurePin instance
    //! corresponding to the word in the generators given by \p first and
    //! \p last. This function triggers a full enumeration.
    //!
    //! \tparam Iterator1 type of the first argument.
    //! \tparam Iterator2 type of the second argument.
    //!
    //! \param first iterator pointing at the first letter in the word.
    //! \param last iterator pointing one beyond the last letter in the word.
    //!
    //! \returns
    //! The index of the element corresponding to the word given by \p first
    //! and \p last, or \ref UNDEFINED if there is no such element.
    //!
    //! \throws LibsemigroupsException if any of the letters (pointed at by
    //! iterators in the range from \p first to \p last) is out of range
    //! (exceeds FroidurePin::number_of_generators).
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the length of the distance from \p first to
    //! \p last.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \sa FroidurePin::to_element
    //! \sa FroidurePin::current_position
    //! \sa FroidurePin::current_position_no_checks
    //! \sa FroidurePin::position_no_checks
    //! \sa FroidurePin::position
    template <typename Iterator1, typename Iterator2>
    [[nodiscard]] element_index_type position(Iterator1 first, Iterator2 last) {
      run();
      return current_position(first, last);
    }

    //! \brief Returns the position of the generator with specified index.
    //!
    //! In many cases \p position_of_generator(i) will equal \p i, examples
    //! of when this will not be the case are:
    //!
    //! * there are duplicate generators;
    //!
    //! * FroidurePin::add_generators was called after a FroidurePinBase
    //! object was already partially enumerated.
    //!
    //! \param i the index of the generator.
    //!
    //! \returns The position of the \p i-th generator.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check its argument is valid. In
    //! particular, if there is no generator with index \p i, then bad
    //! things will happen.
    [[nodiscard]] element_index_type
    position_of_generator_no_checks(generator_index_type i) const {
      LIBSEMIGROUPS_ASSERT(i < _letter_to_pos.size());
      return _letter_to_pos[i];
    }

    //! \brief Returns the position of the generator with specified index.
    //!
    //! In many cases \p position_of_generator(i) will equal \p i, examples
    //! of when this will not be the case are:
    //!
    //! * there are duplicate generators;
    //!
    //! * FroidurePin::add_generators was called after a FroidurePinBase
    //! object was already partially enumerated.
    //!
    //! \param i the index of the generator.
    //!
    //! \returns The position of the \p i-th generator.
    //!
    //! \throws LibsemigroupsException if \p i is greater than or equal to
    //! FroidurePin::number_of_generators.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! This function does not trigger any enumeration.
    [[nodiscard]] element_index_type
    position_of_generator(generator_index_type i) const {
      throw_if_generator_index_out_of_range(i);
      return position_of_generator_no_checks(i);
    }

    //! \brief Returns the maximum length of a word in the generators so far
    //! computed.
    //!
    //! Every element can be expressed as the short-lex least product of the
    //! generators that equals that element.  This function returns the length
    //! of the longest short-lex least word in the generators that has so far
    //! been enumerated.
    //!
    //! \returns
    //! A value of type \c size_t.
    //!
    //! \exceptions
    //! \noexcept
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! This function does not trigger any enumeration.
    [[nodiscard]] size_t current_max_word_length() const noexcept {
      return _length[_enumerate_order.back()];
    }

    //! \brief Returns the degree of any and all elements.
    //!
    //! This function returns the degree of any and all elements.
    //!
    //! \returns
    //! A value of type \c size_t.
    //!
    //! \exceptions
    //! \noexcept
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! This function does not trigger any enumeration.
    [[nodiscard]] size_t degree() const noexcept {
      return _degree;
    }

    //! \brief Returns the number of elements so far enumerated.
    //!
    //! This is only the actual size if the FroidurePinBase object is fully
    //! enumerated.
    //!
    //! \returns
    //! A value of type \c size_t.
    //!
    //! \exceptions
    //! \noexcept
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! This function does not trigger any enumeration.
    [[nodiscard]] size_t current_size() const noexcept {
      return _nr;
    }

    //! \brief Returns the number of rules that have been found so far.
    //!
    //! This is only guaranteed to be the actual number of rules in a
    //! presentation defining the FroidurePinBase object if it is fully
    //! enumerated.
    //!
    //! \returns
    //! The number of rules found so far.
    //!
    //! \exceptions
    //! \noexcept
    //!
    //! \complexity
    //! Constant.
    [[nodiscard]] size_t current_number_of_rules() const noexcept {
      return _nr_rules;
    }

    //! \brief Returns the position of the longest proper prefix.
    //!
    //! Returns the position of the prefix of the element \c x in position
    //! \p pos (of the semigroup) of length one less than the length of \c x.
    //!
    //! \param pos the position.
    //!
    //! \returns A value of type \c element_index_type.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning No checks are made that the argument \p pos is valid. In
    //! particular, if \p pos is greater than or equal to \ref current_size,
    //! then bad things will happen.
    element_index_type prefix_no_checks(element_index_type pos) const {
      LIBSEMIGROUPS_ASSERT(pos < _prefix.size());
      return _prefix[pos];
    }

    //! \brief Returns the position of the longest proper prefix.
    //!
    //! Returns the position of the prefix of the element \c x in position
    //! \p pos of length one less than the length of \c x.
    //!
    //! \param pos the position.
    //!
    //! \returns A value of type \c element_index_type.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref current_size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    element_index_type prefix(element_index_type pos) const {
      throw_if_element_index_out_of_range(pos);
      return prefix_no_checks(pos);
    }

    //! \brief Returns the position of the longest proper suffix.
    //!
    //! Returns the position of the suffix of the element \c x in position
    //! \p pos of length one less than the length of \c x.
    //!
    //! \param pos the position.
    //!
    //! \returns A value of type \c element_index_type.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning No checks are made that the argument \p pos is valid. In
    //! particular, if \p pos is greater than or equal to \ref current_size,
    //! then bad things will happen.
    [[nodiscard]] element_index_type
    suffix_no_checks(element_index_type pos) const {
      LIBSEMIGROUPS_ASSERT(pos < _suffix.size());
      return _suffix[pos];
    }

    //! \brief Returns the position of the longest proper suffix.
    //!
    //! Returns the position of the suffix of the element \c x in position
    //! \p pos of length one less than the length of \c x.
    //!
    //! \param pos the position.
    //!
    //! \returns A value of type \c element_index_type.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref current_size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! This function does not trigger any enumeration.
    [[nodiscard]] element_index_type suffix(element_index_type pos) const {
      throw_if_element_index_out_of_range(pos);
      return suffix_no_checks(pos);
    }

    //! \brief Returns the first letter of the element with specified index.
    //!
    //! This function returns the first letter of the element in position
    //! \p pos, which is the index of the generator corresponding to the first
    //! letter of the element.
    //!
    //! This function does not trigger an enumeration.
    //!
    //! \param pos the position.
    //!
    //! \returns A value of type \c generator_index_type.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! Note that `FroidurePin::generator(first_letter(pos))` is
    //! only equal to `FroidurePin::at(first_letter(pos))` if
    //! there are no duplicate generators.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning No checks are made that the argument \p pos is valid. In
    //! particular, if \p pos is greater than or equal to \ref current_size,
    //! then bad things will happen.
    [[nodiscard]] generator_index_type
    first_letter_no_checks(element_index_type pos) const {
      LIBSEMIGROUPS_ASSERT(pos < _first.size());
      return _first[pos];
    }

    //! \brief Returns the first letter of the element with specified index.
    //!
    //! This function returns the first letter of the element in position
    //! \p pos, which is the index of the generator corresponding to the first
    //! letter of the element.
    //!
    //! \param pos the position.
    //!
    //! \returns A value of type \c generator_index_type.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref current_size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! Note that `FroidurePin::generator(first_letter(pos))` is
    //! only equal to `FroidurePin::at(first_letter(pos))` if
    //! there are no duplicate generators.
    //!
    //! \note
    //! This function does not trigger any enumeration.
    [[nodiscard]] generator_index_type
    first_letter(element_index_type pos) const {
      throw_if_element_index_out_of_range(pos);
      return first_letter_no_checks(pos);
    }

    //! \brief Returns the first letter of the element with specified index.
    //!
    //! This function returns the first letter of the element in position
    //! \p pos, which is the index of the generator
    //! corresponding to the first letter of the element.
    //!
    //! This function does not trigger an enumeration.
    //!
    //! \param pos the position.
    //!
    //! \returns A value of type \c generator_index_type.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! Note that `FroidurePin::generator(first_letter(pos))` is
    //! only equal to `FroidurePin::at(first_letter(pos))` if
    //! there are no duplicate generators.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning No checks are made that the argument \p pos is valid. In
    //! particular, if \p pos is greater than or equal to \ref current_size,
    //! then bad things will happen.
    //!
    [[nodiscard]] generator_index_type
    final_letter_no_checks(element_index_type pos) const {
      LIBSEMIGROUPS_ASSERT(pos < _final.size());
      return _final[pos];
    }

    //! \brief Returns the last letter of the element with specified index.
    //!
    //! This function returns the final letter of the element in position
    //! \p pos, which is the index of the generator corresponding to the final
    //! letter of the element.
    //!
    //! \param pos the position.
    //!
    //! \returns A value of type \c generator_index_type.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref current_size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note
    //! Note that `FroidurePin::generator(first_letter(pos))` is
    //! only equal to `FroidurePin::at(first_letter(pos))` if
    //! there are no duplicate generators.
    //!
    //! \note
    //! This function does not trigger any enumeration.
    [[nodiscard]] generator_index_type
    final_letter(element_index_type pos) const {
      throw_if_element_index_out_of_range(pos);
      return final_letter_no_checks(pos);
    }

    //! \brief Returns the length of the short-lex least word equal to the
    //! element with given index.
    //!
    //! This function returns the length of the short-lex least word (in the
    //! generators) equal to the element with index \p pos. No enumeration is
    //! triggered by calls to this function.
    //!
    //! \param pos the position.
    //!
    //! \returns The length of the word equal to the element with index \p pos.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check that \p pos is valid. In
    //! particular, if `pos > current_size()`, then bad things will happen.
    //!
    //! \sa \ref length.
    [[nodiscard]] size_t
    current_length_no_checks(element_index_type pos) const {
      LIBSEMIGROUPS_ASSERT(pos < _length.size());
      return _length[pos];
    }

    //! \brief Returns the length of the short-lex least word equal to the
    //! element with given index.
    //!
    //! This function returns the length of the short-lex least word (in the
    //! generators) equal to the element with index \p pos.
    //!
    //! \param pos the position.
    //!
    //! \returns The length of the word equal to the element with index \p pos.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref current_size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \sa \ref length.
    [[nodiscard]] size_t current_length(element_index_type pos) const {
      throw_if_element_index_out_of_range(pos);
      return current_length_no_checks(pos);
    }

    //! \brief Returns the length of the short-lex least word equal to the
    //! element with given index.
    //!
    //! This function returns the length of the short-lex least word equal to
    //! the element with given index.
    //!
    //! \param pos the position.
    //!
    //! \returns The length of the element with index \p pos.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \sa
    //! \ref current_length.
    // This function could be a helper, but current_length cannot be, so keeping
    // this as a mem fn.
    [[nodiscard]] size_t length(element_index_type pos);

    //! \brief Returns the length of the short-lex least word equal to the
    //! element with given index.
    //!
    //! This function returns the length of the short-lex least word equal to
    //! the element with given index.
    //!
    //! \param pos the position.
    //!
    //! \returns The length of the element with index \p pos.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \warning This function does not check that \p pos is valid. In
    //! particular, if \p pos is greater than or equal to \ref size, then bad
    //! things will happen.
    //!
    //! \sa \ref current_length.
    // This function could be a helper, but current_length cannot be, so keeping
    // this as a mem fn.
    [[nodiscard]] size_t length_no_checks(element_index_type pos);

    //! \brief Returns the size of a FroidurePin instance.
    //!
    //! This function fully enumerates the FroidurePin instance and then returns
    //! the number of elements.
    //!
    //! \returns
    //! The size.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst \f$O(|S|n)\f$ where \f$S\f$ is the semigroup represented by a
    //! FroidurePinBase instance, and \f$n\f$ is
    //! FroidurePin::number_of_generators.
    //!
    //! \note This function triggers a full enumeration.
    [[nodiscard]] size_t size() {
      run();
      return current_size();
    }

    //! \brief Check if the categorical multiplicative identity is an element.
    //!
    //! This function returns \c true if the return value of
    //! FroidurePin::One() is an element of the FroidurePin instance.
    //!
    //! \returns
    //! Whether or not the multiplicative identity is an element.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst \f$O(|S|n)\f$ where \f$S\f$ is the semigroup represented by a
    //! FroidurePinBase instance, and \f$n\f$ is
    //! FroidurePin::number_of_generators.
    //!
    //! \note This function triggers a full enumeration.
    [[nodiscard]] bool contains_one();

    //! \brief Check if the categorical multiplicative identity is known to be
    //! an element.
    //!
    //! This function returns \c true if the return value of
    //! FroidurePin::One()() is already known to be an element of the
    //! FroidurePin instance.
    //!
    //! \returns
    //! Whether or not the multiplicative identity is already known to be an
    //! element.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst \f$O(|S|n)\f$ where \f$S\f$ is the semigroup represented by a
    //! FroidurePinBase instance, and \f$n\f$ is
    //! FroidurePin::number_of_generators.
    //!
    //! \note
    //! This function does not trigger any enumeration.
    [[nodiscard]] bool currently_contains_one() const noexcept {
      return _found_one;
    }

    //! \brief Returns the total number of relations in the presentation.
    //!
    //! This function returns the total number of relations in the presentation.
    //!
    //! \returns
    //! A value of type `size_t`.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst \f$O(|S|n)\f$ where \f$S\f$ is the semigroup represented by
    //! \c this, and \f$n\f$ is the return value of
    //! FroidurePin::number_of_generators.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \sa \ref cbegin_rules and \ref cend_rules.
    [[nodiscard]] size_t number_of_rules() {
      run();
      return _nr_rules;
    }

    //! \brief Returns a const reference to the right Cayley graph.
    //!
    //! This function returns a const reference to the right Cayley graph.
    //!
    //! \returns A const reference to \ref cayley_graph_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst \f$O(|S|n)\f$ where \f$S\f$ is the semigroup represented by
    //! \c this, and \f$n\f$ is the return value of
    //! FroidurePin::number_of_generators.
    //!
    //! \note This function triggers a full enumeration.
    [[nodiscard]] cayley_graph_type const& right_cayley_graph() {
      run();
      return _right;
    }

    //! \brief Returns a const reference to the right Cayley graph.
    //!
    //! This function triggers a full enumeration, and then returns the right
    //! Cayley graph of the semigroup represented by a FroidurePin instance.
    //!
    //! \returns The full enumerated right Cayley graph.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    [[nodiscard]] cayley_graph_type const&
    current_right_cayley_graph() const noexcept {
      return _right;
    }

    //! \brief Returns a const reference to the left Cayley graph.
    //!
    //! This function triggers a full enumeration, and then returns the left
    //! Cayley graph of the semigroup represented by a FroidurePin instance.
    //!
    //! \returns The fully enumerated left Cayley graph.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function triggers a full enumeration.
    [[nodiscard]] cayley_graph_type const& left_cayley_graph() {
      run();
      return _left;
    }

    //! \brief Returns a const reference to the left Cayley graph.
    //!
    //! This function returns a const reference to the left Cayley graph.
    //!
    //! \returns The (possibly partially enumerated) left Cayley graph.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst \f$O(|S|n)\f$ where \f$S\f$ is the semigroup represented by
    //! the FroidurePinBase instance, and \f$n\f$ is
    //! FroidurePin::number_of_generators.
    //!
    //! \note This function does not trigger any enumeration.
    [[nodiscard]] cayley_graph_type const&
    current_left_cayley_graph() const noexcept {
      return _left;
    }

    // Here's a little summary of the functions for minimal_factorisation:
    // [x] current_minimal_factorisation_no_checks(2 args)
    // [x] current_minimal_factorisation(2 args)
    // [x] minimal_factorisation(2 args)

    //! \brief Output to an iterator the short-lex least word representing an
    //! element given by index.
    //!
    //! This function computes a minimal word with respect to the short-lex
    //! ordering (of the generators) equal to the element in position \p pos and
    //! stores the result in the output range starting from \p d_first.
    //!
    //! \tparam Iterator the type of the first argument (an output iterator).
    //!
    //! \param d_first output iterator for the result.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check that \p pos is valid. In
    //! particular, if `pos > current_size()`, then bad things will happen.
    template <typename Iterator>
    void current_minimal_factorisation_no_checks(Iterator           d_first,
                                                 element_index_type pos) const {
      LIBSEMIGROUPS_ASSERT(pos < current_size());
      while (pos != UNDEFINED) {
        *d_first = first_letter_no_checks(pos);
        pos      = suffix_no_checks(pos);
      }
    }

    //! \brief Output to an iterator the short-lex least word representing an
    //! element given by index.
    //!
    //! This function computes a minimal word with respect to the short-lex
    //! ordering (of the generators) equal to the element in position \p pos and
    //! stores the result in the output range starting from \p d_first.
    //!
    //! \tparam Iterator the type of the first argument (an output iterator).
    //!
    //! \param d_first output iterator for the result.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref current_size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    template <typename Iterator>
    void current_minimal_factorisation(Iterator           d_first,
                                       element_index_type pos) const {
      throw_if_element_index_out_of_range(pos);
      current_minimal_factorisation_no_checks(d_first, pos);
    }

    //! \brief Output to an iterator the short-lex least word representing an
    //! element given by index.
    //!
    //! This function computes a minimal word with respect to the short-lex
    //! ordering (of the generators) equal to the element in position \p pos and
    //! stores the result in the output range starting from \p d_first.
    //!
    //! \tparam Iterator the type of the first argument (an output iterator).
    //!
    //! \param d_first output iterator for the result.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! size().
    //!
    //! \complexity
    //! At worst \f$O(mn)\f$ where \f$m\f$ equals \p pos and \f$n\f$ is the
    //! return value of FroidurePin::number_of_generators.
    //!
    //! \note This function triggers an enumeration until it is complete or at
    //! least \p pos elements are found.
    //
    // Note: There's no no_check version of this function because it doesn't
    // make sense (see the impl of minimal_factorisation(word_type&,
    // element_index_type)
    template <typename Iterator>
    void minimal_factorisation(Iterator d_first, element_index_type pos) {
      if (pos >= current_size() && !finished()) {
        enumerate(pos + 1);
      }
      throw_if_element_index_out_of_range(pos);
      current_minimal_factorisation_no_checks(d_first, pos);
    }

    // Here's a little summary of the functions for (non-minimal) factorisation:
    // [x] current_factorisation_no_checks(2 args)
    // [ ] current_factorisation(2 args) TODO(1)
    // [x] factorisation(2 args)

    //! \brief Output to an iterator a word representing an element given by
    //! index.
    //!
    //! This function computes a (not necessarily minimal) word (in the
    //! generators) equal to the element in position \p pos and stores the
    //! result in the output range starting from \p d_first.
    //!
    //! \tparam Iterator the type of the first argument (an output iterator).
    //!
    //! \param d_first output iterator for the result.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! \f$O(m)\f$ where \f$m\f$ is the length of the returned word.
    //!
    //! \note This function triggers an enumeration until it is complete or at
    //! least \p pos elements are found.
    //!
    //! \warning This function does not check its arguments. In particular, it
    //! is assumed that \p pos is less than \ref current_size.
    // TODO(1) check that all _no_checks functions have this warning
    template <typename Iterator>
    void current_factorisation_no_checks(Iterator           d_first,
                                         element_index_type pos) const {
      current_minimal_factorisation_no_checks(d_first, pos);
    }

    //! \brief Output to an iterator a word representing an element given by
    //! index.
    //!
    //! This function computes a (not necessarily minimal) word (in the
    //! generators) equal to the element in position \p pos and stores the
    //! result in the output range starting from \p d_first.
    //!
    //! \tparam Iterator the type of the first argument (an output iterator).
    //!
    //! \param d_first output iterator for the result.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref size.
    //!
    //! \complexity
    //! At worst \f$O(mn)\f$ where \f$m\f$ equals \p pos and \f$n\f$ is the
    //! return value of FroidurePin::number_of_generators.
    //!
    //! \note This function triggers an enumeration until it is complete or at
    //! least \p pos elements are found.
    //
    // Note: There's no no_check version of this function because it doesn't
    // make sense (see the impl of minimal_factorisation(word_type&,
    // element_index_type)
    template <typename Iterator>
    void factorisation(Iterator d_first, element_index_type pos) {
      minimal_factorisation(d_first, pos);
    }

    //! \brief Enumerate until at least a specified number of elements are
    //! found.
    //!
    //! If an FroidurePinBase instance is already fully enumerated, or the
    //! number of elements previously enumerated exceeds \p limit, then calling
    //! this function does nothing. Otherwise, \ref run attempts to find at
    //! least the maximum of \p limit and \ref batch_size additional elements of
    //! the semigroup.
    //!
    //! \param limit the approximate limit for \ref current_size.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! At worst \f$O(mn)\f$ where \f$m\f$ equals \p limit and \f$n\f$ is
    //! FroidurePin::number_of_generators.
    void enumerate(size_t limit);

    //! \brief Returns the number of elements so far enumerated with length in a
    //! given range.
    //!
    //! This function returns the number of elements that have been enumerated
    //! so far with length in the range \f$[min, max)\f$. This function does
    //! not trigger any enumeration.
    //!
    //! \param min the minimum length.
    //! \param max the maximum length plus one.
    //!
    //! \returns The number of elements with lengths in the specified range.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    [[nodiscard]] size_t number_of_elements_of_length(size_t min,
                                                      size_t max) const;

    //! \brief Returns the number of elements so far enumerated with given
    //! length.
    //!
    //! This function returns the number of elements that have been enumerated
    //! so far with length \p len. This function does not trigger any
    //! enumeration.
    //!
    //! \param len the length.
    //!
    //! \returns The number of elements with length \p len.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    [[nodiscard]] size_t number_of_elements_of_length(size_t len) const;

    //! \brief Return type of \ref cbegin_rules and \ref cend_rules.
    //!
    //! This nested class is the return type of \ref cbegin_rules,
    //! \ref cbegin_current_rules, \ref cend_rules, and \ref cend_current_rules.
    class const_rule_iterator {
#ifndef LIBSEMIGROUPS_PARSED_BY_DOXYGEN

     public:
      using size_type = typename std::vector<relation_type>::size_type;
      using difference_type =
          typename std::vector<relation_type>::difference_type;
      using const_pointer = typename std::vector<relation_type>::const_pointer;
      using pointer       = typename std::vector<relation_type>::pointer;
      using const_reference =
          typename std::vector<relation_type>::const_reference;
      using reference         = typename std::vector<relation_type>::reference;
      using value_type        = relation_type;
      using iterator_category = std::forward_iterator_tag;

      const_rule_iterator()                                      = default;
      const_rule_iterator(const_rule_iterator const&)            = default;
      const_rule_iterator(const_rule_iterator&&)                 = default;
      const_rule_iterator& operator=(const_rule_iterator const&) = default;
      const_rule_iterator& operator=(const_rule_iterator&&)      = default;

      const_rule_iterator(FroidurePinBase const* ptr,
                          enumerate_index_type   pos,
                          generator_index_type   gen);

      ~const_rule_iterator() = default;

      [[nodiscard]] bool
      operator==(const_rule_iterator const& that) const noexcept {
        return _gen == that._gen && _pos == that._pos;
      }

      [[nodiscard]] bool
      operator!=(const_rule_iterator const& that) const noexcept {
        return !(this->operator==(that));
      }

      [[nodiscard]] const_reference operator*() const {
        populate_relation();
        return _relation;
      }

      [[nodiscard]] const_pointer operator->() const {
        populate_relation();
        return &_relation;
      }

      // prefix
      const_rule_iterator const& operator++() noexcept;

      // postfix
      [[nodiscard]] const_rule_iterator operator++(int) noexcept {
        const_rule_iterator copy(*this);
        ++(*this);
        return copy;
      }

      void swap(const_rule_iterator& that) noexcept {
        _current.swap(that._current);
        std::swap(_froidure_pin, that._froidure_pin);
        std::swap(_gen, that._gen);
        std::swap(_pos, that._pos);
      }
#endif  // LIBSEMIGROUPS_PARSED_BY_DOXYGEN not defined

     private:
      void populate_relation() const;

      std::array<generator_index_type, 3> _current;
      FroidurePinBase const*              _froidure_pin;
      generator_index_type                _gen;
      enumerate_index_type                _pos;
      mutable relation_type               _relation;
    };  // const_rule_iterator

    // Assert that the forward iterator requirements are met
    static_assert(std::is_default_constructible_v<const_rule_iterator>,
                  "forward iterator requires default-constructible");
    static_assert(std::is_copy_constructible_v<const_rule_iterator>,
                  "forward iterator requires copy-constructible");
    static_assert(std::is_copy_assignable_v<const_rule_iterator>,
                  "forward iterator requires copy-assignable");
    static_assert(std::is_destructible_v<const_rule_iterator>,
                  "forward iterator requires destructible");

    //! \brief Returns a forward iterator pointing to the first rule (if any).
    //!
    //! Returns a forward iterator pointing to the first rule in a confluent
    //! terminating rewriting system defining a semigroup isomorphic to the
    //! one defined by \c this.
    //!
    //! This function does not perform any enumeration of the FroidurePin
    //! object. If you want to obtain the complete set of rules, then
    //! use \ref cbegin_rules instead.
    //!
    //! \returns An iterator of type \ref const_rule_iterator pointing to a
    //! \ref relation_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \iterator_validity
    //! The iterators returned by this function are valid until the underlying
    //! FroidurePin instance is deleted.
    //!
    //! \sa cend_rules
    //!
    // clang-format off
    //! \par Example
    //! \code
    //! FroidurePin<BMat8> S;
    //! S.add_generator(BMat8({{1, 0, 0, 0},
    //!                        {1, 0, 0, 0},
    //!                        {1, 0, 0, 0},
    //!                        {1, 0, 0, 0}}));
    //! S.add_generator(BMat8({{0, 1, 0, 0},
    //!                        {0, 1, 0, 0},
    //!                        {0, 1, 0, 0},
    //!                        {0, 1, 0, 0}}));
    //! S.add_generator(BMat8({{0, 0, 1, 0},
    //!                        {0, 0, 1, 0},
    //!                        {0, 0, 1, 0},
    //!                        {0, 0, 1, 0}}));
    //! S.add_generator(BMat8({{0, 0, 0, 1},
    //!                        {0, 0, 0, 1},
    //!                        {0, 0, 0, 1},
    //!                        {0, 0, 0, 1}}));
    //! S.size(); //-> 4
    //! std::vector<relation_type>(S.cbegin_rules(), S.cend_rules());
    //! // {{{0, 0}, {0}},
    //! //  {{0, 1}, {1}},
    //! //  {{0, 2}, {2}},
    //! //  {{0, 3}, {3}},
    //! //  {{1, 0}, {0}},
    //! //  {{1, 1}, {1}},
    //! //  {{1, 2}, {2}},
    //! //  {{1, 3}, {3}},
    //! //  {{2, 0}, {0}},
    //! //  {{2, 1}, {1}},
    //! //  {{2, 2}, {2}},
    //! //  {{2, 3}, {3}},
    //! //  {{3, 0}, {0}},
    //! //  {{3, 1}, {1}},
    //! //  {{3, 2}, {2}},
    //! //  {{3, 3}, {3}}}
    //! \endcode
    //! \note This function does not trigger any enumeration.
    // clang-format on
    [[nodiscard]] const_rule_iterator cbegin_current_rules() const {
      return const_rule_iterator(this, UNDEFINED, 0);
    }

    //! \brief Returns a forward iterator pointing to the first rule (if any).
    //!
    //! Returns a forward iterator pointing to the first rule in a confluent
    //! terminating rewriting system defining a semigroup isomorphic to the
    //! one defined by a FroidurePinBase instance.
    //!
    //! This function fully enumerate the FroidurePinBase object. If you want to
    //! obtain a (possibly incomplete) set of rules without triggering a full
    //! enumeration, then use \ref cbegin_current_rules instead.
    //!
    //! \returns An iterator of type \ref const_rule_iterator pointing to a
    //! \ref relation_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \iterator_validity
    //! The iterators returned by this function are valid until the underlying
    //! FroidurePin instance is deleted.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \sa cend_rules
    [[nodiscard]] const_rule_iterator cbegin_rules() {
      run();
      return const_rule_iterator(this, UNDEFINED, 0);
    }

    //! \brief Returns a forward iterator pointing one past the last rule (if
    //! any).
    //!
    //! Returns a forward iterator pointing one-past-the-last rule (currently
    //! known) in a confluent terminating rewriting system defining a semigroup
    //! isomorphic to the one defined by \c this.
    //!
    //! This function does not perform any enumeration of the FroidurePin. If
    //! you want to obtain the complete set of rules, then use \ref cend_rules
    //! instead.
    //!
    //! \returns An iterator of type \ref const_rule_iterator pointing to a
    //! \ref relation_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \iterator_validity
    //! The iterators returned by this function are valid until the underlying
    //! FroidurePin instance is deleted.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \sa cbegin_rules
    //!
    // clang-format off
    //! \par Example
    //! \code
    //! FroidurePin<BMat8> S;
    //! S.add_generator(BMat8({{1, 0, 0, 0},
    //!                        {1, 0, 0, 0},
    //!                        {1, 0, 0, 0},
    //!                        {1, 0, 0, 0}}));
    //! S.add_generator(BMat8({{0, 1, 0, 0},
    //!                        {0, 1, 0, 0},
    //!                        {0, 1, 0, 0},
    //!                        {0, 1, 0, 0}}));
    //! S.add_generator(BMat8({{0, 0, 1, 0},
    //!                        {0, 0, 1, 0},
    //!                        {0, 0, 1, 0},
    //!                        {0, 0, 1, 0}}));
    //! S.add_generator(BMat8({{0, 0, 0, 1},
    //!                        {0, 0, 0, 1},
    //!                        {0, 0, 0, 1},
    //!                        {0, 0, 0, 1}}));
    //! S.size(); //-> 4
    //! std::vector<relation_type>(S.cbegin_rules(), S.cend_rules());
    //! // {{{0, 0}, {0}},
    //! //  {{0, 1}, {1}},
    //! //  {{0, 2}, {2}},
    //! //  {{0, 3}, {3}},
    //! //  {{1, 0}, {0}},
    //! //  {{1, 1}, {1}},
    //! //  {{1, 2}, {2}},
    //! //  {{1, 3}, {3}},
    //! //  {{2, 0}, {0}},
    //! //  {{2, 1}, {1}},
    //! //  {{2, 2}, {2}},
    //! //  {{2, 3}, {3}},
    //! //  {{3, 0}, {0}},
    //! //  {{3, 1}, {1}},
    //! //  {{3, 2}, {2}},
    //! //  {{3, 3}, {3}}}
    //! \endcode
    // clang-format on
    [[nodiscard]] const_rule_iterator cend_current_rules() const {
      return const_rule_iterator(this, current_size(), 0);
    }

    //! \brief Returns a forward iterator pointing one past the last rule (if
    //! any).
    //!
    //! Returns a forward iterator pointing one-past-the-last rule in a
    //! confluent terminating rewriting system defining a semigroup isomorphic
    //! to the one defined by \c this.
    //!
    //! This function fully enumerate the FroidurePin object. If you want to
    //! obtain a (possibly incomplete) set of rules without triggering a full
    //! enumeration, then use \ref cend_rules instead.
    //!
    //! \returns An iterator of type \ref const_rule_iterator
    //! pointing to a \ref relation_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \iterator_validity
    //! The iterators returned by this function are valid until the underlying
    //! FroidurePin instance is deleted.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \sa cbegin_rules
    [[nodiscard]] const_rule_iterator cend_rules() {
      run();
      return const_rule_iterator(this, current_size(), 0);
    }

    // TODO(later) it'd be more efficient to have this be a forward
    // iterator only (i.e. as is done in the GAP version of FroidurePin)
    //! \brief Return type of \ref cbegin_normal_forms and
    //! \ref cend_normal_forms.
    //!
    //! This nested class is the return type of \ref cbegin_normal_forms,
    //! \ref cbegin_current_normal_forms, \ref cend_normal_forms, and
    //! \ref cend_current_normal_forms.
    class const_normal_form_iterator {
#ifndef LIBSEMIGROUPS_PARSED_BY_DOXYGEN
      // Private data
      mutable FroidurePinBase const* _froidure_pin;
      enumerate_index_type           _pos;
      mutable word_type              _word;

     public:
      using size_type       = typename std::vector<word_type>::size_type;
      using difference_type = typename std::vector<word_type>::difference_type;
      using const_pointer   = typename std::vector<word_type>::const_pointer;
      using pointer         = typename std::vector<word_type>::pointer;
      using const_reference = typename std::vector<word_type>::const_reference;
      using reference       = typename std::vector<word_type>::reference;
      using value_type      = word_type;
      using iterator_category = std::random_access_iterator_tag;

      const_normal_form_iterator()                                  = default;
      const_normal_form_iterator(const_normal_form_iterator const&) = default;
      const_normal_form_iterator(const_normal_form_iterator&&)      = default;
      const_normal_form_iterator& operator=(const_normal_form_iterator const&)
          = default;
      const_normal_form_iterator& operator=(const_normal_form_iterator&&)
          = default;

      ~const_normal_form_iterator() = default;

      const_normal_form_iterator(FroidurePinBase const* ptr,
                                 enumerate_index_type   pos)
          : _froidure_pin(ptr), _pos(pos), _word() {}

      [[nodiscard]] bool
      operator==(const_normal_form_iterator const& that) const noexcept {
        return _pos == that._pos;
      }

      [[nodiscard]] bool
      operator!=(const_normal_form_iterator const& that) const noexcept {
        return !(*this == that);
      }

      [[nodiscard]] bool
      operator<(const_normal_form_iterator const& that) const noexcept {
        return _pos < that._pos;
      }

      [[nodiscard]] bool
      operator>(const_normal_form_iterator const& that) const noexcept {
        return _pos > that._pos;
      }

      [[nodiscard]] bool
      operator<=(const_normal_form_iterator const& that) const noexcept {
        return _pos < that._pos;
      }

      [[nodiscard]] bool
      operator>=(const_normal_form_iterator const& that) const noexcept {
        return _pos > that._pos;
      }

      [[nodiscard]] const_reference operator*() const noexcept {
        populate_word();
        return _word;
      }

      [[nodiscard]] const_pointer operator->() const noexcept {
        populate_word();
        return &_word;
      }

      [[nodiscard]] const_reference operator[](size_type index) const {
        const_cast<const_normal_form_iterator*>(this)->_pos += index;
        populate_word();
        const_cast<const_normal_form_iterator*>(this)->_pos -= index;
        return _word;
      }

      // prefix
      const_normal_form_iterator const& operator++() noexcept {
        _pos++;
        return *this;
      }

      [[nodiscard]] const_normal_form_iterator operator++(int) noexcept {
        const_normal_form_iterator copy(*this);
        ++(*this);
        return copy;
      }

      const_normal_form_iterator const& operator--() noexcept {
        _pos--;
        return *this;
      }

      [[nodiscard]] const_normal_form_iterator operator--(int) noexcept {
        const_normal_form_iterator copy(*this);
        --(*this);
        return copy;
      }

      void operator+=(size_type val) noexcept {
        _pos += val;
      }

      void operator-=(size_type val) noexcept {
        _pos -= val;
      }

      [[nodiscard]] const_normal_form_iterator
      operator+(size_type val) const noexcept {
        const_normal_form_iterator copy(*this);
        copy += val;
        return copy;
      }

      [[nodiscard]] const_normal_form_iterator
      operator-(size_type val) const noexcept {
        const_normal_form_iterator copy(*this);
        copy -= val;
        return copy;
      }

      [[nodiscard]] difference_type
      operator-(const_normal_form_iterator const& that) const noexcept {
        return _pos - that._pos;
      }

      void swap(const_normal_form_iterator& that) noexcept {
        std::swap(_froidure_pin, that._froidure_pin);
        std::swap(_pos, that._pos);
        std::swap(_word, that._word);
      }

     private:
      void populate_word() const {
        _word.clear();
        _froidure_pin->current_minimal_factorisation_no_checks(
            std::back_inserter(_word), _pos);
      }
#endif  // LIBSEMIGROUPS_PARSED_BY_DOXYGEN not defined
    };

    //! \brief Returns an iterator pointing at the first normal form (if any).
    //!
    //! This function returns an iterator pointing at the normal form of the
    //! first element of the semigroup represented by a FroidurePinBase
    //! instance (if any).
    //!
    //! This function does not perform any enumeration of the FroidurePin. If
    //! you want to obtain the complete set of rules, then use
    //! \ref cbegin_normal_forms instead.
    //!
    //! \returns An iterator of type \ref const_normal_form_iterator
    //! pointing to a \ref word_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //! \note This function does not trigger any enumeration.
    [[nodiscard]] const_normal_form_iterator
    cbegin_current_normal_forms() const {
      return const_normal_form_iterator(this, 0);
    }

    //! \brief Returns an iterator pointing one beyond the last normal form (if
    //! any).
    //!
    //! This function returns an iterator pointing one beyond the normal
    //! form of the last element of the semigroup represented by a
    //! FroidurePinBase instance (if any).
    //!
    //! This function does not perform any enumeration of the FroidurePin. If
    //! you want to obtain the complete set of rules, then use
    //! \ref cend_normal_forms instead.
    //!
    //! \returns An iterator of type \ref const_normal_form_iterator
    //! pointing to a \ref word_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //! \note This function does not trigger any enumeration.
    [[nodiscard]] const_normal_form_iterator cend_current_normal_forms() const {
      return const_normal_form_iterator(this, current_size());
    }

    //! \brief Returns an iterator pointing at the first normal form (if any).
    //!
    //! This function returns an iterator pointing at the normal form of the
    //! first element of the semigroup represented by a FroidurePinBase
    //! instance (if any).
    //!
    //! This function performs a full enumeration of the FroidurePin. If you
    //! want to obtain the current set of normal forms without triggering an
    //! enumeration, then use \ref cbegin_current_normal_forms instead.
    //!
    //! \returns An iterator of type \ref const_normal_form_iterator
    //! pointing to a \ref word_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Same as FroidurePinBase::enumerate.
    //!
    //! \note This function triggers a full enumeration.
    [[nodiscard]] const_normal_form_iterator cbegin_normal_forms() {
      run();
      return const_normal_form_iterator(this, 0);
    }

    //! \brief Returns an iterator pointing one beyond the last normal form (if
    //! any).
    //!
    //! This function returns an iterator pointing one beyond the normal
    //! form of the last element of the semigroup represented by a
    //! FroidurePinBase instance (if any).
    //!
    //! This function performs a full enumeration of the FroidurePin. If you
    //! want to obtain the current set of rules without triggering an
    //! enumeration, then use \ref cend_current_normal_forms instead.
    //!
    //! \returns An iterator of type \ref const_normal_form_iterator
    //! pointing to a \ref word_type.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function triggers a full enumeration.
    [[nodiscard]] const_normal_form_iterator cend_normal_forms() {
      run();
      return const_normal_form_iterator(this, size());
    }

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

    //! \brief Throw an exception if an index is out of range.
    //!
    //! This function throws an exception if the argument \p i exceeds
    //! \ref current_size.
    //!
    //! \param i the index to check.
    //!
    //! \throws LibsemigroupsException if \p i exceeds \ref current_size.
    void throw_if_element_index_out_of_range(element_index_type i) const;

    //! \brief Throw an exception if a generator index is out of range.
    //!
    //! This function throws an exception if the argument \p i exceeds
    //! FroidurePin::number_of_generators.
    //!
    //! \param i the index to check.
    //!
    //! \throws LibsemigroupsException if \p i exceeds
    //! FroidurePin::number_of_generators.
    void throw_if_generator_index_out_of_range(generator_index_type i) const;

    //! \brief Throw an exception if any generator index is out of range.
    //!
    //! This function throws an exception if any index pointed at by an
    //! iterator in the range between \p first and \p last is greater than
    //! FroidurePin::number_of_generators.
    //!
    //! \tparam Iterator1 the type of the first argument.
    //! \tparam Iterator2 the type of the second argument.
    //!
    //! \param first iterator pointing at the first index.
    //! \param last iterating pointing one beyond the last index.
    //!
    //! \throws LibsemigroupsException if any index pointed at by an iterator
    //! in the range from \p first to \p last exceeds
    //! FroidurePin::number_of_generators.
    template <typename Iterator1, typename Iterator2>
    void throw_if_any_generator_index_out_of_range(Iterator1 first,
                                                   Iterator2 last) const {
      for (auto it = first; it != last; ++it) {
        throw_if_generator_index_out_of_range(*it);
      }
    }

   private:
    ////////////////////////////////////////////////////////////////////////
    // FroidurePinBase - member functions - private
    ////////////////////////////////////////////////////////////////////////
    void partial_copy(FroidurePinBase const& S);
  };  // class FroidurePinBase

  ////////////////////////////////////////////////////////////////////////
  // Implementations of mem fn templates for FroidurePinBase
  ////////////////////////////////////////////////////////////////////////

  template <typename Iterator1, typename Iterator2>
  [[nodiscard]] FroidurePinBase::element_index_type
  FroidurePinBase::current_position_no_checks(Iterator1 first,
                                              Iterator2 last) const {
    if (first == last) {
      if (_found_one) {
        return _pos_one;
      } else {
        return UNDEFINED;
      }
    }
    element_index_type s = position_of_generator_no_checks(*first);
    return v4::word_graph::follow_path_no_checks(
        current_right_cayley_graph(), s, first + 1, last);
  }

  //! \ingroup froidure_pin_group
  //!
  //! \brief This namespace contains helper functions for the FroidurePin class
  //! template.
  namespace froidure_pin {
    //! \brief Compute a product using the Cayley graph.
    //!
    //! This function finds the product of `fpb.at(i)` and `fpb.at(j)` by
    //! following the path in the right Cayley graph from \p i labelled by the
    //! word `fpb.minimal_factorisation(j)` or, if
    //! `fpb.minimal_factorisation(i)` is shorter, by following the path in the
    //! left Cayley graph from \p j labelled by `fpb.minimal_factorisation(i)`.
    //!
    //! \param fpb the FroidurePinBase object.
    //! \param i the index of an element.
    //! \param j another index of an element.
    //!
    //! \returns
    //! A value of type \ref FroidurePinBase::element_index_type.
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the minimum of the lengths of
    //! `minimal_factorisation(i)` and `minimal_factorisation(j)`.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check its arguments. In particular,
    //! if \p i or \p j is greater than or equal
    //! to FroidurePinBase::current_size, then bad things will happen.
    [[nodiscard]] FroidurePinBase::element_index_type
    product_by_reduction_no_checks(
        FroidurePinBase const&                       fpb,
        typename FroidurePinBase::element_index_type i,
        typename FroidurePinBase::element_index_type j);

    //! \brief Compute a product using the Cayley graph.
    //!
    //! This function finds the product of `fpb.at(i)` and `fpb.at(j)` by
    //! following the path in the right Cayley graph from \p i labelled by the
    //! word `fpb.minimal_factorisation(j)` or, if
    //! `fpb.minimal_factorisation(i)` is shorter, by following the path in the
    //! left Cayley graph from \p j labelled by `fpb.minimal_factorisation(i)`.
    //!
    //! \param fpb the FroidurePinBase object.
    //! \param i the index of an element.
    //! \param j another index of an element.
    //!
    //! \returns
    //! A value of type \ref FroidurePinBase::element_index_type.
    //!
    //! \throws LibsemigroupsException if \p i or \p j is greater than or equal
    //! to FroidurePinBase::current_size.
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the minimum of the lengths of
    //! `minimal_factorisation(i)` and `minimal_factorisation(j)`.
    //!
    //! \note This function does not trigger any enumeration.
    [[nodiscard]] typename FroidurePinBase::element_index_type
    product_by_reduction(FroidurePinBase const&                       fpb,
                         typename FroidurePinBase::element_index_type i,
                         typename FroidurePinBase::element_index_type j);

    ////////////////////////////////////////////////////////////////////////
    // Position helper functions
    ////////////////////////////////////////////////////////////////////////

    //! \brief Returns the position corresponding to a word.
    //!
    //! This function returns the position of the element corresponding to the
    //! word \p w; \ref UNDEFINED is returned if the position of the
    //! element corresponding to \p w cannot be determined.
    //!
    //! \tparam Word the type of the argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param w a word in the generators.
    //!
    //! \returns
    //! The index of the element corresponding to the word \p w, or
    //! \ref UNDEFINED if there is no such element.
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the length of the word \p w.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check its argument is valid. In
    //! particular, it is assumed that every item in \p w is strictly less than
    //! \ref FroidurePinBase::current_size.
    //!
    //! \sa FroidurePin::to_element.
    template <typename Word>
    [[nodiscard]] FroidurePinBase::element_index_type
    current_position_no_checks(FroidurePinBase const& fpb, Word const& w) {
      return fpb.current_position_no_checks(std::begin(w), std::end(w));
    }

    //! \copydoc current_position_no_checks(FroidurePinBase const&, Word const&)
    [[nodiscard]] static inline FroidurePinBase::element_index_type
    current_position_no_checks(FroidurePinBase const&            fpb,
                               std::initializer_list<int> const& w) {
      return current_position_no_checks<std::initializer_list<int>>(fpb, w);
    }

    //! \brief Returns the position corresponding to a word.
    //!
    //! This function returns the position of the element corresponding to the
    //! word \p w.
    //!
    //! \tparam Word the type of the argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param w a word in the generators.
    //!
    //! \returns
    //! The index of the element corresponding to the word \p w.
    //!
    //! \throws LibsemigroupsException if \p w does not consist of values
    //! strictly less than FroidurePin::number_of_generators.
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the length of the word \p w.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \sa FroidurePin::to_element.
    template <typename Word>
    [[nodiscard]] FroidurePinBase::element_index_type
    current_position(FroidurePinBase const& fpb, Word const& w) {
      return fpb.current_position(std::begin(w), std::end(w));
    }

    //! \copydoc current_position(FroidurePinBase const&, Word const&)
    [[nodiscard]] static inline FroidurePinBase::element_index_type
    current_position(FroidurePinBase const&            fpb,
                     std::initializer_list<int> const& w) {
      return current_position<std::initializer_list<int>>(fpb, w);
    }

    //! \brief Returns the position corresponding to a word.
    //!
    //! This function returns the position of the element corresponding to the
    //! word \p w.
    //!
    //! \tparam Word the type of the argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param w a word in the generators.
    //!
    //! \returns
    //! The index of the element corresponding to the word \p w.
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the length of the word \p w.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \warning This function does not check its argument is valid. In
    //! particular, it is assumed that every item in \p w is strictly less than
    //! \ref FroidurePinBase::current_size.
    //!
    //! \sa FroidurePin::to_element.
    template <typename Word>
    [[nodiscard]] FroidurePinBase::element_index_type
    position_no_checks(FroidurePinBase& fpb, Word const& w) {
      return fpb.position_no_checks(std::begin(w), std::end(w));
    }

    //! \copydoc position_no_checks(FroidurePinBase&, Word const&)
    [[nodiscard]] static inline FroidurePinBase::element_index_type
    position_no_checks(FroidurePinBase&                  fpb,
                       std::initializer_list<int> const& w) {
      return position_no_checks<std::initializer_list<int>>(fpb, w);
    }

    //! \brief Returns the position corresponding to a word.
    //!
    //! This function returns the position of the element corresponding to the
    //! word \p w.
    //!
    //! \tparam Word the type of the argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param w a word in the generators.
    //!
    //! \returns
    //! The index of the element corresponding to the word \p w.
    //!
    //! \throws LibsemigroupsException if \p w does not consist of values
    //! strictly less than FroidurePin::number_of_generators.
    //!
    //! \complexity
    //! \f$O(n)\f$ where \f$n\f$ is the length of the word \p w.
    //!
    //! \note This function triggers a full enumeration.
    //!
    //! \sa FroidurePin::to_element.
    template <typename Word>
    [[nodiscard]] FroidurePinBase::element_index_type
    position(FroidurePinBase& fpb, Word const& w) {
      return fpb.position(std::begin(w), std::end(w));
    }

    //! \copydoc position(FroidurePinBase&, Word const&)
    [[nodiscard]] static inline FroidurePinBase::element_index_type
    position(FroidurePinBase& fpb, std::initializer_list<int> const& w) {
      return position<std::initializer_list<int>>(fpb, w);
    }

    ////////////////////////////////////////////////////////////////////////
    // Minimal factorisation helper functions
    ////////////////////////////////////////////////////////////////////////

    //! \brief Modify a word to contain the short-lex least word representing an
    //! element given by index.
    //!
    //! This function changes \p word in-place to contain the short-lex minimal
    //! word (in the generators) equal to the element in position \p pos.
    //!
    //! \tparam Word the type of the second argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param word the word to clear and change in-place.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check that \p pos is valid. In
    //! particular, it is assumed that \p pos is strictly less than
    //! \ref FroidurePinBase::current_size.
    template <typename Word>
    void current_minimal_factorisation_no_checks(
        FroidurePinBase const&              fpb,
        Word&                               word,
        FroidurePinBase::element_index_type pos) {
      // TODO(1) remove the clear (makes the functions more flexible), but will
      // require care
      word.clear();
      fpb.current_minimal_factorisation_no_checks(std::back_inserter(word),
                                                  pos);
    }

    //! \brief Returns the short-lex least word representing an element given by
    //! index.
    //!
    //! This function is similar to the 3-argument version
    //! \ref current_minimal_factorisation_no_checks, but it returns a
    //! \c Word by value instead of modifying its argument in-place.
    //!
    //! \tparam Word the type of the returned word (defaults to \ref word_type).
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \returns
    //! A word containing the factorisation of the given element.
    //!
    //! \complexity
    //! At worst \f$O(mn)\f$ where \f$m\f$ equals \p pos and \f$n\f$ is the
    //! return value of FroidurePin::number_of_generators.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check that \p pos is valid. In
    //! particular, it is assumed that \p pos is strictly less than
    //! \ref FroidurePinBase::current_size.
    template <typename Word = word_type>
    [[nodiscard]] Word current_minimal_factorisation_no_checks(
        FroidurePinBase const&              fpb,
        FroidurePinBase::element_index_type pos) {
      Word word;
      current_minimal_factorisation_no_checks(fpb, word, pos);
      return word;
    }

    //! \brief Modify a word to contain the short-lex least word representing an
    //! element given by index.
    //!
    //! This function changes \p word in-place to contain the short-lex minimal
    //! word (in the generators) equal to the element in position \p pos.
    //!
    //! \tparam Word the type of the second argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param word the word to clear and change in-place.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref FroidurePinBase::current_size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    template <typename Word>
    void
    current_minimal_factorisation(FroidurePinBase const&              fpb,
                                  Word&                               word,
                                  FroidurePinBase::element_index_type pos) {
      // TODO(1) remove the clear (makes the functions more flexible), but will
      // require care
      word.clear();
      fpb.current_minimal_factorisation(std::back_inserter(word), pos);
    }

    //! \brief Returns the short-lex least word representing an element given by
    //! index.
    //!
    //! This function is similar to the 3-argument version
    //! \ref current_minimal_factorisation, but it returns a
    //! \c Word by value instead of modifying its argument in-place.
    //!
    //! \tparam Word the type of the returned word (defaults to \ref word_type).
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \returns
    //! A word containing the factorisation of the given element.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref FroidurePinBase::current_size.
    //!
    //! \complexity
    //! At worst \f$O(mn)\f$ where \f$m\f$ equals \p pos and \f$n\f$ is the
    //! return value of FroidurePin::number_of_generators.
    //!
    //! \note This function does not trigger any enumeration.
    template <typename Word = word_type>
    [[nodiscard]] Word
    current_minimal_factorisation(FroidurePinBase const&              fpb,
                                  FroidurePinBase::element_index_type pos) {
      Word word;
      current_minimal_factorisation(fpb, word, pos);
      return word;
    }

    //! \brief Modify a word to contain the short-lex least word representing an
    //! element given by index.
    //!
    //! This function changes \p word in-place to contain the short-lex minimal
    //! word (in the generators) equal to the element in position \p pos.
    //!
    //! \tparam Word the type of the second argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param word the word to clear and change in-place.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! \ref FroidurePinBase::current_size.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function triggers an enumeration until it is complete or at
    //! least \p pos elements are found.
    // Note: there's no no_check version of this function because it doesn't
    // make sense (see the impl of minimal_factorisation(word_type&,
    // FroidurePinBase::element_index_type);
    template <typename Word>
    void minimal_factorisation(FroidurePinBase&                    fpb,
                               Word&                               word,
                               FroidurePinBase::element_index_type pos) {
      // TODO(1) remove the clear (makes the functions more flexible), but will
      // require care
      word.clear();
      fpb.minimal_factorisation(std::back_inserter(word), pos);
    }

    //! \brief Returns the short-lex least word representing an element given by
    //! index.
    //!
    //! This is similar to the three-argument version for
    //! \ref minimal_factorisation, but it returns a \c Word by value
    //! instead of modifying an argument in-place.
    //!
    //! \tparam Word the type of the second argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \returns
    //! A word containing the factorisation of the given element.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! size().
    //!
    //! \complexity
    //! At worst \f$O(mn)\f$ where \f$m\f$ equals \p pos and \f$n\f$ is the
    //! return value of FroidurePin::number_of_generators.
    //!
    //! \note This function triggers an enumeration until it is complete or at
    //! least \p pos elements are found.
    // Notes:
    // * There's no no_check version of this function because it doesn't make
    //   sense (see the impl of minimal_factorisation(word_type&,
    //   FroidurePinBase::element_index_type);
    template <typename Word = word_type>
    [[nodiscard]] Word
    minimal_factorisation(FroidurePinBase&                    fpb,
                          FroidurePinBase::element_index_type pos) {
      Word word;
      minimal_factorisation(fpb, word, pos);
      return word;
    }

    ////////////////////////////////////////////////////////////////////////
    // (Non-minimal) factorisation helper functions
    ////////////////////////////////////////////////////////////////////////

    //! \brief Modify a word to contain a word representing an element given by
    //! index.
    //!
    //! This function changes \p word in-place to contain a (maybe not minimal)
    //! word (in the generators) equal to the element in position \p pos.
    //!
    //! \tparam Word the type of the second argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param word the word to clear and change in-place.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function does not trigger any enumeration.
    //!
    //! \warning This function does not check that \p pos is valid. In
    //! particular, it is assumed that \p pos is strictly less than
    //! \ref FroidurePinBase::current_size.
    template <typename Word>
    void
    current_factorisation_no_checks(FroidurePinBase const&              fpb,
                                    Word&                               word,
                                    FroidurePinBase::element_index_type pos) {
      return current_minimal_factorisation_no_checks<Word>(fpb, word, pos);
    }

    //! \brief Modify a word to contain a word representing an
    //! element given by index.
    //!
    //! This function changes \p word in-place to contain a word (in the
    //! generators) equal to the element in position \p pos.
    //!
    //! \tparam Word the type of the second argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param word the word to clear and change in-place.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \complexity
    //! Constant.
    //!
    //! \note This function triggers an enumeration until it is complete or at
    //! least \p pos elements are found.
    //!
    //! \warning This function does not check that \p pos is valid. In
    //! particular, it is assumed that \p pos is strictly less than
    //! \ref FroidurePinBase::current_size.
    template <typename Word>
    void factorisation(FroidurePinBase&                    fpb,
                       Word&                               word,
                       FroidurePinBase::element_index_type pos) {
      return minimal_factorisation<Word>(fpb, word, pos);
    }

    //! \brief Returns a word representing an element given by index.
    //!
    //! This is similar to the three-argument version for
    //! \ref minimal_factorisation, but it returns a \c Word by value
    //! instead of modifying an argument in-place.
    //!
    //! \tparam Word the type of the second argument.
    //!
    //! \param fpb the FroidurePinBase instance.
    //! \param pos the index of the element whose factorisation is sought.
    //!
    //! \returns
    //! A word containing the factorisation of the given element.
    //!
    //! \throws LibsemigroupsException if \p pos is greater than or equal to
    //! size().
    //!
    //! \complexity
    //! At worst \f$O(mn)\f$ where \f$m\f$ equals \p pos and \f$n\f$ is the
    //! return value of FroidurePin::number_of_generators.
    //!
    //! \note This function triggers an enumeration until it is complete or at
    //! least \p pos elements are found.
    template <typename Word = word_type>
    [[nodiscard]] Word factorisation(FroidurePinBase&                    fpb,
                                     FroidurePinBase::element_index_type pos) {
      return minimal_factorisation<Word>(fpb, pos);
    }

    //! \brief Returns a range object containing normal forms for the so-far
    //! enumerated elements.
    //!
    //! This function returns a range object containing normal forms for the
    //! elements of \p fpb that have been enumerated so far.
    //!
    //! \param fpb the FroidurePinBase object.
    //!
    //! \returns The normal forms that have been enumerated so far.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \note This function does not trigger any enumeration.
    // TODO(1) complexity?
    [[nodiscard]] rx::iterator_range<
        FroidurePinBase::const_normal_form_iterator>
    current_normal_forms(FroidurePinBase const& fpb);

    //! \brief Returns a range object containing normal forms for all the
    //! elements.
    //!
    //! This function returns a range object containing normal forms for all of
    //! the elements of the semigroup. Calling this function triggers a full
    //! enumeration.
    //!
    //! \param fpb the FroidurePinBase object.
    //!
    //! \returns A range object containing the normal forms.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! The same as FroidurePinBase::enumerate.
    //!
    //! \note This function triggers a full enumeration.
    [[nodiscard]] rx::iterator_range<
        FroidurePinBase::const_normal_form_iterator>
    normal_forms(FroidurePinBase& fpb);

    //! \brief Returns a range object containing the so-far enumerated
    //! rules.
    //!
    //! This function returns a range object containing the rules of the
    //! semigroup that have been enumerated so far. Calling this function does
    //! not trigger any enumeration.
    //!
    //! \param fpb the FroidurePinBase object.
    //!
    //! \returns A range object containing the rules that have been enumerated
    //! so far.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \note This function does not trigger any enumeration.
    // TODO(1) complexity?
    [[nodiscard]] rx::iterator_range<FroidurePinBase::const_rule_iterator>
    current_rules(FroidurePinBase const& fpb);

    //! \brief Returns a range object containing all of the rules.
    //!
    //! This function returns a range object containing all of the rules of the
    //! semigroup that have been enumerated so far. Calling this function
    //! triggers a full enumeration.
    //!
    //! \param fpb the FroidurePinBase object.
    //!
    //! \returns A range object containing all of the rules.
    //!
    //! \exceptions
    //! \no_libsemigroups_except
    //!
    //! \complexity
    //! The same as FroidurePinBase::enumerate.
    //!
    //! \note This function triggers a full enumeration.
    [[nodiscard]] rx::iterator_range<FroidurePinBase::const_rule_iterator>
    rules(FroidurePinBase& fpb);

  }  // namespace froidure_pin
}  // namespace libsemigroups
#endif  // LIBSEMIGROUPS_FROIDURE_PIN_BASE_HPP_
