/*
 * Copyright (c) 2024, 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
 */

#ifndef MYSQLSHDK_SCRIPTING_POLYGLOT_LANGUAGES_POLYGLOT_LANGUAGE_H_
#define MYSQLSHDK_SCRIPTING_POLYGLOT_LANGUAGES_POLYGLOT_LANGUAGE_H_

#include <cassert>
#include <cstdint>
#include <list>
#include <memory>
#include <stack>
#include <string>
#include <vector>

#include "mysqlshdk/scripting/polyglot/utils/polyglot_api_clean.h"

#include "mysqlshdk/include/scripting/naming_style.h"
#include "mysqlshdk/include/scripting/types.h"
#include "mysqlshdk/scripting/polyglot/file_system/polyglot_file_system.h"
#include "mysqlshdk/scripting/polyglot/languages/polyglot_common_context.h"
#include "mysqlshdk/scripting/polyglot/native_wrappers/polyglot_collectable.h"
#include "mysqlshdk/scripting/polyglot/polyglot_wrappers/types_polyglot.h"
#include "mysqlshdk/scripting/polyglot/utils/graalvm_exceptions.h"
#include "mysqlshdk/scripting/polyglot/utils/polyglot_error.h"
#include "mysqlshdk/scripting/polyglot/utils/polyglot_store.h"

namespace shcore {
namespace polyglot {

class ICollectable;
class Polyglot_language;
/**
 * Allows creating a temporary global variable with a random name which will
 * only live until the execution context of the instance of this class ends.
 *
 * The reason this is needed is because in graal, non primitive values
 * originating from guest languages (like JS) are not sharable, this means:
 *
 * - A guest language specific object is not mappable to C++ equivalent, i.e.
 * (i.e. undefined, ArrayBuffer)
 * - There's no way we can generate a guest language specific object from C++
 * (i.e. undefined, ArrayBuffer or Exception Objects)
 *
 * The way this problem is addressed, is by creating a temporary object in the
 * global context, to create the object in the guest language and then if
 * needed, storing it to create a reference so the object can be deleted from
 * the main context (to avoid pollution) without actually losing the object,
 * then the reference is provided to the guest language.
 *
 * Additional Information:
 * https://github.com/oracle/graaljs/issues/70#issuecomment-436257058
 * https://github.com/oracle/graal/blob/master/sdk/CHANGELOG.md#version-10-rc9
 */
class Scoped_global final {
 public:
  Scoped_global() = delete;

  // Generates a random name for the scoped value, if value is provided, it will
  // be set as the global value, otherwise just a random name is created to be
  // used in the execute function.
  Scoped_global(const Polyglot_language *language, poly_value value = nullptr);

  Scoped_global(const Scoped_global &) = delete;
  Scoped_global &operator=(const Scoped_global &) = delete;

  Scoped_global(Scoped_global &&) = delete;
  Scoped_global &operator=(Scoped_global &&) = delete;

  // Executes some code referencing the scoped value, the string <<global>>
  // should be used in the text to refer to the created global (internally
  // <<global>> will be replaced with the generated value), returns the result
  // of the code execution
  poly_value execute(const std::string &code);

  ~Scoped_global();

 private:
  const Polyglot_language *m_language;
  std::string m_name;
};

/**
 * The polyglot library may support several languages (guest languages), and
 * provides out of the box C++ interfaces to handle common language elements,
 * such as Objects, Maps, Arrays, Functions and primitive data types.
 *
 * However, it does not provide out of the box C++ interface for guest language
 * non primitive data types or specific objects.
 *
 * This class is to be used as the base class to handle the common language
 * elements supported in the polyglot library, any guest language supported by
 * the shell is meant to extend this class and add handling of the elements that
 * are specific to it.
 */
class Polyglot_language
    : public std::enable_shared_from_this<Polyglot_language> {
 public:
  class Script_scope final {
   public:
    Script_scope() = delete;

    Script_scope(Polyglot_language *owner, const std::string &s)
        : m_owner(owner) {
      assert(m_owner);
      m_owner->m_current_script.push(s);
    }

    Script_scope(const Script_scope &) = delete;
    Script_scope(Script_scope &&) = default;

    Script_scope &operator=(const Script_scope &) = delete;
    Script_scope &operator=(Script_scope &&) = default;

    ~Script_scope() { m_owner->m_current_script.pop(); }

   private:
    Polyglot_language *m_owner = nullptr;
  };

  class Current_script {
   public:
    Current_script();

    Current_script(const Current_script &) = delete;
    Current_script &operator=(const Current_script &) = delete;

    Current_script(Current_script &&) = delete;
    Current_script &operator=(Current_script &&) = delete;

    void push(const std::string &script);

    void pop() { m_scripts.pop(); }

    std::string current_script() const { return m_scripts.top(); }

    std::string current_folder() const;

   private:
    std::string m_root;
    std::stack<std::string> m_scripts;
  };

  explicit Polyglot_language(Polyglot_common_context *common_context);

  Polyglot_language(const Polyglot_language &) = delete;
  Polyglot_language &operator=(const Polyglot_language &) = delete;

  Polyglot_language(Polyglot_language &&) = delete;
  Polyglot_language &operator=(Polyglot_language &&) = delete;

  virtual ~Polyglot_language() = default;

  virtual const char *get_language_id() const = 0;

  virtual NamingStyle naming_style() const = 0;

  virtual void init_context_builder();

  virtual void initialize(const std::shared_ptr<IFile_system> &fs = {});

  virtual void finalize();

  /**
   * The polyglot_utils file has the following functions that can be used to
   * export C++ functions as polyglot global functions:
   *
   * - polyglot_handler*
   * - native_handler*
   *
   * These wrappers allow exposing class functions as long as the class
   * containing them contains a language() function that returns a pointer to a
   * Polyglot language.
   *
   * This function, basically enables subclasses to expose functions as polyglot
   * globals.
   */
  Polyglot_language *language() { return this; }

  /**
   * Throws a specific exception type in the GraalVM environment, with a
   * specific message
   */
  void throw_graalvm_exception(const Graalvm_exception &exception);

  // These functions represent operations than have specific handling depending
  // on the guest language, for that reason only the interface is defined here
  // and the subclasses provide specific language handling.

  /**
   * Return the a guest language interpretation of the Undefined C++ value.
   */
  virtual poly_value undefined() const = 0;

  /**
   * Returns true if the provided value is undefined in the guest language.
   */
  virtual bool is_undefined(poly_value value) const = 0;

  /**
   * Return the guest language interpretation of a binary string.
   */
  virtual poly_value array_buffer(const std::string &data) const = 0;

  /**
   * Return true if the guest language identifies the value as an object
   */
  virtual bool is_object(poly_value object,
                         std::string *class_name = nullptr) const = 0;

  /**
   * Converts a C++ object into a representation of the object in the guest
   * language
   */
  virtual poly_value from_native_object(
      const Object_bridge_ref &object) const = 0;

  /**
   * Converts a guest language object into its C++ representation
   */
  virtual shcore::Value to_native_object(poly_value object,
                                         const std::string &class_name);

  /**
   * Translates a dictionary object into an exception in the guest language.
   */
  void throw_exception_object(const shcore::Dictionary_t &data) const;

  /**
   * Translates a Polyglot_error into an exception in the guest language.
   */
  void throw_exception_object(const Polyglot_error &data) const;

  /**
   * Retrieves the list of keywords in the guest language
   */
  virtual const std::vector<std::string> &keywords() const = 0;

  /**
   * Wraps a call to poly_context_eval
   */
  int64_t eval(const std::string &source, const std::string &code_str,
               poly_value *result) const;

  /**
   * Executes the given script
   */
  std::pair<Value, bool> execute(const std::string &code_str,
                                 const std::string &source);

  /**
   * This function should be implemented to provide plugin load support
   */
  virtual bool load_plugin([[maybe_unused]] const std::string &path) {
    throw std::runtime_error("Plugin support is not available.");
  }

  /**
   * Creates a copy of the global context
   */
  poly_context copy_global_context() const;

  poly_value poly_string(const std::string &data) const;
  std::string to_string(poly_value obj) const;

  poly_context context() const;
  poly_thread thread() const;
  const std::shared_ptr<Polyglot_object> &globals() const;

  poly_reference store(poly_value value);
  void erase(poly_reference value);

  void set_global(const std::string &name, const Value &value) const;
  void set_global(const std::string &name, poly_value value) const;
  void set_global_function(const std::string &name, poly_callback callback,
                           void *data = nullptr);
  Store get_global(const std::string &name) const;

  void terminate();
  bool is_terminating() const;
  void clear_is_terminating();

  Script_scope enter_script(const std::string &s);

  Value convert(poly_value value) const;
  poly_value convert(const Value &value) const;

  Argument_list convert_args(const std::vector<poly_value> &args) const;
  poly_value type_info(poly_value value) const;

  bool get_member(poly_value object, const char *name,
                  poly_value *member) const;

  std::string current_script_folder() const;

  Polyglot_common_context *common_context() { return m_common_context; }

 protected:
  poly_value wrap_callback(poly_callback callback, void *data) const;

  Polyglot_common_context *m_common_context;

  poly_thread m_thread = nullptr;
  poly_context_builder m_context_builder = nullptr;
  Store m_context;
  std::unique_ptr<Polyglot_type_bridger> m_types;
  std::shared_ptr<Polyglot_object> m_globals;
  Current_script m_current_script;
  std::unique_ptr<Polyglot_storage> m_storage;
  std::shared_ptr<IFile_system> m_file_system;

 private:
  std::unique_ptr<Polyglot_scope> m_scope;
  bool m_terminating = false;
  void set_file_system();
  void throw_exception_object(poly_value exception) const;
  static void output_callback(const char *bytes, size_t length, void *data);
  static void error_callback(const char *bytes, size_t length, void *data);

  virtual poly_value create_exception_object(
      const std::string &error, poly_value exception_object) const = 0;
  virtual void output_handler(const char *bytes, size_t length) = 0;
  virtual void error_handler(const char *bytes, size_t length) = 0;
};

}  // namespace polyglot
}  // namespace shcore

#endif  // MYSQLSHDK_SCRIPTING_POLYGLOT_LANGUAGES_POLYGLOT_LANGUAGE_H_
