/**
 * Python plugin for Orthanc
 * Copyright (C) 2020-2021 Osimis S.A., Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


// http://edcjones.tripod.com/refcount.html
// https://docs.python.org/3/extending/extending.html

// https://www.codevate.com/blog/7-concurrency-with-embedded-python-in-a-multi-threaded-c-application
// https://fr.slideshare.net/YiLungTsai/embed-python


#include "IncomingHttpRequestFilter.h"
#include "OnChangeCallback.h"
#include "OnStoredInstanceCallback.h"

#include "RestCallbacks.h"
#include "PythonModule.h"

#include "Autogenerated/sdk.h"

#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"

#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp>


// The "dl_iterate_phdr()" function (to walk through shared libraries)
// is not available on Microsoft Windows and Apple OS X
#if defined(_WIN32)
#  define HAS_DL_ITERATE  0
#elif defined(__APPLE__) && defined(__MACH__)
#  define HAS_DL_ITERATE  0
#else
#  define HAS_DL_ITERATE  1
#endif



static bool pythonEnabled_ = false;
static std::string userScriptName_;
static std::vector<PyMethodDef>  globalFunctions_;


static void InstallClasses(PyObject* module)
{
  RegisterOrthancSdk(module);
}


static void SetupGlobalFunctions()
{
  if (!globalFunctions_.empty())
  {
    ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
  }

  /**
   * Add all the manual global functions
   **/

  std::list<PyMethodDef> functions;

  {
    PyMethodDef f = { "RegisterRestCallback", RegisterRestCallback, METH_VARARGS, "" };
    functions.push_back(f);
  }

  {
    PyMethodDef f = { "RegisterOnChangeCallback", RegisterOnChangeCallback, METH_VARARGS, "" };
    functions.push_back(f);
  }

  {
    PyMethodDef f = { "RegisterOnStoredInstanceCallback", RegisterOnStoredInstanceCallback,
                      METH_VARARGS, "" };
    functions.push_back(f);
  }

  {
    // New in release 3.0
    PyMethodDef f = { "RegisterIncomingHttpRequestFilter", RegisterIncomingHttpRequestFilter, METH_VARARGS, "" };
    functions.push_back(f);
  }
  
  /**
   * Append all the global functions that were automatically generated
   **/
  
  const PyMethodDef* sdk = GetOrthancSdkFunctions();

  for (size_t i = 0; sdk[i].ml_name != NULL; i++)
  {
    functions.push_back(sdk[i]);
  }

  /**
   * Flatten the list of functions into the vector
   **/

  globalFunctions_.resize(functions.size());
  std::copy(functions.begin(), functions.end(), globalFunctions_.begin());

  PyMethodDef sentinel = { NULL };
  globalFunctions_.push_back(sentinel);
}

  
static PyMethodDef* GetGlobalFunctions()
{
  if (globalFunctions_.empty())
  {
    // "SetupGlobalFunctions()" should have been called
    ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
  }
  else
  {
    return &globalFunctions_[0];
  }
}



#if HAS_DL_ITERATE == 1

#include <dlfcn.h>
#include <link.h>  // For dl_phdr_info

static int ForceImportCallback(struct dl_phdr_info *info, size_t size, void *data)
{ 
  /**
   * The following line solves the error: "ImportError:
   * /usr/lib/python2.7/dist-packages/PIL/_imaging.x86_64-linux-gnu.so:
   * undefined symbol: PyExc_SystemError"
   * https://stackoverflow.com/a/48517485/881731
   * 
   * dlopen("/usr/lib/x86_64-linux-gnu/libpython2.7.so", RTLD_NOW | RTLD_LAZY | RTLD_GLOBAL);
   * 
   * Another fix consists in using LD_PRELOAD as follows:
   * LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libpython2.7.so ~/Subversion/orthanc/i/Orthanc tutu.json
   **/
    
  std::string module(info->dlpi_name);

  if (module.find("python") != std::string::npos)
  {
    OrthancPlugins::LogWarning("Force global loading of Python shared library: " + module);
    dlopen(module.c_str(), RTLD_NOW | RTLD_LAZY | RTLD_GLOBAL);
  }
  
  return 0;
}

#endif


extern "C"
{
  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
  {
    OrthancPlugins::SetGlobalContext(c);
    OrthancPlugins::LogWarning("Python plugin is initializing");
    

    /* Check the version of the Orthanc core */
    if (OrthancPluginCheckVersion(c) == 0)
    {
      char info[1024];
      sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
              c->orthancVersion,
              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
      OrthancPluginLogError(c, info);
      return -1;
    }
    
    OrthancPluginSetDescription(c, "Run Python scripts as Orthanc plugins");
    
    try
    {
      /**
       * Detection of the user script
       **/

      OrthancPlugins::OrthancConfiguration config;

      static const char* const OPTION = "PythonScript";
    
      std::string script;
      if (!config.LookupStringValue(script, OPTION))
      {
        pythonEnabled_ = false;
      
        OrthancPlugins::LogWarning("The option \"" + std::string(OPTION) + "\" is not provided: " +
                                   "Python scripting is disabled");
      }
      else
      {
        pythonEnabled_ = true;
      
        /**
         * Installation of the user script
         **/

        const boost::filesystem::path path(script);
        if (!boost::iequals(path.extension().string(), ".py"))
        {
          OrthancPlugins::LogError("Python script must have the \".py\" file extension: " +
                                   path.string());
          return -1;
        }

        if (!boost::filesystem::is_regular_file(path))
        {
          OrthancPlugins::LogError("Inexistent directory for the Python script: " +
                                   path.string());
          return -1;
        }

        boost::filesystem::path userScriptDirectory = boost::filesystem::absolute(path).parent_path();

        {
          boost::filesystem::path module = path.filename().replace_extension("");
          userScriptName_ = module.string();
        }

        OrthancPlugins::LogWarning("Using Python script \"" + userScriptName_ +
                                   ".py\" from directory: " + userScriptDirectory.string());
    
    
        /**
         * Initialization of Python
         **/

#if HAS_DL_ITERATE == 1
        dl_iterate_phdr(ForceImportCallback, NULL);
#endif

        SetupGlobalFunctions();
        PythonLock::GlobalInitialize("orthanc", "Exception",
                                     GetGlobalFunctions, InstallClasses,
                                     config.GetBooleanValue("PythonVerbose", false));
        PythonLock::AddSysPath(userScriptDirectory.string());

      
        /**
         * Force loading the declarations in the user script
         **/

        PythonLock lock;

        {
          PythonModule module(lock, userScriptName_);
        }
      
        std::string traceback;
        if (lock.HasErrorOccurred(traceback))
        {
          OrthancPlugins::LogError("Error during the installation of the Python script, "
                                   "traceback:\n" + traceback);
          return -1;
        }
      }
    }
    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
    {
      OrthancPlugins::LogError("Exception while starting the Python plugin: " +
                               std::string(e.What(c)));
      return -1;
    }
    
    return 0;
  }


  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
  {
    OrthancPlugins::LogWarning("Python plugin is finalizing");

    if (pythonEnabled_)
    {
      FinalizeOnChangeCallback();
      FinalizeRestCallbacks();
      FinalizeOnStoredInstanceCallback();
      FinalizeIncomingHttpRequestFilter();
      
      PythonLock::GlobalFinalize();
    }
  }


  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
  {
    return "python";
  }


  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
  {
    return PLUGIN_VERSION;
  }
}
