#pragma once

#include "AftrGlobals.h"
#include "AftrOpenGLIncludes.h"
#include "ModelDataSharedID.h"
#include "Vector.h"
#include <string>
#include <map>
#include <unordered_map>
#include <future>

namespace Aftr
{

class ModelDataShared;

/**
   This class manages all multiplicities of all ModelDataShared objects that
   were loaded from a file format such as .3ds or .wrl.
*/

enum class RENDER_HW_MODE : unsigned char { rhmVA_VERTEX_ARRAY, rhmVBO_VERTEX_BUFFER_OBJECT, rhmNUM_RENDER_HW_MODE };
std::ostream& operator<<( std::ostream& out, const RENDER_HW_MODE& rhm );

class ManagerModelMultiplicity
{
public:

   ///The func_resume *will always occur* on the *main thread*, so it can mutate OpenGL!
   ///This is the lower-level function that sends VBO data to GPU memory.
   using func_send_to_GPU_on_main_thread = std::function<ModelDataSharedID()>;

   ///The func_resume *will always occur* on the *main thread*, so it can mutate OpenGL!
   ///This stores the user's lambda which may access the internals of the MDS to set
   ///things like skin or shader values after the MDS has been sent to the GPU and is
   ///fully loaded.
   using func_resume_Model_onCreate_after_MDS_completed = std::function<void()>;

   static void init();
   static void shutdown();
   /**
      Loads and manages all multiplicities of a model's geometry, textures, colors, material
      properties, shaders, etc. The in passed modelData is overwritten by this method. When
      this method returns, modelData points to the valid ModelDataShared object populated
      from the corresponding file.
   */
   static void loadModelDataShared( ModelDataShared** modelData, const std::string& modelFileName, 
                                    const Vector& scale, const MESH_SHADING_TYPE& shadingType );

   /**
      Same as loadModelDataShared but runs asynchronously. If this is a unique filename / scale pair, the
      filename will be loaded and parsed asychronously via the parser, such as ASSIMP. Once the ModelDataShared
      has been parsed and loaded into CPU-side memory, a callable continuation will be scheduled on the main 
      thread (which owns the OpenGL context). The next time the GLView::updateWorld() is called within the main
      thread, the CPU-side data will be streamed to OpenGL (this take significantly less time than the parsing
      and texture loading. Any subsequent loads to the same filename / scale will return immediately with the
      shared ModelDataShared instance already shared across AftrBurner.

      Using this will reduce loadMap times but will cause WOs to stream into existance *after* the 3DVW begins
      rendering frames, so the user may need to ensure the model data is only accessed after it is loaded.
   */
   static void loadModelDataSharedAsync( ModelDataShared** modelData, const std::string& modelFileName,
                                         const Vector& scale, const MESH_SHADING_TYPE& shadingType,
                                         func_resume_Model_onCreate_after_MDS_completed func );
   
   /// This method can only be called from the *MAIN THREAD* or else OpenGL will enter into undefined behavior.
   /// When async loading 3D models from a file, worker threads are dispatched to parse the 3D model, build the
   /// OpenGL indexed geometry structures, and prepare them to be sent to the GPU. However, OpenGL function
   /// calls may only be called from the main thread which created the OpenGL context; ie, the MAIN THREAD.
   /// As a result, the main loop (on the main thread) may call this function when the user wants to
   /// perform the final step of loading a 3D model from file. If any worker threads have finished parsing
   /// and building the data structures, then this method simply sends those data structures to OpenGL.
   /// Internally, this method calls on completion handlers for each model. This includes the 
   /// func_resume_Model_onCreate_after_MDS_completed which correspond to the lambdas enqueued via
   ///  Model::upon_async_model_loaded( ...completion_handler_func...).
   /// This is how one ensures each Model's upon_async_model_loaded(...) is *always* run on the main
   /// thread.
   static int loadPendingAsyncModelDataSharedInstances();
   
   ///This method is called automatically within a ModelDataShared destructor. Internally, this calls
   /// destroyModelSahredData(...) to free the memory of the 
   static void unRegisterOneModelDataSharedInstance( const ModelDataSharedID& id );

   /// This method is used by unRegisterOneModelDataSharedInstance
   static void destroyModelSharedData( ModelDataShared* s );

   static RENDER_HW_MODE getRenderMode() { return hardwareRenderMode; }
   static void sendModelDataSharedServerSide( ModelDataShared* m );
   
   /// Queries if the in passed ModelDataShared is fully initialized on the GPU and 
   /// safe to use. For example, if this was a request to load a duplicate MDS, this method
   /// would return true iff the main thread had already initialized this MDS on the GPU.
   /// If so, this method returns true and the completion handler lamdas for this MDS copy
   /// could be safely called. If false is returned, the async parser may still be reading
   /// from file, so dispatching any lambdas on the MDS's completion handlers would result
   /// in undefined behavior.
   /// This method MUST ONLY BE CALLED FROM THE MAIN THREAD; otherwise OpenGL may enter
   /// into undefined behavior! This method is intended to be called in 
   /// ManagerModelMultiplicity::sendPendingAsyncModelDataSharedToGfxMemory()
   static bool has_sent_ModelDataShared_to_ServerSide( ModelDataShared const* mds );

   /// Typically called by an MGL that has begun to asychronously create its own ModelDataShared via a
   /// call similar to:
   /// std::future< ManagerModelMultiplicity::func_send_to_GPU_on_main_thread > fut = std::async( std::launch::async,
   ///                                     do_async_terrain_loading_on_worker_thread, std::move( resumeOnCreateFunc ) );
   /// This completes everything except actually sending the ModelDataShared to the GPU, which must be done on the main
   /// thread which created the OpenGL context.
   /// This method consumes the pending future and enqueues it until it is ready. Upon becoming ready, the callable
   /// inside the future is invoked on the main thread to transfer the data to the GPU.
   static void async_sendModelDataSharedServerSide( std::future<ManagerModelMultiplicity::func_send_to_GPU_on_main_thread>&& fut );
protected:

   static ModelDataShared* getModelDataSharedFromFile( const std::string& modelFileName,
                               const Vector& scale, const MESH_SHADING_TYPE& shadingType );

   static func_send_to_GPU_on_main_thread
          getModelDataSharedFromFileAsync( ModelDataShared** modelData, const std::string modelFileName,
               const Vector scale, const MESH_SHADING_TYPE shadingType,
               std::map< ModelDataSharedID, ModelDataShared* >::iterator iter,
               ManagerModelMultiplicity::func_resume_Model_onCreate_after_MDS_completed resumeOnCreateFunc );

   /// After the first unique instance of a ModelDataShared has been fully uploaded to the
   /// GPU and is rendering normally, any subsequent requests to load another copy of that 
   /// MDS instance cannot "hook" into the notification that occurs when the first instance completes (because that has already happened).
   /// MUTATES only ModelManagerMultiplicit::duplicate_MDS_queued via a calls to
   /// complete_construction_of_duplicate_MDS_now_that_async_MDS_has_loaded().
   static int loadPendingAsyncModelDataSharedInstances_after_first_instance_has_completed_notification_callbacks();

   /// Invokes callbacks if the corresponding MDS of the in passed ID is fully ready on the GPU/OpenGL-side.
   /// After invoking all callbacks, the in passed ID and all awaiting functors are removed from 
   /// duplicate_MDS_queue since they have been fully processed. This means a call to this method
   /// MAY invalid any iterators to duplicate_MDS_queue if it returns a non-zero value indicating some MDSes
   /// were created / processed to completion.
   static int complete_construction_of_duplicate_MDS_now_that_async_MDS_has_loaded( const ModelDataSharedID& id_of_ready_MDS );

   static RENDER_HW_MODE hardwareRenderMode; ///< If VBOs are supported, they are used; otherwise, VAs are used.
   static std::map< ModelDataSharedID, ModelDataShared* > models;
   //These callables are guaranteed to execute on the main thread which created the OpenGL context.
   //They are called at the bottom of GLView::updateWorld(...) via 
   //a call to ManagerModelMultiplicity::sendPendingAsyncModelDataSharedToGfxMemory();
   static std::vector< std::future< func_send_to_GPU_on_main_thread > > parsed_MDS_ready_to_send_to_GPU_via_func;
   static std::mutex parsed_MDS_ready_to_send_to_GPU_via_func_MUTEX;

   //For duplicate models being loaded while an async thread is concurrently creating the 3D ModelDataShared,
   //we enqueue the ModelDataSharedID and its corresponding completion lambda. Once the async thread completes
   //loading and the main OpenGL thread has sent the data to the GPU, we then do the following:
   //All duplicate models that have a matching ModelDataSharedID (but are not yet loaded) will have their
   //corresponding lambdas invoked from this container. The lambda will create a new ModelDataShared with 
   //modifiable ModelMeshes / Skins -- exactly providing the behavior expected from a ModelDataShared.
   //By only touching this on the main thread, no mutual exclusion is necessary.
   static std::unordered_multimap< ModelDataSharedID, func_resume_Model_onCreate_after_MDS_completed > duplicate_MDS_queued;
};
} //namespace Aftr

