/**
 * 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-2025 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2025 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"

#if defined(_WIN32)
// "Please include winsock2.h before windows.h"
#  include <winsock2.h>
#endif

#if !defined(HAVE_MALLOPT)
#  error Macro HAVE_MALLOPT must be defined
#endif

#if HAVE_MALLOPT == 1
#  include <malloc.h>
#endif


// This must be *before* the inclusion of "Logging.h"
#if defined(__ORTHANC_FILE__)
// Prevents the system-wide Google Protobuf library from leaking the
// full path of this source file
#  undef __FILE__
#  define __FILE__ __ORTHANC_FILE__
#endif


#if ORTHANC_ENABLE_PLUGINS == 1
#  include <google/protobuf/stubs/common.h>
#  include <google/protobuf/any.h>
#endif


#include "OrthancInitialization.h"

#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
#include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h"
#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
#include "../../OrthancFramework/Sources/HttpClient.h"
#include "../../OrthancFramework/Sources/Logging.h"
#include "../../OrthancFramework/Sources/OrthancException.h"
#include "../../OrthancFramework/Sources/SerializationToolbox.h"

#include "Database/SQLiteDatabaseWrapper.h"
#include "OrthancConfiguration.h"

#include <OrthancServerResources.h>

#include <dcmtk/dcmnet/diutil.h>  // For DCM_dcmnetLogger



static const char* const STORAGE_DIRECTORY = "StorageDirectory";
static const char* const ORTHANC_STORAGE = "OrthancStorage";


namespace Orthanc
{
  static void RegisterUserMetadata(const Json::Value& config)
  {
    static const char* const USER_METADATA = "UserMetadata";
    
    if (config.isMember(USER_METADATA))
    {
      const Json::Value& parameter = config[USER_METADATA];

      Json::Value::Members members = parameter.getMemberNames();
      for (size_t i = 0; i < members.size(); i++)
      {
        const std::string& name = members[i];

        if (!parameter[name].isInt())
        {
          throw OrthancException(ErrorCode_BadParameterType,
                                 "Not a number in this user-defined metadata: " + name);
        }

        int metadata = parameter[name].asInt();        

        LOG(INFO) << "Registering user-defined metadata: " << name << " (index " 
                  << metadata << ")";

        try
        {
          RegisterUserMetadata(metadata, name);
        }
        catch (OrthancException&)
        {
          LOG(ERROR) << "Cannot register this user-defined metadata: " << name;
          throw;
        }
      }
    }
  }


  static void RegisterUserContentType(const Json::Value& config)
  {
    static const char* const USER_CONTENT_TYPE = "UserContentType";
    
    if (config.isMember(USER_CONTENT_TYPE))
    {
      const Json::Value& parameter = config[USER_CONTENT_TYPE];

      Json::Value::Members members = parameter.getMemberNames();
      for (size_t i = 0; i < members.size(); i++)
      {
        const std::string& name = members[i];
        std::string mime = MIME_BINARY;

        const Json::Value& value = parameter[name];
        int contentType;

        if (value.isArray() &&
            value.size() == 2 &&
            value[0].isInt() &&
            value[1].isString())
        {
          contentType = value[0].asInt();
          mime = value[1].asString();
        }
        else if (value.isInt())
        {
          contentType = value.asInt();
        }
        else
        {
          throw OrthancException(ErrorCode_BadParameterType,
                                 "Not a number in this user-defined attachment type: " + name);
        }

        LOG(INFO) << "Registering user-defined attachment type: " << name << " (index " 
                  << contentType << ") with MIME type \"" << mime << "\"";

        try
        {
          RegisterUserContentType(contentType, name, mime);
        }
        catch (OrthancException&)
        {
          throw;
        }
      }
    }
  }


  static void LoadExternalDictionaries(const Json::Value& configuration)
  {
    static const char* const EXTERNAL_DICTIONARIES = "ExternalDictionaries";
    
    if (configuration.type() == Json::objectValue &&
        configuration.isMember(EXTERNAL_DICTIONARIES))
    {
      std::vector<std::string> dictionaries;
      SerializationToolbox::ReadArrayOfStrings(dictionaries, configuration, EXTERNAL_DICTIONARIES);
      FromDcmtkBridge::LoadExternalDictionaries(dictionaries);
    }
  }


  static void LoadCustomDictionary(const Json::Value& configuration)
  {
    static const char* const DICTIONARY = "Dictionary";
    
    if (configuration.type() != Json::objectValue ||
        !configuration.isMember(DICTIONARY) ||
        configuration[DICTIONARY].type() != Json::objectValue)
    {
      return;
    }

    Json::Value::Members tags(configuration[DICTIONARY].getMemberNames());

    for (Json::Value::ArrayIndex i = 0; i < tags.size(); i++)
    {
      const Json::Value& content = configuration[DICTIONARY][tags[i]];
      if (content.type() != Json::arrayValue ||
          content.size() < 2 ||
          content.size() > 5 ||
          content[0].type() != Json::stringValue ||
          content[1].type() != Json::stringValue ||
          (content.size() >= 3 && content[2].type() != Json::intValue) ||
          (content.size() >= 4 && content[3].type() != Json::intValue) ||
          (content.size() >= 5 && content[4].type() != Json::stringValue))
      {
        throw OrthancException(ErrorCode_BadFileFormat, "The definition of the '" + tags[i] + "' dictionary entry is invalid.");
      }

      DicomTag tag(FromDcmtkBridge::ParseTag(tags[i]));
      ValueRepresentation vr = StringToValueRepresentation(content[0].asString(), true);
      std::string name = content[1].asString();
      unsigned int minMultiplicity = (content.size() >= 2) ? content[2].asUInt() : 1;
      unsigned int maxMultiplicity = (content.size() >= 3) ? content[3].asUInt() : 1;
      std::string privateCreator = (content.size() >= 4) ? content[4].asString() : "";

      FromDcmtkBridge::RegisterDictionaryTag(tag, vr, name, minMultiplicity, maxMultiplicity, privateCreator);
    }
  }

  static void LoadMainDicomTags(const Json::Value& configuration)
  {
    static const char* const EXTRA_MAIN_DICOM_TAGS = "ExtraMainDicomTags";
    
    DicomMap::ResetDefaultMainDicomTags();

    if (configuration.type() != Json::objectValue ||
        !configuration.isMember(EXTRA_MAIN_DICOM_TAGS) ||
        configuration[EXTRA_MAIN_DICOM_TAGS].type() != Json::objectValue)
    {
      return;
    }

    Json::Value::Members levels(configuration[EXTRA_MAIN_DICOM_TAGS].getMemberNames());

    for (Json::Value::ArrayIndex i = 0; i < levels.size(); i++)
    {
      ResourceType level = StringToResourceType(levels[i].c_str());

      const Json::Value& content = configuration[EXTRA_MAIN_DICOM_TAGS][levels[i]];

      if (content.type() != Json::arrayValue)
      {
        throw OrthancException(ErrorCode_BadFileFormat, "The definition of the '" + levels[i] + "' ExtraMainDicomTags entry is invalid (not an array).");
      }

      if (content.size() > 0)
      {
        LOG(INFO) << "Configured Extra Main Dicom Tags for " << levels[i] << ":";

        for (Json::Value::ArrayIndex t = 0; t < content.size(); t++)
        {
          const std::string& tagName = content[t].asString();
          DicomTag tag(FromDcmtkBridge::ParseTag(tagName));

          if (DicomMap::IsComputedTag(tag))
          {
            LOG(WARNING) << "  - " << tagName << " cannot be added in the Extra Main Dicom Tags since the value of this tag is computed when requested";
          }
          else
          {
            ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
            if (vr == ValueRepresentation_Sequence)
            {
              LOG(INFO) << "  - " << tagName << " (sequence)";
            }
            else
            {
              LOG(INFO) << "  - " << tagName;
            }

            try
            {
              DicomMap::AddMainDicomTag(tag, level);
            }
            catch(OrthancException& e)
            {
              LOG(WARNING) << "  - !!! " << tagName << " is already defined as a standard MainDicomTags, it is useless to include it in the ExtraMainDicomTags";
            }
            
            
          }
        }
      }
    }
  }

  static void ConfigurePkcs11(const Json::Value& config)
  {
    static const char* const MODULE = "Module";
    static const char* const VERBOSE = "Verbose";
    static const char* const PIN = "Pin";
    
    if (config.type() != Json::objectValue ||
        !config.isMember(MODULE) ||
        config[MODULE].type() != Json::stringValue)
    {
      throw OrthancException(ErrorCode_BadFileFormat,
                             "No path to the PKCS#11 module (DLL or .so) is provided "
                             "for HTTPS client authentication");
    }

    std::string pin;
    if (config.isMember(PIN))
    {
      if (config[PIN].type() == Json::stringValue)
      {
        pin = config[PIN].asString();
      }
      else
      {
        throw OrthancException(ErrorCode_BadFileFormat,
                               "The PIN number in the PKCS#11 configuration must be a string");
      }
    }

    bool verbose = false;
    if (config.isMember(VERBOSE))
    {
      if (config[VERBOSE].type() == Json::booleanValue)
      {
        verbose = config[VERBOSE].asBool();
      }
      else
      {
        throw OrthancException(ErrorCode_BadFileFormat,
                               "The Verbose option in the PKCS#11 configuration must be a Boolean");
      }
    }

    HttpClient::InitializePkcs11(config[MODULE].asString(), pin, verbose);
  }



  void OrthancInitialize(const char* configurationFile)
  {
    static const char* const LOCALE = "Locale";
    static const char* const PKCS11 = "Pkcs11";
    static const char* const DEFAULT_ENCODING = "DefaultEncoding";
    static const char* const MALLOC_ARENA_MAX = "MallocArenaMax";
    static const char* const LOAD_PRIVATE_DICTIONARY = "LoadPrivateDictionary";
    
    OrthancConfiguration::WriterLock lock;

#if ORTHANC_ENABLE_PLUGINS == 1
    GOOGLE_PROTOBUF_VERIFY_VERSION;
#endif
    
    InitializeServerEnumerations();

    // Read the user-provided configuration
    lock.GetConfiguration().Read(configurationFile);

    {
      std::string locale;
      
      if (lock.GetJson().isMember(LOCALE))
      {
        locale = lock.GetConfiguration().GetStringParameter(LOCALE, "");
      }
      
      bool loadPrivate = lock.GetConfiguration().GetBooleanParameter(LOAD_PRIVATE_DICTIONARY, true);
      Orthanc::InitializeFramework(locale, loadPrivate);
    }

    // The Orthanc framework is now initialized

    if (lock.GetJson().isMember(DEFAULT_ENCODING))
    {
      std::string encoding = lock.GetConfiguration().GetStringParameter(DEFAULT_ENCODING, "");
      SetDefaultDicomEncoding(StringToEncoding(encoding.c_str()));
    }
    else
    {
      SetDefaultDicomEncoding(ORTHANC_DEFAULT_DICOM_ENCODING);
    }

    if (lock.GetJson().isMember(PKCS11))
    {
      ConfigurePkcs11(lock.GetJson()[PKCS11]);
    }

    RegisterUserMetadata(lock.GetJson());
    RegisterUserContentType(lock.GetJson());

    LoadExternalDictionaries(lock.GetJson());  // New in Orthanc 1.9.4
    LoadCustomDictionary(lock.GetJson());

    lock.GetConfiguration().LoadWarnings();
    lock.GetConfiguration().LoadJobsEngineThreadsCount();

    LoadMainDicomTags(lock.GetJson());  // New in Orthanc 1.11.0

    lock.GetConfiguration().RegisterFont(ServerResources::FONT_UBUNTU_MONO_BOLD_16);

#if HAVE_MALLOPT == 1
    // New in Orthanc 1.8.2
    // https://orthanc.uclouvain.be/book/faq/scalability.html#controlling-memory-usage
    unsigned int maxArena = lock.GetConfiguration().GetUnsignedIntegerParameter(MALLOC_ARENA_MAX, 5);
    if (maxArena != 0)
    {
      // https://man7.org/linux/man-pages/man3/mallopt.3.html
      LOG(INFO) << "Calling mallopt(M_ARENA_MAX, " << maxArena << ")";
      if (mallopt(M_ARENA_MAX, maxArena) != 1 /* success */)
      {
        throw OrthancException(ErrorCode_InternalError, "The call to mallopt(M_ARENA_MAX, " +
                               boost::lexical_cast<std::string>(maxArena) + ") has failed");
      }
    }
#else
    if (lock.GetJson().isMember(MALLOC_ARENA_MAX))
    {
      LOG(INFO) << "Your platform does not support mallopt(), ignoring configuration option \""
                << MALLOC_ARENA_MAX << "\"";
    }
#endif
  }



  void OrthancFinalize()
  {
    OrthancConfiguration::WriterLock lock;
    Orthanc::FinalizeFramework();

#if ORTHANC_ENABLE_PLUGINS == 1
    google::protobuf::ShutdownProtobufLibrary();
#endif
  }


  static IDatabaseWrapper* CreateSQLiteWrapper()
  {
    OrthancConfiguration::ReaderLock lock;

    std::string storageDirectoryStr = 
      lock.GetConfiguration().GetStringParameter(STORAGE_DIRECTORY, ORTHANC_STORAGE);

    // Open the database
    boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(
      lock.GetConfiguration().GetStringParameter("IndexDirectory", storageDirectoryStr));

    LOG(WARNING) << "SQLite index directory: " << indexDirectory;

    try
    {
      boost::filesystem::create_directories(indexDirectory);
    }
    catch (boost::filesystem::filesystem_error&)
    {
    }

    return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index");
  }


  namespace
  {
    // Anonymous namespace to avoid clashes between compilation modules

    class FilesystemStorageWithoutDicom : public IStorageArea
    {
    private:
      FilesystemStorage storage_;

    public:
      FilesystemStorageWithoutDicom(const std::string& path,
                                    bool fsyncOnWrite) :
        storage_(path, fsyncOnWrite)
      {
      }

      virtual void Create(const std::string& uuid,
                          const void* content, 
                          size_t size,
                          FileContentType type) ORTHANC_OVERRIDE
      {
        if (type != FileContentType_Dicom)
        {
          storage_.Create(uuid, content, size, type);
        }
      }

      virtual IMemoryBuffer* ReadRange(const std::string& uuid,
                                       FileContentType type,
                                       uint64_t start /* inclusive */,
                                       uint64_t end /* exclusive */) ORTHANC_OVERRIDE
      {
        if (type != FileContentType_Dicom)
        {
          return storage_.ReadRange(uuid, type, start, end);
        }
        else
        {
          throw OrthancException(ErrorCode_UnknownResource);
        }
      }

      virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
      {
        return storage_.HasEfficientReadRange();
      }

      virtual void Remove(const std::string& uuid,
                          FileContentType type) ORTHANC_OVERRIDE
      {
        if (type != FileContentType_Dicom)
        {
          storage_.Remove(uuid, type);
        }
      }
    };
  }


  static IPluginStorageArea* CreateFilesystemStorage()
  {
    static const char* const SYNC_STORAGE_AREA = "SyncStorageArea";
    static const char* const STORE_DICOM = "StoreDicom";
    
    OrthancConfiguration::ReaderLock lock;

    std::string storageDirectoryStr = 
      lock.GetConfiguration().GetStringParameter(STORAGE_DIRECTORY, ORTHANC_STORAGE);

    boost::filesystem::path storageDirectory = 
      lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr);

    LOG(WARNING) << "Storage directory: " << storageDirectory;

    // New in Orthanc 1.7.4
    bool fsyncOnWrite = lock.GetConfiguration().GetBooleanParameter(SYNC_STORAGE_AREA, true);

    if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM, true))
    {
      return new PluginStorageAreaAdapter(new FilesystemStorage(storageDirectory.string(), fsyncOnWrite));
    }
    else
    {
      LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode";
      return new PluginStorageAreaAdapter(new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite));
    }
  }


  IDatabaseWrapper* CreateDatabaseWrapper()
  {
    return CreateSQLiteWrapper();
  }


  IPluginStorageArea* CreateStorageArea()
  {
    return CreateFilesystemStorage();
  }


  static void SetDcmtkVerbosity(Verbosity verbosity)
  {
    // INFO_LOG_LEVEL was the DCMTK log level in Orthanc <= 1.8.0    
    // https://support.dcmtk.org/docs-dcmrt/classOFLogger.html#ae20bf2616f15313c1f089da2eefb8245

    OFLogger::LogLevel dataLevel, networkLevel;

    switch (verbosity)
    {
      case Verbosity_Default:
        // Turn off logging in DCMTK core
        dataLevel = OFLogger::OFF_LOG_LEVEL;
        networkLevel = OFLogger::OFF_LOG_LEVEL;
        break;

      case Verbosity_Verbose:
        dataLevel = OFLogger::INFO_LOG_LEVEL;
        networkLevel = OFLogger::INFO_LOG_LEVEL;
        break;

      case Verbosity_Trace:
        dataLevel = OFLogger::INFO_LOG_LEVEL;  // DEBUG here makes DCMTK too verbose to be useful
        networkLevel = OFLogger::DEBUG_LOG_LEVEL;
        break;

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }

    OFLog::configure(dataLevel);
    assert(dcmtk::log4cplus::Logger::getRoot().getChainedLogLevel() == dataLevel);
    
    DCM_dcmdataLogger.setLogLevel(dataLevel);    // This seems to be implied by "OFLog::configure()"
    DCM_dcmnetLogger.setLogLevel(networkLevel);  // This will display PDU in DICOM networking
  }


  void SetGlobalVerbosity(Verbosity verbosity)
  {
    SetDcmtkVerbosity(verbosity);
    
    switch (verbosity)
    {
      case Verbosity_Default:
        Logging::EnableInfoLevel(false);
        Logging::EnableTraceLevel(false);
        break;

      case Verbosity_Verbose:
        Logging::EnableInfoLevel(true);
        Logging::EnableTraceLevel(false);
        break;

      case Verbosity_Trace:
        Logging::EnableInfoLevel(true);
        Logging::EnableTraceLevel(true);
        break;

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }

  
  Verbosity GetGlobalVerbosity()
  {
    if (Logging::IsTraceLevelEnabled())
    {
      return Verbosity_Trace;
    }
    else if (Logging::IsInfoLevelEnabled())
    {
      return Verbosity_Verbose;
    }
    else
    {
      return Verbosity_Default;
    }
  }

  
  void SetCategoryVerbosity(Logging::LogCategory category,
                            Verbosity verbosity)
  {
    switch (verbosity)
    {
      case Verbosity_Default:
        Logging::SetCategoryEnabled(Logging::LogLevel_INFO, category, false);
        Logging::SetCategoryEnabled(Logging::LogLevel_TRACE, category, false);
        break;

      case Verbosity_Verbose:
        Logging::SetCategoryEnabled(Logging::LogLevel_INFO, category, true);
        Logging::SetCategoryEnabled(Logging::LogLevel_TRACE, category, false);
        break;

      case Verbosity_Trace:
        Logging::SetCategoryEnabled(Logging::LogLevel_INFO, category, true);
        Logging::SetCategoryEnabled(Logging::LogLevel_TRACE, category, true);
        break;

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }

    if (category == Logging::LogCategory_DICOM)
    {
      SetDcmtkVerbosity(verbosity);
    }
  }
  

  Verbosity GetCategoryVerbosity(Logging::LogCategory category)
  {
    if (Logging::IsCategoryEnabled(Logging::LogLevel_TRACE, category))
    {
      return Verbosity_Trace;
    }
    else if (Logging::IsCategoryEnabled(Logging::LogLevel_INFO, category))
    {
      return Verbosity_Verbose;
    }
    else
    {
      return Verbosity_Default;
    }
  }
}
