

#include "toonz/scriptengine.h"
#include "toonz/scriptbinding.h"
#include "toonz/scriptbinding_files.h"
#include "trenderer.h"
#include "toonz/toonzfolders.h"
#include <QScriptEngine>
#include <QScriptProgram>
#include <QFile>
#include <QTextStream>
#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QtGlobal>
#include <QApplication>

//=========================================================

namespace {

void sleep(unsigned long msec) {
  QMutex mutex;
  mutex.lock();
  QWaitCondition waitCondition;
  waitCondition.wait(&mutex, msec);
  mutex.unlock();
}

QString print(QScriptValue arg, bool addQuotes) {
  if (arg.isArray()) {
    QString s   = "[";
    quint32 len = arg.property(QLatin1String("length")).toUInt32();
    for (quint32 i = 0; i < len; ++i) {
      QScriptValue item = arg.property(i);
      if (i > 0) s += ",";
      s += print(item, addQuotes);
    }
    s += "]";
    return s;
  } else if (arg.isBool()) {
    return arg.toBool() ? "true" : "false";
  } else if (arg.isString()) {
    if (addQuotes)
      return "\"" + arg.toString() + "\"";
    else
      return arg.toString();
  } else
    return arg.toString();
}

QScriptValue printFunction(QScriptContext *context, QScriptEngine *engine) {
  QString result;
  for (int i = 0; i < context->argumentCount(); ++i) {
    if (i > 0) result.append(" ");
    result.append(print(context->argument(i), false));
  }
  QScriptValue calleeData = context->callee().data();
  ScriptEngine *se = qobject_cast<ScriptEngine *>(calleeData.toQObject());
  se->emitOutput(ScriptEngine::SimpleText, result);
  sleep(50);
  return se->voidValue();
}

QScriptValue warningFunction(QScriptContext *context, QScriptEngine *engine) {
  QString result;
  for (int i = 0; i < context->argumentCount(); ++i) {
    if (i > 0) result.append(" ");
    result.append(print(context->argument(i), false));
  }
  QScriptValue calleeData = context->callee().data();
  ScriptEngine *se = qobject_cast<ScriptEngine *>(calleeData.toQObject());
  se->emitOutput(ScriptEngine::Warning, result);
  sleep(50);
  return se->voidValue();
}

QScriptValue runFunction(QScriptContext *context, QScriptEngine *engine) {
  if (context->argumentCount() != 1) {
    return context->throwError("expected one parameter");
  }
  TFilePath fp;
  QScriptValue err =
      TScriptBinding::checkFilePath(context, context->argument(0), fp);
  if (err.isError()) return err;
  if (!fp.isAbsolute()) {
    fp = ToonzFolder::getLibraryFolder() + "scripts" + fp;
  }

  QString fpStr = QString::fromStdWString(fp.getWideString());
  QFile file(fpStr);
  if (!file.open(QIODevice::ReadOnly)) {
    return context->throwError("can't read file " + fpStr);
  }
  QTextStream in(&file);
  QString content = in.readAll();
  file.close();

  QScriptProgram program(content, fpStr);

  QScriptContext *parent = context->parentContext();
  if (parent != 0) {
    context->setActivationObject(context->parentContext()->activationObject());
    context->setThisObject(context->parentContext()->thisObject());
  }

  QScriptValue ret = engine->evaluate(program);

  if (engine->hasUncaughtException()) {
    int line = engine->uncaughtExceptionLineNumber();
    return context->throwError(QString("%1, at line %2 of %3")
                                   .arg(ret.toString())
                                   .arg(line)
                                   .arg(fpStr));
  }

  return ret;
}

/*
  QScriptValue glub(QScriptContext *context, QScriptEngine *engine)
  {
    QScriptValue global = engine->globalObject();
    QScriptValue te = global.property("_engine");
    ScriptEngine *se = qscriptvalue_cast<ScriptEngine*>(te);
    se->postCommand(context->argument(0));
    return 0;
  }
  */

}  // namespace

//=========================================================

class ScriptEngine::Executor final : public QThread {
  ScriptEngine *m_engine;
  QString m_cmd;

public:
  Executor(ScriptEngine *engine, const QString &cmd)
      : m_engine(engine), m_cmd(cmd) {}

  void run() override {
    m_engine->m_engine->collectGarbage();
    QScriptValue result = m_engine->m_engine->evaluate(m_cmd);
    if (result.isError()) {
      m_engine->emitOutput(ScriptEngine::SyntaxError, result.toString());
    } else if (result.isUndefined()) {
      m_engine->emitOutput(ScriptEngine::UndefinedEvaluationResult,
                           "undefined");
    } else {
      if (qscriptvalue_cast<TScriptBinding::Void *>(result)) {
      } else {
        m_engine->emitOutput(ScriptEngine::EvaluationResult,
                             print(result, true));
      }
    }
  }
};

class ScriptEngine::MainThreadEvaluationData {
public:
  QMutex m_mutex;
  QWaitCondition m_cond;
  QScriptValue m_fun, m_args, m_result;
};

//=========================================================

inline void defineFunction(ScriptEngine *se, const QString &name,
                           QScriptEngine::FunctionSignature f) {
  QScriptEngine *engine = se->getQScriptEngine();
  QScriptValue fObj     = engine->newFunction(f);
  fObj.setData(engine->newQObject(se));
  engine->globalObject().setProperty(name, fObj);
}

//=========================================================

ScriptEngine::ScriptEngine() : m_executor(0), m_engine(new QScriptEngine()) {
  // I must call TRenderer::initialize(), because a script could cause a
  // rendering driven by a working thread
  TRenderer::initialize();

  m_mainThreadEvaluationData = new MainThreadEvaluationData();
  QScriptValue global        = m_engine->globalObject();
  QScriptValue ctor;

  QScriptEngine &engine = *m_engine;

  defineFunction(this, "print", printFunction);
  defineFunction(this, "warning", warningFunction);
  defineFunction(this, "run", runFunction);

  /*
QScriptValue print = engine.newFunction(printFunction);
print.setData(engine.newQObject(this));
engine.globalObject().setProperty("print", print);

QScriptValue print = engine.newFunction(printFunction);
print.setData(engine.newQObject(this));
engine.globalObject().setProperty("print", print);

QScriptValue run = engine.newFunction(runFunction);
run.setData(engine.newQObject(this));
engine.globalObject().setProperty("run", run);

*/
  // QScriptValue g = engine.newFunction(glub);
  // g.setData(engine.newQObject(this));
  // engine.globalObject().setProperty("glub", g);

  // engine.globalObject().setProperty("_engine", engine.newQObject(this));

  m_voidValue  = new QScriptValue();
  *m_voidValue = engine.newQObject(new TScriptBinding::Void(),
                                   QScriptEngine::AutoOwnership);

  engine.globalObject().setProperty("void", *m_voidValue);

  TScriptBinding::bindAll(engine);
  bool ret = connect(this, SIGNAL(mainThreadEvaluationPosted()), this,
                     SLOT(onMainThreadEvaluationPosted()));
  assert(ret);
}

ScriptEngine::~ScriptEngine() {
  delete m_mainThreadEvaluationData;
  delete m_voidValue;
}

const QScriptValue &ScriptEngine::evaluateOnMainThread(
    const QScriptValue &fun, const QScriptValue &arguments) {
  MainThreadEvaluationData *d = m_mainThreadEvaluationData;
  QMutexLocker locker(&d->m_mutex);
  d->m_fun    = fun;
  d->m_args   = arguments;
  d->m_result = QScriptValue();
  emit mainThreadEvaluationPosted();
  d->m_cond.wait(&d->m_mutex);
  return d->m_result;
}

void ScriptEngine::onMainThreadEvaluationPosted() {
  Q_ASSERT(qApp && qApp->thread() == QThread::currentThread());
  MainThreadEvaluationData *d = m_mainThreadEvaluationData;
  QMutexLocker locker(&d->m_mutex);
  d->m_result = d->m_fun.call(d->m_fun, d->m_args);
  d->m_cond.wakeOne();
}

void ScriptEngine::evaluate(const QString &cmd) {
  if (m_executor) return;
  m_executor = new Executor(this, cmd);
  connect(m_executor, SIGNAL(finished()), this, SLOT(onTerminated()));
  m_executor->start();
}

bool ScriptEngine::isEvaluating() const { return m_engine->isEvaluating(); }

void ScriptEngine::interrupt() { m_engine->abortEvaluation(); }

void ScriptEngine::onTerminated() {
  emit evaluationDone();
  delete m_executor;
  m_executor = 0;
}
