/**
 * 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"

#ifndef NOMINMAX
#define NOMINMAX
#endif

#include "DicomImageInformation.h"

#include "../Compatibility.h"
#include "../Constants.h"
#include "../Logging.h"
#include "../OrthancException.h"
#include "../SerializationToolbox.h"
#include "../Toolbox.h"

#include <boost/lexical_cast.hpp>
#include <limits>
#include <cassert>
#include <stdio.h>
#include <memory>


namespace Orthanc
{
  DicomImageInformation::DicomImageInformation(const DicomMap& values)
  {
    std::string sopClassUid;
    if (values.LookupStringValue(sopClassUid, DICOM_TAG_SOP_CLASS_UID, false))
    {
      sopClassUid = Toolbox::StripSpaces(sopClassUid);
      if (sopClassUid == "1.2.840.10008.5.1.4.1.1.481.3" /* RT-STRUCT */)
      {
        LOG(WARNING) << "Orthanc::DicomImageInformation() should not be applied to SOP Class UID: " << sopClassUid;
      }
    }

    uint32_t pixelRepresentation = 0;
    uint32_t planarConfiguration = 0;

    try
    {
      std::string p;
      if (values.LookupStringValue(p, DICOM_TAG_PHOTOMETRIC_INTERPRETATION, false)) {
        Toolbox::ToUpperCase(p);

        if (p == "RGB")
        {
          photometric_ = PhotometricInterpretation_RGB;
        }
        else if (p == "MONOCHROME1")
        {
          photometric_ = PhotometricInterpretation_Monochrome1;
        }
        else if (p == "MONOCHROME2")
        {
          photometric_ = PhotometricInterpretation_Monochrome2;
        }
        else if (p == "PALETTE COLOR")
        {
          photometric_ = PhotometricInterpretation_Palette;
        }
        else if (p == "HSV")
        {
          photometric_ = PhotometricInterpretation_HSV;
        }
        else if (p == "ARGB")
        {
          photometric_ = PhotometricInterpretation_ARGB;
        }
        else if (p == "CMYK")
        {
          photometric_ = PhotometricInterpretation_CMYK;
        }
        else if (p == "YBR_FULL")
        {
          photometric_ = PhotometricInterpretation_YBRFull;
        }
        else if (p == "YBR_FULL_422")
        {
          photometric_ = PhotometricInterpretation_YBRFull422;
        }
        else if (p == "YBR_PARTIAL_420")
        {
          photometric_ = PhotometricInterpretation_YBRPartial420;
        }
        else if (p == "YBR_PARTIAL_422")
        {
          photometric_ = PhotometricInterpretation_YBRPartial422;
        }
        else if (p == "YBR_ICT")
        {
          photometric_ = PhotometricInterpretation_YBR_ICT;
        }
        else if (p == "YBR_RCT")
        {
          photometric_ = PhotometricInterpretation_YBR_RCT;
        }
        else
        {
          photometric_ = PhotometricInterpretation_Unknown;
        }
      }
      else
      {
        photometric_ = PhotometricInterpretation_Unknown;
      }

      values.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(width_); // in some US images, we've seen tag values of "800\0"; that's why we parse the 'first' value
      values.GetValue(DICOM_TAG_ROWS).ParseFirstUnsignedInteger(height_);

      if (height_ > 65535 || width_ > 65535)
      {
        throw OrthancException(ErrorCode_BadFileFormat, "Image width or height exceed DICOM VR US range (65535)");
      }

      if (!values.ParseUnsignedInteger32(bitsAllocated_, DICOM_TAG_BITS_ALLOCATED))
      {
        throw OrthancException(ErrorCode_BadFileFormat);
      }

      if (!values.ParseUnsignedInteger32(samplesPerPixel_, DICOM_TAG_SAMPLES_PER_PIXEL))
      {
        samplesPerPixel_ = 1;  // Assume 1 color channel
      }

      if (!values.ParseUnsignedInteger32(bitsStored_, DICOM_TAG_BITS_STORED))
      {
        bitsStored_ = bitsAllocated_;
      }

      if (bitsStored_ > bitsAllocated_)
      {
        throw OrthancException(ErrorCode_BadFileFormat);
      }

      if (!values.ParseUnsignedInteger32(highBit_, DICOM_TAG_HIGH_BIT))
      {
        highBit_ = bitsStored_ - 1;
      }

      if (!values.ParseUnsignedInteger32(pixelRepresentation, DICOM_TAG_PIXEL_REPRESENTATION))
      {
        pixelRepresentation = 0;  // Assume unsigned pixels
      }

      if (samplesPerPixel_ > 1)
      {
        // The "Planar Configuration" is only set when "Samples per Pixels" is greater than 1
        // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3

        if (!values.ParseUnsignedInteger32(planarConfiguration, DICOM_TAG_PLANAR_CONFIGURATION))
        {
          planarConfiguration = 0;  // Assume interleaved color channels
        }
      }
    }
    catch (boost::bad_lexical_cast&)
    {
      THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
    }
    catch (OrthancException&)
    {
      THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
    }

    
    if (values.HasTag(DICOM_TAG_NUMBER_OF_FRAMES))
    {
      if (!values.ParseUnsignedInteger32(numberOfFrames_, DICOM_TAG_NUMBER_OF_FRAMES))
      {
        THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_NotImplemented);
      }
    }
    else
    {
      numberOfFrames_ = 1;
    }

    if (bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
        bitsAllocated_ != 24 && bitsAllocated_ != 32 &&
        bitsAllocated_ != 1 /* new in Orthanc 1.10.0 */)
    {
      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast<std::string>(bitsAllocated_) + " bits allocated");
    }
    else if (numberOfFrames_ == 0)
    {
      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported (no frames)");
    }
    else if (planarConfiguration != 0 && planarConfiguration != 1)
    {
      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: planar configuration is " + boost::lexical_cast<std::string>(planarConfiguration));
    }

    if (samplesPerPixel_ == 0)
    {
      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0");
    }

    if (bitsStored_ == 1)
    {
      // This is the case of DICOM SEG, new in Orthanc 1.10.0
      if (bitsAllocated_ != 1)
      {
        throw OrthancException(ErrorCode_BadFileFormat);
      }
      else if (width_ % 8 != 0)
      {
        throw OrthancException(ErrorCode_BadFileFormat, "Bad number of columns for a black-and-white image");
      }
      else
      {
        bytesPerValue_ = 0;  // Arbitrary initialization
      }
    }
    else
    {
      bytesPerValue_ = bitsAllocated_ / 8;
    }

    isPlanar_ = (planarConfiguration != 0 ? true : false);
    isSigned_ = (pixelRepresentation != 0 ? true : false);

    // New in Orthanc 1.12.7
    double d;

    if (values.ParseDouble(d, DICOM_TAG_RESCALE_SLOPE))
    {
      rescaleSlope_ = d;
    }
    else
    {
      rescaleSlope_ = 1;
    }

    if (values.ParseDouble(d, DICOM_TAG_RESCALE_INTERCEPT))
    {
      rescaleIntercept_ = d;
    }
    else
    {
      rescaleIntercept_ = 0;
    }

    if (values.ParseDouble(d, DICOM_TAG_DOSE_GRID_SCALING))
    {
      rescaleSlope_ *= d;
    }

    const std::string centerTag = values.GetStringValue(DICOM_TAG_WINDOW_CENTER, "", false);
    const std::string widthTag = values.GetStringValue(DICOM_TAG_WINDOW_WIDTH, "", false);
    if (!centerTag.empty() &&
        !widthTag.empty())
    {
      std::vector<std::string> centers, widths;
      Toolbox::TokenizeString(centers, centerTag, '\\');
      Toolbox::TokenizeString(widths, widthTag, '\\');
      if (centers.size() == widths.size())
      {
        for (size_t i = 0; i < centers.size(); i++)
        {
          double center, width;
          if (SerializationToolbox::ParseDouble(center, centers[i]) &&
              SerializationToolbox::ParseDouble(width, widths[i]))
          {
            windows_.push_back(Window(center, width));
          }
        }
      }
    }
  }

  DicomImageInformation* DicomImageInformation::Clone() const
  {
    std::unique_ptr<DicomImageInformation> target(new DicomImageInformation);
    target->width_ = width_;
    target->height_ = height_;
    target->samplesPerPixel_ = samplesPerPixel_;
    target->numberOfFrames_ = numberOfFrames_;
    target->isPlanar_ = isPlanar_;
    target->isSigned_ = isSigned_;
    target->bytesPerValue_ = bytesPerValue_;
    target->bitsAllocated_ = bitsAllocated_;
    target->bitsStored_ = bitsStored_;
    target->highBit_ = highBit_;
    target->photometric_ = photometric_;
    target->rescaleSlope_ = rescaleSlope_;
    target->rescaleIntercept_ = rescaleIntercept_;
    target->windows_ = windows_;

    return target.release();
  }

  unsigned int DicomImageInformation::GetWidth() const
  {
    return width_;
  }

  unsigned int DicomImageInformation::GetHeight() const
  {
    return height_;
  }

  unsigned int DicomImageInformation::GetNumberOfFrames() const
  {
    return numberOfFrames_;
  }

  unsigned int DicomImageInformation::GetChannelCount() const
  {
    return samplesPerPixel_;
  }

  unsigned int DicomImageInformation::GetBitsStored() const
  {
    return bitsStored_;
  }

  size_t DicomImageInformation::GetBytesPerValue() const
  {
    if (bitsStored_ == 1)
    {
      throw OrthancException(ErrorCode_BadSequenceOfCalls,
                             "This call is incompatible with black-and-white images");
    }
    else
    {
      assert(bitsAllocated_ >= 8);
      return bytesPerValue_;
    }
  }

  bool DicomImageInformation::IsSigned() const
  {
    return isSigned_;
  }

  unsigned int DicomImageInformation::GetBitsAllocated() const
  {
    return bitsAllocated_;
  }

  unsigned int DicomImageInformation::GetHighBit() const
  {
    return highBit_;
  }

  bool DicomImageInformation::IsPlanar() const
  {
    return isPlanar_;
  }

  unsigned int DicomImageInformation::GetShift() const
  {
    return highBit_ + 1 - bitsStored_;
  }

  PhotometricInterpretation DicomImageInformation::GetPhotometricInterpretation() const
  {
    return photometric_;
  }

  bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format,
                                                 bool ignorePhotometricInterpretation) const
  {
    if (photometric_ == PhotometricInterpretation_Palette)
    {
      if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_RGB24;
        return true;
      }

      if (GetBitsStored() == 16 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_RGB48;
        return true;
      }
    }
    
    if (ignorePhotometricInterpretation ||
        photometric_ == PhotometricInterpretation_Monochrome1 ||
        photometric_ == PhotometricInterpretation_Monochrome2)
    {
      if (GetBitsStored() == 8 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_Grayscale8;
        return true;
      }
      
      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_Grayscale16;
        return true;
      }

      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned())
      {
        format = PixelFormat_SignedGrayscale16;
        return true;
      }
      
      if (GetBitsAllocated() == 32 && GetChannelCount() == 1 && !IsSigned())
      {
        format = PixelFormat_Grayscale32;
        return true;
      }

      if (GetBitsStored() == 1 && GetChannelCount() == 1 && !IsSigned())
      {
        // This is the case of DICOM SEG, new in Orthanc 1.10.0
        format = PixelFormat_Grayscale8;
        return true;
      }
    }

    if (GetBitsStored() == 8 &&
        GetChannelCount() == 3 &&
        !IsSigned() &&
        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
    {
      format = PixelFormat_RGB24;
      return true;
    }

    if (GetBitsStored() == 16 &&
        GetChannelCount() == 3 &&
        !IsSigned() &&
        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
    {
      format = PixelFormat_RGB48;
      return true;
    }

    return false;
  }


  size_t DicomImageInformation::GetFrameSize() const
  {
    uint64_t totalFrameSize;

    if (bitsStored_ == 1)
    {
      assert(GetWidth() % 8 == 0);
      
      if (GetChannelCount() == 1)
      {
        totalFrameSize = static_cast<uint64_t>(GetHeight()) * static_cast<uint64_t>(GetWidth()) / 8;
      }
      else
      {
        throw OrthancException(ErrorCode_IncompatibleImageFormat,
                               "Image not supported (multi-channel black-and-image image)");
      }
    }
    else
    {
      totalFrameSize = (static_cast<uint64_t>(GetHeight()) *
                        static_cast<uint64_t>(GetWidth()) *
                        static_cast<uint64_t>(GetBytesPerValue()) *
                        static_cast<uint64_t>(GetChannelCount()));
    }

    if (totalFrameSize > MAX_IMAGE_FRAME_SIZE ||
        static_cast<uint64_t>(static_cast<size_t>(totalFrameSize)) != totalFrameSize)
    {
      std::ostringstream errorMessage;
      errorMessage << "DICOM Frame size overflow  (" << totalFrameSize << " vs. " << MAX_IMAGE_FRAME_SIZE << ")";
      throw OrthancException(ErrorCode_BadFileFormat, errorMessage.str());
    }
    else
    {
      return static_cast<size_t>(totalFrameSize);
    }
  }


  void DicomImageInformation::ThrowIfInvalidFrameSize() const
  {
    GetFrameSize(); // GetFrameSize() will throw if the image dimensions are too large
  }

  unsigned int DicomImageInformation::GetUsefulTagLength()
  {
    return 256;
  }


  ValueRepresentation DicomImageInformation::GuessPixelDataValueRepresentation(const DicomTransferSyntax& transferSyntax,
                                                                               unsigned int bitsAllocated)
  {
    /**
     * This approach is validated in "Tests/GuessPixelDataVR.py":
     * https://orthanc.uclouvain.be/hg/orthanc-tests/file/default/Tests/GuessPixelDataVR.py
     **/

    if (transferSyntax == DicomTransferSyntax_LittleEndianExplicit ||
        transferSyntax == DicomTransferSyntax_BigEndianExplicit)
    {
      /**
       * Same rules apply to Little Endian Explicit and Big Endian
       * Explicit (now retired). The VR of the pixel data directly
       * depends upon the "Bits Allocated (0028,0100)" tag:
       * https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.2.html
       * https://dicom.nema.org/medical/dicom/2016b/output/chtml/part05/sect_A.3.html
       **/
      if (bitsAllocated > 8)
      {
        return ValueRepresentation_OtherWord;
      }
      else
      {
        return ValueRepresentation_OtherByte;
      }
    }
    else if (transferSyntax == DicomTransferSyntax_LittleEndianImplicit)
    {
      // Assume "OW" for DICOM Implicit VR Little Endian Transfer Syntax
      // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/chapter_A.html#sect_A.1
      return ValueRepresentation_OtherWord;
    }
    else
    {
      // Assume "OB" for all the compressed transfer syntaxes
      // https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_A.4.html
      return ValueRepresentation_OtherByte;
    }
  }


  const Window& DicomImageInformation::GetWindow(size_t index) const
  {
    if (index < windows_.size())
    {
      return windows_[index];
    }
    else
    {
      throw OrthancException(ErrorCode_ParameterOutOfRange);
    }
  }


  double DicomImageInformation::ApplyRescale(double value) const
  {
    return rescaleSlope_ * value + rescaleIntercept_;
  }


  Window DicomImageInformation::GetDefaultWindow() const
  {
    if (windows_.empty())
    {
      const double width = static_cast<double>(1 << GetBitsStored());
      const double center = width / 2.0;
      return Window(center, width);
    }
    else
    {
      return windows_[0];
    }
  }


  void DicomImageInformation::ComputeRenderingTransform(double& offset,
                                                        double& scaling,
                                                        const Window& window) const
  {
    // Check out "../../../OrthancServer/Resources/ImplementationNotes/windowing.py"

    double windowWidth = std::abs(window.GetWidth());

    // Avoid divisions by zero
    static const double MIN = 0.0001;
    if (windowWidth <= MIN)
    {
      windowWidth = MIN;
    }

    if (GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1)
    {
      scaling = -255.0 * GetRescaleSlope() / windowWidth;
      offset = 255.0 * (window.GetCenter() - GetRescaleIntercept()) / windowWidth + 127.5;
    }
    else
    {
      scaling = 255.0 * GetRescaleSlope() / windowWidth;
      offset = 255.0 * (GetRescaleIntercept() - window.GetCenter()) / windowWidth + 127.5;
    }
  }


  void DicomImageInformation::ComputeRenderingTransform(double& offset,
                                                        double& scaling) const
  {
    ComputeRenderingTransform(offset, scaling, GetDefaultWindow());
  }
}
