//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2022 James D. Mitchell
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

// This file contains an implementation of Stephen's procedure for finitely
// presented semigroups and monoids.

#include "libsemigroups/stephen.hpp"

#include <chrono>                                  // for duration, duration...
#include <tuple>                                   // for tie, tuple
#include <utility>                                 // for move, pair
                                                   //
#include "libsemigroups/constants.hpp"             // for operator==, Undefined
#include "libsemigroups/debug.hpp"                 // for LIBSEMIGROUPS_ASSERT
#include "libsemigroups/digraph-helper.hpp"        // for last_node_on_path_nc
#include "libsemigroups/exception.hpp"             // for LibsemigroupsExcep...
#include "libsemigroups/present.hpp"               // for Presentation<>::wo...
#include "libsemigroups/report.hpp"                // for REPORT_DEFAULT_V3
#include "libsemigroups/string.hpp"                // for group_digits
#include "libsemigroups/todd-coxeter-digraph.hpp"  // for ToddCoxeterDigraph...
#include "libsemigroups/types.hpp"                 // for word_type

namespace libsemigroups {

  ////////////////////////////////////////////////////////////////////////
  // Public
  ////////////////////////////////////////////////////////////////////////

  Stephen::Stephen()
      : _finished(false), _accept_state(UNDEFINED), _word(), _word_graph() {}

  Stephen& Stephen::set_word(word_type const& w) {
    presentation().validate_word(w.cbegin(), w.cend());
    reset();
    _word = w;
    return *this;
  }

  Stephen& Stephen::set_word(word_type&& w) {
    presentation().validate_word(w.cbegin(), w.cend());
    reset();
    _word = std::move(w);
    return *this;
  }

  Stephen::node_type Stephen::accept_state() {
    if (_accept_state == UNDEFINED) {
      using action_digraph_helper::last_node_on_path_nc;
      run();
      _accept_state
          = last_node_on_path_nc(_word_graph, 0, _word.cbegin(), _word.cend())
                .first;
    }
    return _accept_state;
  }

  ////////////////////////////////////////////////////////////////////////
  // Private Member Functions
  ////////////////////////////////////////////////////////////////////////

  void Stephen::init_impl(Presentation<word_type>&& p, non_lvalue_tag) {
    if (p.alphabet().empty()) {
      LIBSEMIGROUPS_EXCEPTION(
          "the argument (Presentation) must not have 0 generators");
    }
    reset();
    _presentation = std::move(p);
    presentation::normalize_alphabet(_presentation);
    _word_graph.init(_presentation);
    _word.clear();
  }

  void Stephen::report_status(
      std::chrono::high_resolution_clock::time_point const& start_time) {
    if (!report()) {
      return;
    }
    using std::chrono::duration_cast;
    using std::chrono::seconds;
    auto now        = std::chrono::high_resolution_clock::now();
    auto total_time = duration_cast<seconds>(now - start_time);

    auto&   stats = _word_graph.stats();
    int64_t diff  = int64_t(_word_graph.number_of_nodes_active()
                           - stats.prev_active_nodes);

    // TODO(v3) use fmtlib
    static bool first_call = true;
    if (first_call) {
      first_call = false;
      REPORT_DEFAULT_V3("Stephen: " + std::string(60, '-') + "\n");
      REPORT_DEFAULT_V3("Stephen: %11s | %11s | %11s | %11s |\n",
                        "nodes",
                        "defined",
                        "killed",
                        "diff");
      REPORT_DEFAULT_V3("Stephen: " + std::string(60, '-') + "\n");
    }
    REPORT_DEFAULT_V3(
        "Stephen: %11s | %11s | %11s | %11s | "
        "(%llus)\n",
        detail::group_digits(_word_graph.number_of_nodes_active()).c_str(),
        ("+"
         + detail::group_digits(int64_t(_word_graph.number_of_nodes_defined()
                                        - stats.prev_nodes_defined)))
            .c_str(),
        ("-"
         + detail::group_digits(int64_t(_word_graph.number_of_nodes_killed()
                                        - stats.prev_nodes_killed)))
            .c_str(),
        ((diff < 0 ? "" : "+") + detail::group_digits(diff)).c_str(),
        total_time.count());
    _word_graph.stats_check_point();
  }

  void Stephen::reset() noexcept {
    _finished     = false;
    _accept_state = UNDEFINED;
  }

  void Stephen::run_impl() {
    auto start_time = std::chrono::high_resolution_clock::now();
    validate();  // throws if no presentation is defined
    _word_graph.init(presentation());
    _word_graph.complete_path(0, _word.cbegin(), _word.cend());
    node_type& current     = _word_graph.cursor();
    auto const rules_begin = presentation().rules.cbegin();
    auto const rules_end   = presentation().rules.cend();
    bool       did_change  = true;

    do {
      current    = 0;
      did_change = false;
      while (current != _word_graph.first_free_node() && !stopped()) {
        for (auto it = rules_begin; it < rules_end; it += 2) {
          node_type                 u_end;
          word_type::const_iterator rit;
          bool                      did_def = false;
          std::tie(u_end, rit) = action_digraph_helper::last_node_on_path_nc(
              _word_graph, current, it->cbegin(), it->cend());
          node_type c;
          if (rit == it->cend()) {
            ++it;
            std::tie(did_def, c) = _word_graph.complete_path(
                current, it->cbegin(), it->cend() - 1);
            node_type v_end = _word_graph.unsafe_neighbor(c, it->back());
            if (v_end == UNDEFINED) {
              did_def = true;
              _word_graph.add_edge_nc(c, u_end, it->back());
            } else if (u_end != v_end) {
              did_def = true;
              _word_graph.coincide_nodes(u_end, v_end);
              _word_graph.process_coincidences();
            }
            --it;
          } else {
            ++it;
            node_type v_end;
            std::tie(v_end, rit) = action_digraph_helper::last_node_on_path_nc(
                _word_graph, current, it->cbegin(), it->cend());
            if (rit == it->cend()) {
              --it;
              c = _word_graph
                      .complete_path(current, it->cbegin(), it->cend() - 1)
                      .second;
              u_end = _word_graph.unsafe_neighbor(c, it->back());
              LIBSEMIGROUPS_ASSERT(u_end == UNDEFINED);
              _word_graph.add_edge_nc(c, v_end, it->back());
              did_def = true;
            } else {
              --it;
            }
          }
          did_change |= did_def;
        }
        report_status(start_time);
        current = _word_graph.next_active_node(current);
      }
    } while (did_change && !stopped());
    if (!stopped()) {
      _finished = true;
      standardize();
    }
  }

  void Stephen::standardize() {
    digraph_with_sources::standardize(_word_graph);
    _word_graph.shrink_to_fit(_word_graph.number_of_nodes_active());
  }

  void Stephen::validate() const {
    if (_presentation.alphabet().empty()) {
      LIBSEMIGROUPS_EXCEPTION(
          "no presentation defined, use Stephen::init to set the presentation");
    }
  }

  namespace stephen {

    bool accepts(Stephen& s, word_type const& w) {
      using action_digraph_helper::last_node_on_path;
      s.run();
      return s.accept_state()
             == last_node_on_path(s.word_graph(), 0, w.cbegin(), w.cend())
                    .first;
    }

    bool is_left_factor(Stephen& s, word_type const& w) {
      using action_digraph_helper::last_node_on_path;
      s.run();
      return last_node_on_path(s.word_graph(), 0, w.cbegin(), w.cend()).second
             == w.cend();
    }
  }  // namespace stephen

}  // namespace libsemigroups
