/* ----------------------------------------------------------------------- *
 *
 *   Copyright 2010 Intel Corporation; author: H. Peter Anvin
 *
 *   Permission is hereby granted, free of charge, to any person
 *   obtaining a copy of this software and associated documentation
 *   files (the "Software"), to deal in the Software without
 *   restriction, including without limitation the rights to use,
 *   copy, modify, merge, publish, distribute, sublicense, and/or
 *   sell copies of the Software, and to permit persons to whom
 *   the Software is furnished to do so, subject to the following
 *   conditions:
 *
 *   The above copyright notice and this permission notice shall
 *   be included in all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *   OTHER DEALINGS IN THE SOFTWARE.
 *
 * ----------------------------------------------------------------------- */

/*
 * getfssec.c
 *
 * Generic getfssec implementation for disk-based filesystems, which
 * support the next_extent() method.
 *
 * The expected semantics of next_extent are as follows:
 *
 * The second argument will contain the initial sector number to be
 * mapped.  The routine is expected to populate
 * inode->next_extent.pstart and inode->next_extent.len (the caller
 * will store the initial sector number into inode->next_extent.lstart
 * on return.)
 *
 * If inode->next_extent.pstart is EXTENT_ZERO, then no disk I/O is
 * performed, and the data in the extent is all zero.
 *
 * If inode->next_extent.len != 0 on entry then the routine is allowed
 * to assume inode->next_extent contains valid data from the previous
 * usage, which can be used for optimization purposes.
 *
 * If the filesystem can map the entire file as a single extent
 * (e.g. iso9660), then the filesystem can simply insert the extent
 * information into inode->next_extent at searchdir/iget time, and point
 * next_extent() to the generic function no_next_extent().
 *
 * Note: the filesystem driver is not required to do extent coalescing,
 * if that is difficult to do; this routine will perform extent lookahead
 * and coalescing.  However, if the filesystem can do extent coalescing
 * very cheaply by using filesystem-specific knowledge, then that is
 * preferred (e.g. FAT).
 */

#include <dprintf.h>
#include <minmax.h>
#include "fs.h"

static inline sector_t next_psector(sector_t psector, uint32_t skip)
{
    if (EXTENT_SPECIAL(psector))
	return psector;
    else
	return psector + skip;
}

static inline sector_t next_pstart(const struct extent *e)
{
    return next_psector(e->pstart, e->len);
}


static void get_next_extent(struct inode *inode)
{
    /* The logical start address that we care about... */
    uint32_t lstart = inode->this_extent.lstart + inode->this_extent.len;

    if (inode->fs->fs_ops->next_extent(inode, lstart))
	inode->next_extent.len = 0; /* ERROR */
    inode->next_extent.lstart = lstart;

    dprintf("Extent: inode %p @ %u start %llu len %u\n",
	    inode, inode->next_extent.lstart,
	    inode->next_extent.pstart, inode->next_extent.len);
}

uint32_t generic_getfssec(struct file *file, char *buf,
			  int sectors, bool *have_more)
{
    struct inode *inode = file->inode;
    struct fs_info *fs = file->fs;
    struct disk *disk = fs->fs_dev->disk;
    uint32_t bytes_read = 0;
    uint32_t bytes_left = inode->size - file->offset;
    uint32_t sectors_left =
	(bytes_left + SECTOR_SIZE(fs) - 1) >> SECTOR_SHIFT(fs);
    uint32_t lsector;

    if (sectors > sectors_left)
	sectors = sectors_left;

    if (!sectors)
	return 0;

    lsector = file->offset >> SECTOR_SHIFT(fs);
    dprintf("Offset: %u  lsector: %u\n", file->offset, lsector);

    if (lsector < inode->this_extent.lstart ||
	lsector >= inode->this_extent.lstart + inode->this_extent.len) {
	/* inode->this_extent unusable, maybe next_extent is... */
	inode->this_extent = inode->next_extent;
    }

    if (lsector < inode->this_extent.lstart ||
	lsector >= inode->this_extent.lstart + inode->this_extent.len) {
	/* Still nothing useful... */
	inode->this_extent.lstart = lsector;
	inode->this_extent.len = 0;
    } else {
	/* We have some usable information */
	uint32_t delta = lsector - inode->this_extent.lstart;
	inode->this_extent.lstart = lsector;
	inode->this_extent.len -= delta;
	inode->this_extent.pstart
	    = next_psector(inode->this_extent.pstart, delta);
    }

    dprintf("this_extent: lstart %u pstart %llu len %u\n",
	    inode->this_extent.lstart,
	    inode->this_extent.pstart,
	    inode->this_extent.len);

    while (sectors) {
	uint32_t chunk;
	size_t len;

	while (sectors > inode->this_extent.len) {
	    if (!inode->next_extent.len ||
		inode->next_extent.lstart !=
		inode->this_extent.lstart + inode->this_extent.len)
		get_next_extent(inode);

	    if (!inode->this_extent.len) {
		/* Doesn't matter if it's contiguous... */
		inode->this_extent = inode->next_extent;
		if (!inode->next_extent.len) {
		    sectors = 0; /* Failed to get anything... we're dead */
		    break;
		}
	    } else if (inode->next_extent.len &&
		inode->next_extent.pstart == next_pstart(&inode->this_extent)) {
		/* Coalesce extents and loop */
		inode->this_extent.len += inode->next_extent.len;
	    } else {
		/* Discontiguous extents */
		break;
	    }
	}

	dprintf("this_extent: lstart %u pstart %llu len %u\n",
		inode->this_extent.lstart,
		inode->this_extent.pstart,
		inode->this_extent.len);

	chunk = min(sectors, inode->this_extent.len);
	len = chunk << SECTOR_SHIFT(fs);

	dprintf("   I/O: inode %p @ %u start %llu len %u\n",
		inode, inode->this_extent.lstart,
		inode->this_extent.pstart, chunk);

	if (inode->this_extent.pstart == EXTENT_ZERO) {
	    memset(buf, 0, len);
	} else {
	    disk->rdwr_sectors(disk, buf, inode->this_extent.pstart, chunk, 0);
	    inode->this_extent.pstart += chunk;
	}

	buf += len;
	sectors -= chunk;
	bytes_read += len;
	inode->this_extent.lstart += chunk;
	inode->this_extent.len -= chunk;
    }

    bytes_read = min(bytes_read, bytes_left);
    file->offset += bytes_read;

    if (have_more)
	*have_more = bytes_read < bytes_left;

    return bytes_read;
}
