/*
 * Copyright © 2006 Ondra Kamenik
 * Copyright © 2019 Dynare Team
 *
 * This file is part of Dynare.
 *
 * Dynare 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.
 *
 * Dynare 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 Dynare.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "atom_assignings.hh"
#include "location.hh"
#include "parser_exception.hh"

#include "utils/cc/exception.hh"

#include <limits>
#include <iostream>
#include <sstream>
#include <iomanip>

using namespace ogp;

AtomAssignings::AtomAssignings(const AtomAssignings &aa, ogp::StaticAtoms &a)
  : atoms(a), expr(aa.expr, atoms), left_names(aa.left_names),
    lname2expr(aa.lname2expr), order(aa.order)
{
}

/** A global symbol for passing info to the AtomAssignings from
 * asgn_parse(). */
AtomAssignings *aparser;

/** The declaration of functions defined in asgn_ll.cc and asgn_tab.cc
 * generated from assign.lex assign.y */
void *asgn__scan_string(const char *);
void asgn__destroy_buffer(void *);
void asgn_parse();
extern location_type asgn_lloc;

void
AtomAssignings::parse(const string &stream)
{
  asgn_lloc.off = 0;
  asgn_lloc.ll = 0;
  void *p = asgn__scan_string(stream.c_str());
  aparser = this;
  asgn_parse();
  asgn__destroy_buffer(p);
}

void
AtomAssignings::error(string mes)
{
  throw ParserException(std::move(mes), asgn_lloc.off);
}

void
AtomAssignings::add_assignment_to_double(string name, double val)
{
  // if left hand side is a registered atom, insert it to tree
  int t;
  try
    {
      if (atoms.check(name))
        t = expr.add_nulary(name);
      else
        t = -1;
    }
  catch (const ParserException &e)
    {
      t = -1;
    }
  // register left hand side in order
  order.push_back(t);

  // add the double to the tree
  std::ostringstream buf;
  buf << std::setprecision(std::numeric_limits<double>::max_digits10)
      << val;
  try
    {
      expr.parse(buf.str());
    }
  catch (const ParserException &e)
    {
      // should never happen
      throw ParserException(string("Error parsing double ")+buf.str()+": "+e.message(), 0);
    }

  // register name of the left hand side and put to lname2expr
  left_names.insert(name);
  lname2expr.emplace(std::move(name), order.size()-1);
}

void
AtomAssignings::add_assignment(int asgn_off, const string &str, int name_len,
                               int right_off, int right_len)
{
  // the order of doing things here is important: since the
  // FormulaParser requires that all references from the i-th tree
  // refere to trees with index lass than i, so to capture also a
  // nulary term for the left hand side, it must be inserted to the
  // expression tree before the expression is parsed.

  // find the name in the atoms
  string name = str.substr(0, name_len);

  // if left hand side is a registered atom, insert it to tree
  int t;
  try
    {
      t = atoms.check(name);
      if (t == -1)
        t = expr.add_nulary(name);
    }
  catch (const ParserException &e)
    {
      atoms.register_name(name);
      t = expr.add_nulary(name);
    }
  // register left hand side in order
  order.push_back(t);

  // parse expression on the right
  try
    {
      expr.parse(str.substr(right_off, right_len));
    }
  catch (const ParserException &e)
    {
      throw ParserException(e, asgn_off+right_off);
    }

  // register name of the left hand side and put to lname2expr
  left_names.insert(name);
  if (lname2expr.find(name) != lname2expr.end())
    {
      // Prevent the occurrence of #415
      std::cerr << "Changing the value of " << name << " through a second assignment (e.g. in initval) is not supported. Aborting." << std::endl;
      exit(EXIT_FAILURE);
    }
  lname2expr[name] = order.size()-1;
}

void
AtomAssignings::apply_subst(const AtomSubstitutions::Toldnamemap &mm)
{
  // go through all old variables and see what are their derived new
  // variables
  for (const auto &it : mm)
    {
      const string &oldname = it.first;
      const AtomSubstitutions::Tshiftnameset &sset = it.second;
      if (!sset.empty())
        {
          int told = atoms.index(oldname);
          if (told < 0 && !atoms.get_name_storage().query(oldname))
            atoms.register_name(oldname);
          if (told == -1)
            told = expr.add_nulary(oldname);
          // at least one substitution here, so make an expression
          expr.add_formula(told);
          // say that this expression is not assigned to any atom
          order.push_back(-1);
          // now go through all new names derived from the old name and
          // reference to the newly added formula
          for (const auto &itt : sset)
            {
              const string &newname = itt.first;
              left_names.insert(newname);
              lname2expr.emplace(newname, expr.nformulas()-1);
            }
        }
    }
}

void
AtomAssignings::print() const
{
  std::cout << "Atom Assignings\nExpressions:\n";
  expr.print();
  std::cout << "Left names:\n";
  for (auto it : lname2expr)
    std::cout << it.first << u8" ⇒ " << expr.formula(it.second) << " (t=" << order[it.second] << ")\n";
}

void
AtomAsgnEvaluator::setValues(EvalTree &et) const
{
  // set values of constants
  aa.atoms.setValues(et);

  // set values of variables to NaN or to user set values
  double nan = std::numeric_limits<double>::quiet_NaN();
  for (int i = 0; i < aa.atoms.nvar(); i++)
    {
      const string &ss = aa.atoms.name(i);
      int t = aa.atoms.index(ss);
      if (t >= 0)
        {
          auto it = user_values.find(t);
          if (it == user_values.end())
            et.set_nulary(t, nan);
          else
            et.set_nulary(t, it->second);
        }
    }
}

void
AtomAsgnEvaluator::set_user_value(const string &name, double val)
{
  int t = aa.atoms.index(name);
  if (t >= 0)
    {
      auto it = user_values.find(t);
      if (it == user_values.end())
        user_values.emplace(t, val);
      else
        it->second = val;
    }
}

void
AtomAsgnEvaluator::load(int i, double res)
{
  // set the value
  operator[](i) = res;
  // if i-th expression is atom, set its value to this EvalTree
  int t = aa.order[i];
  if (t >= 0)
    etree.set_nulary(t, res);
}

double
AtomAsgnEvaluator::get_value(const string &name) const
{
  auto it = aa.lname2expr.find(name);
  if (it == aa.lname2expr.end())
    return std::numeric_limits<double>::quiet_NaN();
  else
    return operator[](it->second);
}
