/*

OOCombinedEmissionMapGenerator.m


Copyright (C) 2010-2013 Jens Ayton

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

*/

#import "OOCombinedEmissionMapGenerator.h"

#import "OOColor.h"
#import "OOPixMapChannelOperations.h"
#import "OOTextureScaling.h"
#import "OOTextureInternal.h"
#import "OOMaterialSpecifier.h"
#import "OOCollectionExtractors.h"


#define DUMP_COMBINER	0


static OOColor *ModulateColor(OOColor *a, OOColor *b);
static void ScaleToMatch(OOPixMap *pmA, OOPixMap *pmB);


@interface OOCombinedEmissionMapGenerator (Private)

- (id) initWithEmissionMapSpec:(NSDictionary *)emissionMapSpec
				 emissionColor:(OOColor *)emissionColor
					diffuseMap:(OOTexture *)diffuseMap
				  diffuseColor:(OOColor *)diffuseColor
		   illuminationMapSpec:(NSDictionary *)illuminationMapSpec
			 illuminationColor:(OOColor *)illuminationColor
				 isCombinedMap:(BOOL)isCombinedMap
			  optionsSpecifier:(NSDictionary *)spec;

- (NSString *)constructCacheKey;

@end


@implementation OOCombinedEmissionMapGenerator

- (id) initWithEmissionMapSpec:(NSDictionary *)emissionMapSpec
				 emissionColor:(OOColor *)emissionColor
					diffuseMap:(OOTexture *)diffuseMap
				  diffuseColor:(OOColor *)diffuseColor
		   illuminationMapSpec:(NSDictionary *)illuminationMapSpec
			 illuminationColor:(OOColor *)illuminationColor
			  optionsSpecifier:(NSDictionary *)spec
{
	return [self initWithEmissionMapSpec:emissionMapSpec
						   emissionColor:emissionColor
							  diffuseMap:diffuseMap
							diffuseColor:diffuseColor
					 illuminationMapSpec:illuminationMapSpec
					   illuminationColor:illuminationColor
						   isCombinedMap:NO
						optionsSpecifier:spec];
}


- (id) initWithEmissionAndIlluminationMapSpec:(NSDictionary *)emissionAndIlluminationMapSpec
								   diffuseMap:(OOTexture *)diffuseMap
								 diffuseColor:(OOColor *)diffuseColor
								emissionColor:(OOColor *)emissionColor
							illuminationColor:(OOColor *)illuminationColor
							 optionsSpecifier:(NSDictionary *)spec
{
	return [self initWithEmissionMapSpec:emissionAndIlluminationMapSpec
						   emissionColor:emissionColor
							  diffuseMap:diffuseMap
							diffuseColor:diffuseColor
					 illuminationMapSpec:nil
					   illuminationColor:illuminationColor
						   isCombinedMap:YES
						optionsSpecifier:spec];
}


- (id) initWithEmissionMapSpec:(NSDictionary *)emissionMapSpec
				 emissionColor:(OOColor *)emissionColor
					diffuseMap:(OOTexture *)diffuseMap
				  diffuseColor:(OOColor *)diffuseColor
		   illuminationMapSpec:(NSDictionary *)illuminationMapSpec
			 illuminationColor:(OOColor *)illuminationColor
				 isCombinedMap:(BOOL)isCombinedMap
			  optionsSpecifier:(NSDictionary *)spec
{
	if (emissionMapSpec == nil && illuminationMapSpec == nil)
	{
		[self release];
		return nil;
	}
	
	NSParameterAssert(illuminationMapSpec == nil || !isCombinedMap);
	
	uint32_t options;
	GLfloat anisotropy;
	GLfloat lodBias;
	OOInterpretTextureSpecifier(spec, NULL, &options, &anisotropy, &lodBias, YES);
	options = OOApplyTextureOptionDefaults(options);
	
	if ((self = [super initWithPath:@"<generated emission map>" options:options]))
	{
		/*	Illumination contribution is:
			illuminationMap * illuminationColor * diffuseMap * diffuseColor
			Since illuminationColor and diffuseColor aren't used otherwise,
			we may as well combine them up front.
		*/
		illuminationColor = ModulateColor(diffuseColor, illuminationColor);
										  
		if ([emissionColor isWhite])  emissionColor = nil;
		if ([illuminationColor isWhite])  illuminationColor = nil;
		if (!isCombinedMap && illuminationMapSpec == nil)  diffuseMap = nil;	// Diffuse map is only used with illumination
		
		// Insert extraShrink flag here instead of using extraOptions later because we need it in cache key too.
		NSMutableDictionary *emissionSpec = [emissionMapSpec mutableCopy];
		[emissionSpec oo_setBool:YES forKey:kOOTextureSpecifierExtraShrinkKey];
		_emissionSpec = emissionSpec;
		
		NSMutableDictionary *illuminationSpec = [illuminationMapSpec mutableCopy];
		[illuminationSpec oo_setBool:YES forKey:kOOTextureSpecifierExtraShrinkKey];
		_illuminationSpec = illuminationSpec;
		
		_diffuseMap = [diffuseMap retain];
		
		_emissionColor = [emissionColor retain];
		_illuminationColor = [illuminationColor retain];
		_isCombinedMap = isCombinedMap;
		
		_textureOptions = options;
		_anisotropy = anisotropy;
		_lodBias = lodBias;
		
		_cacheKey = [self constructCacheKey];
		
		if ([OOTexture existingTextureForKey:_cacheKey] == nil)
		{
			/*	Extract pixmap from diffuse map. This must be done in the main
				thread even if scheduling is fixed, because it might involve
				reading back pixels from OpenGL.
			*/
			if (diffuseMap != nil)
			{
				_diffusePx = [diffuseMap copyPixMapRepresentation];
#ifndef NDEBUG
				_diffuseDesc = [[diffuseMap shortDescription] copy];
#endif
			}
			
			/*	Extract emission and illumination pixmaps from loaders. Ideally,
				this would be done asynchronously, but that requires dependency
				management in OOAsyncWorkManager.
			*/
			OOTextureDataFormat format;
			if (_emissionSpec != nil)
			{
				OOTextureLoader *emissionMapLoader = [OOTextureLoader loaderWithTextureSpecifier:_emissionSpec
																					extraOptions:0
																						  folder:@"Textures"];
				[emissionMapLoader getResult:&_emissionPx format:&format originalWidth:NULL originalHeight:NULL];
#ifndef NDEBUG
				_emissionDesc = [[emissionMapLoader shortDescription] copy];
#endif
			}
			if (_illuminationSpec != nil)
			{
				OOTextureLoader *illuminationMapLoader = [OOTextureLoader loaderWithTextureSpecifier:_illuminationSpec
																						extraOptions:0
																							  folder:@"Textures"];
				[illuminationMapLoader getResult:&_illuminationPx format:&format originalWidth:NULL originalHeight:NULL];
#ifndef NDEBUG
				_illuminationDesc = [[illuminationMapLoader shortDescription] copy];
#endif
			}
		}
	}
	
	return self;
}


- (NSString *)constructCacheKey
{
	NSMutableString *cacheKey = [NSMutableString string];
	
	if (_isCombinedMap)  [cacheKey appendString:@"combined emission and illumination map;"];
	else  if (_emissionSpec == nil)  [cacheKey appendString:@"illumination map;"];
	else  if (_illuminationSpec == nil)  [cacheKey appendString:@"emission map;"];
	else [cacheKey appendString:@"merged emission and illumination map;"];
	
	NSString *emissionDesc = nil;
	if (_emissionSpec != nil)
	{
		emissionDesc = OOTextureCacheKeyForSpecifier(_emissionSpec);
		[cacheKey appendFormat:@"emission:{%@}", emissionDesc];
		if (_emissionColor != nil)  [cacheKey appendFormat:@"*%@", [_emissionColor rgbaDescription]];
		[cacheKey appendString:@";"];
	}
	
	NSString *illuminationDesc = nil;
	if (_isCombinedMap)
	{
		illuminationDesc = [NSString stringWithFormat:@"%@:a", emissionDesc];
	}
	else if (_illuminationSpec != nil)
	{
		illuminationDesc = OOTextureCacheKeyForSpecifier(_illuminationSpec);
	}
	
	if (illuminationDesc != nil)
	{
		[cacheKey appendFormat:@"illumination:{%@}*{%@}", illuminationDesc, [_diffuseMap cacheKey]];
		if (_illuminationColor != nil)  [cacheKey appendFormat:@"*%@", [_illuminationColor rgbaDescription]];
		[cacheKey appendString:@";"];
	}
	
	return cacheKey;
}


- (void) dealloc
{
	DESTROY(_emissionSpec);
	DESTROY(_illuminationSpec);
	DESTROY(_diffuseMap);
	
	OOFreePixMap(&_emissionPx);
	OOFreePixMap(&_illuminationPx);
	OOFreePixMap(&_diffusePx);
	DESTROY(_emissionColor);
	DESTROY(_illuminationColor);
	
#ifndef NDEBUG
	DESTROY(_diffuseDesc);
	DESTROY(_emissionDesc);
	DESTROY(_illuminationDesc);
#endif
	
	[super dealloc];
}


#ifndef NDEBUG
- (NSString *) descriptionComponents
{
	NSMutableString *result = [NSMutableString string];
	BOOL haveIllumination = NO;
	
	if (_emissionDesc != nil)
	{
		[result appendFormat:@"emission map: %@", _emissionDesc];
		if (_isCombinedMap)
		{
			[result appendString:@".rgb"];
		}
		if (_emissionColor != nil)
		{
			[result appendFormat:@" * %@", [_emissionColor rgbaDescription]];
		}
		
		if (_isCombinedMap)
		{
			[result appendFormat:@", illumination map: %@.a", _emissionDesc];
			haveIllumination = YES;
		}
	}
	
	if (_illuminationDesc != nil)
	{
		if (_emissionDesc != nil)  [result appendString:@", "];
		[result appendFormat:@"illumination map: %@", _illuminationDesc];
		haveIllumination = YES;
	}
	
	if (haveIllumination)
	{
		if (_diffuseDesc != nil)
		{
			[result appendFormat:@" * %@", _diffuseDesc];
		}
		if (_illuminationColor != nil)
		{
			[result appendFormat:@" * %@", [_illuminationColor rgbaDescription]];
		}
	}
	
	return result;
}
#endif


- (uint32_t) textureOptions
{
	return _textureOptions;
}


- (GLfloat) anisotropy
{
	return _anisotropy;
}


- (GLfloat) lodBias
{
	return _lodBias;
}


- (NSString *) cacheKey
{
	return _cacheKey;
}


- (void) loadTexture
{
	OOPixMap illuminationPx = kOONullPixMap;
	BOOL haveEmission = NO, haveIllumination = NO, haveDiffuse = NO;
	
#if DUMP_COMBINER
	static unsigned sTexID = 0;
	unsigned texID = ++sTexID, dumpCount = 0;
	
#define DUMP(pm, label) OODumpPixMap(pm, [NSString stringWithFormat:@"lightmap %u.%u - %@", texID, ++dumpCount, label]);
#else
#define DUMP(pm, label) do {} while (0)
#endif
	
	haveEmission = !OOIsNullPixMap(_emissionPx);
	if (haveEmission)  DUMP(_emissionPx, @"source emission map");
	
	// Extract illumination component if emission_and_illumination_map.
	if (haveEmission && _isCombinedMap && OOPixMapFormatHasAlpha(_emissionPx.format))
	{
		OOPixMapToRGBA(&_emissionPx);
		illuminationPx = OODuplicatePixMap(_emissionPx, 0);
		OOExtractPixMapChannel(&illuminationPx, 3, YES);
		haveIllumination = YES;
		DUMP(illuminationPx, @"extracted illumination map");
	}
	
	// Tint emission map if necessary.
	if (haveEmission && _emissionColor != nil)
	{
		OOPixMapModulateUniform(&_emissionPx, [_emissionColor redComponent], [_emissionColor greenComponent], [_emissionColor blueComponent], 1.0);
		DUMP(_emissionPx, @"modulated emission map");
	}
	
	if (!OOIsNullPixMap(_illuminationPx))
	{
		NSAssert(!_isCombinedMap, @"OOCombinedEmissionMapGenerator configured with both illumination map and combined emission/illumination map.");
		
		illuminationPx = _illuminationPx;
		_illuminationPx.pixels = NULL;
		haveIllumination = YES;
		DUMP(illuminationPx, @"source illumination map");
	}
	
	// Tint illumination map if necessary.
	if (haveIllumination && _illuminationColor != nil)
	{
		OOPixMapModulateUniform(&illuminationPx, [_illuminationColor redComponent], [_illuminationColor greenComponent], [_illuminationColor blueComponent], 1.0);
		DUMP(illuminationPx, @"modulated illumination map");
	}
	
	// Load diffuse map and combine with illumination map.
	haveDiffuse = !OOIsNullPixMap(_diffusePx);
	if (haveDiffuse)  DUMP(_diffusePx, @"source diffuse map");
	
	if (haveIllumination && haveDiffuse)
	{
		// Modulate illumination with diffuse map.
		ScaleToMatch(&_diffusePx, &illuminationPx);
		OOPixMapToRGBA(&_diffusePx);
		OOPixMapModulatePixMap(&illuminationPx, _diffusePx);
		DUMP(illuminationPx, @"combined diffuse and illumination map");
	}
	OOFreePixMap(&_diffusePx);
	
	if (haveIllumination)
	{
		if (haveEmission)
		{
			OOPixMapToRGBA(&illuminationPx);
			OOPixMapAddPixMap(&_emissionPx, illuminationPx);
			OOFreePixMap(&illuminationPx);
			DUMP(_emissionPx, @"combined emission and illumination map");
		}
		else if (haveIllumination)
		{
			// No explicit emission map -> modulated illumination map is our only emission map.
			_emissionPx = illuminationPx;
			haveEmission = YES;
			illuminationPx.pixels = NULL;
		}
		haveIllumination = NO;	// Either way, illumination is now baked into emission.
	}
	
	(void)haveEmission;
	(void)haveIllumination;
	
	// Done: emissionPx now contains combined emission map.
	OOCompactPixMap(&_emissionPx);
	if (OOIsValidPixMap(_emissionPx))
	{
		_data = _emissionPx.pixels;
		_width = _emissionPx.width;
		_height = _emissionPx.height;
		_rowBytes = _emissionPx.rowBytes;
		_format = _emissionPx.format;
		
		_emissionPx.pixels = NULL;	// So it won't be freed by -dealloc.
	}
	if (_data == NULL)
	{
		OOLogERR(@"texture.combinedEmissionMap.error", @"Unknown error loading %@", self);
	}
}

@end


static OOColor *ModulateColor(OOColor *a, OOColor *b)
{
	if (a == nil)  return b;
	if (b == nil)  return a;
	
	OORGBAComponents ac, bc;
	ac = [a rgbaComponents];
	bc = [b rgbaComponents];
	
	ac.r *= bc.r;
	ac.g *= bc.g;
	ac.b *= bc.b;
	ac.a *= bc.a;
	
	return [OOColor colorWithRGBAComponents:ac];
}


static void ScaleToMatch(OOPixMap *pmA, OOPixMap *pmB)
{
	NSCParameterAssert(pmA != NULL && pmB != NULL && OOIsValidPixMap(*pmA) && OOIsValidPixMap(*pmB));
	
	OOPixMapDimension minWidth = MIN(pmA->width, pmB->width);
	OOPixMapDimension minHeight = MIN(pmA->height, pmB->height);
	
	if (pmA->width != minWidth || pmA->height != minHeight)
	{
		*pmA = OOScalePixMap(*pmA, minWidth, minHeight, NO);
	}
	if (pmB->width != minWidth || pmB->height != minHeight)
	{
		*pmB = OOScalePixMap(*pmB, minWidth, minHeight, NO);
	}
}
