//
//  DrawingUtilities.m
//  Fluid
//
//  Created by Jason Harris on 1/4/07.
//  Copyright 2007 Geekspiff. All rights reserved.
//

#import "DrawingUtilities.h"
#import <Accelerate/Accelerate.h>
#import <QuartzCore/QuartzCore.h>


#if defined(__BIG_ENDIAN__)
	CGImageAlphaInfo const kDefaultAlphaLocation = kCGImageAlphaPremultipliedLast;
#else
	CGImageAlphaInfo const kDefaultAlphaLocation = kCGImageAlphaPremultipliedFirst;
#endif


@implementation DrawingUtilities


+ (DrawingUtilities *)sharedUtilities
{
    static DrawingUtilities *sSingletonInstance = nil;
    if (!sSingletonInstance)
    {
        sSingletonInstance = [[[self class] alloc] init];
        NSParameterAssert( sSingletonInstance != nil );
    }
    return sSingletonInstance;
}


- (void)_killSharedBitmapCache
{
    if (_sharedBitmapData)      free(_sharedBitmapData);            _sharedBitmapData    = NULL;
    if (_sharedBitmapContext)   CFRelease(_sharedBitmapContext);    _sharedBitmapContext = NULL;
    [_sharedCIContext release];                                     _sharedCIContext     = nil;
    _sharedSize = CGSizeZero;
}


- (void)dealloc
{
    if (_screenColorspace) CGColorSpaceRelease(_screenColorspace);
    [self _killSharedBitmapCache];
    [super dealloc];
}


- (CGColorSpaceRef)getScreenColorspace
{	
	if (!_screenColorspace)
	{
		CMProfileRef systemProfile = NULL;
		OSStatus status = CMGetSystemProfile(&systemProfile);
		NSParameterAssert( noErr == status);
		_screenColorspace = CGColorSpaceCreateWithPlatformColorSpace(systemProfile);
		CMCloseProfile(systemProfile);
	}
	return _screenColorspace;
}


- (size_t)optimalRowBytesForWidth: (size_t)width bytesPerPixel: (size_t)bytesPerPixel
{
    size_t rowBytes = width * bytesPerPixel;
    
    //Widen rowBytes out to a integer multiple of 16 bytes
    rowBytes = (rowBytes + 15) & ~15;
    
    //Make sure we are not an even power of 2 wide. 
    //Will loop a few times for rowBytes <= 16.
    while( 0 == (rowBytes & (rowBytes - 1) ) )
        rowBytes += 16;
    
    return rowBytes;
}
    
    
- (void)_createContextsForImageSize: (CGSize)imageSize
{
    if (imageSize.width > _sharedSize.width || imageSize.height > _sharedSize.height)
        [self _killSharedBitmapCache];
    if (_sharedBitmapData) return;
    
    BOOL success = NO;
    size_t width = (size_t)imageSize.width;
    size_t height = (size_t)imageSize.height;
    size_t bitsPerComponent = 8;
    size_t bytesPerRow = [self optimalRowBytesForWidth: width bytesPerPixel: 4];
    CGColorSpaceRef colorspace = [self getScreenColorspace];
    CGBitmapInfo bitmapInfo = kDefaultAlphaLocation | kCGBitmapByteOrderDefault;
    
    _sharedBitmapData = malloc(bytesPerRow * height);
    if (_sharedBitmapData)
    {
        _sharedBitmapContext = CGBitmapContextCreate(_sharedBitmapData, width, height, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo);
        if (_sharedBitmapContext)
        {
            NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: 
                (id)colorspace,     kCIContextOutputColorSpace, 
                (id)colorspace,     kCIContextWorkingColorSpace, 
                nil];
            _sharedCIContext = [[CIContext contextWithCGContext: _sharedBitmapContext options: options] retain];
            if (_sharedCIContext)
                success = YES;
        }
    }
    
    if (!success)
        [self _killSharedBitmapCache];
    else
        _sharedSize = imageSize;
    NSParameterAssert( success == YES );
}


- (uint32_t const *)createPremultipliedRGBA_8888_BitmapForCIImage: (CIImage *)image rowBytes: (size_t *)rowBytes
{
    NSParameterAssert( image != nil );
    NSParameterAssert( rowBytes != NULL );
    
    // - Get the geometry of the image
    
    CGRect extent = [image extent];
    CGRect zeroRect = {CGPointZero, extent.size};
    NSParameterAssert( CGRectIsInfinite(extent) == NO );
    size_t destWidth = (size_t)extent.size.width;
    size_t destHeight = (size_t)extent.size.height;
    
    // - Create shared contexts and render the image into them
    
    [self _createContextsForImageSize: extent.size];
    CGContextClearRect(_sharedBitmapContext, zeroRect);
    [_sharedCIContext drawImage: image atPoint: CGPointZero fromRect: extent];
    CGContextFlush(_sharedBitmapContext);
    
    // - Get the geometry of the shared contexts
    
    size_t sharedHeight = CGBitmapContextGetHeight(_sharedBitmapContext);
    *rowBytes = CGBitmapContextGetBytesPerRow(_sharedBitmapContext);
    
    // - Trim the bitmap to the desired height - we don't care about extra rows
    
    uint32_t const *sharedBitmap = _sharedBitmapData;
    sharedBitmap = (uint32_t const *) ((uintptr_t)sharedBitmap + *rowBytes * (sharedHeight - destHeight));
    
    // - Create the src for the vImage operation
    
    vImage_Buffer src, dest;
    src.data = (void *)sharedBitmap;
    src.width = destWidth;
    src.height = destHeight;
    src.rowBytes = *rowBytes;
    
    // - Create the destination buffer
    
    size_t bufferSize = *rowBytes * destHeight;
    uint32_t *destBuffer = malloc(bufferSize);
    NSParameterAssert( destBuffer != NULL );
    
    // - Create the destination for the vImage operation
    
    dest.data = destBuffer;
    dest.width = destWidth;
    dest.height = destHeight;
    dest.rowBytes = *rowBytes;
	
#if defined(__BIG_ENDIAN__)
	memcpy(destBuffer, sharedBitmap, *rowBytes * destHeight);
#else
	uint8_t permuteMap[4] = {3, 2, 1, 0};
	vImage_Error error = vImagePermuteChannels_ARGB8888(&src, &dest, permuteMap, 0);
    NSParameterAssert( error == noErr );
#endif
    return destBuffer;
}


- (uint32_t const *)createUnpremultipliedRGBA_8888_BitmapForCIImage: (CIImage *)image rowBytes: (size_t *)rowBytes
{
	uint32_t const *srcBuffer = [self createPremultipliedRGBA_8888_BitmapForCIImage: image rowBytes: rowBytes];
	NSParameterAssert( srcBuffer != NULL );
	
    // - Get the geometry of the image
    
    CGRect extent = [image extent];
    NSParameterAssert( CGRectIsInfinite(extent) == NO );
    size_t destWidth = (size_t)extent.size.width;
    size_t destHeight = (size_t)extent.size.height;

    // - Create the src for the vImage operation
    
    vImage_Buffer src;
    src.data = (void *)srcBuffer;
    src.width = destWidth;
    src.height = destHeight;
    src.rowBytes = *rowBytes;
	
    vImage_Error error = vImageUnpremultiplyData_RGBA8888(&src, &src, 0);
    NSParameterAssert( error == noErr );
    return srcBuffer;
}


- (CGContextRef)createBitmapContextOfSize: (CGSize)contextSize createdBackingBuffer: (void **)backingBuffer
{    
    CGContextRef context = NULL;
    size_t width = (size_t)contextSize.width;
    size_t height = (size_t)contextSize.height;
    size_t bitsPerComponent = 8;
    size_t bytesPerRow = [self optimalRowBytesForWidth: width bytesPerPixel: 4];
    CGColorSpaceRef colorspace = [self getScreenColorspace];
    CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault;
    
	void *buffer = NULL;
	if (backingBuffer)
		buffer = *backingBuffer = malloc(bytesPerRow * height);
    if (!backingBuffer || *backingBuffer)
    {
        context = CGBitmapContextCreate(buffer, width, height, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo);
        if (!context && backingBuffer)
        {
			free(*backingBuffer);
            *backingBuffer = NULL;
        }
    }
    return context;
}


- (CGImageRef)createCGImageFromCIImage: (CIImage *)ciImage forceRect: (CGRect *)forceRect
{
    if (nil == ciImage) return NULL;
	
	CGRect extent = [ciImage extent];
	if (CGRectIsNull(extent) || CGRectIsEmpty(extent)) return NULL;
	if (CGRectIsInfinite(extent)) {
		extent.size.width = 1024;
		extent.size.height = 1024;
		NSLog(@"CGImageFromCIImage: Trimmed infinite rect to arbitrary finite rect");
	}
	
	if (forceRect)
		extent = *forceRect;
	[self _createContextsForImageSize: extent.size];
	return [_sharedCIContext createCGImage: ciImage fromRect: extent];
}


- (void)convertRGB: (CGFloat const *)rgbComponents toHSV: (CGFloat *)hsvComponents
{
    CGFloat themin,themax,delta;
    
    themin = MIN(rgbComponents[0], MIN(rgbComponents[1], rgbComponents[2]));
    themax = MAX(rgbComponents[0], MAX(rgbComponents[1], rgbComponents[2]));
    delta = themax - themin;
	
    hsvComponents[0] = 0;
    hsvComponents[1] = 0;
    hsvComponents[2] = themax;
	
    if (themax != 0)
	{
        hsvComponents[1] = delta / themax;
		if (delta != 0)
		{
			CGFloat deltaR = (((themax - rgbComponents[0]) / 6.0) + (delta / 2.0)) / delta;
			CGFloat deltaG = (((themax - rgbComponents[1]) / 6.0) + (delta / 2.0)) / delta;
			CGFloat deltaB = (((themax - rgbComponents[2]) / 6.0) + (delta / 2.0)) / delta;
			
			if      (rgbComponents[0] == themax)		hsvComponents[0] = deltaB - deltaG;
			else if (rgbComponents[1] == themax)		hsvComponents[0] = (1.0 / 3.0) + deltaR - deltaB;
			else if (rgbComponents[2] == themax)		hsvComponents[0] = (2.0 / 3.0) + deltaG - deltaR;
			
			while (hsvComponents[0] < 0.0)		hsvComponents[0] += 1.0;
			while (hsvComponents[0] > 1.0)		hsvComponents[0] -= 1.0;
		}
	}
}


- (void)convertHSV: (CGFloat const *)hsvComponents toRGB: (CGFloat *)rgbComponents;
{
	if (0.0 == hsvComponents[1])
	{
		rgbComponents[0] = rgbComponents[1] = rgbComponents[2] = hsvComponents[2];
	}
	else
	{
		CGFloat tempH = hsvComponents[0] * 6.0;
		
		if (tempH >= 6.0 || tempH < 0.0) tempH = 0.0; // H must be < 1
		int intH = (int)floorf(tempH); // guaranteed to be between 0 and 5 inclusive
		CGFloat term1 = hsvComponents[2] * (1.0 - hsvComponents[1]);
		CGFloat term2 = hsvComponents[2] * (1.0 - hsvComponents[1] * (tempH - (CGFloat)intH));
		CGFloat term3 = hsvComponents[2] * (1.0 - hsvComponents[1] * (1.0 - (tempH - (CGFloat)intH)));
		
		switch(intH)
		{
			case 0:
				rgbComponents[0] = hsvComponents[2];
				rgbComponents[1] = term3;
				rgbComponents[2] = term1;
				break;
				
			case 1:
				rgbComponents[0] = term2;
				rgbComponents[1] = hsvComponents[2];
				rgbComponents[2] = term1;
				break;
				
			case 2:
				rgbComponents[0] = term1;
				rgbComponents[1] = hsvComponents[2];
				rgbComponents[2] = term3;
				break;
				
			case 3:
				rgbComponents[0] = term1;
				rgbComponents[1] = term2;
				rgbComponents[2] = hsvComponents[2];
				break;
				
			case 4:
				rgbComponents[0] = term3;
				rgbComponents[1] = term1;
				rgbComponents[2] = hsvComponents[2];
				break;
				
			case 5:
				rgbComponents[0] = hsvComponents[2];
				rgbComponents[1] = term1;
				rgbComponents[2] = term2;
				break;
		}
	}
}


- (NSDictionary *)scalesAndPathsForImageName: (NSString *)imageName directory: (NSString *)directoryName inBundle: (NSBundle *)bundle error: (NSError **)error
{
	NSString *imageFileName = [imageName stringByDeletingPathExtension];
	NSString *imagePathExtension = [imageName pathExtension];
	if (imagePathExtension && 0 == [imagePathExtension length])
		imagePathExtension = nil;
	
	if (!bundle) bundle = [NSBundle mainBundle];
	NSString *directoryPath = [bundle resourcePath];
	if (directoryName)
		directoryPath = [directoryPath stringByAppendingPathComponent: directoryName];
	
	NSMutableDictionary *entries = [NSMutableDictionary dictionary];
	NSFileManager *fm = [NSFileManager defaultManager];
	NSArray *filenames = [fm contentsOfDirectoryAtPath: directoryPath error: error];
	if (!filenames) return nil;
	NSArray *imageFileTypes = [NSImage imageFileTypes];
	
	for (NSString *filename in filenames)
	{
		NSString *extensionlessFilename = [filename stringByDeletingPathExtension];
		if (![extensionlessFilename hasPrefix: imageFileName]) continue;
		NSString *pathExtension = [filename pathExtension];
		if (imagePathExtension && ![imagePathExtension isEqualToString: pathExtension]) continue;
		if (!imagePathExtension && (!pathExtension || ![imageFileTypes containsObject: pathExtension])) continue;
		
		CGFloat scaleFactor;
		NSString *scaleFactorString = [extensionlessFilename substringFromIndex: [imageFileName length]];
		if (0 == [scaleFactorString length])
			scaleFactor = 1.0;
		else
		{
			NSParameterAssert ('_' == [scaleFactorString characterAtIndex: 0] );
			scaleFactorString = [scaleFactorString substringFromIndex: 1];
			NSScanner *scanner = [NSScanner scannerWithString: scaleFactorString];
			BOOL scaleFactorStringWasNumeric = [scanner scanFloat: &scaleFactor];
			NSParameterAssert( scaleFactorStringWasNumeric == YES );
		}
		[entries setObject: [directoryPath stringByAppendingPathComponent: filename] forKey: [NSNumber numberWithFloat: scaleFactor]];
	}
	
	return entries;
}


- (NSString *)bestPathFromChoices: (NSDictionary *)choices forScaleFactor: (CGFloat)scaleFactor
{
	NSUInteger count = [choices count];
	NSParameterAssert( count > 0 );
	
	NSArray *allKeys = [choices allKeys];
	if (1 == count)
		return [choices objectForKey: [allKeys lastObject]];
	
	NSMutableArray *mutableScaleFactors = [NSMutableArray arrayWithArray: allKeys];
	NSSortDescriptor *sd = [[[NSSortDescriptor alloc] initWithKey: @"scaleFactor" ascending: YES] autorelease];
	[mutableScaleFactors sortUsingDescriptors: [NSArray arrayWithObject: sd]];
	for (NSNumber *thisScaleFactorObject in mutableScaleFactors)
	{
		float thisScaleFactor = [thisScaleFactorObject floatValue];
		if (scaleFactor <= thisScaleFactor)
			return [choices objectForKey: thisScaleFactorObject];
	}
	return [choices objectForKey: [mutableScaleFactors lastObject]];
}


- (CGImageRef)createCGImageAtPath: (NSString *)path desiredScaleFactor: (float)desiredScaleFactor originalScaleFactor: (float)originalScaleFactor
{
	NSParameterAssert( path != nil );
	NSParameterAssert( originalScaleFactor >= 0.1 );
	NSParameterAssert( desiredScaleFactor >= 0.1 );
	
	CGImageRef			sourceImage = NULL;
	CGImageRef			finalImage = NULL;
	CFDictionaryRef		properties = NULL;
	CGImageSourceRef	imageSource = NULL;
	CGContextRef		context = NULL;
	
	@try
	{
		// - Figure out our destination resolution
		
		float desiredDPIWidth = 72.0, desiredDPIHeight = 72.0;
		desiredDPIWidth  *= desiredScaleFactor;
		desiredDPIHeight *= desiredScaleFactor;
		
		// - Grab the full size image and the image's properties
		
		NSURL *url = [NSURL fileURLWithPath: path];
		imageSource = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
		NSParameterAssert( imageSource != NULL );
		sourceImage = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
		properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
		CFRelease(imageSource);					imageSource = NULL;
		NSParameterAssert( sourceImage != NULL );
		NSParameterAssert( properties != NULL );
		
		// - Find the image size and resolution
		
		NSNumber *floatObject;
		floatObject = [(NSDictionary *)properties objectForKey: (NSString *)kCGImagePropertyPixelWidth];
		float pixelsWide = floatObject ? [floatObject floatValue] : (float)CGImageGetWidth(sourceImage);
		
		floatObject = [(NSDictionary *)properties objectForKey: (NSString *)kCGImagePropertyPixelHeight];
		float pixelsHigh = floatObject ? [floatObject floatValue] : (float)CGImageGetWidth(sourceImage);
		
		floatObject = [(NSDictionary *)properties objectForKey: (NSString *)kCGImagePropertyDPIWidth];
		float dpiWidth = floatObject ? [floatObject floatValue] : 72.0;
		
		floatObject = [(NSDictionary *)properties objectForKey: (NSString *)kCGImagePropertyDPIHeight];
		float dpiHeight = floatObject ? [floatObject floatValue] : 72.0;
		CFRelease(properties);					properties = NULL;
		
		// - Use a fudge factor for the image's resolution.  If it's between 71 and 76, we'll consider it 
		//   to be 72.
		
		if (dpiWidth  >= 71.0 && dpiWidth  <= 76.0) dpiWidth  = 72.0;
		if (dpiHeight >= 71.0 && dpiHeight <= 76.0) dpiHeight = 72.0;
		
		// - Photoshop CS3 has a bug that adds a rounding error to the DPI.  Floor the DPI in case that bug
		//   is manifesting itself.
		
		dpiWidth  = floorf(dpiWidth);
		dpiHeight = floorf(dpiHeight);
		
		// - If the dpi is 72 but the originalScaleFactor isn't 1x, this is probably an image with its DPI set 
		//   incorrectly.  Do I want to assume that and correct here, or force the artist to regenerate the 
		//   image with the correct DPI?  Probably the latter.
		
		if ((72.0 == dpiWidth || 72.0 == dpiHeight) && 1.0 != originalScaleFactor)
		{
			NSLog(@"Incorrect DPI of (%.1f, %.1f) set for image with scale factor %.1f at \"%@\"\n", dpiWidth, dpiHeight, originalScaleFactor, path);
			
			// - Hack, this works around the incorrectly set DPI
			
			if (72.0 == dpiWidth)
				dpiWidth *= originalScaleFactor;
			if (72.0 == dpiHeight)
				dpiHeight *= originalScaleFactor;
		}
		
		// - Okay, figure out what the actual scale factor of the source image is.  
		
		float adjustedDPIWidth  = dpiWidth  / originalScaleFactor;
		float adjustedDPIHeight = dpiHeight / originalScaleFactor;
		
		// - And figure out the change needed to get to our desired factor.
		
		float finalScaleWidth  = desiredDPIWidth  / adjustedDPIWidth;
		float finalScaleHeight = desiredDPIHeight / adjustedDPIHeight;
		
		// - Bail if we can, otherwise, create a context of the correct size for drawing the "real" image
		//   and blit it out.
		
		if (finalScaleWidth == 1.0 && finalScaleHeight == 1.0)
			return sourceImage;
		
		CGSize destinationSize = {pixelsWide * finalScaleWidth / originalScaleFactor, pixelsHigh * finalScaleHeight / originalScaleFactor};
		CGSize contextSize = {ceilf(destinationSize.width), ceilf(destinationSize.height)};
		context = [self createBitmapContextOfSize: contextSize createdBackingBuffer: NULL];
		NSParameterAssert( context != NULL );
		CGRect destRect = {CGPointZero, destinationSize};
		CGContextDrawImage(context, destRect, sourceImage);
		CFRelease(sourceImage);					sourceImage = NULL;
		finalImage = CGBitmapContextCreateImage(context);
		CFRelease(context);						context = NULL;
		NSParameterAssert( finalImage != NULL );
	}
	@catch (NSException *e)
	{
		if (sourceImage)	CFRelease(sourceImage);
		if (properties)		CFRelease(properties);
		if (imageSource)	CFRelease(imageSource);
		if (context)		CFRelease(context);
		[e raise];
	}
	return finalImage;
}

@end