/*
 * Copyright © 2005 Ondra Kamenik
 * Copyright © 2019-2022 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 "utils/cc/exception.hh"
#include "dynamic_atoms.hh"

using namespace ogp;

void
NameStorage::insert(string name)
{
  if (!query(name))
    {
      name_store.push_back(name);
      name_set.insert(std::move(name));
    }
}

void
NameStorage::print() const
{
  for (auto i : name_store)
    std::cout << i << '\n';
}

void
Constants::import_constants(const Constants &c, OperationTree &otree, Tintintmap &tmap)
{
  for (auto it : c.cmap)
    {
      int told = it.first;
      int tnew = otree.add_nulary();
      tmap.emplace(told, tnew);
      add_constant(tnew, it.second);
    }
}

void
Constants::setValues(EvalTree &et) const
{
  for (const auto &it : cmap)
    et.set_nulary(it.first, it.second);
}

void
Constants::add_constant(int t, double val)
{
  cmap.emplace(t, val);
  cinvmap.emplace(val, t);
}

bool
Constants::is_constant(int t) const
{
  if (t < OperationTree::num_constants)
    return true;
  auto it = cmap.find(t);
  return (it != cmap.end());
}

double
Constants::get_constant_value(int t) const
{
  auto it = cmap.find(t);
  if (it != cmap.end())
    return it->second;
  else
    throw ogu::Exception(__FILE__, __LINE__,
                         "Tree index is not constant in Constants::get_constant_value");
}

int
Constants::check(const string &str) const
{
  double d = std::stod(str);
  auto it = cinvmap.find(d);
  if (it != cinvmap.end())
    return it->second;
  else
    return -1;
}

void
Constants::print() const
{
  for (const auto &it : cmap)
    std::cout << "$" << it.first << ":  " << it.second << "\n";
}

int
DynamicAtoms::check(const string &name) const
{
  if (is_string_constant(name))
    return Constants::check(name);

  return check_variable(name);
}

int
DynamicAtoms::check_variable(const string &name) const
{
  string str;
  int ll;
  parse_variable(name, str, ll);
  auto it = vars.find(str);

  if (it != vars.end())
    {
      const Tlagmap &lmap = it->second;
      auto itt = lmap.find(ll);
      if (itt != lmap.end())
        return itt->second;
    }
  return -1;
}

void
DynamicAtoms::assign(const string &name, int t)
{
  if (is_string_constant(name))
    assign_constant(name, t);
  else
    assign_variable(name, t);
}

void
DynamicAtoms::assign_constant(const string &name, int t)
{
  double val = std::stod(name);
  add_constant(t, val);
}

// parse the name and then call assing_variable(varname, ll, t)

void
DynamicAtoms::assign_variable(const string &name, int t)
{
  int ll;
  string str;
  parse_variable(name, str, ll);
  // here str is just name without lead/lag
  varnames.insert(str);

  assign_variable(str, ll, t);
}

void
DynamicAtoms::assign_variable(const string &varname, int ll, int t)
{
  if (indices.end() != indices.find(t))
    throw ogu::Exception(__FILE__, __LINE__,
                         "Attempt to assign already allocated tree index");

  auto it = vars.find(varname);
  if (it != vars.end())
    {
      Tlagmap &lmap = it->second;
      if (lmap.end() != lmap.find(ll))
        throw ogu::Exception(__FILE__, __LINE__,
                             "Attempt to assign already allocated variable");
      lmap.emplace(ll, t);
    }
  else
    {
      Tlagmap lmap;
      lmap.emplace(ll, t);
      vars.emplace(varname, lmap);
    }
  indices.emplace(t, varname);

  nv++;
  minlag = std::min(ll, minlag);
  maxlead = std::max(ll, maxlead);
}

void
DynamicAtoms::unassign_variable(const string &varname, int ll, int t)
{
  auto it = vars.find(varname);
  if (it != vars.end())
    {
      Tlagmap &lmap = it->second;
      auto itt = lmap.find(ll);
      if (itt != lmap.end())
        {
          if (itt->second == t)
            {
              // erase it from the lagmap; if it becomes empty,
              // erase the lagmap from varmap
              lmap.erase(itt);
              if (lmap.size() == 0)
                vars.erase(it);
              // erase it from the indices
              auto ittt = indices.find(t);
              if (ittt != indices.end())
                indices.erase(ittt);

              nv--;
              if (ll == minlag || ll == maxlead)
                update_minmaxll();
            }
          else
            throw ogu::Exception(__FILE__, __LINE__,
                                 "Tree index inconsistent in DynamicAtoms::unassign_variable");
        }
      else
        throw ogu::Exception(__FILE__, __LINE__,
                             "Lead/lag of the variable not found in DynamicAtoms::unassign_variable");
    }
  else
    throw ogu::Exception(__FILE__, __LINE__,
                         "Variable not found in DynamicAtoms::unassign_variable");
}

void
DynamicAtoms::update_minmaxll()
{
  minlag = std::numeric_limits<int>::max();
  maxlead = std::numeric_limits<int>::min();
  for (const auto &it : vars)
    {
      const Tlagmap &lmap = it.second;
      for (auto itt : lmap)
        {
          int ll = itt.first;
          minlag = std::min(ll, minlag);
          maxlead = std::max(ll, maxlead);
        }
    }
}

vector<int>
DynamicAtoms::variables() const
{
  vector<int> res;
  for (const auto &var : vars)
    {
      const Tlagmap &lmap = var.second;
      for (auto itt : lmap)
        res.push_back(itt.second);
    }
  return res;
}

void
DynamicAtoms::varspan(int t, int &mlead, int &mlag) const
{
  auto it = indices.find(t);
  if (indices.end() == it)
    {
      mlead = std::numeric_limits<int>::min();
      mlag = std::numeric_limits<int>::max();
      return;
    }
  varspan(it->second, mlead, mlag);
}

void
DynamicAtoms::varspan(const string &name, int &mlead, int &mlag) const
{
  auto it = vars.find(name);
  if (vars.end() == it)
    {
      mlead = std::numeric_limits<int>::min();
      mlag = std::numeric_limits<int>::max();
      return;
    }
  const Tlagmap &lmap = it->second;
  auto beg = lmap.begin();
  auto end = lmap.rbegin();
  mlag = beg->first;
  mlead = end->first;
}

void
DynamicAtoms::varspan(const vector<string> &names, int &mlead, int &mlag) const
{
  mlead = std::numeric_limits<int>::min();
  mlag = std::numeric_limits<int>::max();
  for (const auto &name : names)
    {
      int lag, lead;
      varspan(name, lead, lag);
      mlead = std::max(lead, mlead);
      mlag = std::min(lag, mlag);
    }
}

bool
DynamicAtoms::is_named_atom(int t) const
{
  return indices.end() != indices.find(t);
}

int
DynamicAtoms::index(const string &name, int ll) const
{
  auto it = vars.find(name);
  if (vars.end() != it)
    {
      const Tlagmap &lmap = it->second;
      auto itt = lmap.find(ll);
      if (lmap.end() != itt)
        return itt->second;
    }
  return -1;
}

bool
DynamicAtoms::is_referenced(const string &name) const
{
  return vars.find(name) != vars.end();
}

const DynamicAtoms::Tlagmap &
DynamicAtoms::lagmap(const string &name) const
{
  auto it = vars.find(name);
  if (vars.end() == it)
    throw ogu::Exception(__FILE__, __LINE__,
                         "Couldn't find the name "
                         + name + " in DynamicAtoms::lagmap");
  return it->second;
}

const string &
DynamicAtoms::name(int t) const
{
  auto it = indices.find(t);
  if (indices.end() == it)
    throw ogu::Exception(__FILE__, __LINE__,
                         "Couldn't find tree index in DynamicAtoms::name");
  return it->second;
}

int
DynamicAtoms::lead(int t) const
{
  const string &nam = name(t);
  const Tlagmap &lmap = lagmap(nam);
  auto it = lmap.begin();
  while (it != lmap.end() && it->second != t)
    ++it;
  if (lmap.end() == it)
    throw ogu::Exception(__FILE__, __LINE__,
                         "Couldn't find the three index in DynamicAtoms::lead");
  return it->first;
}

void
DynamicAtoms::print() const
{
  std::cout << "names:\n";
  varnames.print();
  std::cout << "constants:\n";
  Constants::print();
  std::cout << "variables:\n";
  for (const auto &var : vars)
    {
      const Tlagmap &lmap = var.second;
      for (auto itt : lmap)
        std::cout << "$" << itt.second << ": " << var.first << "(" << itt.first << ")\n";
    }
  std::cout << "indices:\n";
  for (auto indice : indices)
    std::cout << "t=" << indice.first << " ⇒ " << indice.second << "\n";
}

/** Note that the str has been parsed by the lexicographic
 * analyzer. It can be either a variable or a double. So it is easy to
 * recognize it by the first character. */
bool
DynamicAtoms::is_string_constant(const string &str)
{
  return str[0] == '.' || str[0] == '-' || (str[0] >= '0' && str[0] <= '9');
}

VarOrdering::VarOrdering(const VarOrdering &vo, const vector<string> &vnames,
                         const DynamicAtoms &a)
  : n_stat(vo.n_stat), n_pred(vo.n_pred), n_both(vo.n_both), n_forw(vo.n_forw),
    der_atoms(vo.der_atoms), positions(vo.positions),
    outer2y(vo.outer2y), y2outer(vo.y2outer), varnames(vnames), atoms(a)
{
}

bool
VarOrdering::check(int t) const
{
  return positions.find(t) != positions.end();
}

int
VarOrdering::get_pos_of(int t) const
{
  auto it = positions.find(t);
  if (it != positions.end())
    return it->second;
  else
    throw ogu::Exception(__FILE__, __LINE__,
                         "Couldn't find the tree index in VarOrdering::get_pos_of");
}

void
VarOrdering::do_general(ord_type ordering)
{
  // auxiliary vectors for setting der_atoms and map
  vector<int> pred_minus;
  vector<int> both_minus;
  vector<int> stat;
  vector<int> pred_pad;
  vector<int> both_pad;
  vector<int> forw_pad;
  vector<int> both_plus;
  vector<int> forw_plus;

  // auxiliary vectors for setting y2outer and outer2y
  vector<int> y2o_stat;
  vector<int> y2o_pred;
  vector<int> y2o_both;
  vector<int> y2o_forw;

  for (unsigned int i = 0; i < varnames.size(); i++)
    {
      const string &ss = varnames[i];
      int lead;
      int lag;
      atoms.varspan(ss, lead, lag);
      if (lag == 0 && lead == 0)
        {
          stat.push_back(atoms.index(ss, 0));
          y2o_stat.push_back(i);
        }
      else if (lag == -1 && lead < 1)
        {
          pred_minus.push_back(atoms.index(ss, -1));
          pred_pad.push_back(atoms.index(ss, 0));
          y2o_pred.push_back(i);
        }
      else if (lag > -1 && lead == 1)
        {
          forw_pad.push_back(atoms.index(ss, 0));
          forw_plus.push_back(atoms.index(ss, 1));
          y2o_forw.push_back(i);
        }
      else if (lag == -1 && lead == 1)
        {
          both_minus.push_back(atoms.index(ss, -1));
          both_pad.push_back(atoms.index(ss, 0));
          both_plus.push_back(atoms.index(ss, 1));
          y2o_both.push_back(i);
        }
      else
        throw ogu::Exception(__FILE__, __LINE__,
                             "A wrong lag/lead of a variable in VarOrdering::do_pbspbfbf");
    }

  // here we fill ords according to ordering
  vector<int> *ords[8];
  if (ordering == pbspbfbf)
    {
      ords[0] = &pred_minus;
      ords[1] = &both_minus;
      ords[2] = &stat;
      ords[3] = &pred_pad;
      ords[4] = &both_pad;
      ords[5] = &forw_pad;
      ords[6] = &both_plus;
      ords[7] = &forw_plus;
    }
  else if (ordering == bfspbfpb)
    {
      ords[0] = &both_plus;
      ords[1] = &forw_plus;
      ords[2] = &stat;
      ords[3] = &pred_pad;
      ords[4] = &both_pad;
      ords[5] = &forw_pad;
      ords[6] = &pred_minus;
      ords[7] = &both_minus;
    }
  else // BEWARE: when implementing a new ordering, check also the code below setting y2outer
    throw ogu::Exception(__FILE__, __LINE__,
                         "Ordering not implemented in VarOrdering::do_general");

  // make der_atoms and positions
  int off = 0;
  for (auto &ord : ords)
    for (unsigned int j = 0; j < ord->size(); j++, off++)
      if ((*ord)[j] != -1)
        {
          der_atoms.push_back((*ord)[j]);
          positions.emplace((*ord)[j], off);
        }

  // set integer constants
  n_stat = stat.size();
  n_pred = pred_pad.size();
  n_both = both_pad.size();
  n_forw = forw_pad.size();

  // make y2outer mapping
  y2outer.insert(y2outer.end(), y2o_stat.begin(), y2o_stat.end());
  y2outer.insert(y2outer.end(), y2o_pred.begin(), y2o_pred.end());
  y2outer.insert(y2outer.end(), y2o_both.begin(), y2o_both.end());
  y2outer.insert(y2outer.end(), y2o_forw.begin(), y2o_forw.end());
  // make outer2y mapping
  outer2y.resize(y2outer.size(), -1);
  for (unsigned int i = 0; i < y2outer.size(); i++)
    outer2y[y2outer[i]] = i;
}

void
VarOrdering::do_increasing_time()
{
  // get maxlead and minlag of the variables
  int mlag, mlead;
  atoms.varspan(varnames, mlead, mlag);
  // setup the matrix of tree indices, if there is no occurrence,
  // the index is set to -1
  vector<int> ll_init(varnames.size(), -1);
  vector<vector<int>> tree_ind(mlead-mlag+1, ll_init);
  for (unsigned int iv = 0; iv < varnames.size(); iv++)
    {
      try
        {
          const DynamicAtoms::Tlagmap &lmap = atoms.lagmap(varnames[iv]);
          for (auto it : lmap)
            {
              int ll = it.first;
              int t = it.second;
              tree_ind[ll-mlag][iv] = t;
            }
        }
      catch (const ogu::Exception &e)
        {
          // ignore the error of not found variable in the tree
        }
    }

  // setup der_atoms and positions
  for (int ll = mlag; ll <= mlead; ll++)
    for (unsigned int iv = 0; iv < varnames.size(); iv++)
      {
        int t = tree_ind[ll-mlag][iv];
        if (t != -1)
          {
            der_atoms.push_back(t);
            int pos = (ll-mlag)*varnames.size() + iv;
            positions.emplace(t, pos);
          }
      }

  // set outer2y and y2outer to identities
  for (unsigned int iv = 0; iv < varnames.size(); iv++)
    {
      outer2y.push_back(iv);
      y2outer.push_back(iv);
    }

  // set n_stat, n_pred, n_both, and n_forw
  for (auto varname : varnames)
    {
      int mmlag, mmlead;
      atoms.varspan(varname, mmlead, mmlag);
      if (mmlead == 0 && mmlag == 0)
        n_stat++;
      else if (mmlead <= 0 && mmlag < 0)
        n_pred++;
      else if (mmlead > 0 && mmlag >= 0)
        n_forw++;
      else if (mmlead > 0 && mmlag < 0)
        n_both++;
      else if (mmlead < mmlag)
        // variable does not occur in the tree, cound as static
        n_stat++;
      else
        throw ogu::Exception(__FILE__, __LINE__,
                             "A wrong lag/lead of a variable in VarOrdering::do_increasing_time");
    }
}

void
VarOrdering::print() const
{
  std::cout << "nstat=" << n_stat << ", npred=" << n_pred << ", nboth=" << n_both
            << ", nforw=" << n_forw << "\n"
            << "der_atoms:\n";
  for (int der_atom : der_atoms)
    std::cout << " " << der_atom;
  std::cout << "\nmap:\n";
  for (auto position : positions)
    std::cout << " [" << position.first << "→" << position.second << "]";
  std::cout << "\ny2outer:\n";
  for (int i : y2outer)
    std::cout << " " <<  i;
  std::cout << "\nouter2y:\n";
  for (int i : outer2y)
    std::cout << " " << i;
  std::cout << "\n";
}
