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

// VTK_DEPRECATED_IN_9_4_0()
#define VTK_DEPRECATION_LEVEL 0

#include "vtkGLTFReader.h"

#include "vtkCommand.h"
#include "vtkDataArraySelection.h"
#include "vtkDoubleArray.h"
#include "vtkEventForwarderCommand.h"
#include "vtkFieldData.h"
#include "vtkFloatArray.h"
#include "vtkGLTFDocumentLoader.h"
#include "vtkGLTFTexture.h"
#include "vtkImageData.h"
#include "vtkInformation.h"
#include "vtkInformationVector.h"
#include "vtkLegacy.h"
#include "vtkMultiBlockDataSet.h"
#include "vtkPointData.h"
#include "vtkResourceStream.h"
#include "vtkStreamingDemandDrivenPipeline.h"
#include "vtkStringArray.h"
#include "vtkTexture.h"
#include "vtkTransform.h"
#include "vtkTransformPolyDataFilter.h"
#include "vtkWeightedTransformFilter.h"
#include "vtksys/SystemTools.hxx"

#include <array>
#include <sstream>

VTK_ABI_NAMESPACE_BEGIN
namespace
{
//------------------------------------------------------------------------------
// Replacement for std::to_string as it is not supported by certain compilers
template <typename T>
std::string value_to_string(const T& val)
{
  std::ostringstream ss;
  ss << val;
  return ss.str();
}

//------------------------------------------------------------------------------
std::string MakeUniqueNonEmptyName(
  const std::string& name, std::map<std::string, unsigned int>& duplicateCounters)
{
  std::string newName = "Unnamed";
  if (!name.empty())
  {
    newName = name;
  }
  if (duplicateCounters.count(newName) > 0)
  {
    duplicateCounters[newName]++;
    newName += '_' + value_to_string(duplicateCounters[newName] - 1);
    duplicateCounters[newName] = 1;
  }
  else
  {
    duplicateCounters[newName] = 1;
  }
  return newName;
}

//------------------------------------------------------------------------------
void AddIntegerToFieldData(
  std::string arrayName, int value, vtkSmartPointer<vtkFieldData> fieldData)
{
  vtkNew<vtkIntArray> array;
  array->SetName(arrayName.c_str());
  array->SetNumberOfComponents(1);
  array->SetNumberOfValues(1);
  array->SetValue(0, value);
  fieldData->AddArray(array);
}

//------------------------------------------------------------------------------
void AddFloatToFieldData(
  std::string arrayName, float value, vtkSmartPointer<vtkFieldData> fieldData)
{
  vtkNew<vtkFloatArray> array;
  array->SetName(arrayName.c_str());
  array->SetNumberOfComponents(1);
  array->SetNumberOfValues(1);
  array->SetValue(0, value);
  fieldData->AddArray(array);
}

//------------------------------------------------------------------------------
void AddVecNfToFieldData(const std::string& arrayName, const std::vector<float>& multiplier,
  vtkSmartPointer<vtkFieldData> fieldData)
{
  vtkNew<vtkFloatArray> array;
  array->SetName(arrayName.c_str());
  array->SetNumberOfComponents(static_cast<int>(multiplier.size()));
  array->SetNumberOfTuples(1);
  array->SetTypedTuple(0, multiplier.data());
  fieldData->AddArray(array);
}

//------------------------------------------------------------------------------
void AddTextureInfoToFieldData(const std::string& prefix, int textureIndex, int textureCoordIndex,
  vtkSmartPointer<vtkFieldData> fieldData, std::vector<float> multiplier = std::vector<float>())
{
  AddIntegerToFieldData(prefix + "TextureIndex", textureIndex, fieldData);
  if (multiplier.size() == 3 || multiplier.size() == 4)
  {
    AddVecNfToFieldData(prefix + "Multiplier", multiplier, fieldData);
  }
  AddIntegerToFieldData(prefix + "TexCoordIndex", textureCoordIndex, fieldData);
}

//------------------------------------------------------------------------------
void AddMaterialToFieldData(int materialId, vtkSmartPointer<vtkFieldData> fieldData,
  const vtkGLTFDocumentLoader::Model& model)
{
  int nbTextures = static_cast<int>(model.Textures.size());

  // Append material information (multiplier, texture indices, and texture coordinate array name)
  if (materialId >= 0 && materialId < static_cast<int>(model.Materials.size()))
  {
    auto material = model.Materials[materialId];
    auto pbr = material.PbrMetallicRoughness;
    if (pbr.BaseColorTexture.Index >= 0 && pbr.BaseColorTexture.Index < nbTextures)
    {
      AddTextureInfoToFieldData(
        "BaseColor", pbr.BaseColorTexture.Index, pbr.BaseColorTexture.TexCoord, fieldData);
    }
    std::vector<float> multiplier(4, 1.0);
    if (pbr.BaseColorFactor.size() == 3 || pbr.BaseColorFactor.size() == 4)
    {
      multiplier = std::vector<float>{ pbr.BaseColorFactor.begin(), pbr.BaseColorFactor.end() };
    }
    AddVecNfToFieldData("BaseColorMultiplier", multiplier, fieldData);
    if (pbr.MetallicRoughnessTexture.Index >= 0 && pbr.MetallicRoughnessTexture.Index < nbTextures)
    {
      AddTextureInfoToFieldData("MetallicRoughness", pbr.MetallicRoughnessTexture.Index,
        pbr.MetallicRoughnessTexture.TexCoord, fieldData);
    }
    multiplier = std::vector<float>{ 0, pbr.MetallicFactor, pbr.RoughnessFactor };
    AddVecNfToFieldData("MetallicRoughness", multiplier, fieldData);
    if (material.NormalTexture.Index >= 0 && material.NormalTexture.Index < nbTextures)
    {
      AddTextureInfoToFieldData("Normal", material.NormalTexture.Index,
        material.NormalTexture.TexCoord, fieldData,
        std::vector<float>(3, material.NormalTextureScale));
    }
    if (material.OcclusionTexture.Index >= 0 && material.OcclusionTexture.Index < nbTextures)
    {
      AddTextureInfoToFieldData("Occlusion", material.OcclusionTexture.Index,
        material.OcclusionTexture.TexCoord, fieldData,
        std::vector<float>(3, material.OcclusionTextureStrength));
    }
    if (material.EmissiveTexture.Index >= 0 && material.EmissiveTexture.Index < nbTextures)
    {
      AddTextureInfoToFieldData("Emissive", material.EmissiveTexture.Index,
        material.EmissiveTexture.TexCoord, fieldData,
        std::vector<float>{ material.EmissiveFactor.begin(), material.EmissiveFactor.end() });
    }
    // Add alpha cutoff value, alpha mode
    if (material.AlphaMode == vtkGLTFDocumentLoader::Material::AlphaModeType::MASK)
    {
      AddFloatToFieldData("AlphaCutoff", material.AlphaCutoff, fieldData);
    }
    else if (material.AlphaMode == vtkGLTFDocumentLoader::Material::AlphaModeType::OPAQUE)
    {
      AddIntegerToFieldData("ForceOpaque", 1, fieldData);
    }
  }
  else
  {
    // Append default material information
    AddVecNfToFieldData("BaseColorMultiplier", std::vector<float>(4, 1.0f), fieldData);
    AddVecNfToFieldData("MetallicRoughness", std::vector<float>(3, 1.0f), fieldData);
    AddVecNfToFieldData("Emissive", std::vector<float>(3, 0.0f), fieldData);
    AddIntegerToFieldData("ForceOpaque", 1, fieldData);
  }
}

//------------------------------------------------------------------------------
vtkSmartPointer<vtkDataArray> ApplyMorphingToDataArray(vtkSmartPointer<vtkDataArray> origin,
  const std::vector<float>& weights, const std::vector<vtkSmartPointer<vtkFloatArray>>& targets)
{
  if (origin == nullptr)
  {
    return nullptr;
  }

  vtkSmartPointer<vtkDataArray> result = vtkSmartPointer<vtkDataArray>::Take(origin->NewInstance());
  result->DeepCopy(origin);

  if (targets.empty() || weights.empty() || targets.size() != weights.size())
  {
    return origin;
  }
  std::vector<double> tuple(origin->GetNumberOfComponents(), 0);
  for (int tupleId = 0; tupleId < origin->GetNumberOfTuples(); tupleId++)
  {
    origin->GetTuple(tupleId, tuple.data());
    for (unsigned int targetId = 0; targetId < targets.size(); targetId++)
    {
      for (int component = 0; component < targets[targetId]->GetNumberOfComponents(); component++)
      {
        // Morphing:
        // P the resulting tuple, P0 the primitive's tuple, wi the weights, Ti the targets' tuples:
        // P = P0 + sum(wi * Ti)
        tuple[component] += weights[targetId] * targets[targetId]->GetTuple(tupleId)[component];
      }
    }
    result->SetTuple(tupleId, tuple.data());
  }
  return result;
}

//------------------------------------------------------------------------------
void SetupWeightedTransformFilterForGLTFSkinning(vtkSmartPointer<vtkWeightedTransformFilter> filter,
  const std::vector<vtkSmartPointer<vtkMatrix4x4>>& jointMats,
  const vtkSmartPointer<vtkPolyData> poly)
{
  filter->SetInputData(poly);

  // Add transforms to weightedTransformFilter (min. 4  transforms. If we have less than 4
  // transforms, complete with identity)
  size_t nbTransforms = vtkMath::Max<size_t>(jointMats.size(), 4);
  filter->SetNumberOfTransforms(static_cast<int>(nbTransforms));
  for (unsigned int i = 0; i < nbTransforms; i++)
  {
    vtkNew<vtkTransform> transform;
    if (i >= jointMats.size())
    {
      filter->SetTransform(transform, i);
    }
    else
    {
      transform->SetMatrix(jointMats[i]);
      filter->SetTransform(transform, i);
    }
  }

  // Add joint index and weight array information
  filter->SetTransformIndexArray("JOINTS_0");
  filter->SetWeightArray("WEIGHTS_0");
}

//------------------------------------------------------------------------------
void AddTransformToFieldData(const vtkSmartPointer<vtkMatrix4x4> transform,
  vtkSmartPointer<vtkFieldData> fieldData, const std::string& name)
{
  vtkSmartPointer<vtkDoubleArray> matrixArray;
  if (fieldData->HasArray(name.c_str()))
  {
    matrixArray = vtkDoubleArray::SafeDownCast(fieldData->GetArray(name.c_str()));
    matrixArray->Resize(0);
  }
  else
  {
    matrixArray = vtkSmartPointer<vtkDoubleArray>::New();
    matrixArray->SetName(name.c_str());
    fieldData->AddArray(matrixArray);
  }
  // Create array to store the matrix's values
  for (int i = 0; i < 16; i++)
  {
    matrixArray->InsertNextValue(transform->GetElement(i / 4, i % 4));
  }
}

//------------------------------------------------------------------------------
void AddJointMatricesToFieldData(const std::vector<vtkSmartPointer<vtkMatrix4x4>>& jointMats,
  vtkSmartPointer<vtkFieldData> fieldData)
{
  for (unsigned int matId = 0; matId < jointMats.size(); matId++)
  {
    AddTransformToFieldData(jointMats[matId], fieldData, "jointMatrix_" + value_to_string(matId));
  }
}

//------------------------------------------------------------------------------
void AddGlobalTransformToFieldData(
  const vtkSmartPointer<vtkMatrix4x4> globalTransform, vtkSmartPointer<vtkFieldData> fieldData)
{
  // Create array to store the matrix's values
  AddTransformToFieldData(globalTransform, fieldData, "globalTransform");
}

//------------------------------------------------------------------------------
void AddMorphingWeightsToFieldData(
  const std::vector<float>& weights, vtkSmartPointer<vtkFieldData> fieldData)
{
  vtkNew<vtkFloatArray> weightsArray;
  weightsArray->SetName("morphingWeights");
  weightsArray->SetNumberOfValues(static_cast<vtkIdType>(weights.size()));
  fieldData->AddArray(weightsArray);
  for (unsigned int weightId = 0; weightId < weights.size(); weightId++)
  {
    weightsArray->SetValue(weightId, weights[weightId]);
  }
}

//------------------------------------------------------------------------------
void AddInfoToFieldData(const std::vector<float>* morphingWeights,
  const std::vector<vtkSmartPointer<vtkMatrix4x4>>& jointMats,
  vtkSmartPointer<vtkMatrix4x4> globalTransform, vtkSmartPointer<vtkFieldData> fieldData)
{
  if (morphingWeights != nullptr && !morphingWeights->empty())
  {
    AddMorphingWeightsToFieldData(*morphingWeights, fieldData);
  }

  if (!jointMats.empty())
  {
    AddJointMatricesToFieldData(jointMats, fieldData);
  }

  AddGlobalTransformToFieldData(globalTransform, fieldData);
}

//------------------------------------------------------------------------------
void PrepareMorphingTargetArrays(std::vector<vtkSmartPointer<vtkFloatArray>>& positionArrays,
  std::vector<vtkSmartPointer<vtkFloatArray>>& normalArrays,
  std::vector<vtkSmartPointer<vtkFloatArray>>& tangentArrays,
  std::vector<vtkGLTFDocumentLoader::MorphTarget>& targets)
{
  for (auto& target : targets)
  {
    if (target.AttributeValues.count("POSITION"))
    {
      positionArrays.push_back(target.AttributeValues["POSITION"]);
    }
    if (target.AttributeValues.count("NORMAL"))
    {
      normalArrays.push_back(target.AttributeValues["NORMAL"]);
    }
    if (target.AttributeValues.count("TANGENT"))
    {
      tangentArrays.push_back(target.AttributeValues["TANGENT"]);
    }
  }
}

//------------------------------------------------------------------------------
void ApplyMorphingToPolyData(std::vector<vtkGLTFDocumentLoader::MorphTarget>& targets,
  std::vector<float>* morphingWeights, vtkSmartPointer<vtkPolyData> inputPolyData,
  vtkSmartPointer<vtkPolyData> outputPolyData)
{
  // Prepare target arrays
  std::vector<vtkSmartPointer<vtkFloatArray>> positionArrays;
  std::vector<vtkSmartPointer<vtkFloatArray>> normalArrays;
  std::vector<vtkSmartPointer<vtkFloatArray>> tangentArrays;
  PrepareMorphingTargetArrays(positionArrays, normalArrays, tangentArrays, targets);

  // Apply morphing with all targets
  auto points = ApplyMorphingToDataArray(
    inputPolyData->GetPoints()->GetData(), *morphingWeights, positionArrays);
  auto normals = ApplyMorphingToDataArray(
    inputPolyData->GetPointData()->GetNormals(), *morphingWeights, normalArrays);
  auto tangents = ApplyMorphingToDataArray(
    inputPolyData->GetPointData()->GetArray("tangents"), *morphingWeights, tangentArrays);

  // Add morphed arrays to current polydata
  if (points != nullptr)
  {
    outputPolyData->SetPoints(vtkSmartPointer<vtkPoints>::New());
    outputPolyData->GetPoints()->SetData(points);
  }
  if (normals != nullptr)
  {
    outputPolyData->GetPointData()->SetNormals(normals);
  }
  if (tangents != nullptr)
  {
    outputPolyData->GetPointData()->AddArray(tangents);
  }
}

//------------------------------------------------------------------------------
bool BuildMultiBlockDatasetFromMesh(vtkGLTFDocumentLoader::Model& m, unsigned int meshId,
  vtkSmartPointer<vtkMultiBlockDataSet> parentDataSet,
  vtkSmartPointer<vtkMultiBlockDataSet> meshDataSet, std::string& dataSetName,
  vtkSmartPointer<vtkMatrix4x4> globalTransform,
  const std::vector<vtkSmartPointer<vtkMatrix4x4>>& jointMats, bool applyDeformations,
  std::vector<float>* morphingWeights, int outputPointsPrecision)
{
  if (meshId >= m.Meshes.size())
  {
    vtkErrorWithObjectMacro(nullptr, "Invalid mesh index " << meshId);
    return false;
  }
  bool createNewPolyData = false;
  // If meshDataSet is not defined, create it and append it to the parent dataset.
  if (!meshDataSet && !createNewPolyData)
  {
    createNewPolyData = true;
    meshDataSet = vtkSmartPointer<vtkMultiBlockDataSet>::New();
    parentDataSet->SetBlock(parentDataSet->GetNumberOfBlocks(), meshDataSet);
    parentDataSet->GetMetaData(parentDataSet->GetNumberOfBlocks() - 1)
      ->Set(vtkCompositeDataSet::NAME(), dataSetName);
  }

  vtkGLTFDocumentLoader::Mesh mesh = m.Meshes[meshId];

  int blockId = 0;
  for (auto& primitive : mesh.Primitives)
  {
    vtkSmartPointer<vtkPolyData> meshPolyData;

    // Even though no weights are defined in the node, meshes may contain default weights
    if ((morphingWeights == nullptr || morphingWeights->empty()) && !mesh.Weights.empty())
    {
      morphingWeights = &(mesh.Weights);
    }
    // Apply deformations (skins and morph targets to each primitive's geometry, then add the
    // resulting polydata to the parent dataSet)
    if (applyDeformations)
    {
      meshPolyData = vtkSmartPointer<vtkPolyData>::New();
      meshPolyData->ShallowCopy(primitive.Geometry);
      // Add material information to fieldData
      AddMaterialToFieldData(primitive.Material, meshPolyData->GetFieldData(), m);

      vtkNew<vtkTransformPolyDataFilter> filter;
      filter->SetOutputPointsPrecision(outputPointsPrecision);

      // Morphing
      if (morphingWeights != nullptr && !morphingWeights->empty())
      {
        // Number of weights should be equal to the number of targets
        if (morphingWeights->size() != primitive.Targets.size())
        {
          vtkErrorWithObjectMacro(nullptr, "Invalid number of morphing weights");
          return false;
        }
        ApplyMorphingToPolyData(
          primitive.Targets, morphingWeights, primitive.Geometry, meshPolyData);
      }

      // Skinning
      if (!jointMats.empty())
      {
        // Setup filter
        vtkNew<vtkWeightedTransformFilter> skinningFilter;
        SetupWeightedTransformFilterForGLTFSkinning(skinningFilter, jointMats, meshPolyData);
        // Connect to TransformPolyDataFilter
        filter->SetInputConnection(skinningFilter->GetOutputPort(0));
      }
      else
      {
        filter->SetInputData(meshPolyData);
      }

      // Node transform
      vtkNew<vtkTransform> transform;
      transform->SetMatrix(globalTransform);
      filter->SetTransform(transform);
      if (createNewPolyData)
      {
        meshDataSet->SetBlock(meshDataSet->GetNumberOfBlocks(), (filter->GetOutputDataObject(0)));
      }
      else
      {
        filter->SetOutput(vtkPolyData::SafeDownCast(meshDataSet->GetBlock(blockId)));
      }
      filter->Update();
    }
    else
    {
      if (createNewPolyData)
      {
        meshPolyData = vtkSmartPointer<vtkPolyData>::New();
        meshPolyData->ShallowCopy(primitive.Geometry);
        // Add material information to fieldData
        AddMaterialToFieldData(primitive.Material, meshPolyData->GetFieldData(), m);
        meshDataSet->SetBlock(meshDataSet->GetNumberOfBlocks(), meshPolyData);
      }
      else
      {
        meshPolyData = vtkPolyData::SafeDownCast(meshDataSet->GetBlock(blockId));
      }
    }
    AddInfoToFieldData(morphingWeights, jointMats, globalTransform,
      vtkPolyData::SafeDownCast(meshDataSet->GetBlock(blockId))->GetFieldData());
  }
  return true;
}

//------------------------------------------------------------------------------
bool BuildMultiBlockDataSetFromNode(vtkGLTFDocumentLoader::Model& m, unsigned int nodeId,
  vtkSmartPointer<vtkMultiBlockDataSet> parentDataSet,
  vtkSmartPointer<vtkMultiBlockDataSet> nodeDataset, std::string nodeName, bool applyDeformations,
  int outputPointsPrecision)
{
  if (nodeId >= m.Nodes.size())
  {
    vtkErrorWithObjectMacro(nullptr, "Invalid node index " << nodeId);
    return false;
  }
  bool createNewBlocks = false;
  // If nodeDataset is not defined, create it and append it to the parent dataset
  if (!nodeDataset)
  {
    createNewBlocks = true;
    nodeDataset = vtkSmartPointer<vtkMultiBlockDataSet>::New();
    parentDataSet->SetBlock(parentDataSet->GetNumberOfBlocks(), nodeDataset);
    parentDataSet->GetMetaData(parentDataSet->GetNumberOfBlocks() - 1)
      ->Set(vtkCompositeDataSet::NAME(), nodeName);
  }

  vtkGLTFDocumentLoader::Node node = m.Nodes[nodeId];
  int blockId = 0;
  if (node.Mesh >= 0)
  {
    std::vector<vtkSmartPointer<vtkMatrix4x4>> jointMats;

    if (node.Skin >= 0)
    {
      // Compute skinning matrices
      const vtkGLTFDocumentLoader::Skin& skin = m.Skins[node.Skin];
      vtkGLTFDocumentLoader::ComputeJointMatrices(m, skin, node, jointMats);
    }

    std::vector<float>* morphingWeights = nullptr;
    if (!node.Weights.empty())
    {
      morphingWeights = &(node.Weights);
    }
    else if (!node.InitialWeights.empty())
    {
      morphingWeights = &(node.InitialWeights);
    }

    vtkSmartPointer<vtkMultiBlockDataSet> meshDataSet = nullptr;
    if (!createNewBlocks)
    {
      meshDataSet = vtkMultiBlockDataSet::SafeDownCast(nodeDataset->GetBlock(blockId));
    }
    std::string meshDatasetName = "Mesh_" + value_to_string(node.Mesh);
    if (!BuildMultiBlockDatasetFromMesh(m, node.Mesh, nodeDataset, meshDataSet, meshDatasetName,
          node.GlobalTransform, jointMats, applyDeformations, morphingWeights,
          outputPointsPrecision))
    {
      vtkErrorWithObjectMacro(
        nullptr, "Could not build vtkMultiBlockDataSet from mesh " << node.Mesh);
      return false;
    }
    blockId++;
  }
  for (int child : node.Children)
  {
    // look for existing dataset for this node
    vtkSmartPointer<vtkMultiBlockDataSet> childDataset;
    std::string childDatasetName = "Node_" + value_to_string(child);
    if (!createNewBlocks)
    {
      // find existing child dataset for this node
      childDataset = vtkMultiBlockDataSet::SafeDownCast(nodeDataset->GetBlock(blockId));
    }
    if (!BuildMultiBlockDataSetFromNode(m, child, nodeDataset, childDataset, childDatasetName,
          applyDeformations, outputPointsPrecision))
    {
      vtkErrorWithObjectMacro(nullptr, "Could not build vtkMultiBlockDataSet from node " << child);

      return false;
    }
    blockId++;
  }
  return true;
}

//------------------------------------------------------------------------------
bool BuildMultiBlockDataSetFromScene(vtkGLTFDocumentLoader::Model& m, vtkIdType sceneId,
  vtkSmartPointer<vtkMultiBlockDataSet> dataSet, bool applyDeformations, int outputPointsPrecision)
{
  if (sceneId < 0 || sceneId >= static_cast<vtkIdType>(m.Scenes.size()))
  {
    vtkErrorWithObjectMacro(nullptr, "Invalid scene index " << sceneId);
    return false;
  }

  vtkGLTFDocumentLoader::Scene scene = m.Scenes[sceneId];

  bool createNewBlocks = (dataSet->GetNumberOfBlocks() == 0);

  int blockId = 0;
  for (int node : scene.Nodes)
  {
    std::string nodeDatasetName = "Node_" + value_to_string(node);
    vtkSmartPointer<vtkMultiBlockDataSet> nodeDataset = nullptr;
    if (!createNewBlocks)
    {
      // find existing child dataset for this node
      nodeDataset = vtkMultiBlockDataSet::SafeDownCast(dataSet->GetBlock(blockId));
    }
    if (!BuildMultiBlockDataSetFromNode(
          m, node, dataSet, nodeDataset, nodeDatasetName, applyDeformations, outputPointsPrecision))
    {
      vtkErrorWithObjectMacro(nullptr, "Could not build vtkMultiBlockDataSet from node " << node);
      return false;
    }
    blockId++;
  }
  return true;
}
}

//------------------------------------------------------------------------------
vtkStandardNewMacro(vtkGLTFReader);

//------------------------------------------------------------------------------
vtkGLTFReader::vtkGLTFReader()
{
  this->SetNumberOfInputPorts(0);
}

//------------------------------------------------------------------------------
vtkGLTFReader::~vtkGLTFReader()
{
  this->SetFileName(nullptr);
}

//------------------------------------------------------------------------------
void vtkGLTFReader::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os, indent);
  os << indent << "FileName: " << (this->FileName ? this->FileName : "(none)") << "\n";
  os << indent << "IsModelLoaded: " << (this->IsModelLoaded ? "On" : "Off") << "\n";
  os << indent << "IsMetaDataLoaded: " << (this->IsMetaDataLoaded ? "On" : "Off") << "\n";
  os << indent
     << "ApplyDeformationsToGeometry: " << (this->ApplyDeformationsToGeometry ? "On" : "Off")
     << "\n";
}

//------------------------------------------------------------------------------
void vtkGLTFReader::StoreTextureData()
{
  if (!this->Textures.empty())
  {
    this->Textures.clear();
  }
  if (this->Loader == nullptr || this->Loader->GetInternalModel()->Textures.empty())
  {
    return;
  }
  auto model = this->Loader->GetInternalModel();
  int nbTextures = static_cast<int>(model->Textures.size());
  int nbSamplers = static_cast<int>(model->Samplers.size());

  this->Textures.reserve(this->Loader->GetInternalModel()->Textures.size());
  for (const auto& loaderTexture : this->Loader->GetInternalModel()->Textures)
  {
    vtkNew<vtkGLTFTexture> readerTexture;
    if (loaderTexture.Source >= 0 && loaderTexture.Source < nbTextures)
    {
      readerTexture->Image = model->Images[loaderTexture.Source].ImageData;
    }
    else
    {
      vtkWarningMacro("Image index is out of range");
      continue;
    }
    if (loaderTexture.Sampler >= 0 && loaderTexture.Sampler < nbSamplers)
    {
      auto sampler = model->Samplers[loaderTexture.Sampler];
      readerTexture->Sampler = sampler;
    }
    this->Textures.emplace_back(readerTexture);
  }
}

//------------------------------------------------------------------------------
void vtkGLTFReader::InitializeLoader()
{
  this->Loader = vtkSmartPointer<vtkGLTFDocumentLoader>::New();
  this->Loader->SetGLBStart(this->GLBStart);
}

//------------------------------------------------------------------------------
int vtkGLTFReader::RequestInformation(
  vtkInformation* request, vtkInformationVector** inputVector, vtkInformationVector* outputVector)
{
  if (!this->Superclass::RequestInformation(request, inputVector, outputVector))
  {
    return 0;
  }

  // Read file metadata
  // Make sure we have a file to read

  if (this->Stream)
  {
    // Check for stream change in case the loader was already created
    if (this->Loader != nullptr && this->Loader->GetInternalModel() &&
      (this->Loader->GetInternalModel()->Stream != this->Stream ||
        this->Stream->GetMTime() != this->LastStreamTimeStamp))
    {
      this->IsMetaDataLoaded = false;
      this->IsModelLoaded = false;
      this->Textures.clear();
    }

    this->InitializeLoader();
    if (!this->Loader->LoadModelMetaDataFromStream(this->Stream, this->URILoader))
    {
      vtkErrorMacro("Error loading model metadata from stream");
      return 0;
    }

    this->LastStreamTimeStamp = this->Stream->GetMTime();

    vtkNew<vtkEventForwarderCommand> forwarder;
    forwarder->SetTarget(this);
    this->Loader->AddObserver(vtkCommand::ProgressEvent, forwarder);

    this->CreateAnimationSelection();
    this->CreateSceneNamesArray();
    this->SetCurrentScene(this->Loader->GetInternalModel()->DefaultScene);
    this->IsMetaDataLoaded = true;
  }
  else if (this->FileName)
  {
    std::string fileNameAsString(this->FileName);

    if (fileNameAsString.find('\\') != std::string::npos)
    {
      vtksys::SystemTools::ConvertToUnixSlashes(fileNameAsString);
    }

    if (!vtksys::SystemTools::FileIsFullPath(fileNameAsString))
    {
      fileNameAsString = vtksys::SystemTools::CollapseFullPath(fileNameAsString);
    }

    if (this->FileName != fileNameAsString)
    {
      this->SetFileName(fileNameAsString.c_str());
    }

    // Check for filename change in case the loader was already created
    if (this->Loader != nullptr && this->Loader->GetInternalModel() &&
      this->Loader->GetInternalModel()->FileName != this->FileName)
    {
      this->IsMetaDataLoaded = false;
      this->IsModelLoaded = false;
      this->Textures.clear();
    }

    // Load model metadata if not done previously
    if (!this->IsMetaDataLoaded)
    {
      this->InitializeLoader();
      if (!this->Loader->LoadModelMetaDataFromFile(this->FileName))
      {
        vtkErrorMacro("Error loading model metadata from file " << this->FileName);
        return 0;
      }

      vtkNew<vtkEventForwarderCommand> forwarder;
      forwarder->SetTarget(this);
      this->Loader->AddObserver(vtkCommand::ProgressEvent, forwarder);

      this->CreateAnimationSelection();
      this->CreateSceneNamesArray();
      this->SetCurrentScene(this->Loader->GetInternalModel()->DefaultScene);
      this->IsMetaDataLoaded = true;
    }
  }
  else
  {
    vtkErrorMacro("A FileName or a Stream must be specified.");
    return 0;
  }

  // Get model information (numbers and names of animations and scenes, time range of animations)
  // Add this info to the output vtkInformation
  auto model = this->Loader->GetInternalModel();
  vtkInformation* info = outputVector->GetInformationObject(0);

  // Find maximum animation duration (for TIME_RANGE())
  double maxDuration = 0.0;
  if (this->AnimationSelection != nullptr)
  {
    for (vtkIdType i = 0; i < this->AnimationSelection->GetNumberOfArrays(); i++)
    {
      // Only use enabled animations to track maximum duration values
      if (this->AnimationSelection->ArrayIsEnabled(this->AnimationSelection->GetArrayName(i)))
      {
        float duration = model->Animations[i].Duration;
        if (maxDuration < duration)
        {
          maxDuration = duration;
        }
      }
    }
  }

  // Append TIME_STEPS and/or TIME_RANGE
  if (maxDuration == 0.0)
  {
    info->Remove(vtkStreamingDemandDrivenPipeline::TIME_STEPS());
  }
  else
  {
    // Add TIME_RANGE
    std::array<double, 2> timeRange = { { 0.0, maxDuration } };
    info->Set(vtkStreamingDemandDrivenPipeline::TIME_RANGE(), timeRange.data(), 2);

    // If a framerate is set, TIME_STEPS are expected
    if (this->GetFrameRate() > 0)
    {
      int maxFrameIndex = vtkMath::Floor(this->GetFrameRate() * maxDuration);
      if (info->Has(vtkStreamingDemandDrivenPipeline::TIME_STEPS()))
      {
        info->Remove(vtkStreamingDemandDrivenPipeline::TIME_STEPS());
      }
      double period = 1.0 / this->GetFrameRate();
      // Append sampled time steps
      for (int i = 0; i <= maxFrameIndex; i++)
      {
        info->Append(vtkStreamingDemandDrivenPipeline::TIME_STEPS(), i * period);
      }
      // Append the last step of the animation, if it doesn't match with the last sampled step
      if (maxDuration != maxFrameIndex * period)
      {
        info->Append(vtkStreamingDemandDrivenPipeline::TIME_STEPS(), maxDuration);
      }
    }
    else if (info->Has(vtkStreamingDemandDrivenPipeline::TIME_STEPS()))
    {
      info->Remove(vtkStreamingDemandDrivenPipeline::TIME_STEPS());
    }
  }

  this->NumberOfAnimations = static_cast<vtkIdType>(model->Animations.size());
  this->NumberOfScenes = static_cast<vtkIdType>(model->Scenes.size());
  return 1;
}

//------------------------------------------------------------------------------
int vtkGLTFReader::RequestData(
  vtkInformation*, vtkInformationVector**, vtkInformationVector* outputVector)
{
  // Get the output
  vtkMultiBlockDataSet* output = vtkMultiBlockDataSet::GetData(outputVector);

  auto model = this->Loader->GetInternalModel();
  if (!this->IsModelLoaded)
  {
    std::vector<char> glbBuffer;

    if (this->Stream)
    {
      this->Loader->LoadStreamBuffer(this->Stream, glbBuffer);
    }
    else if (this->FileName)
    {
      this->Loader->LoadFileBuffer(this->FileName, glbBuffer);
    }
    else
    {
      vtkErrorMacro("A FileName or a Stream must be specified.");
      return 0;
    }

    // Load buffer data
    if (!this->Loader->LoadModelData(glbBuffer))
    {
      vtkErrorMacro("Error loading model data");
      return 0;
    }
    // Build polydata and transforms
    if (!this->Loader->BuildModelVTKGeometry())
    {
      vtkErrorMacro("Error building model vtk data");
      return 0;
    }
    this->StoreTextureData();
    this->IsModelLoaded = true;
  }

  if (this->OutputDataSet == nullptr)
  {
    this->OutputDataSet = vtkSmartPointer<vtkMultiBlockDataSet>::New();
  }

  // Apply selected animations on specified time step to the model's transforms
  vtkInformation* info = outputVector->GetInformationObject(0);

  if (info->Has(vtkStreamingDemandDrivenPipeline::UPDATE_TIME_STEP()))
  {
    double time = info->Get(vtkStreamingDemandDrivenPipeline::UPDATE_TIME_STEP());
    for (vtkIdType i = 0; i < this->NumberOfAnimations; i++)
    {
      if (this->AnimationSelection->GetArraySetting(i))
      {
        if (!this->Loader->ApplyAnimation(time, i))
        {
          vtkErrorMacro("Error applying animation");
          return 0;
        }
      }
      else if (this->PreviousAnimationSelection->GetArraySetting(i))
      {
        // Reset transforms and weights
        this->Loader->ResetAnimation(i);
      }
    }
    this->Loader->BuildGlobalTransforms();
  }

  vtkIdType selectedScene = this->CurrentScene;
  if (selectedScene < 0 || selectedScene >= static_cast<vtkIdType>(model->Scenes.size()))
  {
    selectedScene = model->DefaultScene;
  }

  if (!BuildMultiBlockDataSetFromScene(*(model), selectedScene, this->OutputDataSet,
        this->ApplyDeformationsToGeometry, this->OutputPointsPrecision))
  {
    vtkErrorMacro("Error building MultiBlockDataSet object");
    return 0;
  }

  // Save current animations
  this->PreviousAnimationSelection->CopySelections(this->AnimationSelection);

  output->CompositeShallowCopy(this->OutputDataSet);
  return 1;
}

//------------------------------------------------------------------------------
void vtkGLTFReader::EnableAnimation(vtkIdType animationIndex)
{
  if (this->AnimationSelection == nullptr)
  {
    vtkErrorMacro("Error accessing animations: model is not loaded yet");
    return;
  }
  if (animationIndex < 0 || animationIndex >= this->AnimationSelection->GetNumberOfArrays())
  {
    vtkErrorMacro("Out of range animation index");
    return;
  }
  auto name = this->AnimationSelection->GetArrayName(animationIndex);
  this->AnimationSelection->EnableArray(name);
  this->Modified();
}

//------------------------------------------------------------------------------
void vtkGLTFReader::DisableAnimation(vtkIdType animationIndex)
{
  if (this->AnimationSelection == nullptr)
  {
    vtkErrorMacro("Error accessing animations: model is not loaded yet");
    return;
  }
  if (animationIndex < 0 || animationIndex >= this->AnimationSelection->GetNumberOfArrays())
  {
    vtkErrorMacro("Out of range animation index");
    return;
  }
  auto name = this->AnimationSelection->GetArrayName(animationIndex);
  this->AnimationSelection->DisableArray(name);
  this->Modified();
}

//------------------------------------------------------------------------------
bool vtkGLTFReader::IsAnimationEnabled(vtkIdType animationIndex)
{
  if (this->AnimationSelection == nullptr)
  {
    vtkErrorMacro("Error accessing animations: model is not loaded yet");
    return false;
  }
  if (animationIndex < 0 || animationIndex >= this->AnimationSelection->GetNumberOfArrays())
  {
    vtkErrorMacro("Out of range animation index");
    return false;
  }
  auto name = this->AnimationSelection->GetArrayName(animationIndex);
  return this->AnimationSelection->ArrayIsEnabled(name) != 0;
}

//------------------------------------------------------------------------------
std::string vtkGLTFReader::GetAnimationName(vtkIdType animationIndex)
{
  if (this->Loader == nullptr || this->Loader->GetInternalModel() == nullptr)
  {
    vtkErrorMacro("Error while accessing animations: model is not loaded");
    return "";
  }
  if (animationIndex < 0 ||
    animationIndex >= static_cast<vtkIdType>(this->Loader->GetInternalModel()->Animations.size()))
  {
    vtkErrorMacro("Out of range animation index");
    return "";
  }
  return this->Loader->GetInternalModel()->Animations[animationIndex].Name;
}

//------------------------------------------------------------------------------
float vtkGLTFReader::GetAnimationDuration(vtkIdType animationIndex)
{
  if (this->Loader == nullptr || this->Loader->GetInternalModel() == nullptr)
  {
    vtkErrorMacro("Error while accessing animations: model is not loaded");
    return 0.0;
  }
  if (animationIndex < 0 ||
    animationIndex >= static_cast<vtkIdType>(this->Loader->GetInternalModel()->Animations.size()))
  {
    vtkErrorMacro("Out of range animation index");
    return 0.0;
  }
  return this->Loader->GetInternalModel()->Animations[animationIndex].Duration;
}

//------------------------------------------------------------------------------
std::string vtkGLTFReader::GetSceneName(vtkIdType sceneIndex)
{
  if (this->Loader == nullptr || this->Loader->GetInternalModel() == nullptr)
  {
    vtkErrorMacro("Error while accessing scenes: model is not loaded");
    return "";
  }
  if (sceneIndex < 0 ||
    sceneIndex >= static_cast<vtkIdType>(this->Loader->GetInternalModel()->Scenes.size()))
  {
    vtkErrorMacro("Out of range scene index");
    return "";
  }
  return this->Loader->GetInternalModel()->Scenes[sceneIndex].Name;
}

//------------------------------------------------------------------------------
vtkIdType vtkGLTFReader::GetNumberOfTextures()
{
  return static_cast<vtkIdType>(this->Textures.size());
}

//------------------------------------------------------------------------------
vtkSmartPointer<vtkGLTFTexture> vtkGLTFReader::GetTexture(vtkIdType textureIndex)
{
  if (textureIndex < 0 || textureIndex >= static_cast<vtkIdType>(this->Textures.size()))
  {
    vtkErrorMacro("Out of range texture index");
    vtkNew<vtkGLTFTexture> t;
    return t;
  }
  return this->Textures[textureIndex];
}

//------------------------------------------------------------------------------
vtkGLTFReader::GLTFTexture vtkGLTFReader::GetGLTFTexture(vtkIdType textureIndex)
{
  VTK_LEGACY_REPLACED_BODY(vtkGLTFReader::GetGLTFTexture, "VTK 9.4", vtkGLTFReader::GetTexture);
  if (textureIndex < 0 || textureIndex >= static_cast<vtkIdType>(this->Textures.size()))
  {
    vtkErrorMacro("Out of range texture index");
    return vtkGLTFReader::GLTFTexture{ nullptr, 0, 0, 0, 0 };
  }
  auto t = this->Textures[textureIndex];
  GLTFTexture gltfTexture{ t->Image, t->Sampler.MinFilter, t->Sampler.MagFilter, t->Sampler.WrapS,
    t->Sampler.WrapT };
  return gltfTexture;
}

//------------------------------------------------------------------------------
void vtkGLTFReader::SetScene(const std::string& scene)
{
  if (this->SceneNames == nullptr)
  {
    this->CurrentScene = 0;
    return;
  }
  for (vtkIdType i = 0; i < this->SceneNames->GetNumberOfValues(); i++)
  {
    if (scene == this->SceneNames->GetValue(i))
    {
      this->SetCurrentScene(i);
      this->OutputDataSet = nullptr;
      return;
    }
  }
  vtkWarningMacro("Scene '" << scene << "' does not exist.");
}

//------------------------------------------------------------------------------
void vtkGLTFReader::CreateSceneNamesArray()
{
  if (this->Loader == nullptr || this->Loader->GetInternalModel() == nullptr)
  {
    vtkErrorMacro("Error while accessing scenes: model is not loaded");
    return;
  }
  this->SceneNames = vtkSmartPointer<vtkStringArray>::New();
  this->SceneNames->SetNumberOfComponents(1);

  std::map<std::string, unsigned int> duplicateNameCounters;

  for (const auto& scene : this->Loader->GetInternalModel()->Scenes)
  {
    this->SceneNames->InsertNextValue(MakeUniqueNonEmptyName(scene.Name, duplicateNameCounters));
  }
}

//------------------------------------------------------------------------------
vtkStringArray* vtkGLTFReader::GetAllSceneNames()
{
  if (this->Loader == nullptr || this->Loader->GetInternalModel() == nullptr)
  {
    vtkErrorMacro("Error while accessing scenes: model is not loaded");
    return nullptr;
  }
  return this->SceneNames;
}

//------------------------------------------------------------------------------
vtkDataArraySelection* vtkGLTFReader::GetAnimationSelection()
{
  return this->AnimationSelection;
}

//------------------------------------------------------------------------------
void vtkGLTFReader::CreateAnimationSelection()
{
  if (this->Loader == nullptr || this->Loader->GetInternalModel() == nullptr)
  {
    vtkErrorMacro("Error while accessing animations: model is not loaded");
    return;
  }
  this->AnimationSelection = vtkSmartPointer<vtkDataArraySelection>::New();
  std::map<std::string, unsigned int> duplicateNameCounters;
  for (const auto& animation : this->Loader->GetInternalModel()->Animations)
  {
    this->AnimationSelection->AddArray(
      MakeUniqueNonEmptyName(animation.Name, duplicateNameCounters).c_str(), false);
  }
  this->PreviousAnimationSelection = vtkSmartPointer<vtkDataArraySelection>::New();
  this->PreviousAnimationSelection->CopySelections(this->AnimationSelection);
  this->AnimationSelection->AddObserver(vtkCommand::ModifiedEvent, this, &vtkGLTFReader::Modified);
}

//------------------------------------------------------------------------------
void vtkGLTFReader::SetApplyDeformationsToGeometry(bool flag)
{
  if (this->ApplyDeformationsToGeometry != flag)
  {
    this->OutputDataSet = nullptr;
    this->Modified();
  }
  this->ApplyDeformationsToGeometry = flag;
}
VTK_ABI_NAMESPACE_END
