// SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
// SPDX-FileCopyrightText: Copyright (c) Sandia Corporation
// SPDX-License-Identifier: BSD-3-Clause

/*
   Use the "documented" trick involving checking for _DEBUG
   and undefined that symbol while we include Python headers.
   Update: this method does not fool Microsoft Visual C++ 8 anymore; two
   of its header files (crtdefs.h and use_ansi.h) check if _DEBUG was set
   or not, and set flags accordingly (_CRT_MANIFEST_RETAIL,
   _CRT_MANIFEST_DEBUG, _CRT_MANIFEST_INCONSISTENT). The next time the
   check is performed in the same compilation unit, and the flags are found,
   and error is triggered. Let's prevent that by setting _CRT_NOFORCE_MANIFEST.
*/

#ifdef _DEBUG
#undef _DEBUG
#if defined(_MSC_VER) && _MSC_VER >= 1400
#define _CRT_NOFORCE_MANIFEST 1
#endif
#include <Python.h>
#define _DEBUG
#else
#include <Python.h>
#endif

#ifndef PyMODINIT_FUNC
#define PyMODINIT_FUNC extern "C" void
#endif // PyMODINIT_FUNC

// self include
#include "pqPythonEventSource.h"

// system includes
#include <signal.h>

// Qt include
#include <QApplication>
#include <QCoreApplication>
#include <QEvent>
#include <QFile>
#include <QMetaObject>
#include <QMetaProperty>
#include <QStringList>
#include <QThread>
#include <QVariant>
#include <QtDebug>

// Qt testing includes
#include "pqEventDispatcher.h"
#include "pqObjectNaming.h"
#include "pqWidgetEventPlayer.h"

// TODO not have a global instance pointer?
static pqPythonEventSource* Instance = NULL;
static QString PropertyObject;
static QString PropertyResult;
static QString PropertyValue;
static QStringList ObjectList;

static PyObject* QtTesting_playCommand(PyObject* /*self*/, PyObject* args)
{
  // this gives the main thread some time to refresh before we start sending
  // commands. Avoids crazy hangs in QDialog::exec() calls.
  pqThreadedEventSource::msleep(500);

  // void QtTesting.playCommand('object', 'command', 'arguments')
  //   an exception is thrown in this fails

  const char* object = 0;
  const char* command = 0;
  const char* arguments = 0;

  if (!PyArg_ParseTuple(args, const_cast<char*>("sss"), &object, &command, &arguments))
  {
    PyErr_SetString(PyExc_TypeError, "bad arguments to playCommand()");
    return NULL;
  }

  if (Instance)
  {
    if (!Instance->postNextEvent(object, command, arguments))
    {
      PyErr_SetString(PyExc_AssertionError, "error processing event");
      return NULL;
    }
  }
  else
  {
    PyErr_SetString(PyExc_AssertionError, "pqPythonEventSource not defined");
    return NULL;
  }

  return Py_BuildValue(const_cast<char*>(""));
}

static PyObject* QtTesting_getProperty(PyObject* /*self*/, PyObject* args)
{
  // string QtTesting.getProperty('object', 'property')
  //    returns the string value of the property

  const char* object = 0;
  const char* property = 0;

  if (!PyArg_ParseTuple(args, const_cast<char*>("ss"), &object, &property))
  {
    return NULL;
  }

  PropertyObject = object;
  PropertyResult = property;
  PropertyValue = QString();

  if (Instance && QThread::currentThread() != QApplication::instance()->thread())
  {
    QMetaObject::invokeMethod(Instance, "threadGetProperty", Qt::QueuedConnection);
    if (!Instance->waitForGUI())
    {
      PyErr_SetString(PyExc_ValueError, "error getting property");
      return NULL;
    }
  }
  else if (QThread::currentThread() == QApplication::instance()->thread())
  {
    PropertyValue = pqPythonEventSource::getProperty(PropertyObject, PropertyResult);
  }
  else
  {
    PyErr_SetString(PyExc_AssertionError, "pqPythonEventSource not defined");
    return NULL;
  }

  if (PropertyObject.isEmpty())
  {
    PyErr_SetString(PyExc_ValueError, "object not found");
    return NULL;
  }

  if (PropertyResult.isEmpty())
  {
    PyErr_SetString(PyExc_ValueError, "property not found");
    return NULL;
  }

  return Py_BuildValue(const_cast<char*>("s"), PropertyValue.toUtf8().data());
}

static PyObject* QtTesting_setProperty(PyObject* /*self*/, PyObject* args)
{
  // string QtTesting.setProperty('object', 'property', 'value')
  //    returns the string value of the property

  const char* object = 0;
  const char* property = 0;
  const char* value = 0;

  if (!PyArg_ParseTuple(args, const_cast<char*>("sss"), &object, &property, &value))
  {
    return NULL;
  }

  PropertyObject = object;
  PropertyResult = property;
  PropertyValue = value;

  if (Instance && QThread::currentThread() != QApplication::instance()->thread())
  {
    QMetaObject::invokeMethod(Instance, "threadSetProperty", Qt::QueuedConnection);
    if (!Instance->waitForGUI())
    {
      PyErr_SetString(PyExc_ValueError, "error setting property");
      return NULL;
    }
  }
  else if (QThread::currentThread() == QApplication::instance()->thread())
  {
    pqPythonEventSource::setProperty(PropertyObject, PropertyResult, PropertyValue);
  }
  else
  {
    PyErr_SetString(PyExc_AssertionError, "pqPythonEventSource not defined");
    return NULL;
  }

  if (PropertyObject.isEmpty())
  {
    PyErr_SetString(PyExc_ValueError, "object not found");
    return NULL;
  }

  if (PropertyResult.isEmpty())
  {
    PyErr_SetString(PyExc_ValueError, "property not found");
    return NULL;
  }

  return Py_BuildValue(const_cast<char*>("s"), "");
}

static PyObject* QtTesting_getQtVersion(PyObject* /*self*/, PyObject* /*args*/)
{
  // string QtTesting.getQtVersion()
  //    returns the Qt version as a string

  return Py_BuildValue(const_cast<char*>("s"), qVersion());
}

static PyObject* QtTesting_getChildren(PyObject* /*self*/, PyObject* args)
{
  // string QtTesting.getChildren('object')
  //    returns the a list of strings with object names

  const char* object = 0;

  if (!PyArg_ParseTuple(args, const_cast<char*>("s"), &object))
  {
    return NULL;
  }

  PropertyObject = object;
  ObjectList.clear();

  if (Instance && QThread::currentThread() != QApplication::instance()->thread())
  {
    QMetaObject::invokeMethod(Instance, "threadGetChildren", Qt::QueuedConnection);
    if (!Instance->waitForGUI())
    {
      PyErr_SetString(PyExc_ValueError, "error getting children");
      return NULL;
    }
  }
  else if (QThread::currentThread() == QApplication::instance()->thread())
  {
    ObjectList = pqPythonEventSource::getChildren(PropertyObject);
  }
  else
  {
    PyErr_SetString(PyExc_AssertionError, "pqPythonEventSource not defined");
    return NULL;
  }

  if (PropertyObject.isEmpty())
  {
    PyErr_SetString(PyExc_ValueError, "object not found");
    return NULL;
  }

  QString objs = ObjectList.join(", ");
  QString ret = QString("[%1]").arg(objs);

  return Py_BuildValue(const_cast<char*>("s"), ret.toUtf8().data());
}

static PyObject* QtTesting_invokeMethod(PyObject* /*self*/, PyObject* args)
{
  // string QtTesting.invokeMethod('object', 'method')
  //    calls a method and returns its value

  const char* object = 0;
  const char* method = 0;

  if (!PyArg_ParseTuple(args, const_cast<char*>("ss"), &object, &method))
  {
    return NULL;
  }

  PropertyObject = object;
  PropertyValue = method;
  PropertyResult = QString();

  if (Instance && QThread::currentThread() != QApplication::instance()->thread())
  {
    QMetaObject::invokeMethod(Instance, "threadInvokeMethod", Qt::QueuedConnection);
    if (!Instance->waitForGUI())
    {
      PyErr_SetString(PyExc_ValueError, "error invoking method");
      return NULL;
    }
  }
  else if (QThread::currentThread() == QApplication::instance()->thread())
  {
    PropertyResult = pqPythonEventSource::invokeMethod(PropertyObject, PropertyValue);
  }
  else
  {
    PyErr_SetString(PyExc_AssertionError, "pqPythonEventSource not defined");
    return NULL;
  }

  if (PropertyObject.isEmpty())
  {
    PyErr_SetString(PyExc_ValueError, "object not found");
    return NULL;
  }
  else if (PropertyValue.isEmpty())
  {
    PyErr_SetString(PyExc_ValueError, "method not found");
    return NULL;
  }

  return Py_BuildValue(const_cast<char*>("s"), PropertyResult.toUtf8().data());
}

static PyMethodDef QtTestingMethods[] = {
  { const_cast<char*>("playCommand"), QtTesting_playCommand, METH_VARARGS,
    const_cast<char*>("Play a test command.") },
  { const_cast<char*>("getProperty"), QtTesting_getProperty, METH_VARARGS,
    const_cast<char*>("Get a property of an object.") },
  { const_cast<char*>("setProperty"), QtTesting_setProperty, METH_VARARGS,
    const_cast<char*>("Set a property of an object.") },

  { const_cast<char*>("getQtVersion"), QtTesting_getQtVersion, METH_VARARGS,
    const_cast<char*>("Get the version of Qt being used.") },
  { const_cast<char*>("getChildren"), QtTesting_getChildren, METH_VARARGS,
    const_cast<char*>("Return a list of child objects.") },
  { const_cast<char*>("invokeMethod"), QtTesting_invokeMethod, METH_VARARGS,
    const_cast<char*>("Invoke a Qt slot with the signature \"QVariant foo()\".") },

  { NULL, NULL, 0, NULL } // Sentinal
};

PyMODINIT_FUNC initQtTesting(void)
{
  Py_InitModule(const_cast<char*>("QtTesting"), QtTestingMethods);
}

class pqPythonEventSource::pqInternal
{
public:
  QString FileName;
};

pqPythonEventSource::pqPythonEventSource(QObject* p)
  : pqThreadedEventSource(p)
{
  this->Internal = new pqInternal;
}

pqPythonEventSource::~pqPythonEventSource()
{
  delete this->Internal;
}

void pqPythonEventSource::initPythonIfNeeded()
{
  int initPy = Py_IsInitialized();
  if (!initPy)
  {
    // initialize python
    Py_Initialize();
#ifdef SIGINT
    signal(SIGINT, SIG_DFL);
#endif
  }
  // add QtTesting to python's inittab, so it is
  // available to all interpreters
  PyImport_AppendInittab(const_cast<char*>("QtTesting"), initQtTesting);
}

void pqPythonEventSource::setContent(const QString& path)
{
  // start the python thread
  this->Internal->FileName = path;
  this->start();
}

QString pqPythonEventSource::getProperty(QString& object, QString& prop)
{
  // ensure other tasks have been completed
  pqEventDispatcher::processEventsAndWait(1);
  QVariant ret;

  QObject* qobject = pqObjectNaming::GetObject(object);
  if (!qobject)
  {
    object = QString();
    return QString();
  }
  int idx = qobject->metaObject()->indexOfProperty(prop.toUtf8().data());
  if (idx == -1)
  {
    prop = QString();
    return QString();
  }
  else
  {
    QMetaProperty metaProp = qobject->metaObject()->property(idx);
    ret = metaProp.read(qobject);

    if (metaProp.type() == QVariant::List || metaProp.type() == QVariant::StringList)
    {
      return ret.toStringList().join(";");
    }
    return ret.toString();
  }
}

void pqPythonEventSource::threadGetProperty()
{
  PropertyValue = this->getProperty(PropertyObject, PropertyResult);
  this->guiAcknowledge();
}

void pqPythonEventSource::setProperty(QString& object, QString& prop, const QString& value)
{
  // ensure other tasks have been completed
  pqEventDispatcher::processEventsAndWait(1);
  QVariant ret;

  QObject* qobject = pqObjectNaming::GetObject(object);
  if (!qobject)
  {
    object = QString();
    return;
  }

  int idx = qobject->metaObject()->indexOfProperty(prop.toUtf8().data());
  if (idx == -1)
  {
    prop = QString();
    return;
  }
  else
  {
    QVariant val = value;
    QMetaProperty metaProp = qobject->metaObject()->property(idx);
    if (metaProp.type() == QVariant::List || metaProp.type() == QVariant::StringList)
    {
      val = value.split(";");
    }
    qobject->setProperty(prop.toUtf8().data(), val);
  }
}

void pqPythonEventSource::threadSetProperty()
{
  this->setProperty(PropertyObject, PropertyResult, PropertyValue);
  this->guiAcknowledge();
}

QStringList pqPythonEventSource::getChildren(QString& object)
{
  // ensure other tasks have been completed
  pqEventDispatcher::processEventsAndWait(1);
  QStringList ret;

  QObject* qobject = pqObjectNaming::GetObject(object);
  if (!qobject)
  {
    object = QString();
  }
  else
  {
    const QObjectList& children = qobject->children();
    Q_FOREACH (QObject* child, children)
    {
      ret.append(pqObjectNaming::GetName(*child));
    }
  }
  return ret;
}

void pqPythonEventSource::threadGetChildren()
{
  ObjectList = this->getChildren(PropertyObject);
  this->guiAcknowledge();
}

void pqPythonEventSource::start()
{
  this->initPythonIfNeeded();

  PyEval_InitThreads();
  PyEval_ReleaseLock();
  pqThreadedEventSource::start();
}

void pqPythonEventSource::run()
{
  QFile file(this->Internal->FileName);
  if (!file.open(QFile::ReadOnly | QFile::Text))
  {
    printf("Unable to open python script\n");
    return;
  }
  this->initPythonIfNeeded();
  Instance = this;

  PyGILState_STATE gstate = PyGILState_Ensure();

  // finally run the script
  QByteArray wholeFile = file.readAll();
  int result = PyRun_SimpleString(wholeFile.data()) == 0 ? 0 : 1;

  PyGILState_Release(gstate);
  PyEval_ReleaseLock();

  this->done(result);
}

void pqPythonEventSource::threadInvokeMethod()
{
  PropertyResult = this->invokeMethod(PropertyObject, PropertyValue);
  this->guiAcknowledge();
}

QString pqPythonEventSource::invokeMethod(QString& object, QString& method)
{
  // ensure other tasks have been completed
  pqEventDispatcher::processEventsAndWait(1);
  QVariant ret;

  QObject* qobject = pqObjectNaming::GetObject(object);
  if (!qobject)
  {
    object = QString();
  }
  else
  {
    if (!QMetaObject::invokeMethod(qobject, method.toUtf8().data(), Q_RETURN_ARG(QVariant, ret)))
    {
      method = QString();
    }
  }
  return ret.toString();
}
