/***************************************************************************

    fileio.c

    File access functions.

    Copyright Nicola Salmoria and the MAME Team.
    Visit http://mamedev.org for licensing and usage restrictions.

***************************************************************************/

#include "osdepend.h"
#include "corefile.h"
#include "astring.h"
#include "driver.h"
#include "chd.h"
#include "hash.h"
#include "unzip.h"
#include "options.h"


/***************************************************************************
    CONSTANTS
***************************************************************************/

#define PLAIN_FILE				0
#define ZIPPED_FILE				1

#define OPEN_FLAG_HAS_CRC		0x10000

#ifdef MAME_DEBUG
#define DEBUG_COOKIE			0xbaadf00d
#endif



/***************************************************************************
    TYPE DEFINITIONS
***************************************************************************/

/* typedef struct _mame_file mame_file -- declared in fileio.h */
struct _mame_file
{
#ifdef DEBUG_COOKIE
	UINT32			debug_cookie;					/* sanity checking for debugging */
#endif
	astring *		filename;						/* full filename */
	core_file *		file;							/* core file pointer */
	UINT32			openflags;						/* flags we used for the open */
	char			hash[HASH_BUF_SIZE];			/* hash data for the file */
	zip_file *		zipfile;						/* ZIP file pointer */
	UINT8 *			zipdata;						/* ZIP file data */
	UINT64			ziplength;						/* ZIP file length */
};


typedef struct _path_iterator path_iterator;
struct _path_iterator
{
	const char *	base;
	const char *	cur;
	int				index;
};


/* typedef struct _mame_path mame_path -- declared in fileio.h */
struct _mame_path
{
	path_iterator	iterator;
	osd_directory *	curdir;
	astring *		pathbuffer;
	int				buflen;
};



/***************************************************************************
    FUNCTION PROTOTYPES
***************************************************************************/

/* core functions */
static void fileio_exit(running_machine *machine);

/* file open/close */
static file_error fopen_internal(core_options *opts, const char *searchpath, const char *filename, UINT32 crc, UINT32 flags, mame_file **file);
static file_error fopen_attempt_zipped(astring *fullname, UINT32 crc, UINT32 openflags, mame_file *file);

/* path iteration */
static void path_iterator_init(path_iterator *iterator, core_options *opts, const char *searchpath);
static int path_iterator_get_next(path_iterator *iterator, astring *buffer);

/* misc helpers */
static file_error load_zipped_file(mame_file *file);
static int zip_filename_match(const zip_file_header *header, const astring *afilename);
static int zip_header_is_path(const zip_file_header *header);



/***************************************************************************
    CORE FUNCTIONS
***************************************************************************/

/*-------------------------------------------------
    fileio_init - initialize the internal file
    I/O system; note that the OS layer is free to
    call mame_fopen before fileio_init
-------------------------------------------------*/

void fileio_init(running_machine *machine)
{
	add_exit_callback(machine, fileio_exit);
}


/*-------------------------------------------------
    fileio_exit - clean up behind ourselves
-------------------------------------------------*/

static void fileio_exit(running_machine *machine)
{
	zip_file_cache_clear();
}



/***************************************************************************
    FILE OPEN/CLOSE
***************************************************************************/

/*-------------------------------------------------
    mame_fopen - open a file for access and
    return an error code
-------------------------------------------------*/

file_error mame_fopen(const char *searchpath, const char *filename, UINT32 openflags, mame_file **file)
{
	return fopen_internal(mame_options(), searchpath, filename, 0, openflags, file);
}


/*-------------------------------------------------
    mame_fopen_crc - open a file by name or CRC
    and return an error code
-------------------------------------------------*/

file_error mame_fopen_crc(const char *searchpath, const char *filename, UINT32 crc, UINT32 openflags, mame_file **file)
{
	return mame_fopen_crc_options(mame_options(), searchpath, filename, crc, openflags, file);
}


/*-------------------------------------------------
    mame_fopen_options - open a file for access and
    return an error code
-------------------------------------------------*/

file_error mame_fopen_options(core_options *opts, const char *searchpath, const char *filename, UINT32 openflags, mame_file **file)
{
	return fopen_internal(opts, searchpath, filename, 0, openflags, file);
}


/*-------------------------------------------------
    mame_fopen_crc_options - open a file by name
    or CRC and return an error code
-------------------------------------------------*/

file_error mame_fopen_crc_options(core_options *opts, const char *searchpath, const char *filename, UINT32 crc, UINT32 openflags, mame_file **file)
{
	return fopen_internal(opts, searchpath, filename, crc, openflags | OPEN_FLAG_HAS_CRC, file);
}


/*-------------------------------------------------
    mame_fopen_ram - open a "file" which is
    actually just an array of data in RAM
-------------------------------------------------*/

file_error mame_fopen_ram(const void *data, UINT32 length, UINT32 openflags, mame_file **file)
{
	file_error filerr;

	/* allocate the file itself */
	*file = (mame_file *)malloc(sizeof(**file));
	if (*file == NULL)
		return FILERR_OUT_OF_MEMORY;

	/* reset the file handle */
	memset(*file, 0, sizeof(**file));
	(*file)->openflags = openflags;
#ifdef DEBUG_COOKIE
	(*file)->debug_cookie = DEBUG_COOKIE;
#endif

	/* attempt to open the file directly */
	filerr = core_fopen_ram(data, length, openflags, &(*file)->file);
	if (filerr == FILERR_NONE)
		goto error;

	/* handle errors and return */
error:
	if (filerr != FILERR_NONE)
	{
		mame_fclose(*file);
		*file = NULL;
	}
	return filerr;
}


/*-------------------------------------------------
    fopen_internal - open a file
-------------------------------------------------*/

static file_error fopen_internal(core_options *opts, const char *searchpath, const char *filename, UINT32 crc, UINT32 openflags, mame_file **file)
{
	file_error filerr = FILERR_NOT_FOUND;
	path_iterator iterator;

	/* can either have a hash or open for write, but not both */
	if ((openflags & OPEN_FLAG_HAS_CRC) && (openflags & OPEN_FLAG_WRITE))
		return FILERR_INVALID_ACCESS;

	/* allocate the file itself */
	*file = (mame_file *)malloc(sizeof(**file));
	if (*file == NULL)
		return FILERR_OUT_OF_MEMORY;

	/* reset the file handle */
	memset(*file, 0, sizeof(**file));
	(*file)->openflags = openflags;
#ifdef DEBUG_COOKIE
	(*file)->debug_cookie = DEBUG_COOKIE;
#endif

	/* if the path is absolute, null out the search path */
	if (searchpath != NULL && osd_is_absolute_path(searchpath))
		searchpath = NULL;

	/* determine the maximum length of a composed filename, plus some extra space for .zip extensions */
	path_iterator_init(&iterator, opts, searchpath);

	/* loop over paths */
	(*file)->filename = astring_alloc();
	while (path_iterator_get_next(&iterator, (*file)->filename))
	{
		/* compute the full pathname */
		if (astring_len((*file)->filename) > 0)
			astring_catc((*file)->filename, PATH_SEPARATOR);
		astring_catc((*file)->filename, filename);

		/* attempt to open the file directly */
		filerr = core_fopen(astring_c((*file)->filename), openflags, &(*file)->file);
		if (filerr == FILERR_NONE)
			break;

		/* if we're opening for read-only we have other options */
		if ((openflags & (OPEN_FLAG_READ | OPEN_FLAG_WRITE)) == OPEN_FLAG_READ)
		{
			filerr = fopen_attempt_zipped((*file)->filename, crc, openflags, *file);
			if (filerr == FILERR_NONE)
				break;
		}
	}

	/* handle errors and return */
	if (filerr != FILERR_NONE)
	{
		mame_fclose(*file);
		*file = NULL;
	}
	return filerr;
}


/*-------------------------------------------------
    fopen_attempt_zipped - attempt to open a
    ZIPped file
-------------------------------------------------*/

static file_error fopen_attempt_zipped(astring *fullname, UINT32 crc, UINT32 openflags, mame_file *file)
{
	astring *filename = astring_alloc();
	zip_error ziperr;
	zip_file *zip;

	/* loop over directory parts up to the start of filename */
	while (1)
	{
		const zip_file_header *header;
		int dirsep;

		/* find the final path separator */
		dirsep = astring_rchr(fullname, 0, PATH_SEPARATOR[0]);
		if (dirsep == -1)
		{
			astring_free(filename);
			return FILERR_NOT_FOUND;
		}

		/* insert the part from the right of the separator into the head of the filename */
		if (astring_len(filename) > 0)
			astring_insc(filename, 0, "/");
		astring_inssubstr(filename, 0, fullname, dirsep + 1, -1);

		/* remove this part of the filename and append a .zip extension */
		astring_substr(fullname, 0, dirsep);
		astring_catc(fullname, ".zip");

		/* attempt to open the ZIP file */
		ziperr = zip_file_open(astring_c(fullname), &zip);

		/* chop the .zip back off the filename before continuing */
		astring_substr(fullname, 0, dirsep);

		/* if we failed to open this file, continue scanning */
		if (ziperr != ZIPERR_NONE)
			continue;

		/* see if we can find a file with the right name and (if available) crc */
		for (header = zip_file_first_file(zip); header != NULL; header = zip_file_next_file(zip))
			if (zip_filename_match(header, filename) && (!(openflags & OPEN_FLAG_HAS_CRC) || header->crc == crc))
				break;

		/* if that failed, look for a file with the right crc, but the wrong filename */
		if (header == NULL && (openflags & OPEN_FLAG_HAS_CRC))
			for (header = zip_file_first_file(zip); header != NULL; header = zip_file_next_file(zip))
				if (header->crc == crc && (!zip_header_is_path(header)))
					break;

		/* if that failed, look for a file with the right name; reporting a bad checksum */
		/* is more helpful and less confusing than reporting "rom not found" */
		if (header == NULL)
			for (header = zip_file_first_file(zip); header != NULL; header = zip_file_next_file(zip))
				if (zip_filename_match(header, filename))
					break;

		/* if we got it, read the data */
		if (header != NULL)
		{
			UINT8 crcs[4];

			file->zipfile = zip;
			file->ziplength = header->uncompressed_length;

			/* build a hash with just the CRC */
			hash_data_clear(file->hash);
			crcs[0] = header->crc >> 24;
			crcs[1] = header->crc >> 16;
			crcs[2] = header->crc >> 8;
			crcs[3] = header->crc >> 0;
			hash_data_insert_binary_checksum(file->hash, HASH_CRC, crcs);

			astring_free(filename);
			if (openflags & OPEN_FLAG_NO_PRELOAD)
				return FILERR_NONE;
			else
				return load_zipped_file(file);
		}

		/* close up the ZIP file and try the next level */
		zip_file_close(zip);
	}
}


/*-------------------------------------------------
    mame_fclose - closes a file
-------------------------------------------------*/

void mame_fclose(mame_file *file)
{
#ifdef DEBUG_COOKIE
	assert(file->debug_cookie == DEBUG_COOKIE);
	file->debug_cookie = 0;
#endif

	/* close files and free memory */
	if (file->zipfile != NULL)
		zip_file_close(file->zipfile);
	if (file->file != NULL)
		core_fclose(file->file);
	if (file->zipdata != NULL)
		free(file->zipdata);
	if (file->filename != NULL)
		astring_free(file->filename);
	free(file);
}


/*-------------------------------------------------
    mame_fcompress - enable/disable streaming file
    compression via zlib; level is 0 to disable
    compression, or up to 9 for max compression
-------------------------------------------------*/

file_error mame_fcompress(mame_file *file, int level)
{
	return core_fcompress(file->file, level);
}



/***************************************************************************
    FILE POSITIONING
***************************************************************************/

/*-------------------------------------------------
    mame_fseek - seek within a file
-------------------------------------------------*/

int mame_fseek(mame_file *file, INT64 offset, int whence)
{
	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return 1;
	}

	/* seek if we can */
	if (file->file != NULL)
		return core_fseek(file->file, offset, whence);

	return 1;
}


/*-------------------------------------------------
    mame_ftell - return the current file position
-------------------------------------------------*/

UINT64 mame_ftell(mame_file *file)
{
	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return 0;
	}

	/* tell if we can */
	if (file->file != NULL)
		return core_ftell(file->file);

	return 0;
}


/*-------------------------------------------------
    mame_feof - return 1 if we're at the end
    of file
-------------------------------------------------*/

int mame_feof(mame_file *file)
{
	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return 0;
	}

	/* return EOF if we can */
	if (file->file != NULL)
		return core_feof(file->file);

	return 0;
}


/*-------------------------------------------------
    mame_fsize - returns the size of a file
-------------------------------------------------*/

UINT64 mame_fsize(mame_file *file)
{
	/* use the ZIP length if present */
	if (file->zipfile != NULL)
		return file->ziplength;

	/* return length if we can */
	if (file->file != NULL)
		return core_fsize(file->file);

	return 0;
}



/***************************************************************************
    FILE READ
***************************************************************************/

/*-------------------------------------------------
    mame_fread - read from a file
-------------------------------------------------*/

UINT32 mame_fread(mame_file *file, void *buffer, UINT32 length)
{
	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return 0;
	}

	/* read the data if we can */
	if (file->file != NULL)
		return core_fread(file->file, buffer, length);

	return 0;
}


/*-------------------------------------------------
    mame_fgetc - read a character from a file
-------------------------------------------------*/

int mame_fgetc(mame_file *file)
{
	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return EOF;
	}

	/* read the data if we can */
	if (file->file != NULL)
		return core_fgetc(file->file);

	return EOF;
}


/*-------------------------------------------------
    mame_ungetc - put back a character read from
    a file
-------------------------------------------------*/

int mame_ungetc(int c, mame_file *file)
{
	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return 1;
	}

	/* read the data if we can */
	if (file->file != NULL)
		return core_ungetc(c, file->file);

	return 1;
}


/*-------------------------------------------------
    mame_fgets - read a line from a text file
-------------------------------------------------*/

char *mame_fgets(char *s, int n, mame_file *file)
{
	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return NULL;
	}

	/* read the data if we can */
	if (file->file != NULL)
		return core_fgets(s, n, file->file);

	return NULL;
}



/***************************************************************************
    FILE WRITE
***************************************************************************/

/*-------------------------------------------------
    mame_fwrite - write to a file
-------------------------------------------------*/

UINT32 mame_fwrite(mame_file *file, const void *buffer, UINT32 length)
{
	/* write the data if we can */
	if (file->file != NULL)
		return core_fwrite(file->file, buffer, length);

	return 0;
}


/*-------------------------------------------------
    mame_fputs - write a line to a text file
-------------------------------------------------*/

int mame_fputs(mame_file *file, const char *s)
{
	/* write the data if we can */
	if (file->file != NULL)
		return core_fputs(file->file, s);

	return 0;
}


/*-------------------------------------------------
    mame_vfprintf - vfprintf to a text file
-------------------------------------------------*/

static int mame_vfprintf(mame_file *file, const char *fmt, va_list va)
{
	/* write the data if we can */
	return (file->file != NULL) ? core_vfprintf(file->file, fmt, va) : 0;
}


/*-------------------------------------------------
    mame_fprintf - vfprintf to a text file
-------------------------------------------------*/

int CLIB_DECL mame_fprintf(mame_file *file, const char *fmt, ...)
{
	int rc;
	va_list va;
	va_start(va, fmt);
	rc = mame_vfprintf(file, fmt, va);
	va_end(va);
	return rc;
}



/***************************************************************************
    FILE ENUMERATION
***************************************************************************/

/*-------------------------------------------------
    mame_openpath - open a search path (multiple
    directories) for iteration
-------------------------------------------------*/

mame_path *mame_openpath(core_options *opts, const char *searchpath)
{
	mame_path *path;

	/* allocate a new mame_path */
	path = (mame_path *)malloc(sizeof(*path));
	if (path == NULL)
		return NULL;
	memset(path, 0, sizeof(*path));

	/* initialize the iterator */
	path_iterator_init(&path->iterator, opts, searchpath);

	/* allocate a buffer to hold the current path */
	path->pathbuffer = astring_alloc();
	return path;
}


/*-------------------------------------------------
    mame_readpath - return information about the
    next file in the search path
-------------------------------------------------*/

const osd_directory_entry *mame_readpath(mame_path *path)
{
	const osd_directory_entry *result;

	/* loop over potentially empty directories */
	while (1)
	{
		/* if no open directory, get the next path */
		while (path->curdir == NULL)
		{
			/* if we fail to get anything more, we're done */
			if (!path_iterator_get_next(&path->iterator, path->pathbuffer))
				return NULL;

			/* open the path */
			path->curdir = osd_opendir(astring_c(path->pathbuffer));
		}

		/* get the next entry from the current directory */
		result = osd_readdir(path->curdir);
		if (result != NULL)
			return result;

		/* we're done; close this directory */
		osd_closedir(path->curdir);
		path->curdir = NULL;
	}
}


/*-------------------------------------------------
    mame_closepath - close an open seach path
-------------------------------------------------*/

void mame_closepath(mame_path *path)
{
	/* close anything open */
	if (path->curdir != NULL)
		osd_closedir(path->curdir);

	/* free memory */
	if (path->pathbuffer != NULL)
		astring_free(path->pathbuffer);
	free(path);
}



/***************************************************************************
    MISCELLANEOUS
***************************************************************************/

/*-------------------------------------------------
    mame_core_file - return the core_file
    underneath the mame_file
-------------------------------------------------*/

core_file *mame_core_file(mame_file *file)
{
	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return NULL;
	}

	/* return the core file */
	return file->file;
}


/*-------------------------------------------------
    mame_file_full_name - return the full filename
    for a given mame_file
-------------------------------------------------*/

const char *mame_file_full_name(mame_file *file)
{
	/* return a pointer to the string if we have one; otherwise, return NULL */
	if (file->filename != NULL)
		return astring_c(file->filename);
	return NULL;
}


/*-------------------------------------------------
    mame_fhash - returns the hash for a file
-------------------------------------------------*/

const char *mame_fhash(mame_file *file, UINT32 functions)
{
	const UINT8 *filedata;
	UINT32 wehave;

	/* if we already have the functions we need, just return */
	wehave = hash_data_used_functions(file->hash);
	if ((wehave & functions) == functions)
		return file->hash;

	/* load the ZIP file now if we haven't yet */
	if (file->zipfile != NULL)
	{
		if (load_zipped_file(file) != FILERR_NONE)
			return file->hash;
	}
	if (file->file == NULL)
		return file->hash;

	/* read the data if we can */
	filedata = (const UINT8 *)core_fbuffer(file->file);
	if (filedata == NULL)
		return file->hash;

	/* compute the hash */
	hash_compute(file->hash, filedata, core_fsize(file->file), wehave | functions);
	return file->hash;
}



/***************************************************************************
    PATH ITERATION
***************************************************************************/

/*-------------------------------------------------
    path_iterator_init - prepare to iterate over
    a given path type
-------------------------------------------------*/

static void path_iterator_init(path_iterator *iterator, core_options *opts, const char *searchpath)
{
	/* reset the structure */
	memset(iterator, 0, sizeof(*iterator));
	iterator->base = (searchpath != NULL) ? options_get_string(opts, searchpath) : "";
	iterator->cur = iterator->base;
}


/*-------------------------------------------------
    path_iterator_get_next - get the next entry
    in a multipath sequence
-------------------------------------------------*/

static int path_iterator_get_next(path_iterator *iterator, astring *buffer)
{
	const char *semi;

	/* if none left, return FALSE to indicate we are done */
	if (iterator->index != 0 && *iterator->cur == 0)
		return FALSE;

	/* copy up to the next semicolon */
	semi = strchr(iterator->cur, ';');
	if (semi == NULL)
		semi = iterator->cur + strlen(iterator->cur);
	astring_cpych(buffer, iterator->cur, semi - iterator->cur);
	iterator->cur = (*semi == 0) ? semi : semi + 1;

	/* bump the index and return TRUE */
	iterator->index++;
	return TRUE;
}



/***************************************************************************
    MISC HELPERS
***************************************************************************/

/*-------------------------------------------------
    load_zipped_file - load a ZIPped file
-------------------------------------------------*/

static file_error load_zipped_file(mame_file *file)
{
	file_error filerr;
	zip_error ziperr;

	assert(file->file == NULL);
	assert(file->zipdata == NULL);
	assert(file->zipfile != NULL);

	/* allocate some memory */
	file->zipdata = (UINT8 *)malloc(file->ziplength);
	if (file->zipdata == NULL)
		return FILERR_OUT_OF_MEMORY;

	/* read the data into our buffer and return */
	ziperr = zip_file_decompress(file->zipfile, file->zipdata, file->ziplength);
	if (ziperr != ZIPERR_NONE)
	{
		free(file->zipdata);
		file->zipdata = NULL;
		return FILERR_FAILURE;
	}

	/* convert to RAM file */
	filerr = core_fopen_ram(file->zipdata, file->ziplength, file->openflags, &file->file);
	if (filerr != FILERR_NONE)
	{
		free(file->zipdata);
		file->zipdata = NULL;
		return FILERR_FAILURE;
	}

	/* close out the ZIP file */
	zip_file_close(file->zipfile);
	file->zipfile = NULL;
	return FILERR_NONE;
}


/*-------------------------------------------------
    zip_filename_match - compare zip filename
    to expected filename, ignoring any directory
-------------------------------------------------*/

static int zip_filename_match(const zip_file_header *header, const astring *filename)
{
	const char *zipfile = header->filename + header->filename_length - astring_len(filename);

	return (zipfile >= header->filename && astring_icmpc(filename, zipfile) == 0 &&
		(zipfile == header->filename || zipfile[-1] == '/'));
}

/*-------------------------------------------------
    zip_header_is_path - check whether filename
                         in header is a path
  -------------------------------------------------*/

static int zip_header_is_path(const zip_file_header *header)
{
	const char *zipfile = header->filename + header->filename_length - 1;

	return (zipfile >= header->filename && zipfile[0] == '/');
}
