//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2024-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/>.
//

namespace libsemigroups {

  template <typename Word>
  Congruence<Word>& Congruence<Word>::init_no_base_classes() {
    report_prefix("Congruence");
    _generating_pairs.clear();
    _presentation.init();
    _race.init();
    _race.report_prefix("Congruence");
    _runners_initted = false;
    _runner_kinds.clear();
    return *this;
  }

  ////////////////////////////////////////////////////////////////////////
  // Congruence - out of line implementations
  ////////////////////////////////////////////////////////////////////////

  template <typename Word>
  Congruence<Word>::Congruence()
      : detail::CongruenceCommon(),
        _generating_pairs(),
        _presentation(),
        _race(),
        _runners_initted(),
        _runner_kinds() {
    init_no_base_classes();
  }

  template <typename Word>
  Congruence<Word>& Congruence<Word>::init() {
    detail::CongruenceCommon::init();
    return init_no_base_classes();
  }

  template <typename Word>
  Congruence<Word>::Congruence(Congruence const&) = default;

  template <typename Word>
  Congruence<Word>::Congruence(Congruence&&) = default;

  template <typename Word>
  Congruence<Word>& Congruence<Word>::operator=(Congruence const&) = default;

  template <typename Word>
  Congruence<Word>& Congruence<Word>::operator=(Congruence&&) = default;

  template <typename Word>
  Congruence<Word>::~Congruence() = default;

  template <typename Word>
  Congruence<Word>& Congruence<Word>::init(congruence_kind           knd,
                                           Presentation<Word> const& p) {
    p.throw_if_bad_alphabet_or_rules();
    detail::CongruenceCommon::init(knd);
    init_no_base_classes();
    _presentation = p;
    _race.max_threads(POSITIVE_INFINITY);
    return *this;
  }

  template <typename Word>
  template <typename Iterator1,
            typename Iterator2,
            typename Iterator3,
            typename Iterator4>
  tril Congruence<Word>::currently_contains_no_checks(Iterator1 first1,
                                                      Iterator2 last1,
                                                      Iterator3 first2,
                                                      Iterator4 last2) const {
    if (std::equal(first1, last1, first2, last2)) {
      return tril::TRUE;
    } else if (finished()) {
      auto winner_kind = _runner_kinds[_race.winner_index()];
      if (winner_kind == RunnerKind::TC) {
        return std::static_pointer_cast<ToddCoxeter<Word>>(_race.winner())
            ->currently_contains_no_checks(first1, last1, first2, last2);
      } else if (winner_kind == RunnerKind::KB) {
        return std::static_pointer_cast<KnuthBendix<Word>>(_race.winner())
            ->currently_contains_no_checks(first1, last1, first2, last2);
      } else {
        LIBSEMIGROUPS_ASSERT(winner_kind == RunnerKind::K);
        return std::static_pointer_cast<Kambites<Word>>(_race.winner())
            ->currently_contains_no_checks(first1, last1, first2, last2);
      }
    }
    init_runners();
    tril result = tril::unknown;
    for (auto const& [i, runner] : rx::enumerate(_race)) {
      if (_runner_kinds[i] == RunnerKind::TC) {
        result
            = std::static_pointer_cast<ToddCoxeter<Word>>(runner)
                  ->currently_contains_no_checks(first1, last1, first2, last2);
      } else if (_runner_kinds[i] == RunnerKind::KB) {
        result
            = std::static_pointer_cast<KnuthBendix<Word>>(runner)
                  ->currently_contains_no_checks(first1, last1, first2, last2);
      } else {
        LIBSEMIGROUPS_ASSERT(_runner_kinds[i] == RunnerKind::K);
        result
            = std::static_pointer_cast<Kambites<Word>>(runner)
                  ->currently_contains_no_checks(first1, last1, first2, last2);
      }
      if (result != tril::unknown) {
        break;
      }
    }
    return result;
  }

  template <typename Word>
  template <typename OutputIterator, typename Iterator1, typename Iterator2>
  OutputIterator
  Congruence<Word>::reduce_no_run_no_checks(OutputIterator d_first,
                                            Iterator1      first,
                                            Iterator2      last) const {
    // If we're finished then there's no longer a 1-1 correspondence between
    // entries in _runner_kinds and the Runners in _race, and so we need this
    // special case.
    if (finished()) {
      auto winner_kind = _runner_kinds[_race.winner_index()];
      if (winner_kind == RunnerKind::TC) {
        return std::static_pointer_cast<ToddCoxeter<Word>>(_race.winner())
            ->reduce_no_run_no_checks(d_first, first, last);
      } else if (winner_kind == RunnerKind::KB) {
        return std::static_pointer_cast<KnuthBendix<Word>>(_race.winner())
            ->reduce_no_run_no_checks(d_first, first, last);
      } else {
        LIBSEMIGROUPS_ASSERT(winner_kind == RunnerKind::K);
        return std::static_pointer_cast<Kambites<Word>>(_race.winner())
            ->reduce_no_run_no_checks(d_first, first, last);
      }
    }
    init_runners();
    // TODO(1) the following could be more nuanced.
    if (_runner_kinds[0] == RunnerKind::TC) {
      return std::static_pointer_cast<ToddCoxeter<Word>>(*_race.begin())
          ->reduce_no_run_no_checks(d_first, first, last);
    } else if (_runner_kinds[0] == RunnerKind::KB) {
      return std::static_pointer_cast<KnuthBendix<Word>>(*_race.begin())
          ->reduce_no_run_no_checks(d_first, first, last);
    } else {
      LIBSEMIGROUPS_ASSERT(_runner_kinds[0] == RunnerKind::K);
      return std::static_pointer_cast<Kambites<Word>>(*_race.begin())
          ->reduce_no_run_no_checks(d_first, first, last);
    }
  }

  template <typename Word>
  template <typename Iterator1, typename Iterator2>
  void Congruence<Word>::throw_if_letter_not_in_alphabet(Iterator1 first,
                                                         Iterator2 last) const {
    _presentation.throw_if_letter_not_in_alphabet(first, last);
  }

  template <typename Word>
  template <typename Thing>
  std::shared_ptr<Thing> Congruence<Word>::get() const {
    init_runners();
    RunnerKind       val;
    std::string_view name;
    if constexpr (std::is_same_v<Thing, Kambites<Word>>) {
      val  = RunnerKind::K;
      name = "Kambites";
    } else if constexpr (std::is_same_v<Thing, KnuthBendix<Word>>) {
      val  = RunnerKind::KB;
      name = "KnuthBendix";
    } else if constexpr (std::is_same_v<Thing, ToddCoxeter<Word>>) {
      val  = RunnerKind::TC;
      name = "ToddCoxeter";
    } else {
      LIBSEMIGROUPS_EXCEPTION(
          "There's no object of the specified type in the congruence");
    }

    if (finished()) {
      if (_runner_kinds[_race.winner_index()] == val) {
        return std::static_pointer_cast<Thing>(_race.winner());
      } else {
        LIBSEMIGROUPS_EXCEPTION("There's no {} in the congruence", name)
      }
    }
    auto it = std::find(_runner_kinds.begin(), _runner_kinds.end(), val);
    if (it == _runner_kinds.end()) {
      LIBSEMIGROUPS_EXCEPTION("There's no {} in the congruence", name)
    }
    return std::static_pointer_cast<Thing>(
        *(_race.begin() + std::distance(_runner_kinds.begin(), it)));
  }

  template <typename Word>
  template <typename Thing>
  bool Congruence<Word>::has() const {
    if (_race.empty()) {
      return false;
    }
    init_runners();
    RunnerKind val;
    if constexpr (std::is_same_v<Thing, Kambites<Word>>) {
      val = RunnerKind::K;
    } else if constexpr (std::is_same_v<Thing, KnuthBendix<Word>>) {
      val = RunnerKind::KB;
    } else if constexpr (std::is_same_v<Thing, ToddCoxeter<Word>>) {
      val = RunnerKind::TC;
    } else {
      return false;
    }

    if (finished()) {
      return _runner_kinds[_race.winner_index()] == val;
    }

    return std::find(_runner_kinds.begin(), _runner_kinds.end(), val)
           != _runner_kinds.end();
    // For some reason the following line works in C++ but not in the
    // python bindings
    // return _race.find_runner<Thing>() != nullptr;
  }

  template <typename Word>
  uint64_t Congruence<Word>::number_of_classes() {
    if (is_obviously_infinite(*this)) {
      return POSITIVE_INFINITY;
    }
    run();
    auto winner_kind = _runner_kinds[_race.winner_index()];
    if (winner_kind == RunnerKind::TC) {
      return std::static_pointer_cast<ToddCoxeter<Word>>(_race.winner())
          ->number_of_classes();
    } else if (winner_kind == RunnerKind::KB) {
      return std::static_pointer_cast<KnuthBendix<Word>>(_race.winner())
          ->number_of_classes();
    } else {
      LIBSEMIGROUPS_ASSERT(winner_kind == RunnerKind::K);
      return std::static_pointer_cast<Kambites<Word>>(_race.winner())
          ->number_of_classes();
    }
  }

  template <typename Word>
  void Congruence<Word>::init_runners() const {
    if (!_runners_initted) {
      _runners_initted = true;
      if (_race.empty()) {
        // Must call "is_obviously_infinite" before adding runners, because if
        // we don't then "is_obviously_infinite" will check if any of the so far
        // added runners are obviously infinite, which if presentation() defines
        // an infinite semigroup will return "true" regardless of what
        // generating pairs will be added later in this function.
        bool const is_infinite = is_obviously_infinite(*this);

        if (kind() == congruence_kind::twosided) {
          add_runner(std::make_shared<Kambites<Word>>(kind(), presentation()));
        }
        add_runner(std::make_shared<KnuthBendix<Word>>(kind(), presentation()));

        if (!is_infinite) {
          // We do this check here because of:
          // https://github.com/libsemigroups/libsemigroups/issues/907
          // The issue being that in Congruence::run_impl we run_until stopped,
          // and this means that any ToddCoxeter runners will actually run,
          // rather than refusing to because they are obviously infinite.
          // ToddCoxeter only refuses to run if it is obviously infinite and we
          // are trying to run to until finished, in which case it would run
          // forever. On the other if ToddCoxeter is run_until or run_for and is
          // obviously infinite, then this is okay assuming that stopped() will
          // return true at some point. It seems that in some of the CI jobs for
          // the GAP Semigroups package that this causes the machine to run out
          // of resources because it is running ToddCoxeter on an infinite
          // semigroup.
          add_runner(
              std::make_shared<ToddCoxeter<Word>>(kind(), presentation()));
          auto tc = std::make_shared<ToddCoxeter<Word>>(kind(), presentation());
          tc->strategy(ToddCoxeter<Word>::options::strategy::felsch);
          add_runner(std::move(tc));
        }
      }

      for (auto const& [i, runner] : rx::enumerate(_race)) {
        auto first = internal_generating_pairs().cbegin();
        auto last  = internal_generating_pairs().cend();
        // We have to call the specific add_generating_pair_no_checks for
        // each type of runner so that the generating pairs are correctly
        // modified to match the native letters in the alphabet
        if (_runner_kinds[i] == RunnerKind::TC) {
          auto         tc = std::static_pointer_cast<ToddCoxeter<Word>>(runner);
          size_t const n  = tc->number_of_generating_pairs();
          // Only add the generating pairs not already in the runners
          for (auto it = first + n; it != last; it += 2) {
            tc->add_generating_pair_no_checks(std::begin(*it),
                                              std::end(*it),
                                              std::begin(*(it + 1)),
                                              std::end(*(it + 1)));
          }
        } else if (_runner_kinds[i] == RunnerKind::KB) {
          auto         kb = std::static_pointer_cast<KnuthBendix<Word>>(runner);
          size_t const n  = kb->number_of_generating_pairs();
          // Only add the generating pairs not already in the runners
          for (auto it = first + n; it != last; it += 2) {
            kb->add_generating_pair_no_checks(std::begin(*it),
                                              std::end(*it),
                                              std::begin(*(it + 1)),
                                              std::end(*(it + 1)));
          }
        } else {
          LIBSEMIGROUPS_ASSERT(_runner_kinds[i] == RunnerKind::K);
          auto         k = std::static_pointer_cast<Kambites<Word>>(runner);
          size_t const n = k->number_of_generating_pairs();
          for (auto it = first + n; it != last; it += 2) {
            k->add_generating_pair_no_checks(std::begin(*it),
                                             std::end(*it),
                                             std::begin(*(it + 1)),
                                             std::end(*(it + 1)));
          }
        }
      }
    }
  }

  template <typename Word>
  void Congruence<Word>::run_impl() {
    init_runners();
    _race.run_until([this]() { return this->stopped(); });
  }

  template <typename Word>
  std::string to_human_readable_repr(Congruence<Word> const& c) {
    return fmt::format(
        "<{} Congruence over {} with {} gen. pair{}, {} runners>",
        c.kind() == congruence_kind::twosided ? "2-sided" : "1-sided",
        to_human_readable_repr(c.presentation()),
        detail::group_digits(c.number_of_generating_pairs()),
        c.number_of_generating_pairs() == 1 ? "" : "s",
        c.number_of_runners(),
        c.number_of_runners() > 1 ? "s" : "");
  }

  // This function is declared in obv-inf.hpp
  template <typename Word>
  bool is_obviously_infinite(Congruence<Word> const& c) {
    if (c.template has<ToddCoxeter<Word>>()) {
      auto const& tc = *c.template get<ToddCoxeter<Word>>();
      if (is_obviously_infinite(tc)) {
        return true;
      }
      auto const& wg = tc.current_word_graph();
      if (v4::word_graph::is_complete_no_checks(
              wg, rx::begin(wg.active_nodes()), rx::end(wg.active_nodes()))) {
        return false;
      }
    } else if (c.template has<KnuthBendix<Word>>()
               && is_obviously_infinite(*c.template get<KnuthBendix<Word>>())) {
      return true;
    } else if (c.template has<Kambites<Word>>()
               && is_obviously_infinite(*c.template get<Kambites<Word>>())) {
      return true;
    } else if (!c.finished()) {
      // We check last whether or not the internal presentation and generating
      // pairs indicate that "c" is obviously infinite. If not, then we
      // continue. This means we can potentially check whether a congruence is
      // obviously infinite without any runners being defined.
      // We have to do this last in case p defines the free semigroup, but there
      // is a ToddCoxeter runner that was created from a WordGraph, which is
      // caught above in the ToddCoxeter clause.
      auto const&         p = c.presentation();
      IsObviouslyInfinite ioi(p.alphabet().size());
      ioi.add_rules_no_checks(p.alphabet(), p.rules.cbegin(), p.rules.cend());
      ioi.add_rules_no_checks(p.alphabet(),
                              c.generating_pairs().cbegin(),
                              c.generating_pairs().cend());
      auto result = ioi.result();
      if (result) {
        return result;
      }
    }
    return false;
  }

}  // namespace libsemigroups
