/*
 * 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 "planner_builder.hh"
#include "dynare_exception.hh"
#include "dynare_model.hh"

#include <cmath>
#include <utility>

using namespace ogdyn;

const IntegerMatrix &
IntegerMatrix::operator=(const IntegerMatrix &im)
{
  if (nr != im.nr || nc != im.nc)
    throw DynareException(__FILE__, __LINE__,
                          "Matrices have different dimensions in IntegerMatrix::operator=");
  std::copy_n(im.data.get(), nr*nc, data.get());
  return *this;
}

const IntegerArray3 &
IntegerArray3::operator=(const IntegerArray3 &ia3)
{
  if (n1 != ia3.n1 || n2 != ia3.n2 || n3 != ia3.n3)
    throw DynareException(__FILE__, __LINE__,
                          "Arrays have different dimensions in IntegerArray3::operator=");
  std::copy_n(ia3.data.get(), n1*n2*n3, data.get());
  return *this;
}

PlannerBuilder::PlannerBuilder(DynareModel &m, const Tvarset &yyset,
                               Teqset ffset)
  : yset(), fset(std::move(ffset)), model(m),
    tb(model.t_plobjective), tbeta(model.t_pldiscount),
    maxlead(model.atoms.get_maxlead()),
    minlag(model.atoms.get_minlag()),
    diff_b(yyset.size(), 1-minlag),
    diff_f(yyset.size(), fset.size(), 1+maxlead-minlag),
    static_atoms(),
    static_tree(),
    diff_b_static(yyset.size(), 1-minlag),
    diff_f_static(yyset.size(), fset.size(), 1+maxlead-minlag)
{
  info.num_new_terms -= model.getParser().getTree().get_num_op();

  fill_yset(m.atoms.get_name_storage(), yyset);

  add_derivatives_of_b();
  add_derivatives_of_f();
  shift_derivatives_of_b();
  shift_derivatives_of_f();
  beta_multiply_b();
  beta_multiply_f();
  make_static_version();
  lagrange_mult_f();
  form_equations();

  info.num_new_terms += model.getParser().getTree().get_num_op();
}

PlannerBuilder::PlannerBuilder(const PlannerBuilder &pb, ogdyn::DynareModel &m)
  : yset(), fset(pb.fset), model(m),
    tb(pb.tb), tbeta(pb.tbeta),
    maxlead(pb.maxlead), minlag(pb.minlag),
    diff_b(pb.diff_b), diff_f(pb.diff_f),
    static_atoms(pb.static_atoms),
    static_tree(pb.static_tree),
    diff_b_static(pb.diff_b_static),
    diff_f_static(pb.diff_f_static),
    aux_map(), static_aux_map()
{
  fill_yset(m.atoms.get_name_storage(), pb.yset);
  fill_aux_map(m.atoms.get_name_storage(), pb.aux_map, pb.static_aux_map);
}

void
PlannerBuilder::add_derivatives_of_b()
{
  int yi = 0;
  for (auto yname = yset.begin(); yname != yset.end(); ++yname, yi++)
    for (int ll = minlag; ll <= 0; ll++)
      {
        int yt = model.atoms.index(*yname, ll);
        if (yt != -1)
          diff_b(yi, ll-minlag) = model.eqs.add_derivative(tb, yt);
        else
          diff_b(yi, ll-minlag) = ogp::OperationTree::zero;
      }
}

void
PlannerBuilder::add_derivatives_of_f()
{
  int yi = 0;
  for (auto yname = yset.begin(); yname != yset.end(); ++yname, yi++)
    for (unsigned int fi = 0; fi < fset.size(); fi++)
      for (int ll = minlag; ll <= maxlead; ll++)
        {
          int yt = model.atoms.index(*yname, ll);
          if (yt != -1)
            diff_f(yi, fi, ll-minlag)
              = model.eqs.add_derivative(model.eqs.formula(fset[fi]), yt);
          else
            diff_f(yi, fi, ll-minlag) = ogp::OperationTree::zero;
        }
}

void
PlannerBuilder::shift_derivatives_of_b()
{
  map<int, int> subst;
  for (int yi = 0; yi < diff_b.nrows(); yi++)
    for (int ll = minlag; ll < 0; ll++)
      if (diff_b(yi, ll-minlag) != ogp::OperationTree::zero)
        {
          model.variable_shift_map(model.eqs.nulary_of_term(diff_b(yi, ll-minlag)),
                                   -ll, subst);
          diff_b(yi, ll-minlag) = model.eqs.add_substitution(diff_b(yi, ll-minlag), subst);
        }
}

void
PlannerBuilder::shift_derivatives_of_f()
{
  map<int, int> subst;
  for (int yi = 0; yi < diff_f.dim1(); yi++)
    for (int fi = 0; fi < diff_f.dim2(); fi++)
      {
        // first do it leads which are put under expectation before t: no problem
        for (int ll = 0; ll <= maxlead; ll++)
          if (diff_f(yi, fi, ll-minlag) != ogp::OperationTree::zero)
            {
              model.variable_shift_map(model.eqs.nulary_of_term(diff_f(yi, fi, ll-minlag)),
                                       -ll, subst);
              diff_f(yi, fi, ll-minlag)
                = model.eqs.add_substitution(diff_f(yi, fi, ll-minlag), subst);
            }
        /* now do it for lags, these are put as leads under expectations after
           time t, so we have to introduce auxiliary variables at time t, and
           make leads of them here */
        for (int ll = minlag; ll < 0; ll++)
          {
            int ft = diff_f(yi, fi, ll-minlag);
            if (ft != ogp::OperationTree::zero)
              {
                /* if the ft term has a lead, than we need to introduce an
                   auxiliary variable zₜ, define it as 𝔼ₜ[ft] and put z_{t-ll}
                   to the equation. Otherwise, we just put leaded ft to the
                   equation directly. */
                int ft_maxlead, ft_minlag;
                model.termspan(ft, ft_maxlead, ft_minlag);
                if (ft_maxlead > 0)
                  {
                    // make an auxiliary variable
                    std::string name;
                    name = "AUX_" + std::to_string(yi) + '_' + std::to_string(fset[fi]) + '_' + std::to_string(-ll);
                    model.atoms.register_uniq_endo(name);
                    info.num_aux_variables++;
                    int taux = model.eqs.add_nulary(name);
                    name = "AUX_" + std::to_string(yi) + '_' + std::to_string(fset[fi]) + '_' + std::to_string(-ll) + '(' + std::to_string(-ll) + ')';
                    int taux_leaded = model.eqs.add_nulary(name);
                    // put aux_leaded to the equation
                    diff_f(yi, fi, ll-minlag) = taux_leaded;
                    // save auxiliary variable and the term
                    aux_map.emplace(model.atoms.name(taux), ft);
                  }
                else
                  {
                    /* no auxiliary variable is needed and the term ft can be
                       leaded in place */
                    model.variable_shift_map(model.eqs.nulary_of_term(ft), -ll, subst);
                    diff_f(yi, fi, ll-minlag) = model.eqs.add_substitution(ft, subst);
                  }
              }
          }
      }
}

void
PlannerBuilder::beta_multiply_b()
{
  int beta_pow = ogp::OperationTree::one;
  for (int ll = 0; ll >= minlag; ll--,
         beta_pow = model.eqs.add_binary(ogp::code_t::TIMES, beta_pow, tbeta))
    for (int yi = 0; yi < diff_b.nrows(); yi++)
      if (diff_b(yi, ll-minlag) != ogp::OperationTree::zero)
        diff_b(yi, ll-minlag)
          = model.eqs.add_binary(ogp::code_t::TIMES, beta_pow, diff_b(yi, ll-minlag));
}

void
PlannerBuilder::beta_multiply_f()
{
  int beta_pow = ogp::OperationTree::one;
  for (int ll = 0; ll <= maxlead; ll++,
         beta_pow = model.eqs.add_binary(ogp::code_t::DIVIDE, beta_pow, tbeta))
    for (int yi = 0; yi < diff_f.dim1(); yi++)
      for (int fi = 0; fi < diff_f.dim2(); fi++)
        if (diff_f(yi, fi, ll-minlag) != ogp::OperationTree::zero)
          diff_f(yi, fi, ll-minlag)
            = model.eqs.add_binary(ogp::code_t::TIMES, beta_pow, diff_f(yi, fi, ll-minlag));

  beta_pow = ogp::OperationTree::one;
  for (int ll = 0; ll >= minlag; ll--,
         beta_pow = model.eqs.add_binary(ogp::code_t::TIMES, beta_pow, tbeta))
    for (int yi = 0; yi < diff_f.dim1(); yi++)
      for (int fi = 0; fi < diff_f.dim2(); fi++)
        if (diff_f(yi, fi, ll-minlag) != ogp::OperationTree::zero)
          diff_f(yi, fi, ll-minlag)
            = model.eqs.add_binary(ogp::code_t::TIMES, beta_pow, diff_f(yi, fi, ll-minlag));
}

void
PlannerBuilder::make_static_version()
{
  // map holding substitutions from dynamic to static
  ogp::StaticFineAtoms::Tintintmap tmap;

  // fill static atoms with outer ordering
  static_atoms.import_atoms(model.atoms, static_tree, tmap);

  // go through diff_b and fill diff_b_static
  for (int ll = minlag; ll <= 0; ll++)
    for (int yi = 0; yi < diff_b.nrows(); yi++)
      diff_b_static(yi, ll-minlag)
        = static_tree.add_substitution(diff_b(yi, ll-minlag),
                                       tmap, model.eqs.getTree());

  // go through diff_f and fill diff_f_static
  for (int ll = minlag; ll <= maxlead; ll++)
    for (int yi = 0; yi < diff_f.dim1(); yi++)
      for (int fi = 0; fi < diff_f.dim2(); fi++)
        diff_f_static(yi, fi, ll-minlag)
          = static_tree.add_substitution(diff_f(yi, fi, ll-minlag),
                                         tmap, model.eqs.getTree());

  // go through aux_map and fill static_aux_map
  for (const auto &it : aux_map)
    {
      int tstatic = static_tree.add_substitution(it.second, tmap, model.eqs.getTree());
      static_aux_map.emplace(it.first, tstatic);
    }
}

void
PlannerBuilder::lagrange_mult_f()
{
  // register multipliers
  std::string mult_name;
  for (int fi = 0; fi < diff_f.dim2(); fi++)
    {
      mult_name = "MULT" + std::to_string(fset[fi]);
      model.atoms.register_uniq_endo(mult_name);
      info.num_lagrange_mults++;
    }
  // multiply with the multipliers
  for (int yi = 0; yi < diff_f.dim1(); yi++)
    for (int fi = 0; fi < diff_f.dim2(); fi++)
      for (int ll = minlag; ll <= maxlead; ll++)
        if (diff_f(yi, fi, ll-minlag) != ogp::OperationTree::zero)
          {
            mult_name = "MULT" + std::to_string(fset[fi]) + '(' + std::to_string(-ll) + ')';
            int tm = model.eqs.add_nulary(mult_name);
            diff_f(yi, fi, ll-minlag)
              = model.eqs.add_binary(ogp::code_t::TIMES, tm, diff_f(yi, fi, ll-minlag));
          }
}

void
PlannerBuilder::form_equations()
{
  // add planner’s FOCs
  for (int yi = 0; yi < diff_f.dim1(); yi++)
    {
      int eq = ogp::OperationTree::zero;
      for (int ll = minlag; ll <= 0; ll++)
        eq = model.eqs.add_binary(ogp::code_t::PLUS, eq, diff_b(yi, ll-minlag));
      for (int fi = 0; fi < diff_f.dim2(); fi++)
        for (int ll = minlag; ll <= maxlead; ll++)
          eq = model.eqs.add_binary(ogp::code_t::PLUS, eq, diff_f(yi, fi, ll-minlag));
      model.eqs.add_formula(eq);
    }

  // add equations for auxiliary variables
  for (const auto &it : aux_map)
    {
      int t = model.atoms.index(it.first, 0);
      model.eqs.add_formula(model.eqs.add_binary(ogp::code_t::MINUS, t, it.second));
    }
}

void
PlannerBuilder::fill_yset(const ogp::NameStorage &ns,
                          const PlannerBuilder::Tvarset &yyset)
{
  for (auto it : yyset)
    yset.insert(it);
}

void
PlannerBuilder::fill_aux_map(const ogp::NameStorage &ns, const Tsubstmap &aaux_map,
                             const Tsubstmap &astatic_aux_map)
{
  // fill aux_map
  for (auto it : aaux_map)
    aux_map.insert(it);

  // fill static_aux_map
  for (auto it : astatic_aux_map)
    static_aux_map.insert(it);
}

MultInitSS::MultInitSS(const PlannerBuilder &pb, const Vector &pvals, Vector &yy)
  : builder(pb), b(builder.diff_b_static.nrows()),
    F(builder.diff_f_static.dim1(), builder.diff_f_static.dim2())
{
  b.zeros();
  F.zeros();

  // first evaluate substitutions (auxiliary variables) from the builder
  ogdyn::DynareStaticSteadySubstitutions dss(builder.model.atoms, builder.static_atoms,
                                             builder.static_tree,
                                             builder.static_aux_map, pvals, yy);

  /* gather all the terms from builder.diff_b_static and builder.diff_f_static
     to the vector, the ordering is important, since the index of this vector
     will have to be decoded to the position in b and F. */
  vector<int> terms;
  for (int yi = 0; yi < builder.diff_b_static.nrows(); yi++)
    for (int l = 0; l < builder.diff_b_static.ncols(); l++)
      terms.push_back(builder.diff_b_static(yi, l));
  for (int yi = 0; yi < builder.diff_f_static.dim1(); yi++)
    for (int fi = 0; fi < builder.diff_f_static.dim2(); fi++)
      for (int l = 0; l < builder.diff_f_static.dim3(); l++)
        terms.push_back(builder.diff_f_static(yi, fi, l));

  /* evaluate the terms, it will call a series of load(i,res), which sum the
     results through lags/leads to b and F */
  DynareStaticSteadyAtomValues dssav(builder.model.atoms, builder.static_atoms, pvals, yy);
  ogp::FormulaCustomEvaluator fe(builder.static_tree, terms);
  fe.eval(dssav, *this);

  // solve overdetermined system b+F*lambda=0 using SVD decomposition
  SVDDecomp decomp(F);
  Vector lambda(builder.diff_f_static.dim2());
  decomp.solve(b, lambda);
  lambda.mult(-1);

  // take values of lambda and put it to yy
  for (int fi = 0; fi < builder.diff_f_static.dim2(); fi++)
    {
      std::string mult_name = "MULT" + std::to_string(builder.fset[fi]);
      int iouter = builder.model.atoms.name2outer_endo(mult_name);
      int iy = builder.model.atoms.outer2y_endo()[iouter];
      if (!std::isfinite(yy[iy]))
        yy[iy] = lambda[fi];

      /* go through all substitutions of the multiplier and set them as well */
      if (builder.model.atom_substs)
        {
          const ogp::AtomSubstitutions::Toldnamemap &old2new
            = builder.model.atom_substs->get_old2new();
          auto it = old2new.find(mult_name);
          if (it != old2new.end())
            {
              const ogp::AtomSubstitutions::Tshiftnameset &sset = it->second;
              for (const auto &itt : sset)
                {
                  const std::string &newname = itt.first;
                  int iouter = builder.model.atoms.name2outer_endo(newname);
                  int iy = builder.model.atoms.outer2y_endo()[iouter];
                  if (!std::isfinite(yy[iy]))
                    yy[iy] = lambda[fi];
                }
            }
        }
    }
}

void
MultInitSS::load(int i, double res)
{
  /* we can afford it, since the evaluator sets res to exact zero if the term
     is zero */
  if (res == 0)
    return;
  // decode i and add to either b or F
  if (i < builder.diff_b_static.nrows()*builder.diff_b_static.ncols())
    // add to b
    b[i / builder.diff_b_static.ncols()] += res;
  else
    {
      // add to F
      i -= builder.diff_b_static.nrows()*builder.diff_b_static.ncols();
      int yifi = i / builder.diff_f_static.dim3();
      int yi = yifi / builder.diff_f_static.dim2();
      int fi = yifi % builder.diff_f_static.dim2();
      F.get(yi, fi) += res;
    }
}
