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

Doom 3 BFG Edition GPL Source Code
Copyright (C) 1993-2012 id Software LLC, a ZeniMax Media company. 

This file is part of the Doom 3 BFG Edition GPL Source Code ("Doom 3 BFG Edition Source Code").  

Doom 3 BFG Edition 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 3 of the License, or
(at your option) any later version.

Doom 3 BFG Edition 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 Doom 3 BFG Edition Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 BFG Edition Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 BFG Edition Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

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





#include "tr_local.h"

// do this with a pointer, in case we want to make the actual manager
// a private virtual subclass
idImageManager	imageManager;
idImageManager * globalImages = &imageManager;

idCVar preLoad_Images( "preLoad_Images", "1", CVAR_SYSTEM | CVAR_BOOL, "preload images during beginlevelload" );
idCVar image_anisotropy(
	"image_anisotropy",
	"16",
	CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER,
	"anisotropic filtering level for mipmapped material textures",
	1,
	16 );
idCVar image_downSize(
	"image_downSize",
	"0",
	CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL,
	"downsize material textures to image_downSizeLimit" );
idCVar image_downSizeLimit(
	"image_downSizeLimit",
	"0",
	CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER,
	"maximum material texture dimension when image_downSize is enabled; 0 disables the limit",
	0,
	32768 );
idCVar image_downSizeSpecular(
	"image_downSizeSpecular",
	"0",
	CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL,
	"downsize specular textures to image_downSizeSpecularLimit" );
idCVar image_downSizeSpecularLimit(
	"image_downSizeSpecularLimit",
	"64",
	CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER,
	"maximum specular texture dimension when image_downSizeSpecular is enabled; 0 disables the limit",
	0,
	32768 );
idCVar image_downSizeBump(
	"image_downSizeBump",
	"0",
	CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL,
	"downsize bump textures to image_downSizeBumpLimit" );
idCVar image_downSizeBumpLimit(
	"image_downSizeBumpLimit",
	"256",
	CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER,
	"maximum bump texture dimension when image_downSizeBump is enabled; 0 disables the limit",
	0,
	32768 );
idCVar image_ignoreHighQuality(
	"image_ignoreHighQuality",
	"0",
	CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL,
	"ignore material highquality / uncompressed image usage hints" );

static void R_NormalizeInternalImageName( idStr& name ) {
	// Runtime render targets are referenced from materials with option hashes
	// (for example _postProcessAlbedo0#__0200). Treat those as the same image.
	if ( name.IsEmpty() || name[0] != '_' ) {
		return;
	}

	const int optionsPos = name.Find( "#__" );
	if ( optionsPos >= 0 ) {
		name.CapLength( optionsPos );
	}
}

static bool R_IsQ4LightImageNamespace( const char *name ) {
	return name != NULL
		&& ( idStr::Icmpn( name, "lights/", 7 ) == 0
			|| idStr::Icmpn( name, "gfx/lights/", 11 ) == 0 );
}

static bool R_IsQ4PresentationImageNamespace( const char *name ) {
	return name != NULL
		&& ( idStr::Icmpn( name, "gfx/guis/", 9 ) == 0
			|| idStr::Icmpn( name, "fonts", 5 ) == 0
			|| idStr::Icmpn( name, "newfonts", 8 ) == 0 );
}

static bool R_AllowImageDownSizeForName( const char *name, bool allowDownSize ) {
	if ( !allowDownSize ) {
		return false;
	}

	return !R_IsQ4PresentationImageNamespace( name );
}

/*
===============
R_ReloadImages_f

Regenerate all images that came directly from files that have changed, so
any saved changes will show up in place.

New r_texturesize/r_texturedepth variables will take effect on reload

reloadImages <all>
===============
*/
void R_ReloadImages_f( const idCmdArgs &args ) {
	bool all = false;

	if ( args.Argc() == 2 ) {
		if ( !idStr::Icmp( args.Argv(1), "all" ) ) {
			all = true;
		} else {
			common->Printf( "USAGE: reloadImages <all>\n" );
			return;
		}
	}

	globalImages->ReloadImages( all );
}

static void R_ImageDDSSelfTest_f( const idCmdArgs &args ) {
	(void)args;
	if ( !R_ImageDDS_RunSelfTest() ) {
		common->Warning( "Image DDS self-test failed" );
	}
}

typedef struct {
	idImage	*image;
	int		size;
	int		index;
} sortedImage_t;

/*
=======================
R_QsortImageSizes

=======================
*/
static int R_QsortImageSizes( const void *a, const void *b ) {
	const sortedImage_t	*ea, *eb;

	ea = (sortedImage_t *)a;
	eb = (sortedImage_t *)b;

	if ( ea->size > eb->size ) {
		return -1;
	}
	if ( ea->size < eb->size ) {
		return 1;
	}
	return idStr::Icmp( ea->image->GetName(), eb->image->GetName() );
}

/*
=======================
R_QsortImageName

=======================
*/
static int R_QsortImageName( const void* a, const void* b ) {
	const sortedImage_t	*ea, *eb;

	ea = (sortedImage_t *)a;
	eb = (sortedImage_t *)b;

	return idStr::Icmp( ea->image->GetName(), eb->image->GetName() );
}

/*
===============
R_ListImages_f
===============
*/
void R_ListImages_f( const idCmdArgs &args ) {
	int		i, partialSize;
	idImage	*image;
	int		totalSize;
	int		count = 0;
	bool	uncompressedOnly = false;
	bool	unloaded = false;
	bool	failed = false;
	bool	sorted = false;
	bool	duplicated = false;
	bool	overSized = false;
	bool	sortByName = false;

	if ( args.Argc() == 1 ) {

	} else if ( args.Argc() == 2 ) {
		if ( idStr::Icmp( args.Argv( 1 ), "uncompressed" ) == 0 ) {
			uncompressedOnly = true;
		} else if ( idStr::Icmp( args.Argv( 1 ), "sorted" ) == 0 ) {
			sorted = true;
		} else if ( idStr::Icmp( args.Argv( 1 ), "namesort" ) == 0 ) {
			sortByName = true;
		} else if ( idStr::Icmp( args.Argv( 1 ), "unloaded" ) == 0 ) {
			unloaded = true;
		} else if ( idStr::Icmp( args.Argv( 1 ), "duplicated" ) == 0 ) {
			duplicated = true;
		} else if ( idStr::Icmp( args.Argv( 1 ), "oversized" ) == 0 ) {
			sorted = true;
			overSized = true;
		} else {
			failed = true;
		}
	} else {
		failed = true;
	}

	if ( failed ) {
		common->Printf( "usage: listImages [ sorted | namesort | unloaded | duplicated | showOverSized ]\n" );
		return;
	}

	const char *header = "       -w-- -h-- filt -fmt-- wrap  use  size --name-------\n";
	common->Printf( "\n%s", header );

	totalSize = 0;

	idList<sortedImage_t> sortedImages;
	sortedImage_t *sortedArray = NULL;
	if ( sorted || sortByName ) {
		sortedImages.SetNum( globalImages->images.Num() );
		sortedArray = sortedImages.Ptr();
	}

	for ( i = 0 ; i < globalImages->images.Num() ; i++ ) {
		image = globalImages->images[ i ];

		if ( uncompressedOnly ) {
			if ( image->IsCompressed() ) {
				continue;
			}
		}
		if ( unloaded == image->IsLoaded() ) {
			continue;
		}

		// only print duplicates (from mismatched wrap / clamp, etc)
		if ( duplicated ) {
			int j;
			for ( j = i+1 ; j < globalImages->images.Num() ; j++ ) {
				if ( idStr::Icmp( image->GetName(), globalImages->images[ j ]->GetName() ) == 0 ) {
					break;
				}
			}
			if ( j == globalImages->images.Num() ) {
				continue;
			}
		}

		if ( sorted || sortByName ) {
			sortedArray[count].image = image;
			sortedArray[count].size = image->StorageSize();
			sortedArray[count].index = i;
		} else {
			common->Printf( "%4i:",	i );
			image->Print();
		}
		totalSize += image->StorageSize();
		count++;
	}

	if ( sorted || sortByName ) {
		if ( sortByName) {
			qsort( sortedArray, count, sizeof( sortedImage_t ), R_QsortImageName );
		} else {
			qsort( sortedArray, count, sizeof( sortedImage_t ), R_QsortImageSizes );
		}
		partialSize = 0;
		for ( i = 0 ; i < count ; i++ ) {
			common->Printf( "%4i:",	sortedArray[i].index );
			sortedArray[i].image->Print();
			partialSize += sortedArray[i].image->StorageSize();
			if ( ( (i+1) % 10 ) == 0 ) {
				common->Printf( "-------- %5.1f of %5.1f megs --------\n", 
					partialSize / (1024*1024.0), totalSize / (1024*1024.0) );
			}
		}
	}

	common->Printf( "%s", header );
	common->Printf( " %i images (%i total)\n", count, globalImages->images.Num() );
	common->Printf( " %5.1f total megabytes of images\n\n\n", totalSize / (1024*1024.0) );
}

/*
==============
AllocImage

Allocates an idImage, adds it to the list,
copies the name, and adds it to the hash chain.
==============
*/
idImage *idImageManager::AllocImage( const char *name ) {
	if (strlen(name) >= MAX_IMAGE_NAME ) {
		common->Error ("idImageManager::AllocImage: \"%s\" is too long\n", name);
	}

	int hash = idStr( name ).FileNameHash();

	idImage * image = new idImage( name );

	imageHash.Add( hash, images.Append( image ) );

	return image;
}

/*
==============
AllocStandaloneImage

Allocates an idImage,does not add it to the list or hash chain

==============
*/
idImage *idImageManager::AllocStandaloneImage( const char *name ) {
	if (strlen(name) >= MAX_IMAGE_NAME ) {
		common->Error ("idImageManager::AllocImage: \"%s\" is too long\n", name);
	}

	idImage * image = new idImage( name );

	return image;
}

/*
==================
ImageFromFunction

Images that are procedurally generated are allways specified
with a callback which must work at any time, allowing the OpenGL
system to be completely regenerated if needed.
==================
*/
idImage *idImageManager::ImageFromFunction( const char *_name, void (*generatorFunction)( idImage *image ) ) {

	// strip any .tga file extensions from anywhere in the _name
	idStr name = _name;
	name.Replace( ".tga", "" );
	name.BackSlashesToSlashes();
	R_NormalizeInternalImageName( name );

	// see if the image already exists
	int hash = name.FileNameHash();
	for ( int i = imageHash.First( hash ); i != -1; i = imageHash.Next( i ) ) {
		idImage * image = images[i];
		if ( name.Icmp( image->GetName() ) == 0 ) {
			if ( image->generatorFunction != generatorFunction ) {
				common->DPrintf( "WARNING: reused image %s with mixed generators\n", name.c_str() );
			}
			return image;
		}
	}

	// create the image and issue the callback
	idImage	* image = AllocImage( name );

	image->generatorFunction = generatorFunction;

	// check for precompressed, load is from the front end
	image->referencedOutsideLevelLoad = true;
	image->ActuallyLoadImage( false );

	return image;
}


/*
===============
GetImageWithParameters
==============
*/
idImage	*idImageManager::GetImageWithParameters( const char *_name, textureFilter_t filter, textureRepeat_t repeat, textureUsage_t usage, cubeFiles_t cubeMap, bool allowDownSize, unsigned int flags ) const {
	if ( !_name || !_name[0] || idStr::Icmp( _name, "default" ) == 0 || idStr::Icmp( _name, "_default" ) == 0 ) {
		declManager->MediaPrint( "DEFAULTED\n" );
		return globalImages->defaultImage;
	}
	if ( usage == TD_DEFAULT ) {
		if ( idStr::Icmpn( _name, "fonts", 5 ) == 0 || idStr::Icmpn( _name, "newfonts", 8 ) == 0 ) {
			usage = TD_FONT;
		}
		if ( R_IsQ4LightImageNamespace( _name ) ) {
			usage = TD_LIGHT;
		}
	}
	// strip any .tga file extensions from anywhere in the _name, including image program parameters
	idStr name = _name;
	name.Replace( ".tga", "" );
	name.BackSlashesToSlashes();
	R_NormalizeInternalImageName( name );
	allowDownSize = R_AllowImageDownSizeForName( name.c_str(), allowDownSize );
	int hash = name.FileNameHash();
	for ( int i = imageHash.First( hash ); i != -1; i = imageHash.Next( i ) ) {
		idImage	* image = images[i];
		if ( name.Icmp( image->GetName() ) == 0 ) {
			// the built in's, like _white and _flat always match the other options
			if ( name[0] == '_' ) {
				return image;
			}
			if ( image->cubeFiles != cubeMap ) {
				common->Error( "Image '%s' has been referenced with conflicting cube map states", _name );
			}
			if ( image->filter != filter || image->repeat != repeat ) {
				// we might want to have the system reset these parameters on every bind and
				// share the image data
				continue;
			}
			if ( image->usage != usage || image->allowDownSize != allowDownSize || image->flags != flags ) {
				// If an image is used differently then we need 2 copies of it because usage affects the way it's compressed and swizzled
				continue;
			}
			return image;
		}
	}
	return NULL;
}
/*
===============
ImageFromFile

Finds or loads the given image, always returning a valid image pointer.
Loading of the image may be deferred for dynamic loading.
==============
*/
idImage	*idImageManager::ImageFromFile( const char *_name, textureFilter_t filter, 
						 textureRepeat_t repeat, textureUsage_t usage, cubeFiles_t cubeMap, bool allowDownSize, unsigned int flags ) {

	if ( !_name || !_name[0] || idStr::Icmp( _name, "default" ) == 0 || idStr::Icmp( _name, "_default" ) == 0 ) {
		declManager->MediaPrint( "DEFAULTED\n" );
		return globalImages->defaultImage;
	}
	if ( idStr::Icmpn( _name, "fonts", 5 ) == 0 || idStr::Icmpn( _name, "newfonts", 8 ) == 0 ) {
		usage = TD_FONT;
	}
	if ( R_IsQ4LightImageNamespace( _name ) ) {
		usage = TD_LIGHT;
	}

	// strip any .tga file extensions from anywhere in the _name, including image program parameters
	idStr name = _name;
	name.Replace( ".tga", "" );
	name.BackSlashesToSlashes();
	R_NormalizeInternalImageName( name );
	allowDownSize = R_AllowImageDownSizeForName( name.c_str(), allowDownSize );

	//
	// see if the image is already loaded, unless we
	// are in a reloadImages call
	//
	int hash = name.FileNameHash();
	for ( int i = imageHash.First( hash ); i != -1; i = imageHash.Next( i ) ) {
		idImage	* image = images[i];
		if ( name.Icmp( image->GetName() ) == 0 ) {
			// the built in's, like _white and _flat always match the other options
			if ( name[0] == '_' ) {
				return image;
			}
			if ( image->cubeFiles != cubeMap ) {
				common->Error( "Image '%s' has been referenced with conflicting cube map states", _name );
			}

			if ( image->filter != filter || image->repeat != repeat ) {
				// we might want to have the system reset these parameters on every bind and
				// share the image data
				continue;
			}
			if ( image->usage != usage || image->flags != flags ) {
				// If an image is used differently then we need 2 copies of it because usage affects the way it's compressed and swizzled
				continue;
			}

			const bool mergedAllowDownSize = image->allowDownSize && allowDownSize;
			const bool allowDownSizeChanged = image->allowDownSize != mergedAllowDownSize;
			image->allowDownSize = mergedAllowDownSize;
			image->usage = usage;
			image->levelLoadReferenced = true;

			if ( ( !insideLevelLoad  || preloadingMapImages ) && ( !image->IsLoaded() || allowDownSizeChanged ) ) {
				image->referencedOutsideLevelLoad = ( !insideLevelLoad && !preloadingMapImages );
				image->ActuallyLoadImage( false );	// load is from front end
				declManager->MediaPrint( "%ix%i %s (reload for mixed referneces)\n", image->GetUploadWidth(), image->GetUploadHeight(), image->GetName() );
			}
			return image;
		}
	}

	//
	// create a new image
	//
	idImage	* image = AllocImage( name );
	image->cubeFiles = cubeMap;
	image->usage = usage;
	image->filter = filter;
	image->repeat = repeat;
	image->allowDownSize = allowDownSize;
	image->flags = flags;

	image->levelLoadReferenced = true;

	// load it if we aren't in a level preload
	if ( !insideLevelLoad || preloadingMapImages ) {
		image->referencedOutsideLevelLoad = ( !insideLevelLoad && !preloadingMapImages );
		image->ActuallyLoadImage( false );	// load is from front end
		declManager->MediaPrint( "%ix%i %s\n", image->GetUploadWidth(), image->GetUploadHeight(), image->GetName() );
	} else {
		declManager->MediaPrint( "%s\n", image->GetName() );
	}

	return image;
}

/*
===============
ImageHandleDeferred

Returns an image handle without forcing file IO or level-load residency.
The texture can then be streamed in lazily on first bind.
===============
*/
idImage *idImageManager::ImageHandleDeferred( const char *_name, textureFilter_t filter,
						 textureRepeat_t repeat, textureUsage_t usage, cubeFiles_t cubeMap, bool allowDownSize, unsigned int flags ) {

	if ( !_name || !_name[0] || idStr::Icmp( _name, "default" ) == 0 || idStr::Icmp( _name, "_default" ) == 0 ) {
		declManager->MediaPrint( "DEFAULTED\n" );
		return globalImages->defaultImage;
	}
	if ( usage == TD_DEFAULT ) {
		// Keep the legacy convenience remap for generic callers, but preserve any
		// explicit material-requested usage class such as TD_HIGH_QUALITY.
		if ( idStr::Icmpn( _name, "fonts", 5 ) == 0 || idStr::Icmpn( _name, "newfonts", 8 ) == 0 ) {
			usage = TD_FONT;
		}
		if ( R_IsQ4LightImageNamespace( _name ) ) {
			usage = TD_LIGHT;
		}
	}

	idStr name = _name;
	name.Replace( ".tga", "" );
	name.BackSlashesToSlashes();
	R_NormalizeInternalImageName( name );
	allowDownSize = R_AllowImageDownSizeForName( name.c_str(), allowDownSize );

	int hash = name.FileNameHash();
	for ( int i = imageHash.First( hash ); i != -1; i = imageHash.Next( i ) ) {
		idImage *image = images[i];
		if ( name.Icmp( image->GetName() ) == 0 ) {
			if ( name[0] == '_' ) {
				return image;
			}
			if ( image->cubeFiles != cubeMap ) {
				common->Error( "Image '%s' has been referenced with conflicting cube map states", _name );
			}
			if ( image->filter != filter || image->repeat != repeat ) {
				continue;
			}
			if ( image->usage != usage || image->flags != flags ) {
				continue;
			}
			image->allowDownSize = image->allowDownSize && allowDownSize;
			return image;
		}
	}

	idImage *image = AllocImage( name );
	image->cubeFiles = cubeMap;
	image->usage = usage;
	image->filter = filter;
	image->repeat = repeat;
	image->allowDownSize = allowDownSize;
	image->flags = flags;
	return image;
}

/*
========================
idImageManager::ScratchImage
========================
*/
idImage * idImageManager::ScratchImage( const char *_name, idImageOpts *imgOpts, textureFilter_t filter, textureRepeat_t repeat, textureUsage_t usage ) {
	if ( !_name || !_name[0] ) {
		idLib::FatalError( "idImageManager::ScratchImage called with empty name" );
	}

	if ( imgOpts == NULL ) {
		idLib::FatalError( "idImageManager::ScratchImage called with NULL imgOpts" );
	}

	idStr name = _name;
	name.BackSlashesToSlashes();
	R_NormalizeInternalImageName( name );

	//
	// see if the image is already loaded, unless we
	// are in a reloadImages call
	//
	int hash = name.FileNameHash();
	for ( int i = imageHash.First( hash ); i != -1; i = imageHash.Next( i ) ) {
		idImage	* image = images[i];
		if ( name.Icmp( image->GetName() ) == 0 ) {
			image->usage = usage;
			image->levelLoadReferenced = true;
			image->referencedOutsideLevelLoad = true;

			if ( image->GetFilter() != filter || image->GetRepeat() != repeat || !( image->GetOpts() == *imgOpts ) || !image->IsLoaded() ) {
				image->AllocImage( *imgOpts, filter, repeat );
			}
			return image;
		}
	}

	// clamp is the only repeat mode that makes sense for cube maps, but
	// some platforms let them stay in repeat mode and get border seam issues
	if ( imgOpts->textureType == TT_CUBIC && repeat != TR_CLAMP ) {
		repeat = TR_CLAMP;
	}

	//
	// create a new image
	//
	idImage* newImage = AllocImage( name );
	if ( newImage != NULL ) {
		newImage->usage = usage;
		newImage->levelLoadReferenced = true;
		newImage->referencedOutsideLevelLoad = true;
		newImage->AllocImage( *imgOpts, filter, repeat );
	}
	return newImage;
}

/*
===============
idImageManager::GetImage
===============
*/
idImage *idImageManager::GetImage( const char *_name ) const {

	if ( !_name || !_name[0] || idStr::Icmp( _name, "default" ) == 0 || idStr::Icmp( _name, "_default" ) == 0 ) {
		declManager->MediaPrint( "DEFAULTED\n" );
		return globalImages->defaultImage;
	}

	// strip any .tga file extensions from anywhere in the _name, including image program parameters
	idStr name = _name;
	name.Replace( ".tga", "" );
	name.BackSlashesToSlashes();
	R_NormalizeInternalImageName( name );

	//
	// look in loaded images
	//
	int hash = name.FileNameHash();
	for ( int i = imageHash.First( hash ); i != -1; i = imageHash.Next( i ) ) {
		idImage * image = images[i];
		if ( name.Icmp( image->GetName() ) == 0 ) {
			return image;
		}
	}

	return NULL;
}

/*
===============
PurgeAllImages
===============
*/
void idImageManager::PurgeAllImages() {
	int		i;
	idImage	*image;

	for ( i = 0; i < images.Num() ; i++ ) {
		image = images[i];

		image->PurgeImage();
	}
}

/*
===============
ReloadImages
===============
*/
void idImageManager::ReloadImages( bool all ) {
	for ( int i = 0 ; i < globalImages->images.Num() ; i++ ) {
		globalImages->images[ i ]->Reload( all );
	}
}

/*
===============
R_CombineCubeImages_f

Used to combine animations of six separate tga files into
a serials of 6x taller tga files, for preparation to roq compress
===============
*/
void R_CombineCubeImages_f( const idCmdArgs &args ) {
	if ( args.Argc() != 2 ) {
		common->Printf( "usage: combineCubeImages <baseName>\n" );
		common->Printf( " combines basename[1-6][0001-9999].tga to basenameCM[0001-9999].tga\n" );
		common->Printf( " 1: forward 2:right 3:back 4:left 5:up 6:down\n" );
		return;
	}

	idStr	baseName = args.Argv( 1 );
	common->SetRefreshOnPrint( true );

	for ( int frameNum = 1 ; frameNum < 10000 ; frameNum++ ) {
		idStr	filename;
		byte	*pics[6];
		int		width = 0, height = 0;
		int		side;
		int		orderRemap[6] = { 1,3,4,2,5,6 };
		for ( side = 0 ; side < 6 ; side++ ) {
			char suffix[32];
			idStr::snPrintf( suffix, sizeof( suffix ), "%i%04i.tga", orderRemap[side], frameNum );
			filename = baseName;
			filename += suffix;

			common->Printf( "reading %s\n", filename.c_str() );
			R_LoadImage( filename.c_str(), &pics[side], &width, &height, NULL, true );

			if ( !pics[side] ) {
				common->Printf( "not found.\n" );
				break;
			}

			// convert from "camera" images to native cube map images
			switch( side ) {
			case 0:	// forward
				R_RotatePic( pics[side], width);
				break;
			case 1:	// back
				R_RotatePic( pics[side], width);
				R_HorizontalFlip( pics[side], width, height );
				R_VerticalFlip( pics[side], width, height );
				break;
			case 2:	// left
				R_VerticalFlip( pics[side], width, height );
				break;
			case 3:	// right
				R_HorizontalFlip( pics[side], width, height );
				break;
			case 4:	// up
				R_RotatePic( pics[side], width);
				break;
			case 5: // down
				R_RotatePic( pics[side], width);
				break;
			}
		}

		if ( side != 6 ) {
			for ( int i = 0 ; i < side ; i++ ) {
				Mem_Free( pics[i] );
			}
			break;
		}

		if ( width <= 0 || height <= 0 || height > idMath::INT_MAX / 6 ) {
			common->Warning( "combineCubeImages: invalid image dimensions %dx%d", width, height );
			for ( int i = 0 ; i < 6 ; i++ ) {
				Mem_Free( pics[i] );
			}
			break;
		}
		const int combinedHeight = height * 6;
		if ( width > idMath::INT_MAX / combinedHeight || width * combinedHeight > idMath::INT_MAX / 4 ) {
			common->Warning( "combineCubeImages: image dimensions are too large %dx%d", width, height );
			for ( int i = 0 ; i < 6 ; i++ ) {
				Mem_Free( pics[i] );
			}
			break;
		}

		const int combinedBytes = width * combinedHeight * 4;
		idTempArray<byte> buf( combinedBytes );
		byte	*combined = (byte *)buf.Ptr();
		for (  side = 0 ; side < 6 ; side++ ) {
			memcpy( combined+width*height*4*side, pics[side], width*height*4 );
			Mem_Free( pics[side] );
		}
		char suffix[32];
		idStr::snPrintf( suffix, sizeof( suffix ), "CM%04i.tga", frameNum );
		filename = baseName;
		filename += suffix;

		common->Printf( "writing %s\n", filename.c_str() );
		R_WriteTGA( filename.c_str(), combined, width, combinedHeight );
	}
	common->SetRefreshOnPrint( false );
}

/*
===============
UnbindAll
===============
*/
void idImageManager::UnbindAll() {
	int oldTMU = backEnd.glState.currenttmu;
	for ( int i = 1; i < MAX_MULTITEXTURE_UNITS; ++i ) {
		GL_SelectTextureNoClient(i);
		BindNull();
	}
	backEnd.glState.currenttmu = oldTMU;
}

/*
===============
BindNull
===============
*/
void idImageManager::BindNull() {
	tmu_t* tmu;

	tmu = &backEnd.glState.tmu[backEnd.glState.currenttmu];

	RB_LogComment("BindNull()\n");
	if (tmu->textureType == TT_CUBIC) {
		glDisable(GL_TEXTURE_CUBE_MAP_EXT);
	}
	else if (tmu->textureType == TT_2D) {
		glDisable(GL_TEXTURE_2D);
	}
	tmu->textureType = TT_DISABLED;

}

/*
===============
Init
===============
*/
void idImageManager::Init() {

	images.Resize( 1024, 1024 );
	imageHash.ResizeIndex( 1024 );

	CreateIntrinsicImages();

	cmdSystem->AddCommand( "reloadImages", R_ReloadImages_f, CMD_FL_RENDERER, "reloads images" );
	cmdSystem->AddCommand( "listImages", R_ListImages_f, CMD_FL_RENDERER, "lists images" );
	cmdSystem->AddCommand( "imageDDSSelfTest", R_ImageDDSSelfTest_f, CMD_FL_RENDERER, "validates DDS naming and BC7 metadata handling" );
	cmdSystem->AddCommand( "combineCubeImages", R_CombineCubeImages_f, CMD_FL_RENDERER, "combines six images for roq compression" );

	// should forceLoadImages be here?
}

/*
===============
Shutdown
===============
*/
void idImageManager::Shutdown() {
	images.DeleteContents( true );
	imageHash.Clear();

}

/*
====================
idImageManager::BeginLevelLoad
Frees all images used by the previous level
====================
*/
void idImageManager::BeginLevelLoad() {
	insideLevelLoad = true;

	for ( int i = 0 ; i < images.Num() ; i++ ) {
		idImage	*image = images[ i ];
		image->ClearUseCount();

		// generator function images are always kept around
		if ( image->generatorFunction || image->GetOpts().isPersistant ) {
			continue;
		}

		if ( !image->referencedOutsideLevelLoad && image->IsLoaded() ) {
			image->PurgeImage();
			//idLib::Printf( "purging %s\n", image->GetName() );
		} else {
			//idLib::Printf( "not purging %s\n", image->GetName() );
		}

		image->levelLoadReferenced = false;
	}
}


/*
====================
idImageManager::ExcludePreloadImage
====================
*/
bool idImageManager::ExcludePreloadImage( const char *name ) {
	idStr imgName = name;
	imgName.ToLower();
	if ( imgName.Find( "newfonts/", false ) >= 0 ) {
		return true;
	}
	if ( imgName.Find( "generated/swf/", false ) >= 0 ) {
		return true;
	}
	if ( imgName.Find( "/loadscreens/", false ) >= 0 ) {
		return true;
	}
	return false;
}

/*
===============
idImageManager::LoadLevelImages
===============
*/
int idImageManager::LoadLevelImages( bool pacifier ) {
	int	loadCount = 0;
	for ( int i = 0 ; i < images.Num() ; i++ ) {
		if ( pacifier ) {
			//common->UpdateLevelLoadPacifier();

		}

		idImage	*image = images[ i ];
		if ( image->generatorFunction ) {
			continue;
		}
		if ( image->levelLoadReferenced && !image->IsLoaded() ) {
			loadCount++;
			image->ActuallyLoadImage( false );
			session->AdvanceLoadingAssetQueue( 1 );
		}
	}
	return loadCount;
}

/*
===============
idImageManager::CountPendingLevelLoads
===============
*/
int idImageManager::CountPendingLevelLoads() const {
	int pendingCount = 0;

	for ( int i = 0; i < images.Num(); i++ ) {
		const idImage *image = images[ i ];
		if ( image->generatorFunction ) {
			continue;
		}
		if ( image->levelLoadReferenced && !image->IsLoaded() ) {
			pendingCount++;
		}
	}

	return pendingCount;
}

/*
===============
idImageManager::EndLevelLoad
===============
*/
void idImageManager::EndLevelLoad() {
	insideLevelLoad = false;

	common->Printf( "----- idImageManager::EndLevelLoad -----\n" );
	int start = Sys_Milliseconds();
	int	loadCount = LoadLevelImages( true );

	int	end = Sys_Milliseconds();
	common->Printf( "%5i images loaded in %5.1f seconds\n", loadCount, (end-start) * 0.001 );
	common->Printf( "----------------------------------------\n" );
	//R_ListImages_f( idCmdArgs( "sorted sorted", false ) );
}

/*
===============
idImageManager::StartBuild
===============
*/
void idImageManager::StartBuild() {
}

/*
===============
idImageManager::FinishBuild
===============
*/
void idImageManager::FinishBuild( bool removeDups ) {
}

/*
===============
idImageManager::PrintMemInfo
===============
*/
void idImageManager::PrintMemInfo( MemInfo_t *mi ) {
	int i, j, total = 0;
	int *sortIndex;
	idFile *f;

	f = fileSystem->OpenFileWrite( mi->filebase + "_images.txt" );
	if ( !f ) {
		return;
	}

	// sort first
	sortIndex = new int[images.Num()];

	for ( i = 0; i < images.Num(); i++ ) {
		sortIndex[i] = i;
	}

	for ( i = 0; i < images.Num() - 1; i++ ) {
		for ( j = i + 1; j < images.Num(); j++ ) {
			if ( images[sortIndex[i]]->StorageSize() < images[sortIndex[j]]->StorageSize() ) {
				int temp = sortIndex[i];
				sortIndex[i] = sortIndex[j];
				sortIndex[j] = temp;
			}
		}
	}

	// print next
	for ( i = 0; i < images.Num(); i++ ) {
		idImage *im = images[sortIndex[i]];
		int size;

		size = im->StorageSize();
		total += size;

		f->Printf( "%s %3i %s\n", idStr::FormatNumber( size ).c_str(), im->refCount, im->GetName() );
	}

	delete [] sortIndex;
	mi->imageAssetsTotal = total;

	f->Printf( "\nTotal image bytes allocated: %s\n", idStr::FormatNumber( total ).c_str() );
	fileSystem->CloseFile( f );
}
