#pragma once

#include "AftrGlobals.h"
#include "AftrOpenGLIncludes.h"
#include "Mat4Fwd.h"
#include "Tex.h"
#include <vector>
#include <string>
#include <tuple>
#include <iosfwd>

namespace Aftr
{

class GLSLShader;
class ModelMesh;
class Camera;

/**
   This class encapsulates a "skin" that is applied on the surface of 
   a mesh when it is rendered. The skin is composed of 3 main parts:
   -# A set of textures that are applied in parallel (multi-texture)
   -# A GLSLShader to perform OpenGL Vertex and Fragment shader operations
   -# OpenGL Color/Material properties (ambient, diffuse, specular, color RGBA)
   
   When rendering a mesh, that mesh's corresponding skin is "bound", then
   the mesh's geometry is rendered, then the skin may be "unbound". At most,
   one skin can be bound at any time.

   If the OpenGL Context is 3.2+ Core, then the Default Shader has uniforms to store the
   skin's Ambient, Diffuse, and Specular data. The Shader is the fully responsible for performing
   the desired lighting. Depricated and options like enabling/disabling USE_COLOR_MATERIAL no 
   longer exist and are not called.

   When using the default 3.2+ shaders (such as GLSLShaderDefaultGL32):
   
   TRANSPARENCY is determined by:
   float alpha = min( ModelMeshSkin::ambient.a, ModelMeshSkin::multiTextures[usedTex].a ).
   That is, the alpha is set by either the ambients alpha channel or the used texture's alpha channel,
   which ever is smaller. If this will not work for an object, an alternative shader may be bound that
   performs the desired transparency.
   
   --------------------------------
   OLD SCHOOL Fixed function 2.1+ Compatibility mode:

   When NOT using default 3.2+ shaders and relying on old "fixed functionality":

   A brief note on OpenGL and Materials vs Colors and USE_COLOR_MATERIAL and 
   how this skin implementation works with them:

   If USE_COLOR_MATERIAL is enabled (and OpenGL Lighting is enabled),
   then the OpenGL color state is used to approximate the 
   material properties. Materials are always used if OpenGL Lighting
   is enabled; therefore, one must either use the material properties 
   (ambient, diffuse, specular), or use the colors (r,g,b,a) to approximate
   the material properties. If using colors to approximate materials, then
   each vertex's color data can be used. If each vertex contains no color
   data, then the single color (this->color[4]) is used to approximate the
   material across all verticies of this mesh.

   If USE_COLOR_MATERIAL is disabled and OpenGL Lighting is enabled, 
   then this->ambient[4], this->diffuse[4], and this->specular[4]
   are used as the materials; OpenGL color state has no effect.

   If USE_COLOR_MATERIAL is disabled and OpenGL Lighting is disabled, 
   each vertex's color data is used. If each vertex contains no color data,
   then this->color[4] is used as the color (materials are disabled and have
   no effect).

   If lighting is disabled, then OpenGL Material properties cannot be used; instead
   OpenGL Colors must be used. If each vertex has color data, then that color will
   be used. If each vertex contains no color data, then this->color[4] will be used
   across all meshes to set the color (Materials are disabled and have no effect).
*/
class ModelMeshSkin
{
public:
   ModelMeshSkin();
   ModelMeshSkin( ModelMeshSkin&& toMove ) noexcept;
   ModelMeshSkin( const ModelMeshSkin& toCopy ) = delete;
   ModelMeshSkin( Tex const& tex );
   ModelMeshSkin( const std::vector< Tex >& textures );
   ModelMeshSkin( GLSLShader* shader );
   ModelMeshSkin( Tex const& tex, GLSLShader* shader );
   virtual ~ModelMeshSkin();

   ModelMeshSkin& operator =( ModelMeshSkin&& moveSkin ) noexcept;
   ModelMeshSkin& operator =( const ModelMeshSkin& skin ) = delete;
   
   //The ModelMesh contains a std::vector of ModelMeshSkins, and the ModelMesh must by
   //copy construct/assignable. Since the ModelMeshSkin deletes those, I've added a special
   //clone() method that will return a prefect copy. However, be aware, two textures may own
   //the same handle to the same GLTex and their lifetime may delete that texture. This is *not*
   //a problem if the texture was created as a sharedTexOwnsHandle or SharedTexSharedHandle, but
   //will be if the texture was created as a ownsTexOwnsHandle.
   //It is expected that the user knowns what this means if they call this method -- which is
   //why the default copy construct/assign are deleted, so one doesn't accidently call this.
   ModelMeshSkin clone() const;

   /**
      This method binds the current skin. All geometry rendered while this
      skin is bound will utilize this skins' multiTexture set, shader, and
      material properties.

      When using the default 3.2+ shaders (such as GLSLShaderDefaultGL32, transparency is determined by:
      float alpha = min( ModelMeshSkin::ambient.a, ModelMeshSkin::multiTextures[usedTex].a ).
      That is, the alpha is set by either the ambient's alpha channel or the used texture's alpha channel,
      which ever is smaller.

      The optional tuple contains common/useful data for GLSLShader uniforms, params [0,2]:
                       const Mat4& modelMatrix, const Mat4& normalMatrix, const Camera& cam
      That will be passed into the shader's bind method. If these parameters are not supplied and a GL 3.2+ 
      context without any fixed functionality pipeline is being used, then the user is responsible for updating
      the shader values via their own mechanism.
   */
   virtual void bind( const std::tuple< const Mat4&, const Mat4&, const Camera& >* const shaderParams = nullptr ) const;

   /**
      This method unbinds the current skin. This consists of deactivating
      all but the 0th texture, unbinding the currently bound texture,
      and unbinding the current shader.
   */
   virtual void unbind() const;

   std::vector< Tex >& getMultiTextureSet();
   const std::vector< Tex >& getMultiTextureSet() const;
   GLSLShader* getShader() const;
   
   //Same as getShader(), but is templated so the user does not need to perform
   //a static_cast<T> on the return type. This is simply a convenience method.
   template< typename T, typename R = std::remove_pointer_t<T> > R* getShaderT()
   {
      if( this->shader == nullptr )
      {
         std::cout << "ERROR: " << AFTR_FILE_LINE_STR << ": Shader is nullptr... Is this a valid object? Goodbye...\n";
         std::abort();
      }
      if constexpr( std::is_pointer_v<T> )
         return static_cast<R*>(this->shader);//Lets skin->getShaderT<GLSLShaderPointTesselatorBillboard*>() work as expected.
      return static_cast<R*>(this->shader);   //Lets wo->getShaderT<GLSLShaderPointTesselatorBillboard>() work as expected.
   }

   /// Deletes and replaces any existing shader already associated with this ModelMeshSkin.
   void setShader( GLSLShader* shader_owned_by_this_instance );

   MESH_SHADING_TYPE getMeshShadingType() const { return this->shadingType; } 
   void setMeshShadingType(MESH_SHADING_TYPE shadingType) { this->shadingType = shadingType; }
   GLenum getGLPrimType() const { return this->glPrimType; }
   void setGLPrimType(GLenum type) { this->glPrimType = type; }
   MESH_RENDER_TYPE getMeshRenderType() const { return this->renderType; }
   void setMeshRenderType( MESH_RENDER_TYPE type );
   
   std::string toString() const;

   const std::string& getName() { return this->name; }
   void setName( const std::string& name ) { this->name = name; }

   ///Transparency is determined by the min value of the alpha component of the ambient or the bound texture's alpha value.
   void setAmbient( const aftrColor4f& ambient )   { this->ambient = ambient; }
   void setDiffuse( const aftrColor4f& diffuse )   { this->diffuse = diffuse; }
   void setSpecular( const aftrColor4f& specular ) { this->specular = specular; }
   void setColor4f( const aftrColor4f& color )     { this->color = color; }
   void setColor4ub( const aftrColor4ub& color )   { this->color = aftrColor4f( (GLubyte)color.r, (GLubyte)color.g, (GLubyte)color.b, ( GLubyte )color.a ); }
   void setSpecularCoefficient( GLfloat exponent ){ this->specularCoefficient = exponent; }
   /// Each int must be in the range [0,255]
   void setColori( int r, int g, int b, int a );
   /// Each float must be in the range [0, 1]
   void setColorf( float r, float g, float b, float a );
   void setColor( const aftrColor4ub& c );
   /// Returns ambient colors [0,3]: r,g,b,a, respectively.
   aftrColor4f const& getAmbient() const noexcept { return ambient; }
   aftrColor4f& getAmbient() noexcept { return ambient; }
   /// Returns diffuse colors [0,3]: r,g,b,a, respectively.
   aftrColor4f const& getDiffuse() const noexcept { return diffuse; }
   aftrColor4f& getDiffuse() noexcept { return diffuse; }
   /// Returns specular [0,3]: r,g,b,a, respectively.
   aftrColor4f const& getSpecular() const noexcept { return specular; }
   aftrColor4f& getSpecular() noexcept { return specular; }
   /// Exponent on the specular highlighting equation. See documentation for this variable in this class.
   GLfloat getSpecularCoefficient() const noexcept { return this->specularCoefficient; }
   /// Returns colors [0,3]: r,g,b,a, respectively.
   const aftrColor4f& getColor() const noexcept { return this->color; }
   aftrColor4f& getColor() noexcept { return this->color; }
   
   void isUsingColorMaterial( bool x ) { useColorMaterial = x; }
   bool isUsingColorMaterial() const { return useColorMaterial; }

   void isRenderingNormals( bool x ) { renderNormals = x; }
   bool isRenderingNormals() const { return renderNormals; }

   void setNormalsScale( float x ) { normalsScale = x; normalsScaleModified = true; }
   float getNormalsScale() { return normalsScale; }
   bool isNormalScaleModified() { return normalsScaleModified; }
   void isNormalScaleModified( bool x ) { normalsScaleModified = x; }

   bool isVisible() const { return visible; }
   void isVisible( bool visible ) { this->visible = visible; }

   void setGLLineWidth( float glLineWidthThickness ) { this->glLineWidthThickness = glLineWidthThickness; }
   float getGLLineWidth() { return this->glLineWidthThickness; }

   void setNormalLineWidth( float normalLineWidthThickness ) { this->normalLineWidthThickness = normalLineWidthThickness; }
   float getNormalLineWidth() { return this->normalLineWidthThickness; }

protected:

   void initMemberData();

   std::vector< Tex > multiTextures; ///< Owned by this instance, delete upon destruction
   GLSLShader* shader = nullptr; ///< Owned by this instance, delete upon destruction
   
   std::string name = ""; //Optional name or description to help identify this skin/material information. Ex: "glass windshield".

   aftrColor4f ambient; ///< The ambient material property of this mesh. Used only if using default 3.2+ shader or useColorMaterial is false.
   aftrColor4f diffuse; ///< The diffuse material property of this mesh. Used only if using default 3.2+ shader or useColorMaterial is false.
   aftrColor4f specular; ///< The specular material property of this mesh. Used only if using default 3.2+ shader or useColorMaterial is false.
   
   /**
      Ranges from 0 to 1000+, this is the specular exponent used in shininess. The smaller the
      value, the larger the shiny spot (and less intense). The larger the value, the smaller
      the shiny spot (but more intense).
   */
   GLfloat specularCoefficient;
   
   /**
      r,g,b,a. Used in glColor(...) when rendered iff, this->useColorMaterial is true and the 
      corresponding Mesh has no color data in its vertex list.
      This is NOT used if the default GLSLShaderDefaultGL32 is bound.
   */
   aftrColor4f color;

   /**
      If true (and OpenGL Lighting is enabled),
      then the OpenGL color state is used to approximate the 
      material properties. Materials are always used if OpenGL Lighting
      is enabled; therefore, one must either use the material properties 
      (ambient, diffuse, specular), or use the colors (r,g,b,a) to approximate
      the material properties. If using colors to approximate materials, then
      each vertex's color data can be used. If each vertex contains no color
      data, then the single color (this->color[4]) is used to approximate the
      material across all verticies of this mesh.

      If this is false and OpenGL Lighting is enabled, then this->ambient[4],
      this->diffuse[4], and this->specular[4] are used as the materials.

      If this is false and OpenGL Lighting is disabled, each vertex's color data
      is used or this->color[4] is used as the color (materials are disabled).

      If lighting is disabled, then OpenGL Material properties cannot be used; instead
      OpenGL Colors must be used.
   */
   bool useColorMaterial;
   
   /**
      Determines if this mesh should be rendered using Smooth Shading (mstShared)
      (one normal per vertex) or Flat Shading (MESH_SHADING_TYPE::mstFLAT) (one normal per face).
      This value defaults to MESH_SHADING_TYPE::mstNOTINITIALIZED and must be set prior to actually
      using this skin (typically this is set by the parser reading in a model from
      file.
   */
   MESH_SHADING_TYPE shadingType;

   /**
      Determines the type of primitive this skin tries to render.  If the associated
      shading type-primitive type render data has not been loaded, it will be the first
      time this skin is rendered.

      This value defaults to GL_TRIANGLES.
      Valid values include GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, 
      GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS, and GL_POLYGON 
   */
   GLenum glPrimType;

   /**
      Specifies the rasterized width of both aliased and antialiased lines. Using a line
      width other than 1 has different effects, depending on whether line antialiasing is
      enabled. To enable and disable line antialiasing, call glEnable and glDisable with 
      argument GL_LINE_SMOOTH. Line antialiasing is initially disabled.

      This is only used if glPrimType is set to GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP,
      GL_LINE_STRIP_ADJACENCY, or GL_LINES_ADJACENCY

      If line antialiasing is disabled, the actual width is determined by rounding the
      supplied width to the nearest integer. (If the rounding results in the value 0,
      it is as if the line width were 1.) If dx >= dy, i pixels are filled in each column
      that is rasterized, where i is the rounded value of width. Otherwise, i pixels are
      filled in each row that is rasterized.
   */
   float glLineWidthThickness;

   /**
      The mesh hardware render type for this skin.
      If VA will stream to server every render.
      If VBO will live on server if room.
      If VA_AND_VBO will use VBO when available, but will keep copy of VA data on host.
   */
   MESH_RENDER_TYPE renderType;

   bool renderNormals;

   float normalsScale; ///< Defines length of normals. Default length is 1. This value scales the length.
   float normalLineWidthThickness; ///< Defines the glLineWidth() of the Normal vectors of the associated mesh
   bool normalsScaleModified; ///< After changing the normal scale, this flag is set to let the ModelMesh
                              ///< ModelMeshDataShared know that it needs to recompute its normal lengths
                              ///< so they can subsequently be rendered to the length of normal scale.
                              ///< After regeneration of normals, this flag is automatically set back
                              ///< to false.

   bool visible;
};

}

