/*
 * kernel/power/tuxonice_file.c
 *
 * Copyright (C) 2005-2010 Nigel Cunningham (nigel at tuxonice net)
 *
 * Distributed under GPLv2.
 *
 * This file encapsulates functions for usage of a simple file as a
 * backing store. It is based upon the swapallocator, and shares the
 * same basic working. Here, though, we have nothing to do with
 * swapspace, and only one device to worry about.
 *
 * The user can just
 *
 * echo TuxOnIce > /path/to/my_file
 *
 * dd if=/dev/zero bs=1M count=<file_size_desired> >> /path/to/my_file
 *
 * and
 *
 * echo /path/to/my_file > /sys/power/tuxonice/file/target
 *
 * then put what they find in /sys/power/tuxonice/resume
 * as their resume= parameter in lilo.conf (and rerun lilo if using it).
 *
 * Having done this, they're ready to hibernate and resume.
 *
 * TODO:
 * - File resizing.
 */

#include <linux/blkdev.h>
#include <linux/mount.h>
#include <linux/fs.h>
#include <linux/fs_uuid.h>

#include "tuxonice.h"
#include "tuxonice_modules.h"
#include "tuxonice_bio.h"
#include "tuxonice_alloc.h"
#include "tuxonice_builtin.h"
#include "tuxonice_sysfs.h"
#include "tuxonice_ui.h"
#include "tuxonice_io.h"

#define target_is_normal_file() (S_ISREG(target_inode->i_mode))

static struct toi_module_ops toi_fileops;

static struct file *target_file;
static struct block_device *toi_file_target_bdev;
static unsigned long pages_available, pages_allocated;
static char toi_file_target[256];
static struct inode *target_inode;
static int file_target_priority;
static int used_devt;
static int target_claim;
static dev_t toi_file_dev_t;
static int sig_page_index;

/* For test_toi_file_target */
static struct toi_bdev_info *file_chain;

static int has_contiguous_blocks(struct toi_bdev_info *dev_info, int page_num)
{
	int j;
	sector_t last = 0;

	for (j = 0; j < dev_info->blocks_per_page; j++) {
		sector_t this = bmap(target_inode,
				page_num * dev_info->blocks_per_page + j);

		if (!this || (last && (last + 1) != this))
			break;

		last = this;
	}

	return j == dev_info->blocks_per_page;
}

static unsigned long get_usable_pages(struct toi_bdev_info *dev_info)
{
	unsigned long result = 0;
	struct block_device *bdev = dev_info->bdev;
	int i;

	switch (target_inode->i_mode & S_IFMT) {
	case S_IFSOCK:
	case S_IFCHR:
	case S_IFIFO: /* Socket, Char, Fifo */
		return -1;
	case S_IFREG: /* Regular file: current size - holes + free
			 space on part */
		for (i = 0; i < (target_inode->i_size >> PAGE_SHIFT) ; i++) {
			if (has_contiguous_blocks(dev_info, i))
				result++;
		}
		break;
	case S_IFBLK: /* Block device */
		if (!bdev->bd_disk) {
			toi_message(TOI_IO, TOI_VERBOSE, 0,
					"bdev->bd_disk null.");
			return 0;
		}

		result = (bdev->bd_part ?
			bdev->bd_part->nr_sects :
			get_capacity(bdev->bd_disk)) >> (PAGE_SHIFT - 9);
	}


	return result;
}

static int toi_file_register_storage(void)
{
	struct toi_bdev_info *devinfo;
	int result = 0;
	struct fs_info *fs_info;

	toi_message(TOI_IO, TOI_VERBOSE, 0, "toi_file_register_storage.");
	if (!strlen(toi_file_target)) {
		toi_message(TOI_IO, TOI_VERBOSE, 0, "Register file storage: "
				"No target filename set.");
		return 0;
	}

	target_file = filp_open(toi_file_target, O_RDONLY|O_LARGEFILE, 0);
	toi_message(TOI_IO, TOI_VERBOSE, 0, "filp_open %s returned %p.",
			toi_file_target, target_file);

	if (IS_ERR(target_file) || !target_file) {
		target_file = NULL;
		toi_file_dev_t = name_to_dev_t(toi_file_target);
		if (!toi_file_dev_t) {
			struct kstat stat;
			int error = vfs_stat(toi_file_target, &stat);
			printk(KERN_INFO "Open file %s returned %p and "
					"name_to_devt failed.\n",
					toi_file_target, target_file);
			if (error) {
				printk(KERN_INFO "Stating the file also failed."
					" Nothing more we can do.\n");
				return 0;
			} else
				toi_file_dev_t = stat.rdev;
		}

		toi_file_target_bdev = toi_open_by_devnum(toi_file_dev_t);
		if (IS_ERR(toi_file_target_bdev)) {
			printk(KERN_INFO "Got a dev_num (%lx) but failed to "
					"open it.\n",
					(unsigned long) toi_file_dev_t);
			toi_file_target_bdev = NULL;
			return 0;
		}
		used_devt = 1;
		target_inode = toi_file_target_bdev->bd_inode;
	} else
		target_inode = target_file->f_mapping->host;

	toi_message(TOI_IO, TOI_VERBOSE, 0, "Succeeded in opening the target.");
	if (S_ISLNK(target_inode->i_mode) || S_ISDIR(target_inode->i_mode) ||
	    S_ISSOCK(target_inode->i_mode) || S_ISFIFO(target_inode->i_mode)) {
		printk(KERN_INFO "File support works with regular files,"
				" character files and block devices.\n");
		/* Cleanup routine will undo the above */
		return 0;
	}

	if (!used_devt) {
		if (S_ISBLK(target_inode->i_mode)) {
			toi_file_target_bdev = I_BDEV(target_inode);
			if (!bd_claim(toi_file_target_bdev, &toi_fileops))
				target_claim = 1;
		} else
			toi_file_target_bdev = target_inode->i_sb->s_bdev;
		if (!toi_file_target_bdev) {
			printk(KERN_INFO "%s is not a valid file allocator "
					"target.\n", toi_file_target);
			return 0;
		}
		toi_file_dev_t = toi_file_target_bdev->bd_dev;
	}

	devinfo = toi_kzalloc(39, sizeof(struct toi_bdev_info), GFP_ATOMIC);
	if (!devinfo) {
		printk("Failed to allocate a toi_bdev_info struct for the file allocator.\n");
		return -ENOMEM;
	}

	devinfo->bdev = toi_file_target_bdev;
	devinfo->allocator = &toi_fileops;
	devinfo->allocator_index = 0;

	fs_info = fs_info_from_block_dev(toi_file_target_bdev);
	if (fs_info && !IS_ERR(fs_info)) {
		memcpy(devinfo->uuid, &fs_info->uuid, 16);
		free_fs_info(fs_info);
	} else
		result = (int) PTR_ERR(fs_info);

	/* Unlike swap code, only complain if fs_info_from_block_dev returned
	 * -ENOMEM. The 'file' might be a full partition, so might validly not
	 * have an identifiable type, UUID etc.
	 */
	if (result)
		printk(KERN_DEBUG "Failed to get fs_info for file device (%d).\n",
				result);
	devinfo->dev_t = toi_file_dev_t;
	devinfo->prio = file_target_priority;
	devinfo->bmap_shift = target_inode->i_blkbits - 9;
	devinfo->blocks_per_page =
		(1 << (PAGE_SHIFT - target_inode->i_blkbits));
	sprintf(devinfo->name, "file %s", toi_file_target);
	file_chain = devinfo;
	toi_message(TOI_IO, TOI_VERBOSE, 0, "Dev_t is %lx. Prio is %d. Bmap "
			"shift is %d. Blocks per page %d.",
			devinfo->dev_t, devinfo->prio, devinfo->bmap_shift,
			devinfo->blocks_per_page);

	/* Keep one aside for the signature */
	pages_available = get_usable_pages(devinfo) - 1;

	toi_message(TOI_IO, TOI_VERBOSE, 0, "Registering file storage, %lu "
			"pages.", pages_available);

	toi_bio_ops.register_storage(devinfo);
	return 0;
}

static unsigned long toi_file_storage_available(void)
{
	return pages_available;
}

static int toi_file_allocate_storage(struct toi_bdev_info *chain,
		unsigned long request)
{
	unsigned long available = pages_available - pages_allocated;
	unsigned long to_add = min(available, request);

	toi_message(TOI_IO, TOI_VERBOSE, 0, "Pages available is %lu. Allocated "
		"is %lu. Allocating %lu pages from file.",
		pages_available, pages_allocated, to_add);
	pages_allocated += to_add;

	return to_add;
}

/**
 * __populate_block_list - add an extent to the chain
 * @min:	Start of the extent (first physical block = sector)
 * @max:	End of the extent (last physical block = sector)
 *
 * If TOI_TEST_BIO is set, print a debug message, outputting the min and max
 * fs block numbers.
 **/
static int __populate_block_list(struct toi_bdev_info *chain, int min, int max)
{
	if (test_action_state(TOI_TEST_BIO))
		toi_message(TOI_IO, TOI_VERBOSE, 0, "Adding extent %d-%d.",
			min << chain->bmap_shift,
			((max + 1) << chain->bmap_shift) - 1);

	return toi_add_to_extent_chain(&chain->blocks, min, max);
}

static int get_main_pool_phys_params(struct toi_bdev_info *chain)
{
	int i, extent_min = -1, extent_max = -1, result = 0, have_sig_page = 0;
	unsigned long pages_mapped = 0;

	toi_message(TOI_IO, TOI_VERBOSE, 0, "Getting file allocator blocks.");

	if (chain->blocks.first)
		toi_put_extent_chain(&chain->blocks);

	if (!target_is_normal_file()) {
		result = (pages_available > 0) ?
			__populate_block_list(chain, chain->blocks_per_page,
				(pages_allocated + 1) *
				chain->blocks_per_page - 1) : 0;
		return result;
	}

	/*
	 * FIXME: We are assuming the first page is contiguous. Is that
	 * assumption always right?
	 */

	for (i = 0; i < (target_inode->i_size >> PAGE_SHIFT); i++) {
		sector_t new_sector;

		if (!has_contiguous_blocks(chain, i))
			continue;

		if (!have_sig_page) {
			have_sig_page = 1;
			sig_page_index = i;
			continue;
		}

		pages_mapped++;

		/* Ignore first page - it has the header */
		if (pages_mapped == 1)
			continue;

		new_sector = bmap(target_inode, (i * chain->blocks_per_page));

		/*
		 * I'd love to be able to fill in holes and resize
		 * files, but not yet...
		 */

		if (new_sector == extent_max + 1)
			extent_max += chain->blocks_per_page;
		else {
			if (extent_min > -1) {
				result = __populate_block_list(chain,
						extent_min, extent_max);
				if (result)
					return result;
			}

			extent_min = new_sector;
			extent_max = extent_min +
				chain->blocks_per_page - 1;
		}

		if (pages_mapped == pages_allocated)
			break;
	}

	if (extent_min > -1) {
		result = __populate_block_list(chain, extent_min, extent_max);
		if (result)
			return result;
	}

	return 0;
}

static void toi_file_free_storage(struct toi_bdev_info *chain)
{
	pages_allocated = 0;
	file_chain = NULL;
}

/**
 * toi_file_print_debug_stats - print debug info
 * @buffer:	Buffer to data to populate
 * @size:	Size of the buffer
 **/
static int toi_file_print_debug_stats(char *buffer, int size)
{
	int len = scnprintf(buffer, size, "- File Allocator active.\n");

	len += scnprintf(buffer+len, size-len, "  Storage available for "
			"image: %lu pages.\n", pages_available);

	return len;
}

static void toi_file_cleanup(int finishing_cycle)
{
	if (toi_file_target_bdev) {
		if (target_claim) {
			bd_release(toi_file_target_bdev);
			target_claim = 0;
		}

		if (used_devt) {
			blkdev_put(toi_file_target_bdev,
					FMODE_READ | FMODE_NDELAY);
			used_devt = 0;
		}
		toi_file_target_bdev = NULL;
		target_inode = NULL;
	}

	if (target_file) {
		filp_close(target_file, NULL);
		target_file = NULL;
	}

	pages_available = 0;
}

/**
 * test_toi_file_target - sysfs callback for /sys/power/tuxonince/file/target
 *
 * Test wheter the target file is valid for hibernating.
 **/
static void test_toi_file_target(void)
{
	int result = toi_file_register_storage();
	sector_t sector;
	char buf[50];
	struct fs_info *fs_info;

	if (result || !file_chain)
		return;

	/* This doesn't mean we're in business. Is any storage available? */
	if (!pages_available)
		goto out;

	toi_file_allocate_storage(file_chain, 1);
	result = get_main_pool_phys_params(file_chain);
	if (result)
		goto out;


	sector = bmap(target_inode, sig_page_index *
			file_chain->blocks_per_page) << file_chain->bmap_shift;

	/* Use the uuid, or the dev_t if that fails */
	fs_info = fs_info_from_block_dev(toi_file_target_bdev);
	if (!fs_info || IS_ERR(fs_info)) {
		bdevname(toi_file_target_bdev, buf);
		sprintf(resume_file, "/dev/%s:%llu", buf,
				(unsigned long long) sector);
	} else {
		int i;
		hex_dump_to_buffer(fs_info->uuid, 16, 32, 1, buf, 50, 0);

		/* Remove the spaces */
		for (i = 1; i < 16; i++) {
			buf[2 * i] = buf[3 * i];
			buf[2 * i + 1] = buf[3 * i + 1];
		}
		buf[32] = 0;
		sprintf(resume_file, "UUID=%s:0x%llx", buf,
				(unsigned long long) sector);
		free_fs_info(fs_info);
	}

	toi_attempt_to_parse_resume_device(0);
out:
	toi_file_free_storage(file_chain);
	toi_bio_ops.free_storage();
}

static struct toi_sysfs_data sysfs_params[] = {
	SYSFS_STRING("target", SYSFS_RW, toi_file_target, 256,
		SYSFS_NEEDS_SM_FOR_WRITE, test_toi_file_target),
	SYSFS_INT("enabled", SYSFS_RW, &toi_fileops.enabled, 0, 1, 0, NULL),
	SYSFS_INT("priority", SYSFS_RW, &file_target_priority, -4095,
			4096, 0, NULL),
};

static struct toi_bio_allocator_ops toi_bio_fileops = {
	.register_storage			= toi_file_register_storage,
	.storage_available			= toi_file_storage_available,
	.allocate_storage			= toi_file_allocate_storage,
	.bmap					= get_main_pool_phys_params,
	.free_storage				= toi_file_free_storage,
};

static struct toi_module_ops toi_fileops = {
	.type					= BIO_ALLOCATOR_MODULE,
	.name					= "file storage",
	.directory				= "file",
	.module					= THIS_MODULE,
	.print_debug_info			= toi_file_print_debug_stats,
	.cleanup				= toi_file_cleanup,
	.bio_allocator_ops			= &toi_bio_fileops,

	.sysfs_data		= sysfs_params,
	.num_sysfs_entries	= sizeof(sysfs_params) /
		sizeof(struct toi_sysfs_data),
};

/* ---- Registration ---- */
static __init int toi_file_load(void)
{
	return toi_register_module(&toi_fileops);
}

#ifdef MODULE
static __exit void toi_file_unload(void)
{
	toi_unregister_module(&toi_fileops);
}

module_init(toi_file_load);
module_exit(toi_file_unload);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Nigel Cunningham");
MODULE_DESCRIPTION("TuxOnIce FileAllocator");
#else
late_initcall(toi_file_load);
#endif
