/**
 * 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 "ServerEnumerations.h"

#include "../../OrthancFramework/Sources/OrthancException.h"
#include "../../OrthancFramework/Sources/EnumerationDictionary.h"
#include "../../OrthancFramework/Sources/Logging.h"
#include "../../OrthancFramework/Sources/Toolbox.h"

#include <boost/thread.hpp>

namespace Orthanc
{
  typedef std::map<FileContentType, std::string>  MimeTypes;

  static boost::mutex enumerationsMutex_;
  static EnumerationDictionary<MetadataType> dictMetadataType_;
  static EnumerationDictionary<FileContentType> dictContentType_;
  static MimeTypes  mimeTypes_;

  void InitializeServerEnumerations()
  {
    boost::mutex::scoped_lock lock(enumerationsMutex_);

    dictMetadataType_.Clear();
    dictContentType_.Clear();
    
    dictMetadataType_.Add(MetadataType_Instance_IndexInSeries, "IndexInSeries");
    dictMetadataType_.Add(MetadataType_Instance_ReceptionDate, "ReceptionDate");
    dictMetadataType_.Add(MetadataType_RemoteAet, "RemoteAET");
    dictMetadataType_.Add(MetadataType_Series_ExpectedNumberOfInstances, "ExpectedNumberOfInstances");
    dictMetadataType_.Add(MetadataType_ModifiedFrom, "ModifiedFrom");
    dictMetadataType_.Add(MetadataType_AnonymizedFrom, "AnonymizedFrom");
    dictMetadataType_.Add(MetadataType_LastUpdate, "LastUpdate");
    dictMetadataType_.Add(MetadataType_Instance_Origin, "Origin");
    dictMetadataType_.Add(MetadataType_Instance_TransferSyntax, "TransferSyntax");
    dictMetadataType_.Add(MetadataType_Instance_SopClassUid, "SopClassUid");
    dictMetadataType_.Add(MetadataType_Instance_RemoteIp, "RemoteIP");
    dictMetadataType_.Add(MetadataType_Instance_CalledAet, "CalledAET");
    dictMetadataType_.Add(MetadataType_Instance_HttpUsername, "HttpUsername");
    dictMetadataType_.Add(MetadataType_Instance_PixelDataOffset, "PixelDataOffset");
    dictMetadataType_.Add(MetadataType_MainDicomTagsSignature, "MainDicomTagsSignature");
    dictMetadataType_.Add(MetadataType_MainDicomSequences, "MainDicomSequences");
    dictMetadataType_.Add(MetadataType_Instance_PixelDataVR, "PixelDataVR");
    dictMetadataType_.Add(MetadataType_Patient_IsProtected, "IsProtected");
    dictMetadataType_.Add(MetadataType_Patient_PatientRecyclingOrder, "PatientRecyclingOrder");

    dictContentType_.Add(FileContentType_Dicom, "dicom");
    dictContentType_.Add(FileContentType_DicomAsJson, "dicom-as-json");
    dictContentType_.Add(FileContentType_DicomUntilPixelData, "dicom-until-pixel-data");
  }

  void RegisterUserMetadata(int metadata,
                            const std::string& name)
  {
    boost::mutex::scoped_lock lock(enumerationsMutex_);

    MetadataType type = static_cast<MetadataType>(metadata);

    if (metadata < 0 || 
        !IsUserMetadata(type))
    {
      LOG(ERROR) << "A user content type must have index between "
                 << static_cast<int>(MetadataType_StartUser) << " and "
                 << static_cast<int>(MetadataType_EndUser) << ", but \""
                 << name << "\" has index " << metadata;
        
      throw OrthancException(ErrorCode_ParameterOutOfRange);
    }

    if (dictMetadataType_.Contains(type))
    {
      LOG(ERROR) << "Cannot associate user content type \""
                 << name << "\" with index " << metadata 
                 << ", as this index is already used";
        
      throw OrthancException(ErrorCode_ParameterOutOfRange);
    }

    dictMetadataType_.Add(type, name);
  }

  std::string EnumerationToString(MetadataType type)
  {
    // This function MUST return a "std::string" and not "const
    // char*", as the result is not a static string
    boost::mutex::scoped_lock lock(enumerationsMutex_);
    return dictMetadataType_.Translate(type);
  }

  MetadataType StringToMetadata(const std::string& str)
  {
    boost::mutex::scoped_lock lock(enumerationsMutex_);
    return dictMetadataType_.Translate(str);
  }

  void GetRegisteredUserMetadata(std::map<std::string, int>& allEntries)
  {
    boost::mutex::scoped_lock lock(enumerationsMutex_);

    allEntries.clear();

    std::map<std::string, MetadataType> allEntriesTyped = dictMetadataType_.GetAllEntries();

    for (std::map<std::string, MetadataType>::const_iterator it = allEntriesTyped.begin(); it != allEntriesTyped.end(); ++it)
    {
      if (it->second >= MetadataType_StartUser)
      {
        allEntries[it->first] = it->second;
      }
    }
  }

  void RegisterUserContentType(int contentType,
                               const std::string& name,
                               const std::string& mime)
  {
    boost::mutex::scoped_lock lock(enumerationsMutex_);

    FileContentType type = static_cast<FileContentType>(contentType);

    if (contentType < 0 || 
        !IsUserContentType(type))
    {
      LOG(ERROR) << "A user content type must have index between "
                 << static_cast<int>(FileContentType_StartUser) << " and "
                 << static_cast<int>(FileContentType_EndUser) << ", but \""
                 << name << "\" has index " << contentType;
        
      throw OrthancException(ErrorCode_ParameterOutOfRange);
    }

    if (dictContentType_.Contains(type))
    {
      LOG(ERROR) << "Cannot associate user content type \""
                 << name << "\" with index " << contentType 
                 << ", as this index is already used";
        
      throw OrthancException(ErrorCode_ParameterOutOfRange);
    }

    dictContentType_.Add(type, name);
    mimeTypes_[type] = mime;
  }

  std::string EnumerationToString(FileContentType type)
  {
    // This function MUST return a "std::string" and not "const
    // char*", as the result is not a static string
    boost::mutex::scoped_lock lock(enumerationsMutex_);
    return dictContentType_.Translate(type);
  }

  std::string GetFileContentMime(FileContentType type)
  {
    if (type >= FileContentType_StartUser &&
        type <= FileContentType_EndUser)
    {
      boost::mutex::scoped_lock lock(enumerationsMutex_);
      
      MimeTypes::const_iterator it = mimeTypes_.find(type);
      if (it != mimeTypes_.end())
      {
        return it->second;
      }
    }

    switch (type)
    {
      case FileContentType_Dicom:
        return EnumerationToString(MimeType_Dicom);

      case FileContentType_DicomAsJson:
        return MIME_JSON_UTF8;

      default:
        return EnumerationToString(MimeType_Binary);
    }
  }

  FileContentType StringToContentType(const std::string& str)
  {
    boost::mutex::scoped_lock lock(enumerationsMutex_);
    return dictContentType_.Translate(str);
  }


  FindStorageAccessMode StringToFindStorageAccessMode(const std::string& value)
  {
    if (value == "Always")
    {
      return FindStorageAccessMode_DiskOnLookupAndAnswer;
    }
    else if (value == "Never")
    {
      return FindStorageAccessMode_DatabaseOnly;
    }
    else if (value == "Answers")
    {
      return FindStorageAccessMode_DiskOnAnswer;
    }
    else
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange,
                             "Configuration option \"StorageAccessOnFind\" "
                             "should be \"Always\", \"Never\" or \"Answers\": " + value);
    }    
  }

  bool IsStorageAccessAllowedForAnswers(FindStorageAccessMode mode)
  {
    return mode != FindStorageAccessMode_DatabaseOnly;
  }

  bool IsStorageAccessAllowedForLookup(FindStorageAccessMode mode)
  {
    return mode == FindStorageAccessMode_DiskOnLookupAndAnswer;
  }

  MaxStorageMode StringToMaxStorageMode(const std::string& value)
  {
    if (value == "Recycle")
    {
      return MaxStorageMode_Recycle;
    }
    else if (value == "Reject")
    {
      return MaxStorageMode_Reject;
    }
    else
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange,
                             "Configuration option \"MaxStorageMode\" "
                             "should be \"Recycle\" or \"Reject\": " + value);
    }    
  }

  BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& value)
  {
    if (value == "Before")
    {
      return BuiltinDecoderTranscoderOrder_Before;
    }
    else if (value == "After")
    {
      return BuiltinDecoderTranscoderOrder_After;
    }
    else if (value == "Disabled")
    {
      return BuiltinDecoderTranscoderOrder_Disabled;
    }
    else
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange,
                             "Configuration option \"BuiltinDecoderTranscoderOrder\" "
                             "should be \"After\", \"Before\" or \"Disabled\": " + value);
    }    
  }


  Verbosity StringToVerbosity(const std::string& str)
  {
    if (str == "default")
    {
      return Verbosity_Default;
    }
    else if (str == "verbose")
    {
      return Verbosity_Verbose;
    }
    else if (str == "trace")
    {
      return Verbosity_Trace;
    }
    else
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange,
                             "Verbosity can be \"default\", \"verbose\" or \"trace\": " + str);
    }    
  }
  

  std::string GetBasePath(ResourceType type,
                          const std::string& publicId)
  {
    switch (type)
    {
      case ResourceType_Patient:
        return "/patients/" + publicId;

      case ResourceType_Study:
        return "/studies/" + publicId;

      case ResourceType_Series:
        return "/series/" + publicId;

      case ResourceType_Instance:
        return "/instances/" + publicId;
      
      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }

  const char* EnumerationToString(SeriesStatus status)
  {
    switch (status)
    {
      case SeriesStatus_Complete:
        return "Complete";

      case SeriesStatus_Missing:
        return "Missing";

      case SeriesStatus_Inconsistent:
        return "Inconsistent";

      case SeriesStatus_Unknown:
        return "Unknown";

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }

  const char* EnumerationToString(StoreStatus status)
  {
    switch (status)
    {
      case StoreStatus_Success:
        return "Success";

      case StoreStatus_AlreadyStored:
        return "AlreadyStored";

      case StoreStatus_Failure:
        return "Failure";

      case StoreStatus_FilteredOut:
        return "FilteredOut";

      case StoreStatus_StorageFull:
        return "StorageFull";

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }


  const char* EnumerationToString(ChangeType type)
  {
    switch (type)
    {
      case ChangeType_CompletedSeries:
        return "CompletedSeries";

      case ChangeType_NewInstance:
        return "NewInstance";

      case ChangeType_NewPatient:
        return "NewPatient";

      case ChangeType_NewSeries:
        return "NewSeries";

      case ChangeType_NewStudy:
        return "NewStudy";

      case ChangeType_AnonymizedStudy:
        return "AnonymizedStudy";

      case ChangeType_AnonymizedSeries:
        return "AnonymizedSeries";

      case ChangeType_ModifiedStudy:
        return "ModifiedStudy";

      case ChangeType_ModifiedSeries:
        return "ModifiedSeries";

      case ChangeType_AnonymizedPatient:
        return "AnonymizedPatient";

      case ChangeType_ModifiedPatient:
        return "ModifiedPatient";

      case ChangeType_StablePatient:
        return "StablePatient";

      case ChangeType_StableStudy:
        return "StableStudy";

      case ChangeType_StableSeries:
        return "StableSeries";

      case ChangeType_Deleted:
        return "Deleted";

      case ChangeType_NewChildInstance:
        return "NewChildInstance";

      case ChangeType_UpdatedAttachment:
        return "UpdatedAttachment";

      case ChangeType_UpdatedMetadata:
        return "UpdatedMetadata";

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }

  ChangeType StringToChangeType(const std::string& value)
  {
    if (value == "CompletedSeries")
    {
      return ChangeType_CompletedSeries;
    }
    else if (value == "NewInstance")
    {
      return ChangeType_NewInstance;
    }
    else if (value == "NewPatient")
    {
      return ChangeType_NewPatient;
    }
    else if (value == "NewSeries")
    {
      return ChangeType_NewSeries;
    }
    else if (value == "NewStudy")
    {
      return ChangeType_NewStudy;
    }
    else if (value == "AnonymizedStudy")
    {
      return ChangeType_AnonymizedStudy;
    }
    else if (value == "AnonymizedSeries")
    {
      return ChangeType_AnonymizedSeries;
    }
    else if (value == "ModifiedStudy")
    {
      return ChangeType_ModifiedStudy;
    }
    else if (value == "ModifiedSeries")
    {
      return ChangeType_ModifiedSeries;
    }
    else if (value == "AnonymizedPatient")
    {
      return ChangeType_AnonymizedPatient;
    }
    else if (value == "ModifiedPatient")
    {
      return ChangeType_ModifiedPatient;
    }
    else if (value == "StablePatient")
    {
      return ChangeType_StablePatient;
    }
    else if (value == "StableStudy")
    {
      return ChangeType_StableStudy;
    }
    else if (value == "StableSeries")
    {
      return ChangeType_StableSeries;
    }
    else if (value == "Deleted")
    {
      return ChangeType_Deleted;
    }
    else if (value == "NewChildInstance")
    {
      return ChangeType_NewChildInstance;
    }
    else if (value == "UpdatedAttachment")
    {
      return ChangeType_UpdatedAttachment;
    }
    else if (value == "UpdatedMetadata")
    {
      return ChangeType_UpdatedMetadata;
    }
    else
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange, "Invalid value for a change: " + value);
    }
  }


  const char* EnumerationToString(Verbosity verbosity)
  {
    switch (verbosity)
    {
      case Verbosity_Default:
        return "default";
        
      case Verbosity_Verbose:
        return "verbose";
        
      case Verbosity_Trace:
        return "trace";

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }    

  
  bool IsUserMetadata(MetadataType metadata)
  {
    return (metadata >= MetadataType_StartUser &&
            metadata <= MetadataType_EndUser);
  }


  void GetTransferSyntaxGroup(std::set<DicomTransferSyntax>& target,
                              TransferSyntaxGroup source)
  {
    target.clear();

    switch (source)
    {    
      // Transfer syntaxes supported since Orthanc 0.7.2
      case TransferSyntaxGroup_Deflated:
        target.insert(DicomTransferSyntax_DeflatedLittleEndianExplicit);
        break;
        
      case TransferSyntaxGroup_Jpeg:
        target.insert(DicomTransferSyntax_JPEGProcess1);
        target.insert(DicomTransferSyntax_JPEGProcess2_4);
        target.insert(DicomTransferSyntax_JPEGProcess3_5);
        target.insert(DicomTransferSyntax_JPEGProcess6_8);
        target.insert(DicomTransferSyntax_JPEGProcess7_9);
        target.insert(DicomTransferSyntax_JPEGProcess10_12);
        target.insert(DicomTransferSyntax_JPEGProcess11_13);
        target.insert(DicomTransferSyntax_JPEGProcess14);
        target.insert(DicomTransferSyntax_JPEGProcess15);
        target.insert(DicomTransferSyntax_JPEGProcess16_18);
        target.insert(DicomTransferSyntax_JPEGProcess17_19);
        target.insert(DicomTransferSyntax_JPEGProcess20_22);
        target.insert(DicomTransferSyntax_JPEGProcess21_23);
        target.insert(DicomTransferSyntax_JPEGProcess24_26);
        target.insert(DicomTransferSyntax_JPEGProcess25_27);
        target.insert(DicomTransferSyntax_JPEGProcess28);
        target.insert(DicomTransferSyntax_JPEGProcess29);
        target.insert(DicomTransferSyntax_JPEGProcess14SV1);
        break;

      case TransferSyntaxGroup_Jpeg2000:
        target.insert(DicomTransferSyntax_JPEG2000);
        target.insert(DicomTransferSyntax_JPEG2000LosslessOnly);
        target.insert(DicomTransferSyntax_JPEG2000Multicomponent);
        target.insert(DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly);
        break;

      case TransferSyntaxGroup_JpegLossless:
        target.insert(DicomTransferSyntax_JPEGLSLossless);
        target.insert(DicomTransferSyntax_JPEGLSLossy);
        break;

      case TransferSyntaxGroup_Jpip:
        target.insert(DicomTransferSyntax_JPIPReferenced);
        target.insert(DicomTransferSyntax_JPIPReferencedDeflate);
        break;

      case TransferSyntaxGroup_Mpeg2:
        target.insert(DicomTransferSyntax_MPEG2MainProfileAtMainLevel);
        target.insert(DicomTransferSyntax_MPEG2MainProfileAtHighLevel);
        break;

      case TransferSyntaxGroup_Rle:
        target.insert(DicomTransferSyntax_RLELossless);
        break;

      case TransferSyntaxGroup_Mpeg4:
        // New in Orthanc 1.6.0
        target.insert(DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1);
        target.insert(DicomTransferSyntax_MPEG4HighProfileLevel4_1);
        target.insert(DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo);
        target.insert(DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo);
        target.insert(DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2);
        break;

      case TransferSyntaxGroup_H265:
        // New in Orthanc 1.9.0
        target.insert(DicomTransferSyntax_HEVCMainProfileLevel5_1);
        target.insert(DicomTransferSyntax_HEVCMain10ProfileLevel5_1);
        break;
        
      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }

  ResponseContentFlags StringToResponseContent(const std::string& value)
  {
    if (value == "MainDicomTags")
    {
      return ResponseContentFlags_MainDicomTags;
    }
    else if (value == "RequestedTags")
    {
      return ResponseContentFlags_RequestedTags;
    }
    else if (value == "Metadata")
    {
      return ResponseContentFlags_Metadata;
    }
    else if (value == "Status")
    {
      return ResponseContentFlags_Status;
    }
    else if (value == "Parent")
    {
      return ResponseContentFlags_Parent;
    }
    else if (value == "Children")
    {
      return ResponseContentFlags_Children;
    }
    else if (value == "Labels")
    {
      return ResponseContentFlags_Labels;
    }
    else if (value == "Attachments")
    {
      return ResponseContentFlags_Attachments;
    }
    else if (value == "IsStable")
    {
      return ResponseContentFlags_IsStable;
    }
    else if (value == "IsProtected")
    {
      return ResponseContentFlags_IsProtected;
    }
    else
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange,
                             "Unrecognized value for \"ResponseContent\": " + value);
    }    
  }

  OverwriteInstancesMode StringToOverwriteInstancesMode(const std::string& value)
  {
    if (value == "Never")
    {
      return OverwriteInstancesMode_Never;
    }
    else if (value == "Always")
    {
      return OverwriteInstancesMode_Always;
    }
    else if (value == "IfChanged")
    {
      return OverwriteInstancesMode_IfChanged;
    }
    else
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange,
                             "Unrecognized value for \"OverwriteInstances\": " + value);
    }    
  }

  const char* EnumerationToString(OverwriteInstancesMode mode)
  {
    switch(mode)
    {
      case OverwriteInstancesMode_Never:
        return "Never";
      case OverwriteInstancesMode_Always:
        return "Always";
      case OverwriteInstancesMode_IfChanged:
        return "IfChanged";
      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);     
    }
  }
}