#pragma once
#include "filewatcher.hh"
#include "utilities.hh"


/*= INPUT FILES ==============================================================*/

// Type of input chunk
enum class E_ShaderInputChunk {
	CODE ,
	INCLUDE ,
	UNIFORMS
};

// Input chunk data
struct T_ShaderInputChunk
{
	E_ShaderInputChunk type;
	T_String text;
	uint32_t lines;

	T_ShaderInputChunk( ) = default;
	T_ShaderInputChunk(
			const E_ShaderInputChunk type ,
			T_String text ,
			const uint32_t lines )
		: type( type ) , text( std::move( text ) ) , lines( lines )
	{ }
};

// Input file type
enum class E_ShaderInput {
	CHUNK ,			// Chunk that may be repeated
	LIBRARY ,		// Library (will only be loaded once)

	// "Main" shader source files
	VERTEX , FRAGMENT , GEOMETRY ,
	COMPUTE
};

// Preprocessing errors
struct T_ShaderInputError
{
	uint32_t line;
	T_String error;

	T_ShaderInputError(
			const uint32_t line ,
			T_String error )
		: line( line ) , error( std::move( error ) )
	{ }
};

// Uniform types
enum class E_UniformType {
	F1 , F2 , F3 , F4 ,
	I1 , I2 , I3 , I4 ,
	SAMPLER2D
};

// Uniform declarations
struct T_ShaderUniform
{
	T_String name;
	bool global;
	E_UniformType type;
};

// Source file
struct T_ShaderInput
{
	E_ShaderInput type = E_ShaderInput::CHUNK;
	T_Array< T_ShaderInputChunk > chunks;
	T_Array< T_ShaderInputError > errors;
	T_ObjectTable< T_String , T_ShaderUniform > uniforms{
		[]( T_ShaderUniform const& su ) -> T_String {
			return su.name;
		}
	};

	bool load( T_String const& path );
};
using P_ShaderInput = T_OwnPtr< T_ShaderInput >;


// Type of shader
enum class E_ShaderType {
	VERTEX , FRAGMENT , GEOMETRY ,
	COMPUTE ,
	__COUNT__
};


// Errors in shader code - the errors it represents may come from either
// the input loader, the shader loader or the driver.
struct T_ShaderError
{
	T_String source;
	uint32_t line;
	T_String error;

	T_ShaderError( ) = default;

	T_ShaderError( T_String source ,
			const uint32_t line ,
			T_String error )
		: source( std::move( source ) ) , line( line ) ,
			error( std::move( error ) )
	{ }

	T_ShaderError( T_String source ,
			const uint32_t line ,
			T_StringBuilder& error )
		: source( std::move( source ) ) , line( line ) ,
			error( std::move( error ) )
	{ }
};


struct T_ShaderManager;

struct T_ShaderPipeline
{
	friend struct T_ShaderManager;

	T_ShaderPipeline( );
	COPY( T_ShaderPipeline );
	MOVE( T_ShaderPipeline );
	~T_ShaderPipeline( );

	bool valid( ) const noexcept;
	void enable( ) const;
	GLuint id( ) const;

	GLuint program( const E_ShaderType program ) const;

    private:
	explicit T_ShaderPipeline( T_String id ) noexcept;
	T_String id_;
};


struct T_ShaderManager
{
	friend struct T_ShaderPipeline;

	struct T_ShaderCode
	{
		E_ShaderType type;
		T_AutoArray< uint32_t , 8 > starts;	// Position of chunk in source file
		T_AutoArray< uint32_t , 8 > counts;	// Chunk lengths
		T_AutoArray< T_String , 8 > sources;	// Chunk source files
		T_StringBuilder code;
		T_Array< T_ShaderError > errors;
		T_KeyValueTable< T_String , bool > files;
	};

	T_ShaderManager( ) noexcept;

	T_ShaderPipeline pipeline(
			std::initializer_list< T_String > shaders );

	void update( );

	void makeUI( );
	bool& uiEnabled( )
		{ return uiEnabled_; }

    private:
	struct T_Pipeline_
	{
		T_String idString;
		uint32_t references = 0;
		GLuint id = 0;
		T_Array< T_String > programs{ 8 };

		T_Pipeline_( T_String const& id ,
				std::initializer_list< T_String > shaders )
			: idString{ id } , programs{ shaders }
		{}
	};

	struct T_Program_
	{
		std::string name;
		T_Array< T_String > references;
		T_ShaderCode code;
		GLuint id;
		T_Optional< T_WatchedFiles > watch;
	};

	bool uiEnabled_ = false;

	T_ObjectTable< T_String , T_Pipeline_ > pipelines_;

	T_Array< T_Program_ > programs_;
	T_KeyValueTable< T_String , uint32_t > programIndex_;

	T_KeyValueTable< T_String , P_ShaderInput > inputs_;

	std::map< std::string , std::set< std::string > > missing_;
	std::set< std::string > updates_;

	uint32_t newProgramRecord( );

	void loadProgram(
			T_String const& pipeline ,
			std::string const& name );
	bool useExistingProgram(
			T_String const& pipeline ,
			std::string const& name );

	T_ShaderInput const* getInput(
			T_String const& name );

	void dereferencePipeline( T_String const& id );
	void dereferenceProgram(
			const uint32_t index ,
			T_String const& pipeline );

	void initPipeline( T_Pipeline_& pipeline ) const;
	void initProgram( T_Program_& program );

	void parseGLSLError(
			T_ShaderCode& code ,
			char const* errorLine );

	void programUpdated( std::string const& name );
	void resetProgram( T_Program_& program );
};