/*
 * Copyright © 2019-2026 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 <algorithm>
#include <cassert>
#include <ranges>

#include "Environment.hh"
#include "Expressions.hh"

using namespace macro;

void
Environment::define(const VariablePtr& var, const ExpressionPtr& value)
{
  string name = var->getName();
  if (functions.contains(name))
    throw StackTrace("Variable " + name + " was previously defined as a function");
  variables[move(name)] = value->eval(*this);
}

void
Environment::define(FunctionPtr func, ExpressionPtr value)
{
  string name = func->getName();
  if (variables.contains(name))
    throw StackTrace("Variable " + name + " was previously defined as a variable");
  functions[name] = {move(func), move(value)};
}

ExpressionPtr
Environment::getVariable(const string& name) const
{
  if (auto it = variables.find(name); it != variables.end())
    return it->second;

  if (!parent)
    throw StackTrace("Unknown variable " + name);

  return getGlobalEnv()->getVariable(name);
}

pair<FunctionPtr, ExpressionPtr>
Environment::getFunction(const string& name) const
{
  if (auto it = functions.find(name); it != functions.end())
    return it->second;

  if (!parent)
    throw StackTrace("Unknown function " + name);

  return parent->getFunction(name);
}

codes::BaseType
Environment::getType(const string& name) const
{
  return getVariable(name)->eval(const_cast<Environment&>(*this))->getType();
}

bool
Environment::isVariableDefined(const string& name) const noexcept
{
  try
    {
      getVariable(name);
      return true;
    }
  catch (StackTrace& ex)
    {
      return false;
    }
}

bool
Environment::isFunctionDefined(const string& name) const noexcept
{
  try
    {
      getFunction(name);
      return true;
    }
  catch (StackTrace& ex)
    {
      return false;
    }
}

void
Environment::print(ostream& output, const vector<string>& vars, int line, bool save) const
{
  if (!save && !variables.empty())
    output << "Macro Variables (at line " << line << "):" << '\n';

  // For sorting the symbols in a case-insensitive way, see #128
  auto case_insensitive_string_less = [](const string& a, const string& b) {
    return ranges::lexicographical_compare(
        a, b, [](char c1, char c2) { return tolower(c1) < tolower(c2); });
  };

  if (vars.empty())
    {
      vector<string> variables_sorted;
      ranges::copy(views::keys(variables), back_inserter(variables_sorted));
      ranges::sort(variables_sorted, case_insensitive_string_less);
      for (const auto& it : variables_sorted)
        printVariable(output, it, line, save);
    }
  else
    for (const auto& it : vars)
      if (isVariableDefined(it))
        printVariable(output, it, line, save);

  if (!save && !functions.empty())
    output << "Macro Functions (at line " << line << "):" << '\n';

  if (vars.empty())
    {
      vector<string> functions_sorted;
      ranges::copy(views::keys(functions), back_inserter(functions_sorted));
      ranges::sort(functions_sorted, case_insensitive_string_less);
      for (const auto& it : functions_sorted)
        printFunction(output, it, line, save);
    }
  else
    for (const auto& it : vars)
      if (isFunctionDefined(it))
        printFunction(output, it, line, save);

  if (parent)
    parent->print(output, vars, line, save);
}

void
Environment::printVariable(ostream& output, const string& name, int line, bool save) const
{
  output << (save ? "options_.macrovars_line_" + to_string(line) + "." : "  ") << name << " = ";
  getVariable(name)->eval(const_cast<Environment&>(*this))->print(output, save);
  if (save)
    output << ";";
  output << '\n';
}

void
Environment::printFunction(ostream& output, const string& name, int line, bool save) const
{
  auto [func_signature, func_body] = getFunction(name);
  output << (save ? "options_.macrovars_line_" + to_string(line) + ".function." : "  ");
  if (save)
    {
      func_signature->printName(output);
      output << " = '";
    }

  func_signature->print(output);
  output << " = ";
  func_body->print(output);

  if (save)
    output << "';";
  output << '\n';
}
