/*
 * dviFile.c --
 *
 *      This file implements access routines for DVI files.
 *
 * Copyright © 1999 Anselm Lingnau <lingnau@tm.informatik.uni-frankfurt.de>
 * See file COPYING for conditions on use and distribution.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <limits.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#if HAVE_MMAP
#include <sys/types.h>
#include <sys/mman.h>
#endif /* HAVE_MMAP */

#include "dviInt.h"
#include "dviOps.h"

#ifndef lint
static char rcsid[] VAR_UNUSED = "$Id: dviFile.c,v 1.7 2000/06/29 10:59:07 lingnau Exp $";
#endif /* lint */

static Dvi_File *dviFileList = (Dvi_File *)0;
static Dvi_FileInfo *dviFileInfoList = (Dvi_FileInfo *)0;

static Dvi_FileInfo *OpenDviFile _ANSI_ARGS_((const char *name,
					      Dvi_ErrorProc *errorProc,
					      ClientData errorClientData));
static int CloseDviFile _ANSI_ARGS_((Dvi_FileInfo *dviFileInfoPtr,
				     int forceClose));

static U8 *FindPostamble _ANSI_ARGS_((Dvi_FileInfo *dviFile));
static U8 **MakePageTable _ANSI_ARGS_((Dvi_FileInfo *dviFile));

/*
 * ------------------------------------------------------------------------
 *
 * Dvi_OpenFile --
 *
 *      Given the name of a DVI file, return a pointer to a Dvi_File
 *      structure corresponding to it. The file is opened and mapped into
 *      memory if necessary, or a pointer to an existing Dvi_File structure
 *      is returned if the file has been loaded before.
 *
 * Side effects:
 *      The file named by name will be opened and mapped into memory,
 *      or read into a dynamically allocated array if the underlying
 *      operating system doesn't support memory-mapped files.
 *
 * ------------------------------------------------------------------------
 */

Dvi_File *
Dvi_FileOpen (name, reloadProc, reloadClientData, errorProc, errorClientData)
    const char *name;		/* String containing name of the DVI file */
    Dvi_ReloadProc *reloadProc;	/* Callback for reloads */
    ClientData reloadClientData;
    Dvi_ErrorProc *errorProc;	/* Callback for errors */
    ClientData errorClientData;
{
    Dvi_FileInfo *dviFileInfoPtr;	/* Temporary pointer to actual file info */
    Dvi_File *dviFile;		/* Temporary pointer to DVI file record */

    if ((dviFileInfoPtr = OpenDviFile(name, errorProc, errorClientData))
	== (Dvi_FileInfo *)0) {
	return (Dvi_File *)0;
    }
    dviFileInfoPtr->generation = 0;

    if ((dviFile = (Dvi_File *)ckalloc(sizeof(Dvi_File))) == (Dvi_File *)0) {
	CloseDviFile(dviFileInfoPtr, 0);
	(* errorProc)(errorClientData, DVI_NO_MEMORY);
	return (Dvi_File *)0;
    }

    dviFile->infoPtr = dviFileInfoPtr;

    dviFile->reloadProc = reloadProc;
    dviFile->reloadClientData = reloadClientData;

    dviFile->nextPtr = dviFileList;
    dviFileList = dviFile;

    return dviFile;
}

static Dvi_FileInfo *
OpenDviFile (name, errorProc, errorClientData)
    const char *name;
    Dvi_ErrorProc *errorProc;
    ClientData errorClientData;
{
    struct stat statBuf;	/* Used for getting the file size */
    caddr_t addr;		/* Return value of mmap() */
    Dvi_FileInfo *dviFileInfoPtr; /* Temporary pointer to new structure */
    int fileDesc;		/* O/S file descriptor for DVI file */

    /*
     * Open the file tentatively and find out its device and inode numbers
     * so we can later check whether it has already been opened earlier.
     */

    fileDesc = open(name, O_RDONLY);
    if (fileDesc < 0) {
	if (errorProc) {
	    (* errorProc)(errorClientData, strerror(errno));
	}
        return (Dvi_FileInfo *)0;
    }

    if (fstat(fileDesc, &statBuf) < 0) {
	if (errorProc) {
	    (* errorProc)(errorClientData, strerror(errno));
	}
        close(fileDesc);
        return (Dvi_FileInfo *)0;
    }

    /*
     * Check whether the file is a regular file or symbolic link. If not,
     * then refuse to deal with this file.
     */

    if (!(S_ISREG(statBuf.st_mode) || S_ISLNK(statBuf.st_mode))) {
	if (errorProc) {
	    (* errorProc)(errorClientData, "cannot handle this type of file");
	}
	close(fileDesc);
	return (Dvi_FileInfo *)0;
    }

    /*
     * Check whether file is already open. If so, increment reference
     * count and return pointer to the already-opened file.
     * ??? Close file here before returning ???
     */
    
    for (dviFileInfoPtr = dviFileInfoList;
	 dviFileInfoPtr != (Dvi_FileInfo *)0
	     && (statBuf.st_dev != dviFileInfoPtr->devNo
		 || (statBuf.st_dev == dviFileInfoPtr->devNo
		     && statBuf.st_ino != dviFileInfoPtr->inodeNo));
	 dviFileInfoPtr = dviFileInfoPtr->nextPtr)
	;
    if (dviFileInfoPtr != (Dvi_FileInfo *)0) {
	dviFileInfoPtr->refCount++;
	return dviFileInfoPtr;
    }
	
    /*
     * Allocate and fill in a Dvi_File structure for the file.
     */

    dviFileInfoPtr = (Dvi_FileInfo *)ckalloc(sizeof(Dvi_FileInfo));
    if (dviFileInfoPtr == 0) {
	if (errorProc) {
	    (* errorProc)(errorClientData, DVI_NO_MEMORY);
	}
	return (Dvi_FileInfo *)0;
    }

    dviFileInfoPtr->fileDesc = fileDesc;
    dviFileInfoPtr->fileSize = statBuf.st_size;
    dviFileInfoPtr->devNo = statBuf.st_dev;
    dviFileInfoPtr->inodeNo = statBuf.st_ino;
    dviFileInfoPtr->lastModTime = statBuf.st_mtime;

    /*
     * Map the file into memory if the O/S allows it; else read it
     * into a byte array to speed things up. The latter may not work
     * well with big DVI files on machines with little memory. Tough
     * luck, use xdvi.
     */

#if HAVE_MMAP
    if ((addr = mmap(0, (size_t)statBuf.st_size, PROT_READ, MAP_SHARED,
                     dviFileInfoPtr->fileDesc, (off_t)0)) == (caddr_t)-1) {
	if (errorProc) {
	    (* errorProc)(errorClientData, strerror(errno));
	}
        close(dviFileInfoPtr->fileDesc);
        ckfree((char *)dviFileInfoPtr);
        return (Dvi_FileInfo *)0;
    }
#else /* not HAVE_MMAP */
    if ((addr = ckalloc((size_t)statBuf.st_size)) == 0) {
	if (errorProc) {
	    (* errorProc)(errorClientData, DVI_NO_MEMORY);
	}
        close(dviFileInfoPtr->fileDesc);
        return (Dvi_FileInfo *)0;
    }
    if (read(dviFileInfoPtr->fileDesc, addr, (size_t)statBuf.st_size)
	    != statBuf.st_size) {
	if (errorProc) {
	    (* errorProc)(errorClientData, strerror(errno));
	}
        close(dviFileInfoPtr->fileDesc);
        ckfree(addr);
        ckfree(dviFileInfoPtr);
        return (Dvi_FileInfo *)0;
    }
    close(dviFileInfoPtr->fileDesc);
#endif /* not HAVE_MMAP */

    /*
     * Check whether the file is actually a DVI file, and obtain basic
     * parameters from the preamble.
     */

    dviFileInfoPtr->contents = (unsigned char *)addr;
    if (dviFileInfoPtr->contents[0] != D_PRE
	|| dviFileInfoPtr->contents[1] != D_ID) {
	if (errorProc) {
	    (* errorProc)(errorClientData, "not a DVI file (bad beginning)");
	}
#if HAVE_MMAP
        munmap((caddr_t)dviFileInfoPtr->contents, dviFileInfoPtr->fileSize);
        close(dviFileInfoPtr->fileDesc);
#else /* not HAVE_MMAP */
        ckfree(dviFileInfoPtr->contents);
#endif /* not HAVE_MMAP */
        ckfree((char *)dviFileInfoPtr);
	return (Dvi_FileInfo *)0;
    }

    dviFileInfoPtr->refCount = 1;
    dviFileInfoPtr->name = ckalloc(strlen(name)+1);
    if (dviFileInfoPtr->name == (char *)0) {
	if (errorProc) {
	    (* errorProc)(errorClientData, DVI_NO_MEMORY);
	}
#if HAVE_MMAP
        munmap((caddr_t)dviFileInfoPtr->contents, dviFileInfoPtr->fileSize);
        close(dviFileInfoPtr->fileDesc);
#else /* not HAVE_MMAP */
        ckfree((char *)dviFileInfoPtr->contents);
#endif /* not HAVE_MMAP */
        ckfree((char *)dviFileInfoPtr);
	return (Dvi_FileInfo *)0;
    }
    strcpy(dviFileInfoPtr->name, name);

    dviFileInfoPtr->postamble = FindPostamble(dviFileInfoPtr);
    dviFileInfoPtr->codePtr = (Dvi_Code *)0;

    dviFileInfoPtr->nextPtr = dviFileInfoList;
    dviFileInfoList = dviFileInfoPtr;

    return dviFileInfoPtr;
}

int
Dvi_FileReload (dviFile, errorProc, errorClientData)
    Dvi_File *dviFile;
    Dvi_ErrorProc *errorProc;
    ClientData errorClientData;
{
    Dvi_FileInfo *oldDviFileInfoPtr = dviFile->infoPtr;
    Dvi_FileInfo *newDviFileInfoPtr;
    Dvi_File *listPtr;
    unsigned int oldGeneration = oldDviFileInfoPtr->generation;
    char *name = ckalloc(strlen(oldDviFileInfoPtr->name)+1);

    if (name == (char *)0) {
	if (errorProc) {
	    (* errorProc)(errorClientData, DVI_NO_MEMORY);
	}
    }
    strcpy(name, oldDviFileInfoPtr->name);

    CloseDviFile(oldDviFileInfoPtr, 1);
    newDviFileInfoPtr = OpenDviFile(name, errorProc, errorClientData);
    ckfree(name);

    if (newDviFileInfoPtr == (Dvi_FileInfo *)0) {
	return 0;
    }

    newDviFileInfoPtr->generation = oldGeneration + 1;
    newDviFileInfoPtr->refCount = 0; /* will be reconstructed shortly */

    for (listPtr = dviFileList; listPtr; listPtr = listPtr->nextPtr) {
	if (listPtr->infoPtr == oldDviFileInfoPtr) {
	    listPtr->infoPtr = newDviFileInfoPtr;
	    newDviFileInfoPtr->refCount++;
	    if (listPtr->reloadProc) {
		(* listPtr->reloadProc)(listPtr->reloadClientData, listPtr);
	    }
	}
    }
    return 1;
}

/*
 * ------------------------------------------------------------------------
 *
 * Dvi_FileClose --
 *
 *      Closes a DVI file.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      The file described by `*dviFile' will no longer be used by the
 *      caller. If DviOpenFile has been invoked several times over for
 *      the same DVI file, a matching number of invocations to this
 *      function is necessary before the file is actually closed and
 *      its memory freed.
 *
 * ------------------------------------------------------------------------
 */

int
Dvi_FileClose (dviFile)
    Dvi_File *dviFile;		/* Pointer to Dvi_File structure to free */
{
    Dvi_File *listPtr;

    CloseDviFile(dviFile->infoPtr, 0);

    if (dviFile == dviFileList) {
	dviFileList = dviFile->nextPtr;
    } else {
	for (listPtr = dviFileList;
	     listPtr != (Dvi_File *)0;
	     listPtr = listPtr->nextPtr) {

	    if (listPtr->nextPtr == dviFile) {
		listPtr->nextPtr = listPtr->nextPtr->nextPtr;
	    }
	}
    }

    ckfree((char *)dviFile);
    return 0;
}

static int
CloseDviFile (dviFileInfoPtr, forceClose)
    Dvi_FileInfo *dviFileInfoPtr;
    int forceClose;
{ 
    Dvi_FileInfo *listPtr;
    Dvi_FileInfo *prevPtr;

    if (forceClose || --dviFileInfoPtr->refCount == 0) {
	if (dviFileInfoPtr->contents != 0) {
#if HAVE_MMAP
	    munmap((caddr_t)dviFileInfoPtr->contents,
		   dviFileInfoPtr->fileSize);
	    close(dviFileInfoPtr->fileDesc);
#else /* not HAVE_MMAP */
	    ckfree(dviFileInfoPtr->contents);
#endif /* not HAVE_MMAP */
	}
	ckfree(dviFileInfoPtr->name);

	if (dviFileInfoPtr->codePtr != 0) {
	    Dvi_CodeDestroy(dviFileInfoPtr->codePtr);
	}

	for (listPtr = dviFileInfoList, prevPtr = (Dvi_FileInfo *)0;
	     listPtr != (Dvi_FileInfo *)0 && listPtr != dviFileInfoPtr;
	     prevPtr = listPtr, listPtr = listPtr->nextPtr)
	    ;
	if (listPtr == (Dvi_FileInfo *)0) {
	    return 0;
	}
	if (prevPtr == (Dvi_FileInfo *)0) {
	    dviFileInfoList = listPtr->nextPtr;
	} else {
	    prevPtr->nextPtr = listPtr->nextPtr;
	}
	ckfree((char *)dviFileInfoPtr);
	return 0;
    }
    return dviFileInfoPtr->refCount;
}

/*
 * ------------------------------------------------------------------------
 *
 * Dvi_FileChanged --
 *
 *      Determine whether a DVI file was changed.
 *
 * Results:
 *      Returns 1 if the underlying file has been modified since it was
 *      read, 0 if it is still the same, <0 if there was an error.
 *
 * Side effects:
 *      None.
 *
 * ------------------------------------------------------------------------
 */

int
Dvi_FileChanged (dviFile)
    Dvi_File *dviFile;
{
    int result;
    struct stat statBuf;

    if ((result = stat(dviFile->infoPtr->name, &statBuf)) < 0) {
	return result;
    }

    return statBuf.st_mtime > dviFile->infoPtr->lastModTime;
}

/*
 * ------------------------------------------------------------------------
 *
 * FindPostamble --
 *
 *      Locate the postamble of a DVI file.
 *
 * Results:
 *      The return value is a pointer to the D_POST command at the
 *      beginning of the postamble or the null pointer if the file
 *      does not contain a postamble.
 *
 * Side effects:
 *      None.
 *
 * ------------------------------------------------------------------------
 */

static U8 *
FindPostamble (dviFileInfoPtr)
    Dvi_FileInfo *dviFileInfoPtr;
{
    U8 *startPtr = dviFileInfoPtr->contents;
    U8 *ptr = &startPtr[dviFileInfoPtr->fileSize - 1];
    S32 offset;

    /*
     * Work forward from the end of the file, skipping 223's.
     */

    while (ptr >= startPtr && *ptr == D_PAD) {
	--ptr;
    }

    /*
     * There must be at least 5 bytes in front of the current position,
     * and the current byte must be a 2 (D_ID). Before the D_ID, there
     * is the backpointer and then a D_POSTPOST byte. If these conditions
     * aren't true, then the file has no postamble.
     */

    if (ptr - startPtr < 5 || *ptr != D_ID || ptr[-5] != D_POSTPOST) {
	return (U8 *)0;
    }

    /*
     * Ensure that the backpointer has a sensible value (i.e., points
     * to a place between the start of the file and the current offset.
     * Follow the backpointer to the start of the postamble. If the
     * postamble doesn't start with D_POST, something's wrong.
     */

    if ((offset = DviGetS32(&ptr[-4])) < 0 || offset >= ptr - startPtr) {
	return (U8 *)0;
    }
    ptr = startPtr + offset;
    if (*ptr != D_POST) {
	return (U8 *)0;
    }
    return ptr;
}

int
Dvi_FileParameters (dviFileInfoPtr, postPtr, numPtr, denPtr, magPtr,
		    stackSizePtr, pageCountPtr)
    Dvi_FileInfo *dviFileInfoPtr;
    unsigned char **postPtr;
    long *numPtr;
    long *denPtr;
    long *magPtr;
    unsigned int *stackSizePtr;
    unsigned int *pageCountPtr;
{
    *numPtr = DviGetS32(&(dviFileInfoPtr->contents)[2]);
    *denPtr = DviGetS32(&(dviFileInfoPtr->contents)[6]);
    *magPtr = DviGetS32(&(dviFileInfoPtr->contents)[10]);

    if (dviFileInfoPtr->postamble != 0) {
	*postPtr = dviFileInfoPtr->postamble;
	*stackSizePtr = DviGetU16(*postPtr + 25);
	*pageCountPtr = DviGetU16(*postPtr + 27);
    } else {
	*stackSizePtr = *pageCountPtr = 0;
    }

    return 1;
}

void
Dvi_FileComment (dviFile, commentPtr, lengthPtr)
    Dvi_File *dviFile;
    char **commentPtr;
    unsigned int *lengthPtr;
{
    *lengthPtr = DviGetU8(&(dviFile->infoPtr->contents)[14]);
    *commentPtr = &(dviFile->infoPtr->contents)[15];
}

/*
 * ------------------------------------------------------------------------
 *
 * Dvi_PurgeFiles --
 *
 *     Purge all DVI files from memory
 *
 *     This procedure is used to `clear the slate' during the automatic
 *     test runs.
 *
 * ------------------------------------------------------------------------
 */

#ifdef DVI_DEBUG
int
Dvi_PurgeFiles ()
{
    while (dviFileList != 0) {
	Dvi_FileClose(dviFileList);
    }
    return TCL_OK;
}
#endif /* DVI_DEBUG */
