/****************************************************************************
 *   This file is part of the aGrUM/pyAgrum library.                        *
 *                                                                          *
 *   Copyright (c) 2005-2026 by                                             *
 *       - Pierre-Henri WUILLEMIN(_at_LIP6)                                 *
 *       - Christophe GONZALES(_at_AMU)                                     *
 *                                                                          *
 *   The aGrUM/pyAgrum library is free software; you can redistribute it    *
 *   and/or modify it under the terms of either :                           *
 *                                                                          *
 *    - the GNU Lesser General Public License as published by               *
 *      the Free Software Foundation, either version 3 of the License,      *
 *      or (at your option) any later version,                              *
 *    - the MIT license (MIT),                                              *
 *    - or both in dual license, as here.                                   *
 *                                                                          *
 *   (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html)    *
 *                                                                          *
 *   This aGrUM/pyAgrum library is distributed in the hope that it will be  *
 *   useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,          *
 *   INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS *
 *   FOR A PARTICULAR PURPOSE  AND NONINFRINGEMENT. IN NO EVENT SHALL THE   *
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,        *
 *   ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  *
 *   OTHER DEALINGS IN THE SOFTWARE.                                        *
 *                                                                          *
 *   See LICENCES for more details.                                         *
 *                                                                          *
 *   SPDX-FileCopyrightText: Copyright 2005-2026                            *
 *       - Pierre-Henri WUILLEMIN(_at_LIP6)                                 *
 *       - Christophe GONZALES(_at_AMU)                                     *
 *   SPDX-License-Identifier: LGPL-3.0-or-later OR MIT                      *
 *                                                                          *
 *   Contact  : info_at_agrum_dot_org                                       *
 *   homepage : http://agrum.gitlab.io                                      *
 *   gitlab   : https://gitlab.com/agrumery/agrum                           *
 *                                                                          *
 ****************************************************************************/


#include <agrum/base/core/math/formula.h>

// Keep this here because of cyclic dependencies
#include <agrum/base/core/math/cocoR/Parser.h>
#include <agrum/base/core/math/cocoR/Scanner.h>

namespace gum {

  // ==========================================================================
  // ===                         Class FormulaPart                          ===
  // ==========================================================================

  // Helper function for debuging
  void print_stack(std::stack< FormulaPart > s) {
    std::cout << std::endl;
    std::list< FormulaPart > l;
    while (!s.empty()) {
      l.push_front(s.top());
      s.pop();
    }

    std::cout << "Stack: ";
    for (const auto& elt: l) {
      std::cout << elt.str() << " ";
    }
    std::cout << std::endl;
  }

  // Helper function for debuging
  void print_output(std::vector< FormulaPart > v) {
    std::cout << "Output: ";
    for (const auto& elt: v) {
      std::cout << elt.str() << " ";
    }
    std::cout << std::endl;
  }

  std::string func2str(FormulaPart::token_function func) {
    switch (func) {
      case FormulaPart::token_function::exp : {
        return "exp";
      }
      case FormulaPart::token_function::log : {
        return "log";
      }
      case FormulaPart::token_function::ln : {
        return "ln";
      }
      case FormulaPart::token_function::pow : {
        return "pow";
      }
      case FormulaPart::token_function::sqrt : {
        return "sqrt";
      }
      case FormulaPart::token_function::nil : {
        return "nil";
      }
      default : {
        GUM_ERROR(OperationNotAllowed, "unknown function")
      }
    }
  }

  FormulaPart::FormulaPart() : type(token_type::NIL), number(NAN), character('\0'), function(nil) {
    GUM_CONSTRUCTOR(FormulaPart);
  }

  FormulaPart::FormulaPart(token_type t, double n) :
      type(t), number(n), character('\0'), function(nil) {
    GUM_CONSTRUCTOR(FormulaPart);
  }

  FormulaPart::FormulaPart(token_type t, char c) :
      type(t), number(NAN), character(c), function(nil) {
    GUM_CONSTRUCTOR(FormulaPart);
  }

  FormulaPart::FormulaPart(token_type t, token_function func) :
      type(t), number(NAN), character('\0'), function(func) {
    GUM_CONSTRUCTOR(FormulaPart);
  }

  FormulaPart::FormulaPart(const FormulaPart& source) :
      type(source.type), number(source.number), character(source.character),
      function(source.function) {
    GUM_CONS_CPY(FormulaPart);
  }

  FormulaPart::FormulaPart(FormulaPart&& source) :
      type(std::move(source.type)), number(std::move(source.number)),
      character(std::move(source.character)), function(std::move(source.function)) {
    GUM_CONS_MOV(FormulaPart);
  }

  FormulaPart::~FormulaPart() {
    GUM_DESTRUCTOR(FormulaPart);
    ;
  }

  FormulaPart& FormulaPart::operator=(const FormulaPart& source) {
    if (this == &source) { return *this; }

    type      = source.type;
    number    = source.number;
    character = source.character;
    function  = source.function;

    return *this;
  }

  FormulaPart& FormulaPart::operator=(FormulaPart&& source) {
    if (this == &source) { return *this; }

    type      = std::move(source.type);
    number    = std::move(source.number);
    character = std::move(source.character);
    function  = std::move(source.function);

    return *this;
  }

  std::string FormulaPart::str() const {
    switch (type) {
      case token_type::NUMBER : {
        return std::format("{}", number);
      }

      case token_type::PARENTHESIS :
      case token_type::OPERATOR : {
        return (character == '\0') ? "\\0" : std::string(1, character);
      }

      case token_type::FUNCTION : {
        return func2str(function);
      }

      default : {
        GUM_ERROR(OperationNotAllowed, "unknown type")
      }
    }
  }

  // ==========================================================================
  // ===                        Class Formula                               ===
  // ==========================================================================

  void Formula::_initialise_() {
    auto c_str   = (unsigned char*)_formula_.c_str();
    auto scanner = new gum::formula::Scanner(c_str, (int)_formula_.size());
    _scanner_    = std::unique_ptr< gum::formula::Scanner >(scanner);

    auto parser = new gum::formula::Parser(scanner);
    _parser_    = std::unique_ptr< gum::formula::Parser >(parser);
    _parser_->formula(this);
  }

  Formula::Formula(short s) : _formula_(std::to_string(s)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(unsigned short us) : _formula_(std::to_string(us)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(int i) : _formula_(std::to_string(i)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(unsigned int ui) : _formula_(std::to_string(ui)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(long l) : _formula_(std::to_string(l)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(unsigned long ul) : _formula_(std::to_string(ul)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(long long l) : _formula_(std::to_string(l)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(unsigned long long ul) :
      _formula_(std::to_string(ul)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(float f) : _formula_(std::to_string(f)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(double d) : _formula_(std::to_string(d)), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);
    _initialise_();
  }

  Formula::Formula(const std::string& f) : _formula_(f), _last_token_(FormulaPart()) {
    GUM_CONSTRUCTOR(Formula);

    _initialise_();
  }

  Formula::Formula(const Formula& source) :
      _formula_(source._formula_), _last_token_(source._last_token_), _output_(source._output_),
      _stack_(source._stack_) {
    GUM_CONS_CPY(Formula);

    _initialise_();
  }

  Formula::Formula(Formula&& source) :
      _formula_(std::move(source._formula_)), _scanner_(std::move(source._scanner_)),
      _parser_(std::move(source._parser_)), _last_token_(std::move(source._last_token_)),
      _output_(std::move(source._output_)), _stack_(std::move(source._stack_)) {
    GUM_CONS_CPY(Formula);

    _parser_->formula(this);
  }

  Formula::~Formula() {
    GUM_DESTRUCTOR(Formula);
    ;
  }

  Formula& Formula::operator=(const Formula& source) {
    if (this == &source) { return *this; }

    _formula_    = source._formula_;
    _last_token_ = source._last_token_;
    _output_     = source._output_;
    _stack_      = source._stack_;

    _initialise_();

    return *this;
  }

  Formula& Formula::operator=(Formula&& source) {
    if (this == &source) { return *this; }

    _formula_ = std::move(source._formula_);
    _scanner_ = std::move(source._scanner_);
    _parser_  = std::move(source._parser_);
    _parser_->formula(this);
    _last_token_ = std::move(source._last_token_);
    _output_     = std::move(source._output_);
    _stack_      = std::move(source._stack_);

    return *this;
  }

  double Formula::result() const {
    _parser_->Parse();

    std::stack< FormulaPart > stack;
    if (_output_.empty()) { GUM_ERROR(OperationNotAllowed, "no output found") }

    for (auto item: _output_) {
      switch (item.type) {
        case FormulaPart::token_type::NUMBER : {
          stack.push(item);
          break;
        }

        case FormulaPart::token_type::OPERATOR :
        case FormulaPart::token_type::FUNCTION : {
          _reduceOperatorOrFunction_(item, stack);
          break;
        }

        default : {
          GUM_ERROR(OperationNotAllowed, "expecting numbers, operators or functions")
        }
      }
    }

    if (stack.size() != 1) {
      GUM_ERROR(OperationNotAllowed, "too many inputs")

    } else if (stack.top().type != FormulaPart::token_type::NUMBER) {
      GUM_ERROR(OperationNotAllowed, "too many inputs")
    }
    return stack.top().number;
  }

  bool FormulaPart::isLeftAssociative() const {
    switch (character) {
      case '+' :
      case '-' :
      case '*' :
      case '/' : {
        return true;
      }

      case '_' : {
        return false;
      }
      case '^' : {
        return false;
      }

      default : {
        GUM_ERROR(OperationNotAllowed, "A - not an operator")
      }
    }
  }

  int FormulaPart::precedence() const {
    switch (character) {
      case '+' :
      case '-' : {
        return 2;
      }

      case '*' :
      case '/' : {
        return 3;
      }

      case '^' : {
        return 4;
      }

      case '_' : {
        return 5;
      }

      default : {
        GUM_ERROR(OperationNotAllowed, "B - not an operator")
      }
    }
  }

  size_t FormulaPart::argc() const {
    switch (type) {
      case OPERATOR : {
        return _operator_argc_();
      }

      case FUNCTION : {
        return _function_argc_();
      }

      default : {
        GUM_ERROR(OperationNotAllowed, "expecting a function or an operator")
      }
    }
  }

  size_t FormulaPart::_operator_argc_() const {
    switch (character) {
      case '_' : {
        return (size_t)1;
      }
      case '+' :
      case '-' :
      case '*' :
      case '/' :
      case '^' : {
        return (size_t)2;
      }

      default : {
        GUM_ERROR(OperationNotAllowed, "C - not an operator")
      }
    }
  }

  size_t FormulaPart::_function_argc_() const {
    switch (function) {
      case FormulaPart::token_function::exp : {
        return 1;
      }
      case FormulaPart::token_function::log : {
        return 1;
      }
      case FormulaPart::token_function::ln : {
        return 1;
      }
      case FormulaPart::token_function::pow : {
        return 2;
      }
      case FormulaPart::token_function::sqrt : {
        return 1;
      }
      // case FormulaPart::token_function::nil: { return "nil"; }
      default : {
        GUM_ERROR(OperationNotAllowed, "unknown function")
      }
    }
  }

  double FormulaPart::_operator_eval_(const std::vector< FormulaPart >& args) const {
    switch (character) {
      case '+' : {
        return args[1].number + args[0].number;
      }

      case '-' : {
        return args[1].number - args[0].number;
      }

      case '*' : {
        return args[1].number * args[0].number;
      }

      case '/' : {
        return args[1].number / args[0].number;
      }

      case '^' : {
        return std::pow(args[1].number, args[0].number);
      }

      case '_' : {
        return 0 - args[0].number;
      }

      default : {
        GUM_ERROR(OperationNotAllowed, "D - not an operator")
      }
    }
  }

  double FormulaPart::_function_eval_(const std::vector< FormulaPart >& args) const {
    switch (function) {
      case FormulaPart::token_function::exp : {
        return std::exp(args[0].number);
      }
      case FormulaPart::token_function::log : {
        return std::log(args[0].number);
      }
      case FormulaPart::token_function::ln : {
        return std::log(args[0].number);
      }
      case FormulaPart::token_function::pow : {
        return std::pow(args[1].number, args[0].number);
      }
      case FormulaPart::token_function::sqrt : {
        return std::sqrt(args[0].number);
      }
      // case FormulaPart::token_function::nil: { return "nil"; }
      default : {
        GUM_ERROR(OperationNotAllowed, "unknown function")
      }
    }
  }

  FormulaPart FormulaPart::eval(const std::vector< FormulaPart >& args) const {
    switch (type) {
      case OPERATOR : {
        return {token_type::NUMBER, _operator_eval_(args)};
      }

      case FUNCTION : {
        return {token_type::NUMBER, _function_eval_(args)};
      }

      default : {
        GUM_ERROR(OperationNotAllowed, "cannot evaluate expression")
      }
    }
  }

  void Formula::_finalize_() {
    while (!_stack_.empty()) {
      if (_stack_.top().character == '(') { GUM_ERROR(OperationNotAllowed, "expecting ')'") }

      _push_output_(_stack_.top());
      _stack_.pop();
    }
  }

  void Formula::_push_function_(std::string_view func) {
    if (func == "exp") {
      FormulaPart t(FormulaPart::token_type::FUNCTION, FormulaPart::token_function::exp);
      _push_stack_(t);

    } else if (func == "log") {
      FormulaPart t(FormulaPart::token_type::FUNCTION, FormulaPart::token_function::log);
      _push_stack_(t);

    } else if (func == "ln") {
      FormulaPart t(FormulaPart::token_type::FUNCTION, FormulaPart::token_function::ln);
      _push_stack_(t);

    } else if (func == "pow") {
      FormulaPart t(FormulaPart::token_type::FUNCTION, FormulaPart::token_function::pow);
      _push_stack_(t);

    } else if (func == "sqrt") {
      FormulaPart t(FormulaPart::token_type::FUNCTION, FormulaPart::token_function::sqrt);
      _push_stack_(t);

    } else {
      GUM_ERROR(OperationNotAllowed, "unknown function")
    }
  }

  void Formula::_push_identifier_(std::string_view ident) {
    try {
      _push_function_(ident);

    } catch (OperationNotAllowed const&) {
      try {
        _push_variable_(ident);

      } catch (OperationNotAllowed const&) { GUM_ERROR(OperationNotAllowed, "unknown identifier") }
    }
  }
}   // namespace gum

#ifdef GUM_NO_INLINE
#  include <agrum/base/core/math/formula_inl.h>
#endif   // GUM_NO_INLINE
