/*=========================================================================

  Program:   Visualization Toolkit
  Module:    vtkPythonCommand.cxx

  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
  All rights reserved.
  See Copyright.txt or http://www.kitware.com/Copyright.htm for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=========================================================================*/

#include "vtkPythonCommand.h"
#include "vtkObject.h"
#include "vtkPythonUtil.h"

vtkPythonCommand::vtkPythonCommand()
{
  this->obj = nullptr;
  this->ThreadState = nullptr;
  vtkPythonUtil::RegisterPythonCommand(this);
}

vtkPythonCommand::~vtkPythonCommand()
{
  vtkPythonUtil::UnRegisterPythonCommand(this);
  if (this->obj && Py_IsInitialized())
  {
    vtkPythonScopeGilEnsurer gilEnsurer;
    Py_DECREF(this->obj);
  }
  this->obj = nullptr;
}

void vtkPythonCommand::SetObject(PyObject* o)
{
  vtkPythonScopeGilEnsurer gilEnsurer;
  Py_INCREF(o);
  this->obj = o;
}

void vtkPythonCommand::SetThreadState(PyThreadState* ts)
{
  this->ThreadState = ts;
}

namespace
{
PyObject* BuildCallDataArgList(
  PyObject* caller, const char* eventname, PyObject* callDataAsPyObject)
{
  PyObject* arglist;
  if (callDataAsPyObject)
  {
    arglist = Py_BuildValue("(NsN)", caller, eventname, callDataAsPyObject);
  }
  else
  {
    PyErr_Clear();
    /* we couldn't create a the expected python object, so we pass in None */
    Py_INCREF(Py_None);
    arglist = Py_BuildValue("(NsN)", caller, eventname, Py_None);
  }
  return arglist;
}
}

void vtkPythonCommand::Execute(vtkObject* ptr, unsigned long eventtype, void* callData)
{
  if (!this->obj)
  {
    return;
  }

  // Sometimes it is possible for the command to be invoked after
  // Py_Finalize is called, this will cause nasty errors so we return if
  // the interpreter is not initialized.
  if (Py_IsInitialized() == 0)
  {
    return;
  }

#ifndef VTK_NO_PYTHON_THREADS
  vtkPythonScopeGilEnsurer gilEnsurer(true);
#else
  // We only need to do this if we are not calling PyGILState_Ensure(), in fact
  // the code below is not safe if not executed on the same thread that the
  // AddObserver call was made on, as we will end up swapping in the wrong
  // thread state.
  //
  // If a threadstate has been set using vtkPythonCommand::SetThreadState,
  // then swap it in here.  See the email to vtk-developers@vtk.org from
  // June 18, 2009 with subject "Py_NewInterpreter and vtkPythonCallback issue"
  PyThreadState* prevThreadState = nullptr;
  if (this->ThreadState)
  {
    prevThreadState = PyThreadState_Swap(this->ThreadState);
  }
#endif

  PyObject* obj2 = nullptr;
  if (eventtype != vtkCommand::DeleteEvent && ptr && ptr->GetReferenceCount() > 0)
  {
    obj2 = vtkPythonUtil::GetObjectFromPointer(ptr);
  }
  else
  {
    Py_INCREF(Py_None);
    obj2 = Py_None;
  }

  const char* eventname = this->GetStringFromEventId(eventtype);

  // extension by Charl P. Botha so that callData is available from Python:
  // * callData used to be ignored completely: this is not entirely desirable,
  //   e.g. with catching ErrorEvent
  // * I have extended this code so that callData can be caught whilst not
  //   affecting any existing VTK Python code
  // * make sure your observer python function has a CallDataType string
  //   attribute that describes how callData should be passed through, e.g.:
  //   def handler(theObject, eventType, message):
  //      print "Error: %s" % (message)
  //   # we know that ErrorEvent passes a null-terminated string
  //   handler.CallDataType = "string0"
  //   someObject.AddObserver('ErrorEvent', handler)
  //
  // support for additional types has then been added by Jean-Christophe Fillion-Robin
  //
  char callDataTypeLiteral[] = "CallDataType"; // Need char*, not const char*.
  PyObject* callDataTypeObj = PyObject_GetAttrString(this->obj, callDataTypeLiteral);

  PyObject* arglist = nullptr;
  if (callData && callDataTypeObj)
  {
    if (PyInt_Check(callDataTypeObj))
    {
      long callDataTypeLong = PyInt_AsLong(callDataTypeObj);
      int invalid = (callDataTypeLong == -1) && PyErr_Occurred();
      if (!invalid)
      {
        if (callDataTypeLong == VTK_STRING)
        {
          // this means the user wants the callData cast as a string
          PyObject* callDataAsString = PyString_FromString(reinterpret_cast<char*>(callData));
          arglist = BuildCallDataArgList(obj2, eventname, callDataAsString);
        }
        else if (callDataTypeLong == VTK_OBJECT)
        {
          // this means the user wants the callData cast as a vtkObject
          PyObject* callDataAsVTKObject =
            vtkPythonUtil::GetObjectFromPointer(reinterpret_cast<vtkObject*>(callData));
          arglist = BuildCallDataArgList(obj2, eventname, callDataAsVTKObject);
        }
        else if (callDataTypeLong == VTK_INT)
        {
          // this means the user wants the callData cast as an int
          PyObject* callDataAsInt = PyInt_FromLong(*reinterpret_cast<int*>(callData));
          arglist = BuildCallDataArgList(obj2, eventname, callDataAsInt);
        }
        else if (callDataTypeLong == VTK_LONG)
        {
          // this means the user wants the callData cast as a long
          PyObject* callDataAsInt = PyLong_FromLong(*reinterpret_cast<long*>(callData));
          arglist = BuildCallDataArgList(obj2, eventname, callDataAsInt);
        }
        else if (callDataTypeLong == VTK_DOUBLE)
        {
          // this means the user wants the callData cast as a double
          PyObject* callDataAsInt = PyFloat_FromDouble(*reinterpret_cast<double*>(callData));
          arglist = BuildCallDataArgList(obj2, eventname, callDataAsInt);
        }
        else if (callDataTypeLong == VTK_FLOAT)
        {
          // this means the user wants the callData cast as a float
          PyObject* callDataAsInt = PyFloat_FromDouble(*reinterpret_cast<float*>(callData));
          arglist = BuildCallDataArgList(obj2, eventname, callDataAsInt);
        }
      }
      else
      {
        // we don't handle this, so we pass in a None as the third parameter
        Py_INCREF(Py_None);
        arglist = Py_BuildValue("(NsN)", obj2, eventname, Py_None);
      }
    }
    else if (PyString_Check(callDataTypeObj))
    {
#ifdef VTK_PY3K
      PyObject* bytes = PyUnicode_AsEncodedString(callDataTypeObj, 0, nullptr);
      const char* callDataTypeString = nullptr;
      if (bytes)
      {
        callDataTypeString = PyBytes_AsString(bytes);
      }
#else
      const char* callDataTypeString = PyString_AsString(callDataTypeObj);
#endif
      if (callDataTypeString)
      {
        if (strcmp(callDataTypeString, "string0") == 0)
        {
          // this means the user wants the callData cast as a string
          PyObject* callDataAsString = PyString_FromString(reinterpret_cast<char*>(callData));
          arglist = BuildCallDataArgList(obj2, eventname, callDataAsString);
        }
      }
      else
      {
        // we don't handle this, so we pass in a None as the third parameter
        Py_INCREF(Py_None);
        arglist = Py_BuildValue("(NsN)", obj2, eventname, Py_None);
      }
#ifdef VTK_PY3K
      Py_XDECREF(bytes);
#endif
    }
    else
    {
      // the handler object has a CallDataType attribute, but it's neither an
      // integer or a string -- then we do traditional arguments
      arglist = Py_BuildValue("(Ns)", obj2, eventname);
    }
    // we have to do this
    Py_DECREF(callDataTypeObj);
  }
  else
  {
    // this means there was no CallDataType attribute, so we do the
    // traditional obj(object, eventname) call
    PyErr_Clear();
    arglist = Py_BuildValue("(Ns)", obj2, eventname);
  }

  PyObject* result = PyEval_CallObject(this->obj, arglist);
  Py_DECREF(arglist);

  if (result)
  {
    Py_DECREF(result);
  }
  else
  {
    if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt))
    {
      cerr << "Caught a Ctrl-C within python, exiting program.\n";
      Py_Exit(1);
    }
    PyErr_Print();
  }

#ifdef VTK_NO_PYTHON_THREADS
  // If we did the swap near the top of this function then swap back now.
  if (this->ThreadState)
  {
    PyThreadState_Swap(prevThreadState);
  }
#endif
}
