// Copyright (c) 2020 OPEN CASCADE SAS
//
// This file is part of Open CASCADE Technology software library.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License version 2.1 as published
// by the Free Software Foundation, with special exception defined in the file
// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
// distribution for complete text of the license and disclaimer of any warranty.
//
// Alternatively, this file may be used under the terms of Open CASCADE
// commercial license or contractual agreement.

#include <Image_DDSParser.hxx>

#include <Image_PixMap.hxx>
#include <Image_SupportedFormats.hxx>
#include <Message.hxx>
#include <OSD_FileSystem.hxx>

IMPLEMENT_STANDARD_RTTIEXT(Image_CompressedPixMap, Standard_Transient)

//! DDS Pixel Format structure.
struct Image_DDSParser::DDSPixelFormat
{
  uint32_t Size;
  uint32_t Flags;
  uint32_t FourCC;
  uint32_t RGBBitCount;
  uint32_t RBitMask;
  uint32_t GBitMask;
  uint32_t BBitMask;
  uint32_t ABitMask;
};

//! DDS File header structure.
struct Image_DDSParser::DDSFileHeader
{
  //! Caps2 flag indicating complete (6 faces) cubemap.
  enum
  {
    DDSCompleteCubemap = 0xFE00
  };

  //! Return TRUE if cubmap flag is set.
  bool IscompleteCubemap() const
  {
    return (Caps2 & DDSFileHeader::DDSCompleteCubemap) == DDSFileHeader::DDSCompleteCubemap;
  }

  uint32_t       Size;
  uint32_t       Flags;
  uint32_t       Height;
  uint32_t       Width;
  uint32_t       PitchOrLinearSize;
  uint32_t       Depth;
  uint32_t       MipMapCount;
  uint32_t       Reserved1[11];
  DDSPixelFormat PixelFormatDef;
  uint32_t       Caps;
  uint32_t       Caps2;
  uint32_t       Caps3;
  uint32_t       Caps4;
  uint32_t       Reserved2;
};

//=================================================================================================

occ::handle<Image_CompressedPixMap> Image_DDSParser::Load(
  const occ::handle<Image_SupportedFormats>& theSupported,
  const TCollection_AsciiString&             theFile,
  const int                                  theFaceIndex,
  const int64_t                              theFileOffset)
{
  const occ::handle<OSD_FileSystem>& aFileSystem = OSD_FileSystem::DefaultFileSystem();
  std::shared_ptr<std::istream>      aFile =
    aFileSystem->OpenIStream(theFile, std::ios::in | std::ios::binary);
  char aHeader[128] = {};
  if (aFile.get() == nullptr || !aFile->good())
  {
    return occ::handle<Image_CompressedPixMap>();
  }
  if (theFileOffset != 0)
  {
    aFile->seekg((std::streamoff)theFileOffset, std::ios::beg);
  }
  aFile->read(aHeader, 128);
  size_t aNbReadBytes = (size_t)aFile->gcount();
  if (aNbReadBytes < 128 || ::memcmp(aHeader, "DDS ", 4) != 0)
  {
    return occ::handle<Image_CompressedPixMap>();
  }

  occ::handle<Image_CompressedPixMap> aDef = parseHeader(*(const DDSFileHeader*)(aHeader + 4));
  if (aDef.IsNull())
  {
    return occ::handle<Image_CompressedPixMap>();
  }

  if (!theSupported.IsNull() && !theSupported->IsSupported(aDef->CompressedFormat()))
  {
    return occ::handle<Image_CompressedPixMap>();
  }

  if (theFaceIndex < 0)
  {
    return aDef;
  }

  if (theFaceIndex >= aDef->NbFaces() || aDef->FaceBytes() == 0)
  {
    Message::SendFail(TCollection_AsciiString("DDS Reader error - invalid face index #")
                      + theFaceIndex + " within file\n" + theFile);
    return occ::handle<Image_CompressedPixMap>();
  }

  const size_t anOffset = aDef->FaceBytes() * theFaceIndex;
  if (anOffset != 0)
  {
    aFile->seekg((std::streamoff)anOffset, std::ios::cur);
  }
  occ::handle<NCollection_Buffer> aBuffer =
    new NCollection_Buffer(Image_PixMap::DefaultAllocator(), aDef->FaceBytes());
  aFile->read((char*)aBuffer->ChangeData(), aDef->FaceBytes());
  aNbReadBytes = (size_t)aFile->gcount();
  if (aNbReadBytes < aDef->FaceBytes())
  {
    Message::SendFail(TCollection_AsciiString("DDS Reader error - unable to read face #")
                      + theFaceIndex + " data from file\n" + theFile);
    return occ::handle<Image_CompressedPixMap>();
  }
  aDef->SetFaceData(aBuffer);
  return aDef;
}

//=================================================================================================

occ::handle<Image_CompressedPixMap> Image_DDSParser::Load(
  const occ::handle<Image_SupportedFormats>& theSupported,
  const occ::handle<NCollection_Buffer>&     theBuffer,
  const int                                  theFaceIndex)
{
  if (theBuffer.IsNull() || theBuffer->Size() < 128 || ::memcmp(theBuffer->Data(), "DDS ", 4) != 0)
  {
    return occ::handle<Image_CompressedPixMap>();
  }

  occ::handle<Image_CompressedPixMap> aDef =
    parseHeader(*(const DDSFileHeader*)(theBuffer->Data() + 4));
  if (aDef.IsNull())
  {
    return occ::handle<Image_CompressedPixMap>();
  }

  if (!theSupported.IsNull() && !theSupported->IsSupported(aDef->CompressedFormat()))
  {
    return occ::handle<Image_CompressedPixMap>();
  }

  if (theFaceIndex < 0)
  {
    return aDef;
  }

  if (theFaceIndex >= aDef->NbFaces() || aDef->FaceBytes() == 0)
  {
    Message::SendFail(TCollection_AsciiString("DDS Reader error - invalid face index #")
                      + theFaceIndex + " within buffer");
    return occ::handle<Image_CompressedPixMap>();
  }

  const size_t anOffset = aDef->FaceBytes() * theFaceIndex + 128;
  if (theBuffer->Size() < anOffset + aDef->FaceBytes())
  {
    Message::SendFail(TCollection_AsciiString("DDS Reader error - unable to read face #")
                      + theFaceIndex + " data from buffer");
    return occ::handle<Image_CompressedPixMap>();
  }

  occ::handle<NCollection_Buffer> aBuffer =
    new NCollection_Buffer(Image_PixMap::DefaultAllocator(), aDef->FaceBytes());
  memcpy(aBuffer->ChangeData(), theBuffer->Data() + anOffset, aDef->FaceBytes());
  aDef->SetFaceData(aBuffer);
  return aDef;
}

//=================================================================================================

occ::handle<Image_CompressedPixMap> Image_DDSParser::parseHeader(const DDSFileHeader& theHeader)
{
  if (theHeader.Size != 124 || theHeader.Width == 0 || theHeader.Height == 0
      || theHeader.PixelFormatDef.Size != 32)
  {
    return occ::handle<Image_CompressedPixMap>();
  }

  Image_Format           aBaseFormat = Image_Format_UNKNOWN;
  Image_CompressedFormat aFormat     = Image_CompressedFormat_UNKNOWN;
  int                    aBlockSize  = 8;
  const bool             hasAlpha    = (theHeader.PixelFormatDef.Flags & 0x1) != 0;
  if (::memcmp(&theHeader.PixelFormatDef.FourCC, "DXT5", 4) == 0)
  {
    aBaseFormat = Image_Format_RGBA;
    aFormat     = Image_CompressedFormat_RGBA_S3TC_DXT5;
    aBlockSize  = 16;
  }
  else if (::memcmp(&theHeader.PixelFormatDef.FourCC, "DXT3", 4) == 0)
  {
    aBaseFormat = Image_Format_RGBA;
    aFormat     = Image_CompressedFormat_RGBA_S3TC_DXT3;
    aBlockSize  = 16;
  }
  else if (::memcmp(&theHeader.PixelFormatDef.FourCC, "DXT1", 4) == 0)
  {
    aBaseFormat = hasAlpha ? Image_Format_RGBA : Image_Format_RGB;
    aFormat =
      hasAlpha ? Image_CompressedFormat_RGBA_S3TC_DXT1 : Image_CompressedFormat_RGB_S3TC_DXT1;
    aBlockSize = 8;
  }
  if (aFormat == Image_CompressedFormat_UNKNOWN)
  {
    return occ::handle<Image_CompressedPixMap>();
  }

  occ::handle<Image_CompressedPixMap> aDef = new Image_CompressedPixMap();
  aDef->SetSize((int)theHeader.Width, (int)theHeader.Height);
  aDef->SetNbFaces(theHeader.IscompleteCubemap() != 0 ? 6 : 1);
  aDef->SetBaseFormat(aBaseFormat);
  aDef->SetCompressedFormat(aFormat);

  const int aNbMipMaps = std::max((int)theHeader.MipMapCount, 1);
  aDef->ChangeMipMaps().Resize(0, aNbMipMaps - 1, false);
  {
    size_t                aFaceSize = 0;
    NCollection_Vec2<int> aMipSizeXY(aDef->SizeX(), aDef->SizeY());
    for (int aMipIter = 0;; ++aMipIter)
    {
      const int aMipLength = ((aMipSizeXY.x() + 3) / 4) * ((aMipSizeXY.y() + 3) / 4) * aBlockSize;
      aFaceSize += aMipLength;
      aDef->ChangeMipMaps().SetValue(aMipIter, aMipLength);
      if (aMipIter + 1 >= aNbMipMaps)
      {
        break;
      }

      aMipSizeXY /= 2;
      if (aMipSizeXY.x() == 0)
      {
        aMipSizeXY.x() = 1;
      }
      if (aMipSizeXY.y() == 0)
      {
        aMipSizeXY.y() = 1;
      }
    }
    aDef->SetCompleteMipMapSet(aMipSizeXY.x() == 1 && aMipSizeXY.y() == 1);
    aDef->SetFaceBytes(aFaceSize);
  }

  return aDef;
}
