/*
    AVFS: A Virtual File System Library
    Copyright (C) 1998-2001  Miklos Szeredi <miklos@szeredi.hu>

    This program can be distributed under the terms of the GNU GPL.
    See the file COPYING.
*/

#ifdef linux
/* For pread()/pwrite() */
#define _XOPEN_SOURCE 500
#endif

#include "serialfile.h"
#include "cache.h"

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

struct sfile {
    const struct sfilefuncs *func;
    void *data;
    int flags;
    void *conndata;
    char *localfile;
    avoff_t numbytes;
    int fd;
    int dirty;
    enum { SF_BEGIN, SF_READ, SF_FINI } state;
};

static void sfile_init(struct sfile *fil)
{
    fil->conndata = NULL;
    fil->localfile = NULL;
    fil->numbytes = 0;
    fil->fd = -1;
    fil->state = SF_BEGIN;
    fil->dirty = 0;
}

static void sfile_end(struct sfile *fil)
{
    close(fil->fd);
    av_del_tmpfile(fil->localfile);
    av_unref_obj(fil->conndata);
}

static void sfile_reset(struct sfile *fil)
{
    sfile_end(fil);
    sfile_init(fil);
}

static void sfile_reset_usecache(struct sfile *fil)
{
    fil->flags &= ~SFILE_NOCACHE;
    sfile_reset(fil);
}

static void sfile_delete(struct sfile *fil)
{
    sfile_end(fil);
    av_unref_obj(fil->data);
}

struct sfile *av_sfile_new(const struct sfilefuncs *func,
			   void *data, int flags)
{
    struct sfile *fil;

    AV_NEW_OBJ(fil, sfile_delete);
    fil->func = func;
    fil->data = data;
    fil->flags = flags;

    sfile_init(fil);

    return fil;
}

static int sfile_open_localfile(struct sfile *fil)
{
    int res;
    int openfl;

    res = av_get_tmpfile(&fil->localfile);
    if(res < 0)
        return res;
    
    openfl = O_RDWR | O_CREAT | O_TRUNC;
    fil->fd = open(fil->localfile, openfl, 0600);
    if(fil->fd == -1) {
        av_log(AVLOG_ERROR, "Error opening file %s: %s", fil->localfile,
                 strerror(errno));
        return -EIO;
    }
    
    return 0;
}

static int sfile_startget(struct sfile *fil)
{
    int res;

    if(!(fil->flags & SFILE_NOCACHE)) {
        res = sfile_open_localfile(fil);
        if(res < 0)
            return res;
    }
    
    res = fil->func->startget(fil->data, &fil->conndata);
    if(res < 0)
        return res;

    fil->state = SF_READ;
    
    return 0;
}


static avssize_t sfile_do_read(struct sfile *fil, char *buf, avssize_t nbyte)
{
    avsize_t numbytes;

    numbytes = 0;
    while(nbyte > 0) {
        avssize_t res;

        res = fil->func->read(fil->conndata, buf, nbyte);
        if(res < 0)
            return res;
        
        if(res == 0) {
            av_unref_obj(fil->conndata);
            fil->conndata = NULL;
            fil->state = SF_FINI;
            break;
        }
        
        nbyte -= res;
        buf += res;
        numbytes += res;
    }

    return numbytes;
}

static avssize_t sfile_cached_pwrite(struct sfile *fil, const char *buf,
                                     avssize_t nbyte, avoff_t offset)
{
    avssize_t res;

    res = pwrite(fil->fd, buf, nbyte, offset);
    if(res == -1 && (errno == ENOSPC || errno == EDQUOT)) {
        av_cache_diskfull();
        res = pwrite(fil->fd, buf, nbyte, offset);
    }
    if(res == -1) {
        av_log(AVLOG_ERROR, "Error writing file %s: %s", fil->localfile,
                 strerror(errno));
        return -EIO;
    }
    if(res != nbyte) {
        av_log(AVLOG_ERROR, "Error writing file %s: short write",
                 fil->localfile);
        return -EIO;
    }

    /* FIXME: Checking free space is expensive. This should be done in
       a more clever way */
    if(offset + nbyte > fil->numbytes)
        av_cache_checkspace();

    return res;
}

static avssize_t sfile_read(struct sfile *fil, char *buf, avssize_t nbyte)
{
    avssize_t res;

    res = sfile_do_read(fil, buf, nbyte);
    if(res <= 0)
        return res;

    if(!(fil->flags & SFILE_NOCACHE))
        res = sfile_cached_pwrite(fil, buf, res, fil->numbytes);

    if(res > 0)
        fil->numbytes += res;

    return res;
}

static int sfile_dummy_read(struct sfile *fil, avoff_t offset)
{
    avssize_t res;
    //avsize_t nact;
    const int tmpbufsize = 8192;
    char tmpbuf[tmpbufsize];

    //if((fil->flags & SFILE_NOCACHE) != 0)
    //    nact = AV_MIN(tmpbufsize, offset - fil->numbytes);
    //else
    //    nact = tmpbufsize;

    res = sfile_read(fil, tmpbuf, tmpbufsize);
    
    if(res < 0)
        return res;
    
    return 0;
}

static avssize_t sfile_cached_pread(struct sfile *fil, char *buf,
                                   avssize_t nbyte, avoff_t offset)
{
    avssize_t res;

    if(nbyte == 0)
        return 0;

    res = pread(fil->fd, buf, nbyte, offset);
    if(res < 0) {
        av_log(AVLOG_ERROR, "Error reading file %s: %s", fil->localfile,
                 strerror(errno));
        return -EIO;
    }
    if(res != nbyte) {
        av_log(AVLOG_ERROR, "Error reading file %s: short read",
                 fil->localfile);
        return -EIO;
    }

    return res;
}

static avssize_t sfile_finished_read(struct sfile *fil, char *buf,
                                     avsize_t nbyte, avoff_t offset)
{
    avsize_t nact;
    
    if(offset >= fil->numbytes)
        return 0;
        
    nact = AV_MIN(nbyte, fil->numbytes - offset);

    return sfile_cached_pread(fil, buf, nact, offset);
}

static avssize_t sfile_pread(struct sfile *fil, char *buf, avsize_t nbyte,
                             avoff_t offset)
{
    int res;

    while(fil->state == SF_READ) {
        if(offset + nbyte <= fil->numbytes)
            return sfile_cached_pread(fil, buf, nbyte, offset);
        
        if(offset == fil->numbytes)
            return sfile_read(fil, buf, nbyte);
        
        res = sfile_dummy_read(fil, offset);
        if(res < 0)
            return res;
    }

    return sfile_finished_read(fil, buf, nbyte, offset);
}

static avssize_t sfile_pread_start(struct sfile *fil, char *buf,
                                   avsize_t nbyte, avoff_t offset)
{
    int res;

    if((fil->flags & SFILE_NOCACHE) != 0 && offset < fil->numbytes)
        sfile_reset_usecache(fil);

    if(fil->state == SF_BEGIN) {
        res = sfile_startget(fil);
        if(res < 0)
            return res;
    }

    return sfile_pread(fil, buf, nbyte, offset);
}

static avssize_t sfile_pread_force(struct sfile *fil, char *buf,
                                   avsize_t nbyte, avoff_t offset)
{
    avssize_t res;

    res = sfile_pread_start(fil, buf, nbyte, offset);
    if(res < 0) {
        if(res == -EAGAIN && fil->numbytes > 0) {
            sfile_reset(fil);
            res = sfile_pread_start(fil, buf, nbyte, offset);
        }
        if(res < 0) {
            if(res == -EAGAIN)
                res = -EIO;
            
            sfile_reset(fil);
        }
    }

    return res;
}

avssize_t av_sfile_pread(struct sfile *fil, char *buf, avsize_t nbyte,
                           avoff_t offset)
{
    if(nbyte == 0)
        return 0;
    
    return sfile_pread_force(fil, buf, nbyte, offset);
}


static int sfile_read_until(struct sfile *fil, avoff_t offset, int finish)
{
    avssize_t res;

    if(finish && (fil->flags & SFILE_NOCACHE) != 0)
        sfile_reset_usecache(fil);
    else if(fil->state == SF_FINI)
        return 0;

    res = sfile_pread_force(fil, NULL, 0, offset);
    if(res < 0)
        return res;

    if(finish && fil->state != SF_FINI) {
        av_unref_obj(fil->conndata);
        fil->conndata = NULL;
        fil->state = SF_FINI;
    }

    return 0;
}

avoff_t av_sfile_size(struct sfile *fil)
{
    avssize_t res;

    res = sfile_read_until(fil, AV_MAXOFF, 0);
    if(res < 0)
        return res;

    return fil->numbytes;
}

int av_sfile_startget(struct sfile *fil)
{
    return sfile_read_until(fil, 0, 0);
}

int av_sfile_truncate(struct sfile *fil, avoff_t length)
{
    int res;

    if(length == 0) {
        if(fil->state == SF_FINI && fil->numbytes == 0)
            return 0;

        sfile_reset_usecache(fil);
        res = sfile_open_localfile(fil);
        if(res < 0)
            return res;

        fil->state = SF_FINI;
        fil->dirty = 1;
        return 0;
    }
    
    res = sfile_read_until(fil, length, 1);
    if(res < 0)
        return res;

    if(fil->numbytes > length) {
        ftruncate(fil->fd, length);
        fil->numbytes = length;
        fil->dirty = 1;
    }
    
    return 0;
}

avssize_t av_sfile_pwrite(struct sfile *fil, const char *buf, avsize_t nbyte,
                            avoff_t offset)
{
    avssize_t res;
    avoff_t end;

    if(nbyte == 0)
        return 0;

    res = sfile_read_until(fil, AV_MAXOFF, 1);
    if(res < 0)
        return res;
    
    res = sfile_cached_pwrite(fil, buf, nbyte, offset);
    if(res < 0) {
        sfile_reset(fil);
        return res;
    }

    end = offset + nbyte; 
    if(end > fil->numbytes)
        fil->numbytes = end;

    fil->dirty = 1;
    return res;
}

static int sfile_writeout(struct sfile *fil, void *conndata)
{
    avssize_t res;
    const int tmpbufsize = 8192;
    char tmpbuf[tmpbufsize];
    avoff_t offset;

    for(offset = 0; offset < fil->numbytes;) {
        avsize_t nact = AV_MIN(tmpbufsize, fil->numbytes - offset);

        res = sfile_cached_pread(fil, tmpbuf, nact, offset);
        if(res < 0)
            return res;
        
        res = fil->func->write(conndata, tmpbuf, nact);
        if(res < 0)
            return res;

        offset += res;
    }
    
    return 0;
}

int av_sfile_flush(struct sfile *fil)
{
    int res;
    void *conndata;

    if(!fil->dirty)
        return 0;
    
    res = fil->func->startput(fil->data, &conndata);
    if(res == 0) {
        res = sfile_writeout(fil, conndata);
        if(res == 0)
            res = fil->func->endput(conndata);
    }
    av_unref_obj(conndata);
    if(res < 0)
        sfile_reset(fil);

    fil->dirty = 0;

    return res;
}

void *av_sfile_getdata(struct sfile *fil)
{
    return fil->data;
}

avoff_t av_sfile_diskusage(struct sfile *fil)
{
    int res;
    struct stat buf;

    if(fil->fd == -1)
        return 0;
    
    res = fstat(fil->fd, &buf);
    if(res == -1) {
        av_log(AVLOG_ERROR, "Error in fstat() for %s: %s", fil->localfile,
               strerror(errno));
        return -EIO;
    }
    
    return buf.st_blocks * 512;
}
