/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2002 by Björn Stenberg
 *
 * All files in this archive are subject to the GNU General Public License.
 * See the file COPYING in the source tree root for full license agreement.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include <string.h>
#include <errno.h>
#include <stdbool.h>
#include "file.h"
#include "fat.h"
#include "dir.h"
#include "debug.h"

/*
  These functions provide a roughly POSIX-compatible file IO API.

  Since the fat32 driver only manages sectors, we maintain a one-sector
  cache for each open file. This way we can provide byte access without
  having to re-read the sector each time. 
  The penalty is the RAM used for the cache and slightly more complex code.
*/

#define MAX_OPEN_FILES 4

struct filedesc {
    unsigned char cache[SECTOR_SIZE];
    int cacheoffset;
    int fileoffset;
    int size;
    struct fat_file fatfile;
    bool busy;
};

static struct filedesc openfiles[MAX_OPEN_FILES];

int open(char* pathname, int flags)
{
    DIR* dir;
    struct dirent* entry;
    int fd;
    char* name;

    /* For now, we don't support writing */
    if(flags & (O_WRONLY | O_RDWR))
    {
        errno = EROFS;
        return -1;
    }

    if ( pathname[0] != '/' ) {
        DEBUGF("'%s' is not an absolute path.\n",pathname);
        DEBUGF("Only absolute pathnames supported at the moment\n");
        errno = EINVAL;
        return -1;
    }

    /* find a free file descriptor */
    for ( fd=0; fd<MAX_OPEN_FILES; fd++ )
        if ( !openfiles[fd].busy )
            break;

    if ( fd == MAX_OPEN_FILES ) {
        DEBUGF("Too many files open\n");
        errno = EMFILE;
        return -1;
    }

    openfiles[fd].busy = true;

    /* locate filename */
    name=strrchr(pathname+1,'/');
    if ( name ) {
        *name = 0;
        dir = opendir(pathname);
        *name = '/';
        name++;
    }
    else {
        dir = opendir("/");
        name = pathname+1;
    }
    if (!dir) {
        DEBUGF("Failed opening dir\n");
        errno = EIO;
        openfiles[fd].busy = false;
        return -1;
    }

    /* scan dir for name */
    while ((entry = readdir(dir))) {
        if ( !strcasecmp(name, entry->d_name) ) {
            fat_open(entry->startcluster, &(openfiles[fd].fatfile));
            openfiles[fd].size = entry->size;
            break;
        }
    }
    closedir(dir);
    if ( !entry ) {
        DEBUGF("Couldn't find %s in %s\n",name,pathname);
        errno = ENOENT;
        openfiles[fd].busy = false;
        return -1;
    }

    openfiles[fd].cacheoffset = -1;
    openfiles[fd].fileoffset = 0;
    return fd;
}

int close(int fd)
{
    if (fd < 0 || fd > MAX_OPEN_FILES-1) {
        errno = EINVAL;
        return -1;
    }
    if (!openfiles[fd].busy) {
        errno = EBADF;
        return -1;
    }
    openfiles[fd].busy = false;
    return 0;
}

int read(int fd, void* buf, int count)
{
    int sectors;
    int nread=0;

    if ( !openfiles[fd].busy ) {
        errno = EBADF;
        return -1;
    }

    /* attempt to read past EOF? */
    if ( count > openfiles[fd].size - openfiles[fd].fileoffset )
        count = openfiles[fd].size - openfiles[fd].fileoffset;

    /* any head bytes? */
    if ( openfiles[fd].cacheoffset != -1 ) {
        int headbytes;
        int offs = openfiles[fd].cacheoffset;
        if ( count <= SECTOR_SIZE - openfiles[fd].cacheoffset ) {
            headbytes = count;
            openfiles[fd].cacheoffset += count;
            if ( openfiles[fd].cacheoffset >= SECTOR_SIZE )
                openfiles[fd].cacheoffset = -1;
        }
        else {
            headbytes = SECTOR_SIZE - openfiles[fd].cacheoffset;
            openfiles[fd].cacheoffset = -1;
        }

        /* eof? */
        if ( openfiles[fd].fileoffset + headbytes > openfiles[fd].size )
            headbytes = openfiles[fd].size - openfiles[fd].fileoffset;

        memcpy( buf, openfiles[fd].cache + offs, headbytes );
        nread = headbytes;
        count -= headbytes;
    }

    /* read whole sectors right into the supplied buffer */
    sectors = count / SECTOR_SIZE;
    if ( sectors ) {
        int rc = fat_read(&(openfiles[fd].fatfile), sectors, buf+nread );
        if ( rc < 0 ) {
            DEBUGF("Failed reading %d sectors\n",sectors);
            errno = EIO;
            return -1;
        }
        else {
            if ( rc > 0 ) {
                nread += sectors * SECTOR_SIZE;
                count -= sectors * SECTOR_SIZE;
            }
            else {
                /* eof */
                count=0;
            }

            openfiles[fd].cacheoffset = -1;
        }
    }

    /* any tail bytes? */
    if ( count ) {
        if ( fat_read(&(openfiles[fd].fatfile), 1,
                      &(openfiles[fd].cache)) < 0 ) {
            DEBUGF("Failed caching sector\n");
            errno = EIO;
            return -1;
        }

        /* eof? */
        if ( openfiles[fd].fileoffset + count > openfiles[fd].size )
            count = openfiles[fd].size - openfiles[fd].fileoffset;

        memcpy( buf + nread, openfiles[fd].cache, count );
        nread += count;
        openfiles[fd].cacheoffset = count;
    }

    openfiles[fd].fileoffset += nread;
    return nread;
}

int lseek(int fd, int offset, int whence)
{
    int pos;
    int newsector;
    int oldsector;
    int sectoroffset;
    int rc;

    if ( !openfiles[fd].busy ) {
        errno = EBADF;
        return -1;
    }

    switch ( whence ) {
        case SEEK_SET:
            pos = offset;
            break;

        case SEEK_CUR:
            pos = openfiles[fd].fileoffset + offset;
            break;

        case SEEK_END:
            pos = openfiles[fd].size + offset;
            break;

        default:
            errno = EINVAL;
            return -1;
    }
    if ((pos < 0) || (pos > openfiles[fd].size)) {
        errno = EINVAL;
        return -1;
    }

    /* new sector? */
    newsector = pos / SECTOR_SIZE;
    oldsector = openfiles[fd].fileoffset / SECTOR_SIZE;
    sectoroffset = pos % SECTOR_SIZE;

    if ( (newsector != oldsector) ||
         ((openfiles[fd].cacheoffset==-1) && sectoroffset) ) {
        if ( newsector != oldsector ) {
            rc = fat_seek(&(openfiles[fd].fatfile), newsector);
            if ( rc < 0 ) {
                errno = EIO;
                return -1;
            }
        }
        rc = fat_read(&(openfiles[fd].fatfile), 1,
                      &(openfiles[fd].cache));
        if ( rc < 0 ) {
            errno = EIO;
            return -1;
        }
        openfiles[fd].cacheoffset = sectoroffset;
    }
    else
        if ( openfiles[fd].cacheoffset != -1 )
            openfiles[fd].cacheoffset = sectoroffset;

    openfiles[fd].fileoffset = pos;

    return pos;
}

/*
 * local variables:
 * eval: (load-file "../rockbox-mode.el")
 * end:
 */
