
/////////////////////// stdlib includes
#include <utility>

/////////////////////// Qt includes
#include <QObject>
#include <QJSEngine>


/////////////////////// pappsomspp includes
#include <pappsomspp/core/js_qml/pappsojsqml.h>
#include <pappsomspp/core/js_qml/jsclassregistrar.h>


/////////////////////// libXpertMass includes
#include "MsXpS/libXpertMassCore/jsclassregistrar.h"
#include "MsXpS/libXpertMassCore/XpertMassCoreJavaScript.hpp"


/////////////////////// libXpertMassGui includes
#include "MsXpS/libXpertMassGui/XpertMassGuiJavaScript.hpp"


/////////////////////// Local includes
#include "MsXpS/libXpertMassGui/JavaScriptingEnvironment.hpp"
#include "MsXpS/libXpertMassGui/JavaScriptingGuiUtils.hpp"
#include "MsXpS/libXpertMassGui/JavaScriptingWnd.hpp"

namespace MsXpS
{
namespace libXpertMassGui
{

JavaScriptingEnvironment::JavaScriptingEnvironment(QObject *parent)
  : QObject(parent),
    mp_jsEngine(new QJSEngine(this)),
    mp_javaScriptingWnd(dynamic_cast<JavaScriptingWnd *>(parent))
{
}

JavaScriptingEnvironment::~JavaScriptingEnvironment()
{
}

bool
JavaScriptingEnvironment::initializeJsEngine() const
{
  // qDebug() << "Initializing JS engine.";

  Q_ASSERT(mp_jsEngine != nullptr);

  // In very lengthy operations, like in loop with large iteration counts,
  // the user may try to abort the script that is being run. For this there
  // are a number of things to put in place properly.

  // 1. The user puts checkAbort() calls in their own code where they see fit
  //    and take proper decisions on the basis of the return value, like
  //    calling break; if the return value is true.

  // 2. The code below registers a property that is indeed that checkAbort()
  //    function. The initial value of __abortRequested property is false,
  //    logically.

  // 3. When the user clicks on the cancel button, the value of __abortRequested
  //    is set to true, which means that calls to checkAbort() will return true.

  // Create and expose the abort check function
  // This version is for debugging.
  // QJSValue abortFunction = mp_jsEngine->evaluate(
  //   "(function() { this.print(\"Checking abort\") ; this.processEvents() ;
  //   print(\"Returning \" + this.__abortRequested) ; return
  //   this.__abortRequested; })");
  // mp_jsEngine->globalObject().setProperty("checkAbort", abortFunction);

  // Create and expose the abort check function
  // This function first calls processEvents() that is exposed to QJSEngine
  // from JavaScriptingGuiUtils::processEvents().
  QJSValue abortFunction = mp_jsEngine->evaluate(
    "(function() { this.processEvents() ; return this.__abortRequested; })");
  mp_jsEngine->globalObject().setProperty("checkAbort", abortFunction);

  // Initialize the abort flag
  mp_jsEngine->globalObject().setProperty("__abortRequested", QJSValue(false));

  registerJsConstructorForEachClassInRegistrarMap(mp_jsEngine);
  // Register the libXpertMass and libXpertMassGui entities.
  registerOwnJsEntities(mp_jsEngine);
  // Register the libpappsomspp pappso.Enums and maybe other things.
  registerExternalJsEntities("pappso", mp_jsEngine);

  return true;
}

QJSValue
JavaScriptingEnvironment::evaluate(const QString &script)
{
  if(!mp_jsEngine)
    return QJSValue();
  return evaluate(mp_jsEngine, script);
}

QJSValue
JavaScriptingEnvironment::evaluate(QJSEngine *engine_p, const QString &script)
{
  Q_ASSERT(engine_p != nullptr);

  return engine_p->evaluate(script);
}

QJSEngine *
JavaScriptingEnvironment::getJsEngine() const
{
  return mp_jsEngine;
}

bool
JavaScriptingEnvironment::exposeQObject(QJSEngine *js_engine_p,
                                        const QString &name,
                                        const QString &alias,
                                        const QString &description,
                                        QObject *object_p,
                                        QObject *object_parent_p,
                                        QJSValue &returned_js_value,
                                        QJSEngine::ObjectOwnership ownership)
{
  QString error_string;

  if(name.isEmpty())
    {
      error_string = "Object name cannot be empty";
      emit errorOccurredSignal(error_string);
      qDebug() << error_string;
      return false;
    }

  if(object_p == nullptr)
    {
      error_string = "Cannot expose null object";
      emit errorOccurredSignal(error_string);
      qDebug() << error_string;
      return false;
    }

  // Check thread affinity
  if(object_p->thread() != this->thread())
    {
      error_string =
        QString("Object belongs to different thread (object: %1, current: %2)")
          .arg(object_p->thread() ? QString::number(reinterpret_cast<quintptr>(
                                      object_p->thread()))
                                  : "null",
               this->thread()
                 ? QString::number(reinterpret_cast<quintptr>(this->thread()))
                 : "null");

      emit errorOccurredSignal(error_string);
      qDebug() << error_string;
      return false;
    }

  // Should we consider the passed js engine or the member datum ?
  QJSEngine *selected_js_engine_p = js_engine_p;

  if(selected_js_engine_p == nullptr)
    selected_js_engine_p = mp_jsEngine;

  if(selected_js_engine_p != nullptr)
    {
      returned_js_value = selected_js_engine_p->newQObject(object_p);

      // We want JavaScriptOwnership because the object needs to exist as long
      // as the QJSEngine is alive !
      Q_UNUSED(ownership)
      QJSEngine::setObjectOwnership(object_p, QJSEngine::JavaScriptOwnership);

      selected_js_engine_p->globalObject().setProperty(name, returned_js_value);

      if(!alias.isEmpty())
        selected_js_engine_p->globalObject().setProperty(alias,
                                                         returned_js_value);
    }
  else
    {
      error_string = "No usable JavaScript engine available";
      emit errorOccurredSignal(error_string);
      qDebug() << error_string;
      return false;
    }

  // At this point, we can register the object to the exposed objects registry.
  // The registry registers the object_p and sets a connection to
  // QObject::destroyed() so that the object is automatically deregistered upon
  // its destruction.

  if(m_registry.registerQObject(
       object_p, object_parent_p, name, alias, description) == -1)
    return false;

  mp_javaScriptingWnd->feedBackForExposedQObject(name, alias, description);

  return true;
}

bool
JavaScriptingEnvironment::exposeQObject(const QString &name,
                                        const QString &alias,
                                        const QString &description,
                                        QObject *object_p,
                                        QObject *object_parent_p,
                                        QJSValue &returned_js_value,
                                        QJSEngine::ObjectOwnership ownership)
{
  return exposeQObject(mp_jsEngine,
                       name,
                       alias,
                       description,
                       object_p,
                       object_parent_p,
                       returned_js_value,
                       ownership);
}

bool
JavaScriptingEnvironment::exposeJsValueToJsEngine(const QString &name,
                                                  QJSValue &js_value) const
{
  if(mp_jsEngine == nullptr)
    {
      QString error_string = "No JavaScript engine available";
      emit const_cast<JavaScriptingEnvironment *>(this)->errorOccurredSignal(
        error_string);
      qDebug() << error_string;
      return false;
    }
  else
    mp_jsEngine->globalObject().setProperty(name, js_value);

  return true;
}

QJSValue
JavaScriptingEnvironment::getExposedJsEngineObjectByName(
  const QString &name) const
{
  if(mp_jsEngine == nullptr)
    {
      QString error_string = "No JavaScript engine available";
      emit const_cast<JavaScriptingEnvironment *>(this)->errorOccurredSignal(
        error_string);
      qDebug() << error_string;
      return false;
    }

  return mp_jsEngine->globalObject().property(name);
}

void
JavaScriptingEnvironment::registerJsConstructorForClassInRegistrarMap(
  const QString &name_space,
  const QString &class_name,
  QJSEngine *js_engine_p) const
{
  QJSEngine *selected_js_engine_p = js_engine_p;

  if(selected_js_engine_p == nullptr)
    selected_js_engine_p = mp_jsEngine;

  if(selected_js_engine_p == nullptr)
    qFatal() << "No JavaScript engine is currently available.";

  // qDebug() << "Registering JavaScript constructor for" << name_space
  //          << "::" << class_name;

  MsXpS::registerJsConstructorForNameSpaceClassNameInRegistrarMap(
    name_space, class_name, mp_jsEngine);
}

void
JavaScriptingEnvironment::registerJsConstructorForClassesInRegistrarMap(
  const std::vector<std::pair<QString, QString>> &namespace_class_pairs,
  QJSEngine *js_engine_p) const
{
  QJSEngine *selected_js_engine_p = js_engine_p;

  if(selected_js_engine_p == nullptr)
    selected_js_engine_p = mp_jsEngine;

  if(selected_js_engine_p == nullptr)
    qFatal() << "No JavaScript engine is currently available.";

  for(const std::pair<QString, QString> &pair : namespace_class_pairs)
    registerJsConstructorForClassInRegistrarMap(pair.first, pair.second);

  // qDebug() << "At this point, the registry contains"
  //          << MsXpS::getNameSpaceClassNameJsConstructorRegistrarMap().size()
  //          << "pairs";
}

void
JavaScriptingEnvironment::registerJsConstructorForEachClassInRegistrarMap(
  QJSEngine *js_engine_p) const
{
  QJSEngine *selected_js_engine_p = js_engine_p;

  if(selected_js_engine_p == nullptr)
    selected_js_engine_p = mp_jsEngine;

  if(selected_js_engine_p == nullptr)
    qFatal() << "No JavaScript engine is currently available.";

  // qDebug() << "Registering the constructor of the classes marked for JS.";

  // All the classes deemed to be exposed to QJSEngine have a
  // REGISTER_JS_CLASS(namespace, classname)
  // in the MsXpS namespace right after the corresponding class
  // declaration(header file).
  //
  // For example:
  //
  // } // namespace libXpertMassGui
  // MSXPS_REGISTER_JS_CLASS(MsXpS::libXpertMassGui, DecimalPlacesOptionsDlg)
  // } // namespace MsXpS

  MsXpS::registerJsConstructorForEachClassInRegistrarMap(selected_js_engine_p);

  // qDebug() << "Exiting function.";
}

void
JavaScriptingEnvironment::registerOwnJsEntities(QJSEngine *js_engine_p) const
{
  QJSEngine *selected_js_engine_p = js_engine_p;

  if(selected_js_engine_p == nullptr)
    selected_js_engine_p = mp_jsEngine;

  if(selected_js_engine_p == nullptr)
    qFatal() << "No JavaScript engine is currently available.";

  // qDebug() << "Registering MsXpS::libXpertMassCore entities.";
  MsXpS::libXpertMassCore::registerEnumsToQJSEngine(selected_js_engine_p);
  MsXpS::libXpertMassCore::registerGlobalsToQJSEngine(selected_js_engine_p);
  // qDebug() << "Done registering MsXpS::libXpertMassCore entities.";

  // qDebug() << "Registering MsXpS::libXpertMassGui entities.";
  MsXpS::libXpertMassGui::registerEnumsToQJSEngine(selected_js_engine_p);
  MsXpS::libXpertMassGui::registerGlobalsToQJSEngine(selected_js_engine_p);
  // qDebug() << "Done registering MsXpS::libXpertMassGui entities.";

  // qDebug()
  //   << "Now returning from
  //   JavaScriptingEnvironment::registerOwnJsEntities().";
}

void
JavaScriptingEnvironment::registerExternalJsEntities(
  const QString &name_space, QJSEngine *js_engine_p) const
{
  // qDebug() << "Now registering external JS entities for \"" << name_space
  //          << "\"";

  QJSEngine *selected_js_engine_p = js_engine_p;

  if(selected_js_engine_p == nullptr)
    selected_js_engine_p = mp_jsEngine;

  if(selected_js_engine_p == nullptr)
    qFatal() << "No JavaScript engine is currently available.";

  if(name_space == "pappso")
    {
      // qDebug() << "Registering pappso.Enums entities.";
      pappso::registerEnumsToQJSEngine(selected_js_engine_p);
      // qDebug() << "Done registering pappso:: entities.";
    }

  // qDebug() << "Registering pappso class entities.";
  pappso::registerJsConstructorForEachClassInRegistrarMap(selected_js_engine_p);
  // qDebug() << "Done registering pappso class entities.";

  // qDebug() << "Now returning from "
  //             "JavaScriptingEnvironment::registerExternalJsEntities().";
}

} // namespace libXpertMassGui
} // namespace MsXpS
