#include <windows.h>

#define ZLIB_DLL
#include "zlib/zlib.h"

#include <stdio.h>
#include <stdarg.h>

#include "archive.h"

/* Convert unix-path to dos-path */
static void fixpath (char *path)
{
    while (path && *path) {
	if (*path == '/')
	    *path = '\\';
	++path;
    }
}

BOOL ensure_directory (char *pathname, char *new_part, NOTIFYPROC notify)
{
    while (new_part && *new_part && (new_part = strchr (new_part, '\\'))) {
	DWORD attr;
	*new_part = '\0';
	attr = GetFileAttributes (pathname);
	if (attr == -1) {
	    /* nothing found */
	    if (!CreateDirectory (pathname, NULL) && notify)
		notify (SYSTEM_ERROR, "CreateDirectory (%s)", pathname);
	}
	if (attr & FILE_ATTRIBUTE_DIRECTORY) {
	    ;
	} else {
	    SetLastError (183);
	    if (notify)
		notify (SYSTEM_ERROR, "CreateDirectory (%s)", pathname);
	}
	*new_part = '\\';
	++new_part;
    }
    return TRUE;
}

/* XXX Should better explicitely specify
 * uncomp_size and file_times instead of pfhdr!
 */
char *map_new_file (DWORD flags, char *filename,
		    char *pathname_part, int size,
		    WORD wFatDate, WORD wFatTime,
		    NOTIFYPROC notify)
{
    HANDLE hFile, hFileMapping;
    char *dst;
    FILETIME ft;

  try_again:
    if (!flags)
	flags = CREATE_NEW;
    hFile = CreateFile (filename,
			GENERIC_WRITE | GENERIC_READ,
			0, NULL,
			flags,
			FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
	DWORD x = GetLastError();
	switch (x) {
	case ERROR_FILE_EXISTS:
	    if (notify && notify (CAN_OVERWRITE, filename))
	       	hFile = CreateFile (filename,
				    GENERIC_WRITE | GENERIC_READ,
				    0, NULL,
				    CREATE_ALWAYS,
				    FILE_ATTRIBUTE_NORMAL, NULL);
	    else {
		if (notify)
		    notify (FILE_OVERWRITTEN, filename);
		return NULL;
	    }
	    break;
	case ERROR_PATH_NOT_FOUND:
	    if (ensure_directory (filename, pathname_part, notify))
		goto try_again;
	    else
		return FALSE;
	    break;
	default:
	    SetLastError (x);
	    break;
	}
    }
    if (hFile == INVALID_HANDLE_VALUE) {
	if (notify)
	    notify (SYSTEM_ERROR, "CreateFile (%s)", filename);
	return NULL;
    }

    if (notify)
	notify (FILE_CREATED, filename);

    DosDateTimeToFileTime (wFatDate, wFatTime, &ft);
    SetFileTime (hFile, &ft, &ft, &ft);


    if (size == 0) {
	/* We cannot map a zero-length file (Also it makes
	   no sense */
	CloseHandle (hFile);
	return NULL;
    }

    hFileMapping = CreateFileMapping (hFile,
				      NULL, PAGE_READWRITE, 0, size, NULL);

    CloseHandle (hFile);

    if (hFileMapping == INVALID_HANDLE_VALUE) {
	if (notify)
	    notify (SYSTEM_ERROR, "CreateFileMapping (%s)", filename);
	return NULL;
    }

    dst = MapViewOfFile (hFileMapping,
			 FILE_MAP_WRITE, 0, 0, 0);

    CloseHandle (hFileMapping);

    if (!dst) {
	if (notify)
	    notify (SYSTEM_ERROR, "MapViewOfFile (%s)", filename);
	return NULL;
    }
    return dst;
}


BOOL
extract_file (char *dst, char *src, int method, int comp_size,
	      int uncomp_size, NOTIFYPROC notify)
{
    z_stream zstream;
    int result;

    if (method == Z_DEFLATED) {
	int x;
        memset (&zstream, 0, sizeof (zstream));
        zstream.next_in = src;
        zstream.avail_in = comp_size+1;
	zstream.next_out = dst;
        zstream.avail_out = uncomp_size;

/* Apparently an undocumented feature of zlib: Set windowsize
 to negative values to supress the gzip header and be compatible with
 zip! */
	result = TRUE;
        if (Z_OK != (x = inflateInit2(&zstream, -15))) {
	    if (notify)
		notify (ZLIB_ERROR, "inflateInit2 returns %d", x);
	    result = FALSE;
	    goto cleanup;
	}
	if (Z_STREAM_END != (x = inflate(&zstream, Z_FINISH))) {
	    if (notify)
		notify (ZLIB_ERROR, "inflate returns %d", x);
	    result = FALSE;
	}
      cleanup:
	if (Z_OK != (x = inflateEnd(&zstream))) {
	    if (notify)
		notify (ZLIB_ERROR, "inflateEnd returns %d", x);
	    result = FALSE;
	}
    } else if (method == 0) {
	memcpy(dst, src, uncomp_size);
	result = TRUE;
    } else
	result = FALSE;
    UnmapViewOfFile(dst);
    return result;
}

/* Open a zip-compatible archive and extract all files
 * into the specified directory (which is assumed to exist)
 */
BOOL
unzip_archive (char *dirname, char *data, DWORD size, NOTIFYPROC notify)
{
    int n;
    char pathname[MAX_PATH];
    char *new_part;

    /* read the end of central directory record */
    struct eof_cdir *pe = (struct eof_cdir *)&data[size - sizeof
						  (struct eof_cdir)];

    int arc_start = size - sizeof (struct eof_cdir) - pe->nBytesCDir -
	pe->ofsCDir;

    /* set position to start of central directory */
    int pos = arc_start + pe->ofsCDir;

    /* make sure this is a zip file */
    if (pe->tag != 0x06054b50)
	return FALSE;
    
    /* Loop through the central directory, reading all entries */
    for (n = 0; n < pe->nTotalCDir; ++n) {
	char *fname;
	char *pcomp;
	char *dst;
	struct cdir *pcdir = (struct cdir *)&data[pos];
	struct fhdr *pfhdr = (struct fhdr *)&data[pcdir->ofs_local_header +
						 arc_start];

        if (pcdir->tag != 0x02014b50)
	    return FALSE;
	if (pfhdr->tag != 0x04034b50)
	    return FALSE;
	pos += sizeof (struct cdir);
	fname = (char *)&data[pos]; /* This is not null terminated! */
	pos += pcdir->fname_length + pcdir->extra_length +
	    pcdir->comment_length;

	pcomp = &data[pcdir->ofs_local_header
		     + sizeof (struct fhdr)
		     + arc_start
		     + pfhdr->fname_length
		     + pfhdr->extra_length];

	strcpy (pathname, dirname);
	strcat (pathname, "\\");
	new_part = &pathname[lstrlen (pathname)];
	strncat (pathname, fname, pfhdr->fname_length);
	fixpath (pathname);
	if (pathname[strlen(pathname)-1] != '\\') {
	    /*
	     * The local file header (pfhdr) does not always contain
	     * the compressed and uncompressed sizes of the data
	     * depending on bit 3 of the flags field.
	     * So it seems better to use the data from the
	     * central directory (pcdir).
	     */
	    dst = map_new_file (0, pathname, new_part,
				pcdir->uncomp_size,
				pcdir->last_mod_file_date,
				pcdir->last_mod_file_time, notify);
	    if (dst) {
		if (!extract_file (dst, pcomp, pfhdr->method,
				   pcdir->comp_size, pcdir->uncomp_size,
				   notify))
		    return FALSE;
	    } /* else ??? */
	}
	if (notify)
	    notify (NUM_FILES, new_part, (int)pe->nTotalCDir,
		    (int)n+1);
    }
    return TRUE;
}
