//
//  XTResourceFinder.m
//  XTads
//
//  Created by Rune Berg on 04/05/15.
//  Copyright (c) 2015 Rune Berg. All rights reserved.
//

#import "XTLogger.h"
#import "XTFileUtils.h"
#import "XTResourcePhysicalFile.h"
#import "XTResourceFinder.h"
#import "XTResourceEntry.h"
#import "XTAllocDeallocCounter.h"


@interface XTResourceFinder ()

@property NSMutableArray *physicalFiles; // of XTResourcePhysicalFile
@property NSMutableDictionary *entriesByResourceFileName; // of XTResourceFileEntry
@property NSString *rootResourceDir;

@end


@implementation XTResourceFinder

static XTLogger* logger;

+ (void)initialize
{
	logger = [XTLogger loggerForClass:[XTResourceFinder class]];
}

OVERRIDE_ALLOC_FOR_COUNTER
OVERRIDE_DEALLOC_FOR_COUNTER

- (id)init
{
	self = [super init];
	if (self != nil) {
		_entriesByResourceFileName = [NSMutableDictionary dictionary];
		_physicalFiles = [NSMutableArray array];
		_rootResourceDir = nil;
	}
	return self;
}

- (void)setResourceDir:(NSString *)resourceDir
{
	self.rootResourceDir = resourceDir;
}

- (void)setGameFileName:(NSString *)fqGameFileName {
	
	XT_DEF_SELNAME;
	XT_TRACE_1(@"fqGameFileName=", fqGameFileName);

	XTResourcePhysicalFile *physFile = [XTResourcePhysicalFile new];
	physFile.fqFileName = fqGameFileName;
	self.physicalFiles[0] = physFile;
}

- (NSInteger)addPhysicalResourceFile:(NSString *)fqFileName
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"fqFileName=", fqFileName);

	XTResourcePhysicalFile *physFile = [XTResourcePhysicalFile new];
	physFile.fqFileName = fqFileName;
	[self.physicalFiles addObject:physFile];
	NSInteger physFileNo = self.physicalFiles.count - 1;
	return physFileNo;
}

// arrival, alone1 calls this
- (void)setSeekBase:(NSUInteger)seekBase forPhysicalFileNo:(NSUInteger)physicalFileNo {
	
	XT_DEF_SELNAME;
	XT_TRACE_2(@"seekBase=%lu physicalFileNo=%lu", seekBase, physicalFileNo);
	
	if (physicalFileNo < self.physicalFiles.count) {
		XTResourcePhysicalFile *physFile = self.physicalFiles[physicalFileNo];
		physFile.resourceSeekBase = seekBase;
	} else {
		XT_ERROR_2(@"physicalFileNo=%lu is out of range: self.physicalFiles.count=%lu", physicalFileNo, self.physicalFiles.count);
	}
}

- (void)addResourceEntry:(XTResourceEntry *)entry {
	
	XT_DEF_SELNAME;
	XT_TRACE_4(@"resourceName=%@ seekBaseOffset=%ld size=%ld physicalFileNo=%ld", entry.resourceName, entry.seekBaseOffset, entry.size, entry.physicalFileNo);
	
	@synchronized(_entriesByResourceFileName) {
		[self.entriesByResourceFileName setObject:entry forKey:entry.resourceName];
	}
}

- (void)removeAllResourceEntries
{
	//XT_DEF_SELNAME;
	
	@synchronized(_entriesByResourceFileName) {
		[self.entriesByResourceFileName removeAllObjects];
		[self.physicalFiles removeAllObjects];
	}
}

- (XTResourceFindResult *)findResource:(NSString *)resName {

	XT_DEF_SELNAME;
	XT_TRACE_1(@"resName=%@", resName);
	
	XTResourceFindResult *findResult = nil;
	
	findResult = [self findResourceInGameFileWithPath:resName];
	
	if (findResult == nil) {
		findResult = [self findResourceInTerpExecutable:resName ofType:nil];
	}
	
	if (findResult == nil) {
		findResult = [self findResourceInRelativeExternalFileWithPath:resName];
	}
	
	if (findResult == nil) {
		findResult = [self findResourceInAbsoluteExternalFileWithPath:resName];
	}

	if (findResult == nil) {
		XT_WARN_1(@"failed to find resource %@", resName);
	}
	
	return findResult;
}

- (BOOL)resourceExists:(NSString *)resName
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"resName=%@", resName);

	BOOL exists = NO;
	XTResourceFindResult *findResult = [self findResource:resName];
	if (findResult != nil) {
		fclose(findResult.fileHandle); // don't need it, and it's bad to leave it open
		exists = YES;
	}

	XT_TRACE_1(@"-> %d", exists);
	
	return exists;
}


//---------------------------

- (XTResourceFindResult *)findResourceInGameFileWithPath:(NSString *)resPath {
	
	XT_DEF_SELNAME;
	XT_TRACE_1(@"resPath=\"%@\"", resPath);
	
	XTResourceFindResult *findResult = nil;

	XTResourceEntry *entry = [self.entriesByResourceFileName objectForKey:resPath];
	
	if (entry != nil) {
		if (entry.linkToLocalFile != nil) {
			// game was built in debug mode - resources aren't embedded, just "links" to actual local files
			findResult = [self findResourceInRelativeExternalFileWithPath:entry.linkToLocalFile];
			if (findResult == nil) {
				findResult = [self findResourceInRelativeExternalFileWithPath:entry.linkToLocalFile];
			}
			if (findResult == nil) {
				XT_WARN_1(@"failed to find linked file \"%@\"", entry.linkToLocalFile);
			}
		} else if (entry.physicalFileNo < self.physicalFiles.count) {
			XTResourcePhysicalFile *physFile = self.physicalFiles[entry.physicalFileNo];
			const char* fqFileNameCString = XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(physFile.fqFileName);
			osfildef* fileHandle = osfoprb(fqFileNameCString, OSFTBIN);
			if (fileHandle != NULL) {
				findResult = [XTResourceFindResult new];
				findResult.fileHandle = fileHandle;
				findResult.seekPosition = physFile.resourceSeekBase + entry.seekBaseOffset;
				findResult.size = entry.size;
			} else {
				XT_ERROR_1(@"failed to open file \"%s\" for reading", fqFileNameCString);
			}
		} else {
			XT_ERROR_2(@"entry.physicalFileNo=%lu is out of range: self.physicalFiles.count=%lu", entry.physicalFileNo, self.physicalFiles.count);
		}
	}

	return findResult;
}

- (XTResourceFindResult *)findResourceInTerpExecutable:(NSString *)resPath ofType:(NSString *)resType {
	
	XT_DEF_SELNAME;
	XT_TRACE_1(@"resPath=\"%@\"", resPath);
	
	XTResourceFindResult *findResult = nil;
	
	NSBundle *mainBundle = [NSBundle mainBundle];
	NSString *appBundleResPath = [mainBundle pathForResource:resPath ofType:nil];
	
	if (appBundleResPath != nil) {
		const char *appBundleResPathCString = XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(appBundleResPath);
		osfildef *fileHandle = osfoprb(appBundleResPathCString, OSFTBIN);
		if (fileHandle != NULL) {
			findResult = [XTResourceFindResult new];
			findResult.fileHandle = fileHandle;
			findResult.seekPosition = 0;
			findResult.size = [XTFileUtils sizeOfFile:appBundleResPath];
		} else {
			XT_ERROR_1(@"failed to open file \"%s\" for reading", appBundleResPathCString);
		}
	} else {
		XT_TRACE_1(@"failed to find path to terp-bundled resource for resPath=\"%@\"", resPath);
	}
	
	return findResult;
}

/* 
 See http://www.tads.org/t3doc/doc/sysman/terp.htm#file-safety for file safety rules 
 */
- (XTResourceFindResult *)findResourceInRelativeExternalFileWithPath:(NSString *)resPath {

	XT_DEF_SELNAME;
	XT_TRACE_1(@"resPath=\"%@\"", resPath);

	int readSafetyLevel = 4;
	int writeSafetyLevel = 4;
	appctxdef *appctx = [self.tads2AppCtx getAppCtxPtr];
	appctx->get_io_safety_level(appctx->io_safety_level_ctx, &readSafetyLevel, &writeSafetyLevel);
	
	if (readSafetyLevel > 3) {
		XT_WARN_2(@"rejected resPath %@ because of read safety level %d", resPath, readSafetyLevel);
		return nil;
	}

	const char *resPathCString = XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(resPath);
	if (os_is_file_absolute(resPathCString)) {
		XT_TRACE_1(@"rejected resPath %@ because it's absolute", resPath);
		return nil;
	}
	
	XTResourceFindResult *findResult = nil;
	
	char effectiveExternalResDirCString[OSFNMAX] = {0};
	if (self.rootResourceDir != nil) {
		strncpy(effectiveExternalResDirCString, XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(self.rootResourceDir), sizeof(effectiveExternalResDirCString));
	} else {
		if (self.physicalFiles != nil && self.physicalFiles.count >= 1) {
			XTResourcePhysicalFile *physGameFile = self.physicalFiles[0];
			const char* fqGameFileName = XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(physGameFile.fqFileName);
			os_get_path_name(effectiveExternalResDirCString, sizeof(effectiveExternalResDirCString), fqGameFileName);
		}
	}
	
	if (strlen(effectiveExternalResDirCString) >= 1) {
		
		char fqResPathCString[OSFNMAX];
		os_build_full_path(fqResPathCString, sizeof(fqResPathCString), effectiveExternalResDirCString, resPathCString);
		if (osfacc(fqResPathCString) != 0) {
			XT_WARN_1(@"rejected resPath %@ because file does not exist", resPath);
			return nil;
		}
		
		if (readSafetyLevel <= 1) {
			// 0/1 is "read any" - pass
		} else if (readSafetyLevel <= 3) {
			// 2/3 is "read local" - pass if file is indeed local (i.e. in game file's directory, or in specially assigned res dir)
			if (! os_is_file_in_dir(fqResPathCString, effectiveExternalResDirCString, YES, NO)) {
				XT_WARN_2(@"rejected resPath %@ because of read-local safety but not in dir %s", resPath, effectiveExternalResDirCString);
				return nil;
			}
		} else {
			// 4+ - reject
			XT_WARN_2(@"rejected resPath %@ because of read safety level %d", resPath, readSafetyLevel);
			return nil;
		}
		
		char fqResFileNameCString[OSFNMAX] = {0};
		os_build_full_path(fqResFileNameCString, sizeof(fqResFileNameCString),
						   effectiveExternalResDirCString, resPathCString);
		osfildef* fileHandle = osfoprb(fqResFileNameCString, OSFTBIN);
		if (fileHandle != NULL) {
			findResult = [XTResourceFindResult new];
			findResult.fileHandle = fileHandle;
			findResult.seekPosition = 0;
			findResult.size = [XTFileUtils sizeOfFileObj:fileHandle];
		} else {
			XT_ERROR_1(@"failed to open file \"%s\" for reading", fqResFileNameCString);
		}
	}
	
	return findResult;
	
	//TODO consider expl. search path a la html tads
}

- (XTResourceFindResult *)findResourceInAbsoluteExternalFileWithPath:(NSString *)resPath {
	
	XT_DEF_SELNAME;
	XT_TRACE_1(@"resPath=\"%@\"", resPath);
	
	int readSafetyLevel = 4;
	int writeSafetyLevel = 4;
	appctxdef *appctx = [self.tads2AppCtx getAppCtxPtr];
	appctx->get_io_safety_level(appctx->io_safety_level_ctx, &readSafetyLevel, &writeSafetyLevel);
	
	if (readSafetyLevel > 1) {
		XT_WARN_2(@"rejected resPath %@ because of read safety level %d", resPath, readSafetyLevel);
		return nil;
	}
	
	const char *resPathCString = XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(resPath);
	if (! os_is_file_absolute(resPathCString)) {
		XT_TRACE_1(@"rejected resPath %@ because it's not absolute", resPath);
		return nil;
	}
	
	if (osfacc(resPathCString) != 0) {
		XT_WARN_1(@"rejected resPath %@ because file does not exist", resPath);
		return nil;
	}
	
	XTResourceFindResult *findResult = nil;
	
	osfildef* fileHandle = osfoprb(resPathCString, OSFTBIN);
	if (fileHandle != NULL) {
		findResult = [XTResourceFindResult new];
		findResult.fileHandle = fileHandle;
		findResult.seekPosition = 0;
		findResult.size = [XTFileUtils sizeOfFileObj:fileHandle];
	} else {
		XT_ERROR_1(@"failed to open file \"%s\" for reading", resPathCString);
	}

	return findResult;
}

@end

