//
// libsemigroups - C++ library for semigroups and monoids
// Copyright (C) 2019 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 a base class for f.p. semigroup like classes.

#include "libsemigroups/fpsemi-intf.hpp"

#include <algorithm>  // for sort
#include <string>     // for std::string

#include "libsemigroups/config.hpp"             // for LIBSEMIGROUPS_DEBUG
#include "libsemigroups/exception.hpp"          // for LIBSEMIGROUPS_EXCEPTION
#include "libsemigroups/froidure-pin-base.hpp"  // for FroidurePinBase
#include "libsemigroups/report.hpp"             // for REPORT_DEFAULT ...
#include "libsemigroups/string.hpp"             // for detail::to_string

namespace libsemigroups {

  namespace {
    char to_human_readable(char c) noexcept {
      if (c > 95) {
        return c;
      } else {
#ifndef LIBSEMIGROUPS_DEBUG
        return c + 96;
#else
        return c;
#endif
      }
    }
  }  // namespace

  //////////////////////////////////////////////////////////////////////////////
  // FpSemigroupInterface - constructor + destructor - public
  //////////////////////////////////////////////////////////////////////////////

  FpSemigroupInterface::FpSemigroupInterface()
      : Runner(),
        // Non-mutable
        _alphabet(),
        _alphabet_map(),
        _identity_defined(false),
        _identity(),
        _inverses(),
        _rules(),
        // Mutable
        _froidure_pin(nullptr),
        _is_obviously_finite(false),
        _is_obviously_infinite(false) {}

  FpSemigroupInterface::~FpSemigroupInterface() = default;

  ////////////////////////////////////////////////////////////////////////////
  // Runner - pure virtual overridden function - public
  ////////////////////////////////////////////////////////////////////////////

  void FpSemigroupInterface::before_run() {
    if (alphabet().empty()) {
      LIBSEMIGROUPS_EXCEPTION("no alphabet specified!");
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  // FpSemigroupInterface - non-pure virtual member functions - public
  //////////////////////////////////////////////////////////////////////////////

  bool FpSemigroupInterface::equal_to(word_type const& u, word_type const& v) {
    validate_word(u);
    validate_word(v);
    return equal_to(word_to_string(u), word_to_string(v));
  }

  word_type FpSemigroupInterface::normal_form(word_type const& w) {
    validate_word(w);
    return string_to_word(normal_form(word_to_string(w)));
  }

  //////////////////////////////////////////////////////////////////////////////
  // FpSemigroupInterface - non-virtual member functions - public
  //////////////////////////////////////////////////////////////////////////////

  void FpSemigroupInterface::set_alphabet(std::string const& lphbt) {
    if (!_alphabet.empty()) {
      LIBSEMIGROUPS_EXCEPTION("the alphabet cannot be set more than once");
    } else if (lphbt.empty()) {
      LIBSEMIGROUPS_EXCEPTION("the alphabet must be non-empty");
    }
    for (size_t i = 0; i < lphbt.size(); ++i) {
      if (_alphabet_map.find(lphbt[i]) != _alphabet_map.end()) {
        _alphabet_map.clear();  // Strong exception guarantee
        LIBSEMIGROUPS_EXCEPTION(
            "invalid alphabet, it contains the duplicate letter "
            + detail::to_string(lphbt[i]));
      }
      _alphabet_map.emplace(lphbt[i], i);
    }
    _alphabet = lphbt;
    set_alphabet_impl(lphbt);
    reset();
  }

  void FpSemigroupInterface::set_alphabet(size_t number_of_letters) {
    if (!_alphabet.empty()) {
      LIBSEMIGROUPS_EXCEPTION("the alphabet cannot be set more than once");
    } else if (number_of_letters == 0) {
      LIBSEMIGROUPS_EXCEPTION("the alphabet must be non-empty");
    } else if (number_of_letters > 256) {
      LIBSEMIGROUPS_EXCEPTION("the alphabet must contain at most 256 letters");
    }
    for (size_t i = 0; i < number_of_letters; ++i) {
#ifdef LIBSEMIGROUPS_DEBUG
      _alphabet += static_cast<char>(i + 97);
#else
      _alphabet += static_cast<char>(i + 1);
#endif
      _alphabet_map.emplace(_alphabet[i], i);
    }
    set_alphabet_impl(number_of_letters);
    reset();
  }

  void FpSemigroupInterface::add_rules(FroidurePinBase& S) {
    if (!_alphabet.empty() && _alphabet.size() != S.number_of_generators()) {
      LIBSEMIGROUPS_EXCEPTION("incompatible number of generators, found "
                              + detail::to_string(S.number_of_generators())
                              + ", should be at most "
                              + detail::to_string(_alphabet.size()));
    }
    add_rules_impl(S);
    reset();
  }

  // Note that this can be called repeatedly, and that's fine.
  void FpSemigroupInterface::set_identity(std::string const& id) {
    if (validate_identity_impl(id)) {
      _identity = id[0];
      for (auto l : alphabet()) {
        if (l == id[0]) {
          add_rule(id + id, id);
        } else {
          add_rule(detail::to_string(l) + id, detail::to_string(l));
          add_rule(id + detail::to_string(l), detail::to_string(l));
        }
      }
    }
    _identity_defined = true;
  }

  std::string const& FpSemigroupInterface::identity() const {
    if (_identity_defined) {
      return _identity;
    } else {
      LIBSEMIGROUPS_EXCEPTION("no identity has been defined");
    }
  }

  void FpSemigroupInterface::set_inverses(std::string const& inv) {
    if (!_inverses.empty()) {
      LIBSEMIGROUPS_EXCEPTION(
          "inverses already defined, cannot define inverses more than once");
    } else if (_alphabet.empty()) {
      LIBSEMIGROUPS_EXCEPTION(
          "no alphabet has been defined, define an alphabet first");
    } else if (!_identity_defined) {
      LIBSEMIGROUPS_EXCEPTION(
          "no identity has been defined, define an identity first");
    } else if (_alphabet.size() != inv.size()) {
      LIBSEMIGROUPS_EXCEPTION("invalid inverses, expected "
                              + detail::to_string(_alphabet.size())
                              + " but found " + detail::to_string(inv.size()));
    }

    // Checks the inv consists of valid letters in the alphabet
    validate_word(inv);

    std::string cpy = inv;
    std::sort(cpy.begin(), cpy.end());
    for (auto it = cpy.cbegin(); it < cpy.cend() - 1; ++it) {
      if (*it == *(it + 1)) {
        LIBSEMIGROUPS_EXCEPTION(
            "invalid inverses, it contains the duplicate letter "
            + detail::to_string(*it));
      }
    }

    // Check that (x ^ - 1) ^ -1 = x
    for (size_t i = 0; i < _alphabet.size(); ++i) {
      if (_alphabet[i] == identity()[0] && inv[i] != identity()[0]) {
        LIBSEMIGROUPS_EXCEPTION(
            "invalid inverses, the identity is %c, but %c ^ -1 = %c",
            _alphabet[i],
            _alphabet[i],
            inv[i]);
      }
      for (size_t j = 0; j < _alphabet.size(); ++j) {
        if (_alphabet[j] == inv[i]) {
          if (inv[j] != _alphabet[i]) {
            LIBSEMIGROUPS_EXCEPTION(
                "invalid inverses, %c ^ -1 = %c but %c ^ -1 = %c",
                _alphabet[i],
                inv[i],
                inv[i],
                inv[j]);
          }
          break;
        }
      }
    }

    _inverses = inv;

    for (size_t i = 0; i < _alphabet.size(); ++i) {
      if (_alphabet[i] != identity()[0]) {
        add_rule(std::string(1, _alphabet[i]) + _inverses[i], _identity);
      }
    }
  }

  std::string const& FpSemigroupInterface::inverses() const {
    if (!_inverses.empty()) {
      return _inverses;
    } else {
      LIBSEMIGROUPS_EXCEPTION("no inverses have been defined");
    }
  }

  word_type FpSemigroupInterface::string_to_word(std::string const& s) const {
    word_type w;
    w.reserve(s.size());
    for (char const& c : s) {
      w.push_back(char_to_uint(c));
    }
    return w;
  }

  std::string FpSemigroupInterface::word_to_string(word_type const& w) const {
    validate_word(w);
    std::string s;
    s.reserve(w.size());
    for (letter_type const& l : w) {
      s.push_back(uint_to_char(l));
    }
    return s;
  }

  bool FpSemigroupInterface::is_obviously_infinite() {
    REPORT_DEFAULT("checking if the semigroup is obviously infinite . . .\n");
    // If has_parent_froidure_pin(), then that is either finite (and so this
    // is not obviously infinite), or infinite, which is undecidable in
    // general, so we leave the answer to this question to
    // is_quotient_obviously_infinite_impl in the derived class.
    if (alphabet().empty()) {
      // If number_of_generators() is undefined, then there is no quotient yet,
      // and so it is not obviously infinite, or anything!
      REPORT_VERBOSE_DEFAULT("not obviously infinite (no alphabet defined)\n");
      return false;
    } else if (has_froidure_pin() && froidure_pin()->finished()) {
      // If the isomorphic FroidurePin is fully enumerated, it must be
      // finite, and hence this is not (obviously) infinite.
      REPORT_VERBOSE_DEFAULT("not obviously infinite (finite)\n");
      return false;
    } else if (is_obviously_infinite_impl()) {
      // The derived class of FpSemigroupInterface knows the quotient is
      // infinite
      return true;
    }
    return false;
  }

  bool FpSemigroupInterface::is_obviously_finite() {
    if (alphabet().empty()) {
      // If the alphabet is empty, then so is the fp semigrorup and so it is
      // obviously finite.
      REPORT_VERBOSE_DEFAULT("obviously finite (no alphabet defined)\n");
      return true;
    } else if (has_froidure_pin() && froidure_pin()->finished()) {
      // If the isomorphic FroidurePin is fully enumerated, it must be
      // finite, and hence this is (obviously) finite.
      REPORT_VERBOSE_DEFAULT("obviously finite (finite)\n");
      return true;
    } else if (is_obviously_finite_impl()) {
      // The derived class of FpSemigroupInterface knows it is
      // finite
      return true;
    }
    return false;
  }

  std::string FpSemigroupInterface::to_gap_string() {
    auto to_gap_word = [](std::string const& w) -> std::string {
      if (w.empty()) {
        return "One(free)";
      }
      std::string out;
      for (auto it = w.cbegin(); it < w.cend() - 1; ++it) {
        out += to_human_readable(*it);
        out += " * ";
      }
      out += to_human_readable(w.back());
      return out;
    };

    std::string out = "free := FreeMonoid(";
    for (auto x : alphabet()) {
      out += std::string("\"") + to_human_readable(x) + "\"";
      if (x != alphabet().back()) {
        out += ", ";
      }
    }
    out += ");\n";
    out += "AssignGeneratorVariables(free);\n";
    out += "rules := [\n";
    for (auto it = cbegin_rules(); it < cend_rules(); ++it) {
      out += "          [";
      out += to_gap_word(it->first);
      out += ", ";
      out += to_gap_word(it->second);
      if (it != cend_rules() - 1) {
        out += "],\n";
      } else {
        out += "]\n";
      }
    }
    out += "         ];\n";
    out += "S := free / rules;\n";
    return out;
  }

  //////////////////////////////////////////////////////////////////////////////
  // FpSemigroupInterface - non-virtual member functions - protected
  //////////////////////////////////////////////////////////////////////////////

  void FpSemigroupInterface::validate_letter(char c) const {
    if (_alphabet.empty()) {
      LIBSEMIGROUPS_EXCEPTION("no alphabet has been defined");
    } else if (_alphabet_map.find(c) == _alphabet_map.end()) {
      LIBSEMIGROUPS_EXCEPTION(
          "invalid letter %c, valid letters are \"%s\"", c, _alphabet.c_str());
    }
  }

  void FpSemigroupInterface::validate_letter(letter_type c) const {
    if (_alphabet.empty()) {
      LIBSEMIGROUPS_EXCEPTION("no alphabet has been defined");
    } else if (c >= _alphabet.size()) {
      LIBSEMIGROUPS_EXCEPTION(
          "invalid letter %d, the valid range is [0, %d)", c, _alphabet.size());
    }
  }

  // void FpSemigroupInterface::validate_relation(std::string const& l,
  //                                              std::string const& r) const
  //                                              {
  //   validate_word(l);
  //   validate_word(r);
  // }

  // void FpSemigroupInterface::validate_relation(
  //     std::pair<std::string, std::string> const& p) const {
  //   validate_relation(p.first, p.second);
  // }

  // void FpSemigroupInterface::validate_relation(word_type const& l,
  //                                              word_type const& r) const {
  //   validate_word(l);
  //   validate_word(r);
  // }

  // void FpSemigroupInterface::validate_relation(relation_type const& r)
  // const {
  //   validate_relation(r.first, r.second);
  // }

  //////////////////////////////////////////////////////////////////////////////
  // FpSemigroupInterface - non-pure virtual member functions - private
  //////////////////////////////////////////////////////////////////////////////

  void FpSemigroupInterface::set_alphabet_impl(std::string const&) {
    // do nothing
  }

  void FpSemigroupInterface::set_alphabet_impl(size_t) {
    // do nothing
  }

  void FpSemigroupInterface::add_rules_impl(FroidurePinBase& S) {
    S.run();
    for (auto it = S.cbegin_rules(); it != S.cend_rules(); ++it) {
      add_rule(*it);
    }
  }

  bool FpSemigroupInterface::is_obviously_finite_impl() {
    return false;
  }

  void FpSemigroupInterface::validate_word_impl(std::string const& w) const {
    if (w.empty()) {
      LIBSEMIGROUPS_EXCEPTION("invalid word, found the empty word but "
                              "words must be non-empty");
    }
  }

  void FpSemigroupInterface::validate_word_impl(word_type const& w) const {
    if (w.empty()) {
      LIBSEMIGROUPS_EXCEPTION("invalid word, found the empty word but "
                              "words must be non-empty");
    }
  }

  bool
  FpSemigroupInterface::validate_identity_impl(std::string const& id) const {
    if (id.length() != 1) {
      LIBSEMIGROUPS_EXCEPTION(
          "invalid identity, expected 1 letter, found %d letters", id.length())
    }
    validate_letter(id[0]);
    return true;
  }

  //////////////////////////////////////////////////////////////////////////////
  // FpSemigroupInterface - non-virtual member functions - private
  //////////////////////////////////////////////////////////////////////////////

  void FpSemigroupInterface::reset() noexcept {
    // set_finished(false);
    _froidure_pin          = nullptr;
    _is_obviously_finite   = false;
    _is_obviously_infinite = false;
  }

  void FpSemigroupInterface::add_rule_private(std::string&& u,
                                              std::string&& v) {
    if (started()) {
      LIBSEMIGROUPS_EXCEPTION("cannot add further rules at this stage");
      // Note that there is actually nothing that prevents us from adding
      // rules to KnuthBendix (i.e. it is setup so that it can be run
      // (partially or fully) and then more rules can be added and everything
      // is valid. We add this restriction to simplify things in the first
      // instance.
    }
    validate_word(u);
    validate_word(v);
    if (u == v) {
      return;
    }
    _rules.emplace_back(u, v);
    add_rule_impl(_rules.back().first, _rules.back().second);
    reset();
  }
}  // namespace libsemigroups
