/*
 * barebox ext4 support
 *
 * Copyright (c) 2012 Sascha Hauer <s.hauer@pengutronix.de>, Pengutronix
 *
 * See file CREDITS for list of people who contributed to this
 * project.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <common.h>
#include <driver.h>
#include <init.h>
#include <malloc.h>
#include <fs.h>
#include <command.h>
#include <errno.h>
#include <linux/stat.h>
#include <linux/ctype.h>
#include <xfuncs.h>
#include <fcntl.h>
#include "ext4_common.h"

int ext4fs_devread(struct ext_filesystem *fs, int __sector, int byte_offset,
		int byte_len, char *buf)
{
	ssize_t size;
	uint64_t sector = __sector;

	size = cdev_read(fs->cdev, buf, byte_len, sector * SECTOR_SIZE + byte_offset, 0);
	if (size < 0) {
		dev_err(fs->dev, "read error at sector %d: %s\n", __sector,
				strerror(-size));
		return size;
	}

	return 0;
}

static int ext_open(struct device_d *dev, FILE *file, const char *filename)
{
	struct ext_filesystem *fs = dev->priv;
	struct ext2fs_node *inode;
	int ret;

	ret = ext4fs_open(fs->data, filename, &inode);
	if (ret)
		return ret;

	file->size = __le32_to_cpu(inode->inode.size);
	file->inode = inode;

	return 0;
}

static int ext_close(struct device_d *dev, FILE *f)
{
	struct ext_filesystem *fs = dev->priv;

	ext4fs_free_node(f->inode, &fs->data->diropen);

	return 0;
}

static int ext_read(struct device_d *_dev, FILE *f, void *buf, size_t insize)
{
	return ext4fs_read_file(f->inode, f->pos, insize, buf);
}

static loff_t ext_lseek(struct device_d *dev, FILE *f, loff_t pos)
{
	f->pos = pos;

	return f->pos;
}

struct ext4fs_dir {
	struct ext2fs_node *dirnode;
	int fpos;
	DIR dir;
};

static DIR *ext_opendir(struct device_d *dev, const char *pathname)
{
	struct ext_filesystem *fs = dev->priv;
	struct ext4fs_dir *ext4_dir;
	int type, ret;

	ext4_dir = xzalloc(sizeof(*ext4_dir));

	ret = ext4fs_find_file(pathname, &fs->data->diropen, &ext4_dir->dirnode,
				  &type);
	if (ret) {
		free(ext4_dir);
		return NULL;
	}

	if (type != FILETYPE_DIRECTORY)
		return NULL;

	ext4_dir->dir.priv = ext4_dir;

	ret = ext4fs_read_inode(ext4_dir->dirnode->data, ext4_dir->dirnode->ino,
			&ext4_dir->dirnode->inode);
	if (ret) {
		ext4fs_free_node(ext4_dir->dirnode, &fs->data->diropen);
		free(ext4_dir);

		return NULL;
	}

	return &ext4_dir->dir;
}

static struct dirent *ext_readdir(struct device_d *dev, DIR *dir)
{
	struct ext4fs_dir *ext4_dir = dir->priv;
	struct ext2_dirent dirent;
	struct ext2fs_node *diro = ext4_dir->dirnode;
	int ret;
	char *filename;

	if (ext4_dir->fpos >= __le32_to_cpu(diro->inode.size))
		return NULL;

	ret = ext4fs_read_file(diro, ext4_dir->fpos, sizeof(struct ext2_dirent),
			(char *) &dirent);
	if (ret < 0)
		return NULL;

	if (dirent.namelen == 0)
		return NULL;

	filename = xzalloc(dirent.namelen + 1);

	ret = ext4fs_read_file(diro, ext4_dir->fpos + sizeof(struct ext2_dirent),
			dirent.namelen, filename);
	if (ret < 0) {
		free(filename);
		return NULL;
	}

	filename[dirent.namelen] = '\0';

	ext4_dir->fpos += __le16_to_cpu(dirent.direntlen);

	strcpy(dir->d.d_name, filename);

	free(filename);

	return &dir->d;
}

static int ext_closedir(struct device_d *dev, DIR *dir)
{
	struct ext_filesystem *fs = dev->priv;
	struct ext4fs_dir *ext4_dir = dir->priv;

	ext4fs_free_node(ext4_dir->dirnode, &fs->data->diropen);

	free(ext4_dir);

	return 0;
}

static int ext_stat(struct device_d *dev, const char *filename, struct stat *s)
{
	struct ext_filesystem *fs = dev->priv;
	struct ext2fs_node *node;
	int status, ret;

	status = ext4fs_find_file(filename, &fs->data->diropen, &node, NULL);
	if (status)
		return -ENOENT;

	ret = ext4fs_read_inode(node->data, node->ino, &node->inode);
	if (ret)
		return ret;

	s->st_size = __le32_to_cpu(node->inode.size);
	s->st_mode = __le16_to_cpu(node->inode.mode);

	ext4fs_free_node(node, &fs->data->diropen);

	return 0;
}

static int ext_readlink(struct device_d *dev, const char *pathname,
		char *buf, size_t bufsiz)
{
	struct ext_filesystem *fs = dev->priv;
	struct ext2fs_node *node;
	char *symlink;
	int ret, len, type;

	ret = ext4fs_find_file(pathname, &fs->data->diropen, &node, &type);
	if (ret)
		return ret;

	if (type != FILETYPE_SYMLINK)
		return -EINVAL;

	symlink = ext4fs_read_symlink(node);
	if (!symlink)
		return -ENOENT;

	len = min(bufsiz, strlen(symlink));

	memcpy(buf, symlink, len);

	free(symlink);

	return 0;
}

static int ext_probe(struct device_d *dev)
{
	struct fs_device_d *fsdev = dev_to_fs_device(dev);
	char *backingstore = fsdev->backingstore;
	int ret;
	struct ext_filesystem *fs;

	fs = xzalloc(sizeof(*fs));

	dev->priv = fs;
	fs->dev = dev;

	if (!strncmp(backingstore , "/dev/", 5))
		backingstore += 5;

	fs->cdev = cdev_open(backingstore, O_RDWR);
	if (!fs->cdev) {
		ret = -ENOENT;
		goto err_open;
	}

	ret = ext4fs_mount(fs);
	if (ret)
		goto err_mount;

	return 0;

err_mount:
	cdev_close(fs->cdev);
err_open:
	free(fs);

	return ret;
}

static void ext_remove(struct device_d *dev)
{
	struct ext_filesystem *fs = dev->priv;

	ext4fs_umount(fs);
	cdev_close(fs->cdev);
	free(fs);
}

static struct fs_driver_d ext_driver = {
	.open      = ext_open,
	.close     = ext_close,
	.read      = ext_read,
	.lseek     = ext_lseek,
	.opendir   = ext_opendir,
	.readdir   = ext_readdir,
	.closedir  = ext_closedir,
	.stat      = ext_stat,
	.readlink  = ext_readlink,
	.type      = filetype_ext,
	.flags     = 0,
	.drv = {
		.probe  = ext_probe,
		.remove = ext_remove,
		.name = "ext4",
	}
};

static int ext_init(void)
{
	return register_fs_driver(&ext_driver);
}

coredevice_initcall(ext_init);
