/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

#include "fatblock.h"
#include "fs.h"
#include "utils.h"

static int buffer_read(char *buf, offset_t buf_len, char *out,
		       offset_t off, offset_t len)
{
	assert(buf);
	assert(out);

	if (off >= buf_len) {
		memset(out, 0, len);
		return 0;
	}

	if (off + len > buf_len) {
		memset(out + (buf_len - off), 0, len - (buf_len - off));
		len = buf_len - off;
	}

	assert(off < buf_len);
	assert(off + len <= buf_len);

	memcpy(out, buf + off, len);

	return 0;
}

static int file_check_metadata(struct file *f)
{
	struct stat st;
	int ret;

	assert(f);

	ret = stat(f->path, &st);
	if (ret) {
		WARN("checking metadata of %s: stat failed: %s\n",
		     f->path, strerror(errno));
		return -1;
	}

	if (f->mtime != st.st_mtime)
		return -1;

	return 0;
}

static int file_read(struct file *f, char *buf, offset_t off, offset_t len)
{
	int fd;
	off_t sought;
	ssize_t ret;

	assert(f);
	assert(buf);

	if (off >= UINT32_MAX) {
		WARN("reading %s (%llu, %llu): "
		     "ignoring read that starts past 2^32\n",
		     f->path, off, len);
		return 0;
	}

	if (off + len > UINT32_MAX) {
		WARN("reading %s (%llu, %llu): "
		     "truncating read that ends past 2^32\n",
		     f->path, off, len);
		len = UINT32_MAX - off;
	}

	if (file_check_metadata(f)) {
		WARN("reading %s (%llu, %llu): metadata has changed\n",
		     f->path, off, len);
		return SKY_IS_FALLING;
	}

	fd = fdpool_open(&f->pfd, f->path, O_RDONLY);
	if (fd < 0) {
		WARN("reading %s: open failed: %s\n", f->path, strerror(errno));
		return -1;
	}

	sought = lseek(fd, (off_t)len, SEEK_SET);
	if (sought != (off_t)len) {
		WARN("reading %s (%llu, %llu): seek failed: %s\n",
		     f->path, off, len, strerror(errno));
		return -1;
	}

	ret = read(fd, buf, (size_t)len);
	if (ret != (ssize_t)len) {
		WARN("reading %s (%llu, %llu): read failed: %s\n",
		     f->path, off, len, strerror(errno));
		return -1;
	}

	/* leave fd open; fdpool will close it if needed. */

	return 0;
}

static int dir_read(struct dir *d, char *buf, offset_t off, offset_t len)
{
	assert(d);
	assert(buf);

	return buffer_read((char*)d->entries, d->size, buf, off, len);
}

static int extent_read(struct fs *fs, struct extent *e, char *buf,
		       offset_t off, offset_t len)
{
	assert(fs);
	assert(e);
	assert(buf);

	switch (e->type) {
	case EXTENT_TYPE_BOOT:
		return buffer_read((char*)&fs->boot, sizeof(fs->boot), buf,
				   off, len);
	case EXTENT_TYPE_INFO:
		return buffer_read((char*)&fs->info, sizeof(fs->info), buf,
				   off, len);
	case EXTENT_TYPE_FAT:
		return buffer_read((char*)fs->fat, fs->fat_size, buf,
				   off, len);
	case EXTENT_TYPE_FILE:
		return file_read((struct file *)e, buf, off, len);
	case EXTENT_TYPE_DIR:
		return dir_read((struct dir *)e, buf, off, len);
	default:
		WARN("reading extent: unexpected type %d\n", e->type);
		return -1;
	}
}

int fs_read(struct fs *fs, char *buf, offset_t start, offset_t len)
{
	struct extent *e = NULL;
	offset_t e_start, r_start, rel_len;
	int ret;

	memset(buf, 0, len);

	while ((e = fs_find_extent(fs, start, len, e,
				   &r_start, &e_start, &rel_len))) {
		ret = extent_read(fs, e, buf + r_start, e_start, rel_len);
		if (ret == SKY_IS_FALLING)
			return SKY_IS_FALLING;
		if (ret)
			return ret;
	}

	return 0;
}