/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
Copyright (C) 2006 Robert Beckebans <trebor_7@users.sourceforge.net>

This file is part of XreaL source code.

XreaL source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

XreaL source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with XreaL source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/
// tr_shader.c -- this file deals with the parsing and definition of shaders
#include "tr_local.h"

#define MAX_GUIDETEXT_HASH		2048
static char  		  **guideTextHashTable[MAX_GUIDETEXT_HASH];

#define MAX_SHADERTABLE_HASH	1024
static shaderTable_t   *shaderTableHashTable[MAX_SHADERTABLE_HASH];

#define FILE_HASH_SIZE			1024
static shader_t		   *shaderHashTable[FILE_HASH_SIZE];

#define MAX_SHADERTEXT_HASH		2048
static char  		  **shaderTextHashTable[MAX_SHADERTEXT_HASH];

static char            *s_guideText;
static char            *s_shaderText;

// the shader is parsed into these global variables, then copied into
// dynamically allocated memory if it is valid.
static shaderTable_t	table;
static shaderStage_t	stages[MAX_SHADER_STAGES];
static shader_t			shader;
static texModInfo_t		texMods[MAX_SHADER_STAGES][TR_MAX_TEXMODS];
static qboolean			deferLoad;

/*
================
return a hash value for the filename
================
*/
static long generateHashValue(const char *fname, const int size)
{
	int             i;
//	int             len;
	long            hash;
	char            letter;

	hash = 0;
	i = 0;
//	len = strlen(fname);
	
	while(fname[i] != '\0')
//	for(i = 0; i < len; i++)
	{
		letter = tolower(fname[i]);
		
		if(letter == '.')
			break;				// don't include extension
		
		if(letter == '\\')
			letter = '/';		// damn path names
		
		if(letter == PATH_SEP)
			letter = '/';		// damn path names
		
		hash += (long)(letter) * (i + 119);
		i++;
	}
	hash = (hash ^ (hash >> 10) ^ (hash >> 20));
	hash &= (size - 1);
	return hash;
}

void R_RemapShader(const char *shaderName, const char *newShaderName, const char *timeOffset)
{
	char            strippedName[MAX_QPATH];
	int             hash;
	shader_t       *sh, *sh2;
	qhandle_t       h;

	sh = R_FindShaderByName(shaderName);
	if(sh == NULL || sh == tr.defaultShader)
	{
		h = RE_RegisterShaderLightMap(shaderName, 0);
		sh = R_GetShaderByHandle(h);
	}
	if(sh == NULL || sh == tr.defaultShader)
	{
		ri.Printf(PRINT_WARNING, "WARNING: R_RemapShader: shader %s not found\n", shaderName);
		return;
	}

	sh2 = R_FindShaderByName(newShaderName);
	if(sh2 == NULL || sh2 == tr.defaultShader)
	{
		h = RE_RegisterShaderLightMap(newShaderName, 0);
		sh2 = R_GetShaderByHandle(h);
	}

	if(sh2 == NULL || sh2 == tr.defaultShader)
	{
		ri.Printf(PRINT_WARNING, "WARNING: R_RemapShader: new shader %s not found\n", newShaderName);
		return;
	}

	// remap all the shaders with the given name
	// even tho they might have different lightmaps
	Com_StripExtension(shaderName, strippedName, sizeof(strippedName));
	hash = generateHashValue(strippedName, FILE_HASH_SIZE);
	for(sh = shaderHashTable[hash]; sh; sh = sh->next)
	{
		if(Q_stricmp(sh->name, strippedName) == 0)
		{
			if(sh != sh2)
			{
				sh->remappedShader = sh2;
			}
			else
			{
				sh->remappedShader = NULL;
			}
		}
	}
	if(timeOffset)
	{
		sh2->timeOffset = atof(timeOffset);
	}
}

/*
===============
ParseVector
===============
*/
static qboolean ParseVector(char **text, int count, float *v)
{
	char           *token;
	int             i;

	token = Com_ParseExt(text, qfalse);
	if(strcmp(token, "("))
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing parenthesis in shader '%s'\n", shader.name);
		return qfalse;
	}

	for(i = 0; i < count; i++)
	{
		token = Com_ParseExt(text, qfalse);
		if(!token[0])
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing vector element in shader '%s'\n", shader.name);
			return qfalse;
		}
		v[i] = atof(token);
	}

	token = Com_ParseExt(text, qfalse);
	if(strcmp(token, ")"))
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing parenthesis in shader '%s'\n", shader.name);
		return qfalse;
	}

	return qtrue;
}


opstring_t opStrings[] = 
{
	{"bad", OP_BAD},
	
	{"&&", OP_LAND},
	{"||", OP_LOR},
	{">=", OP_GE},
	{"<=", OP_LE},
	{"==", OP_LEQ},
	{"!=", OP_LNE},
	
	{"+", OP_ADD},
	{"-", OP_SUB},
	{"/", OP_DIV},
	{"%", OP_MOD},
	{"*", OP_MUL},
	{"neg", OP_NEG},
	
	{"<", OP_LT},
	{">", OP_GT},
	
	{"(", OP_LPAREN},
	{")", OP_RPAREN},
	{"[", OP_LBRACKET},
	{"]", OP_RBRACKET},
	
	{"c", OP_NUM},
	{"time", OP_TIME},
	{"parm0", OP_PARM0},
	{"parm1", OP_PARM1},
	{"parm2", OP_PARM2},
	{"parm3", OP_PARM3},
	{"parm4", OP_PARM4},
	{"parm5", OP_PARM5},
	{"parm6", OP_PARM6},
	{"parm7", OP_PARM7},
	{"parm8", OP_PARM8},
	{"parm9", OP_PARM9},
	{"parm10", OP_PARM10},
	{"parm11", OP_PARM11},
	{"global0", OP_GLOBAL0},
	{"global1", OP_GLOBAL1},
	{"global2", OP_GLOBAL2},
	{"global3", OP_GLOBAL3},
	{"global4", OP_GLOBAL4},
	{"global5", OP_GLOBAL5},
	{"global6", OP_GLOBAL6},
	{"global7", OP_GLOBAL7},
	{"fragmentPrograms", OP_FRAGMENTPROGRAMS},
	{"sound", OP_SOUND},
	
	{"table", OP_TABLE},
	
	{NULL, OP_BAD}
};

static void GetOpType(char *token, expOperation_t * op)
{
	opstring_t     *opString;
	char            tableName[MAX_QPATH];
	int             hash;
	shaderTable_t  *tb;
	
	if(	(token[0] >= '0' && token[0] <= '9')	||
		//(token[0] == '-' && token[1] >= '0' && token[1] <= '9')	||
		//(token[0] == '+' && token[1] >= '0' && token[1] <= '9')	||
		(token[0] == '.' && token[1] >= '0' && token[1] <= '9'))
	{
		op->type = OP_NUM;
		return;
	}

	Q_strncpyz(tableName, token, sizeof(tableName));
	hash = generateHashValue(tableName, MAX_SHADERTABLE_HASH);

	for(tb = shaderTableHashTable[hash]; tb; tb = tb->next)
	{
		if(Q_stricmp(tb->name, tableName) == 0)
		{
			// match found
			op->type = OP_TABLE;
			op->value = tb->index;
			return;
		}
	}
	
	for(opString = opStrings; opString->s; opString++)
	{
		if(!Q_stricmp(token, opString->s))
		{
			op->type = opString->type;
			return;
		}
	}
	
	op->type = OP_BAD;
}

static qboolean IsOperand(opcode_t oc)
{
	switch (oc)
	{
		case OP_NUM:
		case OP_TIME:
		case OP_PARM0:
		case OP_PARM1:
		case OP_PARM2:
		case OP_PARM3:
		case OP_PARM4:
		case OP_PARM5:
		case OP_PARM6:
		case OP_PARM7:
		case OP_PARM8:
		case OP_PARM9:
		case OP_PARM10:
		case OP_PARM11:
		case OP_GLOBAL0:
		case OP_GLOBAL1:
		case OP_GLOBAL2:
		case OP_GLOBAL3:
		case OP_GLOBAL4:
		case OP_GLOBAL5:
		case OP_GLOBAL6:
		case OP_GLOBAL7:
		case OP_FRAGMENTPROGRAMS:
		case OP_SOUND:
			return qtrue;
			
		default:
			return qfalse;
	}
}

static qboolean IsOperator(opcode_t oc)
{
	switch (oc)
	{
		case OP_LAND:
		case OP_LOR:
		case OP_GE:
		case OP_LE:
		case OP_LEQ:
		case OP_LNE:
		case OP_ADD:
		case OP_SUB:
		case OP_DIV:
		case OP_MOD:
		case OP_MUL:
		case OP_NEG:
		case OP_LT:
		case OP_GT:
		case OP_TABLE:
			return qtrue;
			
		default:
			return qfalse;
	}
}

static int GetOpPrecedence(opcode_t oc)
{
	switch (oc)
	{
		case OP_LOR:
			return 1;
			
		case OP_LAND:
			return 2;
		
		case OP_LEQ:
		case OP_LNE:
			return 3;
			
		case OP_GE:
		case OP_LE:
		case OP_LT:
		case OP_GT:
			return 4;
		
		case OP_ADD:
		case OP_SUB:
			return 5;
		
		case OP_DIV:
		case OP_MOD:
		case OP_MUL:
			return 6;
			
		case OP_NEG:
			return 7;
			
		case OP_TABLE:
			return 8;
			
		default:
			return 0;
	}
}

static char *ParseExpressionElement(char **data_p)
{
	int             c = 0, len;
	char           *data;
	const char    **punc;
	static char     token[MAX_TOKEN_CHARS];
	
	// multiple character punctuation tokens
	const char     *punctuation[] = {
		"&&", "||", "<=", ">=", "==", "!=", NULL
	};

	if(!data_p)
	{
		ri.Error(ERR_FATAL, "ParseExpressionElement: NULL data_p");
	}

	data = *data_p;
	len = 0;
	token[0] = 0;

	// make sure incoming data is valid
	if(!data)
	{
		*data_p = NULL;
		return token;
	}

	// skip whitespace
	while(1)
	{
		// skip whitespace
		while((c = *data) <= ' ')
		{
			if(!c)
			{
				*data_p = NULL;
				return token;
			}
			else if(c == '\n')
			{
				data++;
				*data_p = data;
				return token;
			}
			else
			{
				data++;
			}
		}

		c = *data;

		// skip double slash comments
		if(c == '/' && data[1] == '/')
		{
			data += 2;
			while(*data && *data != '\n')
			{
				data++;
			}
		}
		// skip /* */ comments
		else if(c == '/' && data[1] == '*')
		{
			data += 2;
			while(*data && (*data != '*' || data[1] != '/'))
			{
				data++;
			}
			if(*data)
			{
				data += 2;
			}
		}
		else
		{
			// a real token to parse
			break;
		}
	}

	// handle quoted strings
	if(c == '\"')
	{
		data++;
		while(1)
		{
			c = *data++;

			if((c == '\\') && (*data == '\"'))
			{
				// allow quoted strings to use \" to indicate the " character
				data++;
			}
			else if(c == '\"' || !c)
			{
				token[len] = 0;
				*data_p = (char *)data;
				return token;
			}

			if(len < MAX_TOKEN_CHARS - 1)
			{
				token[len] = c;
				len++;
			}
		}
	}

	// check for a number
	if(	(c >= '0' && c <= '9') ||
		//(c == '-' && data[1] >= '0' && data[1] <= '9') ||
		//(c == '+' && data[1] >= '0' && data[1] <= '9') ||
		(c == '.' && data[1] >= '0' && data[1] <= '9'))
	{
		do
		{
			if(len < MAX_TOKEN_CHARS - 1)
			{
				token[len] = c;
				len++;
			}
			data++;

			c = *data;
		} while((c >= '0' && c <= '9') || c == '.');

		// parse the exponent
		if(c == 'e' || c == 'E')
		{
			if(len < MAX_TOKEN_CHARS - 1)
			{
				token[len] = c;
				len++;
			}
			data++;
			c = *data;

			if(c == '-' || c == '+')
			{
				if(len < MAX_TOKEN_CHARS - 1)
				{
					token[len] = c;
					len++;
				}
				data++;
				c = *data;
			}

			do
			{
				if(len < MAX_TOKEN_CHARS - 1)
				{
					token[len] = c;
					len++;
				}
				data++;

				c = *data;
			} while(c >= '0' && c <= '9');
		}

		if(len == MAX_TOKEN_CHARS)
		{
			len = 0;
		}
		token[len] = 0;

		*data_p = (char *)data;
		return token;
	}

	// check for a regular word
	if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c == '_'))
	{
		do
		{
			if(len < MAX_TOKEN_CHARS - 1)
			{
				token[len] = c;
				len++;
			}
			data++;

			c = *data;
		}
		while
		(
			(c >= 'a' && c <= 'z') ||
			(c >= 'A' && c <= 'Z') ||
			(c == '_') ||
			(c >= '0' && c <= '9')
		);

		if(len == MAX_TOKEN_CHARS)
		{
			len = 0;
		}
		token[len] = 0;

		*data_p = (char *)data;
		return token;
	}

	// check for multi-character punctuation token
	for(punc = punctuation; *punc; punc++)
	{
		int             l;
		int             j;

		l = strlen(*punc);
		for(j = 0; j < l; j++)
		{
			if(data[j] != (*punc)[j])
			{
				break;
			}
		}
		if(j == l)
		{
			// a valid multi-character punctuation
			memcpy(token, *punc, l);
			token[l] = 0;
			data += l;
			*data_p = (char *)data;
			return token;
		}
	}

	// single character punctuation
	token[0] = *data;
	token[1] = 0;
	data++;
	*data_p = (char *)data;

	return token;
}

/*
===============
ParseExpression
===============
*/
static void ParseExpression(char **text, expression_t *exp)
{
	int				i;
	char           *token;
	
	expOperation_t  op, op2;
	
	expOperation_t  inFixOps[MAX_EXPRESSION_OPS];
	int				numInFixOps;
	
	// convert stack
	expOperation_t  tmpOps[MAX_EXPRESSION_OPS];
	int				numTmpOps;
	
	numInFixOps = 0;
	numTmpOps = 0;
	
	exp->numOps = 0;
	
	// push left parenthesis on the stack
	op.type = OP_LPAREN;
	op.value = 0;
	inFixOps[numInFixOps++] = op;

	while(1)
	{
		token = ParseExpressionElement(text);
				
		if(token[0] == 0 || token[0] == ',')
			break;
		
		if(numInFixOps == MAX_EXPRESSION_OPS)
		{
			ri.Printf(PRINT_ALL, "WARNING: too many arithmetic expression operations in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
			return;
		}
		
		GetOpType(token, &op);
		
		switch(op.type)
		{
			case OP_BAD:
				ri.Printf(PRINT_ALL, "WARNING: unknown token '%s' for arithmetic expression in shader '%s'\n", token, shader.name);
				break;
				
			case OP_LBRACKET:
				inFixOps[numInFixOps++] = op;
				
				// add extra (
				op2.type = OP_LPAREN;
				op2.value = 0;
				inFixOps[numInFixOps++] = op2;
				break;
				
			case OP_RBRACKET:
				// add extra )
				op2.type = OP_RPAREN;
				op2.value = 0;
				inFixOps[numInFixOps++] = op2;
				
				inFixOps[numInFixOps++] = op;
				break;
			
			case OP_NUM:
				op.value = atof(token);
				inFixOps[numInFixOps++] = op;
				break;
				
			case OP_TABLE:
				// value already set by GetOpType
				inFixOps[numInFixOps++] = op;
				break;
			
			default:
				op.value = 0;
				inFixOps[numInFixOps++] = op;
				break;
		}
	}
	
	// push right parenthesis on the stack
	op.type = OP_RPAREN;
	op.value = 0;
	inFixOps[numInFixOps++] = op;
	
	for(i = 0; i < (numInFixOps - 1); i++)
	{
		op = inFixOps[i];
		op2 = inFixOps[i + 1];
		
		// convert OP_SUBs that should be unary into OP_NEG
		if(op2.type == OP_SUB && op.type != OP_RPAREN && op.type != OP_TABLE && !IsOperand(op.type))
			inFixOps[i + 1].type = OP_NEG;
	}
	
#if 0
	ri.Printf(PRINT_ALL, "infix:\n");
	for(i = 0; i < numInFixOps; i++)
	{
		op = inFixOps[i];
		
		switch (op.type)
		{
			case OP_NUM:
				ri.Printf(PRINT_ALL, "%f ", op.value);
				break;
				
			case OP_TABLE:
				ri.Printf(PRINT_ALL, "%s ", tr.shaderTables[(int)op.value]->name);
				break;
			
			default:
				ri.Printf(PRINT_ALL, "%s ", opStrings[op.type].s);
				break;
		}
	}
	ri.Printf(PRINT_ALL, "\n");
#endif

	// http://cis.stvincent.edu/swd/stl/stacks/stacks.html
	// http://www.qiksearch.com/articles/cs/infix-postfix/
	// http://www.experts-exchange.com/Programming/Programming_Languages/C/Q_20394130.html
	
	//
	// convert infix representation to postfix
	//
	
	for(i = 0; i < numInFixOps; i++)
	{
		op = inFixOps[i];
		
		// if current operator in infix is digit
		if(IsOperand(op.type))
		{
			exp->ops[exp->numOps++] = op;
		}
		// if current operator in infix is left parenthesis
		else if(op.type == OP_LPAREN)
		{
			tmpOps[numTmpOps++] = op;
		}
		// if current operator in infix is operator
		else if(IsOperator(op.type))
		{
			while(qtrue)
			{
				if(!numTmpOps)
				{
					ri.Printf(PRINT_ALL, "WARNING: invalid infix expression in shader '%s'\n", shader.name);
					return;
				}
				else
				{
					// get top element
					op2 = tmpOps[numTmpOps -1];
					
					if(IsOperator(op2.type))
					{
						if(GetOpPrecedence(op2.type) >= GetOpPrecedence(op.type))
						{
							exp->ops[exp->numOps++] = op2;
							numTmpOps--;
						}
						else
						{
							break;
						}
					}
					else
					{
						break;
					}
				}
			}
			
			tmpOps[numTmpOps++] = op;
		}
		// if current operator in infix is right parenthesis
		else if(op.type == OP_RPAREN)
		{
			while(qtrue)
			{
				if(!numTmpOps)
				{
					ri.Printf(PRINT_ALL, "WARNING: invalid infix expression in shader '%s'\n", shader.name);
					return;
				}
				else
				{
					// get top element
					op2 = tmpOps[numTmpOps -1];
					
					if(op2.type != OP_LPAREN)
					{
						exp->ops[exp->numOps++] = op2;
						numTmpOps--;
					}
					else
					{
						numTmpOps--;
						break;
					}
				}
			}
		}
	}
	
	// everything went ok
	exp->active = qtrue;
	
#if 0
	ri.Printf(PRINT_ALL, "postfix:\n");
	for(i = 0; i < exp->numOps; i++)
	{
		op = exp->ops[i];
		
		switch (op.type)
		{
			case OP_NUM:
				ri.Printf(PRINT_ALL, "%f ", op.value);
				break;
				
			case OP_TABLE:
				ri.Printf(PRINT_ALL, "%s ", tr.shaderTables[(int)op.value]->name);
				break;
			
			default:
				ri.Printf(PRINT_ALL, "%s ", opStrings[op.type].s);
				break;
		}
	}
	ri.Printf(PRINT_ALL, "\n");
#endif
}


/*
===============
NameToAFunc
===============
*/
static unsigned NameToAFunc(const char *funcname)
{
	if(!Q_stricmp(funcname, "GT0"))
	{
		return GLS_ATEST_GT_0;
	}
	else if(!Q_stricmp(funcname, "LT128"))
	{
		return GLS_ATEST_LT_80;
	}
	else if(!Q_stricmp(funcname, "GE128"))
	{
		return GLS_ATEST_GE_80;
	}

	ri.Printf(PRINT_WARNING, "WARNING: invalid alphaFunc name '%s' in shader '%s'\n", funcname, shader.name);
	return 0;
}


/*
===============
NameToSrcBlendMode
===============
*/
static int NameToSrcBlendMode(const char *name)
{
	if(!Q_stricmp(name, "GL_ONE"))
	{
		return GLS_SRCBLEND_ONE;
	}
	else if(!Q_stricmp(name, "GL_ZERO"))
	{
		return GLS_SRCBLEND_ZERO;
	}
	else if(!Q_stricmp(name, "GL_DST_COLOR"))
	{
		return GLS_SRCBLEND_DST_COLOR;
	}
	else if(!Q_stricmp(name, "GL_ONE_MINUS_DST_COLOR"))
	{
		return GLS_SRCBLEND_ONE_MINUS_DST_COLOR;
	}
	else if(!Q_stricmp(name, "GL_SRC_ALPHA"))
	{
		return GLS_SRCBLEND_SRC_ALPHA;
	}
	else if(!Q_stricmp(name, "GL_ONE_MINUS_SRC_ALPHA"))
	{
		return GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA;
	}
	else if(!Q_stricmp(name, "GL_DST_ALPHA"))
	{
		return GLS_SRCBLEND_DST_ALPHA;
	}
	else if(!Q_stricmp(name, "GL_ONE_MINUS_DST_ALPHA"))
	{
		return GLS_SRCBLEND_ONE_MINUS_DST_ALPHA;
	}
	else if(!Q_stricmp(name, "GL_SRC_ALPHA_SATURATE"))
	{
		return GLS_SRCBLEND_ALPHA_SATURATE;
	}

	ri.Printf(PRINT_WARNING, "WARNING: unknown blend mode '%s' in shader '%s', substituting GL_ONE\n", name,
			  shader.name);
	return GLS_SRCBLEND_ONE;
}

/*
===============
NameToDstBlendMode
===============
*/
static int NameToDstBlendMode(const char *name)
{
	if(!Q_stricmp(name, "GL_ONE"))
	{
		return GLS_DSTBLEND_ONE;
	}
	else if(!Q_stricmp(name, "GL_ZERO"))
	{
		return GLS_DSTBLEND_ZERO;
	}
	else if(!Q_stricmp(name, "GL_SRC_ALPHA"))
	{
		return GLS_DSTBLEND_SRC_ALPHA;
	}
	else if(!Q_stricmp(name, "GL_ONE_MINUS_SRC_ALPHA"))
	{
		return GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;
	}
	else if(!Q_stricmp(name, "GL_DST_ALPHA"))
	{
		return GLS_DSTBLEND_DST_ALPHA;
	}
	else if(!Q_stricmp(name, "GL_ONE_MINUS_DST_ALPHA"))
	{
		return GLS_DSTBLEND_ONE_MINUS_DST_ALPHA;
	}
	else if(!Q_stricmp(name, "GL_SRC_COLOR"))
	{
		return GLS_DSTBLEND_SRC_COLOR;
	}
	else if(!Q_stricmp(name, "GL_ONE_MINUS_SRC_COLOR"))
	{
		return GLS_DSTBLEND_ONE_MINUS_SRC_COLOR;
	}

	ri.Printf(PRINT_WARNING, "WARNING: unknown blend mode '%s' in shader '%s', substituting GL_ONE\n", name,
			  shader.name);
	return GLS_DSTBLEND_ONE;
}

/*
===============
NameToGenFunc
===============
*/
static genFunc_t NameToGenFunc(const char *funcname)
{
	if(!Q_stricmp(funcname, "sin"))
	{
		return GF_SIN;
	}
	else if(!Q_stricmp(funcname, "square"))
	{
		return GF_SQUARE;
	}
	else if(!Q_stricmp(funcname, "triangle"))
	{
		return GF_TRIANGLE;
	}
	else if(!Q_stricmp(funcname, "sawtooth"))
	{
		return GF_SAWTOOTH;
	}
	else if(!Q_stricmp(funcname, "inversesawtooth"))
	{
		return GF_INVERSE_SAWTOOTH;
	}
	else if(!Q_stricmp(funcname, "noise"))
	{
		return GF_NOISE;
	}

	ri.Printf(PRINT_WARNING, "WARNING: invalid genfunc name '%s' in shader '%s'\n", funcname, shader.name);
	return GF_SIN;
}


/*
===================
ParseWaveForm
===================
*/
static void ParseWaveForm(char **text, waveForm_t * wave)
{
	char           *token;

	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing waveform parm in shader '%s'\n", shader.name);
		return;
	}
	wave->func = NameToGenFunc(token);

	// BASE, AMP, PHASE, FREQ
	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing waveform parm in shader '%s'\n", shader.name);
		return;
	}
	wave->base = atof(token);

	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing waveform parm in shader '%s'\n", shader.name);
		return;
	}
	wave->amplitude = atof(token);

	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing waveform parm in shader '%s'\n", shader.name);
		return;
	}
	wave->phase = atof(token);

	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing waveform parm in shader '%s'\n", shader.name);
		return;
	}
	wave->frequency = atof(token);
}


/*
===================
ParseTexMod
===================
*/
static qboolean ParseTexMod(char **text, shaderStage_t * stage)
{
	const char     *token;
	texModInfo_t   *tmi;

	if(stage->bundle[0].numTexMods == TR_MAX_TEXMODS)
	{
		ri.Error(ERR_DROP, "ERROR: too many tcMod stages in shader '%s'\n", shader.name);
		return qfalse;
	}

	tmi = &stage->bundle[0].texMods[stage->bundle[0].numTexMods];
	stage->bundle[0].numTexMods++;

	token = Com_ParseExt(text, qfalse);
	
//	ri.Printf(PRINT_ALL, "using tcMod '%s' in shader '%s'\n", token, shader.name);

	// turb
	if(!Q_stricmp(token, "turb"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing tcMod turb parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.base = atof(token);
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing tcMod turb in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.amplitude = atof(token);
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing tcMod turb in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.phase = atof(token);
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing tcMod turb in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.frequency = atof(token);

		tmi->type = TMOD_TURBULENT;
	}
	// scale
	else if(!Q_stricmp(token, "scale"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing scale parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->scale[0] = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing scale parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->scale[1] = atof(token);
		tmi->type = TMOD_SCALE;
	}
	// scroll
	else if(!Q_stricmp(token, "scroll"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing scale scroll parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->scroll[0] = atof(token);
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing scale scroll parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->scroll[1] = atof(token);
		tmi->type = TMOD_SCROLL;
	}
	// stretch
	else if(!Q_stricmp(token, "stretch"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing stretch parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.func = NameToGenFunc(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing stretch parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.base = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing stretch parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.amplitude = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing stretch parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.phase = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing stretch parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->wave.frequency = atof(token);

		tmi->type = TMOD_STRETCH;
	}
	// transform
	else if(!Q_stricmp(token, "transform"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing transform parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->matrix[0][0] = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing transform parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->matrix[0][1] = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing transform parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->matrix[1][0] = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing transform parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->matrix[1][1] = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing transform parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->translate[0] = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing transform parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->translate[1] = atof(token);

		tmi->type = TMOD_TRANSFORM;
	}
	// rotate
	else if(!Q_stricmp(token, "rotate"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing tcMod rotate parms in shader '%s'\n", shader.name);
			return qfalse;
		}
		tmi->rotateSpeed = atof(token);
		tmi->type = TMOD_ROTATE;
	}
	// entityTranslate
	else if(!Q_stricmp(token, "entityTranslate"))
	{
		tmi->type = TMOD_ENTITY_TRANSLATE;
	}
	else
	{
		ri.Printf(PRINT_WARNING, "WARNING: unknown tcMod '%s' in shader '%s'\n", token, shader.name);
		return qfalse;
	}
	
	// Tr3B NOTE: some shaders using tcMod are messed up by artists so we need this bugfix
	while(1)
	{
		token = Com_ParseExt(text, qfalse);
				
		if(token[0] == 0)
			break;
		
		ri.Printf(PRINT_WARNING, "WARNING: obselete tcMod parameter '%s' in shader '%s'\n", token, shader.name);
	}
	
	return qtrue;
}



static qboolean ParseMap(shaderStage_t * stage, char **text, char *buffer, int bufferSize)
{
	int				len;
	char           *token;
	
	// examples
	// map textures/caves/tembrick1crum_local.tga
	// addnormals (textures/caves/tembrick1crum_local.tga, heightmap (textures/caves/tembrick1crum_bmp.tga, 3 ))
	// heightmap( textures/hell/hellbones_d07bbump.tga, 8)

	while(1)
	{
		token = Com_ParseExt(text, qfalse);
				
		if(!token[0])
		{
			// end of line
			break;
		}
				
		Q_strcat(buffer, bufferSize, token);
		Q_strcat(buffer, bufferSize, " ");
	}
			
	if(!buffer[0])
	{
		ri.Printf(PRINT_WARNING, "WARNING: 'map' missing parameter in shader '%s'\n", shader.name);
		return qfalse;
	}
	
	len = strlen(buffer);
	buffer[len -1] = 0;	// replace last ' ' with tailing zero
	
	return qtrue;
}

static qboolean LoadMap(shaderStage_t * stage, char *buffer)
{
	char           *token;
	int				imageBits = 0;
	filterType_t	filterType;
	wrapType_t		wrapType;
	char           *buffer_p = &buffer[0];
	
	if(!buffer || !buffer[0])
	{
		ri.Printf(PRINT_WARNING, "WARNING: NULL parameter for LoadMap in shader '%s'\n", shader.name);
		return qfalse;
	}
	
//	ri.Printf(PRINT_ALL, "LoadMap: buffer '%s'\n", buffer);
	
	token = Com_ParseExt(&buffer_p, qfalse);
	
	if(!Q_stricmp(token, "$whiteimage") || !Q_stricmp(token, "$white") || !Q_stricmp(token, "_white") || !Q_stricmp(token, "*white"))
	{
		stage->bundle[0].image[0] = tr.whiteImage;
		return qtrue;
	}
	else if(!Q_stricmp(token, "$blackimage") || !Q_stricmp(token, "$black") || !Q_stricmp(token, "_black") || !Q_stricmp(token, "*black"))
	{
		stage->bundle[0].image[0] = tr.blackImage;
		return qtrue;
	}
	else if(!Q_stricmp(token, "$flatimage") || !Q_stricmp(token, "$flat") || !Q_stricmp(token, "_flat") || !Q_stricmp(token, "*flat"))
	{
		stage->bundle[0].image[0] = tr.flatImage;
		return qtrue;
	}
	else if(!Q_stricmp(token, "$lightmap") || !Q_stricmp(token, "_lightmap") || !Q_stricmp(token, "*lightmap"))
	{
		stage->type = ST_LIGHTMAP;
		stage->bundle[0].isLightMap = qtrue;
		return qtrue;
	}
	
	// determine image options	
	if(stage->overrideNoPicMip || shader.noPicMip)
	{
		imageBits |= IF_NOPICMIP;
	}
	
	if(stage->type == ST_NORMALMAP || stage->type == ST_HEATHAZEMAP)
	{
		imageBits |= IF_NORMALMAP;
	}
	
	if(stage->overrideFilterType)
	{
		filterType = stage->filterType;
	}
	else
	{
		filterType = shader.filterType;
	}
	
	if(stage->overrideWrapType)
	{
		wrapType = stage->wrapType;
	}
	else
	{
		wrapType = shader.wrapType;
	}
	
	// try to load the image
	stage->bundle[0].image[0] = R_FindImageFile(buffer, imageBits, filterType, wrapType);

	if(!stage->bundle[0].image[0])
	{
		ri.Printf(PRINT_WARNING, "WARNING: R_FindImageFile could not find image '%s' in shader '%s'\n", buffer, shader.name);
		return qfalse;
	}
	
	return qtrue;
}

/*
===================
ParseStage
===================
*/
static qboolean ParseStage(shaderStage_t * stage, char **text)
{
	char           *token;
	int				colorMaskBits = 0;
	int             depthMaskBits = GLS_DEPTHMASK_TRUE, blendSrcBits = 0, blendDstBits = 0, atestBits = 0, depthFuncBits = 0;
	qboolean        depthMaskExplicit = qfalse;
	int				imageBits = 0;
	filterType_t	filterType;
	char            buffer[1024] = "";
	qboolean		loadMap = qfalse;

	while(1)
	{
		token = Com_ParseExt(text, qtrue);
		if(!token[0])
		{
			ri.Printf(PRINT_WARNING, "WARNING: no matching '}' found\n");
			return qfalse;
		}

		if(token[0] == '}')
		{
			break;
		}
		// if(<condition>)
		else if(!Q_stricmp(token, "if"))
		{
			ParseExpression(text, &stage->ifExp);
		}
		// map <name>
		else if(!Q_stricmp(token, "map"))
		{
			if(!ParseMap(stage, text, buffer, sizeof(buffer)))
			{
				//ri.Printf(PRINT_WARNING, "WARNING: ParseMap could not create '%s' in shader '%s'\n", token, shader.name);
				return qfalse;
			}
			else
			{
				loadMap = qtrue;	
			}
		}
		// remoteRenderMap <int> <int>
		else if(!Q_stricmp(token, "remoteRenderMap"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: remoteRenderMap keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		// mirrorRenderMap <int> <int>
		else if(!Q_stricmp(token, "mirrorRenderMap"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: mirrorRenderMap keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		// clampmap <name>
		else if(!Q_stricmp(token, "clampmap"))
		{
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameter for 'clampmap' keyword in shader '%s'\n", shader.name);
				return qfalse;
			}
			
			imageBits = 0;
			if(stage->overrideNoPicMip || shader.noPicMip)
			{
				imageBits |= IF_NOPICMIP;
			}
			
			if(stage->overrideFilterType)
			{
				filterType = stage->filterType;
			}
			else
			{
				filterType = shader.filterType;
			}

			stage->bundle[0].image[0] = R_FindImageFile(token, imageBits, filterType, WT_CLAMP);
			if(!stage->bundle[0].image[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: R_FindImageFile could not find '%s' in shader '%s'\n", token, shader.name);
				return qfalse;
			}
		}
		// animMap <frequency> <image1> .... <imageN>
		else if(!Q_stricmp(token, "animMap"))
		{
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameter for 'animMmap' keyword in shader '%s'\n", shader.name);
				return qfalse;
			}
			stage->bundle[0].imageAnimationSpeed = atof(token);
			
			imageBits = 0;
			if(stage->overrideNoPicMip || shader.noPicMip)
			{
				imageBits |= IF_NOPICMIP;
			}
			
			if(stage->overrideFilterType)
			{
				filterType = stage->filterType;
			}
			else
			{
				filterType = shader.filterType;
			}

			// parse up to MAX_IMAGE_ANIMATIONS animations
			while(1)
			{
				int             num;

				token = Com_ParseExt(text, qfalse);
				if(!token[0])
				{
					break;
				}
				num = stage->bundle[0].numImageAnimations;
				if(num < MAX_IMAGE_ANIMATIONS)
				{
					stage->bundle[0].image[num] = R_FindImageFile(token, imageBits, filterType, WT_REPEAT);
					if(!stage->bundle[0].image[num])
					{
						ri.Printf(PRINT_WARNING,
								  "WARNING: R_FindImageFile could not find '%s' in shader '%s'\n", token, shader.name);
						return qfalse;
					}
					stage->bundle[0].numImageAnimations++;
				}
			}
		}
		else if(!Q_stricmp(token, "videoMap"))
		{
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameter for 'videoMap' keyword in shader '%s'\n", shader.name);
				return qfalse;
			}
			stage->bundle[0].videoMapHandle = ri.CIN_PlayCinematic(token, 0, 0, 256, 256, (CIN_loop | CIN_silent | CIN_shader));
			if(stage->bundle[0].videoMapHandle != -1)
			{
				stage->bundle[0].isVideoMap = qtrue;
				stage->bundle[0].image[0] = tr.scratchImage[stage->bundle[0].videoMapHandle];
			}
		}
		// soundmap [waveform]
		else if(!Q_stricmp(token, "soundMap"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: soundMap keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		else if(!Q_stricmp(token, "cubeMap"))
		{
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameter for 'cubeMap' keyword in shader '%s'\n", shader.name);
				return qfalse;
			}
			
			imageBits = 0;
			if(stage->overrideNoPicMip || shader.noPicMip)
			{
				imageBits |= IF_NOPICMIP;
			}
			
			if(stage->overrideFilterType)
			{
				filterType = stage->filterType;
			}
			else
			{
				filterType = shader.filterType;
			}
			
			stage->bundle[0].image[0] = R_FindCubeImage(token, imageBits, filterType, WT_CLAMP);
			if(!stage->bundle[0].image[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: R_FindCubeImage could not find '%s' in shader '%s'\n", token, shader.name);
				return qfalse;
			}
		}
		// cameraCubeMap <map>
		else if(!Q_stricmp(token, "cameraCubeMap"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: cameraCubeMap keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		// alphafunc <func>
		else if(!Q_stricmp(token, "alphaFunc"))
		{
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING,
						  "WARNING: missing parameter for 'alphaFunc' keyword in shader '%s'\n", shader.name);
				return qfalse;
			}

			atestBits = NameToAFunc(token);
		}
		// alphaTest <exp>
		else if(!Q_stricmp(token, "alphaTest"))
		{
			atestBits = GLS_ATEST_GT_CUSTOM;
			ParseExpression(text, &stage->alphaTestExp);
		}
		// depthFunc <func>
		else if(!Q_stricmp(token, "depthfunc"))
		{
			token = Com_ParseExt(text, qfalse);

			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameter for 'depthfunc' keyword in shader '%s'\n", shader.name);
				return qfalse;
			}

			if(!Q_stricmp(token, "lequal"))
			{
				depthFuncBits = 0;
			}
			else if(!Q_stricmp(token, "equal"))
			{
				depthFuncBits = GLS_DEPTHFUNC_EQUAL;
			}
			else
			{
				ri.Printf(PRINT_WARNING, "WARNING: unknown depthfunc '%s' in shader '%s'\n", token, shader.name);
				continue;
			}
		}
		// ignoreAlphaTest
		else if(!Q_stricmp(token, "ignoreAlphaTest"))
		{
			depthFuncBits = 0;
		}
		// nearest
		else if(!Q_stricmp(token, "nearest"))
		{
			stage->overrideFilterType = qtrue;
			stage->filterType = FT_NEAREST;
		}
		// linear
		else if(!Q_stricmp(token, "linear"))
		{
			stage->overrideFilterType = qtrue;
			stage->filterType = FT_LINEAR;
			
			stage->overrideNoPicMip = qtrue;
		}
		// noPicMip
		else if(!Q_stricmp(token, "noPicMip"))
		{
			stage->overrideNoPicMip = qtrue;
		}
		// clamp
		else if(!Q_stricmp(token, "clamp"))
		{
			stage->overrideWrapType = qtrue;
			stage->wrapType = WT_CLAMP;
		}
		// edgeClamp
		else if(!Q_stricmp(token, "edgeClamp"))
		{
			stage->overrideWrapType = qtrue;
			stage->wrapType = WT_EDGE_CLAMP;
		}
		// zeroClamp
		else if(!Q_stricmp(token, "zeroClamp"))
		{
			stage->overrideWrapType = qtrue;
			stage->wrapType = WT_ZERO_CLAMP;
		}
		// alphaZeroClamp
		else if(!Q_stricmp(token, "alphaZeroClamp"))
		{
			stage->overrideWrapType = qtrue;
			stage->wrapType = WT_ALPHA_ZERO_CLAMP;
		}
		// noClamp
		else if(!Q_stricmp(token, "noClamp"))
		{
			stage->overrideWrapType = qtrue;
			stage->wrapType = WT_REPEAT;
		}
		// uncompressed
		else if(!Q_stricmp(token, "uncompressed"))
		{
			stage->uncompressed = qtrue;
		}
		// highQuality
		else if(!Q_stricmp(token, "highQuality"))
		{
			stage->highQuality = qtrue;
		}
		// forceHighQuality
		else if(!Q_stricmp(token, "forceHighQuality"))
		{
			stage->forceHighQuality = qtrue;
		}
		// detail
		else if(!Q_stricmp(token, "detail"))
		{
			stage->isDetail = qtrue;
			continue;
		}
		// blendfunc <srcFactor> <dstFactor>
		// or blendfunc <add|filter|blend>
		else if(!Q_stricmp(token, "blendfunc"))
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0] == 0)
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for blendFunc in shader '%s'\n", shader.name);
				continue;
			}
			// check for "simple" blends first
			if(!Q_stricmp(token, "add"))
			{
				blendSrcBits = GLS_SRCBLEND_ONE;
				blendDstBits = GLS_DSTBLEND_ONE;
			}
			else if(!Q_stricmp(token, "filter"))
			{
				blendSrcBits = GLS_SRCBLEND_DST_COLOR;
				blendDstBits = GLS_DSTBLEND_ZERO;
			}
			else if(!Q_stricmp(token, "blend"))
			{
				blendSrcBits = GLS_SRCBLEND_SRC_ALPHA;
				blendDstBits = GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;
			}
			else
			{
				// complex double blends
				blendSrcBits = NameToSrcBlendMode(token);

				token = Com_ParseExt(text, qfalse);
				if(token[0] == 0)
				{
					ri.Printf(PRINT_WARNING, "WARNING: missing parm for blendFunc in shader '%s'\n",
							  shader.name);
					continue;
				}
				blendDstBits = NameToDstBlendMode(token);
			}

			// clear depth mask for blended surfaces
			if(!depthMaskExplicit)
			{
				depthMaskBits = 0;
			}
		}
		// blend <srcFactor> , <dstFactor>
		// or blend <add | filter | blend>
		// or blend <diffusemap | bumpmap | specularmap | lightmap>
		else if(!Q_stricmp(token, "blend"))
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0] == 0)
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for blend in shader '%s'\n", shader.name);
				continue;
			}
			
			// check for "simple" blends first
			if(!Q_stricmp(token, "add"))
			{
				blendSrcBits = GLS_SRCBLEND_ONE;
				blendDstBits = GLS_DSTBLEND_ONE;
			}
			else if(!Q_stricmp(token, "filter"))
			{
				blendSrcBits = GLS_SRCBLEND_DST_COLOR;
				blendDstBits = GLS_DSTBLEND_ZERO;
			}
			else if(!Q_stricmp(token, "modulate"))
			{
				blendSrcBits = GLS_SRCBLEND_DST_COLOR;
				blendDstBits = GLS_DSTBLEND_ZERO;
			}
			else if(!Q_stricmp(token, "blend"))
			{
				blendSrcBits = GLS_SRCBLEND_SRC_ALPHA;
				blendDstBits = GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;
			}
			else if(!Q_stricmp(token, "none"))
			{
				blendSrcBits = GLS_SRCBLEND_ZERO;
				blendDstBits = GLS_DSTBLEND_ONE;
			}
			// check for other semantic meanings
			else if(!Q_stricmp(token, "diffuseMap"))
			{
				stage->type = ST_DIFFUSEMAP;
			}
			else if(!Q_stricmp(token, "bumpMap"))
			{
				stage->type = ST_NORMALMAP;
			}
			else if(!Q_stricmp(token, "specularMap"))
			{
				stage->type = ST_SPECULARMAP;
			}
			else
			{
				// complex double blends
				blendSrcBits = NameToSrcBlendMode(token);

				token = Com_ParseExt(text, qfalse);
				if(token[0] != ',')
				{
					ri.Printf(PRINT_WARNING, "WARNING: expecting ',', found '%s' instead for blend in shader '%s'\n", token, shader.name);
					continue;
				}
				
				token = Com_ParseExt(text, qfalse);
				if(token[0] == 0)
				{
					ri.Printf(PRINT_WARNING, "WARNING: missing parm for blend in shader '%s'\n", shader.name);
					continue;
				}
				blendDstBits = NameToDstBlendMode(token);
			}

			// clear depth mask for blended surfaces
			if(!depthMaskExplicit && stage->type == ST_COLORMAP)
			{
				depthMaskBits = 0;
			}
		}
		// stage <type>
		else if(!Q_stricmp(token, "stage"))
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0] == 0)
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameters for stage in shader '%s'\n", shader.name);
				continue;
			}

			if(!Q_stricmp(token, "colorMap"))
			{
				stage->type = ST_COLORMAP;
			}
			else if(!Q_stricmp(token, "diffuseMap"))
			{
				stage->type = ST_DIFFUSEMAP;
			}
			else if(!Q_stricmp(token, "normalMap") || !Q_stricmp(token, "bumpMap"))
			{
				stage->type = ST_NORMALMAP;
			}
			else if(!Q_stricmp(token, "specularMap"))
			{
				stage->type = ST_SPECULARMAP;
			}
			else if(!Q_stricmp(token, "heathazeMap"))
			{
				stage->type = ST_HEATHAZEMAP;
			}
			else if(!Q_stricmp(token, "glowMap"))
			{
				stage->type = ST_GLOWMAP;
			}
			else if(!Q_stricmp(token, "lightMap"))
			{
				stage->type = ST_LIGHTMAP;
			}
			else if(!Q_stricmp(token, "reflectionMap"))
			{
				stage->type = ST_REFLECTIONMAP;
			}
			else if(!Q_stricmp(token, "refractionMap"))
			{
				stage->type = ST_REFRACTIONMAP;
			}
			else if(!Q_stricmp(token, "dispersionMap"))
			{
				stage->type = ST_DISPERSIONMAP;
			}
			else if(!Q_stricmp(token, "skyboxMap"))
			{
				stage->type = ST_SKYBOXMAP;
			}
			else if(!Q_stricmp(token, "liquidMap"))
			{
				stage->type = ST_LIQUIDMAP;
			}
			else if(!Q_stricmp(token, "attenuationMapXY"))
			{
				stage->type = ST_ATTENUATIONMAP_XY;
			}
			else if(!Q_stricmp(token, "attenuationMapZ"))
			{
				stage->type = ST_ATTENUATIONMAP_Z;
			}
			else
			{
				ri.Printf(PRINT_WARNING, "WARNING: unknown stage parameter '%s' in shader '%s'\n", token, shader.name);
				continue;
			}
		}
		// rgbGen
		else if(!Q_stricmp(token, "rgbGen"))
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0] == 0)
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameters for rgbGen in shader '%s'\n",
						  shader.name);
				continue;
			}

			if(!Q_stricmp(token, "wave"))
			{
				ParseWaveForm(text, &stage->rgbWave);
				stage->rgbGen = CGEN_WAVEFORM;
			}
			else if(!Q_stricmp(token, "const"))
			{
				vec3_t          color;

				ParseVector(text, 3, color);
				stage->constantColor[0] = 255 * color[0];
				stage->constantColor[1] = 255 * color[1];
				stage->constantColor[2] = 255 * color[2];

				stage->rgbGen = CGEN_CONST;
			}
			else if(!Q_stricmp(token, "identity"))
			{
				stage->rgbGen = CGEN_IDENTITY;
			}
			else if(!Q_stricmp(token, "identityLighting"))
			{
				stage->rgbGen = CGEN_IDENTITY_LIGHTING;
			}
			else if(!Q_stricmp(token, "entity"))
			{
				stage->rgbGen = CGEN_ENTITY;
			}
			else if(!Q_stricmp(token, "oneMinusEntity"))
			{
				stage->rgbGen = CGEN_ONE_MINUS_ENTITY;
			}
			else if(!Q_stricmp(token, "vertex"))
			{
				stage->rgbGen = CGEN_VERTEX;
				if(stage->alphaGen == 0)
				{
					stage->alphaGen = AGEN_VERTEX;
				}
			}
			else if(!Q_stricmp(token, "exactVertex"))
			{
				stage->rgbGen = CGEN_EXACT_VERTEX;
			}
			else if(!Q_stricmp(token, "lightingDiffuse"))
			{
				stage->type = ST_DIFFUSEMAP;
				stage->rgbGen = CGEN_LIGHTING_DIFFUSE;
			}
			else if(!Q_stricmp(token, "oneMinusVertex"))
			{
				stage->rgbGen = CGEN_ONE_MINUS_VERTEX;
			}
			else
			{
				ri.Printf(PRINT_WARNING, "WARNING: unknown rgbGen parameter '%s' in shader '%s'\n", token, shader.name);
				continue;
			}
		}
		// rgb <arithmetic expression>
		else if(!Q_stricmp(token, "rgb"))
		{
			stage->rgbGen = CGEN_CUSTOM_RGB;
			ParseExpression(text, &stage->rgbExp);
		}
		// red <arithmetic expression>
		else if(!Q_stricmp(token, "red"))
		{
			stage->rgbGen = CGEN_CUSTOM_RGBs;
			ParseExpression(text, &stage->redExp);
		}
		// green <arithmetic expression>
		else if(!Q_stricmp(token, "green"))
		{
			stage->rgbGen = CGEN_CUSTOM_RGBs;
			ParseExpression(text, &stage->greenExp);
		}
		// blue <arithmetic expression>
		else if(!Q_stricmp(token, "blue"))
		{
			stage->rgbGen = CGEN_CUSTOM_RGBs;
			ParseExpression(text, &stage->blueExp);
		}
		// colored
		else if(!Q_stricmp(token, "colored"))
		{
			stage->rgbGen = CGEN_ENTITY;
			stage->alphaGen = AGEN_ENTITY;
		}
		// vertexColor
		else if(!Q_stricmp(token, "vertexColor"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: vertexColor keyword not supported in shader '%s'\n", shader.name);
		}
		// inverseVertexColor
		else if(!Q_stricmp(token, "inverseVertexColor"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: inverseVertexColor keyword not supported in shader '%s'\n", shader.name);
		}
		// alphaGen 
		else if(!Q_stricmp(token, "alphaGen"))
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0] == 0)
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameters for alphaGen in shader '%s'\n",
						  shader.name);
				continue;
			}

			if(!Q_stricmp(token, "wave"))
			{
				ParseWaveForm(text, &stage->alphaWave);
				stage->alphaGen = AGEN_WAVEFORM;
			}
			else if(!Q_stricmp(token, "const"))
			{
				token = Com_ParseExt(text, qfalse);
				stage->constantColor[3] = 255 * atof(token);
				stage->alphaGen = AGEN_CONST;
			}
			else if(!Q_stricmp(token, "identity"))
			{
				stage->alphaGen = AGEN_IDENTITY;
			}
			else if(!Q_stricmp(token, "entity"))
			{
				stage->alphaGen = AGEN_ENTITY;
			}
			else if(!Q_stricmp(token, "oneMinusEntity"))
			{
				stage->alphaGen = AGEN_ONE_MINUS_ENTITY;
			}
			else if(!Q_stricmp(token, "vertex"))
			{
				stage->alphaGen = AGEN_VERTEX;
			}
			else if(!Q_stricmp(token, "lightingSpecular"))
			{
				stage->alphaGen = AGEN_LIGHTING_SPECULAR;
			}
			else if(!Q_stricmp(token, "oneMinusVertex"))
			{
				stage->alphaGen = AGEN_ONE_MINUS_VERTEX;
			}
			else if(!Q_stricmp(token, "portal"))
			{
				stage->alphaGen = AGEN_PORTAL;
				token = Com_ParseExt(text, qfalse);
				if(token[0] == 0)
				{
					shader.portalRange = 256;
					ri.Printf(PRINT_WARNING,
							  "WARNING: missing range parameter for alphaGen portal in shader '%s', defaulting to 256\n",
							  shader.name);
				}
				else
				{
					shader.portalRange = atof(token);
				}
			}
			else
			{
				ri.Printf(PRINT_WARNING, "WARNING: unknown alphaGen parameter '%s' in shader '%s'\n", token, shader.name);
				continue;
			}
		}
		// alpha <arithmetic expression>
		else if(!Q_stricmp(token, "alpha"))
		{
			stage->alphaGen = AGEN_CUSTOM;
			ParseExpression(text, &stage->alphaExp);
		}
		// color <exp>, <exp>, <exp>, <exp>
		else if(!Q_stricmp(token, "color"))
		{
			stage->rgbGen = CGEN_CUSTOM_RGBs;
			stage->alphaGen = AGEN_CUSTOM;
			ParseExpression(text, &stage->redExp);
			ParseExpression(text, &stage->greenExp);
			ParseExpression(text, &stage->blueExp);
			ParseExpression(text, &stage->alphaExp);
		}
		// privatePolygonOffset <float>
		else if(!Q_stricmp(token, "privatePolygonOffset"))
		{
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parameter for 'privatePolygonOffset' keyword in shader '%s'\n", shader.name);
				return qfalse;
			}
			
			stage->privatePolygonOffset = qtrue;
			stage->privatePolygonOffsetValue = atof(token);
		}
		// tcGen <function>
		else if(!Q_stricmp(token, "texGen") || !Q_stricmp(token, "tcGen"))
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0] == 0)
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing texgen parm in shader '%s'\n", shader.name);
				continue;
			}

			if(!Q_stricmp(token, "environment"))
			{
				stage->bundle[0].tcGen = TCGEN_ENVIRONMENT_MAPPED;
			}
			else if(!Q_stricmp(token, "lightmap"))
			{
				stage->bundle[0].tcGen = TCGEN_LIGHTMAP;
			}
			else if(!Q_stricmp(token, "texture") || !Q_stricmp(token, "base"))
			{
				stage->bundle[0].tcGen = TCGEN_TEXTURE;
			}
			else if(!Q_stricmp(token, "vector"))
			{
				ParseVector(text, 3, stage->bundle[0].tcGenVectors[0]);
				ParseVector(text, 3, stage->bundle[0].tcGenVectors[1]);

				stage->bundle[0].tcGen = TCGEN_VECTOR;
			}
			else if(!Q_stricmp(token, "reflect"))
			{
				stage->type = ST_REFLECTIONMAP;
			}
			else if(!Q_stricmp(token, "skybox"))
			{
				stage->type = ST_SKYBOXMAP;
			}
			else
			{
				ri.Printf(PRINT_WARNING, "WARNING: unknown texgen parm in shader '%s'\n", shader.name);
			}
		}
		// tcMod <type> <...>
		else if(!Q_stricmp(token, "tcMod"))
		{
			if(!ParseTexMod(text, stage))
			{
				return qfalse;
			}
		}
		// scroll
		else if(!Q_stricmp(token, "scroll") || !Q_stricmp(token, "translate"))
		{
			texModInfo_t   *tmi;
			
			if(stage->bundle[0].numTexMods == TR_MAX_TEXMODS)
			{
				ri.Error(ERR_DROP, "ERROR: too many tcMod stages in shader '%s'\n", shader.name);
				return qfalse;
			}

			tmi = &stage->bundle[0].texMods[stage->bundle[0].numTexMods];
			stage->bundle[0].numTexMods++;	
			
			ParseExpression(text, &tmi->sExp);
			ParseExpression(text, &tmi->tExp);
			
			tmi->type = TMOD_SCROLL2;
		}
		// scale
		else if(!Q_stricmp(token, "scale"))
		{
			texModInfo_t   *tmi;
			
			if(stage->bundle[0].numTexMods == TR_MAX_TEXMODS)
			{
				ri.Error(ERR_DROP, "ERROR: too many tcMod stages in shader '%s'\n", shader.name);
				return qfalse;
			}

			tmi = &stage->bundle[0].texMods[stage->bundle[0].numTexMods];
			stage->bundle[0].numTexMods++;	
			
			ParseExpression(text, &tmi->sExp);
			ParseExpression(text, &tmi->tExp);
			
			tmi->type = TMOD_SCALE2;
		}
		// centerScale
		else if(!Q_stricmp(token, "centerScale"))
		{
			texModInfo_t   *tmi;
			
			if(stage->bundle[0].numTexMods == TR_MAX_TEXMODS)
			{
				ri.Error(ERR_DROP, "ERROR: too many tcMod stages in shader '%s'\n", shader.name);
				return qfalse;
			}

			tmi = &stage->bundle[0].texMods[stage->bundle[0].numTexMods];
			stage->bundle[0].numTexMods++;	
			
			ParseExpression(text, &tmi->sExp);
			ParseExpression(text, &tmi->tExp);
			
			tmi->type = TMOD_CENTERSCALE;
		}
		// shear
		else if(!Q_stricmp(token, "shear"))
		{
			texModInfo_t   *tmi;
			
			if(stage->bundle[0].numTexMods == TR_MAX_TEXMODS)
			{
				ri.Error(ERR_DROP, "ERROR: too many tcMod stages in shader '%s'\n", shader.name);
				return qfalse;
			}

			tmi = &stage->bundle[0].texMods[stage->bundle[0].numTexMods];
			stage->bundle[0].numTexMods++;	
			
			ParseExpression(text, &tmi->sExp);
			ParseExpression(text, &tmi->tExp);
			
			tmi->type = TMOD_SHEAR;
		}
		// rotate
		else if(!Q_stricmp(token, "rotate"))
		{
			texModInfo_t   *tmi;
			
			if(stage->bundle[0].numTexMods == TR_MAX_TEXMODS)
			{
				ri.Error(ERR_DROP, "ERROR: too many tcMod stages in shader '%s'\n", shader.name);
				return qfalse;
			}

			tmi = &stage->bundle[0].texMods[stage->bundle[0].numTexMods];
			stage->bundle[0].numTexMods++;	
			
			ParseExpression(text, &tmi->rExp);
			
			tmi->type = TMOD_ROTATE2;
		}
		// depthwrite
		else if(!Q_stricmp(token, "depthwrite"))
		{
			depthMaskBits = GLS_DEPTHMASK_TRUE;
			depthMaskExplicit = qtrue;
			continue;
		}
		// maskRed
		else if(!Q_stricmp(token, "maskRed"))
		{
			colorMaskBits |= GLS_REDMASK_FALSE;
		}
		// maskGreen
		else if(!Q_stricmp(token, "maskGreen"))
		{
			colorMaskBits |= GLS_GREENMASK_FALSE;
		}
		// maskBlue
		else if(!Q_stricmp(token, "maskBlue"))
		{
			colorMaskBits |= GLS_BLUEMASK_FALSE;
		}
		// maskAlpha
		else if(!Q_stricmp(token, "maskAlpha"))
		{
			colorMaskBits |= GLS_ALPHAMASK_FALSE;
		}
		// maskColor
		else if(!Q_stricmp(token, "maskColor"))
		{
			colorMaskBits |= GLS_REDMASK_FALSE | GLS_GREENMASK_FALSE | GLS_BLUEMASK_FALSE;
		}
		// maskDepth
		else if(!Q_stricmp(token, "maskDepth"))
		{
			depthMaskBits &= ~GLS_DEPTHMASK_TRUE;
			depthMaskExplicit = qfalse;
		}
		// specularExponent <arithmetic expression>
		else if(!Q_stricmp(token, "specularExponent"))
		{
			ParseExpression(text, &stage->specularExponentExp);
		}
		// refractionIndex <arithmetic expression>
		else if(!Q_stricmp(token, "refractionIndex"))
		{
			ParseExpression(text, &stage->refractionIndexExp);
		}
		// fresnelPower <arithmetic expression>
		else if(!Q_stricmp(token, "fresnelPower"))
		{
			ParseExpression(text, &stage->fresnelPowerExp);
		}
		// fresnelScale <arithmetic expression>
		else if(!Q_stricmp(token, "fresnelScale"))
		{
			ParseExpression(text, &stage->fresnelScaleExp);
		}
		// fresnelBias <arithmetic expression>
		else if(!Q_stricmp(token, "fresnelBias"))
		{
			ParseExpression(text, &stage->fresnelBiasExp);
		}
		// heightScale <arithmetic expression>
		else if(!Q_stricmp(token, "heightScale"))
		{
			ParseExpression(text, &stage->heightScaleExp);
		}
		// heightBias <arithmetic expression>
		else if(!Q_stricmp(token, "heightBias"))
		{
			ParseExpression(text, &stage->heightBiasExp);
		}
		// deformMagnitude <arithmetic expression>
		else if(!Q_stricmp(token, "deformMagnitude"))
		{
			ParseExpression(text, &stage->deformMagnitudeExp);
		}
		// blurMagnitude <arithmetic expression>
		else if(!Q_stricmp(token, "blurMagnitude"))
		{
			ParseExpression(text, &stage->blurMagnitudeExp);
		}
		// fragmentProgram <prog>
		else if(!Q_stricmp(token, "fragmentProgram"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: fragmentProgram keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		// vertexProgram <prog>
		else if(!Q_stricmp(token, "vertexProgram"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: vertexProgram keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		// program <prog>
		else if(!Q_stricmp(token, "program"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: program keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		// vertexParm <index> <exp0> [,exp1] [,exp2] [,exp3]
		else if(!Q_stricmp(token, "vertexParm"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: vertexParm keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		// fragmentMap <index> [options] <map>	
		else if(!Q_stricmp(token, "fragmentMap"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: fragmentMap keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		// megaTexture <mega>
		else if(!Q_stricmp(token, "megaTexture"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: megaTexture keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
		}
		else
		{
			ri.Printf(PRINT_WARNING, "WARNING: unknown parameter '%s' in shader '%s'\n", token, shader.name);
			Com_SkipRestOfLine(text);
			return qfalse;
		}
	}
	
	// parsing succeeded
	stage->active = qtrue;
	
	// load image
	if(loadMap && !LoadMap(stage, buffer))
	{
		return qfalse;	
	}

	// if cgen isn't explicitly specified, use either identity or identitylighting
	if(stage->rgbGen == CGEN_BAD)
	{
		if(blendSrcBits == 0 || blendSrcBits == GLS_SRCBLEND_ONE || blendSrcBits == GLS_SRCBLEND_SRC_ALPHA)
		{
			stage->rgbGen = CGEN_IDENTITY_LIGHTING;
		}
		else
		{
			stage->rgbGen = CGEN_IDENTITY;
		}
	}

	// implicitly assume that a GL_ONE GL_ZERO blend mask disables blending
	if((blendSrcBits == GLS_SRCBLEND_ONE) && (blendDstBits == GLS_DSTBLEND_ZERO))
	{
		blendDstBits = blendSrcBits = 0;
		depthMaskBits = GLS_DEPTHMASK_TRUE;
	}

	// decide which agens we can skip
	if(stage->alphaGen == CGEN_IDENTITY)
	{
		if(stage->rgbGen == CGEN_IDENTITY || stage->rgbGen == CGEN_LIGHTING_DIFFUSE)
		{
			stage->alphaGen = AGEN_SKIP;
		}
	}

	// compute state bits
	stage->stateBits = colorMaskBits | depthMaskBits | blendSrcBits | blendDstBits | atestBits | depthFuncBits;

	return qtrue;
}

/*
===============
ParseDeform

deformVertexes wave <spread> <waveform> <base> <amplitude> <phase> <frequency>
deformVertexes normal <frequency> <amplitude>
deformVertexes move <vector> <waveform> <base> <amplitude> <phase> <frequency>
deformVertexes bulge <bulgeWidth> <bulgeHeight> <bulgeSpeed>
deformVertexes projectionShadow
deformVertexes autoSprite
deformVertexes autoSprite2
deformVertexes text[0-7]
===============
*/
static void ParseDeform(char **text)
{
	char           *token;
	deformStage_t  *ds;

	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing deform parm in shader '%s'\n", shader.name);
		return;
	}

	if(shader.numDeforms == MAX_SHADER_DEFORMS)
	{
		ri.Printf(PRINT_WARNING, "WARNING: MAX_SHADER_DEFORMS in '%s'\n", shader.name);
		return;
	}

	ds = &shader.deforms[shader.numDeforms];
	shader.numDeforms++;

	if(!Q_stricmp(token, "projectionShadow")) 	 
	{ 	 
		ds->deformation = DEFORM_PROJECTION_SHADOW; 	 
		return; 	 
	}

	if(!Q_stricmp(token, "autosprite"))
	{
		ds->deformation = DEFORM_AUTOSPRITE;
		return;
	}

	if(!Q_stricmp(token, "autosprite2"))
	{
		ds->deformation = DEFORM_AUTOSPRITE2;
		return;
	}

	if(!Q_stricmpn(token, "text", 4))
	{
		int             n;

		n = token[4] - '0';
		if(n < 0 || n > 7)
		{
			n = 0;
		}
		ds->deformation = DEFORM_TEXT0 + n;
		return;
	}

	if(!Q_stricmp(token, "bulge"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing deformVertexes bulge parm in shader '%s'\n", shader.name);
			return;
		}
		ds->bulgeWidth = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing deformVertexes bulge parm in shader '%s'\n", shader.name);
			return;
		}
		ds->bulgeHeight = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing deformVertexes bulge parm in shader '%s'\n", shader.name);
			return;
		}
		ds->bulgeSpeed = atof(token);

		ds->deformation = DEFORM_BULGE;
		return;
	}

	if(!Q_stricmp(token, "wave"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing deformVertexes parm in shader '%s'\n", shader.name);
			return;
		}

		if(atof(token) != 0)
		{
			ds->deformationSpread = 1.0f / atof(token);
		}
		else
		{
			ds->deformationSpread = 100.0f;
			ri.Printf(PRINT_WARNING, "WARNING: illegal div value of 0 in deformVertexes command for shader '%s'\n", shader.name);
		}

		ParseWaveForm(text, &ds->deformationWave);
		ds->deformation = DEFORM_WAVE;
		return;
	}

	if(!Q_stricmp(token, "normal"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing deformVertexes parm in shader '%s'\n", shader.name);
			return;
		}
		ds->deformationWave.amplitude = atof(token);

		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing deformVertexes parm in shader '%s'\n", shader.name);
			return;
		}
		ds->deformationWave.frequency = atof(token);

		ds->deformation = DEFORM_NORMALS;
		return;
	}

	if(!Q_stricmp(token, "move"))
	{
		int             i;

		for(i = 0; i < 3; i++)
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0] == 0)
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing deformVertexes parm in shader '%s'\n", shader.name);
				return;
			}
			ds->moveVector[i] = atof(token);
		}

		ParseWaveForm(text, &ds->deformationWave);
		ds->deformation = DEFORM_MOVE;
		return;
	}
	
	if(!Q_stricmp(token, "sprite"))
	{
		ds->deformation = DEFORM_SPRITE;
		return;
	}
	
	if(!Q_stricmp(token, "flare"))
	{
		token = Com_ParseExt(text, qfalse);
		if(token[0] == 0)
		{
			ri.Printf(PRINT_WARNING, "WARNING: missing deformVertexes flare parm in shader '%s'\n", shader.name);
			return;
		}
		ds->flareSize = atof(token);
		return;
	}

	ri.Printf(PRINT_WARNING, "WARNING: unknown deformVertexes subtype '%s' found in shader '%s'\n", token, shader.name);
}


/*
===============
ParseSkyParms

skyParms <outerbox> <cloudheight> <innerbox>
===============
*/
static void ParseSkyParms(char **text)
{
	char           *token;
	static char    *suf[6] = { "rt", "bk", "lf", "ft", "up", "dn" };
	char            pathname[MAX_QPATH];
	char            prefix[MAX_QPATH];
	int             i;

	// outerbox
	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: 'skyParms' missing parameter in shader '%s'\n", shader.name);
		return;
	}
	if(strcmp(token, "-"))
	{
		Q_strncpyz(prefix, token, sizeof(prefix));
		for(i = 0; i < 6; i++)
		{
			Com_sprintf(pathname, sizeof(pathname), "%s_%s", prefix, suf[i]);
			shader.sky.outerbox[i] = R_FindImageFile(pathname, IF_NONE, FT_DEFAULT, WT_EDGE_CLAMP);
			if(!shader.sky.outerbox[i])
			{
				shader.sky.outerbox[i] = tr.defaultImage;
			}
		}
	}

	// cloudheight
	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: 'skyParms' missing parameter in shader '%s'\n", shader.name);
		return;
	}
	shader.sky.cloudHeight = atof(token);
	if(!shader.sky.cloudHeight)
	{
		shader.sky.cloudHeight = 512;
	}
	R_InitSkyTexCoords(shader.sky.cloudHeight);


	// innerbox
	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: 'skyParms' missing parameter in shader '%s'\n", shader.name);
		return;
	}
	if(strcmp(token, "-"))
	{
		Q_strncpyz(prefix, token, sizeof(prefix));
		for(i = 0; i < 6; i++)
		{
			Com_sprintf(pathname, sizeof(pathname), "%s_%s", prefix, suf[i]);
			shader.sky.innerbox[i] = R_FindImageFile(pathname, IF_NONE, FT_DEFAULT, WT_EDGE_CLAMP);
			if(!shader.sky.innerbox[i])
			{
				shader.sky.innerbox[i] = tr.defaultImage;
			}
		}
	}

	shader.isSky = qtrue;
}


/*
=================
ParseSort
=================
*/
static void ParseSort(char **text)
{
	char           *token;

	token = Com_ParseExt(text, qfalse);
	if(token[0] == 0)
	{
		ri.Printf(PRINT_WARNING, "WARNING: missing sort parameter in shader '%s'\n", shader.name);
		return;
	}

	if(!Q_stricmp(token, "portal") || !Q_stricmp(token, "subview"))
	{
		shader.sort = SS_PORTAL;
	}
	else if(!Q_stricmp(token, "sky") || !Q_stricmp(token, "environment"))
	{
		shader.sort = SS_ENVIRONMENT;
	}
	else if(!Q_stricmp(token, "opaque"))
	{
		shader.sort = SS_OPAQUE;
	}
	else if(!Q_stricmp(token, "decal"))
	{
		shader.sort = SS_DECAL;
	}
	else if(!Q_stricmp(token, "seeThrough"))
	{
		shader.sort = SS_SEE_THROUGH;
	}
	else if(!Q_stricmp(token, "banner"))
	{
		shader.sort = SS_BANNER;
	}
	else if(!Q_stricmp(token, "underwater"))
	{
		shader.sort = SS_UNDERWATER;
	}
	else if(!Q_stricmp(token, "far"))
	{
		shader.sort = SS_FAR;
	}
	else if(!Q_stricmp(token, "medium"))
	{
		shader.sort = SS_MEDIUM;
	}
	else if(!Q_stricmp(token, "close"))
	{
		shader.sort = SS_CLOSE;
	}
	else if(!Q_stricmp(token, "additive"))
	{
		shader.sort = SS_BLEND1;
	}
	else if(!Q_stricmp(token, "almostNearest"))
	{
		shader.sort = SS_ALMOST_NEAREST;
	}
	else if(!Q_stricmp(token, "nearest"))
	{
		shader.sort = SS_NEAREST;
	}
	else if(!Q_stricmp(token, "postProcess"))
	{
		shader.sort = SS_POST_PROCESS;
	}
	else
	{
		shader.sort = atof(token);
	}
}



// this table is also present in q3map

typedef struct
{
	char           *name;
	int             clearSolid, surfaceFlags, contents;
} infoParm_t;

infoParm_t	infoParms[] = {
	// server relevant contents
	{"water",			1,	0,	CONTENTS_WATER},
	{"slime",			1,	0,	CONTENTS_SLIME},		// mildly damaging
	{"lava",			1,	0,	CONTENTS_LAVA},			// very damaging
	{"playerclip",		1,	0,	CONTENTS_PLAYERCLIP},
	{"monsterclip",		1,	0,	CONTENTS_MONSTERCLIP},
	{"moveableclip",	1,	0,	0},						// FIXME
	{"ikclip",			1,	0,	0},						// FIXME
	{"nodrop",			1,	0,	CONTENTS_NODROP},		// don't drop items or leave bodies (death fog, lava, etc)
	{"nonsolid",		1,	SURF_NONSOLID,	0},			// clears the solid flag
	
	{"blood",			1,	0,	CONTENTS_WATER},

	// utility relevant attributes
	{"origin",			1,	0,	CONTENTS_ORIGIN},		// center of rotating brushes
	{"trans",			0,	0,	CONTENTS_TRANSLUCENT},	// don't eat contained surfaces
	{"translucent",		0,	0,	CONTENTS_TRANSLUCENT},	// don't eat contained surfaces
	{"detail",			0,	0,	CONTENTS_DETAIL},		// don't include in structural bsp
	{"structural",		0,	0,	CONTENTS_STRUCTURAL},	// force into structural bsp even if trnas
	{"areaportal",		1,	0,	CONTENTS_AREAPORTAL},	// divides areas
	{"clusterportal",	1,	0,  CONTENTS_CLUSTERPORTAL},	// for bots
	{"donotenter",  	1,  0,  CONTENTS_DONOTENTER},	// for bots
	{"botclip",			1,  0,  CONTENTS_BOTCLIP},		// for bots

	{"fog",				1,	0,	CONTENTS_FOG},			// carves surfaces entering
	{"sky",				0,	SURF_SKY,		0},			// emit light from an environment map
	{"lightfilter",		0,	SURF_LIGHTFILTER, 0},		// filter light going through it
	{"alphashadow",		0,	SURF_ALPHASHADOW, 0},		// test light on a per-pixel basis
	{"hint",			0,	SURF_HINT,		0},			// use as a primary splitter

	// server attributes
	{"slick",			0,	SURF_SLICK,		0},
	{"collision",		0,	SURF_NODRAW,	0},
	{"noimpact",		0,	SURF_NOIMPACT,	0},			// don't make impact explosions or marks
	{"nomarks",			0,	SURF_NOMARKS,	0},			// don't make impact marks, but still explode
	{"nooverlays",		0,	SURF_NOMARKS,	0},			// don't make impact marks, but still explode
	{"ladder",			0,	SURF_LADDER,	0},
	{"nodamage",		0,	SURF_NODAMAGE,	0},
	{"metalsteps",		0,	SURF_METALSTEPS,0},
	{"flesh",			0,	SURF_FLESH,		0},
	{"nosteps",			0,	SURF_NOSTEPS,	0},

	// drawsurf attributes
	{"nodraw",			0,	SURF_NODRAW,		0},		// don't generate a drawsurface (or a lightmap)
	{"pointlight",		0,	SURF_POINTLIGHT,	0},		// sample lighting at vertexes
	{"nolightmap",		0,	SURF_NOLIGHTMAP,	0},		// don't generate a lightmap
	{"nodlight",		0,	SURF_NODLIGHT,		0},		// don't ever add dynamic lights
	{"dust",			0,	SURF_DUST,			0},		// leave a dust trail when walking on this surface
	
	// unsupported Doom3 surface types for sound effects and blood splats
	{"metal",			0,	0,				0},
	{"stone",			0,	0,				0},
	{"wood",			0,	0,				0},
	{"cardboard",		0,	0,				0},
	{"liquid",			0,	0,				0},
	{"glass",			0,	0,				0},
	{"plastic",			0,	0,				0},
	{"ricochet",		0,	0,				0},
	{"surftype10",		0,	0,				0},
	{"surftype11",		0,	0,				0},
	{"surftype12",		0,	0,				0},
	{"surftype13",		0,	0,				0},
	{"surftype14",		0,	0,				0},
	{"surftype15",		0,	0,				0},
	
	// other unsupported Doom3 surface types
	{"trigger",			0,	0,				0},
	{"flashlight_trigger",0,0,				0},
	{"aassolid",		0,	0,				0},
	{"aasobstacle",		0,	0,				0},
	{"nullNormal",		0,	0,				0},
	{"discrete",		0,	0,				0},
	
};

/*
===============
ParseSurfaceParm

surfaceparm <name>
===============
*/
static qboolean SurfaceParm(const char *token)
{
	int             numInfoParms = sizeof(infoParms) / sizeof(infoParms[0]);
	int             i;
	
	for(i = 0; i < numInfoParms; i++)
	{
		if(!Q_stricmp(token, infoParms[i].name))
		{
			shader.surfaceFlags |= infoParms[i].surfaceFlags;
			shader.contentFlags |= infoParms[i].contents;
#if 0
			if(infoParms[i].clearSolid)
			{
				si->contents &= ~CONTENTS_SOLID;
			}
#endif
			return qtrue;
		}
	}
	
	return qfalse;
}

static void ParseSurfaceParm(char **text)
{
	char           *token;

	token = Com_ParseExt(text, qfalse);
	SurfaceParm(token);
}

static void ParseDiffuseMap(shaderStage_t * stage, char **text)
{
	char			buffer[1024] = "";
	
	stage->active = qtrue;
	stage->type = ST_DIFFUSEMAP;
	stage->rgbGen = CGEN_IDENTITY;
	stage->stateBits = GLS_DEFAULT;
	
	if(ParseMap(stage, text, buffer, sizeof(buffer)))
	{
		LoadMap(stage, buffer);
	}
}

static void ParseNormalMap(shaderStage_t * stage, char **text)
{
	char			buffer[1024] = "";
	
	stage->active = qtrue;
	stage->type = ST_NORMALMAP;
	stage->rgbGen = CGEN_IDENTITY;
	stage->stateBits = GLS_DEFAULT;
	
	if(ParseMap(stage, text, buffer, sizeof(buffer)))
	{
		LoadMap(stage, buffer);
	}
}

static void ParseSpecularMap(shaderStage_t * stage, char **text)
{
	char			buffer[1024] = "";
	
	stage->active = qtrue;
	stage->type = ST_SPECULARMAP;
	stage->rgbGen = CGEN_IDENTITY;
	stage->stateBits = GLS_DEFAULT;
	
	if(ParseMap(stage, text, buffer, sizeof(buffer)))
	{
		LoadMap(stage, buffer);
	}
}

static void ParseLightMap(shaderStage_t * stage, char **text)
{
	char			buffer[1024] = "";
	
	stage->active = qtrue;
	stage->type = ST_LIGHTMAP;
	stage->rgbGen = CGEN_IDENTITY;
	stage->stateBits = GLS_DEFAULT;
	
	if(ParseMap(stage, text, buffer, sizeof(buffer)))
	{
		LoadMap(stage, buffer);
	}
}

static void ParseLightFalloffImage(shaderStage_t * stage, char **text)
{
	char			buffer[1024] = "";
	
	stage->active = qtrue;
	stage->type = ST_ATTENUATIONMAP_Z;
	stage->rgbGen = CGEN_IDENTITY;
	stage->stateBits = GLS_DEFAULT;
	stage->overrideWrapType = qtrue;
	stage->wrapType = WT_EDGE_CLAMP;
	
	if(ParseMap(stage, text, buffer, sizeof(buffer)))
	{
		LoadMap(stage, buffer);
	}
}

/*
====================
FindShaderInShaderText

Scans the combined text description of all the shader template files for
the given guide name.

return NULL if not found

If found, it will return a valid template
=====================
*/
static char    *FindGuideInGuideText(const char *guideName)
{
	char           *token, *p;

	int             i, hash;

	hash = generateHashValue(guideName, MAX_GUIDETEXT_HASH);

	for(i = 0; guideTextHashTable[hash][i]; i++)
	{
		p = guideTextHashTable[hash][i];
		token = Com_ParseExt(&p, qtrue);
		if(!Q_stricmp(token, guideName))
		{
			//ri.Printf(PRINT_ALL, "found guide '%s' by hashing\n", guideName);
			return p;
		}
	}

	p = s_guideText;

	if(!p)
	{
		return NULL;
	}

	// look for label
	while(1)
	{
		token = Com_ParseExt(&p, qtrue);
		if(token[0] == 0)
		{
			break;
		}

		if(Q_stricmp(token, "guide") && Q_stricmp(token, "inlineGuide"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: expected guide or inlineGuide found '%s'\n", token);
			break;
		}

		// parse guide name
		token = Com_ParseExt(&p, qtrue);

		if(!Q_stricmp(token, guideName))
		{
			ri.Printf(PRINT_ALL, "found guide '%s' by linear search\n", guideName);
			return p;
		}

		// skip parameters
		token = Com_ParseExt(&p, qtrue);
		if(Q_stricmp(token, "("))
		{
			ri.Printf(PRINT_WARNING, "WARNING: expected ( found '%s'\n", token);
			break;
		}

		while(1)
		{
			token = Com_ParseExt(&p, qtrue);

			if(!token[0])
				break;

			if(!Q_stricmp(token, ")"))
				break;
		}

		if(Q_stricmp(token, ")"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: expected ) found '%s'\n", token);
			break;
		}

		// skip guide body
		Com_SkipBracedSection(&p);
	}

	return NULL;
}


/*
=================
CreateShaderByGuide
=================
*/
#define MAX_GUIDE_PARAMETERS 16
static char    *CreateShaderByGuide(const char *guideName, char *shaderText)
{
	int             i;
	char           *guideText;
	char           *token;
	const char     *p;
	static char     buffer[4096];
	char            name[MAX_QPATH];
	int             numGuideParms;
	char            guideParms[MAX_GUIDE_PARAMETERS][MAX_QPATH];
	int             numShaderParms;
	char            shaderParms[MAX_GUIDE_PARAMETERS][MAX_QPATH];
	
	Com_Memset(buffer, 0, sizeof(buffer));
	Com_Memset(guideParms, 0, sizeof(guideParms));
	Com_Memset(shaderParms, 0, sizeof(shaderParms));

	// attempt to define shader from an explicit parameter file
	guideText = FindGuideInGuideText(guideName);
	if(guideText)
	{
		shader.createdByGuide = qtrue;
		
		// parse guide parameters
		numGuideParms = 0;
		
		token = Com_ParseExt(&guideText, qtrue);
		if(Q_stricmp(token, "("))
		{
			ri.Printf(PRINT_WARNING, "WARNING: expected ( found '%s'\n", token);
			return NULL;
		}

		while(1)
		{
			token = Com_ParseExt(&guideText, qtrue);

			if(!token[0])
				break;

			if(!Q_stricmp(token, ")"))
				break;
				
			if(numGuideParms >= MAX_GUIDE_PARAMETERS -1)
			{
				ri.Printf(PRINT_ALL, "WARNING: more than %i guide parameters are not allowed\n", MAX_GUIDE_PARAMETERS);
				return NULL;
			}
			
			//ri.Printf(PRINT_ALL, "guide parameter %i = '%s'\n", numGuideParms, token);
			
			Q_strncpyz(guideParms[numGuideParms], token, MAX_QPATH);
			numGuideParms++;
		}

		if(Q_stricmp(token, ")"))
		{
			ri.Printf(PRINT_ALL, "WARNING: expected ) found '%s'\n", token);
			return NULL;
		}
		
		// parse shader parameters
		numShaderParms = 0;
		
		token = Com_ParseExt(&shaderText, qtrue);
		if(Q_stricmp(token, "("))
		{
			ri.Printf(PRINT_ALL, "WARNING: expected ( found '%s'\n", token);
			return NULL;
		}

		while(1)
		{
			token = Com_ParseExt(&shaderText, qtrue);

			if(!token[0])
				break;

			if(!Q_stricmp(token, ")"))
				break;
				
			if(numShaderParms >= MAX_GUIDE_PARAMETERS -1)
			{
				ri.Printf(PRINT_ALL, "WARNING: more than %i guide parameters are not allowed\n", MAX_GUIDE_PARAMETERS);
				return NULL;
			}
			
			//ri.Printf(PRINT_ALL, "shader parameter %i = '%s'\n", numShaderParms, token);
			
			Q_strncpyz(shaderParms[numShaderParms], token, MAX_QPATH);
			numShaderParms++;
		}

		if(Q_stricmp(token, ")"))
		{
			ri.Printf(PRINT_ALL, "WARNING: expected ) found '%s'\n", token);
			return NULL;
		}
		
		if(numGuideParms != numShaderParms)
		{
			ri.Printf(PRINT_WARNING, "WARNING: %i numGuideParameters != %i numShaderParameters\n",
										numGuideParms, numShaderParms);
			return NULL;
		}
		
		#if 0
		for(i = 0; i < numGuideParms; i++)
		{
			ri.Printf(PRINT_ALL, "guide parameter '%s' = '%s'\n", guideParms[i], shaderParms[i]);
		}
		#endif
		
		token = Com_ParseExt(&guideText, qtrue);
		if(Q_stricmp(token, "{"))
		{
			ri.Printf(PRINT_ALL, "WARNING: expected { found '%s'\n", token);
			return NULL;
		}
		
		// create buffer
		while(1)
		{
			// begin new line
			token = Com_ParseExt(&guideText, qtrue);
			
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: no concluding '}' in guide %s\n", guideName);
				return NULL;
			}
			
			// end of guide definition
			if(token[0] == '}')
			{
				break;
			}
			
			Q_strncpyz(name, token, sizeof(name));
			
			#if 0
			// adjust name by guide parameters if necessary
			for(i = 0; i < numGuideParms; i++)
			{
				if((p = Q_stristr(name, (const char *)guideParms)))
				{
					//ri.Printf(PRINT_ALL, "guide parameter '%s' = '%s'\n", guideParms[i], shaderParms[i]);
					
					Q_strreplace(name, sizeof(name), guideParms[i], shaderParms[i]);
				}
			}
			#endif
				
			Q_strcat(buffer, sizeof(buffer), name);
			Q_strcat(buffer, sizeof(buffer), " ");
			
			// parse rest of line
			while(1)
			{
				token = Com_ParseExt(&guideText, qfalse);
				
				if(!token[0])
				{
					// end of line
					break;	
				}
				
				Q_strncpyz(name, token, sizeof(name));
			
				// adjust name by guide parameters if necessary
				for(i = 0; i < numGuideParms; i++)
				{
					if((p = Q_stristr(name, (const char *)guideParms)))
					{
						//ri.Printf(PRINT_ALL, "guide parameter '%s' = '%s'\n", guideParms[i], shaderParms[i]);
						
						Q_strreplace(name, sizeof(name), guideParms[i], shaderParms[i]);
					}
				}
				
				Q_strcat(buffer, sizeof(buffer), name);
				Q_strcat(buffer, sizeof(buffer), " ");
			}
				
			Q_strcat(buffer, sizeof(buffer), "\n");
		}
		
		if(Q_stricmp(token, "}"))
		{
			ri.Printf(PRINT_ALL, "WARNING: expected } found '%s'\n", token);
			return NULL;
		}
		
		Q_strcat(buffer, sizeof(buffer), "}");
		
		ri.Printf(PRINT_ALL, "----- '%s' -----\n%s\n----------\n", shader.name, buffer);
		
		return buffer;
	}

	return NULL;
}

/*
=================
ParseShader

The current text pointer is at the explicit text definition of the
shader.  Parse it into the global shader variable.  Later functions
will optimize it.
=================
*/
static qboolean ParseShader(char *_text)
{
	char          **text;
	char           *token;
	int             s;

	s = 0;
	shader.explicitlyDefined = qtrue;
	
	text = &_text;

	token = Com_ParseExt(text, qtrue);
	if(token[0] != '{')
	{
		if(!(_text = CreateShaderByGuide(token, _text)))
		{
			ri.Printf(PRINT_WARNING, "WARNING: couldn't create shader '%s' by template '%s'\n", shader.name, token);
			//ri.Printf(PRINT_WARNING, "WARNING: expecting '{', found '%s' instead in shader '%s'\n", token, shader.name);
			return qfalse;
		}
		else
		{
			text = &_text;
		}
	}

	while(1)
	{
		token = Com_ParseExt(text, qtrue);
		if(!token[0])
		{
			ri.Printf(PRINT_WARNING, "WARNING: no concluding '}' in shader %s\n", shader.name);
			return qfalse;
		}

		// end of shader definition
		if(token[0] == '}')
		{
			break;
		}
		// stage definition
		else if(token[0] == '{')
		{
			if(s >= (MAX_SHADER_STAGES -1))
			{
				ri.Printf(PRINT_WARNING, "WARNING: too many stages in shader %s\n", shader.name);
				return qfalse;	
			}
			
			if(!ParseStage(&stages[s], text))
			{
				return qfalse;
			}
			stages[s].active = qtrue;
			s++;
			continue;
		}
		// skip stuff that only the QuakeEdRadient needs
		else if(!Q_stricmpn(token, "qer", 3))
		{
			Com_SkipRestOfLine(text);
			continue;
		}
		// skip description
		else if(!Q_stricmp(token, "description"))
		{
			Com_SkipRestOfLine(text);
			continue;
		}
		// skip renderbump
		else if(!Q_stricmp(token, "renderbump"))
		{
			Com_SkipRestOfLine(text);
			continue;
		}
		// skip unsmoothedTangents
		else if(!Q_stricmp(token, "unsmoothedTangents"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: unsmoothedTangents keyword not supported in shader '%s'\n", shader.name);
			continue;
		}
		// skip guiSurf
		else if(!Q_stricmp(token, "guiSurf"))
		{
			Com_SkipRestOfLine(text);
			continue;
		}
		// skip decalInfo
		else if(!Q_stricmp(token, "decalInfo"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: decalInfo keyword not supported in shader '%s'\n", shader.name);
			Com_SkipRestOfLine(text);
			continue;
		}
		// sun parms
		else if(!Q_stricmp(token, "q3map_sun"))
		{
			float           a, b;

			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for 'q3map_sun' keyword in shader '%s'\n", shader.name);
				continue;
			}
			tr.sunLight[0] = atof(token);
			
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for 'q3map_sun' keyword in shader '%s'\n", shader.name);
				continue;
			}
			tr.sunLight[1] = atof(token);
			
			
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for 'q3map_sun' keyword in shader '%s'\n", shader.name);
				continue;
			}
			tr.sunLight[2] = atof(token);

			VectorNormalize(tr.sunLight);

			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for 'q3map_sun' keyword in shader '%s'\n", shader.name);
				continue;
			}
			a = atof(token);
			VectorScale(tr.sunLight, a, tr.sunLight);

			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for 'q3map_sun' keyword in shader '%s'\n", shader.name);
				continue;
			}
			a = atof(token);
			a = a / 180 * M_PI;

			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for 'q3map_sun' keyword in shader '%s'\n", shader.name);
				continue;
			}
			b = atof(token);
			b = b / 180 * M_PI;

			tr.sunDirection[0] = cos(a) * cos(b);
			tr.sunDirection[1] = sin(a) * cos(b);
			tr.sunDirection[2] = sin(b);
			continue;
		}
		// noShadows
		else if(!Q_stricmp(token, "noShadows"))
		{
			shader.noShadows = qtrue;
			continue;
		}
		// noSelfShadow
		else if(!Q_stricmp(token, "noSelfShadow"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: noSelfShadow keyword not supported in shader '%s'\n", shader.name);
			continue;	
		}
		// forceShadows
		else if(!Q_stricmp(token, "forceShadows"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: forceShadows keyword not supported in shader '%s'\n", shader.name);
			continue;
		}
		// forceOverlays
		else if(!Q_stricmp(token, "forceOverlays"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: forceOverlays keyword not supported in shader '%s'\n", shader.name);
			continue;
		}
		// noPortalFog
		else if(!Q_stricmp(token, "noPortalFog"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: noPortalFog keyword not supported in shader '%s'\n", shader.name);
			continue;
		}
		// fogLight
		else if(!Q_stricmp(token, "fogLight"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: fogLight keyword not supported in shader '%s'\n", shader.name);
			shader.fogLight = qtrue;
			continue;
		}
		// blendLight
		else if(!Q_stricmp(token, "blendLight"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: blendLight keyword not supported in shader '%s'\n", shader.name);
			shader.blendLight = qtrue;
			continue;
		}
		// ambientLight
		else if(!Q_stricmp(token, "ambientLight"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: ambientLight keyword not supported in shader '%s'\n", shader.name);
			shader.ambientLight = qtrue;
			continue;
		}
		// translucent
		else if(!Q_stricmp(token, "translucent"))
		{
			shader.translucent = qtrue;
			continue;
		}
		// forceOpaque
		else if(!Q_stricmp(token, "forceOpaque"))
		{
			shader.forceOpaque = qtrue;
			continue;
		}
		// forceSolid
		else if(!Q_stricmp(token, "forceSolid") || !Q_stricmp(token, "solid"))
		{
			continue;
		}
		else if(!Q_stricmp(token, "deformVertexes") || !Q_stricmp(token, "deform"))
		{
			ParseDeform(text);
			continue;
		}
		else if(!Q_stricmp(token, "tesssize"))
		{
			Com_SkipRestOfLine(text);
			continue;
		}
		// skip noFragment
		if(!Q_stricmp(token, "noFragment"))
		{
			continue;
		}
		else if(!Q_stricmp(token, "clampTime"))
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0])
			{
				shader.clampTime = atof(token);
			}
		}
		// skip stuff that only the q3map needs
		else if(!Q_stricmpn(token, "q3map", 5))
		{
			Com_SkipRestOfLine(text);
			continue;
		}
		// skip stuff that only q3map or the server needs
		else if(!Q_stricmp(token, "surfaceParm"))
		{
			ParseSurfaceParm(text);
			continue;
		}
		// no mip maps
		else if(!Q_stricmp(token, "nomipmaps"))
		{
			shader.filterType = FT_LINEAR;
			shader.noPicMip = qtrue;
			continue;
		}
		// no picmip adjustment
		else if(!Q_stricmp(token, "nopicmip"))
		{
			shader.noPicMip = qtrue;
			continue;
		}
		// polygonOffset
		else if(!Q_stricmp(token, "polygonOffset"))
		{
			shader.polygonOffset = qtrue;
			
			token = Com_ParseExt(text, qfalse);
			if(token[0])
			{
				shader.polygonOffsetValue = atof(token);
			}
			continue;
		}
		// entityMergable, allowing sprite surfaces from multiple entities
		// to be merged into one batch.  This is a savings for smoke
		// puffs and blood, but can't be used for anything where the
		// shader calcs (not the surface function) reference the entity color or scroll
		else if(!Q_stricmp(token, "entityMergable"))
		{
			shader.entityMergable = qtrue;
			continue;
		}
		// fogParms
		else if(!Q_stricmp(token, "fogParms"))
		{
			if(!ParseVector(text, 3, shader.fogParms.color))
			{
				return qfalse;
			}

			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for 'fogParms' keyword in shader '%s'\n", shader.name);
				continue;
			}
			shader.fogParms.depthForOpaque = atof(token);

			// skip any old gradient directions
			Com_SkipRestOfLine(text);
			continue;
		}
		// noFog
		else if(!Q_stricmp(token, "noFog"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: noFog keyword not supported in shader '%s'\n", shader.name);
			continue;
		}
		// portal
		else if(!Q_stricmp(token, "portal") || !Q_stricmp(token, "mirror"))
		{
			shader.sort = SS_PORTAL;
			continue;
		}
		// skyparms <cloudheight> <outerbox> <innerbox>
		else if(!Q_stricmp(token, "skyparms"))
		{
			ParseSkyParms(text);
			continue;
		}
		// light <value> determines flaring in q3map, not needed here
		else if(!Q_stricmp(token, "light"))
		{
			token = Com_ParseExt(text, qfalse);
			continue;
		}
		// cull <face>
		else if(!Q_stricmp(token, "cull"))
		{
			token = Com_ParseExt(text, qfalse);
			if(token[0] == 0)
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing cull parms in shader '%s'\n", shader.name);
				continue;
			}

			if(!Q_stricmp(token, "none") || !Q_stricmp(token, "twoSided") || !Q_stricmp(token, "disable"))
			{
				shader.cullType = CT_TWO_SIDED;
			}
			else if(!Q_stricmp(token, "back") || !Q_stricmp(token, "backside") ||
					!Q_stricmp(token, "backsided"))
			{
				shader.cullType = CT_BACK_SIDED;
			}
			else
			{
				ri.Printf(PRINT_WARNING, "WARNING: invalid cull parm '%s' in shader '%s'\n", token,
						  shader.name);
			}
			continue;
		}
		// twoSided
		else if(!Q_stricmp(token, "twoSided"))
		{
			shader.cullType = CT_TWO_SIDED;
			continue;
		}
		// backSided
		else if(!Q_stricmp(token, "backSided"))
		{
			shader.cullType = CT_BACK_SIDED;
			continue;
		}
		// clamp
		else if(!Q_stricmp(token, "clamp"))
		{
			shader.wrapType = WT_CLAMP;
			continue;
		}
		// edgeClamp
		else if(!Q_stricmp(token, "edgeClamp"))
		{
			shader.wrapType = WT_EDGE_CLAMP;
			continue;
		}
		// zeroClamp
		else if(!Q_stricmp(token, "zeroclamp"))
		{
			shader.wrapType = WT_ZERO_CLAMP;
			continue;
		}
		// alphaZeroClamp
		else if(!Q_stricmp(token, "alphaZeroClamp"))
		{
			shader.wrapType = WT_ALPHA_ZERO_CLAMP;
			continue;
		}
		// sort
		else if(!Q_stricmp(token, "sort"))
		{
			ParseSort(text);
			continue;
		}
		// spectrum
		else if(!Q_stricmp(token, "spectrum"))
		{
			ri.Printf(PRINT_WARNING, "WARNING: spectrum keyword not supported in shader '%s'\n", shader.name);
			
			token = Com_ParseExt(text, qfalse);
			if(!token[0])
			{
				ri.Printf(PRINT_WARNING, "WARNING: missing parm for 'spectrum' keyword in shader '%s'\n", shader.name);
				continue;
			}
			shader.spectrum = qtrue;
			shader.spectrumValue = atoi(token);
			continue;
		}
		// diffuseMap <image>
		else if(!Q_stricmp(token, "diffuseMap"))
		{
			ParseDiffuseMap(&stages[s], text);
			s++;
			continue;
		}
		// normalMap <image>
		else if(!Q_stricmp(token, "normalMap") || !Q_stricmp(token, "bumpMap"))
		{
			ParseNormalMap(&stages[s], text);
			s++;
			continue;
		}
		// specularMap <image>
		else if(!Q_stricmp(token, "specularMap"))
		{
			ParseSpecularMap(&stages[s], text);
			s++;
			continue;
		}
		// lightMap <image>
		else if(!Q_stricmp(token, "lightMap"))
		{
			ParseLightMap(&stages[s], text);
			s++;
			continue;
		}
		// lightFalloffImage <image>
		else if(!Q_stricmp(token, "lightFalloffImage"))
		{
			ParseLightFalloffImage(&stages[s], text);
			s++;
			continue;
		}
		// DECAL_MACRO
		else if(!Q_stricmp(token, "DECAL_MACRO"))
		{
			shader.polygonOffset = qtrue;
			shader.polygonOffsetValue = 1;
			shader.sort = SS_DECAL;
			SurfaceParm("discrete");
			SurfaceParm("noShadows");
			continue;
		}
		else if(SurfaceParm(token))
		{
			continue;
		}
		else
		{
			ri.Printf(PRINT_WARNING, "WARNING: unknown general shader parameter '%s' in '%s'\n", token, shader.name);
			return qfalse;
		}
	}

	// ignore shaders that don't have any stages, unless it is a sky or fog
	if(s == 0 && !shader.forceOpaque && !shader.isSky && !(shader.contentFlags & CONTENTS_FOG))
	{
		return qfalse;
	}

	return qtrue;
}

/*
========================================================================================

SHADER OPTIMIZATION AND FOGGING

========================================================================================
*/

/*
typedef struct
{
	int             blendA;
	int             blendB;

	int             multitextureEnv;
	int             multitextureBlend;
} collapse_t;

static collapse_t collapse[] = {
	{0, GLS_DSTBLEND_SRC_COLOR | GLS_SRCBLEND_ZERO,
	 GL_MODULATE, 0},

	{0, GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR,
	 GL_MODULATE, 0},

	{GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR, GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR,
	 GL_MODULATE, GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR},

	{GLS_DSTBLEND_SRC_COLOR | GLS_SRCBLEND_ZERO, GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR,
	 GL_MODULATE, GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR},

	{GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR, GLS_DSTBLEND_SRC_COLOR | GLS_SRCBLEND_ZERO,
	 GL_MODULATE, GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR},

	{GLS_DSTBLEND_SRC_COLOR | GLS_SRCBLEND_ZERO, GLS_DSTBLEND_SRC_COLOR | GLS_SRCBLEND_ZERO,
	 GL_MODULATE, GLS_DSTBLEND_ZERO | GLS_SRCBLEND_DST_COLOR},

	{0, GLS_DSTBLEND_ONE | GLS_SRCBLEND_ONE,
	 GL_ADD, 0},

	{GLS_DSTBLEND_ONE | GLS_SRCBLEND_ONE, GLS_DSTBLEND_ONE | GLS_SRCBLEND_ONE,
	 GL_ADD, GLS_DSTBLEND_ONE | GLS_SRCBLEND_ONE},
#if 0
	{0, GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA | GLS_SRCBLEND_SRC_ALPHA,
	 GL_DECAL, 0},
#endif
	{-1}
};
*/

/*
================
CollapseMultitexture

Attempt to combine two color stages into a single multitexture stage
FIXME: I think modulated add + modulated add collapses incorrectly
=================
*/
static void CollapseStages(void)
{
//	int             abits, bbits;
	int             i, j;
//	textureBundle_t tmpBundle;
	
	qboolean		hasDiffuseStage;
	qboolean		hasNormalStage;
	qboolean		hasSpecularStage;
	qboolean		hasLightStage;
	qboolean		hasColorStage;
	
	shaderStage_t	tmpDiffuseStage;
	shaderStage_t	tmpNormalStage;
	shaderStage_t	tmpSpecularStage;
	shaderStage_t	tmpLightStage;
	
	int				numStages = 0;
	shaderStage_t	tmpStages[MAX_SHADER_STAGES];
	shader_t		tmpShader;
	
	if(!qglActiveTextureARB || !r_collapseStages->integer)
	{
		return;
	}
	
	//ri.Printf(PRINT_ALL, "...collapsing '%s'\n", shader.name);
	
	Com_Memcpy(&tmpStages[0], &stages[0], sizeof(stages));
	Com_Memcpy(&tmpShader, &shader, sizeof(shader));

	for(j = 0; j < MAX_SHADER_STAGES; j++)
	{
		hasDiffuseStage = qfalse;
		hasNormalStage = qfalse;
		hasSpecularStage = qfalse;
		hasLightStage = qfalse;
		hasColorStage = qfalse;

		Com_Memset(&tmpDiffuseStage, 0, sizeof(shaderStage_t));
		Com_Memset(&tmpNormalStage, 0, sizeof(shaderStage_t));
		Com_Memset(&tmpSpecularStage, 0, sizeof(shaderStage_t));
		Com_Memset(&tmpLightStage, 0, sizeof(shaderStage_t));

		if(!stages[j].active)
			continue;
		
		if(	stages[j].type == ST_COLORMAP ||
			stages[j].type == ST_HEATHAZEMAP ||
			stages[j].type == ST_GLOWMAP ||
			stages[j].type == ST_REFLECTIONMAP ||
			stages[j].type == ST_REFRACTIONMAP ||
			stages[j].type == ST_DISPERSIONMAP ||
			stages[j].type == ST_SKYBOXMAP ||
			stages[j].type == ST_LIQUIDMAP ||
			stages[j].type == ST_ATTENUATIONMAP_XY ||
			stages[j].type == ST_ATTENUATIONMAP_Z)
		{
			// only merge lighting relevant stages
			tmpStages[numStages] = stages[j];
			numStages++;
			continue;
		}
		
		for(i = 0; i < 4; i++)
		{
			if((j+i) >= MAX_SHADER_STAGES)
				continue;
			
			if(!stages[j+i].active)
				continue;
			
			if(stages[j+i].type == ST_DIFFUSEMAP && !hasDiffuseStage)
			{
				hasDiffuseStage = qtrue;
				tmpDiffuseStage = stages[j+i];
			}
			else if(stages[j+i].type == ST_NORMALMAP && !hasNormalStage)
			{
				hasNormalStage = qtrue;
				tmpNormalStage = stages[j+i];
			}
			else if(stages[j+i].type == ST_SPECULARMAP && !hasSpecularStage)
			{
				hasSpecularStage = qtrue;
				tmpSpecularStage = stages[j+i];
			}
			else if(stages[j+i].type == ST_LIGHTMAP && !hasLightStage)
			{
				hasLightStage = qtrue;
				tmpLightStage = stages[j+i];
			}
			else if(stages[j+i].type == ST_COLORMAP && !hasColorStage)
			{
				hasColorStage = qtrue;
			}
		}
	
		
		// NOTE: Tr3B - merge as many stages as possible
		
		// try to merge diffuse/normal/specular/light
		if(	hasDiffuseStage		&&
			hasNormalStage		&&
			hasSpecularStage	&&
			hasLightStage
		)
		{
			//ri.Printf(PRINT_ALL, "lighting_DBS_radiosity\n");
			
			tmpShader.collapseType = COLLAPSE_lighting_DBS_radiosity;
			
			tmpStages[numStages] = tmpDiffuseStage;
			tmpStages[numStages].type = ST_COLLAPSE_lighting_DBS_radiosity;
			
			tmpStages[numStages].bundle[TB_NORMALMAP] = tmpNormalStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_NORMALMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_NORMALMAP].tcGen = TCGEN_SKIP;
			}
			
			tmpStages[numStages].bundle[TB_SPECULARMAP] = tmpSpecularStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_SPECULARMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_SPECULARMAP].tcGen = TCGEN_SKIP;
			}
			
			tmpStages[numStages].bundle[TB_LIGHTMAP] = tmpLightStage.bundle[0];
			
			tmpStages[numStages].stateBits &= ~(GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS);
			
			numStages++;
			j += 3;
			continue;
		}
		// try to merge diffuse/bump/light
		else if(hasDiffuseStage		&&
				hasNormalStage		&&
				hasLightStage
		)
		{
			//ri.Printf(PRINT_ALL, "lighting_DB_radiosity\n");
			
			tmpShader.collapseType = COLLAPSE_lighting_DB_radiosity;
			
			tmpStages[numStages] = tmpDiffuseStage;
			tmpStages[numStages].type = ST_COLLAPSE_lighting_DB_radiosity;
			
			tmpStages[numStages].bundle[TB_NORMALMAP] = tmpNormalStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_NORMALMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_NORMALMAP].tcGen = TCGEN_SKIP;
			}
			
			tmpStages[numStages].bundle[TB_LIGHTMAP] = tmpLightStage.bundle[0];
			
			tmpStages[numStages].stateBits &= ~(GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS);
	
			numStages++;
			j += 2;
			continue;
		}
		else if(hasDiffuseStage		&&
				hasSpecularStage	&&
				hasLightStage
		)
		{
			//ri.Printf(PRINT_ALL, "lighting_D_radiosity\n");
			
			tmpShader.collapseType = COLLAPSE_lighting_D_radiosity;
			
			tmpStages[numStages] = tmpDiffuseStage;
			tmpStages[numStages].type = ST_COLLAPSE_lighting_D_radiosity;
			
			tmpStages[numStages].bundle[TB_LIGHTMAP] = tmpLightStage.bundle[0];
			
			tmpStages[numStages].stateBits &= ~(GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS);
	
			numStages++;
			j += 2;
			continue;
		}
		// try to merge diffuse/light
		else if(hasDiffuseStage		&&
				hasLightStage
		)
		{
			//ri.Printf(PRINT_ALL, "lighting_D_radiosity\n");
			
			tmpShader.collapseType = COLLAPSE_lighting_D_radiosity;
			
			tmpStages[numStages] = tmpDiffuseStage;
			tmpStages[numStages].type = ST_COLLAPSE_lighting_D_radiosity;
			
			tmpStages[numStages].bundle[TB_LIGHTMAP] = tmpLightStage.bundle[0];
			
			tmpStages[numStages].stateBits &= ~(GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS);
	
			numStages++;
			j += 1;
			continue;
		}
		// try to merge diffuse/normal/specular
		else if(hasDiffuseStage		&&
				hasNormalStage		&&
				hasSpecularStage	&&
				tmpDiffuseStage.rgbGen == CGEN_LIGHTING_DIFFUSE
		)
		{
			//ri.Printf(PRINT_ALL, "lighting_DBS_direct\n");
			
			tmpShader.collapseType = COLLAPSE_lighting_DBS_direct;
			
			tmpStages[numStages] = tmpDiffuseStage;
			tmpStages[numStages].type = ST_COLLAPSE_lighting_DBS_direct;
			
			tmpStages[numStages].bundle[TB_NORMALMAP] = tmpNormalStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_NORMALMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_NORMALMAP].tcGen = TCGEN_SKIP;
			}
			
			tmpStages[numStages].bundle[TB_SPECULARMAP] = tmpSpecularStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_SPECULARMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_SPECULARMAP].tcGen = TCGEN_SKIP;
			}
			
			numStages++;
			j += 2;
			continue;
		}
		// try to merge diffuse/normal
		else if(hasDiffuseStage		&&
				hasNormalStage		&&
				tmpDiffuseStage.rgbGen == CGEN_LIGHTING_DIFFUSE
		)
		{
			//ri.Printf(PRINT_ALL, "lighting_DB_direct\n");
			
			tmpShader.collapseType = COLLAPSE_lighting_DB_direct;
			
			tmpStages[numStages] = tmpDiffuseStage;
			tmpStages[numStages].type = ST_COLLAPSE_lighting_DB_direct;
			
			tmpStages[numStages].bundle[TB_NORMALMAP] = tmpNormalStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_NORMALMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_NORMALMAP].tcGen = TCGEN_SKIP;
			}
	
			numStages++;
			j += 1;
			continue;
		}
		// try to merge diffuse/normal/specular
		else if(hasDiffuseStage		&&
				hasNormalStage		&&
				hasSpecularStage
		)
		{
			//ri.Printf(PRINT_ALL, "lighting_DBS_generic\n");
			
			tmpShader.collapseType = COLLAPSE_lighting_DBS_generic;
			
			tmpStages[numStages] = tmpDiffuseStage;
			tmpStages[numStages].type = ST_COLLAPSE_lighting_DBS_generic;
			
			tmpStages[numStages].bundle[TB_NORMALMAP] = tmpNormalStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_NORMALMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_NORMALMAP].tcGen = TCGEN_SKIP;
			}
			
			tmpStages[numStages].bundle[TB_SPECULARMAP] = tmpSpecularStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_SPECULARMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_SPECULARMAP].tcGen = TCGEN_SKIP;
			}
			
			numStages++;
			j += 2;
			continue;
		}
		// try to merge diffuse/normal
		else if(hasDiffuseStage		&&
				hasNormalStage
		)
		{
			//ri.Printf(PRINT_ALL, "lighting_DB_generic\n");
			
			tmpShader.collapseType = COLLAPSE_lighting_DB_generic;
			
			tmpStages[numStages] = tmpDiffuseStage;
			tmpStages[numStages].type = ST_COLLAPSE_lighting_DB_generic;
			
			tmpStages[numStages].bundle[TB_NORMALMAP] = tmpNormalStage.bundle[0];
			if(!tmpStages[numStages].bundle[TB_NORMALMAP].numTexMods)
			{
				tmpStages[numStages].bundle[TB_NORMALMAP].tcGen = TCGEN_SKIP;
			}
	
			numStages++;
			j += 1;
			continue;
		}
		// if there was no merge option just copy stage
		else
		{
			tmpStages[numStages] = stages[j];
			numStages++;
		}
		/*
		// try to merge color/color
		else if(stages[0].active					&&
				stages[0].type == ST_COLORMAP		&&
				stages[1].active					&&
				stages[1].type == ST_COLORMAP
		)
		{
			abits = stages[0].stateBits;
			bbits = stages[1].stateBits;
	
			// make sure that both stages have identical state other than blend modes
			if((abits & ~(GLS_DSTBLEND_BITS | GLS_SRCBLEND_BITS | GLS_DEPTHMASK_TRUE)) !=
			   (bbits & ~(GLS_DSTBLEND_BITS | GLS_SRCBLEND_BITS | GLS_DEPTHMASK_TRUE)))
			{
				return;
			}
	
			abits &= (GLS_DSTBLEND_BITS | GLS_SRCBLEND_BITS);
			bbits &= (GLS_DSTBLEND_BITS | GLS_SRCBLEND_BITS);
	
			// search for a valid multitexture blend function
			for(i = 0; collapse[i].blendA != -1; i++)
			{
				if(abits == collapse[i].blendA && bbits == collapse[i].blendB)
				{
					break;
				}
			}
	
			// nothing found
			if(collapse[i].blendA == -1)
			{
				return;
			}
	
			// GL_ADD is a separate extension
			if(collapse[i].multitextureEnv == GL_ADD && !glConfig.textureEnvAddAvailable)
			{
				return;
			}
	
			// make sure waveforms have identical parameters
			if((stages[0].rgbGen != stages[1].rgbGen) || (stages[0].alphaGen != stages[1].alphaGen))
			{
				return;
			}
	
			// an add collapse can only have identity colors
			if(collapse[i].multitextureEnv == GL_ADD && stages[0].rgbGen != CGEN_IDENTITY)
			{
				return;
			}
	
			if(stages[0].rgbGen == CGEN_WAVEFORM)
			{
				if(memcmp(&stages[0].rgbWave, &stages[1].rgbWave, sizeof(stages[0].rgbWave)))
				{
					return;
				}
			}
			if(stages[0].alphaGen == CGEN_WAVEFORM)
			{
				if(memcmp(&stages[0].alphaWave, &stages[1].alphaWave, sizeof(stages[0].alphaWave)))
				{
					return;
				}
			}
	
			stages[0].bundle[1] = stages[1].bundle[0];
	
			// set the new blend state bits
			shader.collapseType = COLLAPSE_Generic_multi;
			shader.collapseTextureEnv = collapse[i].multitextureEnv;
			stages[0].type = ST_COLLAPSE_Generic_multi;
			stages[0].stateBits &= ~(GLS_DSTBLEND_BITS | GLS_SRCBLEND_BITS);
			stages[0].stateBits |= collapse[i].multitextureBlend;
		
			// move down subsequent stages
			memmove(&stages[1], &stages[2], sizeof(stages[0]) * (MAX_SHADER_STAGES - 2));
			Com_Memset(&stages[MAX_SHADER_STAGES - 1], 0, sizeof(stages[0]));
	
			shader.numStages -= 1;
		}
		*/
	}
	
	// move down subsequent stages
	Com_Memset(&tmpStages[numStages], 0, sizeof(stages[0]) * (MAX_SHADER_STAGES - numStages));
	tmpShader.numStages = numStages;
	
	// copy result
	Com_Memcpy(&stages[0], &tmpStages[0], sizeof(stages));
	Com_Memcpy(&shader, &tmpShader, sizeof(shader));
}

/*
==============
SortNewShader

Positions the most recently created shader in the tr.sortedShaders[]
array so that the shader->sort key is sorted reletive to the other
shaders.

Sets shader->sortedIndex
==============
*/
static void SortNewShader(void)
{
	int             i;
	float           sort;
	shader_t       *newShader;

	newShader = tr.shaders[tr.numShaders - 1];
	sort = newShader->sort;

	for(i = tr.numShaders - 2; i >= 0; i--)
	{
		if(tr.sortedShaders[i]->sort <= sort)
		{
			break;
		}
		tr.sortedShaders[i + 1] = tr.sortedShaders[i];
		tr.sortedShaders[i + 1]->sortedIndex++;
	}

	newShader->sortedIndex = i + 1;
	tr.sortedShaders[i + 1] = newShader;
}


/*
====================
GeneratePermanentShader
====================
*/
static shader_t *GeneratePermanentShader(void)
{
	shader_t       *newShader;
	int             i, b;
	int             size, hash;

	if(tr.numShaders == MAX_SHADERS)
	{
		ri.Printf(PRINT_WARNING, "WARNING: GeneratePermanentShader - MAX_SHADERS hit\n");
		return tr.defaultShader;
	}

	newShader = ri.Hunk_Alloc(sizeof(shader_t), h_low);

	*newShader = shader;

	if(shader.sort <= SS_OPAQUE)
	{
		newShader->fogPass = FP_EQUAL;
	}
	else if(shader.contentFlags & CONTENTS_FOG)
	{
		newShader->fogPass = FP_LE;
	}

	tr.shaders[tr.numShaders] = newShader;
	newShader->index = tr.numShaders;

	tr.sortedShaders[tr.numShaders] = newShader;
	newShader->sortedIndex = tr.numShaders;

	tr.numShaders++;

	for(i = 0; i < newShader->numStages; i++)
	{
		if(!stages[i].active)
		{
			break;
		}
		newShader->stages[i] = ri.Hunk_Alloc(sizeof(stages[i]), h_low);
		*newShader->stages[i] = stages[i];

		for(b = 0; b < MAX_TEXTURE_BUNDLES; b++)
		{
			size = newShader->stages[i]->bundle[b].numTexMods * sizeof(texModInfo_t);
			newShader->stages[i]->bundle[b].texMods = ri.Hunk_Alloc(size, h_low);
			Com_Memcpy(newShader->stages[i]->bundle[b].texMods, stages[i].bundle[b].texMods, size);
		}
	}

	SortNewShader();

	hash = generateHashValue(newShader->name, FILE_HASH_SIZE);
	newShader->next = shaderHashTable[hash];
	shaderHashTable[hash] = newShader;

	return newShader;
}

/*
====================
GeneratePermanentShaderTable
====================
*/
static void GeneratePermanentShaderTable(float *values, int numValues)
{
	shaderTable_t  *newTable;
	int             i;
	int             hash;

	if(tr.numTables == MAX_SHADER_TABLES)
	{
		ri.Printf(PRINT_WARNING, "WARNING: GeneratePermanentShaderTables - MAX_SHADER_TABLES hit\n");
		return;
	}

	newTable = ri.Hunk_Alloc(sizeof(shaderTable_t), h_low);

	*newTable = table;

	tr.shaderTables[tr.numTables] = newTable;
	newTable->index = tr.numTables;

	tr.numTables++;
	
	newTable->numValues = numValues;
	newTable->values = ri.Hunk_Alloc(sizeof(float) * numValues, h_low);

//	ri.Printf(PRINT_ALL, "values: \n");
	for(i = 0; i < numValues; i++)
	{
		newTable->values[i] = values[i];
		
//		ri.Printf(PRINT_ALL, "%f", newTable->values[i]);
		
//		if(i != numValues -1)
//			ri.Printf(PRINT_ALL, ", ");
	}
//	ri.Printf(PRINT_ALL, "\n");

	hash = generateHashValue(newTable->name, MAX_SHADERTABLE_HASH);
	newTable->next = shaderTableHashTable[hash];
	shaderTableHashTable[hash] = newTable;
}

/*
=========================
FinishShader

Returns a freshly allocated shader with all the needed info
from the current global working shader
=========================
*/
static shader_t *FinishShader(void)
{
	int             stage;
	
	// set sky stuff appropriate
	if(shader.isSky)
	{
		shader.sort = SS_ENVIRONMENT;
	}
	
	if(shader.forceOpaque)
	{
		shader.sort = SS_OPAQUE;	
	}

	// set polygon offset
	if(shader.polygonOffset && !shader.sort)
	{
		shader.sort = SS_DECAL;
	}
	
	// all light materials need at least one z attenuation stage as first stage
	if(shader.type == SHADER_LIGHT)
	{
		if(stages[0].type != ST_ATTENUATIONMAP_Z)
		{
			// move up subsequent stages
			memmove(&stages[1], &stages[0], sizeof(stages[0]) * (MAX_SHADER_STAGES - 1));
			
			stages[0].active = qtrue;
			stages[0].type = ST_ATTENUATIONMAP_Z;
			stages[0].rgbGen = CGEN_IDENTITY;
			stages[0].stateBits = GLS_DEFAULT;
			stages[0].overrideWrapType = qtrue;
			stages[0].wrapType = WT_EDGE_CLAMP;
			
			LoadMap(&stages[0], "lights/squarelight1a.tga");
		}
		
		// force following shader stages to be xy attenuation stages
		for(stage = 1; stage < MAX_SHADER_STAGES; stage++)
		{
			shaderStage_t  *pStage = &stages[stage];

			if(!pStage->active)
			{
				break;
			}
			
			pStage->type = ST_ATTENUATIONMAP_XY;
		}
	}

	// set appropriate stage information
	for(stage = 0; stage < MAX_SHADER_STAGES; stage++)
	{
		shaderStage_t  *pStage = &stages[stage];

		if(!pStage->active)
		{
			break;
		}

		// check for a missing texture
		switch(pStage->type)
		{
			case ST_GLOWMAP:
			case ST_LIQUIDMAP:
				// skip
				break;
			
			case ST_COLORMAP:
			default:
			{
				if(!pStage->bundle[0].image[0])
				{
					ri.Printf(PRINT_WARNING, "Shader %s has a colormap stage with no image\n", shader.name);
					pStage->active = qfalse;
					continue;
				}
				break;
			}
			
			case ST_DIFFUSEMAP:
			{
				if(!shader.isSky)
				{
					shader.interactLight = qtrue;
				}

				if(!pStage->bundle[0].image[0])
				{
					ri.Printf(PRINT_WARNING, "Shader %s has a diffusemap stage with no image\n", shader.name);
					pStage->bundle[0].image[0] = tr.defaultImage;
				}
				break;
			}
			
			case ST_NORMALMAP:
			{
				if(!pStage->bundle[0].image[0])
				{
					ri.Printf(PRINT_WARNING, "Shader %s has a normalmap stage with no image\n", shader.name);
					pStage->bundle[0].image[0] = tr.flatImage;
				}
				break;
			}
			
			case ST_SPECULARMAP:
			{
				if(!pStage->bundle[0].image[0])
				{
					ri.Printf(PRINT_WARNING, "Shader %s has a specularmap stage with no image\n", shader.name);
					pStage->bundle[0].image[0] = tr.blackImage;
				}
				break;
			}
			
			case ST_LIGHTMAP:
			{
				if(!pStage->bundle[0].isLightMap)
				{
					ri.Printf(PRINT_WARNING, "Shader %s has a lightmap stage with no image\n", shader.name);
					pStage->active = qfalse;
					continue;
				}
				break;
			}
			
			case ST_ATTENUATIONMAP_XY:
			{
				if(!pStage->bundle[0].image[0])
				{
					ri.Printf(PRINT_WARNING, "Shader %s has a xy attenuationmap stage with no image\n", shader.name);
					pStage->active = qfalse;
					continue;
				}
				break;
			}
			
			case ST_ATTENUATIONMAP_Z:
			{
				if(!pStage->bundle[0].image[0])
				{
					ri.Printf(PRINT_WARNING, "Shader %s has a z attenuationmap stage with no image\n", shader.name);
					pStage->active = qfalse;
					continue;
				}
				break;
			}
		}

		// ditch this stage if it's detail and detail textures are disabled
		if(pStage->isDetail && !r_detailTextures->integer)
		{
			if(stage < (MAX_SHADER_STAGES - 1))
			{
				memmove(pStage, pStage + 1, sizeof(*pStage) * (MAX_SHADER_STAGES - stage - 1));
				Com_Memset(pStage + 1, 0, sizeof(*pStage));
			}
			continue;
		}

		// default texture coordinate generation
		switch(pStage->type)
		{
			case ST_COLORMAP:
			{
				if(pStage->bundle[0].isLightMap)
				{
					if(pStage->bundle[0].tcGen == TCGEN_BAD)
					{
						pStage->bundle[0].tcGen = TCGEN_LIGHTMAP;
					}
				}
				else
				{
					if(pStage->bundle[0].tcGen == TCGEN_BAD)
					{
						pStage->bundle[0].tcGen = TCGEN_TEXTURE;
					}
				}
				break;
			}
			
			case ST_DIFFUSEMAP:
			case ST_NORMALMAP:
			case ST_SPECULARMAP:
			case ST_HEATHAZEMAP:
			{
				if(pStage->bundle[0].tcGen == TCGEN_BAD)
				{
					pStage->bundle[0].tcGen = TCGEN_TEXTURE;
				}
				break;
			}
			
			case ST_LIGHTMAP:
			{
				if(pStage->bundle[0].tcGen == TCGEN_BAD)
				{
					pStage->bundle[0].tcGen = TCGEN_LIGHTMAP;
				}
				break;
			}
			
			case ST_ATTENUATIONMAP_XY:
			case ST_ATTENUATIONMAP_Z:
			{
				if(pStage->bundle[0].tcGen == TCGEN_BAD)
				{
					pStage->bundle[0].tcGen = TCGEN_TEXTURE;
				}
				break;
			}
		
			default:
			{
				break;	
			}
		}
		
		if(shader.forceOpaque)
		{
			pStage->stateBits |= GLS_DEPTHMASK_TRUE;	
		}

		// determine sort order and fog color adjustment
		if((pStage->stateBits & (GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS)) &&
		   (stages[0].stateBits & (GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS)))
		{
			int             blendSrcBits = pStage->stateBits & GLS_SRCBLEND_BITS;
			int             blendDstBits = pStage->stateBits & GLS_DSTBLEND_BITS;

			// fog color adjustment only works for blend modes that have a contribution
			// that aproaches 0 as the modulate values aproach 0 --
			// GL_ONE, GL_ONE
			// GL_ZERO, GL_ONE_MINUS_SRC_COLOR
			// GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA

			// modulate, additive
			if(((blendSrcBits == GLS_SRCBLEND_ONE) && (blendDstBits == GLS_DSTBLEND_ONE)) ||
			   ((blendSrcBits == GLS_SRCBLEND_ZERO) && (blendDstBits == GLS_DSTBLEND_ONE_MINUS_SRC_COLOR)))
			{
				pStage->adjustColorsForFog = ACFF_MODULATE_RGB;
			}
			// strict blend
			else if((blendSrcBits == GLS_SRCBLEND_SRC_ALPHA) &&
					(blendDstBits == GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA))
			{
				pStage->adjustColorsForFog = ACFF_MODULATE_ALPHA;
			}
			// premultiplied alpha
			else if((blendSrcBits == GLS_SRCBLEND_ONE) && (blendDstBits == GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA))
			{
				pStage->adjustColorsForFog = ACFF_MODULATE_RGBA;
			}
			else
			{
				// we can't adjust this one correctly, so it won't be exactly correct in fog
			}

			// don't screw with sort order if this is a portal or environment
			if(!shader.sort)
			{
				// see through item, like a grill or grate
				if(pStage->stateBits & GLS_DEPTHMASK_TRUE)
				{
					shader.sort = SS_SEE_THROUGH;
				}
				else
				{
					shader.sort = SS_BLEND0;
				}
			}
		}
	}
	shader.numStages = stage;

	// there are times when you will need to manually apply a sort to
	// opaque alpha tested shaders that have later blend passes
	if(!shader.sort)
	{
		if(shader.translucent && !shader.forceOpaque)
			shader.sort = SS_DECAL;
		else
			shader.sort = SS_OPAQUE;
	}

	// look for multitexture potential
	CollapseStages();

	// fogonly shaders don't have any normal passes
	if(shader.numStages == 0)
	{
		shader.sort = SS_FOG;
	}

	return GeneratePermanentShader();
}

//========================================================================================

/*
====================
FindShaderInShaderText

Scans the combined text description of all the shader files for
the given shader name.

return NULL if not found

If found, it will return a valid shader
=====================
*/
static char    *FindShaderInShaderText(const char *shaderName)
{

	char           *token, *p;

	int             i, hash;

	hash = generateHashValue(shaderName, MAX_SHADERTEXT_HASH);

	for(i = 0; shaderTextHashTable[hash][i]; i++)
	{
		p = shaderTextHashTable[hash][i];
		token = Com_ParseExt(&p, qtrue);
		if(!Q_stricmp(token, shaderName))
		{
			//ri.Printf(PRINT_ALL, "found shader '%s' by hashing\n", shaderName);
			return p;
		}
	}

	p = s_shaderText;

	if(!p)
	{
		return NULL;
	}

	// look for label
	while(1)
	{
		token = Com_ParseExt(&p, qtrue);
		if(token[0] == 0)
		{
			break;
		}

		if(!Q_stricmp(token, shaderName))
		{
			//ri.Printf(PRINT_ALL, "found shader '%s' by linear search\n", shaderName);
			return p;
		}
		// skip shader tables
		else if(!Q_stricmp(token, "table"))
		{
			// skip table name
			token = Com_ParseExt(&p, qtrue);

			Com_SkipBracedSection(&p);
		}
		// support shader templates
		else if(!Q_stricmp(token, "guide"))
		{
			// parse shader name
			token = Com_ParseExt(&p, qtrue);
			
			if(!Q_stricmp(token, shaderName))
			{
				ri.Printf(PRINT_ALL, "found shader '%s' by linear search\n", shaderName);
				return p;
			}

			// skip guide name
			token = Com_ParseExt(&p, qtrue);

			// skip parameters
			token = Com_ParseExt(&p, qtrue);
			if(Q_stricmp(token, "("))
			{
				break;
			}

			while(1)
			{
				token = Com_ParseExt(&p, qtrue);

				if(!token[0])
					break;

				if(!Q_stricmp(token, ")"))
					break;
			}

			if(Q_stricmp(token, ")"))
			{
				break;
			}
		}
		else
		{
			// skip the shader body
			Com_SkipBracedSection(&p);
		}
	}

	return NULL;
}



/*
==================
R_FindShaderByName

Will always return a valid shader, but it might be the
default shader if the real one can't be found.
==================
*/
shader_t       *R_FindShaderByName(const char *name)
{
	char            strippedName[MAX_QPATH];
	int             hash;
	shader_t       *sh;

	if((name == NULL) || (name[0] == 0))
	{							// bk001205
		return tr.defaultShader;
	}

	Com_StripExtension(name, strippedName, sizeof(strippedName));

	hash = generateHashValue(strippedName, FILE_HASH_SIZE);

	// see if the shader is already loaded
	for(sh = shaderHashTable[hash]; sh; sh = sh->next)
	{
		// NOTE: if there was no shader or image available with the name strippedName
		// then a default shader is created with type == SHADER_3D_DYNAMIC, so we
		// have to check all default shaders otherwise for every call to R_FindShader
		// with that same strippedName a new default shader is created.
		if(Q_stricmp(sh->name, strippedName) == 0)
		{
			// match found
			return sh;
		}
	}

	return tr.defaultShader;
}


/*
===============
R_FindShader

Will always return a valid shader, but it might be the
default shader if the real one can't be found.

In the interest of not requiring an explicit shader text entry to
be defined for every single image used in the game, three default
shader behaviors can be auto-created for any image:

If type == SHADER_2D, then the image will be used
for 2D rendering unless an explicit shader is found

If type == SHADER_3D_DYNAMIC, then the image will have
dynamic diffuse lighting applied to it, as apropriate for most
entity skin surfaces.

If type == SHADER_3D_STATIC, then the image will use
the vertex rgba modulate values, as apropriate for misc_model
pre-lit surfaces.

Other lightmapIndex values will have a lightmap stage created
and src*dest blending applied with the texture, as apropriate for
most world construction surfaces.

===============
*/
shader_t       *R_FindShader(const char *name, shaderType_t type, qboolean mipRawImage)
{
	char            strippedName[MAX_QPATH];
	char            fileName[MAX_QPATH];
	int             i, hash;
	char           *shaderText;
	image_t        *image;
	shader_t       *sh;

	if(name[0] == 0)
	{
		return tr.defaultShader;
	}

	Com_StripExtension(name, strippedName, sizeof(strippedName));

	hash = generateHashValue(strippedName, FILE_HASH_SIZE);

	// see if the shader is already loaded
	for(sh = shaderHashTable[hash]; sh; sh = sh->next)
	{
		// NOTE: if there was no shader or image available with the name strippedName
		// then a default shader is created with type == SHADER_3D_DYNAMIC, so we
		// have to check all default shaders otherwise for every call to R_FindShader
		// with that same strippedName a new default shader is created.
		if((sh->type == type || sh->defaultShader) && !Q_stricmp(sh->name, strippedName))
		{
			// match found
			return sh;
		}
	}

	// make sure the render thread is stopped, because we are probably
	// going to have to upload an image
	if(r_smp->integer)
	{
		R_SyncRenderThread();
	}

	// clear the global shader
	Com_Memset(&shader, 0, sizeof(shader));
	Com_Memset(&stages, 0, sizeof(stages));
	Q_strncpyz(shader.name, strippedName, sizeof(shader.name));
	shader.type = type;
	for(i = 0; i < MAX_SHADER_STAGES; i++)
	{
		stages[i].bundle[0].texMods = texMods[i];
	}

	// attempt to define shader from an explicit parameter file
	shaderText = FindShaderInShaderText(strippedName);
	if(shaderText)
	{
		if(!ParseShader(shaderText))
		{
			// had errors, so use default shader
			shader.defaultShader = qtrue;
		}
		sh = FinishShader();
		return sh;
	}

	// if not defined in the in-memory shader descriptions,
	// look for a single TGA, BMP, or PCX
	Q_strncpyz(fileName, name, sizeof(fileName));
	Com_DefaultExtension(fileName, sizeof(fileName), ".tga");
	image = R_FindImageFile(fileName,
							mipRawImage ? IF_NONE : IF_NOPICMIP,
							mipRawImage ? FT_DEFAULT : FT_LINEAR,
							mipRawImage ? WT_REPEAT : WT_CLAMP);
	if(!image)
	{
		ri.Printf(PRINT_DEVELOPER, "Couldn't find image file for shader %s\n", name);
		shader.defaultShader = qtrue;
		return FinishShader();
	}

	// create the default shading commands
	switch (shader.type)
	{
		case SHADER_2D:
		{
			// GUI elements
			stages[0].bundle[0].image[0] = image;
			stages[0].active = qtrue;
			stages[0].rgbGen = CGEN_VERTEX;
			stages[0].alphaGen = AGEN_VERTEX;
			stages[0].stateBits = GLS_DEPTHTEST_DISABLE |
					GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;
			break;
		}

		case SHADER_3D_DYNAMIC:
		{
			// dynamic colors at vertexes
			stages[0].type = ST_DIFFUSEMAP;
			stages[0].bundle[0].image[0] = image;
			stages[0].active = qtrue;
			stages[0].rgbGen = CGEN_LIGHTING_DIFFUSE;
			stages[0].stateBits = GLS_DEFAULT;
			break;
		}

		case SHADER_3D_STATIC:
		{
			// explicit colors at vertexes
			stages[0].type = ST_DIFFUSEMAP;
			stages[0].bundle[0].image[0] = image;
			stages[0].active = qtrue;
			stages[0].rgbGen = CGEN_EXACT_VERTEX;
			stages[0].alphaGen = AGEN_SKIP;
			stages[0].stateBits = GLS_DEFAULT;
			break;
		}
		
		case SHADER_3D_LIGHTMAP:
		{
			// diffuseMap
			stages[0].type = ST_DIFFUSEMAP;
			stages[0].bundle[0].image[0] = image;
			stages[0].active = qtrue;
			stages[0].rgbGen = CGEN_IDENTITY;
			//stages[0].stateBits |= GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_ZERO;
			stages[0].stateBits = GLS_DEFAULT;
			
			// lightMap
			stages[1].type = ST_LIGHTMAP;
			stages[1].bundle[0].isLightMap = qtrue;
			stages[1].active = qtrue;
			stages[1].rgbGen = CGEN_IDENTITY;	// lightmaps are scaled on creation
			// for identitylight
			stages[1].stateBits = GLS_DEFAULT;
			break;
		}
		
		case SHADER_LIGHT:
		{
			stages[0].type = ST_ATTENUATIONMAP_Z;
			stages[0].bundle[0].image[0] = tr.noFalloffImage; // FIXME should be attenuationZImage
			stages[0].active = qtrue;
			stages[0].rgbGen = CGEN_IDENTITY;
			stages[0].stateBits = GLS_DEFAULT;
	
			stages[1].type = ST_ATTENUATIONMAP_XY;
			stages[1].bundle[0].image[0] = image;
			stages[1].active = qtrue;
			stages[1].rgbGen = CGEN_IDENTITY;
			stages[1].stateBits = GLS_DEFAULT;
			//stages[1].stateBits |= GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_ZERO;
			break;
		}
		
		default:
			break;
	}

	return FinishShader();
}


qhandle_t RE_RegisterShaderFromImage(const char *name, image_t * image, qboolean mipRawImage)
{
	int             i, hash;
	shader_t       *sh;

	hash = generateHashValue(name, FILE_HASH_SIZE);

	// see if the shader is already loaded
	for(sh = shaderHashTable[hash]; sh; sh = sh->next)
	{
		// NOTE: if there was no shader or image available with the name strippedName
		// then a default shader is created with type == SHADER_3D_DYNAMIC, so we
		// have to check all default shaders otherwise for every call to R_FindShader
		// with that same strippedName a new default shader is created.
		if((sh->type == SHADER_2D || sh->defaultShader) && !Q_stricmp(sh->name, name))
		{
			// match found
			return sh->index;
		}
	}

	// make sure the render thread is stopped, because we are probably
	// going to have to upload an image
	if(r_smp->integer)
	{
		R_SyncRenderThread();
	}

	// clear the global shader
	Com_Memset(&shader, 0, sizeof(shader));
	Com_Memset(&stages, 0, sizeof(stages));
	Q_strncpyz(shader.name, name, sizeof(shader.name));
	shader.type = SHADER_2D;
	for(i = 0; i < MAX_SHADER_STAGES; i++)
	{
		stages[i].bundle[0].texMods = texMods[i];
	}

	// create the default shading commands
	
	// GUI elements
	stages[0].bundle[0].image[0] = image;
	stages[0].active = qtrue;
	stages[0].rgbGen = CGEN_VERTEX;
	stages[0].alphaGen = AGEN_VERTEX;
	stages[0].stateBits = GLS_DEPTHTEST_DISABLE |
		GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;

	sh = FinishShader();
	return sh->index;
}


/* 
====================
RE_RegisterShader

This is the exported shader entry point for the rest of the system
It will always return an index that will be valid.

This should really only be used for explicit shaders, because there is no
way to ask for different implicit lighting modes (vertex, lightmap, etc)
====================
*/
qhandle_t RE_RegisterShaderLightMap(const char *name, int lightmapIndex)
{
	shader_t       *sh;

	if(strlen(name) >= MAX_QPATH)
	{
		Com_Printf("Shader name exceeds MAX_QPATH\n");
		return 0;
	}

	sh = R_FindShader(name, lightmapIndex, qtrue);

	// we want to return 0 if the shader failed to
	// load for some reason, but R_FindShader should
	// still keep a name allocated for it, so if
	// something calls RE_RegisterShader again with
	// the same name, we don't try looking for it again
	if(sh->defaultShader)
	{
		return 0;
	}

	return sh->index;
}


/* 
====================
RE_RegisterShader

This is the exported shader entry point for the rest of the system
It will always return an index that will be valid.

This should really only be used for explicit shaders, because there is no
way to ask for different implicit lighting modes (vertex, lightmap, etc)
====================
*/
qhandle_t RE_RegisterShader(const char *name)
{
	shader_t       *sh;

	if(strlen(name) >= MAX_QPATH)
	{
		Com_Printf("Shader name exceeds MAX_QPATH\n");
		return 0;
	}

	sh = R_FindShader(name, SHADER_2D, qtrue);

	// we want to return 0 if the shader failed to
	// load for some reason, but R_FindShader should
	// still keep a name allocated for it, so if
	// something calls RE_RegisterShader again with
	// the same name, we don't try looking for it again
	if(sh->defaultShader)
	{
		return 0;
	}

	return sh->index;
}


/*
====================
RE_RegisterShaderNoMip

For menu graphics that should never be picmiped
====================
*/
qhandle_t RE_RegisterShaderNoMip(const char *name)
{
	shader_t       *sh;

	if(strlen(name) >= MAX_QPATH)
	{
		Com_Printf("Shader name exceeds MAX_QPATH\n");
		return 0;
	}

	sh = R_FindShader(name, SHADER_2D, qfalse);

	// we want to return 0 if the shader failed to
	// load for some reason, but R_FindShader should
	// still keep a name allocated for it, so if
	// something calls RE_RegisterShader again with
	// the same name, we don't try looking for it again
	if(sh->defaultShader)
	{
		return 0;
	}

	return sh->index;
}

/*
====================
RE_RegisterShaderLightAttenuation

For different Doom3 style light effects
====================
*/
qhandle_t RE_RegisterShaderLightAttenuation(const char *name)
{
	shader_t       *sh;

	if(strlen(name) >= MAX_QPATH)
	{
		Com_Printf("Shader name exceeds MAX_QPATH\n");
		return 0;
	}

	sh = R_FindShader(name, SHADER_LIGHT, qfalse);

	// we want to return 0 if the shader failed to
	// load for some reason, but R_FindShader should
	// still keep a name allocated for it, so if
	// something calls RE_RegisterShader again with
	// the same name, we don't try looking for it again
	if(sh->defaultShader)
	{
		return 0;
	}

	return sh->index;
}


/*
====================
R_GetShaderByHandle

When a handle is passed in by another module, this range checks
it and returns a valid (possibly default) shader_t to be used internally.
====================
*/
shader_t       *R_GetShaderByHandle(qhandle_t hShader)
{
	if(hShader < 0)
	{
		ri.Printf(PRINT_WARNING, "R_GetShaderByHandle: out of range hShader '%d'\n", hShader);	// bk: FIXME name
		return tr.defaultShader;
	}
	if(hShader >= tr.numShaders)
	{
		ri.Printf(PRINT_WARNING, "R_GetShaderByHandle: out of range hShader '%d'\n", hShader);
		return tr.defaultShader;
	}
	return tr.shaders[hShader];
}

/*
===============
R_ShaderList_f

Dump information on all valid shaders to the console
A second parameter will cause it to print in sorted order
===============
*/
void R_ShaderList_f(void)
{
	int             i;
	int             count;
	shader_t       *shader;
	char           *s = NULL;

	ri.Printf(PRINT_ALL, "-----------------------\n");

	if(ri.Cmd_Argc() > 1)
	{
		s = ri.Cmd_Argv(1);
	}
	
	count = 0;
	for(i = 0; i < tr.numShaders; i++)
	{
		if(ri.Cmd_Argc() > 2)
		{
			shader = tr.sortedShaders[i];
		}
		else
		{
			shader = tr.shaders[i];
		}
		
		if(s && Q_stricmpn(shader->name, s, strlen(s)) != 0)
			continue;

		ri.Printf(PRINT_ALL, "%i ", shader->numStages);
		
		switch (shader->type)
		{
			case SHADER_2D:
				ri.Printf(PRINT_ALL, "2D   ");
				break;
				
			case SHADER_3D_DYNAMIC:
				ri.Printf(PRINT_ALL, "3D_D ");
				break;
				
			case SHADER_3D_STATIC:
				ri.Printf(PRINT_ALL, "3D_S ");
				break;
			
			case SHADER_3D_LIGHTMAP:
				ri.Printf(PRINT_ALL, "3D_L ");
				break;
				
			case SHADER_LIGHT:
				ri.Printf(PRINT_ALL, "ATTN ");
				break;
		}
	
		if(shader->collapseType == COLLAPSE_Generic_multi)
		{
			if(shader->collapseTextureEnv == GL_ADD)
			{
				ri.Printf(PRINT_ALL, "MT(a)          ");
			}
			else if(shader->collapseTextureEnv == GL_MODULATE)
			{
				ri.Printf(PRINT_ALL, "MT(m)          ");
			}
			else if(shader->collapseTextureEnv == GL_DECAL)
			{
				ri.Printf(PRINT_ALL, "MT(d)          ");
			}
		}
		else if(shader->collapseType == COLLAPSE_lighting_D_radiosity)
		{
			ri.Printf(PRINT_ALL, "D_radiosity    ");
		}
		else if(shader->collapseType == COLLAPSE_lighting_DB_radiosity)
		{
			ri.Printf(PRINT_ALL, "DB_radiosity   ");
		}
		else if(shader->collapseType == COLLAPSE_lighting_DBS_radiosity)
		{
			ri.Printf(PRINT_ALL, "DBS_radiosity  ");
		}
		else if(shader->collapseType == COLLAPSE_lighting_DB_direct)
		{
			ri.Printf(PRINT_ALL, "DB_direct      ");
		}
		else if(shader->collapseType == COLLAPSE_lighting_DBS_direct)
		{
			ri.Printf(PRINT_ALL, "DBS_direct     ");
		}
		else if(shader->collapseType == COLLAPSE_lighting_DB_generic)
		{
			ri.Printf(PRINT_ALL, "DB_generic     ");
		}
		else if(shader->collapseType == COLLAPSE_lighting_DBS_generic)
		{
			ri.Printf(PRINT_ALL, "DBS_generic    ");
		}
		else
		{
			ri.Printf(PRINT_ALL, "               ");
		}
		
		if(shader->createdByGuide)
		{
			ri.Printf(PRINT_ALL, "G ");
		}
		else if(shader->explicitlyDefined)
		{
			ri.Printf(PRINT_ALL, "E ");
		}
		else
		{
			ri.Printf(PRINT_ALL, "  ");
		}

		if(shader->sort == SS_BAD)
		{
			ri.Printf(PRINT_ALL, "SS_BAD              ");
		}
		else if(shader->sort == SS_PORTAL)
		{
			ri.Printf(PRINT_ALL, "SS_PORTAL           ");
		}
		else if(shader->sort == SS_ENVIRONMENT)
		{
			ri.Printf(PRINT_ALL, "SS_ENVIRONMENT      ");
		}
		else if(shader->sort == SS_OPAQUE)
		{
			ri.Printf(PRINT_ALL, "SS_OPAQUE           ");
		}
		else if(shader->sort == SS_DECAL)
		{
			ri.Printf(PRINT_ALL, "SS_DECAL            ");
		}
		else if(shader->sort == SS_SEE_THROUGH)
		{
			ri.Printf(PRINT_ALL, "SS_SEE_THROUGH      ");
		}
		else if(shader->sort == SS_BANNER)
		{
			ri.Printf(PRINT_ALL, "SS_BANNER           ");
		}
		else if(shader->sort == SS_UNDERWATER)
		{
			ri.Printf(PRINT_ALL, "SS_UNDERWATER       ");
		}
		else if(shader->sort == SS_WATER)
		{
			ri.Printf(PRINT_ALL, "SS_WATER            ");
		}
		else if(shader->sort == SS_FAR)
		{
			ri.Printf(PRINT_ALL, "SS_FAR              ");
		}
		else if(shader->sort == SS_MEDIUM)
		{
			ri.Printf(PRINT_ALL, "SS_MEDIUM           ");
		}
		else if(shader->sort == SS_CLOSE)
		{
			ri.Printf(PRINT_ALL, "SS_CLOSE            ");
		}
		else if(shader->sort == SS_BLEND0)
		{
			ri.Printf(PRINT_ALL, "SS_BLEND0           ");
		}
		else if(shader->sort == SS_BLEND1)
		{
			ri.Printf(PRINT_ALL, "SS_BLEND1           ");
		}
		else if(shader->sort == SS_BLEND2)
		{
			ri.Printf(PRINT_ALL, "SS_BLEND2           ");
		}
		else if(shader->sort == SS_BLEND3)
		{
			ri.Printf(PRINT_ALL, "SS_BLEND3           ");
		}
		else if(shader->sort == SS_BLEND6)
		{
			ri.Printf(PRINT_ALL, "SS_BLEND6           ");
		}
		else if(shader->sort == SS_ALMOST_NEAREST)
		{
			ri.Printf(PRINT_ALL, "SS_ALMOST_NEAREST   ");
		}
		else if(shader->sort == SS_NEAREST)
		{
			ri.Printf(PRINT_ALL, "SS_NEAREST          ");
		}
		else if(shader->sort == SS_POST_PROCESS)
		{
			ri.Printf(PRINT_ALL, "SS_POST_PROCESS     ");
		}
		else
		{
			ri.Printf(PRINT_ALL, "                    ");
		}

		if(shader->defaultShader)
		{
			ri.Printf(PRINT_ALL, ": %s (DEFAULTED)\n", shader->name);
		}
		else
		{
			ri.Printf(PRINT_ALL, ": %s\n", shader->name);
		}
		count++;
	}
	ri.Printf(PRINT_ALL, "%i total shaders\n", count);
	ri.Printf(PRINT_ALL, "------------------\n");
}

void R_ShaderExp_f(void)
{
	int             i;
	int				len;
	char			buffer[1024] = "";
	char           *buffer_p = &buffer[0];
	expression_t	exp;
	
	strcpy(shader.name, "dummy");

	ri.Printf(PRINT_ALL, "-----------------------\n");

	for(i = 1; i < ri.Cmd_Argc(); i++)
	{
		strcat(buffer, ri.Cmd_Argv(i));
		strcat(buffer, " ");
	}
	len = strlen(buffer);
	buffer[len -1] = 0;	// replace last " " with tailing zero
	
	ParseExpression(&buffer_p, &exp);

	ri.Printf(PRINT_ALL, "%i total ops\n", exp.numOps);
	ri.Printf(PRINT_ALL, "%f result\n", RB_EvalExpression(&exp, 0));
	ri.Printf(PRINT_ALL, "------------------\n");
}

/*
====================
ScanAndLoadShaderGuides

Finds and loads all .guide files, combining them into
a single large text block that can be scanned for shader template names
=====================
*/
#define	MAX_GUIDE_FILES	1024
static void ScanAndLoadGuideFiles(void)
{
	char          **guideFiles;
	char           *buffers[MAX_GUIDE_FILES];
	char           *p;
	int             numGuides;
	int             i;
	char           *oldp, *token, *hashMem;
	int             guideTextHashTableSizes[MAX_GUIDETEXT_HASH], hash, size;
	char            filename[MAX_QPATH];
	long            sum = 0;
	
	ri.Printf(PRINT_ALL, "----- ScanAndLoadGuideFiles -----\n");

	// scan for guide files
	guideFiles = ri.FS_ListFiles("guides", ".guide", &numGuides);
	
	if(!guideFiles || !numGuides)
	{
		ri.Printf(PRINT_WARNING, "WARNING: no shader guide files found\n");
		return;
	}

	if(numGuides > MAX_GUIDE_FILES)
	{
		numGuides = MAX_GUIDE_FILES;
	}

	// build single large buffer
	for(i = 0; i < numGuides; i++)
	{
		Com_sprintf(filename, sizeof(filename), "guides/%s", guideFiles[i]);

		sum += ri.FS_ReadFile(filename, NULL);
	}
	s_guideText = ri.Hunk_Alloc(sum + numGuides * 2, h_low);

	// load in reverse order, so doubled templates are overriden properly
	for(i = numGuides - 1; i >= 0; i--)
	{
		Com_sprintf(filename, sizeof(filename), "guides/%s", guideFiles[i]);
		
		ri.Printf(PRINT_DEVELOPER, "...loading '%s'\n", filename);
		sum += ri.FS_ReadFile(filename, (void **)&buffers[i]);
		if(!buffers[i])
		{
			ri.Error(ERR_DROP, "Couldn't load %s", filename);
		}
		
		strcat(s_guideText, "\n");
		p = &s_guideText[strlen(s_guideText)];
		strcat(s_guideText, buffers[i]);
		ri.FS_FreeFile(buffers[i]);
		buffers[i] = p;
		Com_Compress(p);
	}

	Com_Memset(guideTextHashTableSizes, 0, sizeof(guideTextHashTableSizes));
	size = 0;
	//
	for(i = 0; i < numGuides; i++)
	{
		Com_sprintf(filename, sizeof(filename), "guides/%s", guideFiles[i]);
		
		Com_BeginParseSession(filename);
		
		// pointer to the first shader file
		p = buffers[i];
		
		// look for label
		while(1)
		{
			token = Com_ParseExt(&p, qtrue);
			if(token[0] == 0)
			{
				break;
			}
			
			if(Q_stricmp(token, "guide") && Q_stricmp(token, "inlineGuide"))
			{
				Com_ParseWarning("expected guide or inlineGuide found '%s'\n", token);
				break;
			}
			
			// parse guide name
			token = Com_ParseExt(&p, qtrue);
			
			//ri.Printf(PRINT_ALL, "guide: '%s'\n", token);
			
			hash = generateHashValue(token, MAX_GUIDETEXT_HASH);
			guideTextHashTableSizes[hash]++;
			size++;
			
			// skip parameters
			token = Com_ParseExt(&p, qtrue);
			if(Q_stricmp(token, "("))
			{
				Com_ParseWarning("expected ( found '%s'\n", token);
				break;
			}
			
			while(1)
			{
				token = Com_ParseExt(&p, qtrue);
				
				if(!token[0])
					break;
				
				if(!Q_stricmp(token, ")"))
					break;
			}
			
			if(Q_stricmp(token, ")"))
			{
				Com_ParseWarning("expected ) found '%s'\n", token);
				break;
			}
				
			// skip guide body
			Com_SkipBracedSection(&p);
			
			// if we passed the pointer to the next shader file
			if(i < numGuides - 1)
			{
				if(p > buffers[i + 1])
				{
					break;
				}
			}
		}
	}

	size += MAX_GUIDETEXT_HASH;

	hashMem = ri.Hunk_Alloc(size * sizeof(char *), h_low);

	for(i = 0; i < MAX_GUIDETEXT_HASH; i++)
	{
		guideTextHashTable[i] = (char **)hashMem;
		hashMem = ((char *)hashMem) + ((guideTextHashTableSizes[i] + 1) * sizeof(char *));
	}

	Com_Memset(guideTextHashTableSizes, 0, sizeof(guideTextHashTableSizes));
	//
	for(i = 0; i < numGuides; i++)
	{
		Com_sprintf(filename, sizeof(filename), "guides/%s", guideFiles[i]);
		
		Com_BeginParseSession(filename);
		
		// pointer to the first shader file
		p = buffers[i];
		
		// look for label
		while(1)
		{
			token = Com_ParseExt(&p, qtrue);
			if(token[0] == 0)
			{
				break;
			}
			
			if(Q_stricmp(token, "guide") && Q_stricmp(token, "inlineGuide"))
			{
				Com_ParseWarning("expected guide or inlineGuide found '%s'\n", token);
				break;
			}
			
			// parse guide name
			oldp = p;
			token = Com_ParseExt(&p, qtrue);
			
			//ri.Printf(PRINT_ALL, "...hashing guide '%s'\n", token);
			
			hash = generateHashValue(token, MAX_GUIDETEXT_HASH);
			guideTextHashTable[hash][guideTextHashTableSizes[hash]++] = oldp;
			
			// skip parameters
			token = Com_ParseExt(&p, qtrue);
			if(Q_stricmp(token, "("))
			{
				Com_ParseWarning("expected ( found '%s'\n", token);
				break;
			}
			
			while(1)
			{
				token = Com_ParseExt(&p, qtrue);
				
				if(!token[0])
					break;
				
				if(!Q_stricmp(token, ")"))
					break;
			}
			
			if(Q_stricmp(token, ")"))
			{
				Com_ParseWarning("expected ) found '%s'\n", token);
				break;
			}
				
			// skip guide body
			Com_SkipBracedSection(&p);
			
			// if we passed the pointer to the next shader file
			if(i < numGuides - 1)
			{
				if(p > buffers[i + 1])
				{
					break;
				}
			}
		}
	}
	
	// free up memory
	ri.FS_FreeFileList(guideFiles);
}

/*
====================
ScanAndLoadShaderFiles

Finds and loads all .shader files, combining them into
a single large text block that can be scanned for shader names
=====================
*/
#define	MAX_SHADER_FILES	4096
static void ScanAndLoadShaderFiles(void)
{
	char          **shaderFiles;
	char           *buffers[MAX_SHADER_FILES];
	char           *p;
	int             numShaders;
	int             i;
	char           *oldp, *token, *hashMem;
	int             shaderTextHashTableSizes[MAX_SHADERTEXT_HASH], hash, size;
	char            filename[MAX_QPATH];
	long            sum = 0;
	
	// scan for shader files
	shaderFiles = ri.FS_ListFiles("scripts", ".shader", &numShaders);
	if(!shaderFiles || !numShaders)
	{
		ri.Printf(PRINT_WARNING, "WARNING: no shader files found\n");
		return;
	}

	if(numShaders > MAX_SHADER_FILES)
	{
		numShaders = MAX_SHADER_FILES;
	}

	// build single large buffer
	for(i = 0; i < numShaders; i++)
	{
		Com_sprintf(filename, sizeof(filename), "scripts/%s", shaderFiles[i]);
		sum += ri.FS_ReadFile(filename, NULL);
	}
	s_shaderText = ri.Hunk_Alloc(sum + numShaders * 2, h_low);

	// load in reverse order, so doubled shaders are overriden properly
	for(i = numShaders - 1; i >= 0; i--)
	{
		Com_sprintf(filename, sizeof(filename), "scripts/%s", shaderFiles[i]);

		ri.Printf(PRINT_DEVELOPER, "...loading '%s'\n", filename);
		sum += ri.FS_ReadFile(filename, (void **)&buffers[i]);
		if(!buffers[i])
		{
			ri.Error(ERR_DROP, "Couldn't load %s", filename);
		}
		
		strcat(s_shaderText, "\n");
		p = &s_shaderText[strlen(s_shaderText)];
		strcat(s_shaderText, buffers[i]);
		ri.FS_FreeFile(buffers[i]);
		buffers[i] = p;
		Com_Compress(p);
	}

	Com_Memset(shaderTextHashTableSizes, 0, sizeof(shaderTextHashTableSizes));
	size = 0;
	for(i = 0; i < numShaders; i++)
	{
		Com_sprintf(filename, sizeof(filename), "scripts/%s", shaderFiles[i]);
		Com_BeginParseSession(filename);
		
		// pointer to the first shader file
		p = buffers[i];
		
		// look for label
		while(1)
		{
			token = Com_ParseExt(&p, qtrue);
			if(token[0] == 0)
			{
				break;
			}
			
			// skip shader tables
			if(!Q_stricmp(token, "table"))
			{
				// skip table name
				token = Com_ParseExt(&p, qtrue);
				
				Com_SkipBracedSection(&p);
			}
			// support shader templates
			else if(!Q_stricmp(token, "guide"))
			{
				// parse shader name
				token = Com_ParseExt(&p, qtrue);
				
				//ri.Printf(PRINT_ALL, "...guided '%s'\n", token);
				
				hash = generateHashValue(token, MAX_SHADERTEXT_HASH);
				shaderTextHashTableSizes[hash]++;
				size++;
				
				// skip guide name
				token = Com_ParseExt(&p, qtrue);
				
				// skip parameters
				token = Com_ParseExt(&p, qtrue);
				if(Q_stricmp(token, "("))
				{
					Com_ParseWarning("expected ( found '%s'\n", token);
					break;
				}
				
				while(1)
				{
					token = Com_ParseExt(&p, qtrue);
					
					if(!token[0])
						break;
					
					if(!Q_stricmp(token, ")"))
						break;
				}
				
				if(Q_stricmp(token, ")"))
				{
					Com_ParseWarning("expected ) found '%s'\n", token);
					break;
				}
			}
			else
			{
				hash = generateHashValue(token, MAX_SHADERTEXT_HASH);
				shaderTextHashTableSizes[hash]++;
				size++;
			
				Com_SkipBracedSection(&p);
			}
			
			// if we passed the pointer to the next shader file
			if(i < numShaders - 1)
			{
				if(p > buffers[i + 1])
				{
					break;
				}
			}
		}
	}

	size += MAX_SHADERTEXT_HASH;

	hashMem = ri.Hunk_Alloc(size * sizeof(char *), h_low);

	for(i = 0; i < MAX_SHADERTEXT_HASH; i++)
	{
		shaderTextHashTable[i] = (char **)hashMem;
		hashMem = ((char *)hashMem) + ((shaderTextHashTableSizes[i] + 1) * sizeof(char *));
	}

	Com_Memset(shaderTextHashTableSizes, 0, sizeof(shaderTextHashTableSizes));

	for(i = 0; i < numShaders; i++)
	{
		Com_sprintf(filename, sizeof(filename), "scripts/%s", shaderFiles[i]);
		Com_BeginParseSession(filename);
		
		// pointer to the first shader file
		p = buffers[i];
		
		// look for label
		while(1)
		{
			oldp = p;
			token = Com_ParseExt(&p, qtrue);
			if(token[0] == 0)
			{
				break;
			}

			// parse shader tables
			if(!Q_stricmp(token, "table"))
			{
				int             depth;
				float			values[FUNCTABLE_SIZE];
				int				numValues;
				shaderTable_t  *tb;
				qboolean        alreadyCreated;
				
				Com_Memset(&table, 0, sizeof(table));
				
				token = Com_ParseExt(&p, qtrue);
				Q_strncpyz(table.name, token, sizeof(table.name));
				
				// check if already created
				alreadyCreated = qfalse;
				hash = generateHashValue(table.name, MAX_SHADERTABLE_HASH);
				for(tb = shaderTableHashTable[hash]; tb; tb = tb->next)
				{
					if(Q_stricmp(tb->name, table.name) == 0)
					{
						// match found
						alreadyCreated = qtrue;
						break;
					}
				}

				depth = 0;
				numValues = 0;
				do
				{
					token = Com_ParseExt(&p, qtrue);
					
					if(!Q_stricmp(token, "snap"))
					{
						table.snap = qtrue;	
					}
					else if(!Q_stricmp(token, "clamp"))
					{
						table.clamp = qtrue;	
					}
					else if(token[0] == '{')
					{
						depth++;
					}
					else if(token[0] == '}')
					{
						depth--;
					}
					else if(token[0] == ',')
					{
						continue;
					}
					else
					{
						if(numValues == FUNCTABLE_SIZE)
						{
							ri.Printf(PRINT_WARNING, "WARNING: FUNCTABLE_SIZE hit\n");
							break;	
						}
						values[numValues++] = atof(token);
					}
				} while(depth && p);
				
				if(!alreadyCreated)
				{
					ri.Printf(PRINT_DEVELOPER, "...generating '%s'\n", table.name);
					GeneratePermanentShaderTable(values, numValues);
				}
			}
			// support shader templates
			else if(!Q_stricmp(token, "guide"))
			{
				// parse shader name
				oldp = p;
				token = Com_ParseExt(&p, qtrue);
				
				//ri.Printf(PRINT_ALL, "...guided '%s'\n", token);
				
				hash = generateHashValue(token, MAX_SHADERTEXT_HASH);
				shaderTextHashTable[hash][shaderTextHashTableSizes[hash]++] = oldp;
				
				// skip guide name
				token = Com_ParseExt(&p, qtrue);
				
				// skip parameters
				token = Com_ParseExt(&p, qtrue);
				if(Q_stricmp(token, "("))
				{
					Com_ParseWarning("expected ( found '%s'\n", token);
					break;
				}
				
				while(1)
				{
					token = Com_ParseExt(&p, qtrue);
					
					if(!token[0])
						break;
					
					if(!Q_stricmp(token, ")"))
						break;
				}
				
				if(Q_stricmp(token, ")"))
				{
					Com_ParseWarning("expected ) found '%s'\n", token);
					break;
				}
			}
			else
			{
				hash = generateHashValue(token, MAX_SHADERTEXT_HASH);
				shaderTextHashTable[hash][shaderTextHashTableSizes[hash]++] = oldp;

				// skip shaderbody
				Com_SkipBracedSection(&p);
			}
			
			// if we passed the pointer to the next shader file
			if(i < numShaders - 1)
			{
				if(p > buffers[i + 1])
				{
					break;
				}
			}
		}
	}
	
	// free up memory
	ri.FS_FreeFileList(shaderFiles);
}


/*
====================
CreateInternalShaders
====================
*/
static void CreateInternalShaders(void)
{
	tr.numShaders = 0;

	// init the default shader
	Com_Memset(&shader, 0, sizeof(shader));
	Com_Memset(&stages, 0, sizeof(stages));

	Q_strncpyz(shader.name, "<default>", sizeof(shader.name));

	shader.type = SHADER_3D_DYNAMIC;
	stages[0].bundle[0].image[0] = tr.defaultImage;
	stages[0].active = qtrue;
	stages[0].stateBits = GLS_DEFAULT;
	tr.defaultShader = FinishShader();
}

static void CreateExternalShaders(void)
{
	tr.projectionShadowShader = R_FindShader("projectionShadow", SHADER_3D_DYNAMIC, qtrue);

	tr.flareShader = R_FindShader("flareShader", SHADER_3D_DYNAMIC, qtrue);
	tr.sunShader = R_FindShader("sun", SHADER_3D_DYNAMIC, qtrue);
	
	tr.defaultPointLightShader = R_FindShader("lights/defaultPointLight", SHADER_LIGHT, qtrue);
	tr.defaultProjectedLightShader = R_FindShader("lights/defaultProjectedLight", SHADER_LIGHT, qtrue);
	tr.defaultDynamicLightShader = R_FindShader("lights/defaultDynamicLight", SHADER_LIGHT, qtrue);
}

/*
==================
R_InitShaders
==================
*/
void R_InitShaders(void)
{
	ri.Printf( PRINT_ALL, "Initializing Shaders\n" );

	Com_Memset(shaderTableHashTable, 0, sizeof(shaderTableHashTable));
	Com_Memset(shaderHashTable, 0, sizeof(shaderHashTable));

	deferLoad = qfalse;

	CreateInternalShaders();
	
	ScanAndLoadGuideFiles();

	ScanAndLoadShaderFiles();

	CreateExternalShaders();
}
