// Copyright (c) 2017-2019 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 <RWGltf_CafWriter.hxx>

#include <BRep_Builder.hxx>
#include <gp_Quaternion.hxx>
#include <Message.hxx>
#include <Message_Messenger.hxx>
#include <Message_ProgressScope.hxx>
#include <NCollection_DataMap.hxx>
#include <OSD_FileSystem.hxx>
#include <OSD_File.hxx>
#include <OSD_Parallel.hxx>
#include <OSD_Path.hxx>
#include <OSD_Timer.hxx>
#include <RWGltf_GltfAccessorLayout.hxx>
#include <RWGltf_GltfArrayType.hxx>
#include <RWGltf_GltfMaterialMap.hxx>
#include <RWGltf_GltfPrimitiveMode.hxx>
#include <RWGltf_GltfRootElement.hxx>
#include <RWGltf_GltfSceneNodeMap.hxx>
#include <RWMesh.hxx>
#include <RWMesh_FaceIterator.hxx>
#include <Standard_Version.hxx>
#include <TDataStd_Name.hxx>
#include <TDF_Tool.hxx>
#include <TDocStd_Document.hxx>
#include <TopoDS_Compound.hxx>
#include <XCAFDoc_DocumentTool.hxx>
#include <XCAFDoc_ShapeTool.hxx>
#include <XCAFPrs_DocumentExplorer.hxx>

#ifdef HAVE_RAPIDJSON
  #include <RWGltf_GltfOStreamWriter.hxx>
#endif

#ifdef HAVE_DRACO
  #include <Standard_WarningsDisable.hxx>
  #include <draco/compression/encode.h>
  #include <Standard_WarningsRestore.hxx>
#endif

IMPLEMENT_STANDARD_RTTIEXT(RWGltf_CafWriter, Standard_Transient)

namespace
{
  //! Write three float values.
  static void writeVec3 (std::ostream& theStream,
                         const gp_XYZ& theVec3)
  {
    Graphic3d_Vec3 aVec3 (float(theVec3.X()), float(theVec3.Y()), float(theVec3.Z()));
    theStream.write ((const char* )aVec3.GetData(), sizeof(aVec3));
  }

  //! Write three float values.
  static void writeVec3 (std::ostream& theStream,
                         const Graphic3d_Vec3& theVec3)
  {
    theStream.write ((const char* )theVec3.GetData(), sizeof(theVec3));
  }

  //! Write two float values.
  static void writeVec2 (std::ostream& theStream,
                         const gp_XY&  theVec2)
  {
    Graphic3d_Vec2 aVec2 (float(theVec2.X()), float(theVec2.Y()));
    theStream.write ((const char* )aVec2.GetData(), sizeof(aVec2));
  }

  //! Write triangle indices.
  static void writeTriangle32 (std::ostream& theStream,
                               const Graphic3d_Vec3i& theTri)
  {
    theStream.write ((const char* )theTri.GetData(), sizeof(theTri));
  }

  //! Write triangle indices.
  static void writeTriangle16 (std::ostream& theStream,
                               const NCollection_Vec3<uint16_t>& theTri)
  {
    theStream.write ((const char* )theTri.GetData(), sizeof(theTri));
  }

#ifdef HAVE_DRACO
  //! Write nodes to Draco mesh
  static void writeNodesToDracoMesh (draco::Mesh& theMesh,
                                     const std::vector<Graphic3d_Vec3>& theNodes)
  {
    if (theNodes.empty())
    {
      return;
    }

    draco::PointAttribute anAttr;
    anAttr.Init (draco::GeometryAttribute::POSITION, 3, draco::DT_FLOAT32, false, sizeof(float) * 3);
    const int anId = theMesh.AddAttribute (anAttr, true, uint32_t(theNodes.size()));
    draco::PointAttribute* aPtr = theMesh.attribute (anId);
    draco::PointIndex anIndex(0);
    for (size_t aNodeInd = 0; aNodeInd < theNodes.size(); ++aNodeInd, ++anIndex)
    {
      aPtr->SetAttributeValue (aPtr->mapped_index(anIndex), theNodes[aNodeInd].GetData());
    }
  }

  //! Write normals to Draco mesh
  static void writeNormalsToDracoMesh (draco::Mesh& theMesh,
                                       const std::vector<Graphic3d_Vec3>& theNormals)
  {
    if (theNormals.empty())
    {
      return;
    }

    draco::PointAttribute anAttr;
    anAttr.Init (draco::GeometryAttribute::NORMAL, 3, draco::DT_FLOAT32, false, sizeof(float) * 3);
    const int anId = theMesh.AddAttribute (anAttr, true, uint32_t(theNormals.size()));
    draco::PointAttribute* aPtr = theMesh.attribute (anId);
    draco::PointIndex anIndex(0);
    for (size_t aNormInd = 0; aNormInd < theNormals.size(); ++aNormInd, ++anIndex)
    {
      aPtr->SetAttributeValue (aPtr->mapped_index(anIndex), theNormals[aNormInd].GetData());
    }
  }

  //! Write texture UV coordinates to Draco mesh
  static void writeTexCoordsToDracoMesh (draco::Mesh& theMesh,
                                         const std::vector<Graphic3d_Vec2>& theTexCoord)
  {
    if (theTexCoord.empty())
    {
      return;
    }

    draco::PointAttribute anAttr;
    anAttr.Init (draco::GeometryAttribute::TEX_COORD, 2, draco::DT_FLOAT32, false, sizeof(float) * 2);
    const int anId = theMesh.AddAttribute (anAttr, true, uint32_t(theTexCoord.size()));
    draco::PointAttribute* aPtr = theMesh.attribute (anId);
    draco::PointIndex anIndex(0);
    for (size_t aTexInd = 0; aTexInd < theTexCoord.size(); ++aTexInd, ++anIndex)
    {
      aPtr->SetAttributeValue (aPtr->mapped_index(anIndex), theTexCoord[aTexInd].GetData());
    }
  }

  //! Write indices to Draco mesh
  static void writeIndicesToDracoMesh (draco::Mesh& theMesh,
                                       const std::vector<Poly_Triangle>& theIndices)
  {
    draco::Mesh::Face aFace;
    int anIndex = 0;
    for (size_t anInd = 0; anInd < theIndices.size(); ++anInd, ++anIndex)
    {
      const Poly_Triangle& anElem = theIndices[anInd];
      aFace[0] = anElem.Value(1);
      aFace[1] = anElem.Value(2);
      aFace[2] = anElem.Value(3);
      theMesh.SetFace (draco::FaceIndex (anIndex), aFace);
    }
  }
#endif
}

#ifdef HAVE_DRACO
//! Functor for parallel execution of encoding meshes to Draco buffers.
class DracoEncodingFunctor
{
public:

  DracoEncodingFunctor (const Message_ProgressRange& theProgress,
                        draco::Encoder& theDracoEncoder,
                        const std::vector<std::shared_ptr<RWGltf_CafWriter::Mesh>>& theMeshes,
                        std::vector<std::shared_ptr<draco::EncoderBuffer>>& theEncoderBuffers)
  : myProgress(theProgress, "Draco compression", Max(1, int(theMeshes.size()))),
    myDracoEncoder(&theDracoEncoder),
    myRanges(0, int(theMeshes.size()) - 1),
    myMeshes(&theMeshes),
    myEncoderBuffers(&theEncoderBuffers)
  { 
    for (int anIndex = 0; anIndex != int(theMeshes.size()); ++anIndex)
    {
      myRanges.SetValue(anIndex, myProgress.Next());
    }
  }

  void operator () (int theMeshIndex) const
  {
    const std::shared_ptr<RWGltf_CafWriter::Mesh>& aCurrentMesh = myMeshes->at(theMeshIndex);
    if (aCurrentMesh->NodesVec.empty())
    {
      return;
    }

    Message_ProgressScope aScope(myRanges[theMeshIndex], NULL, 1);

    draco::Mesh aMesh;
    writeNodesToDracoMesh (aMesh, aCurrentMesh->NodesVec);

    if (!aCurrentMesh->NormalsVec.empty())
    {
      writeNormalsToDracoMesh (aMesh, aCurrentMesh->NormalsVec);
    }

    if (!aCurrentMesh->TexCoordsVec.empty())
    {
      writeTexCoordsToDracoMesh (aMesh, aCurrentMesh->TexCoordsVec);
    }

    writeIndicesToDracoMesh (aMesh, aCurrentMesh->IndicesVec);

    std::shared_ptr<draco::EncoderBuffer> anEncoderBuffer = std::make_shared<draco::EncoderBuffer>();
    draco::Status aStatus = myDracoEncoder->EncodeMeshToBuffer (aMesh, anEncoderBuffer.get());
    if (aStatus.ok())
    {
      myEncoderBuffers->at(theMeshIndex) = anEncoderBuffer;
    }

    aScope.Next();
  }

private:
  Message_ProgressScope   myProgress;
  draco::Encoder*         myDracoEncoder;
  NCollection_Array1<Message_ProgressRange>                   myRanges;
  const std::vector<std::shared_ptr<RWGltf_CafWriter::Mesh>>* myMeshes;
  std::vector<std::shared_ptr<draco::EncoderBuffer>>*         myEncoderBuffers;
};
#endif

//================================================================
// Function : Constructor
// Purpose  :
//================================================================
RWGltf_CafWriter::RWGltf_CafWriter (const TCollection_AsciiString& theFile,
                                    Standard_Boolean theIsBinary)
: myFile          (theFile),
  myTrsfFormat    (RWGltf_WriterTrsfFormat_Compact),
  myNodeNameFormat(RWMesh_NameFormat_InstanceOrProduct),
  myMeshNameFormat(RWMesh_NameFormat_Product),
  myIsBinary      (theIsBinary),
  myIsForcedUVExport (false),
  myToEmbedTexturesInGlb (true),
  myToMergeFaces (false),
  myToSplitIndices16 (false),
  myBinDataLen64  (0),
  myToParallel (false)
{
  myCSTrsf.SetOutputLengthUnit (1.0); // meters
  myCSTrsf.SetOutputCoordinateSystem (RWMesh_CoordinateSystem_glTF);

  TCollection_AsciiString aFolder, aFileName, aShortFileNameBase, aFileExt;
  OSD_Path::FolderAndFileFromPath (theFile, aFolder, aFileName);
  OSD_Path::FileNameAndExtension (aFileName, aShortFileNameBase, aFileExt);

  myBinFileNameShort = aShortFileNameBase + ".bin" + (myIsBinary ? ".tmp" : "");
  myBinFileNameFull = !aFolder.IsEmpty() ? aFolder + myBinFileNameShort : myBinFileNameShort;
}

//================================================================
// Function : Destructor
// Purpose  :
//================================================================
RWGltf_CafWriter::~RWGltf_CafWriter()
{
  myWriter.reset();
}

//================================================================
// Function : formatName
// Purpose  :
//================================================================
TCollection_AsciiString RWGltf_CafWriter::formatName (RWMesh_NameFormat theFormat,
                                                      const TDF_Label& theLabel,
                                                      const TDF_Label& theRefLabel) const
{
  return RWMesh::FormatName (theFormat, theLabel, theRefLabel);
}

//================================================================
// Function : toSkipFaceMesh
// Purpose  :
//================================================================
Standard_Boolean RWGltf_CafWriter::toSkipFaceMesh (const RWMesh_FaceIterator& theFaceIter)
{
  return theFaceIter.IsEmptyMesh();
}

// =======================================================================
// function : saveNodes
// purpose  :
// =======================================================================
void RWGltf_CafWriter::saveNodes (RWGltf_GltfFace& theGltfFace,
                                  std::ostream& theBinFile,
                                  const RWMesh_FaceIterator& theFaceIter,
                                  Standard_Integer& theAccessorNb,
                                  const std::shared_ptr<RWGltf_CafWriter::Mesh>& theMesh) const
{
  if (theGltfFace.NodePos.Id == RWGltf_GltfAccessor::INVALID_ID)
  {
    theGltfFace.NodePos.Id            = theAccessorNb++;
    theGltfFace.NodePos.ByteOffset    = (int64_t )theBinFile.tellp() - myBuffViewPos.ByteOffset;
    theGltfFace.NodePos.Type          = RWGltf_GltfAccessorLayout_Vec3;
    theGltfFace.NodePos.ComponentType = RWGltf_GltfAccessorCompType_Float32;
  }
  else
  {
    if (theMesh.get() == nullptr)
    {
      const int64_t aPos = theGltfFace.NodePos.ByteOffset + myBuffViewPos.ByteOffset + theGltfFace.NodePos.Count * sizeof(Graphic3d_Vec3);
      Standard_ASSERT_RAISE(aPos == (int64_t)theBinFile.tellp(), "wrong offset");
    }
  }
  theGltfFace.NodePos.Count += theFaceIter.NbNodes();

  const Standard_Integer aNodeUpper = theFaceIter.NodeUpper();
  for (Standard_Integer aNodeIter = theFaceIter.NodeLower(); aNodeIter <= aNodeUpper; ++aNodeIter)
  {
    gp_XYZ aNode = theFaceIter.NodeTransformed (aNodeIter).XYZ();
    myCSTrsf.TransformPosition (aNode);
    theGltfFace.NodePos.BndBox.Add (Graphic3d_Vec3d(aNode.X(), aNode.Y(), aNode.Z()));
    if (theMesh.get() != nullptr)
    {
      theMesh->NodesVec.push_back(Graphic3d_Vec3(float(aNode.X()), float(aNode.Y()), float(aNode.Z())));
    }
    else
    {
      writeVec3(theBinFile, aNode);
    }
  }
}

// =======================================================================
// function : saveNormals
// purpose  :
// =======================================================================
void RWGltf_CafWriter::saveNormals (RWGltf_GltfFace& theGltfFace,
                                    std::ostream& theBinFile,
                                    RWMesh_FaceIterator& theFaceIter,
                                    Standard_Integer& theAccessorNb,
                                    const std::shared_ptr<RWGltf_CafWriter::Mesh>& theMesh) const
{
  if (!theFaceIter.HasNormals())
  {
    return;
  }

  if (theGltfFace.NodeNorm.Id == RWGltf_GltfAccessor::INVALID_ID)
  {
    theGltfFace.NodeNorm.Id            = theAccessorNb++;
    theGltfFace.NodeNorm.ByteOffset    = (int64_t )theBinFile.tellp() - myBuffViewNorm.ByteOffset;
    theGltfFace.NodeNorm.Type          = RWGltf_GltfAccessorLayout_Vec3;
    theGltfFace.NodeNorm.ComponentType = RWGltf_GltfAccessorCompType_Float32;
  }
  else
  {
    if (theMesh.get() == nullptr)
    {
      const int64_t aPos = theGltfFace.NodeNorm.ByteOffset + myBuffViewNorm.ByteOffset + theGltfFace.NodeNorm.Count * sizeof(Graphic3d_Vec3);
      Standard_ASSERT_RAISE(aPos == (int64_t)theBinFile.tellp(), "wrong offset");
    }
  }
  theGltfFace.NodeNorm.Count += theFaceIter.NbNodes();

  const Standard_Integer aNodeUpper = theFaceIter.NodeUpper();
  for (Standard_Integer aNodeIter = theFaceIter.NodeLower(); aNodeIter <= aNodeUpper; ++aNodeIter)
  {
    const gp_Dir aNormal = theFaceIter.NormalTransformed (aNodeIter);
    Graphic3d_Vec3 aVecNormal ((float )aNormal.X(), (float )aNormal.Y(), (float )aNormal.Z());
    myCSTrsf.TransformNormal (aVecNormal);
    if (theMesh.get() != nullptr)
    {
      theMesh->NormalsVec.push_back(aVecNormal);
    }
    else
    {
      writeVec3(theBinFile, aVecNormal);
    }
  }
}

// =======================================================================
// function : saveTextCoords
// purpose  :
// =======================================================================
void RWGltf_CafWriter::saveTextCoords (RWGltf_GltfFace& theGltfFace,
                                       std::ostream& theBinFile,
                                       const RWMesh_FaceIterator& theFaceIter,
                                       Standard_Integer& theAccessorNb,
                                       const std::shared_ptr<RWGltf_CafWriter::Mesh>& theMesh) const
{
  if (!theFaceIter.HasTexCoords())
  {
    return;
  }
  if (!myIsForcedUVExport)
  {
    if (theFaceIter.FaceStyle().Material().IsNull())
    {
      return;
    }

    if (RWGltf_GltfMaterialMap::baseColorTexture (theFaceIter.FaceStyle().Material()).IsNull()
     && theFaceIter.FaceStyle().Material()->PbrMaterial().MetallicRoughnessTexture.IsNull()
     && theFaceIter.FaceStyle().Material()->PbrMaterial().EmissiveTexture.IsNull()
     && theFaceIter.FaceStyle().Material()->PbrMaterial().OcclusionTexture.IsNull()
     && theFaceIter.FaceStyle().Material()->PbrMaterial().NormalTexture.IsNull())
    {
      return;
    }
  }

  if (theGltfFace.NodeUV.Id == RWGltf_GltfAccessor::INVALID_ID)
  {
    theGltfFace.NodeUV.Id            = theAccessorNb++;
    theGltfFace.NodeUV.ByteOffset    = (int64_t )theBinFile.tellp() - myBuffViewTextCoord.ByteOffset;
    theGltfFace.NodeUV.Type          = RWGltf_GltfAccessorLayout_Vec2;
    theGltfFace.NodeUV.ComponentType = RWGltf_GltfAccessorCompType_Float32;
  }
  else
  {
    if (theMesh.get() == nullptr)
    {
      const int64_t aPos = theGltfFace.NodeUV.ByteOffset + myBuffViewTextCoord.ByteOffset + theGltfFace.NodeUV.Count * sizeof(Graphic3d_Vec2);
      Standard_ASSERT_RAISE(aPos == (int64_t)theBinFile.tellp(), "wrong offset");
    }
  }
  theGltfFace.NodeUV.Count += theFaceIter.NbNodes();

  const Standard_Integer aNodeUpper = theFaceIter.NodeUpper();
  for (Standard_Integer aNodeIter = theFaceIter.NodeLower(); aNodeIter <= aNodeUpper; ++aNodeIter)
  {
    gp_Pnt2d aTexCoord = theFaceIter.NodeTexCoord (aNodeIter);
    aTexCoord.SetY (1.0 - aTexCoord.Y());
    if (theMesh.get() != nullptr)
    {
      theMesh->TexCoordsVec.push_back(Graphic3d_Vec2((float)aTexCoord.X(), (float)aTexCoord.Y()));
    }
    else
    {
      writeVec2(theBinFile, aTexCoord.XY());
    }
  }
}

// =======================================================================
// function : saveIndices
// purpose  :
// =======================================================================
void RWGltf_CafWriter::saveIndices (RWGltf_GltfFace& theGltfFace,
                                    std::ostream& theBinFile,
                                    const RWMesh_FaceIterator& theFaceIter,
                                    Standard_Integer& theAccessorNb,
                                    const std::shared_ptr<RWGltf_CafWriter::Mesh>& theMesh)
{
  if (theGltfFace.Indices.Id == RWGltf_GltfAccessor::INVALID_ID)
  {
    theGltfFace.Indices.Id            = theAccessorNb++;
    theGltfFace.Indices.ByteOffset    = (int64_t )theBinFile.tellp() - myBuffViewInd.ByteOffset;
    theGltfFace.Indices.Type          = RWGltf_GltfAccessorLayout_Scalar;
    theGltfFace.Indices.ComponentType = theGltfFace.NodePos.Count > std::numeric_limits<uint16_t>::max()
                                      ? RWGltf_GltfAccessorCompType_UInt32
                                      : RWGltf_GltfAccessorCompType_UInt16;
  }
  else
  {
    if (theMesh.get() == nullptr)
    {
      const int64_t aRefPos = (int64_t )theBinFile.tellp();
      const int64_t aPos = theGltfFace.Indices.ByteOffset
                         + myBuffViewInd.ByteOffset
                         + theGltfFace.Indices.Count * (theGltfFace.Indices.ComponentType == RWGltf_GltfAccessorCompType_UInt32 ? sizeof(uint32_t) : sizeof(uint16_t));
      Standard_ASSERT_RAISE (aPos == aRefPos, "wrong offset");
    }
  }

  const Standard_Integer aNodeFirst = theGltfFace.NbIndexedNodes - theFaceIter.ElemLower();
  theGltfFace.NbIndexedNodes += theFaceIter.NbNodes();
  theGltfFace.Indices.Count += theFaceIter.NbTriangles() * 3;

  const Standard_Integer anElemLower = theFaceIter.ElemLower();
  const Standard_Integer anElemUpper = theFaceIter.ElemUpper();
  for (Standard_Integer anElemIter = anElemLower; anElemIter <= anElemUpper; ++anElemIter)
  {
    Poly_Triangle aTri = theFaceIter.TriangleOriented (anElemIter);
    aTri(1) += aNodeFirst;
    aTri(2) += aNodeFirst;
    aTri(3) += aNodeFirst;
    if (theMesh.get() != nullptr)
    {
      theMesh->IndicesVec.push_back(aTri);
    }
    else
    {
      if (theGltfFace.Indices.ComponentType == RWGltf_GltfAccessorCompType_UInt16)
      {
        writeTriangle16(theBinFile, NCollection_Vec3<uint16_t>((uint16_t)aTri(1), (uint16_t)aTri(2), (uint16_t)aTri(3)));
      }
      else
      {
        writeTriangle32(theBinFile, Graphic3d_Vec3i(aTri(1), aTri(2), aTri(3)));
      }
    }
  }
}

// =======================================================================
// function : Perform
// purpose  :
// =======================================================================
bool RWGltf_CafWriter::Perform (const Handle(TDocStd_Document)& theDocument,
                                const TColStd_IndexedDataMapOfStringString& theFileInfo,
                                const Message_ProgressRange& theProgress)
{
  TDF_LabelSequence aRoots;
  Handle(XCAFDoc_ShapeTool) aShapeTool = XCAFDoc_DocumentTool::ShapeTool (theDocument->Main());
  aShapeTool->GetFreeShapes (aRoots);
  return Perform (theDocument, aRoots, NULL, theFileInfo, theProgress);
}

// =======================================================================
// function : Perform
// purpose  :
// =======================================================================
bool RWGltf_CafWriter::Perform (const Handle(TDocStd_Document)& theDocument,
                                const TDF_LabelSequence& theRootLabels,
                                const TColStd_MapOfAsciiString* theLabelFilter,
                                const TColStd_IndexedDataMapOfStringString& theFileInfo,
                                const Message_ProgressRange& theProgress)
{
  Standard_Real aLengthUnit = 1.;
  if (XCAFDoc_DocumentTool::GetLengthUnit(theDocument, aLengthUnit))
  {
    myCSTrsf.SetInputLengthUnit(aLengthUnit);
  }
  const Standard_Integer aDefSamplerId = 0;
  myMaterialMap = new RWGltf_GltfMaterialMap (myFile, aDefSamplerId);
  myMaterialMap->SetDefaultStyle (myDefaultStyle);

  Message_ProgressScope aPSentry (theProgress, "Writing glTF file", 2);
  if (!writeBinData (theDocument, theRootLabels, theLabelFilter, aPSentry.Next()))
  {
    return false;
  }

  if (!aPSentry.More())
  {
    return false;
  }

  return writeJson (theDocument, theRootLabels, theLabelFilter, theFileInfo, aPSentry.Next());
}

// =======================================================================
// function : writeBinData
// purpose  :
// =======================================================================
bool RWGltf_CafWriter::writeBinData (const Handle(TDocStd_Document)& theDocument,
                                     const TDF_LabelSequence& theRootLabels,
                                     const TColStd_MapOfAsciiString* theLabelFilter,
                                     const Message_ProgressRange& theProgress)
{
#ifndef HAVE_DRACO
  if (myDracoParameters.DracoCompression)
  {
    Message::SendFail ("Error: cannot use Draco compression, Draco library missing.");
    return false;
  }
#endif

  myBuffViewPos.Id               = RWGltf_GltfAccessor::INVALID_ID;
  myBuffViewPos.ByteOffset       = 0;
  myBuffViewPos.ByteLength       = 0;
  myBuffViewPos.ByteStride       = 12;
  myBuffViewPos.Target           = RWGltf_GltfBufferViewTarget_ARRAY_BUFFER;

  myBuffViewNorm.Id              = RWGltf_GltfAccessor::INVALID_ID;
  myBuffViewNorm.ByteOffset      = 0;
  myBuffViewNorm.ByteLength      = 0;
  myBuffViewNorm.ByteStride      = 12;
  myBuffViewNorm.Target          = RWGltf_GltfBufferViewTarget_ARRAY_BUFFER;

  myBuffViewTextCoord.Id         = RWGltf_GltfAccessor::INVALID_ID;
  myBuffViewTextCoord.ByteOffset = 0;
  myBuffViewTextCoord.ByteLength = 0;
  myBuffViewTextCoord.ByteStride = 8;
  myBuffViewTextCoord.Target     = RWGltf_GltfBufferViewTarget_ARRAY_BUFFER;

  myBuffViewInd.Id               = RWGltf_GltfAccessor::INVALID_ID;
  myBuffViewInd.ByteOffset       = 0;
  myBuffViewInd.ByteLength       = 0;
  myBuffViewInd.Target           = RWGltf_GltfBufferViewTarget_ELEMENT_ARRAY_BUFFER;

  myBuffViewsDraco.clear();

  myBinDataMap.Clear();
  myBinDataLen64 = 0;

  Message_ProgressScope aScope(theProgress, "Write binary data", myDracoParameters.DracoCompression ? 2 : 1);

  const Handle(OSD_FileSystem)& aFileSystem = OSD_FileSystem::DefaultFileSystem();
  std::shared_ptr<std::ostream> aBinFile = aFileSystem->OpenOStream (myBinFileNameFull, std::ios::out | std::ios::binary);
  if (aBinFile.get() == NULL
   || !aBinFile->good())
  {
    Message::SendFail (TCollection_AsciiString ("File '") + myBinFileNameFull + "' can not be created");
    return false;
  }

  Message_ProgressScope aPSentryBin (aScope.Next(), "Binary data", 4);
  const RWGltf_GltfArrayType anArrTypes[4] =
  {
    RWGltf_GltfArrayType_Position,
    RWGltf_GltfArrayType_Normal,
    RWGltf_GltfArrayType_TCoord0,
    RWGltf_GltfArrayType_Indices
  };

  // dispatch faces
  NCollection_DataMap<XCAFPrs_Style, Handle(RWGltf_GltfFace)> aMergedFaces;
  for (XCAFPrs_DocumentExplorer aDocExplorer (theDocument, theRootLabels, XCAFPrs_DocumentExplorerFlags_OnlyLeafNodes);
       aDocExplorer.More() && aPSentryBin.More(); aDocExplorer.Next())
  {
    const XCAFPrs_DocumentNode& aDocNode = aDocExplorer.Current();
    if (theLabelFilter != NULL
    && !theLabelFilter->Contains (aDocNode.Id))
    {
      continue;
    }

    // transformation will be stored at scene nodes
    aMergedFaces.Clear (false);

    RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, TopLoc_Location(), true, aDocNode.Style);
    if (myToMergeFaces)
    {
      RWGltf_StyledShape aStyledShape (aFaceIter.ExploredShape(), aDocNode.Style);
      if (myBinDataMap.Contains (aStyledShape))
      {
        continue;
      }

      Handle(RWGltf_GltfFaceList) aGltfFaceList = new RWGltf_GltfFaceList();
      myBinDataMap.Add (aStyledShape, aGltfFaceList);
      for (; aFaceIter.More() && aPSentryBin.More(); aFaceIter.Next())
      {
        if (toSkipFaceMesh (aFaceIter))
        {
          continue;
        }

        Handle(RWGltf_GltfFace) aGltfFace;
        if (!aMergedFaces.Find (aFaceIter.FaceStyle(), aGltfFace))
        {
          aGltfFace = new RWGltf_GltfFace();
          aGltfFaceList->Append (aGltfFace);
          aGltfFace->Shape = aFaceIter.Face();
          aGltfFace->Style = aFaceIter.FaceStyle();
          aGltfFace->NbIndexedNodes = aFaceIter.NbNodes();
          aMergedFaces.Bind (aFaceIter.FaceStyle(), aGltfFace);
        }
        else if (myToSplitIndices16
             &&  aGltfFace->NbIndexedNodes < std::numeric_limits<uint16_t>::max()
             && (aGltfFace->NbIndexedNodes + aFaceIter.NbNodes()) >= std::numeric_limits<uint16_t>::max())
        {
          aMergedFaces.UnBind (aFaceIter.FaceStyle());
          aGltfFace = new RWGltf_GltfFace();
          aGltfFaceList->Append (aGltfFace);
          aGltfFace->Shape = aFaceIter.Face();
          aGltfFace->Style = aFaceIter.FaceStyle();
          aGltfFace->NbIndexedNodes = aFaceIter.NbNodes();
          aMergedFaces.Bind (aFaceIter.FaceStyle(), aGltfFace);
        }
        else
        {
          if (aGltfFace->Shape.ShapeType() != TopAbs_COMPOUND)
          {
            TopoDS_Shape anOldShape = aGltfFace->Shape;
            TopoDS_Compound aComp;
            BRep_Builder().MakeCompound (aComp);
            BRep_Builder().Add (aComp, anOldShape);
            aGltfFace->Shape = aComp;
          }
          BRep_Builder().Add (aGltfFace->Shape, aFaceIter.Face());
          aGltfFace->NbIndexedNodes += aFaceIter.NbNodes();
        }
      }
    }
    else
    {
      for (; aFaceIter.More() && aPSentryBin.More(); aFaceIter.Next())
      {
        RWGltf_StyledShape aStyledShape (aFaceIter.Face(), aFaceIter.FaceStyle());
        if (toSkipFaceMesh (aFaceIter)
         || myBinDataMap.Contains (aStyledShape))
        {
          continue;
        }

        Handle(RWGltf_GltfFaceList) aGltfFaceList = new RWGltf_GltfFaceList();
        Handle(RWGltf_GltfFace) aGltfFace = new RWGltf_GltfFace();
        aGltfFace->Shape = aFaceIter.Face();
        aGltfFace->Style = aFaceIter.FaceStyle();
        aGltfFaceList->Append (aGltfFace);
        myBinDataMap.Add (aStyledShape, aGltfFaceList);
      }
    }
  }

  std::vector<std::shared_ptr<RWGltf_CafWriter::Mesh>> aMeshes;
  Standard_Integer aNbAccessors = 0;
  NCollection_Map<Handle(RWGltf_GltfFaceList)> aWrittenFaces;
  NCollection_DataMap<TopoDS_Shape, Handle(RWGltf_GltfFace), TopTools_ShapeMapHasher> aWrittenPrimData;
  for (Standard_Integer aTypeIter = 0; aTypeIter < 4; ++aTypeIter)
  {
    const RWGltf_GltfArrayType anArrType = (RWGltf_GltfArrayType )anArrTypes[aTypeIter];
    RWGltf_GltfBufferView* aBuffView = NULL;
    switch (anArrType)
    {
      case RWGltf_GltfArrayType_Position: aBuffView = &myBuffViewPos;  break;
      case RWGltf_GltfArrayType_Normal:   aBuffView = &myBuffViewNorm; break;
      case RWGltf_GltfArrayType_TCoord0:  aBuffView = &myBuffViewTextCoord; break;
      case RWGltf_GltfArrayType_Indices:  aBuffView = &myBuffViewInd; break;
      default: break;
    }
    aBuffView->ByteOffset = aBinFile->tellp();
    aWrittenFaces.Clear (false);
    aWrittenPrimData.Clear (false);
#ifdef HAVE_DRACO
    size_t aMeshIndex = 0;
#endif
    for (ShapeToGltfFaceMap::Iterator aBinDataIter (myBinDataMap); aBinDataIter.More() && aPSentryBin.More(); aBinDataIter.Next())
    {
      const Handle(RWGltf_GltfFaceList)& aGltfFaceList = aBinDataIter.Value();
      if (!aWrittenFaces.Add (aGltfFaceList)) // skip repeating faces
      {
        continue;
      }
      
      std::shared_ptr<RWGltf_CafWriter::Mesh> aMeshPtr;
#ifdef HAVE_DRACO
      ++aMeshIndex;
      if (myDracoParameters.DracoCompression)
      {
        if (aMeshIndex <= aMeshes.size())
        {
          aMeshPtr = aMeshes.at(aMeshIndex - 1);
        }
        else
        {
          aMeshes.push_back(std::make_shared<RWGltf_CafWriter::Mesh>(RWGltf_CafWriter::Mesh()));
          aMeshPtr = aMeshes.back();
        }
      }
#endif

      for (RWGltf_GltfFaceList::Iterator aGltfFaceIter (*aGltfFaceList); aGltfFaceIter.More() && aPSentryBin.More(); aGltfFaceIter.Next())
      {
        const Handle(RWGltf_GltfFace)& aGltfFace = aGltfFaceIter.Value();

        Handle(RWGltf_GltfFace) anOldGltfFace;
        if (aWrittenPrimData.Find (aGltfFace->Shape, anOldGltfFace))
        {
          switch (anArrType)
          {
            case RWGltf_GltfArrayType_Position:
            {
              aGltfFace->NodePos = anOldGltfFace->NodePos;
              break;
            }
            case RWGltf_GltfArrayType_Normal:
            {
              aGltfFace->NodeNorm = anOldGltfFace->NodeNorm;
              break;
            }
            case RWGltf_GltfArrayType_TCoord0:
            {
              aGltfFace->NodeUV = anOldGltfFace->NodeUV;
              break;
            }
            case RWGltf_GltfArrayType_Indices:
            {
              aGltfFace->Indices = anOldGltfFace->Indices;
              break;
            }
            default:
            {
              break;
            }
          }
          continue;
        }
        aWrittenPrimData.Bind (aGltfFace->Shape, aGltfFace);

        for (RWMesh_FaceIterator aFaceIter (aGltfFace->Shape, aGltfFace->Style); aFaceIter.More() && aPSentryBin.More(); aFaceIter.Next())
        {
          switch (anArrType)
          {
            case RWGltf_GltfArrayType_Position:
            {
              aGltfFace->NbIndexedNodes = 0; // reset to zero before RWGltf_GltfArrayType_Indices step
              saveNodes (*aGltfFace, *aBinFile, aFaceIter, aNbAccessors, aMeshPtr);
              break;
            }
            case RWGltf_GltfArrayType_Normal:
            {
              saveNormals (*aGltfFace, *aBinFile, aFaceIter, aNbAccessors, aMeshPtr);
              break;
            }
            case RWGltf_GltfArrayType_TCoord0:
            {
              saveTextCoords (*aGltfFace, *aBinFile, aFaceIter, aNbAccessors, aMeshPtr);
              break;
            }
            case RWGltf_GltfArrayType_Indices:
            {
              saveIndices (*aGltfFace, *aBinFile, aFaceIter, aNbAccessors, aMeshPtr);
              break;
            }
            default:
            {
              break;
            }
          }

          if (!aBinFile->good())
          {
            Message::SendFail (TCollection_AsciiString ("File '") + myBinFileNameFull + "' cannot be written");
            return false;
          }
        }

        // add alignment by 4 bytes (might happen on RWGltf_GltfAccessorCompType_UInt16 indices)
        if (!myDracoParameters.DracoCompression)
        {
          int64_t aContentLen64 = (int64_t)aBinFile->tellp();
          while (aContentLen64 % 4 != 0)
          {
            aBinFile->write(" ", 1);
            ++aContentLen64;
          }
        }
      }
    }

    if (!myDracoParameters.DracoCompression)
    {
      aBuffView->ByteLength = (int64_t)aBinFile->tellp() - aBuffView->ByteOffset;
    }
    if (!aPSentryBin.More())
    {
      return false;
    }

    aPSentryBin.Next();
  }

  if (myDracoParameters.DracoCompression)
  {
#ifdef HAVE_DRACO
    OSD_Timer aDracoTimer;
    aDracoTimer.Start();
    draco::Encoder aDracoEncoder;
    aDracoEncoder.SetAttributeQuantization (draco::GeometryAttribute::POSITION,  myDracoParameters.QuantizePositionBits);
    aDracoEncoder.SetAttributeQuantization (draco::GeometryAttribute::NORMAL,    myDracoParameters.QuantizeNormalBits);
    aDracoEncoder.SetAttributeQuantization (draco::GeometryAttribute::TEX_COORD, myDracoParameters.QuantizeTexcoordBits);
    aDracoEncoder.SetAttributeQuantization (draco::GeometryAttribute::COLOR,     myDracoParameters.QuantizeColorBits);
    aDracoEncoder.SetAttributeQuantization (draco::GeometryAttribute::GENERIC,   myDracoParameters.QuantizeGenericBits);
    aDracoEncoder.SetSpeedOptions (myDracoParameters.CompressionLevel, myDracoParameters.CompressionLevel);

    std::vector<std::shared_ptr<draco::EncoderBuffer>> anEncoderBuffers(aMeshes.size());
    DracoEncodingFunctor aFunctor (aScope.Next(), aDracoEncoder, aMeshes, anEncoderBuffers);
    OSD_Parallel::For (0, int(aMeshes.size()), aFunctor, !myToParallel);

    for (size_t aBuffInd = 0; aBuffInd != anEncoderBuffers.size(); ++aBuffInd)
    {
      if (anEncoderBuffers.at(aBuffInd).get() == nullptr)
      {
        Message::SendFail(TCollection_AsciiString("Error: mesh not encoded in draco buffer."));
        return false;
      }
      RWGltf_GltfBufferView aBuffViewDraco;
      aBuffViewDraco.Id = (int)aBuffInd;
      aBuffViewDraco.ByteOffset = aBinFile->tellp();
      const draco::EncoderBuffer& anEncoderBuff = *anEncoderBuffers.at(aBuffInd);
      aBinFile->write(anEncoderBuff.data(), std::streamsize(anEncoderBuff.size()));
      if (!aBinFile->good())
      {
        Message::SendFail (TCollection_AsciiString("File '") + myBinFileNameFull + "' cannot be written");
        return false;
      }

      int64_t aLength = (int64_t)aBinFile->tellp();
      while (aLength % 4 != 0)
      {
        aBinFile->write(" ", 1);
        ++aLength;
      }

      aBuffViewDraco.ByteLength = aLength - aBuffViewDraco.ByteOffset;
      myBuffViewsDraco.push_back (aBuffViewDraco);
    }
    aDracoTimer.Stop();
    Message::SendInfo (TCollection_AsciiString("Draco compression time: ") + aDracoTimer.ElapsedTime() + " s");
#endif
  }

  if (myIsBinary
   && myToEmbedTexturesInGlb)
  {
    // save unique image textures
    for (XCAFPrs_DocumentExplorer aDocExplorer (theDocument, theRootLabels, XCAFPrs_DocumentExplorerFlags_OnlyLeafNodes);
         aDocExplorer.More() && aPSentryBin.More(); aDocExplorer.Next())
    {
      const XCAFPrs_DocumentNode& aDocNode = aDocExplorer.Current();
      if (theLabelFilter != NULL
      && !theLabelFilter->Contains (aDocNode.Id))
      {
        continue;
      }

      for (RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, TopLoc_Location(), true, aDocNode.Style);
           aFaceIter.More(); aFaceIter.Next())
      {
        if (toSkipFaceMesh (aFaceIter))
        {
          continue;
        }

        myMaterialMap->AddGlbImages (*aBinFile, aFaceIter.FaceStyle());
      }
    }
  }

  int aBuffViewId = 0;
  if (myBuffViewPos.ByteLength > 0)
  {
    myBuffViewPos.Id = aBuffViewId++;
  }
  if (myBuffViewNorm.ByteLength > 0)
  {
    myBuffViewNorm.Id = aBuffViewId++;
  }
  if (myBuffViewTextCoord.ByteLength > 0)
  {
    myBuffViewTextCoord.Id = aBuffViewId++;
  }
  if (myBuffViewInd.ByteLength > 0)
  {
    myBuffViewInd.Id = aBuffViewId++;
  }
  // myMaterialMap->FlushGlbBufferViews() will put image bufferView's IDs at the end of list

  myBinDataLen64 = aBinFile->tellp();
  aBinFile->flush();
  if (!aBinFile->good())
  {
    Message::SendFail (TCollection_AsciiString ("File '") + myBinFileNameFull + "' cannot be written");
    return false;
  }
  aBinFile.reset();
  return true;
}

//================================================================
// Function : writeJson
// Purpose  :
//================================================================
bool RWGltf_CafWriter::writeJson (const Handle(TDocStd_Document)&  theDocument,
                                  const TDF_LabelSequence&         theRootLabels,
                                  const TColStd_MapOfAsciiString*  theLabelFilter,
                                  const TColStd_IndexedDataMapOfStringString& theFileInfo,
                                  const Message_ProgressRange& theProgress)
{
#ifdef HAVE_RAPIDJSON
  myWriter.reset();

  // write vertex arrays
  Message_ProgressScope aPSentryBin (theProgress, "Header data", 2);

  const Standard_Integer aBinDataBufferId = 0;
  const Standard_Integer aDefSceneId      = 0;

  const TCollection_AsciiString aFileNameGltf = myFile;
  const Handle(OSD_FileSystem)& aFileSystem = OSD_FileSystem::DefaultFileSystem();
  std::shared_ptr<std::ostream> aGltfContentFile = aFileSystem->OpenOStream (aFileNameGltf, std::ios::out | std::ios::binary);
  if (aGltfContentFile.get() == NULL
   || !aGltfContentFile->good())
  {
    Message::SendFail (TCollection_AsciiString ("File '") + aFileNameGltf + "' can not be created");
    return false;
  }
  if (myIsBinary)
  {
    const char* aMagic = "glTF";
    uint32_t aVersion       = 2;
    uint32_t aLength        = 0;
    uint32_t aContentLength = 0;
    uint32_t aContentType   = 0x4E4F534A;

    aGltfContentFile->write (aMagic, 4);
    aGltfContentFile->write ((const char* )&aVersion,       sizeof(aVersion));
    aGltfContentFile->write ((const char* )&aLength,        sizeof(aLength));
    aGltfContentFile->write ((const char* )&aContentLength, sizeof(aContentLength));
    aGltfContentFile->write ((const char* )&aContentType,   sizeof(aContentType));
  }

  // Prepare an indexed map of scene nodes (without assemblies) in correct order.
  // Note: this is also order of meshes in glTF document array.
  RWGltf_GltfSceneNodeMap aSceneNodeMap;
  for (XCAFPrs_DocumentExplorer aDocExplorer (theDocument, theRootLabels, XCAFPrs_DocumentExplorerFlags_OnlyLeafNodes);
       aDocExplorer.More(); aDocExplorer.Next())
  {
    const XCAFPrs_DocumentNode& aDocNode = aDocExplorer.Current();
    if (theLabelFilter != NULL
    && !theLabelFilter->Contains (aDocNode.Id))
    {
      continue;
    }

    bool hasMeshData = false;
    if (!aDocNode.IsAssembly)
    {
      for (RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, TopLoc_Location(), true, aDocNode.Style); aFaceIter.More(); aFaceIter.Next())
      {
        if (!toSkipFaceMesh (aFaceIter))
        {
          hasMeshData = true;
          break;
        }
      }
    }
    if (hasMeshData)
    {
      aSceneNodeMap.Add (aDocNode);
    }
    else
    {
      // glTF disallows empty meshes / primitive arrays
      const TCollection_AsciiString aNodeName = formatName (RWMesh_NameFormat_ProductOrInstance, aDocNode.Label, aDocNode.RefLabel);
      Message::SendWarning (TCollection_AsciiString("RWGltf_CafWriter skipped node '") + aNodeName + "' without triangulation data");
    }
  }

  rapidjson::OStreamWrapper aFileStream (*aGltfContentFile);
  myWriter.reset (new RWGltf_GltfOStreamWriter (aFileStream));

  myWriter->StartObject();

  writeAccessors (aSceneNodeMap);
  writeAnimations();
  writeAsset (theFileInfo);
  writeBufferViews (aBinDataBufferId);
  writeBuffers();
  writeExtensions();

  writeImages    (aSceneNodeMap);
  writeMaterials (aSceneNodeMap);
  writeMeshes    (aSceneNodeMap);

  aPSentryBin.Next();
  if (!aPSentryBin.More())
  {
    return false;
  }

  // root nodes indices starting from 0
  NCollection_Sequence<Standard_Integer> aSceneRootNodeInds;
  writeNodes (theDocument, theRootLabels, theLabelFilter, aSceneNodeMap, aSceneRootNodeInds);
  writeSamplers();
  writeScene (aDefSceneId);
  writeScenes (aSceneRootNodeInds);
  writeSkins();
  writeTextures (aSceneNodeMap);

  myWriter->EndObject();

  if (!myIsBinary)
  {
    aGltfContentFile->flush();
    if (!aGltfContentFile->good())
    {
      Message::SendFail (TCollection_AsciiString ("File '") + aFileNameGltf + "' can not be written");
      return false;
    }
    aGltfContentFile.reset();
    return true;
  }

  int64_t aContentLen64 = (int64_t )aGltfContentFile->tellp() - 20;
  while (aContentLen64 % 4 != 0)
  {
    aGltfContentFile->write (" ", 1);
    ++aContentLen64;
  }

  const uint32_t aBinLength = (uint32_t )myBinDataLen64;
  const uint32_t aBinType   = 0x004E4942;
  aGltfContentFile->write ((const char*)&aBinLength, 4);
  aGltfContentFile->write ((const char*)&aBinType,   4);

  const int64_t aFullLen64 = aContentLen64 + 20 + myBinDataLen64 + 8;
  if (aFullLen64 < std::numeric_limits<uint32_t>::max())
  {
    {
      std::shared_ptr<std::istream> aBinFile = aFileSystem->OpenIStream (myBinFileNameFull, std::ios::in | std::ios::binary);
      if (aBinFile.get() == NULL || !aBinFile->good())
      {
        Message::SendFail (TCollection_AsciiString ("File '") + myBinFileNameFull + "' cannot be opened");
        return false;
      }
      char aBuffer[4096];
      for (; aBinFile->good();)
      {
        aBinFile->read (aBuffer, 4096);
        const Standard_Integer aReadLen = (Standard_Integer )aBinFile->gcount();
        if (aReadLen == 0)
        {
          break;
        }
        aGltfContentFile->write (aBuffer, aReadLen);
      }
    }
    OSD_Path aBinFilePath (myBinFileNameFull);
    OSD_File (aBinFilePath).Remove();
    if (OSD_File (aBinFilePath).Exists())
    {
      Message::SendFail (TCollection_AsciiString ("Unable to remove temporary glTF content file '") + myBinFileNameFull + "'");
    }
  }
  else
  {
    Message::SendFail ("glTF file content is too big for binary format");
    return false;
  }

  const uint32_t aLength        = (uint32_t )aFullLen64;
  const uint32_t aContentLength = (uint32_t )aContentLen64;
  aGltfContentFile->seekp (8);
  aGltfContentFile->write ((const char* )&aLength,        4);
  aGltfContentFile->write ((const char* )&aContentLength, 4);

  aGltfContentFile->flush();
  if (!aGltfContentFile->good())
  {
    Message::SendFail (TCollection_AsciiString ("File '") + aFileNameGltf + "' can not be written");
    return false;
  }
  aGltfContentFile.reset();
  myWriter.reset();
  return true;
#else
  (void )theDocument;
  (void )theRootLabels;
  (void )theLabelFilter;
  (void )theFileInfo;
  (void )theProgress;
  Message::SendFail ("Error: glTF writer is unavailable - OCCT has been built without RapidJSON support [HAVE_RAPIDJSON undefined]");
  return false;
#endif
}

// =======================================================================
// function : writeAccessors
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeAccessors (const RWGltf_GltfSceneNodeMap& )
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeAccessors()");

  myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Accessors));
  myWriter->StartArray();

  const RWGltf_GltfArrayType anArrTypes[4] =
  {
    RWGltf_GltfArrayType_Position,
    RWGltf_GltfArrayType_Normal,
    RWGltf_GltfArrayType_TCoord0,
    RWGltf_GltfArrayType_Indices
  };
  NCollection_Map<Handle(RWGltf_GltfFaceList)> aWrittenFaces;
  NCollection_Map<int> aWrittenIds;
  int aNbAccessors = 0;
  for (Standard_Integer aTypeIter = 0; aTypeIter < 4; ++aTypeIter)
  {
    const RWGltf_GltfArrayType anArrType = (RWGltf_GltfArrayType )anArrTypes[aTypeIter];
    aWrittenFaces.Clear (false);
    for (ShapeToGltfFaceMap::Iterator aBinDataIter (myBinDataMap); aBinDataIter.More(); aBinDataIter.Next())
    {
      const Handle(RWGltf_GltfFaceList)& aGltfFaceList = aBinDataIter.Value();
      if (!aWrittenFaces.Add (aGltfFaceList)) // skip repeating faces
      {
        continue;
      }

      for (RWGltf_GltfFaceList::Iterator aFaceIter (*aGltfFaceList); aFaceIter.More(); aFaceIter.Next())
      {
        const Handle(RWGltf_GltfFace)& aGltfFace = aFaceIter.Value();
        switch (anArrType)
        {
          case RWGltf_GltfArrayType_Position:
          {
            const int anAccessorId = aGltfFace->NodePos.Id;
            if (anAccessorId == RWGltf_GltfAccessor::INVALID_ID
            || !aWrittenIds.Add (anAccessorId))
            {
              break;
            }

            if (anAccessorId != aNbAccessors)
            {
              throw Standard_ProgramError ("Internal error: RWGltf_CafWriter::writeAccessors()");
            }
            ++aNbAccessors;
            writePositions (*aGltfFace);
            break;
          }
          case RWGltf_GltfArrayType_Normal:
          {
            const int anAccessorId = aGltfFace->NodeNorm.Id;
            if (anAccessorId == RWGltf_GltfAccessor::INVALID_ID
            || !aWrittenIds.Add (anAccessorId))
            {
              break;
            }

            if (anAccessorId != aNbAccessors)
            {
              throw Standard_ProgramError ("Internal error: RWGltf_CafWriter::writeAccessors()");
            }
            ++aNbAccessors;
            writeNormals (*aGltfFace);
            break;
          }
          case RWGltf_GltfArrayType_TCoord0:
          {
            const int anAccessorId = aGltfFace->NodeUV.Id;
            if (anAccessorId == RWGltf_GltfAccessor::INVALID_ID
            || !aWrittenIds.Add (anAccessorId)
             )
            {
              break;
            }

            if (anAccessorId != aNbAccessors)
            {
              throw Standard_ProgramError ("Internal error: RWGltf_CafWriter::writeAccessors()");
            }
            ++aNbAccessors;
            writeTextCoords (*aGltfFace);
            break;
          }
          case RWGltf_GltfArrayType_Indices:
          {
            const int anAccessorId = aGltfFace->Indices.Id;
            if (anAccessorId == RWGltf_GltfAccessor::INVALID_ID
            || !aWrittenIds.Add (anAccessorId)
             )
            {
              break;
            }

            if (anAccessorId != aNbAccessors)
            {
              throw Standard_ProgramError ("Internal error: RWGltf_CafWriter::writeAccessors()");
            }
            ++aNbAccessors;
            writeIndices (*aGltfFace);
            break;
          }
          default:
          {
            break;
          }
        }
      }
    }
  }

  myWriter->EndArray();
#endif
}

// =======================================================================
// function : writePositions
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writePositions (const RWGltf_GltfFace& theGltfFace)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writePositions()");
  if (theGltfFace.NodePos.Id == RWGltf_GltfAccessor::INVALID_ID)
  {
    return;
  }

  myWriter->StartObject();
  if (!myDracoParameters.DracoCompression)
  {
    myWriter->Key    ("bufferView");
    myWriter->Int    (myBuffViewPos.Id);
    myWriter->Key    ("byteOffset");
    myWriter->Int64  (theGltfFace.NodePos.ByteOffset);
  }
  myWriter->Key    ("componentType");
  myWriter->Int    (theGltfFace.NodePos.ComponentType);
  myWriter->Key    ("count");
  myWriter->Int64  (theGltfFace.NodePos.Count);

  if (theGltfFace.NodePos.BndBox.IsValid())
  {
    myWriter->Key ("max");
    myWriter->StartArray();
    myWriter->Double (theGltfFace.NodePos.BndBox.CornerMax().x());
    myWriter->Double (theGltfFace.NodePos.BndBox.CornerMax().y());
    myWriter->Double (theGltfFace.NodePos.BndBox.CornerMax().z());
    myWriter->EndArray();

    myWriter->Key("min");
    myWriter->StartArray();
    myWriter->Double (theGltfFace.NodePos.BndBox.CornerMin().x());
    myWriter->Double (theGltfFace.NodePos.BndBox.CornerMin().y());
    myWriter->Double (theGltfFace.NodePos.BndBox.CornerMin().z());
    myWriter->EndArray();
  }
  myWriter->Key    ("type");
  myWriter->String ("VEC3");

  myWriter->EndObject();
#else
  (void )theGltfFace;
#endif
}

// =======================================================================
// function : writeNormals
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeNormals (const RWGltf_GltfFace& theGltfFace)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeNormals()");
  if (theGltfFace.NodeNorm.Id == RWGltf_GltfAccessor::INVALID_ID)
  {
    return;
  }

  myWriter->StartObject();
  if (!myDracoParameters.DracoCompression)
  {
    myWriter->Key    ("bufferView");
    myWriter->Int    (myBuffViewNorm.Id);
    myWriter->Key    ("byteOffset");
    myWriter->Int64  (theGltfFace.NodeNorm.ByteOffset);
  }
  myWriter->Key    ("componentType");
  myWriter->Int    (theGltfFace.NodeNorm.ComponentType);
  myWriter->Key    ("count");
  myWriter->Int64  (theGltfFace.NodeNorm.Count);
  // min/max values are optional, and not very useful for normals - skip them
  /*{
    myWriter->Key ("max");
    myWriter->StartArray();
    myWriter->Double (1.0);
    myWriter->Double (1.0);
    myWriter->Double (1.0);
    myWriter->EndArray();
  }
  {
    myWriter->Key ("min");
    myWriter->StartArray();
    myWriter->Double (0.0);
    myWriter->Double (0.0);
    myWriter->Double (0.0);
    myWriter->EndArray();
  }*/
  myWriter->Key    ("type");
  myWriter->String ("VEC3");

  myWriter->EndObject();
#else
  (void )theGltfFace;
#endif
}

// =======================================================================
// function : writeTextCoords
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeTextCoords (const RWGltf_GltfFace& theGltfFace)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeTextCoords()");
  if (theGltfFace.NodeUV.Id == RWGltf_GltfAccessor::INVALID_ID)
  {
    return;
  }

  myWriter->StartObject();
  if (!myDracoParameters.DracoCompression)
  {
    myWriter->Key    ("bufferView");
    myWriter->Int    (myBuffViewTextCoord.Id);
    myWriter->Key    ("byteOffset");
    myWriter->Int64  (theGltfFace.NodeUV.ByteOffset);
  }
  myWriter->Key    ("componentType");
  myWriter->Int    (theGltfFace.NodeUV.ComponentType);
  myWriter->Key    ("count");
  myWriter->Int64  (theGltfFace.NodeUV.Count);
  // min/max values are optional, and not very useful for UV coordinates - skip them
  /*{
    myWriter->Key ("max");
    myWriter->StartArray();
    myWriter->Double (1.0);
    myWriter->Double (1.0);
    myWriter->Double (1.0);
    myWriter->EndArray();
  }
  {
    myWriter->Key ("min");
    myWriter->StartArray();
    myWriter->Double (0.0);
    myWriter->Double (0.0);
    myWriter->Double (0.0);
    myWriter->EndArray();
  }*/
  myWriter->Key    ("type");
  myWriter->String ("VEC2");

  myWriter->EndObject();
#else
  (void )theGltfFace;
#endif
}

// =======================================================================
// function : writeIndices
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeIndices (const RWGltf_GltfFace& theGltfFace)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeIndices()");
  if (theGltfFace.Indices.Id == RWGltf_GltfAccessor::INVALID_ID)
  {
    return;
  }

  myWriter->StartObject();
  if (!myDracoParameters.DracoCompression)
  {
    myWriter->Key("bufferView");
    myWriter->Int(myBuffViewInd.Id);
    myWriter->Key("byteOffset");
    myWriter->Int64(theGltfFace.Indices.ByteOffset);
  }
  myWriter->Key    ("componentType");
  myWriter->Int    (theGltfFace.Indices.ComponentType);
  myWriter->Key    ("count");
  myWriter->Int64  (theGltfFace.Indices.Count);

  myWriter->Key    ("type");
  myWriter->String ("SCALAR");

  myWriter->EndObject();
#else
  (void )theGltfFace;
#endif
}

// =======================================================================
// function : writeAnimations
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeAnimations()
{
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeAnimations()");

  // This section should be skipped if it doesn't contain any information but not be empty
  //myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Animations));
  //myWriter->StartArray();
  //myWriter->EndArray();
}

// =======================================================================
// function : writeAsset
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeAsset (const TColStd_IndexedDataMapOfStringString& theFileInfo)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeAsset()");

  myWriter->Key    (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Asset));
  myWriter->StartObject();
  myWriter->Key    ("generator");
  myWriter->String ("Open CASCADE Technology " OCC_VERSION_STRING " [dev.opencascade.org]");
  myWriter->Key    ("version");
  myWriter->String ("2.0"); // glTF format version

  bool anIsStarted = false;
  for (TColStd_IndexedDataMapOfStringString::Iterator aKeyValueIter (theFileInfo); aKeyValueIter.More(); aKeyValueIter.Next())
  {
    if (!anIsStarted)
    {
      myWriter->Key ("extras");
      myWriter->StartObject();
      anIsStarted = true;
    }
    myWriter->Key (aKeyValueIter.Key().ToCString());
    myWriter->String (aKeyValueIter.Value().ToCString());
  }
  if (anIsStarted)
  {
    myWriter->EndObject();
  }

  myWriter->EndObject();
#else
  (void )theFileInfo;
#endif
}

// =======================================================================
// function : writeBufferViews
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeBufferViews (const Standard_Integer theBinDataBufferId)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeBufferViews()");

  int aBuffViewId = 0;
  myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_BufferViews));
  myWriter->StartArray();
  if (myBuffViewPos.Id != RWGltf_GltfAccessor::INVALID_ID)
  {
    aBuffViewId++;
    myWriter->StartObject();
    myWriter->Key    ("buffer");
    myWriter->Int    (theBinDataBufferId);
    myWriter->Key    ("byteLength");
    myWriter->Int64  (myBuffViewPos.ByteLength);
    myWriter->Key    ("byteOffset");
    myWriter->Int64  (myBuffViewPos.ByteOffset);
    myWriter->Key    ("byteStride");
    myWriter->Int64  (myBuffViewPos.ByteStride);
    myWriter->Key    ("target");
    myWriter->Int    (myBuffViewPos.Target);
    myWriter->EndObject();
  }
  if (myBuffViewNorm.Id != RWGltf_GltfAccessor::INVALID_ID)
  {
    aBuffViewId++;
    myWriter->StartObject();
    myWriter->Key    ("buffer");
    myWriter->Int    (theBinDataBufferId);
    myWriter->Key    ("byteLength");
    myWriter->Int64  (myBuffViewNorm.ByteLength);
    myWriter->Key    ("byteOffset");
    myWriter->Int64  (myBuffViewNorm.ByteOffset);
    myWriter->Key    ("byteStride");
    myWriter->Int64  (myBuffViewNorm.ByteStride);
    myWriter->Key    ("target");
    myWriter->Int    (myBuffViewNorm.Target);
    myWriter->EndObject();
  }
  if (myBuffViewTextCoord.Id != RWGltf_GltfAccessor::INVALID_ID)
  {
    aBuffViewId++;
    myWriter->StartObject();
    myWriter->Key    ("buffer");
    myWriter->Int    (theBinDataBufferId);
    myWriter->Key    ("byteLength");
    myWriter->Int64  (myBuffViewTextCoord.ByteLength);
    myWriter->Key    ("byteOffset");
    myWriter->Int64  (myBuffViewTextCoord.ByteOffset);
    myWriter->Key    ("byteStride");
    myWriter->Int64  (myBuffViewTextCoord.ByteStride);
    myWriter->Key    ("target");
    myWriter->Int    (myBuffViewTextCoord.Target);
    myWriter->EndObject();
  }
  if (myBuffViewInd.Id != RWGltf_GltfAccessor::INVALID_ID)
  {
    aBuffViewId++;
    myWriter->StartObject();
    myWriter->Key    ("buffer");
    myWriter->Int    (theBinDataBufferId);
    myWriter->Key    ("byteLength");
    myWriter->Int64  (myBuffViewInd.ByteLength);
    myWriter->Key    ("byteOffset");
    myWriter->Int64  (myBuffViewInd.ByteOffset);
    myWriter->Key    ("target");
    myWriter->Int    (myBuffViewInd.Target);
    myWriter->EndObject();
  }
  if (myDracoParameters.DracoCompression)
  {
    for (size_t aBufInd = 0; aBufInd != myBuffViewsDraco.size(); ++aBufInd)
    {
      if (myBuffViewsDraco[aBufInd].Id != RWGltf_GltfAccessor::INVALID_ID)
      {
        aBuffViewId++;
        myWriter->StartObject();
        myWriter->Key("buffer");
        myWriter->Int(theBinDataBufferId);
        myWriter->Key("byteLength");
        myWriter->Int64(myBuffViewsDraco[aBufInd].ByteLength);
        myWriter->Key("byteOffset");
        myWriter->Int64(myBuffViewsDraco[aBufInd].ByteOffset);
        myWriter->EndObject();
      }
    }
  }

  myMaterialMap->FlushGlbBufferViews (myWriter.get(), theBinDataBufferId, aBuffViewId);

  myWriter->EndArray();
#else
  (void )theBinDataBufferId;
#endif
}

// =======================================================================
// function : writeBuffers
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeBuffers()
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeBuffers()");

  myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Buffers));
  myWriter->StartArray();
  {
    myWriter->StartObject();
    {
      myWriter->Key   ("byteLength");
      myWriter->Int64 (myBinDataLen64);
      if (!myIsBinary)
      {
        myWriter->Key   ("uri");
        myWriter->String (myBinFileNameShort.ToCString());
      }
    }
    myWriter->EndObject();
  }
  myWriter->EndArray();
#endif
}

// =======================================================================
// function : writeExtensions
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeExtensions()
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeExtensions()");

  if (myDracoParameters.DracoCompression)
  {
    myWriter->Key(RWGltf_GltfRootElementName(RWGltf_GltfRootElement_ExtensionsUsed));

    myWriter->StartArray();
    {
      myWriter->Key("KHR_draco_mesh_compression");
    }
    myWriter->EndArray();

    myWriter->Key(RWGltf_GltfRootElementName(RWGltf_GltfRootElement_ExtensionsRequired));

    myWriter->StartArray();
    {
      myWriter->Key("KHR_draco_mesh_compression");
    }
    myWriter->EndArray();
  }
#endif
}

// =======================================================================
// function : writeImages
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeImages (const RWGltf_GltfSceneNodeMap& theSceneNodeMap)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeImages()");

  // empty RWGltf_GltfRootElement_Images section should NOT be written to avoid validator errors
  if (myIsBinary
   && myToEmbedTexturesInGlb)
  {
    myMaterialMap->FlushGlbImages (myWriter.get());
  }
  else
  {
    bool anIsStarted = false;
    for (RWGltf_GltfSceneNodeMap::Iterator aSceneNodeIter(theSceneNodeMap); aSceneNodeIter.More(); aSceneNodeIter.Next())
    {
      const XCAFPrs_DocumentNode& aDocNode = aSceneNodeIter.Value();
      for (RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, TopLoc_Location(), true, aDocNode.Style); aFaceIter.More(); aFaceIter.Next())
      {
        myMaterialMap->AddImages (myWriter.get(), aFaceIter.FaceStyle(), anIsStarted);
      }
    }
    if (anIsStarted)
    {
      myWriter->EndArray();
    }
  }
#else
  (void )theSceneNodeMap;
#endif
}

// =======================================================================
// function : writeMaterials
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeMaterials (const RWGltf_GltfSceneNodeMap& theSceneNodeMap)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeMaterials()");

  // empty RWGltf_GltfRootElement_Materials section should NOT be written to avoid validator errors
  bool anIsStarted = false;
  for (RWGltf_GltfSceneNodeMap::Iterator aSceneNodeIter (theSceneNodeMap); aSceneNodeIter.More(); aSceneNodeIter.Next())
  {
    const XCAFPrs_DocumentNode& aDocNode = aSceneNodeIter.Value();
    for (RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, TopLoc_Location(), true, aDocNode.Style); aFaceIter.More(); aFaceIter.Next())
    {
      myMaterialMap->AddMaterial (myWriter.get(), aFaceIter.FaceStyle(), anIsStarted);
    }
  }
  if (anIsStarted)
  {
    myWriter->EndArray();
  }
#else
  (void )theSceneNodeMap;
#endif
}

// =======================================================================
// function : writePrimArray
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writePrimArray (const RWGltf_GltfFace& theGltfFace,
                                       const TCollection_AsciiString& theName,
                                       const int theDracoBufInd,
                                       bool& theToStartPrims)
{
#ifdef HAVE_RAPIDJSON
  if (theToStartPrims)
  {
    theToStartPrims = false;
    myWriter->StartObject();
    if (!theName.IsEmpty())
    {
      myWriter->Key ("name");
      myWriter->String (theName.ToCString());
    }
    myWriter->Key ("primitives");
    myWriter->StartArray();
  }

  const TCollection_AsciiString aMatId = myMaterialMap->FindMaterial (theGltfFace.Style);
  myWriter->StartObject();
  {
    myWriter->Key ("attributes");
    myWriter->StartObject();
    {
      if (theGltfFace.NodeNorm.Id != RWGltf_GltfAccessor::INVALID_ID)
      {
        myWriter->Key ("NORMAL");
        myWriter->Int (theGltfFace.NodeNorm.Id);
      }
      myWriter->Key ("POSITION");
      myWriter->Int (theGltfFace.NodePos.Id);
      if (theGltfFace.NodeUV.Id != RWGltf_GltfAccessor::INVALID_ID)
      {
        myWriter->Key ("TEXCOORD_0");
        myWriter->Int (theGltfFace.NodeUV.Id);
      }
    }
    myWriter->EndObject();

    myWriter->Key ("indices");
    myWriter->Int (theGltfFace.Indices.Id);
    if (!aMatId.IsEmpty())
    {
      myWriter->Key ("material");
      myWriter->Int (aMatId.IntegerValue());
    }
    myWriter->Key ("mode");
    myWriter->Int (RWGltf_GltfPrimitiveMode_Triangles);

    if (myDracoParameters.DracoCompression)
    {
      myWriter->Key("extensions");
      myWriter->StartObject();
      {
        myWriter->Key("KHR_draco_mesh_compression");
        myWriter->StartObject();
        myWriter->Key("bufferView");
        myWriter->Int(myBuffViewsDraco[theDracoBufInd].Id);
        myWriter->Key("attributes");
        myWriter->StartObject();
        {
          int anAttrInd = 0;
          if (theGltfFace.NodePos.Id != RWGltf_GltfAccessor::INVALID_ID)
          {
            myWriter->Key("POSITION");
            myWriter->Int(anAttrInd++);
          }
          if (theGltfFace.NodeNorm.Id != RWGltf_GltfAccessor::INVALID_ID)
          {
            myWriter->Key("NORMAL");
            myWriter->Int(anAttrInd++);
          }
          if (theGltfFace.NodeUV.Id != RWGltf_GltfAccessor::INVALID_ID)
          {
            myWriter->Key("TEXCOORD_0");
            myWriter->Int(anAttrInd++);
          }
        }
        myWriter->EndObject();
        myWriter->EndObject();
      }
      myWriter->EndObject();
    }
  }
  myWriter->EndObject();
#else
  (void )theGltfFace;
  (void )theName;
  (void )theToStartPrims;
  (void )theDracoBufInd;
#endif
}

// =======================================================================
// function : writeMeshes
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeMeshes (const RWGltf_GltfSceneNodeMap& theSceneNodeMap)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeMeshes()");

  myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Meshes));
  myWriter->StartArray();

  int aDracoBufInd = 0;
  NCollection_IndexedDataMap<int, int> aDracoBufIndMap;
  NCollection_Map<Handle(RWGltf_GltfFaceList)> aWrittenFaces;
  for (RWGltf_GltfSceneNodeMap::Iterator aSceneNodeIter (theSceneNodeMap); aSceneNodeIter.More(); aSceneNodeIter.Next())
  {
    const XCAFPrs_DocumentNode& aDocNode = aSceneNodeIter.Value();
    const TCollection_AsciiString aNodeName = formatName (myMeshNameFormat, aDocNode.Label, aDocNode.RefLabel);

    bool toStartPrims = true;
    Standard_Integer aNbFacesInNode = 0;
    aWrittenFaces.Clear (false);
    if (myToMergeFaces)
    {
      TopoDS_Shape aShape;
      if (!XCAFDoc_ShapeTool::GetShape (aDocNode.RefLabel, aShape)
      ||  aShape.IsNull())
      {
        continue;
      }

      Handle(RWGltf_GltfFaceList) aGltfFaceList;
      aShape.Location (TopLoc_Location());
      RWGltf_StyledShape aStyledShape (aShape, aDocNode.Style);
      myBinDataMap.FindFromKey (aStyledShape, aGltfFaceList);
      if (!aWrittenFaces.Add (aGltfFaceList))
      {
        continue;
      }

      for (RWGltf_GltfFaceList::Iterator aFaceGroupIter (*aGltfFaceList); aFaceGroupIter.More(); aFaceGroupIter.Next())
      {
        const Handle(RWGltf_GltfFace)& aGltfFace = aFaceGroupIter.Value();
        const int aPrevSize = aDracoBufIndMap.Size();
        const int aTempDracoBufInd = aDracoBufInd;
        if (myDracoParameters.DracoCompression
        && !aDracoBufIndMap.FindFromKey (aGltfFace->NodePos.Id, aDracoBufInd))
        {
          aDracoBufIndMap.Add (aGltfFace->NodePos.Id, aDracoBufInd);
        }

        writePrimArray (*aGltfFace, aNodeName, aDracoBufInd, toStartPrims);
        if (aTempDracoBufInd != aDracoBufInd)
        {
          aDracoBufInd = aTempDracoBufInd;
        }
        if (!myDracoParameters.DracoCompression || aDracoBufIndMap.Size() > aPrevSize)
        {
          ++aDracoBufInd;
        }
      }
    }
    else
    {
      for (RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, TopLoc_Location(), true, aDocNode.Style); aFaceIter.More(); aFaceIter.Next(), ++aNbFacesInNode)
      {
        if (toSkipFaceMesh (aFaceIter))
        {
          continue;
        }

        RWGltf_StyledShape aStyledShape (aFaceIter.Face(), aFaceIter.FaceStyle());
        const Handle(RWGltf_GltfFaceList)& aGltfFaceList = myBinDataMap.FindFromKey (aStyledShape);
        if (!aWrittenFaces.Add (aGltfFaceList))
        {
          continue;
        }

        const Handle(RWGltf_GltfFace)& aGltfFace = aGltfFaceList->First();
        const int aPrevSize = aDracoBufIndMap.Size();
        const int aTempDracoBufInd = aDracoBufInd;
        if (myDracoParameters.DracoCompression
        && !aDracoBufIndMap.FindFromKey(aGltfFace->NodePos.Id, aDracoBufInd))
        {
          aDracoBufIndMap.Add(aGltfFace->NodePos.Id, aDracoBufInd);
        }

        writePrimArray (*aGltfFace, aNodeName, aDracoBufInd, toStartPrims);
        if (aTempDracoBufInd != aDracoBufInd)
        {
          aDracoBufInd = aTempDracoBufInd;
        }
        if (!myDracoParameters.DracoCompression || aDracoBufIndMap.Size() > aPrevSize)
        {
          ++aDracoBufInd;
        }
      }
    }

    if (!toStartPrims)
    {
      myWriter->EndArray();
      myWriter->EndObject();
    }
  }
  myWriter->EndArray();
#else
  (void )theSceneNodeMap;
#endif
}

// =======================================================================
// function : writeNodes
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeNodes (const Handle(TDocStd_Document)&  theDocument,
                                   const TDF_LabelSequence&         theRootLabels,
                                   const TColStd_MapOfAsciiString*  theLabelFilter,
                                   const RWGltf_GltfSceneNodeMap&   theSceneNodeMap,
                                   NCollection_Sequence<Standard_Integer>& theSceneRootNodeInds)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeNodes()");

  // Prepare full indexed map of scene nodes in correct order.
  RWGltf_GltfSceneNodeMap aSceneNodeMapWithChildren; // indexes starting from 1
  for (XCAFPrs_DocumentExplorer aDocExplorer (theDocument, theRootLabels, XCAFPrs_DocumentExplorerFlags_None);
       aDocExplorer.More(); aDocExplorer.Next())
  {
    const XCAFPrs_DocumentNode& aDocNode = aDocExplorer.Current();
    if (theLabelFilter != NULL
    && !theLabelFilter->Contains (aDocNode.Id))
    {
      continue;
    }

    // keep empty nodes
    //RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, TopLoc_Location(), false);
    //if (!aFaceIter.More()) { continue; }

    Standard_Integer aNodeIndex = aSceneNodeMapWithChildren.Add (aDocNode);
    if (aDocExplorer.CurrentDepth() == 0)
    {
      // save root node index (starting from 0 not 1)
      theSceneRootNodeInds.Append (aNodeIndex - 1);
    }
  }

  // Write scene nodes using prepared map for correct order of array members
  myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Nodes));
  myWriter->StartArray();

  for (RWGltf_GltfSceneNodeMap::Iterator aSceneNodeIter (aSceneNodeMapWithChildren); aSceneNodeIter.More(); aSceneNodeIter.Next())
  {
    const XCAFPrs_DocumentNode& aDocNode = aSceneNodeIter.Value();

    myWriter->StartObject();
    {
      if (aDocNode.IsAssembly)
      {
        myWriter->Key ("children");
        myWriter->StartArray();
        {
          for (TDF_ChildIterator aChildIter (aDocNode.RefLabel); aChildIter.More(); aChildIter.Next())
          {
            const TDF_Label& aChildLabel = aChildIter.Value();
            if (aChildLabel.IsNull())
            {
              continue;
            }

            const TCollection_AsciiString aChildId = XCAFPrs_DocumentExplorer::DefineChildId (aChildLabel, aDocNode.Id);
            Standard_Integer aChildIdx = aSceneNodeMapWithChildren.FindIndex (aChildId);
            if (aChildIdx > 0)
            {
              myWriter->Int (aChildIdx - 1);
            }
          }
        }
        myWriter->EndArray();
      }
    }
    if (!aDocNode.LocalTrsf.IsIdentity())
    {
      gp_Trsf aTrsf = aDocNode.LocalTrsf.Transformation();
      if (aTrsf.Form() != gp_Identity)
      {
        myCSTrsf.TransformTransformation (aTrsf);
        const gp_Quaternion aQuaternion = aTrsf.GetRotation();
        const bool hasRotation = Abs (aQuaternion.X())       > gp::Resolution()
                              || Abs (aQuaternion.Y())       > gp::Resolution()
                              || Abs (aQuaternion.Z())       > gp::Resolution()
                              || Abs (aQuaternion.W() - 1.0) > gp::Resolution();
        const Standard_Real aScaleFactor = aTrsf.ScaleFactor();
        const bool hasScale = Abs (aScaleFactor - 1.0) > Precision::Confusion();
        const gp_XYZ& aTranslPart = aTrsf.TranslationPart();
        const bool hasTranslation = aTranslPart.SquareModulus() > gp::Resolution();

        RWGltf_WriterTrsfFormat aTrsfFormat = myTrsfFormat;
        if (myTrsfFormat == RWGltf_WriterTrsfFormat_Compact)
        {
          aTrsfFormat = hasRotation && hasScale && hasTranslation
                      ? RWGltf_WriterTrsfFormat_Mat4
                      : RWGltf_WriterTrsfFormat_TRS;
        }

        if (aTrsfFormat == RWGltf_WriterTrsfFormat_Mat4)
        {
          // write full matrix
          Graphic3d_Mat4 aMat4;
          aTrsf.GetMat4 (aMat4);
          if (!aMat4.IsIdentity())
          {
            myWriter->Key ("matrix");
            myWriter->StartArray();
            for (Standard_Integer aColIter = 0; aColIter < 4; ++aColIter)
            {
              for (Standard_Integer aRowIter = 0; aRowIter < 4; ++aRowIter)
              {
                myWriter->Double (aMat4.GetValue (aRowIter, aColIter));
              }
            }
            myWriter->EndArray();
          }
        }
        else //if (aTrsfFormat == RWGltf_WriterTrsfFormat_TRS)
        {
          if (hasRotation)
          {
            myWriter->Key ("rotation");
            myWriter->StartArray();
            myWriter->Double (aQuaternion.X());
            myWriter->Double (aQuaternion.Y());
            myWriter->Double (aQuaternion.Z());
            myWriter->Double (aQuaternion.W());
            myWriter->EndArray();
          }
          if (hasScale)
          {
            myWriter->Key ("scale");
            myWriter->StartArray();
            myWriter->Double (aScaleFactor);
            myWriter->Double (aScaleFactor);
            myWriter->Double (aScaleFactor);
            myWriter->EndArray();
          }
          if (hasTranslation)
          {
            myWriter->Key ("translation");
            myWriter->StartArray();
            myWriter->Double (aTranslPart.X());
            myWriter->Double (aTranslPart.Y());
            myWriter->Double (aTranslPart.Z());
            myWriter->EndArray();
          }
        }
      }
    }
    if (!aDocNode.IsAssembly)
    {
      // Mesh order of current node is equal to order of this node in scene nodes map
      Standard_Integer aMeshIdx = theSceneNodeMap.FindIndex (aDocNode.Id);
      if (aMeshIdx > 0)
      {
        myWriter->Key ("mesh");
        myWriter->Int (aMeshIdx - 1);
      }
    }
    {
      const TCollection_AsciiString aNodeName = formatName (myNodeNameFormat, aDocNode.Label, aDocNode.RefLabel);
      if (!aNodeName.IsEmpty())
      {
        myWriter->Key ("name");
        myWriter->String (aNodeName.ToCString());
      }
    }
    myWriter->EndObject();
  }
  myWriter->EndArray();
#else
  (void )theDocument;
  (void )theRootLabels;
  (void )theLabelFilter;
  (void )theSceneNodeMap;
  (void )theSceneRootNodeInds;
#endif
}

// =======================================================================
// function : writeSamplers
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeSamplers()
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeSamplers()");
  if (myMaterialMap->NbImages() == 0)
  {
    return;
  }

  myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Samplers));
  myWriter->StartArray();
  {
    myWriter->StartObject();
    {
      //myWriter->Key ("magFilter");
      //myWriter->Int (9729);
      //myWriter->Key ("minFilter");
      //myWriter->Int (9729);
    }
    myWriter->EndObject();
  }
  myWriter->EndArray();
#endif
}

// =======================================================================
// function : writeScene
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeScene (const Standard_Integer theDefSceneId)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeScene()");

  myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Scene));
  myWriter->Int (theDefSceneId);
#else
  (void )theDefSceneId;
#endif
}

// =======================================================================
// function : writeScenes
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeScenes (const NCollection_Sequence<Standard_Integer>& theSceneRootNodeInds)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeScenes()");

  myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Scenes));
  myWriter->StartArray();
  {
    myWriter->StartObject();
    myWriter->Key ("nodes");
    myWriter->StartArray();
    for (NCollection_Sequence<Standard_Integer>::Iterator aRootIter (theSceneRootNodeInds); aRootIter.More(); aRootIter.Next())
    {
      myWriter->Int (aRootIter.Value());
    }
    myWriter->EndArray();
    myWriter->EndObject();
  }
  myWriter->EndArray();
#else
  (void )theSceneRootNodeInds;
#endif
}

// =======================================================================
// function : writeSkins
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeSkins()
{
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeSkins()");

  // This section should be skipped if it doesn't contain any information but not be empty
  /*myWriter->Key (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Skins));
  myWriter->StartArray();
  myWriter->EndArray();*/
}

// =======================================================================
// function : writeTextures
// purpose  :
// =======================================================================
void RWGltf_CafWriter::writeTextures (const RWGltf_GltfSceneNodeMap& theSceneNodeMap)
{
#ifdef HAVE_RAPIDJSON
  Standard_ProgramError_Raise_if (myWriter.get() == NULL, "Internal error: RWGltf_CafWriter::writeTextures()");

  // empty RWGltf_GltfRootElement_Textures section should not be written to avoid validator errors
  bool anIsStarted = false;
  for (RWGltf_GltfSceneNodeMap::Iterator aSceneNodeIter (theSceneNodeMap); aSceneNodeIter.More(); aSceneNodeIter.Next())
  {
    const XCAFPrs_DocumentNode& aDocNode = aSceneNodeIter.Value();
    for (RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, TopLoc_Location(), true, aDocNode.Style); aFaceIter.More(); aFaceIter.Next())
    {
      myMaterialMap->AddTextures (myWriter.get(), aFaceIter.FaceStyle(), anIsStarted);
    }
  }
  if (anIsStarted)
  {
    myWriter->EndArray();
  }
#else
 (void )theSceneNodeMap;
#endif
}
