// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause

#ifndef vtkVolumeMask_h
#define vtkVolumeMask_h

#include <vtkDataArray.h>
#include <vtkImageData.h>
#include <vtkOpenGLRenderWindow.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkTextureObject.h>

#include <map> // STL required

//----------------------------------------------------------------------------
VTK_ABI_NAMESPACE_BEGIN
class vtkVolumeMask
{
public:
  //--------------------------------------------------------------------------
  vtkVolumeMask()
  {
    this->Texture = nullptr;
    this->Loaded = false;
    this->LoadedExtent[0] = VTK_INT_MAX;
    this->LoadedExtent[1] = VTK_INT_MIN;
    this->LoadedExtent[2] = VTK_INT_MAX;
    this->LoadedExtent[3] = VTK_INT_MIN;
    this->LoadedExtent[4] = VTK_INT_MAX;
    this->LoadedExtent[5] = VTK_INT_MIN;
  }

  //--------------------------------------------------------------------------
  ~vtkVolumeMask()
  {
    if (this->Texture)
    {
      this->Texture->Delete();
      this->Texture = nullptr;
    }
  }

  //--------------------------------------------------------------------------
  vtkTimeStamp GetBuildTime() { return this->BuildTime; }

  //--------------------------------------------------------------------------
  void Activate() { this->Texture->Activate(); }

  //--------------------------------------------------------------------------
  void Deactivate() { this->Texture->Deactivate(); }

  //--------------------------------------------------------------------------
  void Update(vtkRenderer* ren, vtkImageData* input, int cellFlag, int textureExtent[6],
    int scalarMode, int arrayAccessMode, int arrayId, const char* arrayName,
    vtkIdType maxMemoryInBytes)
  {
    bool needUpdate = false;
    bool modified = false;

    if (!this->Texture)
    {
      this->Texture = vtkTextureObject::New();
      needUpdate = true;
    }

    vtkOpenGLRenderWindow* renWin = vtkOpenGLRenderWindow::SafeDownCast(ren->GetRenderWindow());
    auto ostate = renWin->GetState();
    this->Texture->SetContext(renWin);

    if (!this->Texture->GetHandle())
    {
      needUpdate = true;
    }

    int obsolete = needUpdate || !this->Loaded || input->GetMTime() > this->BuildTime;
    if (!obsolete)
    {
      obsolete = cellFlag != this->LoadedCellFlag;
      int i = 0;
      while (!obsolete && i < 6)
      {
        obsolete = obsolete || this->LoadedExtent[i] > textureExtent[i];
        ++i;
        obsolete = obsolete || this->LoadedExtent[i] < textureExtent[i];
        ++i;
      }
    }

    if (obsolete)
    {
      this->Loaded = false;
      int dim[3];
      input->GetDimensions(dim);

      vtkDataArray* scalars = vtkAbstractMapper::GetScalars(
        input, scalarMode, arrayAccessMode, arrayId, arrayName, this->LoadedCellFlag);

      // DON'T USE GetScalarType() or GetNumberOfScalarComponents() on
      // ImageData as it deals only with point data...
      int scalarType = scalars->GetDataType();
      if (scalarType != VTK_UNSIGNED_CHAR)
      {
        cout << "Mask should be VTK_UNSIGNED_CHAR." << endl;
      }
      if (scalars->GetNumberOfComponents() != 1)
      {
        cout << "Mask should be a one-component scalar field." << endl;
      }

      GLint internalFormat = GL_R8;
      GLenum format = GL_RED;
      GLenum type = GL_UNSIGNED_BYTE;

      // Enough memory?
      int textureSize[3];
      int i = 0;
      while (i < 3)
      {
        textureSize[i] = textureExtent[2 * i + 1] - textureExtent[2 * i] + 1;
        ++i;
      }

      GLint width;
      glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE, &width);
      this->Loaded = textureSize[0] <= width && textureSize[1] <= width && textureSize[2] <= width;
      if (this->Loaded)
      {
        // so far, so good but some cards don't report allocation error
        this->Loaded = textureSize[0] * textureSize[1] * textureSize[2] *
            vtkAbstractArray::GetDataTypeSize(scalarType) * scalars->GetNumberOfComponents() <=
          maxMemoryInBytes;
        if (this->Loaded)
        {
          ostate->vtkglPixelStorei(GL_UNPACK_ALIGNMENT, 1);

          if (!(textureExtent[1] - textureExtent[0] + cellFlag == dim[0]))
          {
            ostate->vtkglPixelStorei(GL_UNPACK_ROW_LENGTH, dim[0] - cellFlag);
          }
          if (!(textureExtent[3] - textureExtent[2] + cellFlag == dim[1]))
          {
            ostate->vtkglPixelStorei(GL_UNPACK_IMAGE_HEIGHT, dim[1] - cellFlag);
          }
          void* dataPtr = scalars->GetVoidPointer(
            ((textureExtent[4] * (dim[1] - cellFlag) + textureExtent[2]) * (dim[0] - cellFlag) +
              textureExtent[0]) *
            scalars->GetNumberOfComponents());

          this->Texture->SetDataType(type);
          this->Texture->SetFormat(format);
          this->Texture->SetInternalFormat(internalFormat);
          this->Texture->Create3DFromRaw(
            textureSize[0], textureSize[1], textureSize[2], 1, scalarType, dataPtr);
          this->Texture->SetWrapS(vtkTextureObject::ClampToEdge);
          this->Texture->SetWrapT(vtkTextureObject::ClampToEdge);
          this->Texture->SetWrapR(vtkTextureObject::ClampToEdge);
          this->Texture->SetMagnificationFilter(vtkTextureObject::Nearest);
          this->Texture->SetMinificationFilter(vtkTextureObject::Nearest);
          this->Texture->SetBorderColor(0.0f, 0.0f, 0.0f, 0.0f);

          // Restore the default values.
          ostate->vtkglPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
          ostate->vtkglPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);

          this->LoadedCellFlag = cellFlag;
          i = 0;
          while (i < 6)
          {
            this->LoadedExtent[i] = textureExtent[i];
            ++i;
          }

          double spacing[3];
          double origin[3];
          input->GetSpacing(spacing);
          input->GetOrigin(origin);
          int swapBounds[3];
          swapBounds[0] = (spacing[0] < 0);
          swapBounds[1] = (spacing[1] < 0);
          swapBounds[2] = (spacing[2] < 0);

          if (!this->LoadedCellFlag) // loaded extents represent points
          {
            // slabsPoints[i]=(slabsDataSet[i] - origin[i/2]) / spacing[i/2];
            // in general, x=o+i*spacing.
            // if spacing is positive min extent match the min of the
            // bounding box
            // and the max extent match the max of the bounding box
            // if spacing is negative min extent match the max of the
            // bounding box
            // and the max extent match the min of the bounding box

            // if spacing is negative, we may have to rethink the equation
            // between real point and texture coordinate...
            this->LoadedBounds[0] =
              origin[0] + static_cast<double>(this->LoadedExtent[0 + swapBounds[0]]) * spacing[0];
            this->LoadedBounds[2] =
              origin[1] + static_cast<double>(this->LoadedExtent[2 + swapBounds[1]]) * spacing[1];
            this->LoadedBounds[4] =
              origin[2] + static_cast<double>(this->LoadedExtent[4 + swapBounds[2]]) * spacing[2];
            this->LoadedBounds[1] =
              origin[0] + static_cast<double>(this->LoadedExtent[1 - swapBounds[0]]) * spacing[0];
            this->LoadedBounds[3] =
              origin[1] + static_cast<double>(this->LoadedExtent[3 - swapBounds[1]]) * spacing[1];
            this->LoadedBounds[5] =
              origin[2] + static_cast<double>(this->LoadedExtent[5 - swapBounds[2]]) * spacing[2];
          }
          else // loaded extents represent cells
          {
            int wholeTextureExtent[6];
            input->GetExtent(wholeTextureExtent);
            i = 1;
            while (i < 6)
            {
              wholeTextureExtent[i]--;
              i += 2;
            }

            i = 0;
            while (i < 3)
            {
              if (this->LoadedExtent[2 * i] == wholeTextureExtent[2 * i])
              {
                this->LoadedBounds[2 * i + swapBounds[i]] = origin[i];
              }
              else
              {
                this->LoadedBounds[2 * i + swapBounds[i]] =
                  origin[i] + (static_cast<double>(this->LoadedExtent[2 * i]) + 0.5) * spacing[i];
              }

              if (this->LoadedExtent[2 * i + 1] == wholeTextureExtent[2 * i + 1])
              {
                this->LoadedBounds[2 * i + 1 - swapBounds[i]] = origin[i] +
                  (static_cast<double>(this->LoadedExtent[2 * i + 1]) + 1.0) * spacing[i];
              }
              else
              {
                this->LoadedBounds[2 * i + 1 - swapBounds[i]] = origin[i] +
                  (static_cast<double>(this->LoadedExtent[2 * i + 1]) + 0.5) * spacing[i];
              }
              ++i;
            }
          }
          modified = true;
        }
      }
    }

    if (modified)
    {
      this->BuildTime.Modified();
    }
  }

  //--------------------------------------------------------------------------
  double* GetLoadedBounds() { return this->LoadedBounds; }

  //--------------------------------------------------------------------------
  vtkIdType* GetLoadedExtent() { return this->LoadedExtent; }

  //--------------------------------------------------------------------------
  int GetLoadedCellFlag() { return this->LoadedCellFlag; }

  //--------------------------------------------------------------------------
  bool IsLoaded() { return this->Loaded; }

  // Get the texture unit
  //--------------------------------------------------------------------------
  int GetTextureUnit()
  {
    if (!this->Texture)
    {
      return -1;
    }
    return this->Texture->GetTextureUnit();
  }

  //--------------------------------------------------------------------------
  void ReleaseGraphicsResources(vtkWindow* window)
  {
    if (this->Texture)
    {
      this->Texture->ReleaseGraphicsResources(window);
      this->Texture->Delete();
      this->Texture = nullptr;
    }
  }

protected:
  vtkTextureObject* Texture;
  vtkTimeStamp BuildTime;

  double LoadedBounds[6];
  vtkIdType LoadedExtent[6];

  int LoadedCellFlag;
  bool Loaded;
};

//----------------------------------------------------------------------------
class vtkMapMaskTextureId
{
public:
  std::map<vtkImageData*, vtkVolumeMask*> Map;
  vtkMapMaskTextureId() = default;

private:
  vtkMapMaskTextureId(const vtkMapMaskTextureId& other) = delete;
  vtkMapMaskTextureId& operator=(const vtkMapMaskTextureId& other) = delete;
};

VTK_ABI_NAMESPACE_END
#endif // vtkVolumeMask_h
// VTK-HeaderTest-Exclude: vtkVolumeMask.h
