//
//  XTTads2AppCtx.m
//  XTads
//
//  Created by Rune Berg on 13/01/15.
//  Copyright (c) 2015 Rune Berg. All rights reserved.
//

#import "XTLogger.h"
#import "XTTads2AppCtx.h"
#import "XTPrefs.h"
#import "XTFileUtils.h"
#import "XTResourceFinder.h"
#import "XTResourceEntry.h"
#import "XTResourceFindResult.h"
#import "XTAllocDeallocCounter.h"


//TODO use consistently
#define XTADS_RESOURCENAME_NSSTRING_FROM_CSTRING(cString, cStringLength) \
	[[NSString alloc] initWithBytes:cString length:cStringLength encoding:NSUTF8StringEncoding]


static void setGameName(void *appctxdat, const char *fname);
static int getGameName(void *appctxdat, char *buf, size_t buflen);
static void setResDir(void *appctxdat, const char *fname);
static void setResmapSeek(void *appctxdat, unsigned long seekpos, int fileno);
static void addResource(void *appctxdat, unsigned long ofs,	unsigned long siz, const char *nm, size_t nmlen, int fileno);
static void addResourceLink(void *appctxdat, const char *fname, size_t fnamelen, const char *resname, size_t resnamelen);
static void addResPath(void *appctxdat, const char *path, size_t len);
static osfildef* findResource(void *appctxdat, const char *resname, size_t resnamelen, unsigned long *res_size);
static int addResFile(void *appctxdat, const char *fname);
static int resFileExists(void *appctxdat, const char *res_name, size_t res_name_len);
static void getIOSafetyLevels(void* ctx, int* read, int* write);

static int getIoSafetyLevelFromMode(NSString *mode);


@interface XTTads2AppCtx ()

@property appctxdef appctx;
@property XTPrefs *prefs;
@property NSString *fqGameFileName;
@property XTResourceFinder *resourceFinder;

@end


@implementation XTTads2AppCtx

static XTLogger* logger;

static XTTads2AppCtx *singletonInstance;

// from vmhost.h:
static const int VM_IO_SAFETY_MINIMUM = 0;              /* level 0: minimum safety; read/write any file */
//static const int VM_IO_SAFETY_READ_ANY_WRITE_CUR = 1; /* level 1: read any file, write only to files in the current directory */
static const int VM_IO_SAFETY_READWRITE_CUR = 2;        /* level 2: read/write in current directory only */
//static const int VM_IO_SAFETY_READ_CUR = 3;           /* level 3: read from current directory only, no writing allowed */
static const int VM_IO_SAFETY_MAXIMUM = 4;              /* level 4: maximum safety; no file reading or writing allowed */

+ (void)initialize
{
	if (singletonInstance != nil) {
		// we can be called more than once :-(
		return;
	}
	
	logger = [XTLogger loggerForClass:[XTTads2AppCtx class]];
	singletonInstance = [XTTads2AppCtx new];
}

+ (instancetype)context
{
	return singletonInstance;
}

OVERRIDE_ALLOC_FOR_COUNTER

OVERRIDE_DEALLOC_FOR_COUNTER

- (id)init
{
	self = [super init];
	if (self != nil) {
		[self myCustomInit];
	}
	singletonInstance = self;
	return self;
}

- (void)myCustomInit
{
	memset(&_appctx, 0, sizeof(appctxdef));
	
	_appctx.set_game_name = setGameName;
	_appctx.get_game_name = getGameName;
	_appctx.set_res_dir = setResDir;
	_appctx.set_resmap_seek = setResmapSeek;
	_appctx.add_resource = addResource;
	_appctx.add_resource_link = addResourceLink;
	_appctx.add_res_path = addResPath;
	_appctx.find_resource = findResource;
	_appctx.add_resfile = addResFile;
	_appctx.resfile_exists = resFileExists;
	_appctx.get_io_safety_level = getIOSafetyLevels;
	
	_fqGameFileName = nil;
	_prefs = [XTPrefs prefs];
	_resourceFinder = [XTResourceFinder new];
	_resourceFinder.tads2AppCtx = self;
}

//TODO why is this needed?
- (appctxdef *)getAppCtxPtr
{
	return &_appctx;
}

- (void)resetState
{
	[singletonInstance.resourceFinder removeAllResourceEntries];
}

@end


//--------------------------------------------------------------------
// The functions pointed to by our appctx
// Note: these are regular C funcs, not selectors
//--------------------------------------------------------------------

/*
 *   Set the root path for individual resources.  By default, we use the
 *   directory containing the game file, but this can be used to override
 *   that.
 */
static void setResDir(void *appctxdat, const char *fname) {
	
	NSString *selName = @"setResDir";
	XT_TRACE_1(@"fname=%s", fname);
	
	NSString *resDir = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(fname);
	[singletonInstance.resourceFinder setResourceDir:resDir];
}

/*
 *   Set the resource map address in the game **or external resource bundle file**.  If the .GAM
 *   reader encounters a resource map in the file, it calls this
 *   routine with the seek offset of the first resource.  Each
 *   resource's address is given as an offset from this point.
 *
 *   fileno is the file number assigned by the host system in
 *   add_resfile.  File number zero is always the .GAM file.
 */
static void setResmapSeek(void *appctxdat, unsigned long seekpos, int fileno) {
	
	NSString *selName = @"setResmapSeek";
	XT_TRACE_2(@"seekpos=%lu fileno=%d", seekpos, fileno);

	[singletonInstance.resourceFinder setSeekBase:seekpos forPhysicalFileNo:fileno];
}

/*
 *   Add a resource entry.  The 'ofs' entry is the byte offset of the
 *   start of the resource, relative to the seek position previously
 *   set with set_resmap_seek.  'siz' is the size of the resource in
 *   bytes; the resource is stored as contiguous bytes starting at the
 *   given offset for the given size.  Note that resources may be
 *   added before the resource map seek position is set, so the host
 *   system must simply store the resource information for later use.
 *   The 'fileno' is zero for the .GAM file, or the number assigned by
 *   the host system in add_resfile for other resource files.
 */
static void addResource(void *appctxdat, unsigned long ofs,
				 unsigned long siz, const char *nm, size_t nmlen, int fileno) {
	
	NSString *selName = @"addResource";
	
	XTResourceEntry *entry = [XTResourceEntry new];
	NSString *nameString = [[NSString alloc] initWithBytes:nm length:nmlen encoding:NSUTF8StringEncoding];
	entry.resourceName = nameString;
	entry.seekBaseOffset = ofs;
	entry.size = siz;
	entry.physicalFileNo = fileno;
	
	[singletonInstance.resourceFinder addResourceEntry:entry];
}

// used for games built in debug mode
static void addResourceLink(void *appctxdat,
					 const char *fname, size_t fnamelen,
					 const char *resname, size_t resnamelen) {
	
	NSString *localFileName = XTADS_FILESYSTEM_C_STRING_LEN_TO_NSSTRING(fname, fnamelen);
	NSString *resName = XTADS_RESOURCENAME_NSSTRING_FROM_CSTRING(resname, resnamelen);
	XTResourceEntry *entry = [XTResourceEntry new];
	entry.linkToLocalFile = localFileName;
	entry.resourceName = resName;

	[singletonInstance.resourceFinder addResourceEntry:entry];
}

static void addResPath(void *appctxdat, const char *path, size_t len) {

	// not used
	
	NSString *selName = @"addResPath";
	XT_WARN_0(@"not implemented");
}

 /*
 *   Find a resource entry.  If the resource can be found, this must
 *   return an osfildef* handle to the resource, with its seek position
 *   set to the first byte of the resource data, and set *res_size to
 *   the size in bytes of the resource data in the file.  If the
 *   resource cannot be found, returns null.
 */
static osfildef* findResource(void *appctxdat,
							  const char *resname,
							  size_t resnamelen,
							  unsigned long *res_size)
{
	NSString *selName = @"findResource";

	NSString *resnameString = [[NSString alloc] initWithBytes:resname length:resnamelen encoding:NSUTF8StringEncoding];

	XT_TRACE_1(@"resname=%@", resnameString);
	
	osfildef* res = NULL;
	
	XTResourceFindResult *findResult = [singletonInstance.resourceFinder findResource:resnameString];
	if (findResult != nil) {
		osfseek(findResult.fileHandle, findResult.seekPosition, OSFSK_SET);
		*res_size = findResult.size;
		res = findResult.fileHandle;
	}
	
	return res;
}

// arrival uses this
static int addResFile(void *appctxdat, const char *fname) {
	
	NSString *selName = @"addResFile";
	XT_TRACE_1(@"fname=%s", fname);

	NSString *resFileName = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(fname);
	NSInteger resFileIdx = [singletonInstance.resourceFinder addPhysicalResourceFile:resFileName];
	
	return (int)resFileIdx;
}

static int resFileExists(void *appctxdat, const char *res_name, size_t res_name_len) {
	
	NSString *selName = @"resFileExists";
	
	NSString *resNameString = XTADS_RESOURCENAME_NSSTRING_FROM_CSTRING(res_name, res_name_len);
	BOOL exists = [singletonInstance.resourceFinder resourceExists:resNameString];
	XT_TRACE_2(@"-> %d for resNameString=%@", exists, resNameString);
	
	return exists;
}

static void setGameName(void *appctxdat, const char *fname) {
	
	if (fname != NULL) {
		NSString *fqGameFileName = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(fname);
		singletonInstance.fqGameFileName = fqGameFileName;
			//TODO reconsider wrt. below line
		[singletonInstance.resourceFinder setGameFileName:fqGameFileName];
	}
}

static int getGameName(void *appctxdat, char *buf, size_t buflen) {
	
	int res = 0;
	
	NSString *fqGameFileName = singletonInstance.fqGameFileName;
	if (fqGameFileName != nil) {
		if (fqGameFileName.length < buflen) {
			strncpy(buf, XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(fqGameFileName), buflen);
			res = 1;
		}
	}
	
	return res;
}

static void getIOSafetyLevels(void* ctx, int* read, int* write)
{
	NSString *selName = @"getIOSafetyLevels";
	XT_TRACE_0(@"");
	
	if (singletonInstance != nil) {
		if (read != NULL) {
			NSString *mode = singletonInstance.prefs.readSafetyMode.value;
			*read = getIoSafetyLevelFromMode(mode);
		}
		if (write != NULL) {
			NSString *mode = singletonInstance.prefs.writeSafetyMode.value;
			*write = getIoSafetyLevelFromMode(mode);
		}
	} else {
		XT_ERROR_0(@"singletonInstance == nil");
	}
}

static int getIoSafetyLevelFromMode(NSString *mode)
{
	NSString *selName = @"getIoSafetyLevelFromMode";
	XT_TRACE_1(@"mode=%@", mode);
	
	int res = VM_IO_SAFETY_MAXIMUM;
	
	if ([XTPREFS_IO_SAFETY_MODE_NO_ACCESS isEqualToString:mode]) {
		res = VM_IO_SAFETY_MAXIMUM;
	} else if ([XTPREFS_IO_SAFETY_MODE_GAME_DIR_ONLY isEqualToString:mode]) {
		res = VM_IO_SAFETY_READWRITE_CUR;
	} else if ([XTPREFS_IO_SAFETY_MODE_ANYWHERE isEqualToString:mode]) {
		res = VM_IO_SAFETY_MINIMUM;
	} else {
		XT_WARN_1(@"unknown mode=%d", mode);
	}
	
	XT_TRACE_1(@"-> %d", res);
	
	return res;
}
