/*
 *
 *  Copyright (C) 1996-2002, OFFIS
 *
 *  This software and supporting documentation were developed by
 *
 *    Kuratorium OFFIS e.V.
 *    Healthcare Information and Communication Systems
 *    Escherweg 2
 *    D-26121 Oldenburg, Germany
 *
 *  THIS SOFTWARE IS MADE AVAILABLE,  AS IS,  AND OFFIS MAKES NO  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.
 *
 *  Module:  dcmimgle
 *
 *  Author:  Joerg Riesmeier
 *
 *  Purpose: DicomImage-Interface (Source)
 *
 */


#include "osconfig.h"
#include "dctypes.h"
#include "dcdeftag.h"
#include "dcobject.h"
#include "dcuid.h"
#include "dcdict.h"

#include "dcmimage.h"
#include "diovlimg.h"
#include "dimo1img.h"
#include "dimo2img.h"
#include "didocu.h"
#include "diregbas.h"
#include "diplugin.h"

#define INCLUDE_CCTYPE
#include "ofstdinc.h"

#ifndef FILENAME_MAX
#define FILENAME_MAX 255
#endif


/*------------------*
 *  initialization  *
 *------------------*/

DiRegisterBase *DiRegisterBase::Pointer = NULL;


/*----------------*
 *  constructors  *
 *----------------*/

// --- create 'DicomImage' from 'filename', for valid 'flags' see 'diutils.h'

DicomImage::DicomImage(const char *filename,
                       const unsigned long flags,
                       const unsigned long fstart,
                       const unsigned long fcount)
  : ImageStatus(EIS_Normal),
    PhotometricInterpretation(EPI_Unknown),
    Document(NULL),
    Image(NULL)
{
    if (checkDataDictionary())                  // valid 'dicom.dic' found ?
    {
        Document = new DiDocument(filename, flags | CIF_MayDetachPixelData, fstart, fcount);
        Init();
    }
}


// --- create 'DicomImage' from valid 'DicomObject' with transfer syntax 'xfer'

DicomImage::DicomImage(DcmObject *object,
                       const E_TransferSyntax xfer,
                       const unsigned long flags,
                       const unsigned long fstart,
                       const unsigned long fcount)
  : ImageStatus(EIS_Normal),
    PhotometricInterpretation(EPI_Unknown),
    Document(NULL),
    Image(NULL)
{
    if (checkDataDictionary())                  // valid 'dicom.dic' found ?
    {
        Document = new DiDocument(object, xfer, flags, fstart, fcount);
        Init();
    }
}


// --- create 'DicomImage' from valid 'DicomObject' with given rescale 'slope' and 'intercept'

DicomImage::DicomImage(DcmObject *object,
                       const E_TransferSyntax xfer,
                       const double slope,
                       const double intercept,
                       const unsigned long flags,
                       const unsigned long fstart,
                       const unsigned long fcount)
  : ImageStatus(EIS_Normal),
    PhotometricInterpretation(EPI_Unknown),
    Document(NULL),
    Image(NULL)
{
    if (checkDataDictionary())                  // valid 'dicom.dic' found ?
    {
        Document = new DiDocument(object, xfer, flags, fstart, fcount);
        if ((Document != NULL) && (Document->good()))
        {
            PhotometricInterpretation = EPI_Monochrome2;            // default for presentation states
            Image = new DiMono2Image(Document, ImageStatus, slope, intercept);
        }
    }
}


// --- create 'DicomImage' from valid 'DicomObject' with given modality LUT (specified by 'data' and 'descriptor')

DicomImage::DicomImage(DcmObject *object,
                       E_TransferSyntax xfer,
                       const DcmUnsignedShort &data,
                       const DcmUnsignedShort &descriptor,
                       const DcmLongString *explanation,
                       const unsigned long flags,
                       const unsigned long fstart,
                       const unsigned long fcount)
  : ImageStatus(EIS_Normal),
    PhotometricInterpretation(EPI_Unknown),
    Document(NULL),
    Image(NULL)
{
    if (checkDataDictionary())                  // valid 'dicom.dic' found ?
    {
        Document = new DiDocument(object, xfer, flags, fstart, fcount);
        if ((Document != NULL) && (Document->good()))
        {
            PhotometricInterpretation = EPI_Monochrome2;            // default for presentation states
            Image = new DiMono2Image(Document, ImageStatus, data, descriptor, explanation);
        }
    }
}


// --- protected: create 'DicomImage' from source with different image data and photometric interpretation

DicomImage::DicomImage(const DicomImage *dicom,
                       DiImage *image,
                       const EP_Interpretation interpret)
  : ImageStatus(dicom->ImageStatus),
    PhotometricInterpretation(dicom->PhotometricInterpretation),
    Document(dicom->Document),                   // necessary, ever used ??
    Image(image)
{
    if (interpret != EPI_Unknown)
        PhotometricInterpretation = interpret;
    if (Document != NULL)
        Document->addReference();               // 'Document' is only referenced not copied !
}


/*--------------*
 *  destructor  *
 *--------------*/

DicomImage::~DicomImage()
{
    delete Image;
    if (Document != NULL)
        Document->removeReference();            // only delete if object is no longer referenced
}


/*********************************************************************/


// --- initialize 'DicomImage' object (same procedure for every 'real' constructor)

void DicomImage::Init()
{
    if ((Document != NULL) && (Document->good()))
    {
        const char *str;
        if (hasSOPclassUID(UID_StandaloneOverlayStorage))
        {
            PhotometricInterpretation = EPI_Monochrome2;            // standalone overlays are handled like monochrome
            Image = new DiOverlayImage(Document, ImageStatus);      // images without pixel data
        }
        else if (Document->getFlags() & CIF_UsePresentationState)
        {
            PhotometricInterpretation = EPI_Monochrome2;            // default for presentation states
            Image = new DiMono2Image(Document, ImageStatus);
        }
        else if (Document->getValue(DCM_PhotometricInterpretation, str))
        {
            char *cstr = new char[strlen(str) + 1];
            if (cstr != NULL)
            {
                char *q = cstr;
                for (const char *p = str; *p != 0; p++)             // remove invalid chars
                {
                    if (isalpha(*p))
                        *(q++) = toupper(*p);
                    else if (isdigit(*p))
                        *(q++) = *p;
                }
                *q = '\0';                                          // end of c-string
            }
            else
                cstr = (char *)str;
            const SP_Interpretation *pin = PhotometricInterpretationNames;
            while ((pin->Name != NULL) && (strcmp(pin->Name, cstr) != 0))
                pin++;
            if (cstr != str)
                delete[] cstr;
            PhotometricInterpretation = pin->Type;                  // unknown if last entry
            switch (PhotometricInterpretation)
            {
                case EPI_Monochrome1:                               // create 'Image' depending on color model
                    Image = new DiMono1Image(Document, ImageStatus);
                    break;
                case EPI_Monochrome2:
                    Image = new DiMono2Image(Document, ImageStatus);
                    break;
                default:                                            // unknown or unsupported color model
                    if (DiRegisterBase::Pointer != NULL)
                        Image = DiRegisterBase::Pointer->createImage(Document, ImageStatus, PhotometricInterpretation);
                    if (Image == NULL)
                    {
                        if (PhotometricInterpretation == EPI_Unknown)
                        {
                            ImageStatus = EIS_InvalidValue;
                            if (DicomImageClass::checkDebugLevel(DicomImageClass::DL_Errors))
                            {
                                ofConsole.lockCerr() << "ERROR: invalid value for 'PhotometricInterpretation' (" << str << ") !" << endl;
                                ofConsole.unlockCerr();
                            }
                        } else {
                            ImageStatus = EIS_NotSupportedValue;
                            if (DicomImageClass::checkDebugLevel(DicomImageClass::DL_Errors))
                            {
                                ofConsole.lockCerr() << "ERROR: unsupported value for 'PhotometricInterpretation' (" << str << ") !" << endl
                                                     << "       library 'dcmimage' required to handle color images !" << endl;
                                ofConsole.unlockCerr();
                            }
                        }
                    }
            }
        }
        else if (Document->getFlags() & CIF_AcrNemaCompatibility)    // ACR-NEMA has no 'photometric interpretation'
        {
            PhotometricInterpretation = EPI_Monochrome2;
            Image = new DiMono2Image(Document, ImageStatus);
        }
        else
        {
            ImageStatus = EIS_MissingAttribute;
            if (DicomImageClass::checkDebugLevel(DicomImageClass::DL_Errors))
            {
                ofConsole.lockCerr() << "ERROR: mandatory attribute 'PhotometricInterpretation' is missing !" << endl;
                ofConsole.unlockCerr();
            }
        }
    }
    else
        ImageStatus = EIS_InvalidDocument;
}


// --- check whether the loadable 'DataDictionary' is present/loaded

int DicomImage::checkDataDictionary()
{
    if (!dcmDataDict.isDictionaryLoaded())
    {
        ImageStatus = EIS_NoDataDictionary;
        if (DicomImageClass::checkDebugLevel(DicomImageClass::DL_Errors))
        {
            ofConsole.lockCerr() << "ERROR: can't load data dictionary !" << endl;
            ofConsole.unlockCerr();
        }
    }
    return ImageStatus == EIS_Normal;
}


/*********************************************************************/


const char *DicomImage::getString(const EI_Status status)
{
    switch (status)
    {
      case EIS_Normal:
        return "Status OK";
      case EIS_NoDataDictionary:
        return "No data dictionary";
      case EIS_InvalidDocument:
        return "Invalid DICOM document";
      case EIS_MissingAttribute:
        return "Missing attribute";
      case EIS_InvalidValue:
        return "Invalid data value";
      case EIS_NotSupportedValue:
        return "Unsupported data value";
      case EIS_MemoryFailure:
        return "Out of memory";
      case EIS_InvalidImage:
        return "Invalid DICOM image";
      case EIS_OtherError:
      default:
        return "Unspecified";
    }
}


const char *DicomImage::getString(const EP_Interpretation interpret)
{
    const SP_Interpretation *pin = PhotometricInterpretationNames;
    while ((pin->Name != NULL) && (pin->Type != interpret))
        pin++;
    return pin->Name;
}


// --- return unique 'SOPclassUID' string

const char *DicomImage::getSOPclassUID() const
{
    if (Document != NULL)
    {
        const char *str;
        if (Document->getValue(DCM_SOPClassUID, str))
            return str;
    }
    return NULL;
}


// --- return 'true' (1) if 'Document' has the same 'SOPclassUID' as given in parameter 'uid'

int DicomImage::hasSOPclassUID(const char *uid) const
{
    const char *str = getSOPclassUID();
    return (str != NULL) && (strcmp(str, uid) == 0);
}


// --- create new 'DicomImage' with 'fcount' frames starting with frame 'fstart'

DicomImage *DicomImage::createDicomImage(unsigned long fstart,
                                         unsigned long fcount) const
{
    if ((Image != NULL) && (fstart < getFrameCount()))
    {
        if ((fcount == 0) || (fstart + fcount > getFrameCount()))
            fcount = getFrameCount() - fstart;
        DiImage *image = Image->createImage(fstart, fcount);
        if (image != NULL)
        {
            DicomImage *dicom = new DicomImage(this, image);
            return dicom;
        }
    }
    return NULL;
}


// --- create scaled to given size ('width' and 'height') image, memory isn't handled internally !
// --- if one dimension ist '0' the other is automatically adjusted (with respect to pixel aspect ratio)

DicomImage *DicomImage::createScaledImage(const unsigned long width,
                                          const unsigned long height,
                                          const int interpolate,
                                          int aspect) const
{
    return createScaledImage(0, 0, getWidth(), getHeight(), width, height, interpolate, aspect);
}


// --- create scaled with given factors ('xfactor' and 'yfactor') image, memory isn't handled internally !

DicomImage *DicomImage::createScaledImage(const double xfactor,
                                          const double yfactor,
                                          const int interpolate,
                                          const int aspect) const
{
    return createScaledImage(0, 0, getWidth(), getHeight(), (unsigned long)(xfactor * getWidth()),
        (unsigned long)(yfactor * getHeight()), interpolate, aspect);
}


// --- clip & scale

DicomImage *DicomImage::createScaledImage(const signed long left_pos,
                                          const signed long top_pos,
                                          unsigned long clip_width,
                                          unsigned long clip_height,
                                          unsigned long scale_width,
                                          unsigned long scale_height,
                                          const int interpolate,
                                          int aspect,
                                          const Uint16 pvalue) const
{
    const unsigned long gw = getWidth();
    const unsigned long gh = getHeight();
    if ((Image != NULL) && (gw > 0) && (gh > 0))
    {
        if (clip_width == 0)                                         // set 'width' if parameter is missing
            clip_width = gw - left_pos;
        if (clip_height == 0)                                        // same for 'height'
            clip_height = gh - top_pos;
        if ((scale_width == 0) && (scale_height == 0))
        {
            scale_width = clip_width;                                // auto-set width/height
            scale_height = clip_height;
        }
        if (aspect)                                                  // maintain pixel aspect ratio
        {
            if (scale_width == 0)
                scale_width = (unsigned long)(getWidthHeightRatio() * (double)(scale_height * gw) / gh);
            else if (scale_height == 0)
                scale_height = (unsigned long)(getHeightWidthRatio() * (double)(scale_width * gh) / gw);
            else
                aspect = 0;                                           // ignore pixel aspect ratio
        }
        else                                                          // ignore pixel aspect ratio
        {
            if (scale_width == 0)
                scale_width = (unsigned long)((double)(scale_height * gw) / gh);
            else if (scale_height == 0)
                scale_height = (unsigned long)((double)(scale_width * gh) / gw);
        }
        const unsigned long maxvalue = DicomImageClass::maxval(bitsof(Uint16));
        if (scale_width > maxvalue)
            scale_width = maxvalue;                                   // limit 'width' to maximum value (65535)
        if (scale_height > maxvalue)
            scale_height = maxvalue;                                  // same for 'height'

        /* need to limit clipping region ... !? */

        if (((left_pos < 0) || (top_pos < 0) || ((unsigned long)(left_pos + clip_width) > gw) ||
            ((unsigned long)(top_pos + clip_height) > gh)) &&
            ((clip_width != scale_width) || (clip_height != scale_height)))
        {
            if (DicomImageClass::checkDebugLevel(DicomImageClass::DL_Errors))
            {
                ofConsole.lockCerr() << "ERROR: combined clipping & scaling outside image boundaries not yet supported !" << endl;
                ofConsole.unlockCerr();
            }
        }
        else if ((scale_width > 0) && (scale_height > 0))
        {
            DiImage *image = Image->createScale(left_pos, top_pos, clip_width, clip_height, scale_width, scale_height,
                interpolate, aspect, pvalue);
            if (image != NULL)
            {
                DicomImage *dicom = new DicomImage(this, image);
                return dicom;
            }
        }
    }
    return NULL;
}


// --- clip & scale

DicomImage *DicomImage::createScaledImage(const signed long left_pos,
                                          const signed long top_pos,
                                          unsigned long width,
                                          unsigned long height,
                                          const double xfactor,
                                          const double yfactor,
                                          const int interpolate,
                                          const int aspect,
                                          const Uint16 pvalue) const
{
    if ((xfactor >= 0) && (yfactor >= 0))
    {
        const unsigned long gw = getWidth();
        const unsigned long gh = getHeight();
        if (width == 0)                                     // set 'width' if parameter is missing (0)
            width = gw - left_pos;
        if (height == 0)                                    // same for 'height'
            height = gh - top_pos;
        return createScaledImage(left_pos, top_pos, width, height, (unsigned long)(xfactor * width),
            (unsigned long)(yfactor * height), interpolate, aspect, pvalue);
    }
    return NULL;
}


// --- create clipped to given box ('left_pos', 'top_pos' and 'width', 'height') image,
// ---- memory isn't handled internally! 'width' and 'height' are optional

DicomImage *DicomImage::createClippedImage(const signed long left_pos,
                                           const signed long top_pos,
                                           unsigned long width,
                                           unsigned long height,
                                           const Uint16 pvalue) const
{
    return createScaledImage(left_pos, top_pos, width, height, (unsigned long)0, (unsigned long)0, 0, 0, pvalue);
}


// --- flip image (horizontal: x > 1 and/or vertical y > 1)

int DicomImage::flipImage(int horz,
                          int vert) const
{
    if ((Image != NULL) && (horz || vert))
    {
        if (getWidth() <= 1)
            horz = 0;
        if (getHeight() <= 1)
            vert = 0;
        if (horz || vert)
            return Image->flip(horz, vert);
        else
            return 2;
    }
    return 0;
}


// --- create flipped image (horizontal: x > 1 and/or vertical y > 1), memory isn't handled internally !

DicomImage *DicomImage::createFlippedImage(int horz,
                                           int vert) const
{
    if ((Image != NULL) && (horz || vert))
    {
        if (getWidth() <= 1)                                        // can't flip horizontally
            horz = 0;
        if (getHeight() <= 1)                                       // can't flip vertically
            vert = 0;
        DiImage *image;
        if (horz || vert)                                           // flip at least one axis
            image = Image->createFlip(horz, vert);
        else                                                        // copy image
            image = Image->createImage(0, getFrameCount());
        if (image != NULL)
        {
            DicomImage *dicom = new DicomImage(this, image);
            return dicom;
        }
    }
    return NULL;
}


// -- normalize given 'degree' value to 0, 90, 180, 270

int DicomImage::normalizeDegreeValue(signed int &degree) const
{
    switch (degree)
    {
        case 0:
        case 360:
        case -360:
            degree = 0;
            return 1;
        case 90:
        case -270:
            degree = 90;
            return 1;
        case 180:
        case -180:
            degree = 180;
            return 1;
        case 270:
        case -90:
            degree = 270;
            return 1;
        default:
            return 0;
    }
}


// --- rotate image by given 'degree'

int DicomImage::rotateImage(signed int degree) const
{
    if ((Image != NULL) && normalizeDegreeValue(degree))
    {
        if ((degree == 0) || (getWidth() * getHeight() <= 1))       // nothing to do
            return 2;
        else
            return Image->rotate((int)degree);
    }
    return 0;
}


// --- create by given 'degree' rotated image, memory isn't handled internally !

DicomImage *DicomImage::createRotatedImage(signed int degree) const
{
    if ((Image != NULL) && normalizeDegreeValue(degree))
    {
        DiImage *image = Image->createRotate((int)degree);
        if (image != NULL)
        {
            DicomImage *dicom = new DicomImage(this, image);
            return dicom;
        }
    }
    return NULL;
}


// --- create color-image to mono-image with given 'red', 'green' and 'blue' coefficients converted image, memory ... !

DicomImage *DicomImage::createMonochromeImage(const double red,
                                              const double green,
                                              const double blue) const
{
    if (Image != NULL)
    {
        DiImage *image = Image->createMono(red, green, blue);       // create monochrome image data
        if (image != NULL)
        {
            DicomImage *dicom = new DicomImage(this, image, EPI_Monochrome2);
            return dicom;
        }
    }
    return NULL;
}


// --- create monochrome output image of specified frame (incl. windowing)

DicomImage *DicomImage::createMonoOutputImage(const unsigned long frame,
                                              const int bits)
{
    if ((Image != NULL) && (Image->getMonoImagePtr() != NULL))
    {
        DiImage *image = Image->getMonoImagePtr()->createOutputImage(frame, bits);
        if (image != NULL)
        {
            DicomImage *dicom = new DicomImage(this, image, EPI_Monochrome2);
            return dicom;
        }
    }
    return NULL;
}


/*********************************************************************/


// --- write 'frame' of image data to 'filename' with 'bits' depth

int DicomImage::writePPM(const char *filename,
                         const int bits,
                         const unsigned long frame)
{
    if ((filename != NULL) && (Image != NULL))
    {
        char fname[FILENAME_MAX + 1];
        if (sprintf(fname, filename, frame) >= 0)           // replace '%d' etc. with frame number
            filename = fname;
        FILE *stream = fopen(filename, "w");                // open text file for writing
        int ok = writePPM(stream, bits, frame);
        fclose(stream);
        return ok;
    }
    return 0;
}


// --- same for C++ 'ostream'

int DicomImage::writePPM(ostream &stream,
                         const int bits,
                         const unsigned long frame)
{
    if ((stream.good()) && (Image != NULL))
        return Image->writePPM(stream, frame, Image->getBits(bits));
    return 0;
}


// --- same for C 'FILE'

int DicomImage::writePPM(FILE *stream,
                         const int bits,
                         const unsigned long frame)
{
    if ((stream != NULL) && (Image != NULL))
        return Image->writePPM(stream, frame, Image->getBits(bits));
    return 0;
}


// --- same for RAW PPM (binary form of PPM with a maximum of 8 bits depth)

int DicomImage::writeRawPPM(const char *filename,
                            const int bits,
                            const unsigned long frame)
{
    if ((filename != NULL) && (Image != NULL) && (Image->getBits(bits) <= MAX_RAWPPM_BITS))
    {
        char fname[FILENAME_MAX + 1];
        if (sprintf(fname, filename, frame) >= 0)           // replace '%d' etc. with frame number
            filename = fname;
        FILE *stream = fopen(filename, "wb");               // open binary file for writing
        if (stream != NULL)
        {
            int ok = Image->writeRawPPM(stream, frame, Image->getBits(bits));
            fclose(stream);
            return ok;
        }
    }
    return 0;
}

// --- same for C 'FILE'

int DicomImage::writeRawPPM(FILE *stream,
                            const int bits,
                            const unsigned long frame)
{
    if ((stream != NULL) && (Image != NULL))
        return Image->writeRawPPM(stream, frame, Image->getBits(bits));
    return 0;
}


// --- write 'frame' of image data to 'filename' with 'bits' depth in BMP format

int DicomImage::writeBMP(const char *filename,
                         const int bits,
                         const unsigned long frame)
{
    if ((filename != NULL) && (Image != NULL) && ((bits == 0) || ((bits == 8) && isMonochrome()) || (bits == 24)))
    {
        char fname[FILENAME_MAX + 1];
        if (sprintf(fname, filename, frame) >= 0)           // replace '%d' etc. with frame number
            filename = fname;
        FILE *stream = fopen(filename, "wb");               // open binary file for writing
        if (stream != NULL)
        {
            int ok = Image->writeBMP(stream, frame, bits);
            fclose(stream);
            return ok;
        }
    }
    return 0;
}


// --- same for open C 'FILE' in BMP format

int DicomImage::writeBMP(FILE *stream,
                         const int bits,
                         const unsigned long frame)
{
    if ((stream != NULL) && (Image != NULL) && ((bits == 0) || ((bits == 8) && isMonochrome()) || (bits == 24)))
        return Image->writeBMP(stream, frame, bits);
    return 0;
}


// --- write 'frame' of image data to 'filename' plugable image format

int DicomImage::writePluginFormat(const DiPluginFormat *plugin,
                                  const char *filename,
                                  const unsigned long frame)
{
    if ((plugin != NULL) && (filename != NULL) && (Image != NULL))
    {
        char fname[FILENAME_MAX + 1];
        if (sprintf(fname, filename, frame) >= 0)           // replace '%d' etc. with frame number
            filename = fname;
        FILE *stream = fopen(filename, "wb");               // open binary file for writing
        if (stream != NULL)
        {
            int ok = plugin->write(Image, stream, frame);
            fclose(stream);
            return ok;
        }
    }
    return 0;
}


// --- same for open C 'FILE' in plugable image format

int DicomImage::writePluginFormat(const DiPluginFormat *plugin,
                                  FILE *stream,
                                  const unsigned long frame)
{
    if ((plugin != NULL) && (stream != NULL) && (Image != NULL))
        return plugin->write(Image, stream, frame);
    return 0;
}
