#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <osbind.h>
#include <device.h>
#include <types.h>
#include <dirent.h>
#include "symdir.h"
#include "lib.h"
#ifndef _COMPILER_H
#include <compiler.h>
#endif
    
#ifndef NAME_MAX
#  include <limits.h>
#endif
    
#ifndef _LIB_NAME_MAX
#  define _LIB_NAME_MAX NAME_MAX
#endif
    
#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif
    
char _lOK;			/* symbolic links are OK 		   */
char *_lDIR = ".dir";		/* name of symbolic link directory file    */
char _lAUTO;			/* make automatic symbolic links 	   */
char _lHIDE;			/* hide the _lDIR file from searches 	   */

char _tCASE;			/* translate filenames to lower case	   */
char _tSLASH;			/* '/' seperates directories like '\' does */
char _tDOTS;			/* if != 0, translate '.' into this        */
char _tUNLIMITED;		/* filenames are not restricted to 8.3	   */
char _tDEV;			/* allow /dev/filename for special files   */
char _tROOT;			/* use this drive as default root directory */

/* translations to assume if there is no UNIXMODE environment variable
 * it's assumed that the user will set this via a definition
 *    char *_default_unixmode = whatever
 * if no such definition is present (i.e. _default_unixmode is 0) then
 * "/d" is assumed
 */

char *_default_unixmode;
int _unixmode = 1;	/* this will go away someday */

/*
 * _set_unixmode(mode): set Unix emulation modes. Normally "mode" will come
 * from the environment variable UNIXMODE. These settings should only be
 * changed at the explicit request of the user!
 * The characters in "mode" have the following meanings:
 * 	b	Open all files in binary mode (no CR, only LF).
 *	c	Assume case is already significant to the operating system.
 *	d	Allow special file names like /dev/console.
 *	r drv	Filenames starting with '/' are rooted from this drive.
 *	u	Assume the OS allows unlimited length file names already.
 *	/	Allow '/' as a directory seperator.
 *	. char	Translate extra dots in a filename to 'char'.
 *	L	Allow symbolic links.
 *
 * The following characters are meaningful only if 'L' is present.
 *	A	Automatically create symbolic links for files whose names
 *		are changed by the _unx2dos routine (e.g. filenames that
 *		don't match the TOS 8 character + 3 character extension
 *		rule). Such automatic symbolic links are much "tighter"
 *		than normal symbolic links, and are effectively aliases
 *		for the old name.
 *	H	Hide the .dir file from directory searches. Explicit
 *		requests for it (e.g. unlink()) still find it, though.
 */

static int _adjust __PROTO((char *name, char *result));
static int _canon __PROTO((char *base, char *path, char *result, int level));

void _set_unixmode(mode)
char *mode;
{
    char c;
    
    if (!mode && !(mode = _default_unixmode)) {
#if 0
	/* this is what we will eventually do */
	mode = "/";
#else
	/* compatibility with older versions of the library */
	switch(_unixmode) {
	  case 0: mode = ""; break;
	  case 1: mode = "/d"; break; /* 'd' added to default */
	  case 2: mode = "/.,"; break;
	  default: mode = "/.,LAHd"; break;
	}
#endif
    }
    _tSLASH = _tDOTS = _tUNLIMITED = _tDEV = FALSE;
    _tCASE = TRUE;
    _tROOT = 0;
    _lOK = _lHIDE = _lAUTO = FALSE;
    
    while (c = *mode++) {
	switch(c) {
	  case 'b': _binmode(TRUE); break;
	  case 'c': _tCASE = FALSE; break;
	  case 'd': _tDEV = TRUE; break;
	  case 'u': _tUNLIMITED = TRUE; break;
	  case '/': _tSLASH = TRUE; break;
	  case 'L': _lOK = TRUE; break;
	  case 'A': _lAUTO = TRUE; break;
	  case 'H': _lHIDE = TRUE; break;
	  case '.':
	    if (*mode && !isalpha(*mode))
		_tDOTS = *mode++;
	    break;
	  case 'r':
	    if (*mode && isalpha(*mode))
		_tROOT = *mode++;
	  default:
	    break;
	}
    }
}

/*
 * _uniquefy(name): change name so that it's not the name of any existing
 * file. If the file already exists, then we try changing the file's
 * extension in the following ways:
 * put '$' as the second character
 * put '0'-'9' as the last character (with '$' still in second place).
 * put 'A'-'Z' as the last character (with '$' still in second place).
 * if all of the above fail, put '$$' in the last two characters and
 * give up.
 */

void
    _uniquefy(dos)
char *dos;
{
    char *ext, c;
    struct dirent *_do_stat(char *);
    
    if (!_do_stat(dos))	/* file not found, so "dos" is unique */
	return;
    
    /* make 'ext' point to the (last two characters of) the extension */
    
    ext = strrchr(dos, '\\'); if (!ext) ext = dos;
    ext = strrchr(ext, '.');
    if (ext && ext[1]) {
	ext+=2;
	if (!*ext) {
	    ext[0] = 0; ext[1] = 0;
	}
    }
    else {
	for(ext=dos; *ext; ext++);
	strcpy(ext, ".$0"); ext++;
    }
    
    *ext = '$';
    if (!_do_stat(dos))
	return;
    
    ext[2] = 0;
    for (c = '0'; c <= '9'; c++) {
	ext[1] = c;
	if (!_do_stat(dos))
	    return;
    }
    for (c = 'A'; c <= 'Z'; c++) {
	ext[1] = c;
	if (!_do_stat(dos))
	    return;
    }
    /* at this point we're sunk; punt and try '$$' in the extension */
    ext[1] = '$';
}

/*
 * adjust(name): adjusts a directory entry to be acceptable to DOS.
 * if _tCASE is on, it is converted to upper case.
 * if _tDOTS is on, '.' is converted to _tDOTS wherever necessary to avoid
 *    conflicts with the DOS 8.3 naming convention.
 * unless _tUNLIMITED is set, only an 8 character base name and 3 character
 *    extension are kept.
 * returns: _NM_CHANGE if the name has been transformed in some irrevocable way
 *    i.e. if dos2unx cannot recover the same name
 *          _NM_OK otherwise
 */

static int
    _adjust(name, result)
char *name;
char *result;
{
    char tmp[_LIB_NAME_MAX];
    char *n, *eos, *lastdot = 0;
    int count, change;
    
#ifdef DEBUG
    printf("_adjust(%s)", name); fflush(stdout);
#endif
    strcpy(tmp, name);
    change = 0;
    
    /* first, do case and dot conversion */
    for (n = tmp; *n; n++) {
	if (_tDOTS) {
	    if(*n == '.')
		*n = _tDOTS;
	    else if (*n == _tDOTS)
		change++;
	}
	if (_tCASE) {
	    if (islower(*n))
		*n = toupper(*n);
	    else if (isupper(*n))
		change++;
	}
    }
    
    eos = n;	/* end of the "tmp" string */
    
    /* if dots were converted, change the last one possible back into a '.'
     * e.g. if _tDOTS == '_', file.c.Z -> file_c_Z -> file_c.Z
     */
    if (_tDOTS && !_tUNLIMITED) {
	for (n = tmp; *n; n++) {
	    if (*n == _tDOTS) {
		if ( (eos - n) <= 4 && (n - tmp) > 8 ) {
		    *n = '.';
		    goto dot_was_set;
		}
		else lastdot = n;
	    }
	}
	/* if we found no good place, and if the name is too long to fit without
	 * an extension, we just put the period in the last possible place
	 */
	if (lastdot && (n - tmp > 8 || eos - lastdot <= 4))
	    *lastdot = '.' ;
    }
    
  dot_was_set:
    /*
     * here we enforce the 8 character name + 3 character extension rule
     */
    if (_tUNLIMITED)
	strcpy(result, name);
    else {
	eos = result;
	n = tmp;
	for (count = 0; count < 8; count++) {
	    if (!*n || *n == '.') break;
	    *eos++ = *n++;
	}
	while (*n && *n != '.') {
	    change++; n++;
	}
	if (*n == '.') *eos++ = *n++;
	for (count = 0; count < 3; count++) {
	    if (!*n) break;
	    *eos++ = *n++;
	}
	*eos++ = 0;
	change += strlen(n);
    }
#ifdef DEBUG
    printf("->(%s)\n", result);
#endif
    return change ? _NM_CHANGE : _NM_OK;
}

/*
 * _canon(base, path, result, level):
 * find a canonical form for the given path, based in the directory "base"
 * (e.g. the current directory); the result is copied into "result".
 * "level" is the level of recursion, and is used to prevent infinite
 * loops in symbolic links.
 *
 * "base" must already be in a canonical form; the return value from the
 * GEMDOS Dgetpath() call is almost suitable, but the drive letter must
 * be prefixed to it. No checking of "base" is done.
 * Note that "base" and "result" may be pointers to the same array!
 *
 * returns:
 *    _NM_LINK	if the last component is a symbolic link
 *    _NM_OK	if the name of the last component is recoverable by _dos2unx
 *    _NM_CHANGE otherwise
 *
 * Also, the global variables __link_path and __link_name are set
 * to the path of the last filename component, and the name of
 * the file, respectively (the canonical form of the file name
 * to is returned in result, and will NOT be __link_path\__link_name if
 * __link_name was a symbolic link). If it was a symbolic link, __link_flags
 * is set to the flags associated with the link (e.g. whether it was
 * auto-created), and __link_to is set to the link contents, which were
 * followed in the course of creating the canonical name.
 *
 * All this is done so that functions like "creat" and "rename" that need
 * access to the path and "real" name of the file (if it was a symbolic link)
 * don't have to duplicate work we've already done.
 */

#define DIRSEP(p) ((p) == '\\' || (_tSLASH && (p) == '/'))
#define MAXRECURSE 12

char __link_path[FILENAME_MAX], __link_name[_LIB_NAME_MAX],
    __link_to[FILENAME_MAX];
int __link_flags = 0;

static int
    _canon(base, path, result, level)
char *base, *path;
char *result;
int level;
{
    char tmp[_LIB_NAME_MAX], name[_LIB_NAME_MAX];
    SYMDIR *dir;
    SYMENTRY *ent;
    char *n;
    int found, change = _NM_OK, needschange = _NM_OK;
    
    /* FIX_ME: we really ought to flag an error of some sort here */
    if (level > MAXRECURSE) {
	return _NM_OK;
    }
    
#ifdef DEBUG
    printf("_canon: [%s] + [%s]\n", base, path);
#endif
    if (!*path) {
	if (result != base)
	    strcpy(result, base);
	return _NM_OK;
    }
    
    /* check for paths relative to root of current drive 		*/
    /* if _tROOT is set, then such paths should start at _tROOT 	*/
    
    if (DIRSEP(path[0])) {
	if (result != base)
	    strcpy(result, base);
	if (_tROOT) {
	    if (_tCASE)
		result[0] = toupper(_tROOT);
	    else
		result[0] = _tROOT;
	}
	result[2] = 0;	/* the drive is already in the first part */
	path++;
    }
    /* check for absolute paths with drive letter given */
    else if (path[1] == ':') {
	result[0] = _tCASE ? toupper(path[0]) : path[0];
	result[1] = ':';
	result[2] = 0;
	path += 2;
	if (DIRSEP(path[0])) {
	    path++;
	}
    }
    else if (result != base)
	strcpy(result, base);
    
    /* now path is relative to what's currently in "result" */
    
    while(*path) {
	/* get next name in path */
	n = name;
	while (*path && !DIRSEP(*path))
	    *n++ = *path++;
	*n++ = 0;
	if (*path) path++;
	change = _NM_OK;	/* assume this is a regular name */
	
	/* check for "." and ".." */
	if (!strcmp(name, ".")) continue;
	if (!strcmp(name, "..")) {
	    n = strrchr(result, '\\');
	    if (n) *n = 0;
	    continue;
	}
	
	/* see if "name" is a symbolic link */
	found = 0;
	dir = _read_symdir(result);
#ifdef DEBUG
	if (!dir) printf("unx2dos: _read_symdir(%s) failed\n", result);
#endif
	ent = dir ? dir->s_dir : 0;
	
	while (ent) {
	    if (!strcmp(ent->linkname, name)) {
		change = _NM_LINK;
#ifdef DEBUG
		printf("...following link (%s)->(%s)\n", name, ent->linkto);
#endif
		
		if (level == 0) {
		    strcpy(__link_path, result);
		    strcpy(__link_to, ent->linkto);
		    __link_flags = ent->flags;
		}
		/*
		 * note that _free_symdir caches the directories it frees, so it's OK
		 * to use ent->linkto afterwards (otherwise we'd have to strdup() it.
		 * Also note that we *do* want to free it before recursively calling
		 * ourselves; otherwise it won't be in the cache.
		 */
		found = 1;
		_free_symdir(dir);
		/*
		 * We should follow normal symbolic links all the way through, since it's
		 * OK to have a link to a link.
		 * Auto symbolic links, however, must be linked to a real GEMDOS file.
		 */
		if (ent->flags & SD_AUTO) {
		    strcat(result, "\\");
		    _adjust(ent->linkto, tmp);
		    strcat(result, tmp);
		}
		else {
		    _canon(result, ent->linkto, result, level+1);
		}
		
		break;	/* out of the search */
	    }
	    /*
	     * if the name we're searching for is the target of an automatic link,
	     * then we should have been using that name instead. to prevent
	     * name conflict problems, flag the name as needing to be changed;
	     * _unx2dos can then try to sort out what to do.
	     */
	    else if (_lAUTO && !strcmp(ent->linkto, name)) {
		if (ent->flags & SD_AUTO)
		    needschange = _NM_CHANGE;
	    }
	    ent = ent->next;
	}
	
	/* if found == 1, then "result" has already been appropriately updated */
	/* otherwise, append "name" to path, adjusting appropriately */
	if (!found) {
	    _free_symdir(dir);
	    strcat(result, "\\");
	    change = _adjust(name, tmp);
	    strcat(result, tmp);
	    if (needschange) change = needschange;
	}
    }
    if (level == 0)
	strcpy(__link_name, name);
    return change;
}

/*
 * translate a Unix style filename to a DOS one 
 * returns:
 * _NM_DEV	if the name was of a device
 * _NM_LINK 	if the name is a symbolic link
 * _NM_OK 	if the last component can be recovered via _dos2unx
 * _NM_CHANGE	if the last component is irrevocably changed
 */

int
    _lib_unx2dos(unx, dos)
const char *unx;
char *dos;
{
    char path[FILENAME_MAX];
    static char _old_unixmode = 1;
    struct _device *d;
    int change;
    
    /* compatibility code: will go away someday soon */
    if (_unixmode != _old_unixmode) {
	_old_unixmode = _unixmode;
	_set_unixmode(NULL);
    }
    
    /* check for a TOS device name */
    if (d = _dev_dosname(unx)) {
	strcpy(dos, unx);
	return _NM_DEV;
    }
    /* get current directory */
    path[0] = Dgetdrv() + 'A';
    path[1] = ':';
    (void)Dgetpath(path+2, 0);
    
    if (_tDEV && !strncmp(unx, "/dev/", 5)) {
	/* check for a unix device name */
	if (d = _dev_unxname(unx+5)) {
	    strcpy(dos, d->dosnm);
	    return _NM_DEV;
	}
	/* check for a path like /dev/A/somefile */
	else if (isalpha(unx[5]) && (((unx[6] == '/') || (unx[6] == '\\')) || (!unx[6]))) {
	    path[0] = unx[5];
	    path[2] = 0;
	    unx = (unx[6]) ? &unx[7] : &unx[6];
	}
    }
    
    change = _canon(path, (char *)unx, dos, 0);
    /*
     * If automatic symbolic links are on, then we should try to remap weird
     * filenames into new, unique ones.
     */
    if (change == _NM_CHANGE && _lOK && _lAUTO) {
	_uniquefy(dos);
    }
    return change;
}

/*
 * translate a DOS style filename to Unix. Note that this function does
 * *NOT* look up symbolic links; for that, call _full_dos2unx.
 * The return value may be given a useful purpose someday;
 * for now it's always 0.
 */

int
    _lib_dos2unx(dos, unx)
const char *dos;
char *unx;
{
    char c;
#ifdef DEBUG
    char *oldunx = unx;
    printf("dos2unx(%s)->", dos); fflush(stdout);
#endif
    while(c = *dos++) {
	if (_tSLASH && c == '\\')
	    *unx++ = '/';
	else if (_tDOTS && c == _tDOTS)
	    *unx++ = '.';
	else if (_tCASE)
	    *unx++ = tolower(c);
	else
	    *unx++ = c;
    }
    *unx++ = 0;
#ifdef DEBUG
    printf("(%s)\n", oldunx);
#endif
    return 0;
}

int
    _full_dos2unx(dos, unx)
char *dos, *unx;
{
    SYMDIR *dir;
    SYMENTRY *ent;
    char *n, *curdir, *lastslash, name[_LIB_NAME_MAX], tmp[_LIB_NAME_MAX];
    char slash;
    
    curdir = dos;
    lastslash = 0;
    slash = _tSLASH ? '/' : '\\';
    if (dos[0] && dos[1] == ':') {
	*unx++ = tolower(*dos); dos++;
	*unx++ = *dos++;
    }
    if (*dos == '\\') {
	*unx++ = slash; lastslash = dos; dos++;
    }
    else if (*dos) {	/* something is wrong -- punt */
	return _dos2unx(curdir, unx);
    }
    while (*dos) {
	n = tmp;
	while (*dos && *dos != '\\') {
	    *n++ = *dos++;
	}
	*n++ = 0;
	_dos2unx(tmp, name);
	*lastslash = 0;		/* tie off current directory */
	/* see if the DOS file has a Unix alias */
	dir = _read_symdir(curdir);
	ent = dir ? dir->s_dir : 0;
	while (ent) {
	    if ((ent->flags & SD_AUTO) && 
		!strcmp(ent->linkto, name)) {
		strcpy(name, ent->linkname);
		break;
	    }
	    ent = ent->next;
	}
	_free_symdir(dir);
	for (n = name; *n; n++)
	    *unx++ = *n;
	if (*dos)
	    *unx++ = slash;
	*lastslash = '\\';	/* restore path */
	lastslash = dos;
	if (*dos) dos++;
    }
    *unx++ = 0;
    return 0;
}

/*
 * the stuff below this line is for compatibility with the old unx2dos, and
 * will go away someday
 */

/* system default file name mapping function pointers */

static fnmapfunc_t ux2dos = _lib_unx2dos;
static fnmapfunc_t dos2ux = _lib_dos2unx;

/* set user specified filename mapping function
 *	NULL => use system default mapping function
 */
void fnmapfunc(u2dos, dos2u)
fnmapfunc_t u2dos, dos2u;
{
    ux2dos = (u2dos == NULL) ? _lib_unx2dos : u2dos;
    dos2ux = (dos2u == NULL) ? _lib_dos2unx : dos2u;
}

/* mapping functions -- call the mapping functions via pointers */

#ifdef __GNUC__
asm(".stabs \"_unx2dos\",5,0,0,__unx2dos"); /* dept of clean tricks */
#else
int __unx2dos(u, d)
const char *u;
char *d;
{
    return (*ux2dos)(u, d);
}
#endif

int _unx2dos(u, d)
const char *u;
char *d;
{
    return (*ux2dos)(u, d);
}

#ifdef __GNUC__
asm(".stabs \"_dos2unx\",5,0,0,__dos2unx"); /* dept of clean tricks */
#else
int __dos2unx(d, u)
const char *d;
char *u;
{
    return (*dos2ux)(d, u);
}
#endif

int _dos2unx(d, u)
const char *d;
char *u;
{
    return (*dos2ux)(d, u);
}
