/**
 * 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 Lesser 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 **/


#include "../PrecompiledHeaders.h"
#include "DicomStoreUserConnection.h"

#include "../DicomParsing/FromDcmtkBridge.h"
#include "../DicomParsing/ParsedDicomFile.h"
#include "../Logging.h"
#include "../OrthancException.h"
#include "DicomAssociation.h"

#include <dcmtk/dcmdata/dcdeftag.h>

#include <list>


namespace Orthanc
{
  static void ProgressCallback(void * /*callbackData*/,
                               T_DIMSE_StoreProgress *progress,
                               T_DIMSE_C_StoreRQ * req)
  {
    if (req != NULL &&
        progress->state == DIMSE_StoreBegin)
    {
      OFString str;
      CLOG(TRACE, DICOM) << "Sending Store Request:" << std::endl
                         << DIMSE_dumpMessage(str, *req, DIMSE_OUTGOING);
    }
  }


  bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid,
                                                     const std::set<DicomTransferSyntax>& sourceSyntaxes,
                                                     bool hasPreferred,
                                                     DicomTransferSyntax preferred)
  {
    typedef std::list< std::list<DicomTransferSyntax> >  GroupsOfSyntaxes;

    GroupsOfSyntaxes  groups;

    // Firstly, add one group for each individual transfer syntax
    for (std::set<DicomTransferSyntax>::const_iterator
           it = sourceSyntaxes.begin(); it != sourceSyntaxes.end(); ++it)
    {
      std::list<DicomTransferSyntax> group;
      group.push_back(*it);
      groups.push_back(group);
    }

    // Secondly, add one group with the preferred transfer syntax
    if (hasPreferred &&
        sourceSyntaxes.find(preferred) == sourceSyntaxes.end())
    {
      std::list<DicomTransferSyntax> group;
      group.push_back(preferred);
      groups.push_back(group);
    }

    // Thirdly, add all the uncompressed transfer syntaxes as one single group
    if (proposeUncompressedSyntaxes_)
    {
      static const size_t N = 3;
      static const DicomTransferSyntax UNCOMPRESSED_SYNTAXES[N] = {
        DicomTransferSyntax_LittleEndianImplicit,
        DicomTransferSyntax_LittleEndianExplicit,
        DicomTransferSyntax_BigEndianExplicit
      };

      std::list<DicomTransferSyntax> group;

      for (size_t i = 0; i < N; i++)
      {
        DicomTransferSyntax syntax = UNCOMPRESSED_SYNTAXES[i];
        if (sourceSyntaxes.find(syntax) == sourceSyntaxes.end() &&
            (!hasPreferred || preferred != syntax))
        {
          group.push_back(syntax);
        }
      }

      if (!group.empty())
      {
        groups.push_back(group);
      }
    }

    // Now, propose each of these groups of transfer syntaxes
    if (association_->GetRemainingPropositions() <= groups.size())
    {
      return false;  // Not enough room
    }
    else
    {
      for (GroupsOfSyntaxes::const_iterator it = groups.begin(); it != groups.end(); ++it)
      {
        association_->ProposePresentationContext(sopClassUid, *it);

        // Remember the syntaxes that were individually proposed, in
        // order to avoid renegociation if they are seen again (**)
        if (it->size() == 1)
        {
          DicomTransferSyntax syntax = *it->begin();
          proposedOriginalClasses_.insert(std::make_pair(sopClassUid, syntax));
        }
      }

      return true;
    }
  }


  bool DicomStoreUserConnection::LookupPresentationContext(
    uint8_t& presentationContextId,
    const std::string& sopClassUid,
    DicomTransferSyntax transferSyntax)
  {
    typedef std::map<DicomTransferSyntax, uint8_t>  PresentationContexts;

    PresentationContexts pc;
    if (association_->IsOpen() &&
        association_->LookupAcceptedPresentationContext(pc, sopClassUid))
    {
      PresentationContexts::const_iterator found = pc.find(transferSyntax);
      if (found != pc.end())
      {
        presentationContextId = found->second;
        return true;
      }
    }

    return false;
  }


  DicomStoreUserConnection::DicomStoreUserConnection(
    const DicomAssociationParameters& params) :
    parameters_(params),
    association_(new DicomAssociation),
    proposeCommonClasses_(true),
    proposeUncompressedSyntaxes_(true),
    proposeRetiredBigEndian_(false)
  {
  }

  const DicomAssociationParameters &DicomStoreUserConnection::GetParameters() const
  {
    return parameters_;
  }

  void DicomStoreUserConnection::SetCommonClassesProposed(bool proposed)
  {
    proposeCommonClasses_ = proposed;
  }

  bool DicomStoreUserConnection::IsCommonClassesProposed() const
  {
    return proposeCommonClasses_;
  }

  void DicomStoreUserConnection::SetUncompressedSyntaxesProposed(bool proposed)
  {
    proposeUncompressedSyntaxes_ = proposed;
  }

  bool DicomStoreUserConnection::IsUncompressedSyntaxesProposed() const
  {
    return proposeUncompressedSyntaxes_;
  }

  void DicomStoreUserConnection::SetRetiredBigEndianProposed(bool propose)
  {
    proposeRetiredBigEndian_ = propose;
  }

  bool DicomStoreUserConnection::IsRetiredBigEndianProposed() const
  {
    return proposeRetiredBigEndian_;
  }


  void DicomStoreUserConnection::RegisterStorageClass(const std::string& sopClassUid,
                                                      DicomTransferSyntax syntax)
  {
    RegisteredClasses::iterator found = registeredClasses_.find(sopClassUid);

    if (found == registeredClasses_.end())
    {
      std::set<DicomTransferSyntax> ts;
      ts.insert(syntax);
      registeredClasses_[sopClassUid] = ts;
    }
    else
    {
      found->second.insert(syntax);
    }
  }


  void DicomStoreUserConnection::LookupParameters(std::string& sopClassUid,
                                                  std::string& sopInstanceUid,
                                                  DicomTransferSyntax& transferSyntax,
                                                  DcmFileFormat& dicom)
  {
    if (dicom.getDataset() == NULL)
    {
      throw OrthancException(ErrorCode_InternalError);
    }
    
    OFString a, b;
    if (!dicom.getDataset()->findAndGetOFString(DCM_SOPClassUID, a).good() ||
        !dicom.getDataset()->findAndGetOFString(DCM_SOPInstanceUID, b).good())
    {
      throw OrthancException(ErrorCode_NoSopClassOrInstance,
                             "Unable to determine the SOP class/instance for C-STORE with AET " +
                             parameters_.GetRemoteModality().GetApplicationEntityTitle());
    }

    sopClassUid.assign(a.c_str());
    sopInstanceUid.assign(b.c_str());

    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom))
    {
      throw OrthancException(ErrorCode_InternalError,
                             "Unknown transfer syntax from DCMTK");
    }
  }
  

  bool DicomStoreUserConnection::NegotiatePresentationContext(
    uint8_t& presentationContextId,
    const std::string& sopClassUid,
    DicomTransferSyntax transferSyntax,
    bool hasPreferred,
    DicomTransferSyntax preferred)
  {
    /**
     * Step 1: Check whether this presentation context is already
     * available in the previously negotiated assocation.
     **/

    if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
    {
      CLOG(INFO, DICOM) << "Found an accepted presentation context for SOPClassUID " << sopClassUid << " and transfer syntax " << GetTransferSyntaxUid(transferSyntax);
      return true;
    }

    // The association must be re-negotiated
    if (association_->IsOpen())
    {
      CLOG(INFO, DICOM) << "No accepted presentation context found, re-negotiating DICOM association with "
                        << parameters_.GetRemoteModality().GetApplicationEntityTitle()
                        << " for SOPClassUID " << sopClassUid << " TransferSyntax =" << GetTransferSyntaxUid(transferSyntax);

      // Check if we know that the remote modality was
      // already proposed this individual transfer syntax (**)
      if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) != proposedOriginalClasses_.end())
      {
        CLOG(INFO, DICOM) << "The remote modality has already rejected SOP class UID \""
                          << sopClassUid << "\" with transfer syntax \""
                          << GetTransferSyntaxUid(transferSyntax) << "\", but we will renegotiate anyway";
        // always renegotiating since 1.12.2 // return false;
      }
    }
    else
    {
      CLOG(INFO, DICOM) << "Negotiating DICOM association with "
                        << parameters_.GetRemoteModality().GetApplicationEntityTitle()
                        << " for SOPClassUID " << sopClassUid << " TransferSyntax =" << GetTransferSyntaxUid(transferSyntax);
    }

    association_->ClearPresentationContexts();
    proposedOriginalClasses_.clear();
    RegisterStorageClass(sopClassUid, transferSyntax);  // (*)

    
    /**
     * Step 2: Propose at least the mandatory SOP class.
     **/

    {
      RegisteredClasses::const_iterator mandatory = registeredClasses_.find(sopClassUid);

      if (mandatory == registeredClasses_.end() ||
          mandatory->second.find(transferSyntax) == mandatory->second.end())
      {
        // Should never fail because of (*)
        throw OrthancException(ErrorCode_InternalError);
      }

      if (!ProposeStorageClass(sopClassUid, mandatory->second, hasPreferred, preferred))
      {
        // Should never happen in real life: There are no more than
        // 128 transfer syntaxes in DICOM!
        throw OrthancException(ErrorCode_InternalError,
                               "Too many transfer syntaxes for SOP class UID: " + sopClassUid);
      }
    }

      
    /**
     * Step 3: Propose all the previously spotted SOP classes, as
     * registered through the "RegisterStorageClass()" method.
     **/
      
    for (RegisteredClasses::const_iterator it = registeredClasses_.begin();
         it != registeredClasses_.end(); ++it)
    {
      if (it->first != sopClassUid)
      {
        ProposeStorageClass(it->first, it->second, hasPreferred, preferred);
      }
    }
      

    /**
     * Step 4: As long as there is room left in the proposed
     * presentation contexts, propose the uncompressed transfer syntaxes
     * for the most common SOP classes, as can be found in the
     * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The
     * preferred transfer syntax is "LittleEndianImplicit".
     **/

    if (proposeCommonClasses_)
    {
      // The method "ProposeStorageClass()" will automatically add
      // "LittleEndianImplicit"
      std::set<DicomTransferSyntax> ts;
        
      for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++)
      {
        std::string c(dcmShortSCUStorageSOPClassUIDs[i]);
          
        if (c != sopClassUid &&
            registeredClasses_.find(c) == registeredClasses_.end())
        {
          ProposeStorageClass(c, ts, hasPreferred, preferred);
        }
      }
    }


    /**
     * Step 5: Open the association, and check whether the pair (SOP
     * class UID, transfer syntax) was accepted by the remote host.
     **/

    association_->Open(parameters_);
    return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax);
  }


  void DicomStoreUserConnection::Store(std::string& sopClassUid,
                                       std::string& sopInstanceUid,
                                       DcmFileFormat& dicom,
                                       bool hasMoveOriginator,
                                       const std::string& moveOriginatorAET,
                                       uint16_t moveOriginatorID)
  {
    DicomTransferSyntax transferSyntax;
    LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom);

    LOG(INFO) << "Performing C-Store on instance of SOPClassUID '" << sopClassUid << "'";

    uint8_t presID;
    if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax, proposeUncompressedSyntaxes_,
                                      DicomTransferSyntax_LittleEndianExplicit))
    {
      throw OrthancException(ErrorCode_NetworkProtocol,
                             "No valid presentation context was negotiated for "
                             "SOP class UID [" + sopClassUid + "] and transfer "
                             "syntax [" + GetTransferSyntaxUid(transferSyntax) + "] "
                             "while sending to modality [" +
                             parameters_.GetRemoteModality().GetApplicationEntityTitle() + "]");
    }
    
    // Prepare the transmission of data
    T_DIMSE_C_StoreRQ request;
    memset(&request, 0, sizeof(request));
    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
    strncpy(request.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
    request.Priority = DIMSE_PRIORITY_MEDIUM;
    request.DataSetType = DIMSE_DATASET_PRESENT;
    strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);

    if (hasMoveOriginator)
    {    
      strncpy(request.MoveOriginatorApplicationEntityTitle, 
              moveOriginatorAET.c_str(), DIC_AE_LEN);
      request.opts = O_STORE_MOVEORIGINATORAETITLE;

      request.MoveOriginatorID = moveOriginatorID;  // The type DIC_US is an alias for uint16_t
      request.opts |= O_STORE_MOVEORIGINATORID;
    }

    if (dicom.getDataset() == NULL)
    {
      throw OrthancException(ErrorCode_InternalError);
    }

    // Finally conduct transmission of data
    T_DIMSE_C_StoreRSP response;
    DcmDataset* statusDetail = NULL;
    DicomAssociation::CheckCondition(
      DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request,
                      NULL, dicom.getDataset(), ProgressCallback, NULL,
                      /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
                      /*opt_dimse_timeout*/ GetParameters().GetTimeout(),
                      &response, &statusDetail, NULL),
      GetParameters(), "C-STORE");

    if (statusDetail != NULL) 
    {
      delete statusDetail;
    }

    {
      OFString str;
      CLOG(TRACE, DICOM) << "Received Store Response:" << std::endl
                         << DIMSE_dumpMessage(str, response, DIMSE_INCOMING, NULL, presID);
    }
    
    /**
     * New in Orthanc 1.6.0: Deal with failures during C-STORE.
     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1
     **/
    
    if (response.DimseStatus != 0x0000 &&  // Success
        response.DimseStatus != 0xB000 &&  // Warning - Coercion of Data Elements
        response.DimseStatus != 0xB007 &&  // Warning - Data Set does not match SOP Class
        response.DimseStatus != 0xB006 &&  // Warning - Elements Discarded
        response.DimseStatus != 0x0111)    // Warning - Duplicate SOPInstanceUID (https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3)
    {
      char buf[16];
      sprintf(buf, "%04X", response.DimseStatus);
      throw OrthancException(ErrorCode_NetworkProtocol,
                             "C-STORE SCU to AET \"" +
                             GetParameters().GetRemoteModality().GetApplicationEntityTitle() +
                             "\" has failed with DIMSE status 0x" + buf);
    }
  }


  void DicomStoreUserConnection::Store(std::string& sopClassUid,
                                       std::string& sopInstanceUid,
                                       const void* buffer,
                                       size_t size,
                                       bool hasMoveOriginator,
                                       const std::string& moveOriginatorAET,
                                       uint16_t moveOriginatorID)
  {
    std::unique_ptr<DcmFileFormat> dicom(
      FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));

    if (dicom.get() == NULL)
    {
      throw OrthancException(ErrorCode_InternalError);
    }
    
    Store(sopClassUid, sopInstanceUid, *dicom, hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
  }


#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
  void DicomStoreUserConnection::LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes,
                                                   const std::string& sopClassUid,
                                                   DicomTransferSyntax sourceSyntax,
                                                   bool hasPreferred,
                                                   DicomTransferSyntax preferred)
  {
    acceptedSyntaxes.clear();
    std::map<DicomTransferSyntax, uint8_t> contexts;

    // Make sure a negotiation has already occurred for this transfer
    // syntax if we have not negotiated yet. 
    // We don't use the return code: Transcoding is possible even if the "sourceSyntax" is not supported.
    if (!association_->IsOpen() || !association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
    {
      uint8_t presID;
      NegotiatePresentationContext(presID, sopClassUid, sourceSyntax, hasPreferred, preferred);
    }

    if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
    {
      for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
             it = contexts.begin(); it != contexts.end(); ++it)
      {
        acceptedSyntaxes.insert(it->first);
      }
    }
  }
#endif
  

#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
  void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
                                           std::string& sopInstanceUid /* out */,
                                           IDicomTranscoder& transcoder,
                                           const void* buffer,
                                           size_t size,
                                           DicomTransferSyntax preferredTransferSyntax,
                                           bool hasMoveOriginator,
                                           const std::string& moveOriginatorAET,
                                           uint16_t moveOriginatorID)
  {
    std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
    if (dicom.get() == NULL ||
        dicom->getDataset() == NULL)
    {
      throw OrthancException(ErrorCode_NullPointer);
    }

    DicomTransferSyntax sourceSyntax;
    LookupParameters(sopClassUid, sopInstanceUid, sourceSyntax, *dicom);

    std::set<DicomTransferSyntax> accepted;
    LookupTranscoding(accepted, sopClassUid, sourceSyntax, true, preferredTransferSyntax);

    if (accepted.size() == 0)
    {
      throw OrthancException(ErrorCode_NoPresentationContext, "Cannot C-Store an instance of SOPClassUID " + 
                             sopClassUid + ", the destination has not accepted any TransferSyntax for this SOPClassUID.");
    }

    if (accepted.find(sourceSyntax) != accepted.end())
    {
      // No need for transcoding
      Store(sopClassUid, sopInstanceUid, *dicom,
            hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
    }
    else
    {
      // Transcoding is needed
      IDicomTranscoder::DicomImage source;
      source.AcquireParsed(dicom.release());
      source.SetExternalBuffer(buffer, size);

      const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed());
        
      IDicomTranscoder::DicomImage transcoded;
      bool success = false;
      bool isDestructiveCompressionAllowed = false;
      std::set<DicomTransferSyntax> attemptedSyntaxes;

      LOG(INFO) << "Transcoding is required to C-Store an instance of SOPClassUID '" << sopClassUid << "', preferredTransferSyntax is " << GetTransferSyntaxUid(preferredTransferSyntax);

      if (accepted.find(preferredTransferSyntax) != accepted.end())
      {
        // New in Orthanc 1.9.0: The preferred transfer syntax is
        // accepted by the remote modality => transcode to this syntax
        std::set<DicomTransferSyntax> targetSyntaxes;
        targetSyntaxes.insert(preferredTransferSyntax);
        attemptedSyntaxes.insert(preferredTransferSyntax);

        success = transcoder.Transcode(transcoded, source, targetSyntaxes, true);
        isDestructiveCompressionAllowed = true;
      }

      if (!success)
      {
        // Transcode to either one of the uncompressed transfer
        // syntaxes that are accepted by the remote modality

        std::set<DicomTransferSyntax> targetSyntaxes;

        if (accepted.find(DicomTransferSyntax_LittleEndianImplicit) != accepted.end())
        {
          targetSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
          attemptedSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
        }

        if (accepted.find(DicomTransferSyntax_LittleEndianExplicit) != accepted.end())
        {
          targetSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
          attemptedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
        }

        if (accepted.find(DicomTransferSyntax_BigEndianExplicit) != accepted.end())
        {
          targetSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
          attemptedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
        }

        if (!targetSyntaxes.empty())
        {
          success = transcoder.Transcode(transcoded, source, targetSyntaxes, false);
          isDestructiveCompressionAllowed = false;
        }
      }

      if (success)
      {
        std::string targetUid = IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed());
        if (sourceUid != targetUid)
        {
          if (isDestructiveCompressionAllowed)
          {
            LOG(WARNING) << "Because of the use of a preferred transfer syntax that corresponds to "
                         << "a destructive compression, C-STORE SCU has hanged the SOP Instance UID "
                         << "of a DICOM instance from \"" << sourceUid << "\" to \"" << targetUid << "\"";
          }
          else
          {
            throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP "
                                   "Instance UID while transcoding to an uncompressed transfer syntax");
          }
        }

        DicomTransferSyntax transcodedSyntax;
          
        // Sanity check
        if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) ||
            accepted.find(transcodedSyntax) == accepted.end())
        {
          throw OrthancException(ErrorCode_InternalError);
        }
        else
        {
          Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(),
                hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
        }
      }
      else
      {
        std::string s;
        for (std::set<DicomTransferSyntax>::const_iterator
               it = attemptedSyntaxes.begin(); it != attemptedSyntaxes.end(); ++it)
        {
          s += " " + std::string(GetTransferSyntaxUid(*it));
        }
        
        throw OrthancException(ErrorCode_InternalError, "Cannot transcode instance of SOPClassUID " + 
                               sopClassUid + " from " +
                               std::string(GetTransferSyntaxUid(sourceSyntax)) +
                               " to one of [" + s + " ]");
      }
    }
  }
#endif
  
  
#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
  void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
                                           std::string& sopInstanceUid /* out */,
                                           IDicomTranscoder& transcoder,
                                           const void* buffer,
                                           size_t size,
                                           bool hasMoveOriginator,
                                           const std::string& moveOriginatorAET,
                                           uint16_t moveOriginatorID)
  {
    Transcode(sopClassUid, sopInstanceUid, transcoder, buffer, size, DicomTransferSyntax_LittleEndianExplicit,
              hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
  }
#endif
}
