// Copyright (C) 2006-2012 Nikolaus Gebhardt / Thomas Alten
// This file is part of the "Irrlicht Engine".
// For conditions of distribution and use, see copyright notice in irrlicht.h

#ifndef IRR_I_Q3_LEVEL_SHADER_H_INCLUDED
#define IRR_I_Q3_LEVEL_SHADER_H_INCLUDED

#include "irrArray.h"
#include "fast_atof.h"
#include "IFileSystem.h"
#include "IVideoDriver.h"
#include "coreutil.h"

namespace irr
{
namespace scene
{
namespace quake3
{

	static core::stringc irrEmptyStringc("");

	//! Hold the different Mesh Types used for getMesh
	enum eQ3MeshIndex
	{
		E_Q3_MESH_GEOMETRY = 0,
		E_Q3_MESH_ITEMS,
		E_Q3_MESH_BILLBOARD,
		E_Q3_MESH_FOG,
		E_Q3_MESH_UNRESOLVED,
		E_Q3_MESH_SIZE
	};

	/*! used to customize Quake3 BSP Loader
	*/

	struct Q3LevelLoadParameter
	{
		Q3LevelLoadParameter ()
			:defaultLightMapMaterial ( video::EMT_LIGHTMAP_M4 ),
			defaultModulate ( video::EMFN_MODULATE_4X ),
			defaultFilter ( video::EMF_BILINEAR_FILTER ),
			patchTesselation ( 8 ),
			verbose ( 0 ),
			startTime ( 0 ), endTime ( 0 ),
			mergeShaderBuffer ( 1 ),
			cleanUnResolvedMeshes ( 1 ),
			loadAllShaders ( 0 ),
			loadSkyShader ( 0 ),
			alpharef ( 1 ),
			swapLump ( 0 ),
	#ifdef __BIG_ENDIAN__
			swapHeader ( 1 )
	#else
			swapHeader ( 0 )
	#endif
			{
				memcpy ( scriptDir, "scripts\x0", 8 );
			}

		video::E_MATERIAL_TYPE defaultLightMapMaterial;
		video::E_MODULATE_FUNC defaultModulate;
		video::E_MATERIAL_FLAG defaultFilter;
		s32 patchTesselation;
		s32 verbose;
		u32 startTime;
		u32 endTime;
		s32 mergeShaderBuffer;
		s32 cleanUnResolvedMeshes;
		s32 loadAllShaders;
		s32 loadSkyShader;
		s32 alpharef;
		s32 swapLump;
		s32 swapHeader;
		c8 scriptDir [ 64 ];
	};

	// some useful typedefs
	typedef core::array< core::stringc > tStringList;
	typedef core::array< video::ITexture* > tTexArray;

	// string helper.. TODO: move to generic files
	inline s16 isEqual ( const core::stringc &string, u32 &pos, const c8 * const list[], u16 listSize )
	{
		const char * in = string.c_str () + pos;

		for ( u16 i = 0; i != listSize; ++i )
		{
			if (string.size() < pos)
				return -2;
			u32 len = (u32) strlen ( list[i] );
			if (string.size() < pos+len)
				continue;
			if ( in [len] != 0 && in [len] != ' ' )
				continue;
			if ( strncmp ( in, list[i], len ) )
				continue;

			pos += len + 1;
			return (s16) i;
		}
		return -2;
	}

	inline f32 getAsFloat ( const core::stringc &string, u32 &pos )
	{
		const char * in = string.c_str () + pos;

		f32 value = 0.f;
		pos += (u32) ( core::fast_atof_move ( in, value ) - in ) + 1;
		return value;
	}

	//! get a quake3 vector translated to irrlicht position (x,-z,y )
	inline core::vector3df getAsVector3df ( const core::stringc &string, u32 &pos )
	{
		core::vector3df v;

		v.X = getAsFloat ( string, pos );
		v.Z = getAsFloat ( string, pos );
		v.Y = getAsFloat ( string, pos );

		return v;
	}


	/*
		extract substrings
	*/
	inline void getAsStringList ( tStringList &list, s32 max, const core::stringc &string, u32 &startPos )
	{
		list.clear ();

		s32 finish = 0;
		s32 endPos;
		do
		{
			endPos = string.findNext ( ' ', startPos );
			if ( endPos == -1 )
			{
				finish = 1;
				endPos = string.size();
			}

			list.push_back ( string.subString ( startPos, endPos - startPos ) );
			startPos = endPos + 1;

			if ( list.size() >= (u32) max )
				finish = 1;

		} while ( !finish );

	}

	//! A blend function for a q3 shader.
	struct SBlendFunc
	{
		SBlendFunc ( video::E_MODULATE_FUNC mod )
			: type ( video::EMT_SOLID ), modulate ( mod ),
				param0( 0.f ),
			isTransparent ( 0 ) {}

		video::E_MATERIAL_TYPE type;
		video::E_MODULATE_FUNC modulate;

		f32 param0;
		u32 isTransparent;
	};

	// parses the content of Variable cull
	inline bool getCullingFunction ( const core::stringc &cull )
	{
		if ( cull.size() == 0 )
			return true;

		bool ret = true;
		static const c8 * funclist[] = { "none", "disable", "twosided" };

		u32 pos = 0;
		switch ( isEqual ( cull, pos, funclist, 3 ) )
		{
			case 0:
			case 1:
			case 2:
				ret = false;
				break;
		}
		return ret;
	}

	// parses the content of Variable depthfunc
	// return a z-test
	inline u8 getDepthFunction ( const core::stringc &string )
	{
		u8 ret = video::ECFN_LESSEQUAL;

		if ( string.size() == 0 )
			return ret;

		static const c8 * funclist[] = { "lequal","equal" };

		u32 pos = 0;
		switch ( isEqual ( string, pos, funclist, 2 ) )
		{
			case 0:
				ret = video::ECFN_LESSEQUAL;
				break;
			case 1:
				ret = video::ECFN_EQUAL;
				break;
		}
		return ret;
	}


	/*!
		parses the content of Variable blendfunc,alphafunc
		it also make a hint for rendering as transparent or solid node.

		we assume a typical quake scene would look like this..
		1) Big Static Mesh ( solid )
		2) static scene item ( may use transparency ) but rendered in the solid pass
		3) additional transparency item in the transparent pass

		it's not 100% accurate! it just empirical..
	*/
	inline static void getBlendFunc ( const core::stringc &string, SBlendFunc &blendfunc )
	{
		if ( string.size() == 0 )
			return;

		// maps to E_BLEND_FACTOR
		static const c8 * funclist[] =
		{
			"gl_zero",
			"gl_one",
			"gl_dst_color",
			"gl_one_minus_dst_color",
			"gl_src_color",
			"gl_one_minus_src_color",
			"gl_src_alpha",
			"gl_one_minus_src_alpha",
			"gl_dst_alpha",
			"gl_one_minus_dst_alpha",
			"gl_src_alpha_sat",

			"add",
			"filter",
			"blend",

			"ge128",
			"gt0",
		};


		u32 pos = 0;
		s32 srcFact = isEqual ( string, pos, funclist, 16 );

		if ( srcFact < 0 )
			return;

		u32 resolved = 0;
		s32 dstFact = isEqual ( string, pos, funclist, 16 );

		switch ( srcFact )
		{
			case video::EBF_ZERO:
				switch ( dstFact )
				{
					// gl_zero gl_src_color == gl_dst_color gl_zero
					case video::EBF_SRC_COLOR:
						blendfunc.type = video::EMT_ONETEXTURE_BLEND;
						blendfunc.param0 = video::pack_textureBlendFunc ( video::EBF_DST_COLOR, video::EBF_ZERO, blendfunc.modulate );
						blendfunc.isTransparent = 1;
						resolved = 1;
						break;
				} break;

			case video::EBF_ONE:
				switch ( dstFact )
				{
					// gl_one gl_zero
					case video::EBF_ZERO:
						blendfunc.type = video::EMT_SOLID;
						blendfunc.isTransparent = 0;
						resolved = 1;
						break;

					// gl_one gl_one
					case video::EBF_ONE:
						blendfunc.type = video::EMT_TRANSPARENT_ADD_COLOR;
						blendfunc.isTransparent = 1;
						resolved = 1;
						break;
				} break;

			case video::EBF_SRC_ALPHA:
				switch ( dstFact )
				{
					// gl_src_alpha gl_one_minus_src_alpha
					case video::EBF_ONE_MINUS_SRC_ALPHA:
						blendfunc.type = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
						blendfunc.param0 = 1.f/255.f;
						blendfunc.isTransparent = 1;
						resolved = 1;
						break;
				} break;

			case 11:
				// add
				blendfunc.type = video::EMT_TRANSPARENT_ADD_COLOR;
				blendfunc.isTransparent = 1;
				resolved = 1;
				break;
			case 12:
				// filter = gl_dst_color gl_zero or gl_zero gl_src_color
				blendfunc.type = video::EMT_ONETEXTURE_BLEND;
				blendfunc.param0 = video::pack_textureBlendFunc ( video::EBF_DST_COLOR, video::EBF_ZERO, blendfunc.modulate );
				blendfunc.isTransparent = 1;
				resolved = 1;
				break;
			case 13:
				// blend = gl_src_alpha gl_one_minus_src_alpha
				blendfunc.type = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
				blendfunc.param0 = 1.f/255.f;
				blendfunc.isTransparent = 1;
				resolved = 1;
				break;
			case 14:
				// alphafunc ge128
				blendfunc.type = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
				blendfunc.param0 = 0.5f;
				blendfunc.isTransparent = 1;
				resolved = 1;
				break;
			case 15:
				// alphafunc gt0
				blendfunc.type = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
				blendfunc.param0 = 1.f / 255.f;
				blendfunc.isTransparent = 1;
				resolved = 1;
				break;

		}

		// use the generic blender
		if ( 0 == resolved )
		{
			blendfunc.type = video::EMT_ONETEXTURE_BLEND;
			blendfunc.param0 = video::pack_textureBlendFunc (
					(video::E_BLEND_FACTOR) srcFact,
					(video::E_BLEND_FACTOR) dstFact,
					blendfunc.modulate);

			blendfunc.isTransparent = 1;
		}
	}

	// random noise [-1;1]
	struct Noiser
	{
		static f32 get ()
		{
			static u32 RandomSeed = 0x69666966;
			RandomSeed = (RandomSeed * 3631 + 1);

			f32 value = ( (f32) (RandomSeed & 0x7FFF ) * (1.0f / (f32)(0x7FFF >> 1) ) ) - 1.f;
			return value;
		}
	};

	enum eQ3ModifierFunction
	{
		TCMOD			= 0,
		DEFORMVERTEXES		= 1,
		RGBGEN			= 2,
		TCGEN			= 3,
		MAP			= 4,
		ALPHAGEN		= 5,

		FUNCTION2		= 0x10,
		SCROLL			= FUNCTION2 + 1,
		SCALE			= FUNCTION2 + 2,
		ROTATE			= FUNCTION2 + 3,
		STRETCH			= FUNCTION2 + 4,
		TURBULENCE		= FUNCTION2 + 5,
		WAVE			= FUNCTION2 + 6,

		IDENTITY		= FUNCTION2 + 7,
		VERTEX			= FUNCTION2 + 8,
		TEXTURE			= FUNCTION2 + 9,
		LIGHTMAP		= FUNCTION2 + 10,
		ENVIRONMENT		= FUNCTION2 + 11,
		DOLLAR_LIGHTMAP		= FUNCTION2 + 12,
		BULGE			= FUNCTION2 + 13,
		AUTOSPRITE		= FUNCTION2 + 14,
		AUTOSPRITE2		= FUNCTION2 + 15,
		TRANSFORM		= FUNCTION2 + 16,
		EXACTVERTEX		= FUNCTION2 + 17,
		CONSTANT		= FUNCTION2 + 18,
		LIGHTINGSPECULAR	= FUNCTION2 + 19,
		MOVE			= FUNCTION2 + 20,
		NORMAL			= FUNCTION2 + 21,
		IDENTITYLIGHTING	= FUNCTION2 + 22,

		WAVE_MODIFIER_FUNCTION	= 0x30,
		SINUS			= WAVE_MODIFIER_FUNCTION + 1,
		COSINUS			= WAVE_MODIFIER_FUNCTION + 2,
		SQUARE			= WAVE_MODIFIER_FUNCTION + 3,
		TRIANGLE		= WAVE_MODIFIER_FUNCTION + 4,
		SAWTOOTH		= WAVE_MODIFIER_FUNCTION + 5,
		SAWTOOTH_INVERSE	= WAVE_MODIFIER_FUNCTION + 6,
		NOISE			= WAVE_MODIFIER_FUNCTION + 7,

		UNKNOWN			= -2
	};

	struct SModifierFunction
	{
		SModifierFunction ()
			: masterfunc0 ( UNKNOWN ), masterfunc1( UNKNOWN ), func ( SINUS ),
			tcgen( TEXTURE ), rgbgen ( IDENTITY ), alphagen ( UNKNOWN ),
			base ( 0 ), amp ( 1 ), phase ( 0 ), frequency ( 1 ),
			wave ( 1 ),
			x ( 0 ), y ( 0 ), z( 0 ), count( 0 ) {}

		// "tcmod","deformvertexes","rgbgen", "tcgen"
		eQ3ModifierFunction masterfunc0;
		// depends
		eQ3ModifierFunction masterfunc1;
		// depends
		eQ3ModifierFunction func;

		eQ3ModifierFunction tcgen;
		eQ3ModifierFunction rgbgen;
		eQ3ModifierFunction alphagen;

		union
		{
			f32 base;
			f32 bulgewidth;
		};

		union
		{
			f32 amp;
			f32 bulgeheight;
		};

		f32 phase;

		union
		{
			f32 frequency;
			f32 bulgespeed;
		};

		union
		{
			f32 wave;
			f32 div;
		};

		f32 x;
		f32 y;
		f32 z;
		u32 count;

		f32 evaluate ( f32 dt ) const
		{
			// phase in 0 and 1..
			f32 x = core::fract( (dt + phase ) * frequency );
			f32 y = 0.f;

			switch ( func )
			{
				case SINUS:
					y = sinf ( x * core::PI * 2.f );
					break;
				case COSINUS:
					y = cosf ( x * core::PI * 2.f );
					break;
				case SQUARE:
					y = x < 0.5f ? 1.f : -1.f;
					break;
				case TRIANGLE:
					y = x < 0.5f ? ( 4.f * x ) - 1.f : ( -4.f * x ) + 3.f;
					break;
				case SAWTOOTH:
					y = x;
					break;
				case SAWTOOTH_INVERSE:
					y = 1.f - x;
					break;
				case NOISE:
					y = Noiser::get();
					break;
				default:
					break;
			}

			return base + ( y * amp );
		}


	};

	inline core::vector3df getMD3Normal ( u32 i, u32 j )
	{
		const f32 lng = i * 2.0f * core::PI / 255.0f;
		const f32 lat = j * 2.0f * core::PI / 255.0f;
		return core::vector3df(cosf ( lat ) * sinf ( lng ),
				sinf ( lat ) * sinf ( lng ),
				cosf ( lng ));
	}

	//
	inline void getModifierFunc ( SModifierFunction& fill, const core::stringc &string, u32 &pos )
	{
		if ( string.size() == 0 )
			return;

		static const c8 * funclist[] =
		{
			"sin","cos","square",
			"triangle", "sawtooth","inversesawtooth", "noise"
		};

		fill.func = (eQ3ModifierFunction) isEqual ( string,pos, funclist,7 );
		fill.func = fill.func == UNKNOWN ? SINUS : (eQ3ModifierFunction) ((u32) fill.func + WAVE_MODIFIER_FUNCTION + 1);

		fill.base = getAsFloat ( string, pos );
		fill.amp = getAsFloat ( string, pos );
		fill.phase = getAsFloat ( string, pos );
		fill.frequency = getAsFloat ( string, pos );
	}


	// name = "a b c .."
	struct SVariable
	{
		core::stringc name;
		core::stringc content;

		SVariable ( const c8 * n, const c8 *c = 0 ) : name ( n ), content (c) {}
		virtual ~SVariable () {}

		void clear ()
		{
			name = "";
			content = "";
		}

		s32 isValid () const
		{
			return name.size();
		}

		bool operator == ( const SVariable &other ) const
		{
			return 0 == strcmp ( name.c_str(), other.name.c_str () );
		}

		bool operator < ( const SVariable &other ) const
		{
			return 0 > strcmp ( name.c_str(), other.name.c_str () );
		}

	};


	// string database. "a" = "Hello", "b" = "1234.6"
	struct SVarGroup
	{
		SVarGroup () { Variable.setAllocStrategy ( core::ALLOC_STRATEGY_SAFE ); }
		virtual ~SVarGroup () {}

		u32 isDefined ( const c8 * name, const c8 * content = 0 ) const
		{
			for ( u32 i = 0; i != Variable.size (); ++i )
			{
				if ( 0 == strcmp ( Variable[i].name.c_str(), name ) &&
					(  0 == content || strstr ( Variable[i].content.c_str(), content ) )
					)
				{
					return i + 1;
				}
			}
			return 0;
		}

		// searches for Variable name and returns is content
		// if Variable is not found a reference to an Empty String is returned
		const core::stringc &get( const c8 * name ) const
		{
			SVariable search ( name );
			s32 index = Variable.linear_search ( search );
			if ( index < 0 )
				return irrEmptyStringc;

			return Variable [ index ].content;
		}

		// set the Variable name
		void set ( const c8 * name, const c8 * content = 0 )
		{
			u32 index = isDefined ( name, 0 );
			if ( 0 == index )
			{
				Variable.push_back ( SVariable ( name, content ) );
			}
			else
			{
				Variable [ index ].content = content;
			}
		}


		core::array < SVariable > Variable;
	};

	//! holding a group a variable
	struct SVarGroupList: public IReferenceCounted
	{
		SVarGroupList ()
		{
			VariableGroup.setAllocStrategy ( core::ALLOC_STRATEGY_SAFE );
		}
		virtual ~SVarGroupList () {}

		core::array < SVarGroup > VariableGroup;
	};


	//! A Parsed Shader Holding Variables ordered in Groups
	struct IShader
	{
		IShader ()
			: ID ( 0 ), VarGroup ( 0 )  {}
		virtual ~IShader () {}


		bool operator == (const IShader &other ) const
		{
			return 0 == strcmp ( name.c_str(), other.name.c_str () );
			//return name == other.name;
		}

		bool operator < (const IShader &other ) const
		{
			return strcmp ( name.c_str(), other.name.c_str () ) < 0;
			//return name < other.name;
		}

		u32 getGroupSize () const
		{
			if ( 0 == VarGroup )
				return 0;
			return VarGroup->VariableGroup.size ();
		}

		const SVarGroup * getGroup ( u32 stage ) const
		{
			if ( 0 == VarGroup || stage >= VarGroup->VariableGroup.size () )
				return 0;

			return &VarGroup->VariableGroup [ stage ];
		}

		// id
		s32 ID;
		SVarGroupList *VarGroup; // reference

		// Shader: shader name ( also first variable in first Vargroup )
		// Entity: classname ( variable in Group(1) )
		core::stringc name;
	};

	typedef IShader IEntity;

	typedef core::array < IEntity > tQ3EntityList;

	/*
		dump shader like original layout, regardless of internal data holding
		no recursive folding..
	*/
	inline void dumpVarGroup ( core::stringc &dest, const SVarGroup * group, s32 stack )
	{
		core::stringc buf;

		if ( stack > 0 )
		{
			buf = "";
			for (s32 i = 0; i < stack - 1; ++i )
				buf += '\t';

			buf += "{\n";
			dest.append ( buf );
		}

		for ( u32 g = 0; g != group->Variable.size(); ++g )
		{
			buf = "";
			for (s32 i = 0; i < stack; ++i )
				buf += '\t';

			buf += group->Variable[g].name;
			buf += " ";
			buf += group->Variable[g].content;
			buf += "\n";
			dest.append ( buf );
		}

		if ( stack > 1 )
		{
			buf = "";
			for (s32 i = 0; i < stack - 1; ++i )
				buf += '\t';

			buf += "}\n";
			dest.append ( buf );
		}
	}

	/*!
		dump a Shader or an Entity
	*/
	inline core::stringc & dumpShader ( core::stringc &dest, const IShader * shader, bool entity = false )
	{
		if ( 0 == shader )
			return dest;

		const u32 size = shader->VarGroup->VariableGroup.size ();
		for ( u32 i = 0; i != size; ++i )
		{
			const SVarGroup * group = &shader->VarGroup->VariableGroup[ i ];
			dumpVarGroup ( dest, group, core::clamp( (int)i, 0, 2 ) );
		}

		if ( !entity )
		{
			if ( size <= 1 )
			{
				dest.append ( "{\n" );
			}
			dest.append ( "}\n" );
		}
		return dest;
	}


	/*
		quake3 doesn't care much about tga & jpg
		load one or multiple files stored in name started at startPos to the texture array textures
		if texture is not loaded 0 will be added ( to find missing textures easier)
	*/
	inline void getTextures(tTexArray &textures,
				const core::stringc &name, u32 &startPos,
				const io::IFileSystem *fileSystem,
				video::IVideoDriver* driver)
	{
		static const char * const extension[] =
		{
			".jpg",
			".jpeg",
			".png",
			".dds",
			".tga",
			".bmp",
			".pcx"
		};

		tStringList stringList;
		getAsStringList(stringList, -1, name, startPos);

		textures.clear();

		io::path loadFile;
		for ( u32 i = 0; i!= stringList.size (); ++i )
		{
			video::ITexture* texture = 0;
			for (u32 g = 0; g != 7; ++g)
			{
				core::cutFilenameExtension ( loadFile, stringList[i] );

				if ( loadFile == "$whiteimage" )
				{
					texture = driver->getTexture( "$whiteimage" );
					if ( 0 == texture )
					{
						core::dimension2du s ( 2, 2 );
						u32 image[4] = { 0xFFFFFFFF, 0xFFFFFFFF,0xFFFFFFFF,0xFFFFFFFF };
						video::IImage* w = driver->createImageFromData ( video::ECF_A8R8G8B8, s,&image );
						texture = driver->addTexture( "$whiteimage", w );
						w->drop ();
					}

				}
				else
				if ( loadFile == "$redimage" )
				{
					texture = driver->getTexture( "$redimage" );
					if ( 0 == texture )
					{
						core::dimension2du s ( 2, 2 );
						u32 image[4] = { 0xFFFF0000, 0xFFFF0000,0xFFFF0000,0xFFFF0000 };
						video::IImage* w = driver->createImageFromData ( video::ECF_A8R8G8B8, s,&image );
						texture = driver->addTexture( "$redimage", w );
						w->drop ();
					}
				}
				else
				if ( loadFile == "$blueimage" )
				{
					texture = driver->getTexture( "$blueimage" );
					if ( 0 == texture )
					{
						core::dimension2du s ( 2, 2 );
						u32 image[4] = { 0xFF0000FF, 0xFF0000FF,0xFF0000FF,0xFF0000FF };
						video::IImage* w = driver->createImageFromData ( video::ECF_A8R8G8B8, s,&image );
						texture = driver->addTexture( "$blueimage", w );
						w->drop ();
					}
				}
				else
				if ( loadFile == "$checkerimage" )
				{
					texture = driver->getTexture( "$checkerimage" );
					if ( 0 == texture )
					{
						core::dimension2du s ( 2, 2 );
						u32 image[4] = { 0xFFFFFFFF, 0xFF000000,0xFF000000,0xFFFFFFFF };
						video::IImage* w = driver->createImageFromData ( video::ECF_A8R8G8B8, s,&image );
						texture = driver->addTexture( "$checkerimage", w );
						w->drop ();
					}
				}
				else
				if ( loadFile == "$lightmap" )
				{
					texture = 0;
				}
				else
				{
					loadFile.append ( extension[g] );
				}

				texture = driver->findTexture( loadFile );
				if ( texture )
					break;

				if ( fileSystem->existFile ( loadFile ) )
				{
					texture = driver->getTexture( loadFile );
					if ( texture )
						break;
					texture = 0;
				}
			}
			// take 0 Texture
			textures.push_back(texture);
		}
	}


	//! Manages various Quake3 Shader Styles
	class IShaderManager : public IReferenceCounted
	{
	};

} // end namespace quake3
} // end namespace scene
} // end namespace irr

#endif