/*
 * Jeffrey Friedl
 * Omron Corporation			オムロン（株）
 * Nagaokakyoshi, Japan			〒617長岡京下海印寺
 *
 * jfriedl@nff.ncl.omron.co.jp
 *
 * This work is placed under the terms of the GNU General Purpose License
 * (the "GNU Copyleft").
 *
 * July 1996
 *
 * Routine to access a file with virtualmemory-like access.
 */
#include "config.h"
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
#include "output.h"
#include "xmalloc.h"
#include "strsave.h"
#include "virtfile.h"

#define PAGE_VALID(p)  ((p)->text)

/*
 * Given a filename, return the length of the file.
 */
long int filesize(const char *filename)
{
    struct stat statbuf;
    if (stat(filename, &statbuf) < 0)
	return -1L;
    else
	return statbuf.st_size;
}

/*
 * PAGE_SIZE -- size of pages loaded. The page size limits the
 * length of the longest line.
 */
static unsigned PAGE_SIZE = 0x2000;

/*
 * Number of pages to keep in memory at once, for all files.
 */
static unsigned PAGE_COUNT = 40;

/*
 * pointer to list of pages....
 */
static Page *common = 0;
static unsigned default_blast = 0;

/*
 * Init routine -- fills 'common'. Can change PAGE_COUNT beforehand....
 */
static void init_common(void)
{
    int i;
    if (common)
	return;

    common = xmalloc(sizeof(Page) * PAGE_COUNT);
    bzero(common, sizeof(Page) * PAGE_COUNT);
}


VirtFile *
OpenVertFile(const char *filename)
{
    VirtFile *v;
    int fd = open(filename, 0);

    if (fd < 0)
	return NULL;

    if (!common)
	init_common();

    v = xmalloc(sizeof *v);
    v->fd = fd;
    v->filename = strsave(filename);
    v->length = filesize(filename);

    return v;
}

static Page *
LoadPage(VirtFile *v, fileloc start)
{
     int i;
     Page *empty = 0, *p = 0;

     /* See if a page holding the location is already available */
     for (i = 0; i < PAGE_COUNT; i++)
     {
	 if (!empty && !PAGE_VALID(&common[i]))
	     empty = &common[i];
	 if (common[i].owner == v && common[i].start < start) {
	     p = &common[i];
	     break;
	 }
     }

     if (!p)
     {
	 /* if not, either pick an empty one, or blast a currently-used one */
	 if (empty)
	     p = empty;
	 else {
	     p = &common[default_blast];
	     default_blast = (default_blast + 1) % PAGE_COUNT;
	 }
     }

     /*
      * If page has no text yet, grab some memory */
     if (!PAGE_VALID(p))
	 p->text = xmalloc(PAGE_SIZE);

     /* go to appropriate place in file and read data */
     if (lseek(v->fd, start, SEEK_SET) < 0)
	 die("bad lseek to %ld of %s: %n\n", (long)start);
     if (i = read(v->fd, p->text, PAGE_SIZE), i < 0)
	 die("bad read of %ld bytes starting at %ld of %s: %n\n",
	     (long)PAGE_SIZE, (long)start, v->filename);

     /* fill in other page stuff */
     p->start = start;
     p->end   = start + i;
     p->owner = v;
     return p;
}

/*
 * Given a file an a location, return a page that holds the location
 * somewhere.
 */
static Page *
EnsurePositionInMemory(VirtFile *v, fileloc pos)
{
    Page *p = 0;
    int i;

    /* see if page is already there */
    for (i = 0; i < PAGE_COUNT; i++)
    {
	/* must be valid and owned by me */
	if (!PAGE_VALID(&common[i]) || (common[i].owner != v))
	    continue;
	/* must have a position in range [start .. end) */
	if (pos < common[i].start || pos > common[i].end)
	    continue;

	/* if we don't have a pointer yet, or if this page has the
	   desired location earlier in the page than the one we already
	   found, use this new page instead */
	if (!p || (pos - p->start) > (pos - common[i].start))
	    p = &common[i];
    }

    /* if it's not already here, get a new page */
    if (p)
	return p;
    else
	return LoadPage(v, pos);
}

/*
 * Given a page and an index into the file (which the given page MUST
 * represent), return the length of the line.
 */
struct ScanInfo {
    const unsigned char *str_start;  /* pointer in memory where line starts */
    unsigned short length;           /* length of line */
    char no_end;                     /* true if line extends after page */
};

static __inline__ struct ScanInfo *
ScanStrInPage(Page *p, fileloc start)
{
    static struct ScanInfo result;

    /* PTR is string we'll scan, END is end of the page */
    const unsigned char *ptr = &p->text[start - p->start];
    const unsigned char *end = &p->text[(int)(p->end - p->start)];

    result.str_start = ptr;

    /* scan string looking for newline */
    while ((ptr < end) && (*ptr != '\n'))
	ptr++;
    result.length = ptr - result.str_start;

    /*
     * Past end of page, and... page is full size (i.e. not last page of file)
     */
    result.no_end = (ptr >= end && (p->end - p->start >= PAGE_SIZE));

    return &result;
}

/*
 * Given a file and a location, return a pointer to memory holding the
 * string starting at (or spanning) the given location.
 * Fill *pCount with the length of the string if not NULL.
 */
const unsigned char *
VirtPos2Str(VirtFile *v, fileloc start, unsigned *pCount)
{
    struct ScanInfo *info;

    /* some small sanity check */
    if (start < 0 || start > v->length)
	die("oops: %ld vs. length=%ld.\n", start, v->length);

    /* Get the page with the string, and get the string */
    info = ScanStrInPage(EnsurePositionInMemory(v, start), start);

    /*
     * If we hit the end of the page (and the page is a full page,
     * and not the last one in the file where a newline might not end
     * things), we need to re-load the page such that the target line
     * is at the beginning of the page. This will ensure the whole line
     * is in memory. Of course, if the line is longer than PAGE_LENGTH
     * then life is tough and we return a chopped line.
     */
    if (info->no_end)
	info = ScanStrInPage(LoadPage(v, start), start);

    if (pCount)
	*pCount = info->length;

    return info->str_start;
}
