/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "PrecompiledHeadersServer.h"
#include "LuaScripting.h"

#include "DicomInstanceToStore.h"
#include "OrthancConfiguration.h"
#include "OrthancRestApi/OrthancRestApi.h"
#include "OutgoingDicomInstance.h"
#include "ServerContext.h"
#include "ServerJobs/LuaJobManager.h"

#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
#include "../../OrthancFramework/Sources/HttpServer/StringHttpOutput.h"
#include "../../OrthancFramework/Sources/Logging.h"
#include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h"

#include <OrthancServerResources.h>

static const char* ON_HEART_BEAT = "OnHeartBeat";


namespace Orthanc
{
  struct LuaScripting::PImpl
  {
    LuaJobManager  jobManager_;
  };


  class LuaScripting::IEvent : public IDynamicObject
  {
  public:
    virtual void Apply(LuaScripting& lock) = 0;
  };


  class LuaScripting::OnStoredInstanceEvent : public LuaScripting::IEvent
  {
  private:
    std::string    instanceId_;
    Json::Value    simplifiedTags_;
    Json::Value    metadata_;
    Json::Value    origin_;

  public:
    OnStoredInstanceEvent(const std::string& instanceId,
                          const Json::Value& simplifiedTags,
                          const Json::Value& metadata,
                          const DicomInstanceToStore& instance) :
      instanceId_(instanceId),
      simplifiedTags_(simplifiedTags),
      metadata_(metadata)
    {
      instance.GetOrigin().Format(origin_);
    }

    virtual void Apply(LuaScripting& that) ORTHANC_OVERRIDE
    {
      static const char* NAME = "OnStoredInstance";

      LuaScripting::Lock lock(that);

      if (lock.GetLua().IsExistingFunction(NAME))
      {
        that.InitializeJob();

        LuaFunctionCall call(lock.GetLua(), NAME);
        call.PushString(instanceId_);
        call.PushJson(simplifiedTags_);
        call.PushJson(metadata_);
        call.PushJson(origin_);
        call.Execute();

        that.SubmitJob();
      }
    }
  };


  class LuaScripting::ExecuteEvent : public LuaScripting::IEvent
  {
  private:
    std::string    command_;

  public:
    explicit ExecuteEvent(const std::string& command) :
      command_(command)
    {
    }

    virtual void Apply(LuaScripting& that) ORTHANC_OVERRIDE
    {
      LuaScripting::Lock lock(that);

      if (lock.GetLua().IsExistingFunction(command_.c_str()))
      {
        LuaFunctionCall call(lock.GetLua(), command_.c_str());
        call.Execute();
      }
    }
  };


  class LuaScripting::StableResourceEvent : public LuaScripting::IEvent
  {
  private:
    ServerIndexChange  change_;

    class GetInfoOperations : public ServerIndex::IReadOnlyOperations
    {
    private:
      const ServerIndexChange&            change_;
      bool                                ok_;
      DicomMap                            tags_;
      std::map<MetadataType, std::string> metadata_;      

    public:
      explicit GetInfoOperations(const ServerIndexChange& change) :
        change_(change),
        ok_(false)
      {
      }
      
      virtual void Apply(ServerIndex::ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
      {
        int64_t internalId;
        ResourceType level;
        if (transaction.LookupResource(internalId, level, change_.GetPublicId()) &&
            level == change_.GetResourceType())
        {
          transaction.GetMainDicomTags(tags_, internalId);
          transaction.GetAllMetadata(metadata_, internalId);
          ok_ = true;
        }
      }

      void CallLua(LuaScripting& that,
                   const char* name) const
      {
        if (ok_)
        {
          Json::Value formattedMetadata = Json::objectValue;

          for (std::map<MetadataType, std::string>::const_iterator 
                 it = metadata_.begin(); it != metadata_.end(); ++it)
          {
            std::string key = EnumerationToString(it->first);
            formattedMetadata[key] = it->second;
          }      

          {
            LuaScripting::Lock lock(that);

            if (lock.GetLua().IsExistingFunction(name))
            {
              that.InitializeJob();

              Json::Value json = Json::objectValue;

              if (change_.GetResourceType() == ResourceType_Study)
              {
                DicomMap t;
                tags_.ExtractStudyInformation(t);  // Discard patient-related tags
                FromDcmtkBridge::ToJson(json, t, DicomToJsonFormat_Human);
              }
              else
              {
                FromDcmtkBridge::ToJson(json, tags_, DicomToJsonFormat_Human);
              }

              LuaFunctionCall call(lock.GetLua(), name);
              call.PushString(change_.GetPublicId());
              call.PushJson(json);
              call.PushJson(formattedMetadata);
              call.Execute();

              that.SubmitJob();
            }
          }
        }
      }
    };
    

  public:
    explicit StableResourceEvent(const ServerIndexChange& change) :
      change_(change)
    {
    }

    virtual void Apply(LuaScripting& that) ORTHANC_OVERRIDE
    {
      const char* name;

      switch (change_.GetChangeType())
      {
        case ChangeType_StablePatient:
          name = "OnStablePatient";
          break;

        case ChangeType_StableStudy:
          name = "OnStableStudy";
          break;

        case ChangeType_StableSeries:
          name = "OnStableSeries";
          break;

        default:
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
      }

      {
        // Avoid unnecessary calls to the database if there's no Lua callback
        LuaScripting::Lock lock(that);

        if (!lock.GetLua().IsExistingFunction(name))
        {
          return;
        }
      }
      
      GetInfoOperations operations(change_);
      that.context_.GetIndex().Apply(operations);
      operations.CallLua(that, name);
    }
  };


  class LuaScripting::LuaJobEvent : public LuaScripting::IEvent
  {
  private:
    JobEvent event_;

  public:
    explicit LuaJobEvent(const JobEvent& event) :
      event_(event)
    {
    }

    virtual void Apply(LuaScripting& that) ORTHANC_OVERRIDE
    {
      std::string functionName;
      
      switch (event_.GetEventType())
      {
        case JobEventType_Failure:
          functionName = "OnJobFailure";
          break;

        case JobEventType_Submitted:
          functionName = "OnJobSubmitted";
          break;

        case JobEventType_Success:
          functionName = "OnJobSuccess";
          break;

        default:
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
      }

      {
        LuaScripting::Lock lock(that);

        if (lock.GetLua().IsExistingFunction(functionName.c_str()))
        {
          LuaFunctionCall call(lock.GetLua(), functionName.c_str());
          call.PushString(event_.GetJobId());
          call.Execute();
        }
      }
    }
  };


  class LuaScripting::DeleteEvent : public LuaScripting::IEvent
  {
  private:
    ResourceType  level_;
    std::string   publicId_;

  public:
    DeleteEvent(ResourceType level,
                const std::string& publicId) :
      level_(level),
      publicId_(publicId)
    {
    }

    virtual void Apply(LuaScripting& that) ORTHANC_OVERRIDE
    {
      std::string functionName;
      
      switch (level_)
      {
        case ResourceType_Patient:
          functionName = "OnDeletedPatient";
          break;

        case ResourceType_Study:
          functionName = "OnDeletedStudy";
          break;

        case ResourceType_Series:
          functionName = "OnDeletedSeries";
          break;

        case ResourceType_Instance:
          functionName = "OnDeletedInstance";
          break;

        default:
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
      }

      {
        LuaScripting::Lock lock(that);

        if (lock.GetLua().IsExistingFunction(functionName.c_str()))
        {
          LuaFunctionCall call(lock.GetLua(), functionName.c_str());
          call.PushString(publicId_);
          call.Execute();
        }
      }
    }
  };


  class LuaScripting::UpdateEvent : public LuaScripting::IEvent
  {
  private:
    ResourceType  level_;
    std::string   publicId_;

  public:
    UpdateEvent(ResourceType level,
                const std::string& publicId) :
      level_(level),
      publicId_(publicId)
    {
    }

    virtual void Apply(LuaScripting& that) ORTHANC_OVERRIDE
    {
      std::string functionName;
      
      switch (level_)
      {
        case ResourceType_Patient:
          functionName = "OnUpdatedPatient";
          break;

        case ResourceType_Study:
          functionName = "OnUpdatedStudy";
          break;

        case ResourceType_Series:
          functionName = "OnUpdatedSeries";
          break;

        case ResourceType_Instance:
          functionName = "OnUpdatedInstance";
          break;

        default:
          THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
      }

      {
        LuaScripting::Lock lock(that);

        if (lock.GetLua().IsExistingFunction(functionName.c_str()))
        {
          LuaFunctionCall call(lock.GetLua(), functionName.c_str());
          call.PushString(publicId_);
          call.Execute();
        }
      }
    }
  };


  ServerContext* LuaScripting::GetServerContext(lua_State *state)
  {
    const void* value = LuaContext::GetGlobalVariable(state, "_ServerContext");
    return const_cast<ServerContext*>(reinterpret_cast<const ServerContext*>(value));
  }


  // Syntax in Lua: RestApiGet(uri, builtin)
  int LuaScripting::RestApiGet(lua_State *state)
  {
    ServerContext* serverContext = GetServerContext(state);
    if (serverContext == NULL)
    {
      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
      lua_pushnil(state);
      return 1;
    }

    // Check the types of the arguments
    int nArgs = lua_gettop(state);
    if (nArgs < 1 || nArgs > 3 || 
        !lua_isstring(state, 1) ||                 // URI
        (nArgs >= 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
    {
      LOG(ERROR) << "Lua: Bad parameters to RestApiGet()";
      lua_pushnil(state);
      return 1;
    }

    const char* uri = lua_tostring(state, 1);
    bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false);

    std::map<std::string, std::string> headers;
    LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */);

    try
    {
      std::string result;
      if (IHttpHandler::SimpleGet(result, NULL, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
                                  RequestOrigin_Lua, uri, headers) == HttpStatus_200_Ok)
      {
        lua_pushlstring(state, result.c_str(), result.size());
        return 1;
      }
    }
    catch (OrthancException& e)
    {
      LOG(ERROR) << "Lua: " << e.What();
    }

    LOG(ERROR) << "Lua: Error in RestApiGet() for URI: " << uri;
    lua_pushnil(state);
    return 1;
  }


  int LuaScripting::RestApiPostOrPut(lua_State *state,
                                     bool isPost)
  {
    ServerContext* serverContext = GetServerContext(state);
    if (serverContext == NULL)
    {
      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
      lua_pushnil(state);
      return 1;
    }

    // Check the types of the arguments
    int nArgs = lua_gettop(state);
    if (nArgs < 2 || nArgs > 4 || 
        !lua_isstring(state, 1) ||                 // URI
        !lua_isstring(state, 2) ||                 // Body
        (nArgs >= 3 && !lua_isboolean(state, 3)))  // Restrict to built-in API?
    {
      LOG(ERROR) << "Lua: Bad parameters to " << (isPost ? "RestApiPost()" : "RestApiPut()");
      lua_pushnil(state);
      return 1;
    }

    const char* uri = lua_tostring(state, 1);
    size_t bodySize = 0;
    const char* bodyData = lua_tolstring(state, 2, &bodySize);
    bool builtin = (nArgs == 3 ? lua_toboolean(state, 3) != 0 : false);

    std::map<std::string, std::string> headers;
    LuaContext::GetDictionaryArgument(headers, state, 4, true /* HTTP header key to lower case */);
        
    try
    {
      std::string result;
      if (isPost ?
          IHttpHandler::SimplePost(result, NULL,
                                   serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
                                   RequestOrigin_Lua, uri, bodyData, bodySize, headers) == HttpStatus_200_Ok :
          IHttpHandler::SimplePut(result, NULL,
                                  serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
                                  RequestOrigin_Lua, uri, bodyData, bodySize, headers) == HttpStatus_200_Ok)
      {
        lua_pushlstring(state, result.c_str(), result.size());
        return 1;
      }
    }
    catch (OrthancException& e)
    {
      LOG(ERROR) << "Lua: " << e.What();
    }

    LOG(ERROR) << "Lua: Error in " << (isPost ? "RestApiPost()" : "RestApiPut()") << " for URI: " << uri;
    lua_pushnil(state);
    return 1;
  }


  // Syntax in Lua: RestApiPost(uri, body, builtin)
  int LuaScripting::RestApiPost(lua_State *state)
  {
    return RestApiPostOrPut(state, true);
  }


  // Syntax in Lua: RestApiPut(uri, body, builtin)
  int LuaScripting::RestApiPut(lua_State *state)
  {
    return RestApiPostOrPut(state, false);
  }


  // Syntax in Lua: RestApiDelete(uri, builtin)
  int LuaScripting::RestApiDelete(lua_State *state)
  {
    ServerContext* serverContext = GetServerContext(state);
    if (serverContext == NULL)
    {
      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
      lua_pushnil(state);
      return 1;
    }

    // Check the types of the arguments
    int nArgs = lua_gettop(state);
    if (nArgs < 1 || nArgs > 3 ||
        !lua_isstring(state, 1) ||                 // URI
        (nArgs >= 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
    {
      LOG(ERROR) << "Lua: Bad parameters to RestApiDelete()";
      lua_pushnil(state);
      return 1;
    }

    const char* uri = lua_tostring(state, 1);
    bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false);

    std::map<std::string, std::string> headers;
    LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */);
    
    try
    {
      std::string bodyIgnored;
      if (IHttpHandler::SimpleDelete(bodyIgnored, NULL, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin),
                                     RequestOrigin_Lua, uri, headers) == HttpStatus_200_Ok)
      {
        lua_pushboolean(state, 1);
        return 1;
      }
    }
    catch (OrthancException& e)
    {
      LOG(ERROR) << "Lua: " << e.What();
    }

    LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri;
    lua_pushnil(state);

    return 1;
  }

  // Syntax in Lua: SetStableStatus(resourceId, true)
  int LuaScripting::SetStableStatus(lua_State* state)
  {
    ServerContext* serverContext = GetServerContext(state);
    if (serverContext == NULL)
    {
      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
      lua_pushnil(state);
      return 1;
    }

    // Check the types of the arguments
    int nArgs = lua_gettop(state);
    if (nArgs < 1 || nArgs > 3 ||
        !lua_isstring(state, 1) ||                 // Resource
        !lua_isboolean(state, 2))                  // newStateIsStable
    {
      LOG(ERROR) << "Lua: Bad parameters to SetStableStatus()";
      lua_pushnil(state);
      return 1;
    }

    const char* resourceId = lua_tostring(state, 1);
    bool newStateIsStable = lua_toboolean(state, 2);
    
    try
    {
      bool hasStateChanged = false;

      if (serverContext->GetIndex().SetStableStatus(hasStateChanged, resourceId, newStateIsStable))
      {
        if (hasStateChanged)
        {
          lua_pushboolean(state, 1);
        }
        else
        {
          lua_pushboolean(state, 0);
        }

        return 1;
      }
    }
    catch (OrthancException& e)
    {
      LOG(ERROR) << "Lua: " << e.What();
    }

    LOG(ERROR) << "Lua: Error in SetStableStatus() for Resource: " << resourceId;
    lua_pushnil(state);

    return 1;
  }


  // Syntax in Lua: StoreKeyValue(storeId, key, value)
  int LuaScripting::StoreKeyValue(lua_State* state)
  {
    ServerContext* serverContext = GetServerContext(state);
    if (serverContext == NULL)
    {
      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
      lua_pushnil(state);
      return 1;
    }

    // Check the types of the arguments
    int nArgs = lua_gettop(state);
    if (nArgs != 3 ||
        !lua_isstring(state, 1) ||                 // storeId
        !lua_isstring(state, 2) ||                 // key
        !lua_isstring(state, 3))                   // value
    {
      LOG(ERROR) << "Lua: Bad parameters to StoreKeyValue()";
      lua_pushnil(state);
      return 1;
    }

    if (!serverContext->GetIndex().HasKeyValueStoresSupport())
    {
      LOG(ERROR) << "Lua: Unable to use StoreKeyValue() when the DB backend does not support Key-Value stores";
      lua_pushnil(state);
      return 1;
    }


    const char* storeId = lua_tostring(state, 1);
    const char* key = lua_tostring(state, 2);
    const char* value = lua_tostring(state, 3);
    
    try
    {
      serverContext->GetIndex().StoreKeyValue(storeId, key, value);

      return 1;
    }
    catch (OrthancException& e)
    {
      LOG(ERROR) << "Lua: " << e.What();
    }

    LOG(ERROR) << "Lua: Error in StoreKeyValue() for storeId: " << storeId;
    lua_pushnil(state);

    return 1;
  }


  // Syntax in Lua: value = GetKeyValue(storeId, key)
  int LuaScripting::GetKeyValue(lua_State* state)
  {
    ServerContext* serverContext = GetServerContext(state);
    if (serverContext == NULL)
    {
      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
      lua_pushnil(state);
      return 1;
    }

    // Check the types of the arguments
    int nArgs = lua_gettop(state);
    if (nArgs != 2 ||
        !lua_isstring(state, 1) ||                 // storeId
        !lua_isstring(state, 2))                   // key
    {
      LOG(ERROR) << "Lua: Bad parameters to GetKeyValue()";
      lua_pushnil(state);
      return 1;
    }

    if (!serverContext->GetIndex().HasKeyValueStoresSupport())
    {
      LOG(ERROR) << "Lua: Unable to use GetKeyValue() when the DB backend does not support Key-Value stores";
      lua_pushnil(state);
      return 1;
    }


    const char* storeId = lua_tostring(state, 1);
    const char* key = lua_tostring(state, 2);
    
    try
    {
      std::string value;
      if (serverContext->GetIndex().GetKeyValue(value, storeId, key))
      {
        lua_pushlstring(state, value.c_str(), value.size());
      }
      else
      {
        lua_pushnil(state);
        return 1;
      }

      return 1;
    }
    catch (OrthancException& e)
    {
      LOG(ERROR) << "Lua: " << e.What();
    }

    LOG(ERROR) << "Lua: Error in GetKeyValue() for storeId: " << storeId;
    lua_pushnil(state);

    return 1;
  }


  // Syntax in Lua: DeleteKeyValue(storeId, key, value)
  int LuaScripting::DeleteKeyValue(lua_State* state)
  {
    ServerContext* serverContext = GetServerContext(state);
    if (serverContext == NULL)
    {
      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
      lua_pushnil(state);
      return 1;
    }

    // Check the types of the arguments
    int nArgs = lua_gettop(state);
    if (nArgs != 2 ||
        !lua_isstring(state, 1) ||                 // storeId
        !lua_isstring(state, 2))                   // key
    {
      LOG(ERROR) << "Lua: Bad parameters to DeleteKeyValue()";
      lua_pushnil(state);
      return 1;
    }

    if (!serverContext->GetIndex().HasKeyValueStoresSupport())
    {
      LOG(ERROR) << "Lua: Unable to use DeleteKeyValue() when the DB backend does not support Key-Value stores";
      lua_pushnil(state);
      return 1;
    }


    const char* storeId = lua_tostring(state, 1);
    const char* key = lua_tostring(state, 2);
    
    try
    {
      serverContext->GetIndex().DeleteKeyValue(storeId, key);

      return 1;
    }
    catch (OrthancException& e)
    {
      LOG(ERROR) << "Lua: " << e.What();
    }

    LOG(ERROR) << "Lua: Error in DeleteKeyValue() for storeId: " << storeId;
    lua_pushnil(state);

    return 1;
  }


  // Syntax in Lua: GetOrthancConfiguration()
  int LuaScripting::GetOrthancConfiguration(lua_State *state)
  {
    Json::Value configuration;

    {
      OrthancConfiguration::ReaderLock lock;
      configuration = lock.GetJson();
    }

    LuaContext::GetLuaContext(state).PushJson(configuration);

    return 1;
  }


  static size_t ParseOperation(ServerContext& context,
                               LuaJobManager::Lock& lock,
                               const std::string& operation,
                               const Json::Value& parameters)
  {
    if (operation == "delete")
    {
      LOG(INFO) << "Lua script to delete resource " << parameters["Resource"].asString();
      return lock.AddDeleteResourceOperation(context);
    }

    if (operation == "store-scu")
    {
      std::string localAet;
      if (parameters.isMember("LocalAet"))
      {
        localAet = parameters["LocalAet"].asString();
      }
      else
      {
        localAet = context.GetDefaultLocalApplicationEntityTitle();
      }

      std::string name = parameters["Modality"].asString();
      RemoteModalityParameters modality;

      {
        OrthancConfiguration::ReaderLock configLock;
        modality = configLock.GetConfiguration().GetModalityUsingSymbolicName(name);
      }

      // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()"
      return lock.AddStoreScuOperation(context, localAet, modality);
    }

    if (operation == "store-peer")
    {
      OrthancConfiguration::ReaderLock configLock;
      std::string name = parameters["Peer"].asString();

      WebServiceParameters peer;
      if (configLock.GetConfiguration().LookupOrthancPeer(peer, name))
      {
        return lock.AddStorePeerOperation(peer);
      }
      else
      {
        throw OrthancException(ErrorCode_UnknownResource,
                               "No peer with symbolic name: " + name);
      }
    }

    if (operation == "modify")
    {
      std::unique_ptr<DicomModification> modification(new DicomModification);
      modification->ParseModifyRequest(parameters);

      return lock.AddModifyInstanceOperation(context, modification.release());
    }

    if (operation == "call-system")
    {
      LOG(INFO) << "Lua script to call system command on " << parameters["Resource"].asString();

      const Json::Value& argsIn = parameters["Arguments"];
      if (argsIn.type() != Json::arrayValue)
      {
        throw OrthancException(ErrorCode_BadParameterType);
      }

      std::vector<std::string> args;
      args.reserve(argsIn.size());
      for (Json::Value::ArrayIndex i = 0; i < argsIn.size(); ++i)
      {
        // http://jsoncpp.sourceforge.net/namespace_json.html#7d654b75c16a57007925868e38212b4e
        switch (argsIn[i].type())
        {
          case Json::stringValue:
            args.push_back(argsIn[i].asString());
            break;

          case Json::intValue:
            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asInt()));
            break;

          case Json::uintValue:
            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asUInt()));
            break;

          case Json::realValue:
            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asFloat()));
            break;

          default:
            throw OrthancException(ErrorCode_BadParameterType);
        }
      }

      std::string command = parameters["Command"].asString();
      std::vector<std::string> postArgs;

      return lock.AddSystemCallOperation(command, args, postArgs);
    }

    throw OrthancException(ErrorCode_ParameterOutOfRange);
  }


  void LuaScripting::InitializeJob()
  {
    lua_.Execute("_InitializeJob()");
  }


  void LuaScripting::SubmitJob()
  {
    Json::Value operations;
    LuaFunctionCall call2(lua_, "_AccessJob");
    call2.ExecuteToJson(operations, false);
     
    if (operations.type() != Json::arrayValue)
    {
      THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
    }

    LuaJobManager::Lock lock(pimpl_->jobManager_, context_.GetJobsEngine());

    bool isFirst = true;
    size_t previous = 0;  // Dummy initialization to avoid warning

    for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i)
    {
      if (operations[i].type() != Json::objectValue ||
          !operations[i].isMember("Operation"))
      {
        THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
      }

      const Json::Value& parameters = operations[i];
      if (!parameters.isMember("Resource"))
      {
        THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
      }

      std::string operation = parameters["Operation"].asString();
      size_t index = ParseOperation(context_, lock, operation, operations[i]);
        
      std::string resource = parameters["Resource"].asString();
      if (!resource.empty())
      {
        lock.AddDicomInstanceInput(index, context_, resource);
      }
      else if (!isFirst)
      {
        lock.Connect(previous, index);
      }

      isFirst = false;
      previous = index;
    }
  }


  LuaScripting::LuaScripting(ServerContext& context) :
    pimpl_(new PImpl),
    context_(context),
    state_(State_Setup),
    heartBeatPeriod_(0)
  {
    lua_.SetGlobalVariable("_ServerContext", &context);
    lua_.RegisterFunction("RestApiGet", RestApiGet);
    lua_.RegisterFunction("RestApiPost", RestApiPost);
    lua_.RegisterFunction("RestApiPut", RestApiPut);
    lua_.RegisterFunction("RestApiDelete", RestApiDelete);
    lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration);
    lua_.RegisterFunction("SetStableStatus", SetStableStatus);
    lua_.RegisterFunction("StoreKeyValue", StoreKeyValue);
    lua_.RegisterFunction("GetKeyValue", GetKeyValue);
    lua_.RegisterFunction("DeleteKeyValue", DeleteKeyValue);

    LOG(INFO) << "Initializing Lua for the event handler";
    LoadGlobalConfiguration();
  }


  LuaScripting::~LuaScripting()
  {
    if (state_ == State_Running)
    {
      LOG(ERROR) << "INTERNAL ERROR: LuaScripting::Stop() should be invoked manually to avoid mess in the destruction order!";

      try
      {
        Stop();
      }
      catch (OrthancException& e)
      {
        // Don't throw exceptions in destructors
        LOG(ERROR) << "Exception in destructor: " << e.What();
      }
    }

    assert(pimpl_ != NULL);
    delete pimpl_;
  }

  void LuaScripting::HeartBeatThread(LuaScripting* that)
  {
    Logging::ScopedCurrentThreadNameSetter setter("LUA-HEARTBEAT");

    static const unsigned int GRANULARITY = 100;  // In milliseconds
    
    const boost::posix_time::time_duration PERIODICITY =
      boost::posix_time::seconds(that->heartBeatPeriod_);
    
    boost::posix_time::ptime next =
      boost::posix_time::microsec_clock::universal_time() + PERIODICITY;
    
    bool shouldStop = false;

    while (!shouldStop)
    {
      boost::this_thread::sleep(boost::posix_time::milliseconds(GRANULARITY));

      if (boost::posix_time::microsec_clock::universal_time() >= next)
      {
        LuaScripting::Lock lock(*that);

        if (lock.GetLua().IsExistingFunction(ON_HEART_BEAT))
        {
          LuaFunctionCall call(lock.GetLua(), ON_HEART_BEAT);
          call.Execute();
        }

        next = boost::posix_time::microsec_clock::universal_time() + PERIODICITY;
      }

      {
        boost::recursive_mutex::scoped_lock lock(that->mutex_);
        shouldStop = (that->state_ == State_Done);
      }
    }
  }

  void LuaScripting::EventThread(LuaScripting* that)
  {
    Logging::ScopedCurrentThreadNameSetter setter("LUA-EVENTS");

    for (;;)
    {
      std::unique_ptr<IDynamicObject> event(that->pendingEvents_.Dequeue(100));

      if (event.get() == NULL)
      {
        // The event queue is empty, check whether we should stop
        boost::recursive_mutex::scoped_lock lock(that->mutex_);

        if (that->state_ != State_Running)
        {
          return;
        }
      }
      else
      {
        try
        {
          dynamic_cast<IEvent&>(*event).Apply(*that);
        }
        catch (OrthancException& e)
        {
          LOG(ERROR) << "Error while processing Lua events: " << e.What();
        }
      }

      that->pimpl_->jobManager_.GetDicomConnectionManager().CloseIfInactive();
    }
  }


  void LuaScripting::Start()
  {
    boost::recursive_mutex::scoped_lock lock(mutex_);

    if (state_ != State_Setup ||
        eventThread_.joinable()  /* already started */)
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls);
    }
    else
    {
      LOG(INFO) << "Starting the Lua engine";
      eventThread_ = boost::thread(EventThread, this);
      
      LuaScripting::Lock luaLock(*this);

      if (heartBeatPeriod_ > 0 && luaLock.GetLua().IsExistingFunction(ON_HEART_BEAT))
      {
        LOG(INFO) << "Starting the Lua HeartBeat thread with a period of " << heartBeatPeriod_ << " seconds";
        heartBeatThread_ = boost::thread(HeartBeatThread, this);
      }
      state_ = State_Running;
    }
  }


  void LuaScripting::Stop()
  {
    {
      boost::recursive_mutex::scoped_lock lock(mutex_);

      if (state_ != State_Running)
      {
        throw OrthancException(ErrorCode_BadSequenceOfCalls);
      }

      state_ = State_Done;
    }

    pimpl_->jobManager_.AwakeTrailingSleep();

    if (eventThread_.joinable())
    {
      LOG(INFO) << "Stopping the Lua engine";
      eventThread_.join();
      if (heartBeatThread_.joinable())
      {
        heartBeatThread_.join();
      }
      LOG(INFO) << "The Lua engine has stopped";
    }
  }


  void LuaScripting::SignalStoredInstance(const std::string& publicId,
                                          const DicomInstanceToStore& instance,
                                          const Json::Value& simplifiedTags)
  {
    Json::Value metadata = Json::objectValue;

    for (ServerIndex::MetadataMap::const_iterator 
           it = instance.GetMetadata().begin(); 
         it != instance.GetMetadata().end(); ++it)
    {
      if (it->first.first == ResourceType_Instance)
      {
        metadata[EnumerationToString(it->first.second)] = it->second;
      }
    }

    pendingEvents_.Enqueue(new OnStoredInstanceEvent(publicId, simplifiedTags, metadata, instance));
  }


  void LuaScripting::SignalChange(const ServerIndexChange& change)
  {
    if (change.GetChangeType() == ChangeType_StablePatient ||
        change.GetChangeType() == ChangeType_StableStudy ||
        change.GetChangeType() == ChangeType_StableSeries)
    {
      pendingEvents_.Enqueue(new StableResourceEvent(change));
    }
    else if (change.GetChangeType() == ChangeType_Deleted)
    {
      pendingEvents_.Enqueue(new DeleteEvent(change.GetResourceType(), change.GetPublicId()));
    }
    else if (change.GetChangeType() == ChangeType_UpdatedAttachment ||
             change.GetChangeType() == ChangeType_UpdatedMetadata)
    {
      pendingEvents_.Enqueue(new UpdateEvent(change.GetResourceType(), change.GetPublicId()));
    }
  }


  bool LuaScripting::FilterIncomingInstance(const DicomInstanceToStore& instance,
                                            const Json::Value& simplified)
  {
    static const char* NAME = "ReceivedInstanceFilter";

    boost::recursive_mutex::scoped_lock lock(mutex_);

    if (lua_.IsExistingFunction(NAME))
    {
      LuaFunctionCall call(lua_, NAME);
      call.PushJson(simplified);

      Json::Value origin;
      instance.GetOrigin().Format(origin);
      call.PushJson(origin);

      Json::Value info = Json::objectValue;
      info["HasPixelData"] = instance.HasPixelData();

      DicomTransferSyntax s;
      if (instance.LookupTransferSyntax(s))
      {
        info["TransferSyntaxUID"] = GetTransferSyntaxUid(s);
      }

      call.PushJson(info);

      if (!call.ExecutePredicate())
      {
        return false;
      }
    }

    return true;
  }

  bool LuaScripting::FilterIncomingCStoreInstance(uint16_t& dimseStatus,
                                                  const DicomInstanceToStore& instance,
                                                  const Json::Value& simplified)
  {
    static const char* NAME = "ReceivedCStoreInstanceFilter";

    boost::recursive_mutex::scoped_lock lock(mutex_);

    if (lua_.IsExistingFunction(NAME))
    {
      LuaFunctionCall call(lua_, NAME);
      call.PushJson(simplified);

      Json::Value origin;
      instance.GetOrigin().Format(origin);
      call.PushJson(origin);

      Json::Value info = Json::objectValue;
      info["HasPixelData"] = instance.HasPixelData();

      DicomTransferSyntax s;
      if (instance.LookupTransferSyntax(s))
      {
        info["TransferSyntaxUID"] = GetTransferSyntaxUid(s);
      }

      call.PushJson(info);

      int result;
      call.ExecuteToInt(result);
      if (result > 0xFFFF || result < 0)
      {
        throw OrthancException(ErrorCode_ParameterOutOfRange, "The returned DIMSE status should be >= 0 and <= 0xFFFF");
      }

      dimseStatus = static_cast<uint16_t>(result);
      return dimseStatus == 0; // keep if DimseStatus is SUCCESS
    }

    return true; // keep
  }


  bool LuaScripting::FilterOutgoingCStoreInstance(const OutgoingDicomInstance& instance,
                                                  const Json::Value& simplified)
  {
    static const char* NAME = "OutgoingCStoreInstanceFilter";

    boost::recursive_mutex::scoped_lock lock(mutex_);

    if (lua_.IsExistingFunction(NAME))
    {
      LuaFunctionCall call(lua_, NAME);
      call.PushJson(simplified);

      Json::Value destination;
      instance.GetDestination().Format(destination);
      call.PushJson(destination);

      if (!call.ExecutePredicate())
      {
        return false;
      }
    }

    return true;
  }


  void LuaScripting::Execute(const std::string& command)
  {
    pendingEvents_.Enqueue(new ExecuteEvent(command));
  }


  void LuaScripting::LoadGlobalConfiguration()
  {
    OrthancConfiguration::ReaderLock configLock;

    {
      std::string command;
      Orthanc::ServerResources::GetFileResource(command, Orthanc::ServerResources::LUA_TOOLBOX);
      lua_.Execute(command);
    }    

    std::list<std::string> luaScripts;
    configLock.GetConfiguration().GetListOfStringsParameter(luaScripts, "LuaScripts");
    heartBeatPeriod_ = configLock.GetConfiguration().GetUnsignedIntegerParameter("LuaHeartBeatPeriod");

    LuaScripting::Lock lock(*this);

    for (std::list<std::string>::const_iterator
           it = luaScripts.begin(); it != luaScripts.end(); ++it)
    {
      boost::filesystem::path path = configLock.GetConfiguration().InterpretStringParameterAsPath(*it);
      LOG(INFO) << "Installing the Lua scripts from: " << SystemToolbox::PathToUtf8(path);
      std::string script;
      SystemToolbox::ReadFile(script, path);

      lock.GetLua().Execute(script);
    }
  }

  
  void LuaScripting::SignalJobEvent(const JobEvent& event)
  {
    // Lua has its own event thread and queue to dissociate it completely from the main JobEventsThread
    pendingEvents_.Enqueue(new LuaJobEvent(event));
  }


  TimeoutDicomConnectionManager& LuaScripting::GetDicomConnectionManager()
  {
    return pimpl_->jobManager_.GetDicomConnectionManager();
  }
}
