/**
 * 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 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 "DicomControlUserConnection.h"
#include "DimseErrorPayload.h"

#include "../Compatibility.h"
#include "../DicomFormat/DicomArray.h"
#include "../DicomParsing/FromDcmtkBridge.h"
#include "../Logging.h"
#include "../OrthancException.h"
#include "DicomAssociation.h"

#include <dcmtk/dcmdata/dcdeftag.h>
#include <dcmtk/dcmnet/diutil.h>

#if DCMTK_VERSION_NUMBER < 366
#define STATUS_GET_Pending_SubOperationsAreContinuing                   0xff00
#define STATUS_GET_Success                                              0x0000
#define STATUS_MOVE_Pending_SubOperationsAreContinuing                  0xff00
#define STATUS_MOVE_Success_SubOperationsCompleteNoFailures             0x0000
#endif


namespace Orthanc
{
  static void TestAndCopyTag(DicomMap& result,
                             const DicomMap& source,
                             const DicomTag& tag)
  {
    if (!source.HasTag(tag))
    {
      throw OrthancException(ErrorCode_BadRequest, "Missing tag " + tag.Format());
    }
    else
    {
      result.SetValue(tag, source.GetValue(tag));
    }
  }

  static std::string DimseToHexString(uint16_t dimseStatus)
  {
    char buf[16];
    sprintf(buf, "0x%04X", dimseStatus);
    return buf;
  }

  namespace
  {
    struct FindPayload
    {
      DicomFindAnswers* answers;
      const char*       level;
      bool              isWorklist;
    };
  }


  static void FindCallback(
    /* in */
    void *callbackData,
    T_DIMSE_C_FindRQ *request,      /* original find request */
    int responseCount,
    T_DIMSE_C_FindRSP *response,    /* pending response received */
    DcmDataset *responseIdentifiers /* pending response identifiers */
    )
  {
    if (response != NULL)
    {
      OFString str;
      CLOG(TRACE, DICOM) << "Received Find Response " << responseCount << ":" << std::endl
                         << DIMSE_dumpMessage(str, *response, DIMSE_INCOMING);
    }
      
    if (responseIdentifiers != NULL)
    {
      std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
      responseIdentifiers->print(s);
      CLOG(TRACE, DICOM) << "Response Identifiers "  << responseCount << ":" << std::endl << s.str();
    }
    
    if (responseIdentifiers != NULL)
    {
      FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);

      if (payload.isWorklist)
      {
        const ParsedDicomFile answer(*responseIdentifiers);
        payload.answers->Add(answer);
      }
      else
      {
        DicomMap m;
        std::set<DicomTag> ignoreTagLength;
        FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers, 0 /* don't truncate tags */, ignoreTagLength);
        
        if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
        {
          m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
        }

        payload.answers->Add(m);
      }
    }
  }


  static void NormalizeFindQuery(DicomMap& fixedQuery,
                                 ResourceType level,
                                 const DicomMap& fields)
  {
    std::set<DicomTag> allowedTags;

    // WARNING: Do not add "break" or reorder items in this switch-case!
    switch (level)
    {
      case ResourceType_Instance:
        DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);

      case ResourceType_Series:
        DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);

      case ResourceType_Study:
        DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);

      case ResourceType_Patient:
        DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
        break;

      default:
        THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
    }

    switch (level)
    {
      case ResourceType_Patient:
        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
        break;

      case ResourceType_Study:
        allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
        allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
        break;

      case ResourceType_Series:
        allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
        break;

      default:
        break;
    }

    allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);

    DicomArray query(fields);
    for (size_t i = 0; i < query.GetSize(); i++)
    {
      const DicomTag& tag = query.GetElement(i).GetTag();
      if (allowedTags.find(tag) == allowedTags.end())
      {
        CLOG(WARNING, DICOM) << "Tag not allowed for this C-Find level, will be ignored: ("
                             << tag.Format() << ")";
      }
      else
      {
        fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
      }
    }
  }



  static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
                                             ModalityManufacturer manufacturer)
  {
    // Fix outgoing C-Find requests issue for Syngo.Via and its
    // solution was reported by Emsy Chan by private mail on
    // 2015-06-17. According to Robert van Ommen (2015-11-30), the
    // same fix is required for Agfa Impax. This was generalized for
    // generic manufacturer since it seems to affect PhilipsADW,
    // GEWAServer as well:
    // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=31

    switch (manufacturer)
    {
      case ModalityManufacturer_GenericNoWildcardInDates:
      case ModalityManufacturer_GenericNoUniversalWildcard:
      {
        std::unique_ptr<DicomMap> fix(fields.Clone());

        std::set<DicomTag> tags;
        fix->GetTags(tags);

        for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
        {
          // Replace a "*" wildcard query by an empty query ("") for
          // "date" or "all" value representations depending on the
          // type of manufacturer.
          if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
              (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
               FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
          {
            const DicomValue* value = fix->TestAndGetValue(*it);

            if (value != NULL && 
                !value->IsNull() &&
                value->GetContent() == "*")
            {
              fix->SetValue(*it, "", false);
            }
          }
        }

        return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(),
                                   false /* be strict */);
      }

      default:
        return new ParsedDicomFile(fields, GetDefaultDicomEncoding(),
                                   false /* be strict */);
    }
  }



  void DicomControlUserConnection::SetupPresentationContexts(
    ScuOperationFlags scuOperation,
    const std::set<std::string>& acceptedStorageSopClasses,
    const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes)
  {
    assert(association_.get() != NULL);

    if ((scuOperation & ScuOperationFlags_Echo) != 0)
    {
      association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
    }

    if ((scuOperation & ScuOperationFlags_FindPatient) != 0)
    {
      association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
    }

    if ((scuOperation & ScuOperationFlags_FindStudy) != 0)
    {
      association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
    }

    if ((scuOperation & ScuOperationFlags_FindWorklist) != 0)
    {
      association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
    }

    if ((scuOperation & ScuOperationFlags_MovePatient) != 0)
    {
      association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel);
    }

    if ((scuOperation & ScuOperationFlags_MoveStudy) != 0)
    {
      association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
    }

    if ((scuOperation & ScuOperationFlags_Get) != 0)
    {
      association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel);
      association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel);

      if (acceptedStorageSopClasses.size() == 0)
      {
        throw OrthancException(ErrorCode_BadSequenceOfCalls); // the acceptedStorageSopClassUids should always be defined for a C-Get
      }

      DicomAssociationRole proposedStoreRole = DicomAssociationRole_Scp;

      if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_SiemensSyngoCT)
      {
        proposedStoreRole = DicomAssociationRole_Default; // it seems SyngoCT won't accept a C-Store/SCP only and requires both SCU and SCP roles to be proposed
      }

      for (std::set<std::string>::const_iterator it = acceptedStorageSopClasses.begin(); it != acceptedStorageSopClasses.end(); ++it)
      {
        association_->ProposePresentationContext(*it, proposedStorageTransferSyntaxes, proposedStoreRole);
      }
    }
  }
    

  void DicomControlUserConnection::FindInternal(DicomFindAnswers& answers,
                                                DcmDataset* dataset,
                                                const char* sopClass,
                                                bool isWorklist,
                                                const char* level)
  {
    assert(dataset != NULL);
    assert(isWorklist ^ (level != NULL));
    assert(association_.get() != NULL);

    association_->Open(parameters_);

    FindPayload payload;
    payload.answers = &answers;
    payload.level = level;
    payload.isWorklist = isWorklist;

    // Figure out which of the accepted presentation contexts should be used
    int presID = ASC_findAcceptedPresentationContextID(
      &association_->GetDcmtkAssociation(), sopClass);
    if (presID == 0)
    {
      throw OrthancException(ErrorCode_DicomFindUnavailable,
                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
    }

    T_DIMSE_C_FindRQ request;
    memset(&request, 0, sizeof(request));
    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
    request.Priority = DIMSE_PRIORITY_MEDIUM;
    request.DataSetType = DIMSE_DATASET_PRESENT;

    T_DIMSE_C_FindRSP response;
    DcmDataset* statusDetail = NULL;

#if DCMTK_VERSION_NUMBER >= 364
    int responseCount = 0;
#endif

    {
      std::stringstream s;  // DcmObject::PrintHelper cannot be used with VS2008
      dataset->print(s);

      OFString str;
      CLOG(TRACE, DICOM) << "Sending Find Request:" << std::endl
                         << DIMSE_dumpMessage(str, request, DIMSE_OUTGOING, NULL, presID) << std::endl
                         << s.str();
    }

    OFCondition cond = DIMSE_findUser(
      &association_->GetDcmtkAssociation(), presID, &request, dataset,
#if DCMTK_VERSION_NUMBER >= 364
      responseCount,
#endif
      FindCallback, &payload,
      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
      /*opt_dimse_timeout*/ static_cast<int>(parameters_.GetTimeout()),
      &response, &statusDetail);
    
    if (statusDetail)
    {
      delete statusDetail;
    }

    DicomAssociation::CheckCondition(cond, parameters_, "C-FIND");

    {
      OFString str;
      CLOG(TRACE, DICOM) << "Received Final Find Response:" << std::endl
                         << DIMSE_dumpMessage(str, response, DIMSE_INCOMING);
    }

    
    /**
     * New in Orthanc 1.6.0: Deal with failures during C-FIND.
     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
     **/
    
    if (response.DimseStatus != 0x0000 &&  // Success
        response.DimseStatus != 0xFF00 &&  // Pending - Matches are continuing 
        response.DimseStatus != 0xFF01)    // Pending - Matches are continuing 
    {
      if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
      {
        throw OrthancException(ErrorCode_NetworkProtocol,
                               "C-FIND SCU to AET \"" +
                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                               "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus) +
                               " (unable to process - invalid query ?)")
          .SetHttpStatus(HttpStatus_422_UnprocessableEntity)
          .SetPayload(MakeDimseErrorStatusPayload(response.DimseStatus));
      }
      else
      {
        throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                               "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus))
          .SetPayload(MakeDimseErrorStatusPayload(response.DimseStatus));
      }
    }
  }

  void MoveProgressCallback(void *callbackData,
                            T_DIMSE_C_MoveRQ *request,
                            int responseCount, 
                            T_DIMSE_C_MoveRSP *response)
  {
    DicomControlUserConnection::IProgressListener* listener = reinterpret_cast<DicomControlUserConnection::IProgressListener*>(callbackData);
    if (listener)
    {
      OFString str;
      CLOG(TRACE, DICOM) << "Received Move Progress:" << std::endl
                         << DIMSE_dumpMessage(str, *response, DIMSE_INCOMING);

      listener->OnProgressUpdated(response->NumberOfRemainingSubOperations,
                                  response->NumberOfCompletedSubOperations,
                                  response->NumberOfFailedSubOperations,
                                  response->NumberOfWarningSubOperations);
    }
  }

    
  void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
                                                ResourceType level,
                                                const DicomMap& fields,
                                                uint16_t messageId)
  {
    assert(association_.get() != NULL);
    association_->Open(parameters_);

    std::unique_ptr<ParsedDicomFile> query(
      ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
    DcmDataset* dataset = query->GetDcmtkObject().getDataset();

    const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
    DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(level));

    // Figure out which of the accepted presentation contexts should be used
    int presID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
    if (presID == 0)
    {
      throw OrthancException(ErrorCode_DicomMoveUnavailable,
                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
    }

    T_DIMSE_C_MoveRQ request;
    memset(&request, 0, sizeof(request));
    if (messageId == 0)
    {
      request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
    }
    else
    {
      request.MessageID = messageId;
    }
    
    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
    request.Priority = DIMSE_PRIORITY_MEDIUM;
    request.DataSetType = DIMSE_DATASET_PRESENT;
    strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);

    {
      OFString str;
      CLOG(TRACE, DICOM) << "Sending Move Request:" << std::endl
                         << DIMSE_dumpMessage(str, request, DIMSE_OUTGOING, NULL, presID);
    }
    
    T_DIMSE_C_MoveRSP response;
    DcmDataset* statusDetail = NULL;
    DcmDataset* responseIdentifiers = NULL;
    OFCondition cond = DIMSE_moveUser(
      &association_->GetDcmtkAssociation(), presID, &request, dataset, 
      (progressListener_ != NULL ? MoveProgressCallback : NULL), progressListener_,
      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
      /*opt_dimse_timeout*/ static_cast<int>(parameters_.GetTimeout()),
      &association_->GetDcmtkNetwork(), /*subOpCallback*/ NULL, NULL,
      &response, &statusDetail, &responseIdentifiers);

    if (statusDetail)
    {
      delete statusDetail;
    }

    if (responseIdentifiers)
    {
      delete responseIdentifiers;
    }

    DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE");

    {
      OFString str;
      CLOG(TRACE, DICOM) << "Received Final Move Response:" << std::endl
                         << DIMSE_dumpMessage(str, response, DIMSE_INCOMING);

      if (progressListener_ != NULL)
      {
        progressListener_->OnProgressUpdated(response.NumberOfRemainingSubOperations,
                                             response.NumberOfCompletedSubOperations,
                                             response.NumberOfFailedSubOperations,
                                             response.NumberOfWarningSubOperations);
      }
    }
    
    /**
     * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
     **/
    
    if (response.DimseStatus != STATUS_MOVE_Success_SubOperationsCompleteNoFailures &&
        response.DimseStatus != STATUS_MOVE_Pending_SubOperationsAreContinuing)
    {
      if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
      {
        throw OrthancException(ErrorCode_NetworkProtocol,
                               "C-MOVE SCU to AET \"" +
                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                               "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus) +
                               " (unable to process - resource not found ?)")
          .SetHttpStatus(HttpStatus_422_UnprocessableEntity)
          .SetPayload(MakeDimseErrorStatusPayload(response.DimseStatus));
      }
      else
      {
        throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                               "\" has failed with DIMSE status " + DimseToHexString(response.DimseStatus))
          .SetPayload(MakeDimseErrorStatusPayload(response.DimseStatus));
      }
    }
  }
    

  void DicomControlUserConnection::Get(const DicomMap& getQuery,
                                       CGetInstanceReceivedCallback instanceReceivedCallback,
                                       void* callbackContext)
  {
    assert(association_.get() != NULL);
    association_->Open(parameters_);

    std::unique_ptr<ParsedDicomFile> query(
      ConvertQueryFields(getQuery, parameters_.GetRemoteModality().GetManufacturer()));
    DcmDataset* queryDataset = query->GetDcmtkObject().getDataset();

    std::string remoteAet;
    std::string remoteIp;
    std::string calledAet;

    association_->GetAssociationParameters(remoteAet, remoteIp, calledAet);

    const char* sopClass = NULL;
    const std::string tmp = getQuery.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
    ResourceType level = StringToResourceType(tmp.c_str());
    switch (level)
    {
      case ResourceType_Patient:
        sopClass = UID_GETPatientRootQueryRetrieveInformationModel;
        break;
      case ResourceType_Study:
      case ResourceType_Series:
      case ResourceType_Instance:
        sopClass = UID_GETStudyRootQueryRetrieveInformationModel;
        break;
      default:
        THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
    }

    // Figure out which of the accepted presentation contexts should be used
    int cgetPresID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
    if (cgetPresID == 0)
    {
      throw OrthancException(ErrorCode_DicomGetUnavailable,
                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
    }

    T_DIMSE_Message msgGetRequest;
    memset(reinterpret_cast<void*>(&msgGetRequest), 0, sizeof(msgGetRequest));
    msgGetRequest.CommandField = DIMSE_C_GET_RQ;

    T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ);
    request->MessageID = association_->GetDcmtkAssociation().nextMsgID++;
    strncpy(request->AffectedSOPClassUID, sopClass, DIC_UI_LEN);
    request->Priority = DIMSE_PRIORITY_MEDIUM;
    request->DataSetType = DIMSE_DATASET_PRESENT;

    {
      OFString str;
      CLOG(TRACE, DICOM) << "Sending Get Request:" << std::endl
                         << DIMSE_dumpMessage(str, *request, DIMSE_OUTGOING, NULL, cgetPresID);
    }
    
    OFCondition cond = DIMSE_sendMessageUsingMemoryData(
          &(association_->GetDcmtkAssociation()), cgetPresID, &msgGetRequest, NULL /* statusDetail */, queryDataset,
          NULL, NULL, NULL /* commandSet */);
      
    if (cond.bad())
    {
        OFString tempStr;
        CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond);

        throw OrthancException(ErrorCode_NetworkProtocol, "C-GET SCU to AET \"" +
                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                               "\" has failed to send the C-GET request");

    }

    // equivalent to handleCGETSession in DCMTK
    bool continueSession = true;

    // As long we want to continue (usually, as long as we receive more objects,
    // i.e. the final C-GET response has not arrived yet)
    while (continueSession)
    {
        T_DIMSE_Message rsp;
        // Make sure everything is zeroed (especially options)
        memset(reinterpret_cast<void*>(&rsp), 0, sizeof(rsp));

        // DcmDataset* statusDetail = NULL;
        T_ASC_PresentationContextID cmdPresId = 0;

        OFCondition result = DIMSE_receiveCommand(&(association_->GetDcmtkAssociation()),
                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
                                                  static_cast<int>(parameters_.GetTimeout()),
                                                  &cmdPresId,
                                                  &rsp,
                                                  NULL /* statusDetail */,
                                                  NULL /* not interested in the command set */);

        if (result.bad())
        {
          OFString tempStr;
          CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result);
          throw OrthancException(ErrorCode_NetworkProtocol, "C-GET SCU to AET \"" +
                                 parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                                 "\" has failed to receive DIMSE command");
        }
        // Handle C-GET Response
        if (rsp.CommandField == DIMSE_C_GET_RSP)
        {
          {
            OFString tempStr;
            CLOG(TRACE, DICOM) << "Received C-GET Response: " << std::endl
              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
          }

          if (progressListener_ != NULL)
          {
            progressListener_->OnProgressUpdated(rsp.msg.CGetRSP.NumberOfRemainingSubOperations,
                                                  rsp.msg.CGetRSP.NumberOfCompletedSubOperations,
                                                  rsp.msg.CGetRSP.NumberOfFailedSubOperations,
                                                  rsp.msg.CGetRSP.NumberOfWarningSubOperations);
          }

          if (rsp.msg.CGetRSP.DimseStatus == STATUS_GET_Success)  // final success message
          {
            continueSession = false;
          }
          else if (rsp.msg.CGetRSP.DimseStatus != STATUS_GET_Pending_SubOperationsAreContinuing)
          {
            throw OrthancException(ErrorCode_NetworkProtocol,
                                   "C-GET SCU to AET \"" +
                                   parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                                   "\" has failed with DIMSE status " + DimseToHexString(rsp.msg.CGetRSP.DimseStatus))
              .SetPayload(MakeDimseErrorStatusPayload(rsp.msg.CGetRSP.DimseStatus));
          }
        }
        // Handle C-STORE Request
        else if (rsp.CommandField == DIMSE_C_STORE_RQ)
        {
          {
            OFString tempStr;
            CLOG(TRACE, DICOM) << "Received C-STORE Request: " << std::endl
              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
          }

          T_DIMSE_C_StoreRQ* storeRequest = &(rsp.msg.CStoreRQ);

          // Check if dataset is announced correctly
          if (rsp.msg.CStoreRQ.DataSetType == DIMSE_DATASET_NULL)
          {
            CLOG(WARNING, DICOM) << "C-GET SCU handler: Incoming C-STORE with no dataset";
          }

          Uint16 desiredCStoreReturnStatus = 0;
          DcmDataset* dataObjectRawPtr = NULL;

          // Receive dataset
          result = DIMSE_receiveDataSetInMemory(&(association_->GetDcmtkAssociation()),
                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
                                                  static_cast<int>(parameters_.GetTimeout()),
                                                  &cmdPresId,
                                                  &dataObjectRawPtr,
                                                  NULL, NULL);

          std::unique_ptr<DcmDataset> dataObject(dataObjectRawPtr); // to handle deallocation correctly

          if (result.bad())
          {
            LOG(WARNING) << "C-GET SCU handler: Failed to receive dataset: " << result.text();
            desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand;
          }
          else
          {
            // callback the OrthancServer with the received data
            if (instanceReceivedCallback != NULL)
            {
              desiredCStoreReturnStatus = instanceReceivedCallback(callbackContext, *dataObject, remoteAet, remoteIp, calledAet);
            }
          }

          // send the Store response
          T_DIMSE_Message storeResponse;
          memset(reinterpret_cast<void*>(&storeResponse), 0, sizeof(storeResponse));
          storeResponse.CommandField         = DIMSE_C_STORE_RSP;

          T_DIMSE_C_StoreRSP& storeRsp       = storeResponse.msg.CStoreRSP;
          storeRsp.MessageIDBeingRespondedTo = storeRequest->MessageID;
          storeRsp.DimseStatus               = desiredCStoreReturnStatus;
          storeRsp.DataSetType               = DIMSE_DATASET_NULL;

          OFStandard::strlcpy(
              storeRsp.AffectedSOPClassUID, storeRequest->AffectedSOPClassUID, sizeof(storeRsp.AffectedSOPClassUID));
          OFStandard::strlcpy(
              storeRsp.AffectedSOPInstanceUID, storeRequest->AffectedSOPInstanceUID, sizeof(storeRsp.AffectedSOPInstanceUID));
          storeRsp.opts = O_STORE_AFFECTEDSOPCLASSUID | O_STORE_AFFECTEDSOPINSTANCEUID;

          result = DIMSE_sendMessageUsingMemoryData(&(association_->GetDcmtkAssociation()), 
                                                    cmdPresId, 
                                                    &storeResponse, NULL /* statusDetail */, NULL /* dataObject */,
                                                    NULL, NULL, NULL /* commandSet */);
          if (result.bad())
          {
            throw OrthancException(ErrorCode_NetworkProtocol, "C-GET SCU to AET \"" +
                                    parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                                    "\" has failed to send response message");
          }
          else
          {
            OFString tempStr;
            CLOG(TRACE, DICOM) << "Sent C-STORE Response: " << std::endl
              << DIMSE_dumpMessage(tempStr, storeResponse, DIMSE_OUTGOING, NULL, cmdPresId);
          }
        }
        // Handle other DIMSE command (error since other command than GET/STORE not expected)
        else
        {
          CLOG(WARNING, DICOM) << "Expected C-GET response or C-STORE request but received DIMSE command 0x"
                               << std::hex << std::setfill('0') << std::setw(4)
                               << static_cast<unsigned int>(rsp.CommandField);
          
          throw OrthancException(ErrorCode_NetworkProtocol, "C-GET SCU to AET \"" +
                                 parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                                 "\": Expected C-GET response or C-STORE request but received DIMSE command " + DimseToHexString(rsp.CommandField));
        }

    }
    /* All responses received or break signal occurred */
}


  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation) :
    parameters_(params),
    association_(new DicomAssociation),
    progressListener_(NULL)
  {
    assert((scuOperation & ScuOperationFlags_Get) == 0);  // you must provide acceptedStorageSopClassUids for Get SCU
    std::set<std::string> emptyStorageSopClasses;
    std::list<DicomTransferSyntax> emptyStorageTransferSyntaxes;

    SetupPresentationContexts(scuOperation, emptyStorageSopClasses, emptyStorageTransferSyntaxes);
  }
    
  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, 
                                                         ScuOperationFlags scuOperation,
                                                         const std::set<std::string>& acceptedStorageSopClasses,
                                                         const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes) :
    parameters_(params),
    association_(new DicomAssociation),
    progressListener_(NULL)
  {
    SetupPresentationContexts(scuOperation, acceptedStorageSopClasses, proposedStorageTransferSyntaxes);
  }
    

  void DicomControlUserConnection::Close()
  {
    assert(association_.get() != NULL);
    association_->Close();
  }


  bool DicomControlUserConnection::Echo()
  {
    assert(association_.get() != NULL);
    association_->Open(parameters_);

    DIC_US status;
    DicomAssociation::CheckCondition(
      DIMSE_echoUser(&association_->GetDcmtkAssociation(),
                     association_->GetDcmtkAssociation().nextMsgID++, 
                     /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
                     /*opt_dimse_timeout*/ static_cast<int>(parameters_.GetTimeout()),
                     &status, NULL),
      parameters_, "C-ECHO");
      
    return status == STATUS_Success;
  }


  void DicomControlUserConnection::Find(DicomFindAnswers& result,
                                        ResourceType level,
                                        const DicomMap& originalFields,
                                        bool normalize)
  {
    std::unique_ptr<ParsedDicomFile> query;

    if (normalize)
    {
      DicomMap fields;
      NormalizeFindQuery(fields, level, originalFields);
      query.reset(ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
    }
    else
    {
      query.reset(new ParsedDicomFile(originalFields, GetDefaultDicomEncoding(),
                                      false /* be strict */));
    }
    
    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
    assert(dataset != NULL);

    const char* clevel = ResourceTypeToDicomQueryRetrieveLevel(level);
    const char* sopClass = NULL;

    DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, clevel);

    switch (level)
    {
      case ResourceType_Patient:
        sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
        break;

      case ResourceType_Study:
        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
        break;

      case ResourceType_Series:
        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
        break;

      case ResourceType_Instance:
        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
        break;

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }


    const char* universal;
    if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_GE)
    {
      universal = "*";
    }
    else
    {
      universal = "";
    }      
    

    // Add the expected tags for this query level.
    // WARNING: Do not reorder or add "break" in this switch-case!
    switch (level)
    {
      case ResourceType_Instance:
        if (!dataset->tagExists(DCM_SOPInstanceUID))
        {
          DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
        }

      case ResourceType_Series:
        if (!dataset->tagExists(DCM_SeriesInstanceUID))
        {
          DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
        }

      case ResourceType_Study:
        if (!dataset->tagExists(DCM_AccessionNumber))
        {
          DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
        }

        if (!dataset->tagExists(DCM_StudyInstanceUID))
        {
          DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
        }

      case ResourceType_Patient:
        if (!dataset->tagExists(DCM_PatientID))
        {
          DU_putStringDOElement(dataset, DCM_PatientID, universal);
        }
        
        break;

      default:
        throw OrthancException(ErrorCode_ParameterOutOfRange);
    }

    assert(clevel != NULL && sopClass != NULL);
    FindInternal(result, dataset, sopClass, false, clevel);
  }
    

  void DicomControlUserConnection::Move(const std::string& targetAet,
                                        ResourceType level,
                                        const DicomMap& moveQuery,
                                        uint16_t messageId)
  {
    DicomMap move;
    switch (level)
    {
      case ResourceType_Patient:
        TestAndCopyTag(move, moveQuery, DICOM_TAG_PATIENT_ID);
        break;

      case ResourceType_Study:
        TestAndCopyTag(move, moveQuery, DICOM_TAG_STUDY_INSTANCE_UID);
        break;

      case ResourceType_Series:
        TestAndCopyTag(move, moveQuery, DICOM_TAG_STUDY_INSTANCE_UID);
        TestAndCopyTag(move, moveQuery, DICOM_TAG_SERIES_INSTANCE_UID);
        break;

      case ResourceType_Instance:
        TestAndCopyTag(move, moveQuery, DICOM_TAG_STUDY_INSTANCE_UID);
        TestAndCopyTag(move, moveQuery, DICOM_TAG_SERIES_INSTANCE_UID);
        TestAndCopyTag(move, moveQuery, DICOM_TAG_SOP_INSTANCE_UID);
        break;

      default:
        THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
    }

    MoveInternal(targetAet, level, move, messageId);
  }


  void DicomControlUserConnection::Move(const std::string& targetAet,
                                        const DicomMap& moveQuery,
                                        uint16_t messageId)
  {
    if (!moveQuery.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
    {
      THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
    }

    const std::string tmp = moveQuery.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
    ResourceType level = StringToResourceType(tmp.c_str());

    Move(targetAet, level, moveQuery, messageId);
  }


  void DicomControlUserConnection::FindWorklist(DicomFindAnswers& result,
                                                ParsedDicomFile& query)
  {
    DcmDataset* dataset = query.GetDcmtkObject().getDataset();
    const char* sopClass = UID_FINDModalityWorklistInformationModel;

    FindInternal(result, dataset, sopClass, true, NULL);
  }
}
