//
//  osxtads_dirs.m
//  XTads
//
//  Created by Rune Berg on 12/07/2020.
//  Copyright © 2020 Rune Berg. All rights reserved.
//

#include <stdio.h>
#include "os.h"
#import <Foundation/Foundation.h>
#import "osxtads_support.h"
#import "XTFileUtils.h"

extern void safe_strcpy(char *dst, size_t dstlen, const char *src);
extern void canonicalize_path(char *path);

void resolve_path( char *buf, size_t buflen, const char *path );


static NSMutableDictionary *osdirhdlDict; // NSNumber * -> NSDirectoryEnumerator *
static NSInteger nextKeyForOsdirhdlDict;

void osxtads_dirs_init()
{
	osdirhdlDict = [NSMutableDictionary dictionary];
	nextKeyForOsdirhdlDict = 0;
}

/* Create a directory.
 */
int
os_mkdir( const char* dir, int create_parents )
{
	NSString *dirString = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(dir);
	XTOSIFC_DEF_SELNAME(@"os_mkdir");
	XTOSIFC_TRACE_2(@"%@ %d", dirString, create_parents);
	
	NSURL *url = [XTFileUtils urlForDirectory:dirString];
	BOOL res = [XTFileUtils createDirectoryAtUrl:url withIntermediateDirectories:create_parents];
	
	return res;
}

/*
 *   Remove a directory.  Returns true on success, false on failure.
 *
 *   If the directory isn't already empty, this routine fails.  That is, the
 *   routine does NOT recursively delete the contents of a non-empty
 *   directory.  It's up to the caller to delete any contents before removing
 *   the directory, if that's the caller's intention.  (Note to implementors:
 *   most native OS APIs to remove directories fail by default if the
 *   directory isn't empty, so it's usually safe to implement this simply by
 *   calling the native API.  However, if your system's version of this API
 *   can remove a non-empty directory, you MUST add an extra test before
 *   removing the directory to ensure it's empty, and return failure if it's
 *   not.  For the purposes of this test, "empty" should of course ignore any
 *   special objects that are automatically or implicitly present in all
 *   directories, such as the Unix "." and ".." relative links.)
 */
int
os_rmdir( const char* dir )
{
	NSString *dirString = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(dir);
	XTOSIFC_DEF_SELNAME(@"os_rmdir");
	XTOSIFC_TRACE_1(@"%@", dirString);
	
	NSURL *url = [XTFileUtils urlForDirectory:dirString];
	BOOL res = [XTFileUtils removeDirectoryAtUrl:url];
	
	XTOSIFC_TRACE_1(@"-> %d", res);
	
	return res;
}

/*
 *   Open a directory.  This begins an enumeration of a directory's contents.
 *   'dirname' is a relative or absolute path to a directory.  On success,
 *   returns true, and 'handle' is set to a port-defined handle value that's
 *   used in subsequent calls to os_read_dir() and os_close_dir().  Returns
 *   false on failure.
 *
 *   If the routine succeeds, the caller must eventually call os_close_dir()
 *   to release the resources associated with the handle.
 */
int
os_open_dir( const char* dirname, osdirhdl_t* handle )
{
	XTOSIFC_DEF_SELNAME(@"os_open_dir");
	XTOSIFC_TRACE_1(@"%s", dirname);
	
	// NSFileManager's enumeratorAtURL doesn't resolve symlinks, so do it ourselves
	char dirnameResolved[1024];
	BOOL resolvedOk = [XTFileUtils resolveLinkFully:dirname toFilename:dirnameResolved ofMaxLength:sizeof(dirnameResolved)];
	if (! resolvedOk) {
		XTOSIFC_ERROR_0(@"failed to resolve");
		return 0;
	}
	
	NSString *dirnameResolvedString = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(dirnameResolved);
	NSURL *dirUrl = [XTFileUtils urlForDirectory:dirnameResolvedString];
	
	NSFileManager *fileMgr = [NSFileManager defaultManager];
	
	NSUInteger options = NSDirectoryEnumerationSkipsSubdirectoryDescendants |
	//NSDirectoryEnumerationSkipsHiddenFiles |
	NSDirectoryEnumerationSkipsPackageDescendants;
	
	NSDirectoryEnumerator *dirEnumerator = [fileMgr enumeratorAtURL:dirUrl
										 includingPropertiesForKeys:nil
															options:options
													   errorHandler:^(NSURL *url, NSError *error) {
														   //TODO? XT_ERROR_2(@"%@ (%@)", [error localizedDescription], url);
														   return YES; // continue after the error
													   }];
	
	// Passing a NSDirectoryEnumerator* out of (and back into) ARC scope is not trivial,
	// so instead we put it in an in-ARC map and pass out an int key,
	// so that we can look up the NSDirectoryEnumerator* in os_read_dir() and os_close_dir(),
	// using said int key.
	//
	NSNumber *keyForOsdirhdlDict = [NSNumber numberWithInteger:nextKeyForOsdirhdlDict];
	osdirhdlDict[keyForOsdirhdlDict] = dirEnumerator;
	*handle = nextKeyForOsdirhdlDict;
	
	XTOSIFC_TRACE_1(@"put NSDirectoryEnumerator with key=%d", nextKeyForOsdirhdlDict);
	
	nextKeyForOsdirhdlDict += 1;
	
	XTOSIFC_TRACE_0(@"-> 1");
	return 1;
}

/*
 *   Read the next file in a directory.  'handle' is a handle value obtained
 *   from a call to os_open_dir().  On success, returns true and fills in
 *   'fname' with the next filename; the handle is also internally updated so
 *   that the next call to this function will retrieve the next file, and so
 *   on until all files have been retrieved.  If an error occurs, or there
 *   are no more files in the directory, returns false.
 *
 *   The filename returned is the root filename only, without the path.  The
 *   caller can build the full path by calling os_build_full_path() or
 *   os_combine_paths() with the original directory name and the returned
 *   filename as parameters.
 *
 *   This routine lists all objects in the directory that are visible to the
 *   corresponding native API, and is non-recursive.  The listing should thus
 *   include subdirectory objects, but not the contents of subdirectories.
 *   Implementations are encouraged to simply return all objects returned
 *   from the corresponding native directory scan API; there's no need to do
 *   any filtering, except perhaps in cases where it's difficult or
 *   impossible to represent an object in terms of the osifc APIs (e.g., it
 *   might be reasonable to exclude files without names).  System relative
 *   links, such as the Unix/DOS "." and "..", specifically should be
 *   included in the listing.  For unusual objects that don't fit into the
 *   os_file_stat() taxonomy or that otherwise might create confusion for a
 *   caller, err on the side of full disclosure (i.e., just return everything
 *   unfiltered); if necessary, we can extend the os_file_stat() taxonomy or
 *   add new osifc APIs to create a portable abstraction to handle whatever
 *   is unusual or potentially confusing about the native object.  For
 *   example, Unix implementations should feel free to return symbolic link
 *   objects, including dangling links, since we have the portable
 *   os_resolve_symlink() that lets the caller examine the meaning of the
 *   link object.
 */
int
os_read_dir( osdirhdl_t handle, char* fname, size_t fname_size )
{
	XTOSIFC_TRACE_ENTRY(@"os_read_dir");
	
	NSInteger keyAsint = handle;
	NSNumber *keyForOsdirhdlDict = [NSNumber numberWithInteger:keyAsint];
	NSDirectoryEnumerator *dirEnumerator = osdirhdlDict[keyForOsdirhdlDict];
	
	XTOSIFC_TRACE_1(@"retrieved NSDirectoryEnumerator with key=%d", keyAsint);
	if (dirEnumerator == nil) {
		XTOSIFC_ERROR_0(@"dirEnumerator == nil, -> 0");
		return 0;
	}
	
	NSURL *nextEntry = [dirEnumerator nextObject];
	
	int res = 0;
	
	if (nextEntry != nil) {
		NSString *urlNSString = [nextEntry lastPathComponent];
		if (urlNSString != nil) {
			const char* urlCString = XTADS_NSSTRING_TO_FILESYSTEM_C_STRING(urlNSString);
			if (strlen(urlCString) < fname_size) {
				strcpy(fname, urlCString);
				res = 1;
				XTOSIFC_TRACE_2(@"fname=\"%s\" %d", urlCString, res);
			} else {
				XTOSIFC_ERROR_2(@"strlen(\"%s\") < %u", urlCString, fname_size);
			}
		}
	} else {
		XTOSIFC_TRACE_0(@"nextEntry == nil");
	}
	
	XTOSIFC_TRACE_1(@"-> %d", res);
	
	return res;
}

/*
 *   Close a directory handle.  This releases the resources associated with a
 *   directory search started with os_open_dir().  Every successful call to
 *   os_open_dir() must have a matching call to os_close_dir().  As usual for
 *   open/close protocols, the handle is invalid after calling this function,
 *   so no more calls to os_read_dir() may be made with the handle.
 */
void
os_close_dir( osdirhdl_t handle )
{
	XTOSIFC_DEF_SELNAME(@"os_close_dir");
	
	NSInteger keyAsint = handle;
	NSNumber *keyForOsdirhdlDict = [NSNumber numberWithInteger:keyAsint];
	XTOSIFC_TRACE_1(@"retrieved NSDirectoryEnumerator with key=%d", keyAsint);
	
	if (osdirhdlDict[keyForOsdirhdlDict] == nil) {
		XTOSIFC_ERROR_1(@"nil value for key %@", keyForOsdirhdlDict);
	} else {
		[osdirhdlDict removeObjectForKey:keyForOsdirhdlDict];
	}
}

/*
 *   Determine if the given file is in the given directory.  Returns true if
 *   so, false if not.  'filename' is a relative or absolute file name;
 *   'path' is a relative or absolute directory path, such as one returned
 *   from os_get_path_name().
 *
 *   If 'include_subdirs' is true, the function returns true if the file is
 *   either directly in the directory 'path', OR it's in any subdirectory of
 *   'path'.  If 'include_subdirs' is false, the function returns true only
 *   if the file is directly in the given directory.
 *
 *   If 'match_self' is true, the function returns true if 'filename' and
 *   'path' are the same directory; otherwise it returns false in this case.
 *
 *   This routine is allowed to return "false negatives" - that is, it can
 *   claim that the file isn't in the given directory even when it actually
 *   is.  The reason is that it's not always possible to determine for sure
 *   that there's not some way for a given file path to end up in the given
 *   directory.  In contrast, a positive return must be reliable.
 *
 *   If possible, this routine should fully resolve the names through the
 *   file system to determine the path relationship, rather than merely
 *   analyzing the text superficially.  This can be important because many
 *   systems have multiple ways to reach a given file, such as via symbolic
 *   links on Unix; analyzing the syntax alone wouldn't reveal these multiple
 *   pathways.
 *
 *   SECURITY NOTE: If possible, implementations should fully resolve all
 *   symbolic links, relative paths (e.g., Unix ".."), etc, before rendering
 *   judgment.  One important application for this routine is to determine if
 *   a file is in a sandbox directory, to enforce security restrictions that
 *   prevent a program from accessing files outside of a designated folder.
 *   If the implementation fails to resolve symbolic links or relative paths,
 *   a malicious program or user could bypass the security restriction by,
 *   for example, creating a symbolic link within the sandbox directory that
 *   points to the root folder.  Implementations can avoid this loophole by
 *   converting the file and directory names to absolute paths and resolving
 *   all symbolic links and relative notation before comparing the paths.
 */
// (lethe, sad, ntts use this function)
int
os_is_file_in_dir( const char* filename, const char* path, int include_subdirs,
				  int match_self )
{
	// Essentially a copy of that in frobtads osportable.cc:

	XTOSIFC_DEF_SELNAME(@"os_is_file_in_dir");
	
	NSString *ocFileName = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(filename);
	NSString *ocPath = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(path);
	
	XTOSIFC_TRACE_4(@"filename=\"%@\" path=\"%@\" include_subdirs=%d match_self=%d", ocFileName, ocPath, include_subdirs, match_self);
	
	char filename_buf[OSFNMAX], path_buf[OSFNMAX];
	size_t flen, plen;
	
	// Absolute-ize the filename, if necessary.
	if (! os_is_file_absolute(filename)) {
		os_get_abs_filename(filename_buf, sizeof(filename_buf), filename);
		filename = filename_buf;
	}
	
	// Absolute-ize the path, if necessary.
	if (! os_is_file_absolute(path)) {
		os_get_abs_filename(path_buf, sizeof(path_buf), path);
		path = path_buf;
	}
	
	// Canonicalize the paths, to remove .. and . elements - this will make
	// it possible to directly compare the path strings.  Also resolve it
	// to the extent possible, to make sure we're not fooled by symbolic
	// links.
	safe_strcpy(filename_buf, sizeof(filename_buf), filename);
	canonicalize_path(filename_buf);
	resolve_path(filename_buf, sizeof(filename_buf), filename_buf);
	filename = filename_buf;
	
	safe_strcpy(path_buf, sizeof(path_buf), path);
	canonicalize_path(path_buf);
	resolve_path(path_buf, sizeof(path_buf), path_buf);
	path = path_buf;
	
	// Get the length of the filename and the length of the path.
	flen = strlen(filename);
	plen = strlen(path);
	
	// If the path ends in a separator character, ignore that.
	if (plen > 0 && path[plen-1] == '/')
		--plen;
	
	// if the names match, return true if and only if we're matching the
	// directory to itself
	if (plen == flen && memcmp(filename, path, flen) == 0) {
		XTOSIFC_TRACE_1(@"-> %d (plen == flen && memcmp(filename, path, flen) == 0)", match_self);
		return match_self;
	}
	
	// Check that the filename has 'path' as its path prefix.  First, check
	// that the leading substring of the filename matches 'path', ignoring
	// case.  Note that we need the filename to be at least two characters
	// longer than the path: it must have a path separator after the path
	// name, and at least one character for a filename past that.
	if (flen < plen + 2 || memcmp(filename, path, plen) != 0) {
		XTOSIFC_TRACE_1(@"-> %d (flen < plen + 2 || memcmp(filename, path, plen) != 0)", 0);
		return 0;
	}
	
	// Okay, 'path' is the leading substring of 'filename'; next make sure
	// that this prefix actually ends at a path separator character in the
	// filename.  (This is necessary so that we don't confuse "c:\a\b.txt"
	// as matching "c:\abc\d.txt" - if we only matched the "c:\a" prefix,
	// we'd miss the fact that the file is actually in directory "c:\abc",
	// not "c:\a".)
	if (filename[plen] != '/') {
		XTOSIFC_TRACE_1(@"-> %d (filename[plen] != '/')", 0);
		return 0;
	}
	
	// We're good on the path prefix - we definitely have a file that's
	// within the 'path' directory or one of its subdirectories.  If we're
	// allowed to match on subdirectories, we already have our answer
	// (true).  If we're not allowed to match subdirectories, we still have
	// one more check, which is that the rest of the filename is free of
	// path separator charactres.  If it is, we have a file that's directly
	// in the 'path' directory; otherwise it's in a subdirectory of 'path'
	// and thus isn't a match.
	if (include_subdirs) {
		// Filename is in the 'path' directory or one of its
		// subdirectories, and we're allowed to match on subdirectories, so
		// we have a match.
		XTOSIFC_TRACE_1(@"-> %d (include_subdirs)", 1);
		return 1;
	}
	
	// We're not allowed to match subdirectories, so scan the rest of
	// the filename for path separators.  If we find any, the file is
	// in a subdirectory of 'path' rather than directly in 'path'
	// itself, so it's not a match.  If we don't find any separators,
	// we have a file directly in 'path', so it's a match.
	const char* p;
	for (p = filename; *p != '\0' && *p != '/' ; ++p)
		;
	
	// If we reached the end of the string without finding a path
	// separator character, it's a match .
	int res = (*p == '\0');
	XTOSIFC_TRACE_1(@"-> %d (final)", res);
	return res;
}

/* Resolve symbolic links in a path.  It's okay for 'buf' and 'path'
 * to point to the same buffer if you wish to resolve a path in place.
 */
void
resolve_path( char *buf, size_t buflen, const char *path )
{
	// Basically a copy of that in frobtads osportable.cc:
	
	// Starting with the full path string, try resolving the path with
	// realpath().  The tricky bit is that realpath() will fail if any
	// component of the path doesn't exist, but we need to resolve paths
	// for prospective filenames, such as files or directories we're
	// about to create.  So if realpath() fails, remove the last path
	// component and try again with the remainder.  Repeat until we
	// can resolve a real path, or run out of components to remove.
	// The point of this algorithm is that it will resolve as much of
	// the path as actually exists in the file system, ensuring that
	// we resolve any links that affect the path.  Any portion of the
	// path that doesn't exist obviously can't refer to a link, so it
	// will be taken literally.  Once we've resolved the longest prefix,
	// tack the stripped portion back on to form the fully resolved
	// path.
	
	// make a writable copy of the path to work with
	size_t pathl = strlen(path);
	//char *mypath = new char[pathl + 1];
	char *mypath = calloc(pathl + 1, sizeof(char));
	memcpy(mypath, path, pathl + 1);
	
	// start at the very end of the path, with no stripped suffix yet
	char *suffix = mypath + pathl;
	char sl = '\0';
	
	// keep going until we resolve something or run out of path
	for (;;)
	{
		// resolve the current prefix, allocating the result
		char *rpath = realpath(mypath, 0);
		
		// un-split the path
		*suffix = sl;
		
		// if we resolved the prefix, return the result
		if (rpath != 0)
		{
			// success - if we separated a suffix, reattach it
			if (*suffix != '\0')
			{
				// reattach the suffix (the part after the '/')
				for ( ; *suffix == '/' ; ++suffix) ;
				os_build_full_path(buf, buflen, rpath, suffix);
			}
			else
			{
				// no suffix, so we resolved the entire path
				safe_strcpy(buf, buflen, rpath);
			}
			
			// done with the resolved path
			free(rpath);
			
			// ...and done searching
			break;
		}
		
		// no luck with realpath(); search for the '/' at the end of the
		// previous component in the path
		for ( ; suffix > mypath && *(suffix-1) != '/' ; --suffix) ;
		
		// skip any redundant slashes
		for ( ; suffix > mypath && *(suffix-1) == '/' ; --suffix) ;
		
		// if we're at the root element, we're out of path elements
		if (suffix == mypath)
		{
			// we can't resolve any part of the path, so just return the
			// original path unchanged
			safe_strcpy(buf, buflen, mypath);
			break;
		}
		
		// split the path here into prefix and suffix, and try again
		sl = *suffix;
		*suffix = '\0';
	}
	
	// done with our writable copy of the path
	free(mypath);
}

/*
 *   Get a list of root directories.  If 'buf' is non-null, fills in 'buf'
 *   with a list of strings giving the root directories for the local,
 *   file-oriented devices on the system.  The strings are each null
 *   terminated and are arranged consecutively in the buffer, with an extra
 *   null terminator after the last string to mark the end of the list.
 *
 *   The return value is the length of the buffer required to hold the
 *   results.  If the caller's buffer is null or is too short, the routine
 *   should return the full length required, and leaves the contents of the
 *   buffer undefined; the caller shouldn't expect any contents to be filled
 *   in if the return value is greater than buflen.  Both 'buflen' and the
 *   return value include the null terminators, including the extra null
 *   terminator at the end of the list.  If an error occurs, or the system
 *   has no concept of a root directory, returns zero.
 *
 *   Each result string should be expressed using the syntax for the root
 *   directory on a device.  For example, on Windows, "C:\" represents the
 *   root directory on the C: drive.
 *
 *   "Local" means a device is mounted locally, as opposed to being merely
 *   visible on the network via some remote node syntax; e.g., on Windows
 *   this wouldn't include any UNC-style \\SERVER\SHARE names, and on VMS it
 *   excludes any SERVER:: nodes.  It's up to each system how to treat
 *   virtual local devices, i.e., those that look synctactically like local
 *   devices but are actually mounted network devices, such as Windows mapped
 *   network drives; we recommend including them if it would take extra work
 *   to filter them out, and excluding them if it would take extra work to
 *   include them.  "File-oriented" means that the returned devices are
 *   accessed via file systems, not as character devices or raw block
 *   devices; so this would exclude /dev/xxx devices on Unix and things like
 *   CON: and LPT1: on Windows.
 *
 *   Examples ("." represents a null byte):
 *
 *   Windows: C:\.D:\.E:\..
 *
 *   Unix example: /..
 */
size_t os_get_root_dirs( char* buf, size_t buflen )
{
	XTOSIFC_TRACE_ENTRY(@"os_get_root_dirs");
	
	char *dirs = "/\0\0";
	size_t dirslen = strlen(dirs) + 2;
	
	if (buf != NULL && buflen >= dirslen) {
		strcpy(buf, dirs);
	}
	
	return dirslen;
}

/*
 *   Get a special directory path.  Returns the selected path, in a format
 *   suitable for use with os_build_full_path().  The main program's argv[0]
 *   parameter is provided so that the system code can choose to make the
 *   special paths relative to the program install directory, but this is
 *   entirely up to the system implementation, so the argv[0] parameter can
 *   be ignored if it is not needed.
 *
 *   The 'id' parameter selects which special path is requested; this is one
 *   of the constants defined below.  If the id is not understood, there is
 *   no way of signalling an error to the caller; this routine can fail with
 *   an assert() in such cases, because it indicates that the OS layer code
 *   is out of date with respect to the calling code.
 *
 *   This routine can be implemented using one of the strategies below, or a
 *   combination of these.  These are merely suggestions, though, and systems
 *   are free to ignore these and implement this routine using whatever
 *   scheme is the best fit to local conventions.
 *
 *   - Relative to argv[0].  Some systems use this approach because it keeps
 *   all of the TADS files together in a single install directory tree, and
 *   doesn't require any extra configuration information to find the install
 *   directory.  Since we base the path name on the executable that's
 *   actually running, we don't need any environment variables or parameter
 *   files or registry entries to know where to look for related files.
 *
 *   - Environment variables or local equivalent.  On some systems, it is
 *   conventional to set some form of global system parameter (environment
 *   variables on Unix, for example) for this sort of install configuration
 *   data.  In these cases, this routine can look up the appropriate
 *   configuration variables in the system environment.
 *
 *   - Hard-coded paths.  Some systems have universal conventions for the
 *   installation configuration of compiler-like tools, so the paths to our
 *   component files can be hard-coded based on these conventions.
 *
 *   - Hard-coded default paths with environment variable overrides.  Let the
 *   user set environment variables if they want, but use the standard system
 *   paths as hard-coded defaults if the variables aren't set.  This is often
 *   the best choice; users who expect the standard system conventions won't
 *   have to fuss with any manual settings or even be aware of them, while
 *   users who need custom settings aren't stuck with the defaults.
 */
/* T3 games call this */
void
os_get_special_path( char* buf, size_t buflen, const char* argv0, int id )
{
	XTOSIFC_DEF_SELNAME(@"os_get_special_path");
	
	NSString *ocArgv0 = XTADS_FILESYSTEM_C_STRING_TO_NSSTRING(argv0);
	XTOSIFC_TRACE_2(@"argv0=\"%@\" id=\"%d\"", ocArgv0, id);
	
	switch (id) {
		case OS_GSP_T3_RES:
		case OS_GSP_T3_INC:
		case OS_GSP_T3_LIB:
		case OS_GSP_T3_USER_LIBS:
			// We can safely ignore those. They're needed only by the compiler.
			// OS_GSP_T3_RES is only needed by the base code implementation of
			// charmap.cc (tads3/charmap.cpp) which we don't use.
			
			// TODO? QTads: FIXME: We do use tads3/charmap.cpp, so this needs to be handled.
			XTOSIFC_WARN_1(@"got unknown id %d", id);
			return;
			
		case OS_GSP_T3_APP_DATA:
		case OS_GSP_LOGFILE: {
			// test/data/debuglog.t calls with this arg
			NSURL *url = [XTFileUtils urlForApplicationSupportDirectory];
			if (url != nil) {
				const char* str = url.fileSystemRepresentation;
				if (strlen(str) < buflen) {
					strcpy(buf, str);
					XTOSIFC_TRACE_1(@"-> \"%s\"", buf);
				} else {
					XTOSIFC_ERROR_1(@"buf too short for \"%s\"", str);
				}
			}
			break;
		}
			
		default:
			// We didn't recognize the specified id. That means the base code
			// added a new value for it that we don't know about.
			// TODO? Error dialog.
			XTOSIFC_WARN_1(@"got unknown id %d", id);
			break;
	}
}

