//
//  ZoomStoryID.m
//  ZoomCocoa
//
//  Created by Andrew Hunter on Tue Jan 13 2004.
//  Copyright (c) 2004 Andrew Hunter. All rights reserved.
//

#import "ZoomStoryID.h"
#import "ZoomBlorbFile.h"
#import "ZoomPlugIn.h"
#import "ZoomPlugInManager.h"

#include "ifmetabase.h"
#include "../md5.h"

@implementation ZoomStoryID

+ (ZoomStoryID*) idForFile: (NSString*) filename {
	ZoomStoryID* result = nil;

#ifndef BUILDING_SPOTLIGHT
	ZoomPlugIn* plugin = [[ZoomPlugInManager sharedPlugInManager] instanceForFile: filename];

	if (plugin != nil) {
		// Try asking the plugin for the type of this file
		result = [plugin idForStory];
	}
	
	if (result != nil) return result;
#endif
	
	// If this is a z-code or blorb file, then try the Z-Code ID
	NSString* extension = [[filename pathExtension] lowercaseString];
	
	if ([extension isEqualToString: @"z3"]
		|| [extension isEqualToString: @"z4"]
		|| [extension isEqualToString: @"z5"]
		|| [extension isEqualToString: @"z6"]
		|| [extension isEqualToString: @"z7"]
		|| [extension isEqualToString: @"z8"]
		|| [extension isEqualToString: @"blb"]
		|| [extension isEqualToString: @"zlb"]
		|| [extension isEqualToString: @"zblorb"]) {
		result = [[[ZoomStoryID alloc] initWithZCodeFile: filename] autorelease];
	}	
	
	return result;
}

- (id) initWithIdString: (NSString*) idString {
	self = [super init];
	
	if (self) {
		needsFreeing = YES;
		ident = IFMB_IdFromString([idString UTF8String]);
	}
	
	return self;
}

- (id) initWithZCodeStory: (NSData*) gameData {
	self = [super init];
	
	if (self) {
		const unsigned char* bytes = [gameData bytes];
		int length = [gameData length];
		
		if ([gameData length] < 64) {
			// Too little data for this to be a Z-Code file
			[self release];
			return nil;
		}

		if (bytes[0] == 'F' && bytes[1] == 'O' && bytes[2] == 'R' && bytes[3] == 'M') {
			// This is not a Z-Code file; it's possibly a blorb file, though
			
			// Try to interpret as a blorb file
			ZoomBlorbFile* blorbFile = [[ZoomBlorbFile alloc] initWithData: gameData];
			
			if (blorbFile == nil) {
				[self release];
				return nil;
			}
			
			// See if we can get the ZCOD chunk
			NSData* data = [blorbFile dataForChunkWithType: @"ZCOD"];
			if (data == nil) {
				[blorbFile release];
				[self release];
				return nil;
			}
			
			if ([data length] < 64) {
				// This file is too short to be a Z-Code file
				[blorbFile release];
				[self release];
				return nil;
			}
			
			// Change to using the blorb data instead
			bytes = [[[data retain] autorelease] bytes];
			length = [data length];
			[blorbFile release];
		}
		
		// Interpret the Z-Code data into an identification
		needsFreeing = YES;
		ident = IFMB_ZcodeId((((int)bytes[0x2])<<8)|((int)bytes[0x3]),
							 bytes + 0x12,
							 (((int)bytes[0x1c])<<8)|((int)bytes[0x1d]));
		
		// Scan for the string 'UUID://' - use this as an ident for preference if it exists (and represents a valid UUID)
		int x;
		BOOL gotUUID = NO;
		
		for (x=0; x<length-48; x++) {
			if (bytes[x] == 'U' && bytes[x+1] == 'U' && bytes[x+2] == 'I' && bytes[x+3] == 'D' &&
				bytes[x+4] == ':' && bytes[x+5] == '/' && bytes[x+6] == '/') {
				// This might be a UUID section
				char uuidText[50];
				
				// Check to see if we've got a UUID
				int y;
				int digitCount = 0;
				gotUUID = YES;
				
				for (y=0; y<7; y++) uuidText[y] = bytes[x+y];
				for (y=7; y<48; y++) {
					uuidText[y] = bytes[x+y];
					
					if (bytes[x+y-1] == '/' && bytes[x+y] == '/') break;
					if (bytes[x+y] == '-' || bytes[x+y] == '/') continue;
					if ((bytes[x+y] >= '0' && bytes[x+y] <= '9') ||
						(bytes[x+y] >= 'a' && bytes[x+y] <= 'f') ||
						(bytes[x+y] >= 'A' && bytes[x+y] <= 'F')) {
						digitCount++;
						continue;
					}
					
					gotUUID = NO;
					break;
				}
				uuidText[y] = 0;
				
				if (gotUUID) {
					IFID uuidId = IFMB_IdFromString(uuidText);
					
					if (uuidId == NULL) {
						gotUUID = false;
					} else {
						IFMB_FreeId(ident);
						ident = uuidId;
						needsFreeing = YES;
					}
				}

				if (gotUUID) break;
			}
		}
	}
	
	if (ident == nil) {
		[self release];
		return nil;
	}
	
	return self;
}

- (id) initWithZCodeFile: (NSString*) zcodeFile {
	self = [super init];
	
	if (self) {
		const unsigned char* bytes;
		int length;
		
		NSFileHandle* fh = [NSFileHandle fileHandleForReadingAtPath: zcodeFile];
		NSData* data = [fh readDataToEndOfFile];
		[fh closeFile];
		
		if ([data length] < 64) {
			// This file is too short to be a Z-Code file
			[self release];
			return nil;
		}
		
		bytes = [data bytes];
		length = [data length];
		
		if (bytes[0] == 'F' && bytes[1] == 'O' && bytes[2] == 'R' && bytes[3] == 'M') {
			// This is not a Z-Code file; it's possibly a blorb file, though
						
			// Try to interpret as a blorb file
			ZoomBlorbFile* blorbFile = [[ZoomBlorbFile alloc] initWithContentsOfFile: zcodeFile];
			
			if (blorbFile == nil) {
				[self release];
				return nil;
			}
			
			// See if we can get the ZCOD chunk
			data = [blorbFile dataForChunkWithType: @"ZCOD"];
			if (data == nil) {
				[blorbFile release];
				[self release];
				return nil;
			}
			
			if ([data length] < 64) {
				// This file is too short to be a Z-Code file
				[blorbFile release];
				[self release];
				return nil;
			}
			
			// Change to using the blorb data instead
			bytes = [[[data retain] autorelease] bytes];
			length = [data length];
			[blorbFile release];
		}
		
		if (bytes[0] > 8) {
			// This cannot be a Z-Code file
			[self release];
			return nil;
		}
		
		// Interpret the Z-Code data into an identification
		needsFreeing = YES;
		ident = IFMB_ZcodeId((((int)bytes[0x2])<<8)|((int)bytes[0x3]),
							 bytes + 0x12,
							 (((int)bytes[0x1c])<<8)|((int)bytes[0x1d]));
		
		// Scan for the string 'UUID://' - use this as an ident for preference if it exists (and represents a valid UUID)
		int x;
		BOOL gotUUID = NO;
		
		for (x=0; x<length-48; x++) {
			if (bytes[x] == 'U' && bytes[x+1] == 'U' && bytes[x+2] == 'I' && bytes[x+3] == 'D' &&
				bytes[x+4] == ':' && bytes[x+5] == '/' && bytes[x+6] == '/') {
				// This might be a UUID section
				char uuidText[50];
				
				// Check to see if we've got a UUID
				int y;
				int digitCount = 0;
				gotUUID = YES;
				
				for (y=0; y<7; y++) uuidText[y] = bytes[x+y];
				for (y=7; y<48; y++) {
					uuidText[y] = bytes[x+y];
					
					if (bytes[x+y-1] == '/' && bytes[x+y] == '/') break;
					if (bytes[x+y] == '-' || bytes[x+y] == '/') continue;
					if ((bytes[x+y] >= '0' && bytes[x+y] <= '9') ||
						(bytes[x+y] >= 'a' && bytes[x+y] <= 'f') ||
						(bytes[x+y] >= 'A' && bytes[x+y] <= 'F')) {
						digitCount++;
						continue;
					}
					
					gotUUID = NO;
					break;
				}
				uuidText[y] = 0;
				
				if (gotUUID) {
					IFID uuidId = IFMB_IdFromString(uuidText);
					
					if (uuidId == NULL) {
						gotUUID = false;
					} else {
						IFMB_FreeId(ident);
						ident = uuidId;
						needsFreeing = YES;
					}
				}
				
				if (gotUUID) break;
			}
		}
	}
	
	if (ident == nil) {
		[self release];
		return nil;
	}
	
	return self;
}

- (id) initWithGlulxFile: (NSString*) glulxFile {
	self = [super init];
	
	if (self) {
		// Read the header of this file
		const unsigned char* bytes;
		int length;
		
		NSFileHandle* fh = [NSFileHandle fileHandleForReadingAtPath: glulxFile];
		NSData* data = [[[fh readDataOfLength: 64] retain] autorelease];
		[fh closeFile];
		
		if ([data length] < 64) {
			// This file is too short to be a Glulx file
			[self release];
			return nil;
		}
		
		bytes = [data bytes];
		length = [data length];
		
		if (bytes[0] == 'F' && bytes[1] == 'O' && bytes[2] == 'R' && bytes[3] == 'M') {
			// This is not a Z-Code file; it's possibly a blorb file, though
			
			// Try to interpret as a blorb file
			ZoomBlorbFile* blorbFile = [[ZoomBlorbFile alloc] initWithContentsOfFile: glulxFile];
			
			if (blorbFile == nil) {
				[self release];
				return nil;
			}
			
			// See if we can get the ZCOD chunk
			data = [blorbFile dataForChunkWithType: @"GLUL"];
			if (data == nil) {
				[blorbFile release];
				[self release];
				return nil;
			}
			
			if ([data length] < 64) {
				// This file is too short to be a Z-Code file
				[blorbFile release];
				[self release];
				return nil;
			}
			
			// Change to using the blorb data instead
			bytes = [[[data retain] autorelease] bytes];
			length = [data length];
			[blorbFile release];
		} else if (bytes[0] == 'G' && bytes[1] == 'l' && bytes[2] == 'u' && bytes[3] == 'l') {
			data = [NSData dataWithContentsOfFile: glulxFile];
			bytes = [data bytes];
			
			if ([data length] < 64) {
				[self release];
				return nil;
			}
		} else {
			// Not a Glulx file
			[self release];
			return nil;
		}
		
		// bytes now contains the Glulx file we want the ID for
		int memsize = (bytes[16]<<24) | (bytes[17]<<16) | (bytes[18]<<8) | (bytes[19]<<0);
		if (memsize > [data length]) memsize = [data length];
		
		// Scan for a UUID
		int x;
		BOOL gotUUID = NO;
		
		for (x=0; x<memsize-48; x++) {
			if (bytes[x] == 'U' && bytes[x+1] == 'U' && bytes[x+2] == 'I' && bytes[x+3] == 'D' &&
				bytes[x+4] == ':' && bytes[x+5] == '/' && bytes[x+6] == '/') {
				// This might be a UUID section
				char uuidText[50];
				
				// Check to see if we've got a UUID
				int y;
				int digitCount = 0;
				gotUUID = YES;
				
				for (y=0; y<7; y++) uuidText[y] = bytes[x+y];
				for (y=7; y<48; y++) {
					uuidText[y] = bytes[x+y];
					
					if (bytes[x+y-1] == '/' && bytes[x+y] == '/') break;
					if (bytes[x+y] == '-' || bytes[x+y] == '/') continue;
					if ((bytes[x+y] >= '0' && bytes[x+y] <= '9') ||
						(bytes[x+y] >= 'a' && bytes[x+y] <= 'f') ||
						(bytes[x+y] >= 'A' && bytes[x+y] <= 'F')) {
						digitCount++;
						continue;
					}
					
					gotUUID = NO;
					break;
				}
				uuidText[y] = 0;
				
				if (gotUUID) {
					IFID uuidId = IFMB_IdFromString(uuidText);
					
					if (uuidId == NULL) {
						gotUUID = false;
					} else {
						ident = uuidId;
						needsFreeing = YES;
						return self;
					}
				}
				
				if (gotUUID) break;
			}
		}
		
		// Legacy mode: check if this is an Inform file
		if (bytes[36] == 'I' && bytes[37] == 'n' && bytes[38] == 'f' && bytes[39] == 'o') {
			int release = (bytes[52]<<8) | (bytes[53]);
			int checksum = (bytes[32]<<24) | (bytes[33]<<16) | (bytes[34]<<8) | (bytes[35]<<0);
			
			ident = IFMB_GlulxId(release, bytes + 54, checksum);
			needsFreeing = YES;
		} else {
			int checksum = (bytes[32]<<24) | (bytes[33]<<16) | (bytes[34]<<8) | (bytes[35]<<0);

			ident = IFMB_GlulxIdNotInform(memsize, checksum);
			needsFreeing = YES;
		}
	}
	
	if (ident == nil) {
		[self release];
		return nil;
	}
	
	return self;
}

- (id) initWithData: (NSData*) genericGameData
			   type: (NSString*) type {
	self = [super init];
	
	if (self) {
		// Take MD5 of the data
		md5_state_t md5state;
		unsigned char r[16];
		
		md5_init(&md5state);
		md5_append(&md5state, [genericGameData bytes], [genericGameData length]);
		md5_finish(&md5state, r);
		
		// Build the string
		int len = ([type length]+32+2);
		char* result = malloc(sizeof(char)*len);
		
		snprintf(result, len, "%s-%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
				 [type UTF8String],
				 r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]);
		
		// Allocate the identity block
		ident = IFMB_IdFromString(result);
		needsFreeing = YES;
			
		free(result);
	}
	
	if (ident == nil) {
		[self release];
		return nil;
	}
	
	return self;
}

- (id) initWithData: (NSData*) genericGameData {
	return [self initWithData: genericGameData
						 type: @"MD5"];
}

- (id) initWithIdent: (struct IFID*) idt {
	self = [super init];
	
	if (idt == nil) {
		[self release];
		return nil;
	}
	
	if (self) {
		ident = IFMB_CopyId(idt);
		needsFreeing = YES;
	}
	
	if (ident == nil) {
		[self release];
		return nil;
	}
	
	return self;
}

- (id) initWithZcodeRelease: (int) release
					 serial: (const unsigned char*) serial
				   checksum: (int) checksum {
	self = [super init];
	
	if (self) {
		ident = IFMB_ZcodeId(release, serial, checksum);
		needsFreeing = YES;
	}
	
	return self;
}

- (void) dealloc {
	if (needsFreeing && ident != NULL) {
		IFMB_FreeId(ident);
	}
	
	[super dealloc];
}

- (struct IFID*) ident {
	return ident;
}

// = NSCopying =
- (id) copyWithZone: (NSZone*) zone {
	ZoomStoryID* newID = [[ZoomStoryID allocWithZone: zone] init];
	
	newID->ident = IFMB_CopyId(ident);
	newID->needsFreeing = YES;
	
	return newID;
}

// = NSCoding =
- (void)encodeWithCoder:(NSCoder *)encoder {
	// Version might change later on
	int version = 2;
	
	[encoder encodeValueOfObjCType: @encode(int) 
								at: &version];
	
	char* stringId = IFMB_IdToString(ident);
	NSString* stringIdent = [NSString stringWithUTF8String: stringId];
	[encoder encodeObject: stringIdent];
	free(stringId);
}

enum IFMDFormat {
	IFFormat_Unknown = 0x0,
	
	IFFormat_ZCode,
	IFFormat_Glulx,
	
	IFFormat_TADS,
	IFFormat_HUGO,
	IFFormat_Alan,
	IFFormat_Adrift,
	IFFormat_Level9,
	IFFormat_AGT,
	IFFormat_MagScrolls,
	IFFormat_AdvSys,
	
	IFFormat_UUID,			/* 'Special' format used for games identified by a UUID */
};

typedef unsigned char IFMDByte;

- (id)initWithCoder:(NSCoder *)decoder {
	self = [super init];
	
	if (self) {
		ident = NULL;
		needsFreeing = YES;
		
		// As above, but backwards
		int version;
		
		[decoder decodeValueOfObjCType: @encode(int) at: &version];
		
		if (version == 1) {
			// General stuff (data format, MD5, etc) [old v1 format used by versions of Zoom prior to 1.0.5dev3]
			char md5sum[16];
			IFMDByte usesMd5;
			enum IFMDFormat dataFormat;
			
			[decoder decodeValueOfObjCType: @encode(enum IFMDFormat) 
										at: &dataFormat];
			[decoder decodeValueOfObjCType: @encode(IFMDByte)
										at: &usesMd5];
			if (usesMd5) {
				[decoder decodeArrayOfObjCType: @encode(IFMDByte)
										 count: 16
											at: md5sum];
			}
			
			switch (dataFormat) {
				case IFFormat_ZCode:
				{
					char serial[6];
					int release;
					int checksum;
					
					[decoder decodeArrayOfObjCType: @encode(IFMDByte)
											 count: 6
												at: serial];
					[decoder decodeValueOfObjCType: @encode(int)
												at: &release];
					[decoder decodeValueOfObjCType: @encode(int)
												at: &checksum];
					
					ident = IFMB_ZcodeId(release, serial, checksum);
					needsFreeing = YES;
					break;
				}
					
				case IFFormat_UUID:
				{
					unsigned char uuid[16];
					
					[decoder decodeArrayOfObjCType: @encode(unsigned char)
											 count: 16
												at: uuid];
					ident = IFMB_UUID(uuid);
					needsFreeing = YES;
					break;
				}
					
				default:
					/* No other formats are supported yet */
					break;
			}		
		} else if (version == 2) {
			NSString* idString = (NSString*)[decoder decodeObject];
			
			ident = IFMB_IdFromString([idString UTF8String]);
			needsFreeing = YES;
		} else {
			// Only v1 and v2 decodes supported ATM
			[self release];
			
			NSLog(@"Tried to load a version %i ZoomStoryID (this version of Zoom supports only versions 1 and 2)", version);
			
			return nil;
		}
	}
	
	if (ident == nil) {
		[self release];
		return nil;
	}
	
	return self;
}

// = Hashing/comparing =
- (unsigned) hash {
	return [[self description] hash];
}

- (BOOL) isEqual: (id)anObject {
	if ([anObject isKindOfClass: [ZoomStoryID class]]) {
		ZoomStoryID* compareWith = anObject;
		
		if (IFMB_CompareIds(ident, [compareWith ident]) == 0) {
			return YES;
		} else {
			return NO;
		}
	} else {
		return NO;
	}
}

- (NSString*) description {
	char* stringId = IFMB_IdToString(ident);
	NSString* identString = [NSString stringWithUTF8String: stringId];
	free(stringId);
	
	if (identString == nil)
		return @"(null)";
	
	return identString;
}

// = Port coding =

- (id)replacementObjectForPortCoder:(NSPortCoder *)encoder { 
	if ([encoder isBycopy]) return self; 
	return [super replacementObjectForPortCoder:encoder]; 
} 

@end
