//
//  XTFileUtils.m
//  TadsTerp
//
//  Created by Rune Berg on 04/07/14.
//  Copyright (c) 2014 Rune Berg. All rights reserved.
//

#include <sys/stat.h>
#include <sys/syslimits.h>
#include "os.h"
#import "XTFileUtils.h"
#import "XTLogger.h"


@implementation XTFileUtils

static XTLogger* logger;

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

+ (NSUInteger)sizeOfFileObj:(osfildef *)fileObj
{
	XT_TRACE_ENTRY;

	NSUInteger res = 0;
	struct stat statBuf;
	if (fstat(fileno(fileObj), &statBuf) == 0) {
		res = statBuf.st_size;
	} else {
		XT_WARN_0(@"fstat() failed");
	}
	XT_TRACE_1(@"-> %lu", res);
	return res;
}

+ (NSUInteger)sizeOfFile:(NSString *)filename
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"filename=%@", filename);
	
	NSUInteger res = 0;
	struct stat statBuf;
	const char *filenameCString = XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(filename);
	if (stat(filenameCString, &statBuf) == 0) {
		res = statBuf.st_size;
	} else {
		XT_WARN_0(@"stat() failed");
	}
	XT_TRACE_1(@"-> %lu", res);
	return res;
}

+ (NSURL *)urlForDirectory:(NSString *)path
{
	NSURL *res = [NSURL fileURLWithPath:path isDirectory:YES];
	return res;
}

//TODO replace by isExistingFileUrl below?
+ (BOOL)fileExists:(NSURL *)filenameUrl
{
	NSError *error;
	BOOL res = [filenameUrl checkResourceIsReachableAndReturnError:&error];
	return res;
}

+ (NSURL *)urlIfAbsolute:(NSString *)filename;
{
	NSURL *filenameUrl = [NSURL fileURLWithPath:filename];
	NSURL *filenameUrlAbs = [filenameUrl absoluteURL];
	NSURL *res = ([filenameUrl isEqualTo:filenameUrlAbs] ? filenameUrlAbs : nil);
	return res;
}

//TODO rename - method only checks names
+ (BOOL)directory:(NSURL *)dirUrl containsFile:(NSURL *)fileUrl
{
	//TODO proper impl - current one can "lie" - see NSURL pathComponents

	NSString *dirString = [dirUrl relativeString];
	NSString *fileString = [fileUrl relativeString];
	BOOL foundInDir = NO;
	if ([fileString hasPrefix:dirString]) {
		if ([XTFileUtils fileExists:fileUrl]) {
			foundInDir = YES;
		}
	};
	return foundInDir;
}

+ (NSURL *)existingDirectoryUrlFor:(NSURL *)fileOrDirectoryUrl
{
	XT_DEF_SELNAME;
	
	NSURL *res = nil;
	
	if ([XTFileUtils isExistingDirectoryUrl:fileOrDirectoryUrl]) {
		res = fileOrDirectoryUrl;
	} else if ([XTFileUtils isExistingFileUrl:fileOrDirectoryUrl]) {
		res = [fileOrDirectoryUrl URLByDeletingLastPathComponent];
	} else {
		// in case filename component doesn't yet exist:
		NSURL *temp = [fileOrDirectoryUrl URLByDeletingLastPathComponent];
		if ([XTFileUtils isExistingDirectoryUrl:temp]) {
			res = temp;
		}
	}
	
	XT_TRACE_2(@"\"%@\" -> %@", fileOrDirectoryUrl, res);
	
	return res;
}

+ (BOOL)isExistingDirectoryUrl:(NSURL *)url
{
	XT_DEF_SELNAME;

	BOOL res = [XTFileUtils url:url existsAndIsOfType:NSURLIsDirectoryKey];
	
	XT_TRACE_2(@"\"%@\" -> %d", url, res);
	
	return res;
}

+ (BOOL)isExistingFileUrl:(NSURL *)url
{
	XT_DEF_SELNAME;

	BOOL res = [XTFileUtils url:url existsAndIsOfType:NSURLIsRegularFileKey];
	
	XT_TRACE_2(@"\"%@\" -> %d", url, res);
	
	return res;
}

+ (BOOL)fileUrl:(NSURL *)url hasExtensionIn:(NSArray *)extensions
{
	BOOL res = NO;
	
	if (url != nil && extensions != nil) {
		NSString *urlExt = url.pathExtension;
		if (urlExt != nil && urlExt.length >= 1) {
			NSString *urlExtLowerCase = [urlExt lowercaseString];
			for (NSString *ext in extensions) {
				NSString *extLowerCase = [ext lowercaseString];
				if ([extLowerCase isEqualToString:urlExtLowerCase]) {
					res = YES;
					break;
				}
			}
		}
	}
	
	return res;
}

+ (BOOL)url:(NSURL *)url existsAndIsOfType:(NSString *)typeKey
{
	// http://stackoverflow.com/questions/22277117/how-to-find-out-if-the-nsurl-is-a-directory-or-not

	XT_DEF_SELNAME;
	
	BOOL res = NO;
	
	if (url != nil && typeKey != nil) {
		NSNumber *isDir;
		BOOL resValOk = [url getResourceValue:&isDir forKey:typeKey error:nil];
		if (resValOk && isDir.boolValue) {
			res = YES;
		}
	}
	
	XT_TRACE_3(@"\"%@\" existsAndIsOfType:\"%@\" -> %d", url, typeKey, res);
	
	return res;
}

+ (BOOL)createDirectoryAtUrl:(NSURL *)url withIntermediateDirectories:(BOOL)createIntermediates
{
	XT_DEF_SELNAME;
	XT_TRACE_2(@"url=\"%@\" withIntermediateDirectories=%d", url, createIntermediates);

	NSFileManager *fileMgr = [NSFileManager defaultManager];
	
	NSError *error = nil;
	BOOL res = [fileMgr createDirectoryAtURL:url withIntermediateDirectories:createIntermediates attributes:nil error:&error];
	if (error != nil) {
		XT_ERROR_1(@"%@", [error localizedDescription]);
	}
	
	XT_TRACE_1(@"-> %d", res);
	
	return res;
}

+ (BOOL)removeDirectoryAtUrl:(NSURL *)url
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"url=\"%@\"", url);
	
	NSFileManager *fileMgr = [NSFileManager defaultManager];
	
	BOOL res = NO;

	if ([XTFileUtils isEmptyDirectoryAtUrl:url]) {
		NSError *error = nil;
		res = [fileMgr removeItemAtURL:url error:&error];
		if (error != nil) {
			res = NO;
			XT_ERROR_1(@"%@", [error localizedDescription]);
		}
	}
	
	XT_TRACE_1(@"-> %d", res);

	return res;
}

+ (BOOL)isEmptyDirectoryAtUrl:(NSURL *)url
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"url=\"%@\"", url);
	
	NSFileManager *fileMgr = [NSFileManager defaultManager];
	
	NSError *error = nil;
	NSArray *contents = [fileMgr contentsOfDirectoryAtURL:url includingPropertiesForKeys:nil options:0 error:&error];
	if (error != nil) {
		XT_ERROR_1(@"%@", [error localizedDescription]);
	}
	
	BOOL res = (error == nil && contents != nil && contents.count == 0);
	
	XT_TRACE_1(@"-> %d", res);
	
	return res;
}

+ (BOOL)resolveLinkFully:(const char *)filename toFilename:(char *)resolvedFilename ofMaxLength:(size_t)maxLenOfResolvedFilename
{
	XT_DEF_SELNAME;
	XT_TRACE_1(@"filename=\"%s\"", filename);
	
	NSString *linkString = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(filename);
	NSString *resolved = [linkString stringByResolvingSymlinksInPath];
	BOOL res = NO;
	
	if (resolved == nil || resolved == linkString) {
		XT_ERROR_1(@"stringByResolvingSymlinksInPath failed for \"%s\"", filename);
	} else if ([resolved characterAtIndex:0] != '/') {
		XT_ERROR_1(@"stringByResolvingSymlinksInPath gave relative result \"%@\"", resolved);
	} else if (resolved.length >= maxLenOfResolvedFilename) {
		XT_ERROR_1(@"\"%s\" too long for resolvedFilename buffer", filename);
	} else {
		strncpy(resolvedFilename, XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(resolved), maxLenOfResolvedFilename);
		res = YES;
		XT_TRACE_2(@"resolved \"%s\" to \"%s\"", filename, resolvedFilename);
	}
	
	return res;
}

+ (void)stat:(struct stat*)stat toMode:(unsigned long*)mode forFileName:(const char*)fileName
{
	XT_DEF_SELNAME;

	*mode = 0;
	if (S_ISBLK(stat->st_mode)) {
		*mode |= OSFMODE_BLK;
	}
	if (S_ISCHR(stat->st_mode)) {
		*mode |= OSFMODE_CHAR;
	}
	if (S_ISDIR(stat->st_mode)) {
		*mode |= OSFMODE_DIR;
	}
	if (S_ISFIFO(stat->st_mode)) {
		*mode |= OSFMODE_PIPE;
	}
	if (S_ISREG(stat->st_mode)) {
		*mode |= OSFMODE_FILE;
	}
	if (S_ISLNK(stat->st_mode)) {
		*mode |= OSFMODE_LINK;
	}
	if (S_ISSOCK(stat->st_mode)) {
		*mode |= OSFMODE_SOCKET;
	}
	
	if (XT_TRACE_ON) {
		
		NSMutableString *modesString = [NSMutableString stringWithString:@""];
		if ((*mode & OSFMODE_FILE) == OSFMODE_FILE) {
			[modesString appendString:@"[Regular file] "];
		}
		if ((*mode & OSFMODE_DIR) == OSFMODE_DIR) {
			[modesString appendString:@"[Directory] "];
		}
		if ((*mode & OSFMODE_BLK) == OSFMODE_BLK) {
			[modesString appendString:@"[Block-mode device] "];
		}
		if ((*mode & OSFMODE_CHAR) == OSFMODE_CHAR) {
			[modesString appendString:@"[Character-mode device] "];
		}
		if ((*mode & OSFMODE_PIPE) == OSFMODE_PIPE) {
			[modesString appendString:@"[Pipe/FIFO/other character-oriented IPC] "];
		}
		if ((*mode & OSFMODE_SOCKET) == OSFMODE_SOCKET) {
			[modesString appendString:@"[Network socket] "];
		}
		if ((*mode & OSFMODE_LINK) == OSFMODE_LINK) {
			[modesString appendString:@"[Symbolic link] "];
		}
		XT_TRACE_3(@"\"%s\" - mode (%lu) = %@", fileName, *mode, modesString);
	}
}

+ (int)stat:(const struct stat*)stat toAttrs:(unsigned long *)attrs forFileName:(const char*)fileName
	//TODO fn must be wo path prefix ... always correctly called?
{
	*attrs = 0;
	
	if (fileName[0] == '.') {
		*attrs |= OSFATTR_HIDDEN;
	}
	
	// If we're the owner, check if we have read/write access
	//---------------------------------------------------------
	
	if (geteuid() == stat->st_uid) {
		//TODO re. impl of mode, check that these actually work
		if (stat->st_mode & S_IRUSR) {
			*attrs |= OSFATTR_READ;
		}
		if (stat->st_mode & S_IWUSR) {
			*attrs |= OSFATTR_WRITE;
		}
		[XTFileUtils traceAttrs:*attrs forFileName:fileName];
		return 1;
	}
	
	// We're not the owner, so sheck if one of our groups matches
	// the file's group and if so, check for read/write access
	//------------------------------------------------------------
	
	// Also reserve a spot for the effective group ID, which might
	// not be included in the list in our next call.
	int grpSize = getgroups(0, NULL) + 1;
	// Paranoia.
	if (grpSize > NGROUPS_MAX || grpSize < 0) {
		[XTFileUtils traceAttrs:*attrs forFileName:fileName];
		return 0;
	}
	gid_t* groups = calloc(grpSize, sizeof(gid_t));  //WAS: new gid_t[grpSize];
	if (getgroups(grpSize - 1, groups + 1) < 0) {
		free(groups);
		[XTFileUtils traceAttrs:*attrs forFileName:fileName];
		return 0;
	}
	groups[0] = getegid();
	int i;
	for (i = 0; i < grpSize && stat->st_gid != groups[i]; ++i)
		;
	free(groups);
	if (i < grpSize) {
		//TODO re. impl of mode, check that these actually work
		if (stat->st_mode & S_IRGRP) {
			*attrs |= OSFATTR_READ;
		}
		if (stat->st_mode & S_IWGRP) {
			*attrs |= OSFATTR_WRITE;
		}
		[XTFileUtils traceAttrs:*attrs forFileName:fileName];
		return 1;
	}
	
	// We're neither the owner of the file nor do we belong to its
	// group.  Check whether the file is world readable/writable
	//--------------------------------------------------------------
	
	//TODO re. impl of mode, check that these actually work
	if (stat->st_mode & S_IROTH) {
		*attrs |= OSFATTR_READ;
	}
	if (stat->st_mode & S_IWOTH) {
		*attrs |= OSFATTR_WRITE;
	}

	[XTFileUtils traceAttrs:*attrs forFileName:fileName];
	return 1;
}

+ (void) traceAttrs:(unsigned long)attrs forFileName:(const char*)fileName
{
	XT_DEF_SELNAME;

	if (XT_TRACE_ON) {
		
		NSMutableString *attrString = [NSMutableString stringWithString:@""];
		if ((attrs & OSFATTR_HIDDEN) == OSFATTR_HIDDEN) {
			[attrString appendString:@"[Hidden] "];
		}
		if ((attrs & OSFATTR_SYSTEM) == OSFATTR_SYSTEM) {
			[attrString appendString:@"[System] "];
		}
		if ((attrs & OSFATTR_READ) == OSFATTR_READ) {
			[attrString appendString:@"[Read-access] "];
		}
		if ((attrs & OSFATTR_WRITE) == OSFATTR_WRITE) {
			[attrString appendString:@"[Write-access] "];
		}
		
		XT_TRACE_3(@"\"%s\" - attrs (%lu) = %@", fileName, attrs, attrString);
	}
}

+ (NSURL *)urlForApplicationSupportDirectory
{
	XT_DEF_SELNAME;

	NSFileManager *fileMgr = [NSFileManager defaultManager];
	
	NSURL *appSupportBaseUrl = [fileMgr URLForDirectory:NSApplicationSupportDirectory
								 inDomain:NSUserDomainMask
						appropriateForURL:nil
								   create:NO
									error:nil];
	
	NSURL *appSpecificUrl = nil;
	
	if (appSupportBaseUrl != nil) {
		appSpecificUrl = [appSupportBaseUrl URLByAppendingPathComponent:@"XTads"];
			//TODO don't hard code last bit
		if ([XTFileUtils isExistingDirectoryUrl:appSpecificUrl]) {
			XT_TRACE_1(@"dir \"%@\" already exists", appSpecificUrl);
		} else {
			if ([XTFileUtils createDirectoryAtUrl:appSpecificUrl withIntermediateDirectories:NO]) {
				XT_TRACE_1(@"created dir \"%@\"", appSpecificUrl);
			} else {
				XT_ERROR_1(@"failed to create dir \"%@\"", appSpecificUrl);
				appSpecificUrl = nil;
			}
		}
	}
	
	XT_TRACE_1(@"-> %@", appSpecificUrl);
	return appSpecificUrl;
}

@end
