// Author: Kirill Gavrilov
// Copyright (c) 2016-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.

#ifndef _RWGltf_GltfJsonParser_HeaderFile
#define _RWGltf_GltfJsonParser_HeaderFile

#include <Message_Gravity.hxx>
#include <Message_ProgressScope.hxx>
#include <RWGltf_GltfPrimArrayData.hxx>
#include <RWGltf_GltfLatePrimitiveArray.hxx>
#include <RWGltf_GltfBufferView.hxx>
#include <RWGltf_GltfRootElement.hxx>
#include <RWGltf_MaterialCommon.hxx>
#include <RWGltf_MaterialMetallicRoughness.hxx>
#include <RWMesh_CoordinateSystemConverter.hxx>
#include <RWMesh_NodeAttributes.hxx>
#include <TColStd_IndexedDataMapOfStringString.hxx>
#include <TopoDS_Face.hxx>
#include <TopTools_SequenceOfShape.hxx>

// workaround name collisions with XLib
#ifdef None
  #undef None
#endif
#ifdef Bool
  #undef Bool
#endif

#ifdef HAVE_RAPIDJSON
  // #define RAPIDJSON_ASSERT
  #include <Standard_WarningsDisable.hxx>
  #include <rapidjson/document.h>
  #include <rapidjson/prettywriter.h>
  #include <rapidjson/stringbuffer.h>
  #include <rapidjson/istreamwrapper.h>
  #include <rapidjson/ostreamwrapper.h>
  #include <Standard_WarningsRestore.hxx>

typedef rapidjson::Document::ValueType RWGltf_JsonValue;
#endif

//! INTERNAL tool for parsing glTF document (JSON structure).
class RWGltf_GltfJsonParser
#ifdef HAVE_RAPIDJSON
    : public rapidjson::Document
#endif
{
public:
#ifdef HAVE_RAPIDJSON
  //! Auxiliary method for formatting error code.
  Standard_EXPORT static const char* FormatParseError(rapidjson::ParseErrorCode theCode);
#endif

public:
  //! Empty constructor.
  Standard_EXPORT RWGltf_GltfJsonParser(TopTools_SequenceOfShape& theRootShapes);

  //! Set file path.
  Standard_EXPORT void SetFilePath(const TCollection_AsciiString& theFilePath);

  //! Set flag for probing file without complete reading.
  void SetProbeHeader(bool theToProbe) { myToProbeHeader = theToProbe; }

  //! Return prefix for reporting issues.
  const TCollection_AsciiString& ErrorPrefix() const { return myErrorPrefix; }

  //! Set prefix for reporting issues.
  void SetErrorPrefix(const TCollection_AsciiString& theErrPrefix) { myErrorPrefix = theErrPrefix; }

  //! Set map for storing node attributes.
  void SetAttributeMap(RWMesh_NodeAttributeMap& theAttribMap) { myAttribMap = &theAttribMap; }

  //! Set list for storing external files.
  void SetExternalFiles(NCollection_IndexedMap<TCollection_AsciiString>& theExternalFiles)
  {
    myExternalFiles = &theExternalFiles;
  }

  //! Set metadata map.
  void SetMetadata(TColStd_IndexedDataMapOfStringString& theMetadata) { myMetadata = &theMetadata; }

  //! Set flag to translate asset.extras into metadata.
  void SetReadAssetExtras(bool theToRead) { myToReadAssetExtras = theToRead; }

  //! Return transformation from glTF to OCCT coordinate system.
  const RWMesh_CoordinateSystemConverter& CoordinateSystemConverter() const { return myCSTrsf; }

  //! Set transformation from glTF to OCCT coordinate system.
  void SetCoordinateSystemConverter(const RWMesh_CoordinateSystemConverter& theConverter)
  {
    myCSTrsf = theConverter;
  }

  //! Initialize binary format.
  void SetBinaryFormat(int64_t theBinBodyOffset, int64_t theBinBodyLen)
  {
    myIsBinary      = true;
    myBinBodyOffset = theBinBodyOffset;
    myBinBodyLen    = theBinBodyLen;
  }

  //! Set flag to ignore nodes without Geometry, TRUE by default.
  void SetSkipEmptyNodes(bool theToSkip) { myToSkipEmptyNodes = theToSkip; }

  //! Set flag to flag to load all scenes in the document, FALSE by default which means only main
  //! (default) scene will be loaded.
  void SetLoadAllScenes(bool theToLoadAll) { myToLoadAllScenes = theToLoadAll; }

  //! Set flag to use Mesh name in case if Node name is empty, TRUE by default.
  void SetMeshNameAsFallback(bool theToFallback) { myUseMeshNameAsFallback = theToFallback; }

  //! Parse glTF document.
  Standard_EXPORT bool Parse(const Message_ProgressRange& theProgress);

  //! Return face list for loading triangulation.
  NCollection_Vector<TopoDS_Face>& FaceList() { return myFaceList; }

protected:
#ifdef HAVE_RAPIDJSON
  //! Search mandatory root elements in the document.
  //! Return FALSE if some mandatory element is missing.
  Standard_EXPORT bool gltfParseRoots();

  //! Parse default scene.
  Standard_EXPORT bool gltfParseScene(const Message_ProgressRange& theProgress);

  //! Parse document metadata.
  Standard_EXPORT void gltfParseAsset();

protected:
  //! Parse materials defined in the document.
  Standard_EXPORT void gltfParseMaterials();

  //! Parse standard material.
  Standard_EXPORT bool gltfParseStdMaterial(Handle(RWGltf_MaterialCommon)& theMat,
                                            const RWGltf_JsonValue&        theMatNode);

  //! Parse pbrMetallicRoughness material.
  Standard_EXPORT bool gltfParsePbrMaterial(Handle(RWGltf_MaterialMetallicRoughness)& theMat,
                                            const RWGltf_JsonValue&                   theMatNode);

  //! Parse common material (KHR_materials_common extension).
  Standard_EXPORT bool gltfParseCommonMaterial(Handle(RWGltf_MaterialCommon)& theMat,
                                               const RWGltf_JsonValue&        theMatNode);

  //! Parse texture definition.
  Standard_EXPORT bool gltfParseTexture(Handle(Image_Texture)&  theTexture,
                                        const RWGltf_JsonValue* theTextureId);

  //! Parse texture definition in binary buffer of GLB file.
  Standard_EXPORT bool gltfParseTexturInGlbBuffer(Handle(Image_Texture)&         theTexture,
                                                  const RWGltf_JsonValue&        theBinVal,
                                                  const TCollection_AsciiString& theBufferViewId,
                                                  const RWGltf_JsonValue&        theBufferViewName);

  //! Parse texture definition in binary buffer of glTF file.
  Standard_EXPORT bool gltfParseTextureInBufferView(Handle(Image_Texture)&         theTexture,
                                                    const TCollection_AsciiString& theSourceId,
                                                    const TCollection_AsciiString& theBufferViewhId,
                                                    const RWGltf_JsonValue&        theBufferView);

  //! Bind material definition to the map.
  Standard_EXPORT void gltfBindMaterial(const Handle(RWGltf_MaterialMetallicRoughness)& theMatPbr,
                                        const Handle(RWGltf_MaterialCommon)& theMatCommon);

protected:
  //! Parse scene array of nodes recursively.
  Standard_EXPORT bool gltfParseSceneNodes(TopTools_SequenceOfShape&    theShapeSeq,
                                           const RWGltf_JsonValue&      theSceneNodes,
                                           const Message_ProgressRange& theProgress);

  //! Parse scene node recursively.
  Standard_EXPORT bool gltfParseSceneNode(TopoDS_Shape&                  theNodeShape,
                                          const TCollection_AsciiString& theSceneNodeId,
                                          const RWGltf_JsonValue&        theSceneNode,
                                          const Message_ProgressRange&   theProgress);

  //! Parse mesh element.
  Standard_EXPORT bool gltfParseMesh(TopoDS_Shape&                  theMeshShape,
                                     const TCollection_AsciiString& theMeshId,
                                     const RWGltf_JsonValue&        theMesh);

  //! Parse primitive array.
  Standard_EXPORT bool gltfParsePrimArray(TopoDS_Shape&                  thePrimArrayShape,
                                          const TCollection_AsciiString& theMeshId,
                                          const TCollection_AsciiString& theMeshName,
                                          const RWGltf_JsonValue&        thePrimArray);

  //! Parse accessor.
  Standard_EXPORT bool gltfParseAccessor(const Handle(RWGltf_GltfLatePrimitiveArray)& theMeshData,
                                         const TCollection_AsciiString&               theName,
                                         const RWGltf_JsonValue&                      theAccessor,
                                         const RWGltf_GltfArrayType                   theType,
                                         const RWGltf_JsonValue* theCompBuffView);

  //! Parse buffer view.
  Standard_EXPORT bool gltfParseBufferView(const Handle(RWGltf_GltfLatePrimitiveArray)& theMeshData,
                                           const TCollection_AsciiString&               theName,
                                           const RWGltf_JsonValue&    theBufferView,
                                           const RWGltf_GltfAccessor& theAccessor,
                                           const RWGltf_GltfArrayType theType);

  //! Parse buffer.
  Standard_EXPORT bool gltfParseBuffer(const Handle(RWGltf_GltfLatePrimitiveArray)& theMeshData,
                                       const TCollection_AsciiString&               theName,
                                       const RWGltf_JsonValue&                      theBuffer,
                                       const RWGltf_GltfAccessor&                   theAccessor,
                                       const RWGltf_GltfBufferView&                 theView,
                                       const RWGltf_GltfArrayType                   theType);

protected:
  //! Read vec4 from specified item.
  static bool gltfReadVec4(Graphic3d_Vec4d& theVec4, const RWGltf_JsonValue* theVal)
  {
    if (theVal == NULL || !theVal->IsArray() || theVal->Size() != 4)
    {
      return false;
    }

    for (int aCompIter = 0; aCompIter < 4; ++aCompIter)
    {
      const RWGltf_JsonValue& aGenVal = (*theVal)[aCompIter];
      if (!aGenVal.IsNumber())
      {
        return false;
      }
      theVec4[aCompIter] = aGenVal.GetDouble();
    }
    return true;
  }

  //! Validate color
  static bool validateColor4(const Graphic3d_Vec4d& theVec)
  {
    return theVec.r() >= 0.0 && theVec.r() <= 1.0 && theVec.g() >= 0.0 && theVec.g() <= 1.0
           && theVec.b() >= 0.0 && theVec.b() <= 1.0 && theVec.a() >= 0.0 && theVec.a() <= 1.0;
  }

  //! Read vec3 from specified item.
  static bool gltfReadVec3(Graphic3d_Vec3d& theVec3, const RWGltf_JsonValue* theVal)
  {
    if (theVal == NULL || !theVal->IsArray() || theVal->Size() != 3)
    {
      return false;
    }

    for (int aCompIter = 0; aCompIter < 3; ++aCompIter)
    {
      const RWGltf_JsonValue& aGenVal = (*theVal)[aCompIter];
      if (!aGenVal.IsNumber())
      {
        return false;
      }
      theVec3[aCompIter] = aGenVal.GetDouble();
    }
    return true;
  }

  //! Validate color
  static bool validateColor3(const Graphic3d_Vec3d& theVec)
  {
    return theVec.r() >= 0.0 && theVec.r() <= 1.0 && theVec.g() >= 0.0 && theVec.g() <= 1.0
           && theVec.b() >= 0.0 && theVec.b() <= 1.0;
  }

protected:
  //! Groups for re-using shapes.
  enum ShapeMapGroup
  {
    ShapeMapGroup_Nodes,     //!< nodes
    ShapeMapGroup_Meshes,    //!< meshes
    ShapeMapGroup_PrimArray, //!< primitive array
  };

  //! Bind name attribute.
  void bindNodeShape(TopoDS_Shape&                     theShape,
                     const TopLoc_Location&            theLoc,
                     const TCollection_AsciiString&    theNodeId,
                     const RWGltf_JsonValue*           theUserName,
                     const Handle(TDataStd_NamedData)& theExtras)
  {
    bindNamedShape(theShape, ShapeMapGroup_Nodes, theLoc, theNodeId, theUserName, theExtras);
  }

  //! Bind name attribute.
  void bindMeshShape(TopoDS_Shape&                     theShape,
                     const TCollection_AsciiString&    theMeshId,
                     const RWGltf_JsonValue*           theUserName,
                     const Handle(TDataStd_NamedData)& theExtras)
  {
    bindNamedShape(theShape,
                   ShapeMapGroup_Meshes,
                   TopLoc_Location(),
                   theMeshId,
                   theUserName,
                   theExtras);
  }

  //! Find named shape.
  bool findNodeShape(TopoDS_Shape& theShape, const TCollection_AsciiString& theNodeId) const
  {
    return findNamedShape(theShape, ShapeMapGroup_Nodes, theNodeId);
  }

  //! Find named shape.
  bool findMeshShape(TopoDS_Shape& theShape, const TCollection_AsciiString& theMeshId) const
  {
    return findNamedShape(theShape, ShapeMapGroup_Meshes, theMeshId);
  }

  //! Bind name attribute.
  Standard_EXPORT void bindNamedShape(TopoDS_Shape&                     theShape,
                                      ShapeMapGroup                     theGroup,
                                      const TopLoc_Location&            theLoc,
                                      const TCollection_AsciiString&    theId,
                                      const RWGltf_JsonValue*           theUserName,
                                      const Handle(TDataStd_NamedData)& theExtras);

  //! Find named shape.
  bool findNamedShape(TopoDS_Shape&                  theShape,
                      ShapeMapGroup                  theGroup,
                      const TCollection_AsciiString& theId) const
  {
    return myShapeMap[theGroup].Find(theId, theShape);
  }

  //! Return the string representation of the key.
  static TCollection_AsciiString getKeyString(const RWGltf_JsonValue& theValue)
  {
    if (theValue.IsString())
    {
      return TCollection_AsciiString(theValue.GetString());
    }
    else if (theValue.IsInt())
    {
      return TCollection_AsciiString(theValue.GetInt());
    }
    return TCollection_AsciiString();
  }

protected:
  //! Auxiliary structure for fast look-up of document sub-nodes of specified node.
  class GltfElementMap
  {
  public:
    //! Empty constructor.
    GltfElementMap()
        : myRoot(NULL)
    {
    }

    //! Return TRUE if this element is NULL.
    bool IsNull() const { return myRoot == NULL; }

    //! Access this node.
    const RWGltf_JsonValue* Root() const { return myRoot; }

    //! Find the child node with specified key.
    const RWGltf_JsonValue* FindChild(const TCollection_AsciiString& theKey)
    {
      const RWGltf_JsonValue* aNode = NULL;
      return myChildren.Find(theKey, aNode) ? aNode : NULL;
    }

    //! Find the child node with specified key.
    const RWGltf_JsonValue* FindChild(const RWGltf_JsonValue& theKey)
    {
      const TCollection_AsciiString aKey = getKeyString(theKey);
      if (aKey.IsEmpty())
      {
        return NULL;
      }

      const RWGltf_JsonValue* aNode = NULL;
      return myChildren.Find(aKey, aNode) ? aNode : NULL;
    }

    //! Initialize the element.
    void Init(const TCollection_AsciiString& theRootName, const RWGltf_JsonValue* theRoot);

  private:
    NCollection_DataMap<TCollection_AsciiString, const RWGltf_JsonValue*> myChildren;
    const RWGltf_JsonValue*                                               myRoot;
  };

private:
  //! Parse transformation matrix of the node.
  //! @param theSceneNodeId Name of the node. Used only for printing messages.
  //! @param theMatrixVal Json value containing transformation matrix.
  //! @param theResult TopLoc_Location object where result of parsing will be written.
  //! @param If true - parsing was successful, transformation is written into @p theResult.
  //!        If true - failed to parse, @p theResult is unchanged.
  bool parseTransformationMatrix(const TCollection_AsciiString& theSceneNodeId,
                                 const RWGltf_JsonValue&        theMatrixVal,
                                 TopLoc_Location&               theResult) const;

  //! Parse transformation components of the node.
  //! @param theSceneNodeId Name of the node. Used only for printing messages.
  //! @param theRotationVal Json value containing rotation component of transformation.
  //!        May be null in which case it is ignored.
  //! @param theScaleVal Json value containing scale component of transformation.
  //!        May be null in which case it is ignored.
  //! @param theTranslationVal Json value containing translation component of transformation.
  //!        May be null in which case it is ignored.
  //! @param theResult TopLoc_Location object where result of parsing will be written.
  //! @param If true - parsing was successful, transformation is written into @p theResult.
  //!        If true - failed to parse, @p theResult is unchanged.
  bool parseTransformationComponents(const TCollection_AsciiString& theSceneNodeId,
                                     const RWGltf_JsonValue*        theRotationVal,
                                     const RWGltf_JsonValue*        theScaleVal,
                                     const RWGltf_JsonValue*        theTranslationVal,
                                     TopLoc_Location&               theResult) const;
#endif
protected:
  //! Print message about invalid glTF syntax.
  void reportGltfSyntaxProblem(const TCollection_AsciiString& theMsg,
                               Message_Gravity                theGravity) const;

protected:
  TopTools_SequenceOfShape* myRootShapes; //!< sequence of result root shapes
  RWMesh_NodeAttributeMap*  myAttribMap;  //!< shape attributes
  NCollection_IndexedMap<TCollection_AsciiString>*
    myExternalFiles;                                //!< list of external file references
                                                    // clang-format off
  RWMesh_CoordinateSystemConverter myCSTrsf;        //!< transformation from glTF to OCCT coordinate system
                                                    // clang-format on
  TColStd_IndexedDataMapOfStringString* myMetadata; //!< file metadata

  NCollection_DataMap<TCollection_AsciiString, Handle(RWGltf_MaterialMetallicRoughness)>
                                                                              myMaterialsPbr;
  NCollection_DataMap<TCollection_AsciiString, Handle(RWGltf_MaterialCommon)> myMaterialsCommon;
  NCollection_DataMap<TCollection_AsciiString, Handle(XCAFDoc_VisMaterial)>   myMaterials;
  NCollection_DataMap<TCollection_AsciiString, TopoDS_Shape>                  myShapeMap[3];

  NCollection_DataMap<TCollection_AsciiString, bool>                       myProbedFiles;
  NCollection_DataMap<TCollection_AsciiString, Handle(NCollection_Buffer)> myDecodedBuffers;
  NCollection_Vector<TopoDS_Face> myFaceList; //!< face list for loading triangulation

  TCollection_AsciiString myFilePath;         //!< file path
  TCollection_AsciiString myFolder;           //!< folder
  TCollection_AsciiString myErrorPrefix;      //!< invalid syntax error prefix
  int64_t                 myBinBodyOffset;    //!< offset to binary body
  int64_t                 myBinBodyLen;       //!< binary body length
  bool                    myIsBinary;         //!< binary document
  bool                    myIsGltf1;          //!< obsolete glTF 1.0 version format
  bool                    myToSkipEmptyNodes; //!< ignore nodes without Geometry
  // clang-format off
  bool                      myToLoadAllScenes;  //!< flag to load all scenes in the document, FALSE by default
  bool                      myUseMeshNameAsFallback; //!< flag to use Mesh name in case if Node name is empty, TRUE by default
  bool                      myToProbeHeader;  //!< flag to probe header without full reading, FALSE by default
  bool                      myToReadAssetExtras; //!< flag to translate asset.extras into metadata, TRUE by default
  // clang-format on

#ifdef HAVE_RAPIDJSON
  GltfElementMap myGltfRoots[RWGltf_GltfRootElement_NB]; //!< glTF format root elements
#endif
};

#endif // _RWGltf_GltfJsonParser_HeaderFile
