/*
 *
 *  Copyright (C) 1994-2024, OFFIS e.V.
 *  All rights reserved.  See COPYRIGHT file for details.
 *
 *  This software and supporting documentation were partly developed by
 *
 *    OFFIS e.V.
 *    R&D Division Health
 *    Escherweg 2
 *    D-26121 Oldenburg, Germany
 *
 *  For further copyrights, see the following paragraphs.
 *
 */

/*
**  Copyright (C) 1993/1994, OFFIS, Oldenburg University and CERIUM
**
**  This software and supporting documentation were
**  developed by
**
**    Institut OFFIS
**    Bereich Kommunikationssysteme
**    Westerstr. 10-12
**    26121 Oldenburg, Germany
**
**    Fachbereich Informatik
**    Abteilung Prozessinformatik
**    Carl von Ossietzky Universitaet Oldenburg
**    Ammerlaender Heerstr. 114-118
**    26111 Oldenburg, Germany
**
**    CERIUM
**    Laboratoire SIM
**    Faculte de Medecine
**    2 Avenue du Pr. Leon Bernard
**    35043 Rennes Cedex, France
**
**  for CEN/TC251/WG4 as a contribution to the Radiological
**  Society of North America (RSNA) 1993 Digital Imaging and
**  Communications in Medicine (DICOM) Demonstration.
**
**  THIS SOFTWARE IS MADE AVAILABLE, AS IS, AND NEITHER OFFIS,
**  OLDENBURG UNIVERSITY NOR CERIUM MAKE ANY WARRANTY REGARDING
**  THE SOFTWARE, ITS PERFORMANCE, ITS MERCHANTABILITY OR
**  FITNESS FOR ANY PARTICULAR USE, FREEDOM FROM ANY COMPUTER
**  DISEASES OR ITS CONFORMITY TO ANY SPECIFICATION.  THE
**  ENTIRE RISK AS TO QUALITY AND PERFORMANCE OF THE SOFTWARE
**  IS WITH THE USER.
**
**  Copyright of the software and supporting documentation
**  is, unless otherwise stated, jointly owned by OFFIS,
**  Oldenburg University and CERIUM and free access is hereby
**  granted as a license to use this software, copy this
**  software and prepare derivative works based upon this
**  software. However, any distribution of this software
**  source code or supporting documentation or derivative
**  works (source code and supporting documentation) must
**  include the three paragraphs of this copyright notice.
**
*/

/*
**
** Author: Andrew Hewett                Created: 03-06-93
**
** Module: dimmove
**
** Purpose:
**      This file contains the routines which help with
**      query/retrieve services using the C-MOVE operation.
**
** Module Prefix: DIMSE_
*/

/*
** Include Files
*/

#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */

#include "dcmtk/ofstd/oftimer.h"

#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif

#include "dcmtk/dcmnet/diutil.h"
#include "dcmtk/dcmnet/dimse.h"       /* always include the module header */
#include "dcmtk/dcmnet/cond.h"
#include "dcmtk/ofstd/ofstd.h"

/*
**
*/

static int
selectReadable(
    T_ASC_Association *assoc,
    T_ASC_Network *net,
    T_ASC_Association *subAssoc,
    T_DIMSE_BlockingMode blockMode, int timeout)
{
    T_ASC_Association *assocList[2];
    int assocCount = 0;

    if (net != NULL && subAssoc == NULL) {
        if (ASC_associationWaiting(net, 0)) {
            /* association request waiting on network */
            return 2;
        }
    }
    assocList[0] = assoc;
    assocCount = 1;
    assocList[1] = subAssoc;
    if (subAssoc != NULL) assocCount++;
    if (subAssoc == NULL) {
        timeout = 1;            /* poll wait until an assoc req or move rsp */
    } else {
        if (blockMode == DIMSE_BLOCKING) {
            timeout = 10000;    /* a long time */
        }
    }
    if (!ASC_selectReadableAssociation(assocList, assocCount, timeout)) {
        /* none readable */
        return 0;
    }
    if (assocList[0] != NULL) {
        /* main association readable */
        return 1;
    }
    if (assocList[1] != NULL) {
        /* sub association readable */
        return 2;
    }
    /* should not be reached */
    return 0;
}

OFCondition
DIMSE_moveUser(
    /* in */
    T_ASC_Association *assoc,
    T_ASC_PresentationContextID presID,
    T_DIMSE_C_MoveRQ *request,
    DcmDataset *requestIdentifiers,
    DIMSE_MoveUserCallback callback, void *callbackData,
    /* blocking info for response */
    T_DIMSE_BlockingMode blockMode, int timeout,
    /* sub-operation provider callback */
    T_ASC_Network *net,
    DIMSE_SubOpProviderCallback subOpCallback, void *subOpCallbackData,
    /* out */
    T_DIMSE_C_MoveRSP *response, DcmDataset **statusDetail,
    DcmDataset **rspIds,
    OFBool ignorePendingDatasets)
{
    T_DIMSE_Message req, rsp;
    DIC_US msgId;
    int responseCount = 0;
    T_ASC_Association *subAssoc = NULL;
    DIC_US status = STATUS_MOVE_Pending_SubOperationsAreContinuing;
    OFBool firstLoop = OFTrue;

    if (requestIdentifiers == NULL) return DIMSE_NULLKEY;

    memset((char*)&req, 0, sizeof(req));
    memset((char*)&rsp, 0, sizeof(rsp));

    req.CommandField = DIMSE_C_MOVE_RQ;
    request->DataSetType = DIMSE_DATASET_PRESENT;
    req.msg.CMoveRQ = *request;

    msgId = request->MessageID;

    OFCondition cond = DIMSE_sendMessageUsingMemoryData(assoc, presID, &req, NULL, requestIdentifiers, NULL, NULL);
    if (cond != EC_Normal) {
        return cond;
    }

    /* receive responses */

    OFTimer timer;
    while (cond == EC_Normal && status == STATUS_MOVE_Pending_SubOperationsAreContinuing) {

        /* if user wants, multiplex between net/subAssoc
         * and move responses over main assoc.
         */
        switch (selectReadable(assoc, net, subAssoc, blockMode, timeout)) {
        case 0:
            /* none are readable, timeout */
            if ((blockMode == DIMSE_BLOCKING) || firstLoop) {
                firstLoop = OFFalse;
            } else if ((blockMode == DIMSE_NONBLOCKING) && (timer.getDiff() > timeout)) {
                DCMNET_DEBUG("timeout of " << timeout << " seconds elapsed while waiting for C-MOVE Responses");
                return DIMSE_NODATAAVAILABLE;
            }
            continue;    /* continue with main loop */
        case 1:
            /* main association readable */
            firstLoop = OFFalse;
            break;
        case 2:
            /* net/subAssoc readable */
            if (subOpCallback) {
                subOpCallback(subOpCallbackData, net, &subAssoc);
            }
            firstLoop = OFFalse;
            continue;    /* continue with main loop */
        }

        memset((char*)&rsp, 0, sizeof(rsp));

        cond = DIMSE_receiveCommand(assoc, blockMode, timeout, &presID, &rsp, statusDetail);
        if (cond != EC_Normal) {
            return cond;
        }
        if (rsp.CommandField != DIMSE_C_MOVE_RSP) {
            char buf1[256];
            OFStandard::snprintf(buf1, sizeof(buf1), "DIMSE: Unexpected Response Command Field: 0x%x", (unsigned)rsp.CommandField);
            return makeDcmnetCondition(DIMSEC_UNEXPECTEDRESPONSE, OF_error, buf1);
        }

        *response = rsp.msg.CMoveRSP;

        if (response->MessageIDBeingRespondedTo != msgId) {
            char buf2[256];
            OFStandard::snprintf(buf2, sizeof(buf2), "DIMSE: Unexpected Response MsgId: %d (expected: %d)", response->MessageIDBeingRespondedTo, msgId);
            return makeDcmnetCondition(DIMSEC_UNEXPECTEDRESPONSE, OF_error, buf2);
        }

        status = response->DimseStatus;
        responseCount++;

        switch (status) {
        case STATUS_MOVE_Pending_SubOperationsAreContinuing:
            if (*statusDetail != NULL) {
                DCMNET_WARN(DIMSE_warn_str(assoc) << "moveUser: Pending with statusDetail, ignoring detail");
                delete *statusDetail;
                *statusDetail = NULL;
            }
            if (response->DataSetType != DIMSE_DATASET_NULL) {
                DCMNET_WARN(DIMSE_warn_str(assoc) << "moveUser: Status Pending, but DataSetType!=NULL");
                if (! ignorePendingDatasets) {
                    // Some systems send an (illegal) dataset following C-MOVE-RSP messages
                    // with pending status, which is a protocol violation, but we need to
                    // handle this nevertheless. The MV300 has been reported to exhibit
                    // this behavior.
                    DCMNET_WARN(DIMSE_warn_str(assoc) << "Reading but ignoring response identifier set");
                    DcmDataset *tempset = NULL;
                    cond = DIMSE_receiveDataSetInMemory(assoc, blockMode, timeout, &presID, &tempset, NULL, NULL);
                    delete tempset;
                    if (cond != EC_Normal) {
                        return cond;
                    }
                } else {
                    // The alternative is to assume that the command set is wrong
                    // and not to read a dataset from the network association.
                    DCMNET_WARN(DIMSE_warn_str(assoc) << "Assuming NO response identifiers are present");
                }
            }

            /* execute callback */
            if (callback) {
                callback(callbackData, request, responseCount, response);
            }
            break;
        default:
            if (response->DataSetType != DIMSE_DATASET_NULL) {
                cond = DIMSE_receiveDataSetInMemory(assoc, blockMode, timeout, &presID, rspIds, NULL, NULL);
                if (cond != EC_Normal) {
                    return cond;
                }
            }
            break;
        }
        /* reset the timeout timer */
        timer.reset();
    }

    /* do remaining sub-association work, we may receive a non-pending
     * status before the sub-association has cleaned up.
     */
    while (subAssoc != NULL) {
        if (subOpCallback) {
            subOpCallback(subOpCallbackData, net, &subAssoc);
        }
    }

    return cond;
}

OFCondition
DIMSE_sendMoveResponse(
    T_ASC_Association *assoc,
    T_ASC_PresentationContextID presID,
    const T_DIMSE_C_MoveRQ *request,
    T_DIMSE_C_MoveRSP *response, DcmDataset *rspIds,
    DcmDataset *statusDetail)
{
    OFCondition cond = EC_Normal;
    T_DIMSE_Message rsp;
    unsigned int opts;

    memset((char*)&rsp, 0, sizeof(rsp));
    rsp.CommandField = DIMSE_C_MOVE_RSP;
    rsp.msg.CMoveRSP = *response;
    /* copy over stuff from request */
    rsp.msg.CMoveRSP.MessageIDBeingRespondedTo = request->MessageID;
    /* always send affected sop class uid */
    OFStandard::strlcpy(rsp.msg.CMoveRSP.AffectedSOPClassUID, request->AffectedSOPClassUID, sizeof(rsp.msg.CMoveRSP.AffectedSOPClassUID));
    rsp.msg.CMoveRSP.opts = O_MOVE_AFFECTEDSOPCLASSUID;

    switch (response->DimseStatus) {
    case STATUS_MOVE_Success:
    case STATUS_MOVE_Pending_SubOperationsAreContinuing:
        /* Success cannot have a Failed SOP Instance UID list (no failures).
         * Pending may not send such a list.
         */
        rsp.msg.CMoveRSP.DataSetType = DIMSE_DATASET_NULL;
        rspIds = NULL;  /* zero our local pointer */
        break;
    default:
        /* send it if provided */
        if (rspIds == NULL) rsp.msg.CMoveRSP.DataSetType = DIMSE_DATASET_NULL;
        else rsp.msg.CMoveRSP.DataSetType = DIMSE_DATASET_PRESENT;
        break;
    }

    /*
     * Make sure the numberOf fields are conformant with
     * the status (see Part 4, C.4.2.1.6-9)
     */
    opts = (O_MOVE_NUMBEROFREMAININGSUBOPERATIONS |
            O_MOVE_NUMBEROFCOMPLETEDSUBOPERATIONS |
            O_MOVE_NUMBEROFFAILEDSUBOPERATIONS |
            O_MOVE_NUMBEROFWARNINGSUBOPERATIONS);

    switch (response->DimseStatus) {
    case STATUS_MOVE_Pending_SubOperationsAreContinuing:
    case STATUS_MOVE_Cancel_SubOperationsTerminatedDueToCancelIndication:
        break;
    default:
        /* Remaining sub-operations may not be in responses
         * with a status of Warning, Failed, Refused or Successful
         */
        opts &= (~ O_MOVE_NUMBEROFREMAININGSUBOPERATIONS);
        break;
    }

    rsp.msg.CMoveRSP.opts |= opts;

    cond = DIMSE_sendMessageUsingMemoryData(assoc, presID, &rsp, statusDetail, rspIds, NULL, NULL);

    return cond;

}

OFCondition
DIMSE_moveProvider(
    /* in */
    T_ASC_Association *assoc,
    T_ASC_PresentationContextID presIdCmd,
    T_DIMSE_C_MoveRQ *request,
    DIMSE_MoveProviderCallback callback, void *callbackData,
    /* blocking info for data set */
    T_DIMSE_BlockingMode blockMode, int timeout)
{
    OFCondition cond = EC_Normal;
    T_ASC_PresentationContextID presIdData;
    DcmDataset *statusDetail = NULL;
    DcmDataset *reqIds = NULL;
    DcmDataset *rspIds = NULL;
    OFBool cancelled = OFFalse;
    OFBool normal = OFTrue;
    int responseCount = 0;
    T_DIMSE_C_MoveRSP rsp;

    cond = DIMSE_receiveDataSetInMemory(assoc, blockMode, timeout, &presIdData, &reqIds, NULL, NULL);

    if (cond.good()) {
        if (presIdData != presIdCmd) {
          cond = makeDcmnetCondition(DIMSEC_INVALIDPRESENTATIONCONTEXTID, OF_error, "DIMSE: Presentation Contexts of Command and Data Differ");
        } else {
            memset((char*)&rsp, 0, sizeof(rsp));
            rsp.DimseStatus = STATUS_MOVE_Pending_SubOperationsAreContinuing;   /* assume */

            while (cond == EC_Normal && rsp.DimseStatus == STATUS_MOVE_Pending_SubOperationsAreContinuing && normal) {
                responseCount++;

                cond = DIMSE_checkForCancelRQ(assoc, presIdCmd, request->MessageID);
                if (cond == EC_Normal) {
                    /* cancel received */
                    rsp.DimseStatus = STATUS_MOVE_Cancel_SubOperationsTerminatedDueToCancelIndication;
                    cancelled = OFTrue;
                } else if (cond == DIMSE_NODATAAVAILABLE) {
                    /* timeout */
                } else {
                    /* some exception condition occurred, bail out */
                    normal = OFFalse;
                }

                if (normal) {
                     if (callback) {
                         callback(callbackData, cancelled, request, reqIds, responseCount, &rsp, &statusDetail, &rspIds);
                     } else {
                         return makeDcmnetCondition(DIMSEC_NULLKEY, OF_error, "DIMSE_moveProvider: no callback function");
                     }

                     if (cancelled) {
                         /* make sure */
                         rsp.DimseStatus = STATUS_MOVE_Cancel_SubOperationsTerminatedDueToCancelIndication;
                         if (rspIds != NULL) {
                             delete reqIds;
                             reqIds = NULL;
                         }
                     }

                     cond = DIMSE_sendMoveResponse(assoc, presIdCmd, request, &rsp, rspIds, statusDetail);

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

    delete reqIds;
    delete rspIds;
    return cond;
}
