/* Copyright (c) 2011 Artjom Popov */

#import "UTDerivedResource.h"
#import "NSArray+Additions.h"
#import "NSString+Additions.h"
#import <sys/types.h>
#import <sys/stat.h>
#import <unistd.h>

#define UTDerivedResourceTooOldCache 1.0F * 24.0F * 60.0F * 60.0F // 1 day
#define UTDerivedResourceTooOldDebugCache 1.0F * 1.0F * 60.0F * 60.0F //1 hour

@interface UTDerivedResourceManager : NSObject
{
@private
	NSString *m_path;
	NSMutableDictionary *m_resources;
}

@property (nonatomic, assign, readonly) NSString *path;


+ (UTDerivedResourceManager *)sharedManager;

@end

#pragma mark -

@implementation UTDerivedResourceManager

+ (UTDerivedResourceManager *)sharedManager
{
	static UTDerivedResourceManager *sharedManager = nil;
	
	if(!sharedManager) {
		sharedManager = [[UTDerivedResourceManager alloc] init];
	}
	
	return sharedManager;
}

@synthesize path = m_path;

- (void)cacheData:(NSData *)data forResource:(UTDerivedResource *)resource
{
	@synchronized(self) {
		if(resource && data) {
			NSString *p = resource.path;
			NSFileManager *fileManager = [NSFileManager defaultManager];
			NSString *p_1 = [p stringByDeletingLastPathComponent];
			
			if(![fileManager fileExistsAtPath:p_1]) {
				[fileManager createDirectoryAtPath:p_1 withIntermediateDirectories:YES attributes:nil error:nil];
			}		
			[data writeToFile:p atomically:YES];
		} else {
			NSLog(@"%@: Couldn't cache data", self.class);
		}
	}
}

- (NSData *)dataForResource:(UTDerivedResource *)resource
{
	@synchronized(self) {
		struct stat s;
		if(stat(resource.path.fileSystemRepresentation, &s) == 0 && S_ISDIR(s.st_mode) == 0) {
			FILE *fp = fopen(resource.path.fileSystemRepresentation, "r");
			
			if(fp) {
				char *buffer;			
				buffer=(char *)malloc(s.st_size);
				fread(buffer, s.st_size, 1, fp);
				fclose(fp);
				return [NSData dataWithBytesNoCopy:buffer length:s.st_size];
			} else {
				fclose(fp);
				NSLog(@"Unable to open derived cache file");
				return nil;
			}
			
		}
		return nil;
	}
}

- (void)checkOldCache:(UTDerivedResource *)resource
{
	@synchronized(self) {
		if(resource) {
			NSFileManager *fileManager = [NSFileManager defaultManager];
			NSString *p_1 = [resource.path stringByDeletingLastPathComponent];
			
			for(NSString *path in [fileManager performSelector:@selector(directoryContentsAtPath:) withObject:p_1]) {
				NSTimeInterval delta;
				path = [p_1 stringByAppendingPathComponent:path];
				delta = [[[fileManager attributesOfItemAtPath:path error:nil] objectForKey:NSFileCreationDate] timeIntervalSinceNow];
				UTDerivedResource *temp = [self->m_resources objectForKey:path];
				
#ifdef DEBUG
				if(fabs(delta) > UTDerivedResourceTooOldDebugCache && (!temp || temp.weakReferenceCount == 0)) {
					NSLog(@"%@: Removed old resource at path %@", self.class, path);
					[fileManager removeItemAtPath:path error:nil];
				}
#else
				if(fabs(delta) > UTDerivedResourceTooOldCache && (!temp || temp.weakReferenceCount == 0)) {
					NSLog(@"%@: Removed old resource at path %@", self.class, path);
					[fileManager removeItemAtPath:path error:nil];
				}
#endif
			}
		}
	}
}

- (void)cleanup
{
	@synchronized(self) {
		
		NSFileManager *fileManager = [NSFileManager defaultManager];
		NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:self->m_path];
		NSMutableDictionary *paths = [NSMutableDictionary dictionary];
		NSMutableArray *resources = [NSMutableArray array];
		NSString *path;
		
		for(UTDerivedResource *resource in self->m_resources.objectEnumerator) {
			[paths setObject:resource forKey:resource.path];
		}
		
		while((path = [enumerator nextObject]) != nil) {
			UTDerivedResource *resource;
			
			resource = [paths objectForKey:path];
			
			if(resource) {
				[resources addObject:resource];
			} else {
				resource = [[[UTDerivedResource alloc] initWithPath:[self->m_path stringByAppendingPathComponent:path]] autorelease];
				[resources addObject:resource];
			}
		}
		
		for(UTDerivedResource *resource in resources) {
			if(resource.exists && resource.weakReferenceCount == 0) {
				[fileManager removeItemAtPath:resource.path error:nil];
				[self->m_resources removeObjectsForKeys:[self->m_resources allKeysForObject:resource]];
			}
		}
	}
}

- (void)weakRetainForResource:(UTDerivedResource *)resource
{
	@synchronized(self) {
		if(resource && resource.path) {
			UTDerivedResource *temp = [self->m_resources objectForKey:resource.path];
			if(!temp) {
				[self->m_resources setObject:[[[UTDerivedResource alloc] initWithPath:resource.path] autorelease] forKey:resource.path];
			} else {
				[temp weakRetain];
			}
			
		}
	}
	
}

- (void)weakReleaseForResource:(UTDerivedResource *)resource
{
	@synchronized(self) {
		if(resource && resource.path) {
			UTDerivedResource *temp = [self->m_resources objectForKey:resource.path];
			if(temp && temp.obsolete) {
				NSFileManager *fileManager = [NSFileManager defaultManager];
				[self->m_resources removeObjectForKey:temp.path];
				[fileManager removeItemAtPath:temp.path error:nil];
			} else {
				if(!temp) {
					[self->m_resources setObject:[[[UTDerivedResource alloc] initWithPath:resource.path] autorelease] forKey:resource.path];
					[resource weakRelease];
				} else {
					[temp weakRelease];
				}
			}
			
		}
	}
}	

- (void)delete
{
	for(NSString *path in self->m_resources) {
		((UTDerivedResource *)[self->m_resources objectForKey:path]).obsolete = YES;
	}
}

#pragma mark NSObject

- (id)init
{
	self = [super init];
	
	if(self) {
		self->m_path = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"derived"] retain];
		self->m_resources = [[NSMutableDictionary alloc] init];
	}
	
	return self;
}

- (void)dealloc
{
	[self->m_path release];
	[self->m_resources release];
	[super dealloc];
}

@end

#pragma mark -

@implementation UTDerivedResource

+ (void)deleteAndCleanup
{
	[[UTDerivedResourceManager sharedManager] delete];
	[[UTDerivedResourceManager sharedManager] cleanup];
}

- (id)initWithKey:(NSDictionary *)values
{
	self = [super init];
	
	if(self) {
		if(!values) {
			NSLog(@"%@: Couldn't initialize a web resource without key", self.class);
			
			[self release];
			self = nil;
		} else {			
			NSArray *sortedKeys = [values keysSortedByValueUsingSelector:@selector(compare:)];
			NSString *keyString = @"";
			for(NSString *valueKey in sortedKeys) {
				keyString = [NSString stringWithFormat:@"%@%@=%@;", keyString, valueKey, [values objectForKey:valueKey]];
				
			}
			keyString = [NSString stringWithFormat:@"%@version=%@;", keyString, [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]];
			
			self->m_path = [[[UTDerivedResourceManager sharedManager].path stringByAppendingPathComponent:keyString.sha1String] retain];
			self->m_obsolete = NO;
			
			[[UTDerivedResourceManager sharedManager] weakRetainForResource:self];
		}
		
	}
	
	return self;
}

- (id)initWithPath:(NSString *)path
{
	self = [super init];
	
	if(self) {			
		self->m_path = [path retain];
		self->m_obsolete = NO;
		[self weakRetain];
	}
	
	
	return self;
}

@synthesize weakReferenceCount = m_weakReferenceCount;
@synthesize path = m_path;
@synthesize obsolete = m_obsolete;

@dynamic exists;

- (BOOL)exists
{
	return [[NSFileManager defaultManager] fileExistsAtPath:self->m_path];
}


@dynamic data;

- (NSData *)data
{
	return [[UTDerivedResourceManager sharedManager] dataForResource:self];
}

- (void)setData:(NSData *)data
{
	[[UTDerivedResourceManager sharedManager] checkOldCache:self];
	[[UTDerivedResourceManager sharedManager] cacheData:data forResource:self];
}

- (void)weakRetain
{
	@synchronized(self) {
		self->m_weakReferenceCount++;
	}
}

- (void)weakRelease
{
	@synchronized(self) {
		self->m_weakReferenceCount--;
	}
}

- (void)cleanup
{
	[[UTDerivedResourceManager sharedManager] cleanup];
}

- (void)delete
{
	[[UTDerivedResourceManager sharedManager] delete];
}

#pragma mark NSObject

- (void)dealloc
{	
	[[UTDerivedResourceManager sharedManager] weakReleaseForResource:self];
	
	[self->m_path release];
	
	[super dealloc];
}

@end
