/*
 * Copyright (c) 2024, 2025, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms,
 * as designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 *
 * This program 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, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "mysqlshdk/scripting/polyglot/polyglot_context.h"

#include <cerrno>
#include <list>
#include <stack>

#include "my_config.h"
#include "mysqlshdk/include/scripting/module_registry.h"
#include "mysqlshdk/include/scripting/obj_date.h"
#include "mysqlshdk/include/scripting/object_factory.h"
#include "mysqlshdk/include/scripting/object_registry.h"
#include "mysqlshdk/include/shellcore/base_shell.h"  // FIXME
#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/include/shellcore/scoped_contexts.h"
#include "mysqlshdk/libs/utils/logger.h"
#include "mysqlshdk/libs/utils/utils_file.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_path.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "mysqlshdk/scripting/polyglot/languages/polyglot_language.h"
#include "mysqlshdk/scripting/polyglot/native_wrappers/polyglot_collectable.h"
#include "mysqlshdk/scripting/polyglot/native_wrappers/polyglot_object_wrapper.h"
#include "mysqlshdk/scripting/polyglot/polyglot_type_conversion.h"
#include "mysqlshdk/scripting/polyglot/polyglot_wrappers/types_polyglot.h"
#include "mysqlshdk/scripting/polyglot/shell_javascript.h"
#include "mysqlshdk/scripting/polyglot/utils/polyglot_error.h"
#include "mysqlshdk/scripting/polyglot/utils/polyglot_utils.h"
#include "scripting/jscript_core_definitions.h"

#ifdef HAVE_JS
#include "mysqlshdk/scripting/polyglot/languages/polyglot_javascript.h"
#endif

namespace shcore {
namespace polyglot {

Polyglot_context::Polyglot_context(Object_registry *registry, Language type) {
  m_common_context.initialize({});
  m_language = get_language(type);
  assert(m_language);
  m_language->initialize();
  set_global("globals", Value(registry->_registry));
}

std::shared_ptr<Polyglot_language> Polyglot_context::get_language(
    Language type, const std::string &debug_port, bool wait_attached) {
#ifdef HAVE_JS
  if (type == Language::JAVASCRIPT) {
    return std::make_shared<Shell_javascript>(&m_common_context, debug_port,
                                              wait_attached);
  }
#endif
  return {};
}

poly_context Polyglot_context::context() const { return m_language->context(); }

poly_thread Polyglot_context::thread() const { return m_language->thread(); }

Polyglot_context::~Polyglot_context() {
  set_global("globals", Value{});
  m_language->finalize();
  m_common_context.finalize();
}

void Polyglot_context::set_global(const std::string &name, const Value &value) {
  m_language->set_global(name, convert(value));
}

Value Polyglot_context::get_global(const std::string &name) {
  return convert(m_language->get_global(name).get());
}

std::tuple<polyglot::Store, std::string> Polyglot_context::get_global_polyglot(
    const std::string &name) {
  auto global{m_language->get_global(name)};

  if (global.get() == nullptr) {
    return {};
  }

  if (m_language->is_object(global.get())) {
    auto obj_type = m_language->to_string(type_info(global.get()));

    if (shcore::str_beginswith(obj_type, "m.")) {
      obj_type = obj_type.substr(2);
    }

    return std::make_tuple(std::move(global), std::move(obj_type));
  } else if (is_native_type(thread(), global.get(), Collectable_type::OBJECT)) {
    auto obj_type = m_language->to_string(type_info(global.get()));
    if (shcore::str_beginswith(obj_type, "m.")) {
      obj_type = obj_type.substr(2);
    }

    return std::make_tuple(std::move(global), std::move(obj_type));
  } else {
    return {};
  }
}

std::vector<std::pair<bool, std::string>> Polyglot_context::list_globals() {
  auto globals = m_language->globals();
  auto names = globals->get_members();

  std::vector<std::pair<bool, std::string>> ret;
  for (auto &name : names) {
    auto member = get_member(m_language->thread(), globals->get(), name);
    ret.emplace_back(is_executable(m_language->thread(), member),
                     std::move(name));
  }

  // In graal, the print function for being built in is not returned as member
  // of the globals object, so it is injected here
  ret.emplace_back(true, "print");

  return ret;
}

const std::vector<std::string> &Polyglot_context::keywords() const {
  return m_language->keywords();
}

std::tuple<bool, polyglot::Store, std::string> Polyglot_context::get_member_of(
    const poly_value obj, const std::string &name) {
  auto member = get_member(thread(), obj, name);
  if (member) {
    auto type = to_string(thread(), type_info(member));
    if (shcore::str_beginswith(type, "m.")) {
      type = type.substr(2);
    }

    return {is_executable(thread(), member), Store(thread(), member),
            std::move(type)};
  }

  return {false, Store(nullptr), ""};
}

std::vector<std::pair<bool, std::string>> Polyglot_context::get_members_of(
    const poly_value obj) {
  auto props = get_member_keys(thread(), context(), obj);
  std::vector<std::pair<bool, std::string>> keys;

  for (auto &prop : props) {
    auto member = get_member(thread(), obj, prop);
    keys.emplace_back(is_executable(thread(), member), std::move(prop));
  }

  return keys;
}

Value Polyglot_context::convert(poly_value value) const {
  return m_language->convert(value);
}

poly_value Polyglot_context::convert(const Value &value) const {
  return m_language->convert(value);
}

poly_value Polyglot_context::type_info(poly_value value) const {
  return m_language->type_info(value);
}

void Polyglot_context::set_argv(const std::vector<std::string> &argv) {
  shcore::Value args_value = get_global("sys").as_object()->get_member("argv");
  auto args = args_value.as_array();
  args->clear();
  for (auto &arg : argv) {
    args->push_back(Value(arg));
  }
}

std::pair<Value, bool> Polyglot_context::debug(Object_registry * /*registry*/,
                                               Language type,
                                               const std::string &path) {
  const auto &options = mysqlsh::current_shell_options()->get();
  auto language = get_language(type, options.js_options.debug_port,
                               options.js_options.wait_attached);
  assert(language);

  try {
    language->initialize();

    m_language->copy_global_context(language.get());
    // set_global("globals", Value(registry->_registry));

    return language->debug(path);
  } catch (const Polyglot_error &error) {
    throw Exception::scripting_error(error.format(false));
  } catch (const Polyglot_generic_error &error) {
    throw Exception::scripting_error(error.what());
  }
}

std::pair<Value, bool> Polyglot_context::execute(const std::string &code_str,
                                                 const std::string &source) {
  shcore::Scoped_naming_style style(m_language->naming_style());
  try {
    return m_language->execute(code_str, source);
  } catch (Polyglot_error &error) {
    if (m_language->is_terminating() &&
        (error.is_interrupted() ||
         shcore::str_beginswith(error.message(), "Interrupted"))) {
      mysqlsh::current_console()->print_diag(
          "Script execution interrupted by user.");
    } else {
      throw Exception::scripting_error(error.format(false));
    }
  }

  return {Value(), true};
}

std::pair<Value, bool> Polyglot_context::execute_interactive(
    const std::string &code_str, Input_state *r_state, bool flush) noexcept {
  shcore::Scoped_naming_style style(m_language->naming_style());

  m_language->clear_is_terminating();

  *r_state = Input_state::Ok;

  // automatically release unreferenced local variables
  Polyglot_scope scope{thread()};

  poly_value result;
  auto rc = m_language->eval(k_origin_shell, code_str.c_str(), &result);

  if (rc == poly_ok) {
    return {convert(result), false};
  }

  auto exception = Polyglot_error(thread(), rc);

  const auto console = mysqlsh::current_console();

  if (m_language->is_terminating() &&
      (exception.is_interrupted() ||
       shcore::str_beginswith(exception.message(), "Interrupted"))) {
    console->print_diag("Script execution interrupted by user.");
  } else {
    // If there's no more input then the interpreter should not
    // continue in continued mode so any error should bubble up
    if (!flush) {
      // check if this was an error of type
      // - SyntaxError: "Expected } but found eof"
      // - SyntaxError: "Missing close quote"
      // which we treat as a multiline mode trigger or
      // - SyntaxError: Expected an operand but found error
      // which may be a sign of unfinished C style comment
      constexpr std::string_view k_unexpected_eof = "but found eof";
      constexpr std::string_view k_expected_binding =
          "expected BindingIdentifier or BindingPattern";
      constexpr std::string_view k_missing_closing_quote =
          "Missing close quote";
      constexpr std::string_view k_expected_operand =
          "Expected an operand but found error";

      if (exception.is_syntax_error()) {
        auto message = exception.message();
        if (message.find(k_unexpected_eof) != std::string::npos ||
            message.find(k_expected_binding) != std::string::npos ||
            // For backticks the column is reported as 0 all the time,
            // wihle for the double and single quote it is always the
            // quote location + 1
            (message == k_missing_closing_quote &&
             exception.column().has_value() && *exception.column() == 0)) {
          *r_state = Input_state::ContinuedBlock;
        } else if (message == k_expected_operand) {
          auto comment_pos = code_str.rfind("/*");

          while (comment_pos != std::string::npos &&
                 code_str.find("*/", comment_pos + 2) == std::string::npos) {
            rc = poly_context_parse(
                thread(), context(), m_language->get_language_id(),
                k_origin_shell, code_str.substr(0, comment_pos).c_str(),
                &result);

            if (poly_ok == rc) {
              *r_state = Input_state::ContinuedSingle;
            } else {
              exception = Polyglot_error(thread(), rc);
              message = exception.message();
              if (message.find(k_unexpected_eof) != std::string::npos ||
                  message.find(k_expected_binding) != std::string::npos) {
                *r_state = Input_state::ContinuedBlock;
              } else if (message == k_expected_operand) {
                comment_pos = code_str.rfind("/*", comment_pos - 1);
                continue;
              }
            }
            break;
          }
        }
      }
    }

    if (*r_state == Input_state::Ok) {
      console->print_diag(exception.format(true));
    }
  }

  return {Value(), true};
}

void Polyglot_context::terminate() { m_language->terminate(); }

bool Polyglot_context::load_plugin(const Plugin_definition &plugin) {
  return m_language->load_plugin(plugin.file);
}

}  // namespace polyglot
}  // namespace shcore
