#include <stdio.h>
#include <assert.h>
#include <strings.h>
#include <stdlib.h>
#include <stdarg.h>

#include "misc.h"
#include "llfile.h"

/* Set this to 1 to use small subfiles for testing */
#define TEST 0

/* If SUBFILE_SIZE_BY_STRUCT is 1, then we always write whole
 * items to the subfile, and move to the next subfile when
 * there isn't enough space to write a whole struct to the
 * subfile.  This is slightly inoptimal for space usage and
 * disallows some sanity checks, but is necessary if subfiles
 * are to be used to distributed jobs across machines.
 *
 * Note that we don't explicitly check that the filesize is
 * a multiple of the current struct being written; we just
 * ensure that only full structs are written.  So if you change
 * the length of the struct as you do, all we guarantee is that
 * the last struct written is written in full before moving to
 * the next subfile.
 *
 * If this is 0, then we enforce all subfiles except the last to
 * have size exactly MAX_SUBFILE_SIZE.
 *
 * Note that llfprintf can produce files slightly *LARGER* than
 * MAX_SUBFILE_SIZE, since we don't know how long the fprintf output
 * is until after we do it; so the last fprintf to a subfile will
 * go over MAX_SUBFILE_SIZE, at which point we move to the next
 * subfile.  So, you should leave some space between MAX_SUBFILE_SIZE
 * and the maximum physical file size on your OS.
 */
#define SUBFILE_SIZE_BY_STRUCT 1

#if TEST
#define MAX_SUBFILE_SIZE 1000
#define ASSERT assert
#else
#define MAX_SUBFILE_SIZE (1024*1024*1024)	/* one gigabyte */
#define ASSERT assert
#endif

struct _lfile
{
    char *base_filename, *mode;
    int subfile, ispipe;
    FILE *fp;	/* current subfile pointer */
};


/* Return the filename of the current sub-file.  Uses a static buffer. */
static char *SubFilename(LLFILE *llfp)
{
    static char buf[10240];
    sprintf(buf, "%s.%03d", llfp->base_filename, llfp->subfile);
    return buf;
}

LLFILE *llfopen(const char *filename, const char *mode)
{
    LLFILE *llfp = Calloc(1, sizeof(LLFILE));

    llfp->base_filename = strdup(filename);
    llfp->mode = strdup(mode);
    llfp->subfile = 0;
    llfp->fp = fopen(SubFilename(llfp), mode);
    if(llfp->fp == NULL)
    {
	perror(SubFilename(llfp));
	exit(1);
    }

    return llfp;
}

int llfclose(LLFILE *llfp)
{
    int result;

    assert(llfp);
    assert(llfp->base_filename);
    assert(llfp->mode);
    assert(llfp->fp);

    result = fclose(llfp->fp);

    if(result != 0)
    {
	perror(SubFilename(llfp));
	Fatal("can't close subfile %s for LLFILE %s!",
	    SubFilename(llfp), llfp->base_filename);
    }
    free(llfp->base_filename);
    free(llfp->mode);
    Free(llfp);

    return result;
}



LLFILE *llpopen(const char *command, const char *mode)
{
    LLFILE *llfp = Calloc(1, sizeof(LLFILE));

    llfp->base_filename = strdup(command);
    llfp->mode = strdup(mode);
    llfp->subfile = 0;
    llfp->ispipe = true;
    llfp->fp = popen(command, mode);
    if(llfp->fp == NULL)
    {
	perror("popen");
	Fatal("popen failed on command '%s'", command);
    }

    return llfp;
}

int llpclose(LLFILE *llfp)
{
    int result;

    assert(llfp);
    assert(llfp->base_filename);
    assert(llfp->mode);
    assert(llfp->fp);

    result = pclose(llfp->fp);

    if(result != 0)
    {
	perror("pclose");
	Fatal("pclose failed for command '%s'", llfp->base_filename);
    }
    free(llfp->base_filename);
    free(llfp->mode);
    Free(llfp);

    return result;
}

LLFILE *llFromFILE(FILE *fp)
{
    LLFILE *llfp = Calloc(1, sizeof(LLFILE));

    llfp->base_filename = strdup(":pipe:");
    llfp->mode = strdup(":pipe:");
    llfp->subfile = 0;
    llfp->ispipe = true;
    llfp->fp = fp;
    if(llfp->fp == NULL)
    {
	Fatal("llFromFILE cannot be called with a NULL FILE pointer");
    }

    return llfp;
}




/* Try to go to the next subfile.  If it doesn't exist,
 * we still set the subfile as if it did, but return
 * NULL.  In this case, you can go back to the last
 * valid subfile by immediately calling llfilePrevSubfile.
 */
FILE *llfileNextSubfile(LLFILE *llfp)
{
    assert(llfp->ispipe == false);
    fclose(llfp->fp);
    ++llfp->subfile;
    llfp->fp = fopen(SubFilename(llfp), llfp->mode);
    return llfp->fp;
}



FILE *llfilePrevSubfile(LLFILE *llfp)
{
    FILE *oldfp = llfp->fp;

    assert(llfp->ispipe == false);
    assert(llfp->subfile > 0);

    /* It's possible we're being called immediately after discovering
     * there's no next subfile, in which case llfp->fp == NULL, so
     * we shouldn't try to fclose it.
     */
    if(llfp->fp)
	fclose(llfp->fp);

    --llfp->subfile;
    llfp->fp = fopen(SubFilename(llfp), llfp->mode);
    assert(llfp->fp);

#if !SUBFILE_SIZE_BY_STRUCT
    if(oldfp)
    {
	/* Be paranoid: it has a successor subfile, so it should have the max length */
	fseek(llfp->fp, 0L, SEEK_END);
	assert(ftell(llfp->fp) == MAX_SUBFILE_SIZE);
	fseek(llfp->fp, 0L, SEEK_SET);
    }
#endif
    return llfp->fp;
}

size_t llfwrite(const void *vptr, size_t size, size_t nitems, LLFILE *llfp)
{
    size_t nbytes2write = size * nitems;
    long long lltmp = ((long long)size)*((long long)nitems);
    const char *cptr = (const char*)vptr;

    /* Ensure it all fits into a size_t */
    assert(size >= 0);
    assert(nitems >= 0);
    assert(((long long)nbytes2write) == lltmp);

    if(llfp->ispipe)
    {
	/* If it's a pipe, just pass it along ... */
	return fwrite(vptr, size, nitems, llfp->fp);
    }


    /* General idea: we avoid ever writing past the end of a subfile,
     * so if we have to straddle the end of a subfile to get to the next
     * one, we read to MAX_SUBFILE_SIZE, ensure that it's actually the end
     * (unless SUBFILE_SIZED_BY_STRUCT), and then move to the next subfile.
     */

    while(nbytes2write > 0)
    {
	int subfilepos = ftell(llfp->fp);
	int sub_available = MAX_SUBFILE_SIZE - subfilepos;
	int sub_nbytes2write;
	size_t sub_write_result;

#if SUBFILE_SIZE_BY_STRUCT
	int max_nitems_available = sub_available / size;
	int n_items_to_write = MIN(nitems, max_nitems_available);
	sub_nbytes2write = size * n_items_to_write;
#else
	sub_nbytes2write = MIN(nbytes2write, sub_available);
#endif
	assert(subfilepos <= MAX_SUBFILE_SIZE);
	sub_write_result = fwrite(cptr, 1, sub_nbytes2write, llfp->fp);

	if(sub_write_result != sub_nbytes2write)
	{
	    perror("fwrite");
	    Fatal("llfwrite: fwrite to subfile %s failed", SubFilename(llfp));
	}

	cptr += sub_nbytes2write;
	nbytes2write -= sub_nbytes2write;

	if(nbytes2write > 0)
	{
	    if(llfileNextSubfile(llfp) == NULL)
	    {
		perror("fopen");
		Fatal("llfwrite: fopen on subfile %s failed", SubFilename(llfp));
	    }
	}
    }

    assert(nbytes2write == 0);

    return nitems;
}



size_t llfread(void *vptr, size_t size, size_t nitems, LLFILE *llfp)
{
    size_t nbytes2read = size * nitems;
    size_t total_bytes_read = 0;
    long long lltmp = ((long long)size)*((long long)nitems);
    char *cptr = (char*)vptr;

    /* Ensure it all fits into a size_t */
    assert(size >= 0);
    assert(nitems >= 0);
    assert(((long long)nbytes2read) == lltmp);

    if(llfp->ispipe)
    {
	/* If it's a pipe, just pass it along ... */
	return fread(vptr, size, nitems, llfp->fp);
    }


    while(nbytes2read > 0)
    {
	int subfilepos = ftell(llfp->fp);
	int sub_available = MAX_SUBFILE_SIZE - subfilepos;
	int sub_nbytes2read;
	size_t sub_read;

#if SUBFILE_SIZE_BY_STRUCT
	int max_nitems_available = sub_available / size;
	int n_items_to_read = MIN(nitems, max_nitems_available);
	sub_nbytes2read = size * n_items_to_read;
#else
	sub_nbytes2read = MIN(nbytes2read, sub_available);
#endif
	assert(subfilepos <= MAX_SUBFILE_SIZE);
	sub_read = fread((void*)cptr, 1, sub_nbytes2read, llfp->fp);

#if SUBFILE_SIZE_BY_STRUCT
	if(sub_read % size != 0)
	    Fatal("llfread: did not read an integer number of items!");
#endif

	total_bytes_read += sub_read;

	if(sub_read != sub_nbytes2read)
	{
	    /* Probably EOF */
	    if(feof(llfp->fp))
	    {
		/* Paranoid check: ensure there's no next file */
		assert(llfileNextSubfile(llfp) == NULL);
		llfilePrevSubfile(llfp);
		fseek(llfp->fp, 0L, SEEK_END);
		return total_bytes_read / size;
	    }

	    /* Otherwise, some weird error occured */
	    perror("fread");
	    Warning("llfread: fread encountered a weird error in subfile %s; continuing",
		SubFilename(llfp));
	}

	cptr += sub_read;
	nbytes2read -= sub_read;

	if(nbytes2read > 0)	/* get the next subfile */
	{
#if !SUBFILE_SIZE_BY_STRUCT
	    assert(ftell(llfp->fp) == MAX_SUBFILE_SIZE);
#endif
	    if(llfileNextSubfile(llfp) == NULL)
	    {
		/* No next subfile, assume EOF of the LLFILE */
		llfilePrevSubfile(llfp);
		fseek(llfp->fp, 0L, SEEK_END);
		return total_bytes_read / size;
	    }
	}
    }

    assert(nbytes2read == 0);
    assert(total_bytes_read == nitems*size);

    return nitems;
}

int llfseek(LLFILE *llfp, long long offset, int whence)
{
    int new_subfile, new_subfile_pos, old_subfile;
    FILE *new_fp;

    switch(whence)
    {
    case SEEK_SET:
	assert(offset >= 0LL);

	new_subfile = offset / (long long)MAX_SUBFILE_SIZE;
	new_subfile_pos = offset % (long long)MAX_SUBFILE_SIZE;

	old_subfile = llfp->subfile;
	llfp->subfile = new_subfile;

	new_fp = fopen(SubFilename(llfp), llfp->mode);
	if(new_fp == NULL)
	{
	    Warning("llfseek: attempt to seek past last subfile on %s; "
		"restoring old position", llfp->base_filename);
	    llfp->subfile = old_subfile;
	    return -1;
	}

	fclose(llfp->fp);
	llfp->fp = new_fp;
	fseek(llfp->fp, new_subfile_pos, SEEK_SET);

	break;

    case SEEK_CUR:
	Apology("llfseek from SEEK_CUR not yet implemented");
	break;

    case SEEK_END:
	if(offset == 0LL)	/* find end of file */
	{
	    while(llfileNextSubfile(llfp)) /* do nothing, just finding the last one */
		;
	    llfilePrevSubfile(llfp);	/* go back to last existing subfile */
	    return fseek(llfp->fp, 0L, SEEK_END);
	}
	else
	    Apology("llfseek from SEEK_END only accepts 0LL currently");
	break;
    }

    return 0;
}

long long llftell(LLFILE *llfp)
{
    long long pos = 0LL;

    pos = ((long long)MAX_SUBFILE_SIZE)*llfp->subfile
	+ ftell(llfp->fp);

    return pos;
}

int llfflush(LLFILE *llfp)
{
    return fflush(llfp->fp);
}


/* If you don't have vfprintf on your system, you'll just have to
 * manually, in your code, change all references from
 *     fprintf(file_p, ...)
 * to
 *     fprintf(llfp->fp, ...)
 */
int llfprintf(LLFILE *llfp, const char *fmt, ...)
{
    int result;
    va_list ap;
    va_start(ap, fmt);
    result = vfprintf(llfp->fp, fmt, ap);
    va_end(ap);

    if(ftell(llfp->fp) >= MAX_SUBFILE_SIZE)
	llfileNextSubfile(llfp);

    return result;
}


/* See comment for vfprintf above if your system doesn't have vfscanf */
int llfscanf(LLFILE *llfp, const char *fmt, ...)
{
    int result = 0;
#if 0
    va_list ap;
    va_start(ap, fmt);
    result = vfscanf(llfp->fp, fmt, ap);
    va_end(ap);
#else
    Apology("llfscanf not implemented; see code comment for fix");
#endif

    if(ftell(llfp->fp) >= MAX_SUBFILE_SIZE)
	llfileNextSubfile(llfp);

    return result;
}
