/*-
 * Copyright (c) 2006 Robert N. M. Watson
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * DOS compatibility routines.
 */

#include <sys/param.h>
#include <sys/mount.h>
#include <sys/stat.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "dos.h"
#include "emul.h"

int	_osmajor = 10;
int	_osminor = 0;

/*
 * DOS has a notion of a set of file system roots, each with an assigned
 * number.  Each file system has its own current working directory for the
 * process.  We maintain a set of mappings from drive numbers to root
 * directories and sub-directories.
 */
static FILE	*dos_logfile;		/* Where to send errors. */
static int	 dos_curdisk;		/* Current working disk. */
static int	 dos_diskcount;		/* How many disks? */

/*
 * If dve_unixroot is a zero-length string, the drive is not present.
 *
 * dve_unixroot is a UNIX path.
 *
 * dve_wd is a DOS path.
 */
struct disk_vector_entry {
	char	dve_unixpath[PATH_MAX];	/* Where in UNIX file system? */
	char	dve_wd[PATH_MAX];	/* Working directory on drive. */
};
static struct disk_vector_entry	dos_disk_vector[26];

/*
 * Set up the mapping between DOS and UNIX drives and paths, set initial
 * working drive and directory.
 */
void
dos_init(const char *logfile, const char *disk_vector[], const char *dir)
{
	int i;

	if (logfile != NULL) {
		dos_logfile = fopen(logfile, "w");
		if (dos_logfile == NULL)
			panic("dos_init: fopen('%s'): %s", logfile,
			    strerror(errno));
	}

	dos_curdisk = 0;

	if (disk_vector == NULL)
		return;

	for (i = 0; i < 26; i++) {
		if (disk_vector[i] == NULL)
			break;
		strlcpy(dos_disk_vector[i].dve_unixpath, disk_vector[i],
		    sizeof(dos_disk_vector[i].dve_unixpath));
		dos_diskcount++;
	}

	if (dos_chdir(dir) < 0)
		panic("dos_init_disk_vector: chdir('%s'): %s", dir,
		    strerror(errno));
}

static int
dos_disk_valid(int disk)
{

	if (disk < 0)
		return (0);
	if (disk >= dos_diskcount)
		return (0);
	if (strlen(dos_disk_vector[disk].dve_unixpath) == 0)
		return (0);
	return (1);
}

/*
 * Rewind one '..' on a DOS path, if possible.
 */
static void
dos_path_dotdot(char *path)
{
	char *cp;

	cp = strrchr(path, '\\');
	if (cp != NULL)
		*cp = '\0';
}

/*
 * One of the properties of DOS is that many filename variations can match a
 * particular filename.  This is because spaces at the end of the filename
 * will be ignored, and because the '.' in filenames without extensions can
 * be ignored.  This routine takes a segent and puts it into a canonical
 * form.
 */
static void
dos_canonicalize(char *filename)
{
	char name[PATH_MAX];
	char *p1, *p2;

	strlcpy(name, filename, sizeof(name));

	/*
	 * Split the string into parts before and after the extension.  If
	 * there is any additional extension, it is at this point lost.
	 */
	p2 = name;
	p1 = strsep(&p2, ".");

	/*
	 * Trim trailing white space in both parts.
	 */
	while (strlen(p1) > 0 && p1[strlen(p1) - 1] == ' ')
		p1[strlen(p1) - 1] = '\0';
	if (p2 != NULL) {
		while (strlen(p2) > 0 && p2[strlen(p2) - 1] == ' ')
			p2[strlen(p2) - 1] = '\0';
	}

	if (p2 == NULL)
		strcpy(filename, p1);
	else
		sprintf(filename, "%s.%s", p1, p2);
}

/*
 * Attempt to construct a UNIX path given a DOS path.  This is a bit tricky.
 * If we fail, return (-1, EINVAL), otherwise (0).
 *
 * The buffer passed in must be of at least size PATH_MAX.
 *
 * DOS path names are complicated.  Paths can include a drive identifier; if
 * not, they are with respect to the current drive.  They can also be
 * relative or absolute, and if relative, they are relative to the current
 * working directory of the drive in question.
 */
static int
dospath2unixpath(const char *dos_path, char *unix_path)
{
	char string[PATH_MAX], upath[PATH_MAX];
	int depth, drive, relative;
	char *cp, *sp;

	/*
	 * First: determine whether there is a drive identifier in there or
	 * not.  If so, validate.  If not, use the current disk drive.
	 */
	if (strlen(dos_path) >= 2 && dos_path[1] == ':') {
		/*
		 * Drive letters must be between 'a' and 'z'.
		 *
		 * XXXRW: validate drive?
		 */
		drive = tolower(dos_path[0]);
		if (drive < 'a' || drive > 'z') {
			errno = ENXIO;
			return (-1);
		}
		drive -= 'a';
		dos_path += 2;
	} else
		drive = dos_curdisk;

	/*
	 * There must be at most one ':' in the path, so if there are any at
	 * this point, we have a problem.  Disallow '/' as well.
	 */
	if (strchr(dos_path, ':') != NULL ||
	    strchr(dos_path, '/') != NULL) {
		errno = EINVAL;
		return (-1);
	}

	/*
	 * Determine if an absolute or relative path.
	 */
	if (dos_path[0] == '\\') {
		relative = 0;
		dos_path++;
	} else
		relative = 1;

	/*
	 * First, copy the UNIX root for the drive into the writable buffer.
	 */
	snprintf(unix_path, PATH_MAX, "%s/",
	    dos_disk_vector[drive].dve_unixpath);
	// printf("drive path: %s\n", unix_path);

	/*
	 * Now, we start iterating down the path -- first the relative, if
	 * present, and then into the user path.  We do it segment by
	 * segment, lower-casing as we go, and checking for excess '..'s.
	 * Processing '..'s is a pain; we track current depth using 'depth'.
	 *
	 * XXXRW: We don't handle buffer overflows here properly.
	 */
	depth = 0;
	strcpy(upath, "");
	if (relative) {
		strlcpy(string, dos_disk_vector[drive].dve_wd, PATH_MAX);
		sp = string;
		while ((cp = strsep(&sp, "\\")) != NULL) {
			if (strcmp(cp, "..") == 0)
				dos_path_dotdot(upath);
			else {
				strcat(upath, cp);
				strcat(upath, "/");
			}
		}
	}
	// printf("path after relative: '%s' + '%s'\n", upath, string);

	/*
	 * Now the user bit of the path.  A tricky part of this is that all
	 * DOS files have extensions, even if they are empty, and may also
	 * have spaces.  When copying, we remove the trailing spaces before
	 * the 'dot', and if there are no non-space characters after the dot,
	 * we also remove the dot.
	 *
	 * XXXRW: Buffer overflows.
	 */
	strcpy(string, dos_path);

	sp = string;
	while ((cp = strsep(&sp, "\\")) != NULL) {
		if (strcmp(cp, "..") == 0)
			dos_path_dotdot(upath);
		else {
			dos_canonicalize(cp);
			strcat(upath, cp);
			if (sp != NULL)
				strcat(upath, "/");
		}
	}
	// printf("after user path: '%s'\n", upath);

	/*
	 * Finally, reduce some DOS-isms.
	 */
	for (cp = upath; *cp != '\0'; cp++) {
		if (*cp == '\\')
			*cp = '/';
		*cp = tolower(*cp);
	}
	strcat(unix_path, upath);
	// printf("d2u: %s -> %s\n", dos_path, unix_path);
	return (0);
}

int
filelength(int fd)
{
	struct stat sb;
	int error;

	error = fstat(fd, &sb);
	if (error)
		panic("filelength: fstat: %s", strerror(errno));
	return (sb.st_size);
}

int
_chmod(const char *filename, int pmode, int fauxarg)
{
	char path[PATH_MAX];
	mode_t mode;

	mode = 0;
	if ((pmode | _S_IWRITE | _S_IREAD) != pmode &&
	    pmode != 1)
		panic("_chmod: invalid mode %o", pmode);

	if ((pmode & _S_IWRITE) || (mode == 1))
		mode |= 0400;
	if (pmode & _S_IREAD)
		mode |= 0200;

	if (dospath2unixpath(filename, path) == -1)
		return (-1);
	return (chmod(path, mode));
}

void
getdate(struct date *date)
{
	struct timeval tv;
	struct tm tm;
	time_t t;

	if (gettimeofday(&tv, NULL) < 0)
		panic("getdate: gettimeofday: %s", strerror(errno));

	t = tv.tv_sec;
	localtime_r(&t, &tm);

	date->da_year = tm.tm_year + 1900;
	date->da_day = tm.tm_mday;
	date->da_mon = tm.tm_mon;
}

void
gettime(struct time *time)
{
	struct timeval tv;
	struct tm tm;
	time_t t;

	if (gettimeofday(&tv, NULL) < 0)
		panic("gettime: gettimeofday: %s", strerror(errno));

	t = tv.tv_sec;
	localtime_r(&t, &tm);

	time->ti_hour = tm.tm_hour;
	time->ti_min = tm.tm_min;
	time->ti_sec = tm.tm_sec;
	time->ti_hund = tv.tv_usec / 10000;
}

int
getftime(int fd, struct ftime *ftime)
{

	panic("getftime: %s", strerror(ENOSYS));
}

int
setftime(int fd, struct ftime *ftime)
{

	panic("setftime: %s", strerror(ENOSYS));
}

/*
 * fnsplit() appears to be quite poorly specified, so we take a hack at it
 * and reflect on its inadequacy.
 */
int
fnsplit(const char *path, char *drive, char *dir, char *name, char *ext)
{
	char *bufp, *cp, *dirp, *extp, *filep;
	char buffer[MAXPATH];
	int flags;

	flags = 0;

	if (drive != NULL)
		drive[0] = '\0';

	if (dir != NULL)
		dir[0] = '\0';

	if (name != NULL)
		name[0] = '\0';

	if (ext != NULL)
		ext[0] = '\0';

	/*
	 * Make a local writable copy.
	 */
	strlcpy(buffer, path, MAXPATH);
	bufp = buffer;

	/*
	 * Drive letter?
	 */
	if (strlen(bufp) > 2 && bufp[1] == ':') {
		if (drive != NULL) {
			sprintf(drive, "%c:", toupper(bufp[1]));
			flags |= DRIVE;
		}
		bufp += 2;
	}

	/*
	 * Now see if we can find a trailing filename by looking for the last
	 * bit of the path.
	 */
	cp = strrchr(bufp, '\\');
	if (cp == NULL) {
		/*
		 * Just a filename.
		 */
		filep = bufp;
		dirp = NULL;
	} else {
		/*
		 * A path and a filename.
		 */
		*cp = '\0';
		cp++;
		filep = cp;
		dirp = bufp;
	}

	if (dirp != NULL) {
		flags |= DIRECTORY;
		strcpy(dir, dirp);
	}

	/*
	 * See if we can identify a file extension.  There are two special
	 * cases for filenames with dots in them: "." and "..".  Also handle
	 * the extension-free case in the same way.
	 */
	if ((strcmp(filep, ".") == 0) || (strcmp(filep, "..") == 0) ||
	    (strchr(filep, '.') == NULL)) {
		flags |= FILENAME;
		strcpy(name, filep);
	} else {
		extp = filep;
		strsep(&extp, ".");
		flags |= EXTENSION;
		flags |= FILENAME;
		strcpy(name, filep);
		strcpy(ext, ".");
		strcat(ext, extp);
	}

#if 0
	printf("fnsplit(%s) to ", path);
	if (flags & DRIVE)
		printf("drive %s ", drive);
	if (flags & DIRECTORY)
		printf("directory %s ", dir);
	if (flags & FILENAME)
		printf("filename %s ", name);
	if (flags & EXTENSION)
		printf("extension %s ", ext);
	printf("flags 0x%x\n", flags);
#endif
	return (flags);
}

/*
 * Change the DOS notion of current working directory, and also possibly
 * current drive.
 *
 * XXXRW: What happens if one operation is valid and the other isn't?
 */
int
dos_chdir(const char *filename)
{
	char upath[PATH_MAX];
	struct stat sb;
	int drive;

	// printf("dos_chdir('%s')\n", filename);

	/*
	 * First, convert to a UNIX directory, and make sure it exists/etc.
	 */
	if (dospath2unixpath(filename, upath) < 0)
		return (-1);
	if (stat(upath, &sb) < 0)
		return (-1);
	if (!(S_ISDIR(sb.st_mode))) {
		errno = ENOTDIR;
		return (-1);
	}

	/*
	 * Next, re-parse and set up the working drive and directory.
	 */
	if (strlen(filename) >= 2 && filename[1] == ':') {
		drive = tolower(filename[0]) - 'a';
		if (!(dos_disk_valid(drive))) {
			errno = ENXIO;
			return (-1);
		}
		filename += 2;
	}
	dos_curdisk = drive;

	/*
	 * Remove leading and trailing '\\'s from the path so things are
	 * cleaner later.
	 */
	while (filename[0] == '\\')
		filename++;
	strlcpy(dos_disk_vector[drive].dve_wd, filename,
	    sizeof(dos_disk_vector[drive].dve_wd));
	while (dos_disk_vector[drive].dve_wd[
	    strlen(dos_disk_vector[drive].dve_wd) - 1] == '\\')
		dos_disk_vector[drive].dve_wd[
		    strlen(dos_disk_vector[drive].dve_wd) - 1] = '\0';
	return (0);
}

int
dos_mkdir(const char *filename)
{
	char upath[PATH_MAX];

	if (dospath2unixpath(filename, upath) < 0)
		return (-1);
	return (mkdir(upath, S_IRWXU));
}

int
dos_open(const char *path, int flags, ...)
{
	char upath[PATH_MAX];
	int fd, mode;
	va_list ap;

	va_start(ap, flags);
	if (flags & O_CREAT)
		mode = va_arg(ap, int);
	else
		mode = 0;
	va_end(ap);

	if (dospath2unixpath(path, upath) < 0)
		return (-1);

	/* XXXRW: Be conservative on locking. */
	flags &= 0x00ffffff;
	flags |= O_EXLOCK;

	fd = open(upath, flags, mode);

	if (fd < 0 && dos_logfile != NULL) {
		fprintf(dos_logfile, "dos_open: open(%s, %x, %o): %s\n",
		    upath, flags, mode, strerror(errno));
		fflush(dos_logfile);
	}

	return (fd);
}

int
findfirst(const char *pathname, struct ffblk *ffblk, int attrib)
{
	char path[PATH_MAX];
	char *cp;
	glob_t g;

	if (dospath2unixpath(pathname, path) < 0)
		return (-1);

	bzero(ffblk, sizeof(*ffblk));
	strlcpy(ffblk->_ff_pattern, path, PATH_MAX);
	if (glob(path, 0, NULL, &g) < 0)
		return (-1);
	if (g.gl_matchc == 0)
		return (-1);
	ffblk->_ff_next = 1;
	strlcpy(ffblk->ff_name, g.gl_pathv[0], PATH_MAX);
	for (cp = ffblk->ff_name; *cp != '\0'; cp++)
		*cp = toupper(*cp);
	globfree(&g);
	return (0);
}

int
findnext(struct ffblk *ffblk)
{
	char *cp;
	glob_t g;

	if (glob(ffblk->_ff_pattern, 0, NULL, &g) < 0)
		return (-1);
	if (g.gl_matchc == 0)
		return (-1);
	if (g.gl_matchc <= ffblk->_ff_next)
		return (-1);
	strlcpy(ffblk->ff_name, g.gl_pathv[ffblk->_ff_next], PATH_MAX);
	for (cp = ffblk->ff_name; *cp != '\0'; cp++)
		*cp = toupper(*cp);
	ffblk->_ff_next++;
	globfree(&g);
	return (0);
}

#ifndef timespecadd
#define timespecadd(vvp, uvp)						\
	do {								\
		(vvp)->tv_sec += (uvp)->tv_sec;				\
		(vvp)->tv_nsec += (uvp)->tv_nsec;			\
		if ((vvp)->tv_nsec >= 1000000000) {			\
			(vvp)->tv_sec++;				\
			(vvp)->tv_nsec -= 1000000000;			\
		}							\
	} while (0)
#endif
void
delay(int millisec)
{
	struct timespec total, left;

	total.tv_sec = millisec / 1000;
	total.tv_nsec = (millisec % 1000) * 1000000;
	left.tv_sec = 0;
	left.tv_nsec = 0;

	while (nanosleep(&total, &left) != 0) {
		if (errno != EINTR)
			panic("nanosleep: %s", strerror(errno));
		total = left;
	}
}

void
unixtodos(time_t time, struct date *d, struct time *t)
{

	panic("unixtodos: %s", strerror(ENOSYS));
}

time_t
dostounix(struct date *d, struct dostime *t)
{

	panic("dostounix: %s", strerror(ENOSYS));
}

/*
 * XXXRW: For now, equate blocks, sectors, and clusters.
 *
 * XXXRW: Make no attempt to handle wraparound.  This is a bug.
 *
 * Return a zero'd buffer if the drive isn't present.
 */
void
getdfree(unsigned char drive, struct dfree *ptr)
{
	struct statfs sf;

	bzero(ptr, sizeof(*ptr));
	if (!(dos_disk_valid(drive)))
		return;

	if (statfs(dos_disk_vector[drive].dve_unixpath, &sf) < 0)
		panic("statfs: %s", strerror(errno));

	ptr->df_avail = sf.f_bavail;
	ptr->df_total = sf.f_blocks;
	ptr->df_bsec = sf.f_bsize;
	ptr->df_sclus = 1;
}

int
getcurdir(int drive, char *direc)
{

	if (!(dos_disk_valid(drive))) {
		errno = ENXIO;
		return (-1);
	}

	strcpy(direc, dos_disk_vector[drive].dve_wd);
	return (0);
}

/*
 * There appears to be no defined failure mode for this function.  We don't
 * allow dos_curdisk to be >= dos_diskcount, so we won't change the drive if
 * an invalid one is requested.  Also, if no disks are defined, return 0.
 */
int
setdisk(int drive)
{

	if (dos_disk_valid(drive))
		dos_curdisk = drive;
	return (dos_diskcount);
}

int
chsize(int fildes, long size)
{

	return (ftruncate(fildes, size));
}

int
getdisk(void)
{

	return (dos_curdisk);
}

/*
 * Search current directory and path for a file.  Return the full file path
 * in a static buffer, or NULL.
 */
char *
searchpath(const char *file)
{
	char path[PATH_MAX], checkname[PATH_MAX], *pathp, *curdir;
	static char matchpath[PATH_MAX];

	if (access(file, F_OK) == 0) {
		strlcpy(matchpath, file, sizeof(matchpath));
		return (matchpath);
	}

	pathp = getenv("PATH");
	if (path == NULL)
		return (NULL);

	strlcpy(path, pathp, sizeof(path));
	pathp = path;
	while ((curdir = strsep(&pathp, ":")) != NULL) {
		if (snprintf(checkname, sizeof(checkname), "%s/%s", curdir,
		    file) >= sizeof(checkname))
			continue;
		if (access(checkname, F_OK) == 0) {
			strlcpy(matchpath, checkname, sizeof(matchpath));
			return (matchpath);
		}
	}
	return (NULL);
}

