#pragma once

#include "AftrOpenGLIncludes.h"
#include "AftrGlobals.h"
#include "ModelMeshSkin.h"
#include "BoundingBox.h"
#include "AftrGeometryOBB.h"
#include "Vector.h"
#include "Mat4Fwd.h"
#include <vector>

namespace Aftr
{

class ModelMeshDataShared;
class WorldOctreeNode;
class Camera;
class ModelMeshLOD;

/**
   This class encapsulates a skin and rendering information specific to one mesh.
   The ModelMeshDataShared object is a pointer to the shared geometry information
   used by all identical meshes. The skin, however, is a copy of the original 
   skin loaded from file. The user can easily manipulate the skin to change the
   texture, shader, material properties, etc so this rendered mesh looks much
   different from the original even though it is using the originals geometry.

   After creating the ModelMesh, the user should manually invoke 
   ModelMesh::setParentModel( Model* parent ); so this mesh can access the
   parent Model's position/orientation/hierarchy information. This information
   is used by the culling system as well as methods such as getCurrentPosition(),
   etc.
*/
class ModelMesh
{
public:
   ModelMesh();
   ModelMesh( ModelMeshSkin&& skin, ModelMeshDataShared* meshData );
   ModelMesh( const ModelMesh& toCopy );
   virtual ~ModelMesh();
   ModelMesh& operator =( const ModelMesh& m );
   ModelMeshDataShared* getMeshDataShared() noexcept;
   const ModelMeshDataShared* getMeshDataShared() const noexcept;
   ModelMeshSkin& getSkin() noexcept;
   const ModelMeshSkin& getSkin() const noexcept;
   std::vector< ModelMeshSkin >& getSkins();
   void addSkin( ModelMeshSkin&& skin );
   void useNextSkin();
   void usePreviousSkin();
   void useSkinAtIndex( size_t skinIndex );
   size_t getSkinIndex() const noexcept { return this->skinIndex; }
   BoundingBox& getBoundingBox() { return this->bBox; }
   bool renderBBox = false;;
   virtual void render( const Mat4& modelMatrix, const Mat4& normalMatrix, const Camera& cam );
   virtual void renderSelection( const Mat4& modelMatrix, const Mat4& normalMatrix, const Camera& cam, GLubyte red, GLubyte green, GLubyte blue );
   void setParentModel( Model* parentModel );
   Model* getParentModel() const;
   WorldOctreeNode* getWorldOctreeNodeContainingThisMesh();
   void setWorldOctreeNodeContainingThisMesh( WorldOctreeNode* node );

   const std::string& getDescription() const noexcept { return this->description; }
   void setDescription( const std::string& description ) { this->description = description; }
   
   /**
      Returns the current global x,y,z of the center of this Mesh.
      Prereq: The Mesh's parent model must be set via ModelMesh::setParentModel().
   */
   Vector getCurrentPositionGlobal() const;

   /**
      Returns the current offset between this Mesh's center point and the
      parent model's center point.
      Prereq: The Mesh's parent model must be set via ModelMesh::setParentModel().
   */
   Vector getCurrentOffsetFromParentModel();
   
   /**
      Returns the original offset between this Mesh's center point and the
      Parent Model's center point. That is, this vector is the Mesh's center point
      minus the Parent Model's center point when the Parent Model's center point
      is at the origin with an identity orientation. Since each Mesh's orientation
      is defined by the Parent Model's orientation, a meshes current position is
      always equal to this offset vector transformed by the parent model's display
      matrix plus the parent model's current position.

      Prereq: The Mesh's parent model must be set via ModelMesh::setParentModel().
   */
   Vector getOriginalOffsetFromParentModel();

   /**
      This method sets this Mesh's offset from the center of the parent model. This method
      is typically only used when the mesh of a model has to be moved independently of the model.
      After this method has been invoked, the new global position of this mesh will take the
      parent model's position/orientation into account as well as this original vector transformed
      by the parent model's display matrix.
   */
   void setOriginalOffsetFromParentModel( const Vector& offset );

   /**
      Returns the current Oriented Bounding Box located at the mesh's current
      position and oriented with the mesh's current orientation.
   */
   AftrGeometryOBB getCurrentOBB() const;

   /**
      Using the position and orientation of the in passed cam, this method computes
      the screen space coordinate of this mesh's center point. The screen space (Window space)
      x, y values are placed in outWindowSpace. outWindowSpace.z is the depth into the screen
      at which the mesh's center point resides. outPerspectiveDivide is the w component used
      during the homogeneous transformation. This value is useful if one wants to transform 
      from screen space back into world space without relying a 16/24 bit depth buffer which
      introduces round off error.
   */
   void getScreenSpaceCoordinateOfCurrentPosition( Camera* cam, Vector& outWindowSpace, float& outPerspectiveDivideScalar );

   /**
      Given this mesh's current global position / orientation and the in passed 
      camera's current position / viewing properties, this method computes the
      lengths of a billboarded quad in world space. This billboarded quad is the proper 
      size for rending this mesh to an texture (from camera) and binding that 
      texture across the quad. The returned X is the billboards width, and the
      returned Y is the billboards height.

      This method is utilized by the LOD system.

      Returns the Lx and Ly lengths of the billboard's geometry when rendered
      from camera at this mesh's current global position. Lx and Ly are stored in
      the returned vector's .x and .y components, respectively. Lx and Ly are
      in world space.
   */
   Vector getBillboardLxLyInWorldSpace( Camera* cam, Vector* outBillboardCenterPosInWorldSpace = nullptr ) const;

   /**
      This method takes the current 8 vertices of the this mesh's current
      OBB, transforms them from world space to screen space, builds a tight axis-aligned 
      bounding box, and transforms these new 4 screen space verts back to world space.
      Then the x and y lengths are computed and returned. No z values are returned as
      x represents the billboard's width and y represents the billboard's height.

      Optionally, if the user would also like to retrive the w (perspective divide scalar) of
      this mesh's current centerposition (in world space), an out only point may be passed in.
   */
   Vector getBillboardLxLyInScreenSpace( Camera* cam, Vector* outMeshCenterPtInScreenSpace = nullptr, 
                                         float* outMeshCenterPtPerspectiveDivideScalar = nullptr,
                                         Vector* outCenterOfSSBBox = nullptr ) const;


   /**
      Adds the LOD representation specified by 'lod' into this Mesh's collection of available
      LODs. Returns the index into this Mesh's LOD list where the newly added LOD is stored.
      This index is used as input when invoking ModelMesh::setCurrentLOD().
      To actually use the LOD during rendering, the user must invoke 
      ModelMesh::startUsingCurrentLOD(). To stop using the current LOD, the user must invoke
      ModelMesh::stopUsingCurrentLOD().
   */
   size_t addLOD( ModelMeshLOD* lod );

   /**
      Returns true, if the in passed index is valid and this Mesh's current LOD is set to that
      index; false, otherwise.
   */
   bool setCurrentLODIndex( size_t index );

   /**
      Returns the index of the currently used LOD. In order for the LOD to affect rendering of
      this mesh, ModelMesh::startUsingCurrentLOD() must have been invoked. In order to stop the
      LOD rendering, call ModelMesh::stopUsingCurrentLOD()
   */
   size_t getCurrentLODIndex() const { return this->currentLODIndex; }

   bool isUsingCurrentLOD() const { return this->useLODAtRender; }
   bool isUsingCurrentLOD( bool startUsingLODAtMeshRender );

   /**
      Returns the currently used LOD, as determined by the currently set LOD index 
      (ModelMesh::setCurrentLOD( idx )).
   */
   const ModelMeshLOD* getCurrentLOD() const noexcept;
   ModelMeshLOD* getCurrentLOD() noexcept;

   /**
      Returns a reference to a std::vector containing all current ModelMeshLODs representing
      this mesh. Each LOD represents one specific LOD algorithm / representation; hence, there
      will typically be one or few entries in this vector.
   */
   std::vector< ModelMeshLOD* >& getLODs() { return this->lods; }

   void recomputeOffsetFromCenterOfParentModel();

   /**
      Modifies the underlying model mesh's modelMeshRenderData to re-sample the vertices
      currently only works for points, re-sampling techniques should be possible for other primitive shapes
      but may not make sense for all models (undersampling triangles will leave gaps in the model).
      Currently uses the currently selected skin's prim-type, render-type pair, could be extended to modify
      a specified pairing.
      Factor is the undersampling factor relative to the ORIGINAL data (easy enough for sequential data, may
      take more thought for other types, i.e. passing in 2 will give you half as many original points,
      followed by a subsuquent call of 3 will give you one third as many points, not one sixth.
   */
   void underSampleMeshWithFactor( int factor );
protected:

   void initMemberData();
   void computeOffsetFromCenterOfParentModel();///< only called in constructor, use "recompute" if you manually change the bounding box or something equivilent later

   std::vector< ModelMeshSkin > skins; //skins are only movable, not directly copy-able; however,
            //a ModelMesh ought to have a copy constructable and copy assignable. Therefore, this
            //class' copy construct/assign will invoke a special method to make clones of the copied
            //skins.

   size_t skinIndex = 0; ///< Current skin
   BoundingBox bBox; ///< BoundingBox of this Mesh's geometry

   std::vector< ModelMeshLOD* > lods; ///< Each entry represents one LOD algorithm representing this mesh
   size_t currentLODIndex = 0; ///< Specifies which LOD this mesh is set to currently use.
   bool useLODAtRender = false;; ///If true, this Mesh's render will use the currently selected LOD, instead of this Mesh's actual geometry/skins

   /**
      Vector originating at the parent model's center point (tail) and 
      extending to the center point of this mesh (head). This is initialized
      and maintained when ModelMesh::setParentModel(...) is invoked.
      Expressed in local body frame of the parent mesh
   */
   Vector offsetFromCenterOfParentModel;
   
   /**
      If the simulation is using the WorldOctree for rendering acceleration,
      this pointer points to the node within the octree that currently stores
      this mesh. This mesh will reside in at most one node of the octree at 
      any given time. This pointer is set and maintained by the WorldOctree
      instance accelerating the simulation's rendering. The user typically
      does not need to manipulate this. The pointed to node is not owned by
      this mesh, the mesh should not delete it.
   */
   WorldOctreeNode* octreeNodeContainingThisMesh = nullptr;

   /**
      Pointer to the Model that uses this ModelMesh. This pointer is
      valid only after the parent model completely exits its Model::New()
      method; as a result, this pointer is not valid during any initialization
      of the ModelMesh. This pointer is intended to be used by acceleration
      structures during rendering. This allows those acceleration structures
      to traverse both from a WO -> Model -> ModelMesh and 
      ModelMesh -> Model -> WO during runtime (not initialization).
   */
   Model* parentModel = nullptr;

   //Shared Data
   ModelMeshDataShared* meshData = nullptr; //pointer to the original meshData; not owned by this instance, do not delete

   //Optional textual description describing this mesh.
   //Convenient for setting color/material properties on a specific mesh loaded from a file.
   std::string description = "";
};

}

