/* The different devices we support */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
#include "oopsd.h"
#include "ide.h"

struct device_type
{
	unsigned int major;

	/* Create a LINUX_OOPSER_SET command for this device. */
	void *(*create_set_command)(int devfd, size_t *len,
				    const unsigned int offsets[]);

	/* Get device name, eg. /dev/hda3 */
	char *(*get_device_name)(int major, int minor);

	/* Get devfs-style name. eg. /dev/ide/host0/bus0/target0/lun0/disc */
	char *(*get_devfs_name)(int major, int minor);
};

struct device_type devices[] = {
	/* IDE */
	{
		.major = 3,
		.create_set_command = ide_create_set_command,
		.get_device_name = ide_get_device_name,
		.get_devfs_name = ide_get_devfs_name,
	},
};

static struct device_type *find_device(int major)
{
	int i;

	for (i = 0; i < sizeof(devices)/sizeof(devices[0]); i++) {
		if (devices[i].major == major)
			return &devices[i];
	}

	fprintf(stderr, "Device major %u unsupported\n", major);
	exit(1);
}

static void find_file_offsets(int fd, unsigned int offsets[])
{
	unsigned int sectors_per_block, i;

	if (ioctl(fd, FIGETBSZ, &i) != 0) {
		fprintf(stderr, "Could not get blocksize: %s\n",
			strerror(errno));
		exit(1);
	}
	if (i % 512 != 0) {
		fprintf(stderr, "Blocksize %u not multiple of 512!\n",
			i);
		exit(1);
	}
	sectors_per_block = i / 512;

	for (i = 0; i < LINUX_OOPSER_BLOCKS; i++) {
		unsigned int block;

		/* This is both an in and out parameter */
		block = i / sectors_per_block;
		if (ioctl(fd, FIBMAP, &block) != 0) {
			fprintf(stderr, "Could not get block map from: %s\n",
				strerror(errno));
			exit(1);
		}
		/* Offset = sector offset of fs block + sector offset
                   within block */
		offsets[i] = block * sectors_per_block
			+ (i % sectors_per_block);
	}
}

/* So far only IDE supported */
int open_device(int major, int minor)
{
	char *name, *namedevfs;
	int fd;
	struct device_type *dev;

	dev = find_device(major);

	name = dev->get_device_name(major, minor);
	namedevfs = dev->get_devfs_name(major, minor);

	fd = open(name, O_RDWR);
	if (fd < 0 && errno == ENOENT)
		fd = open(namedevfs, O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "Failure opening major %u: %s\n",
			major, strerror(errno));
		exit(1);
	}
	free(name);
	free(namedevfs);
	return fd;
}

/* Create the set command appropriate for this filename */
void *create_set_command(const char *name, size_t *len)
{
	int devfd;
	struct stat statbuf;
	unsigned int offsets[LINUX_OOPSER_BLOCKS];
	struct device_type *dev;

	devfd = open(name, O_RDWR);
	if (devfd < 0) {
		fprintf(stderr, "Failure opening %s: %s\n",
			name, strerror(errno));
		exit(1);
	}

	fstat(devfd, &statbuf);
	if (S_ISREG(statbuf.st_mode)) {
		int fd = devfd;

		/* If it's a regular file, figure out where on device */
		devfd = open_device(major(statbuf.st_dev),
				    minor(statbuf.st_dev));
		find_file_offsets(fd, offsets);
		close(fd);
		fstat(devfd, &statbuf);
	} else if (S_ISBLK(statbuf.st_mode)) {
		unsigned int i;

		/* Device: consective blocks */
		for (i = 0; sizeof(offsets)/sizeof(offsets[0]); i++)
			offsets[i] = i;
	} else {
		fprintf(stderr, "%s is not a block device nor a file\n",
			name);
		exit(1);
	}

	dev = find_device(major(statbuf.st_rdev));
	return dev->create_set_command(devfd, len, offsets);
}
