// ----------------------------------------------------------------------
//  This software is part of the OpenBeOS distribution and is covered 
//  by the OpenBeOS license.
//
//  File Name:		virtualdrive.c
//
//	Description:	Driver that emulates virtual drives.
//
//	Author:			Marcus Overhagen <Marcus@Overhagen.de>
//					Ingo Weinhold <bonefish@users.sf.net>
//					Axel Doerfler <axeld@pinc-software.de>
// ----------------------------------------------------------------------

#include <fcntl.h>
#include <errno.h>
#include <malloc.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <KernelExport.h>
#include <Drivers.h>
#include <Errors.h>

#include "lock.h"
#include "virtualdrive.h"
#include "virtualdrive_icon.h"

/*
[2:07] <geist> when you open the file in the driver, use stat() to see if it's a file. if it is, call ioctl 10000 on the underlying file
[2:07] <geist> that's the disable-cache ioctl
[2:08] <geist> bfs is probably doing the same algorithm, and seeing that you are a device and not a file, and so it doesn't call 10000 on you
[2:08] <marcus_o> thanks, I will try calling it
[2:08] <geist> and dont bother using dosfs as a host fs, it wont work
[2:09] <geist> bfs is the only fs that's reasonably safe being reentered like that, but only if the underlying one is opened with the cache disabled on that file
[2:09] <geist> that ioctl is used on the swap file as well
[2:10] <marcus_o> I'm currently allocating memory in the driver's write() function hook
[2:10] <geist> cant do that
*/

//#define TRACE(x) dprintf x
#define TRACE(x) ;
#define MB (1024LL * 1024LL)

static int dev_index_for_path(const char *path);

/* -----
	null-terminated array of device names supported by this driver
----- */

static const char *sVirtualDriveName[] = {
	VIRTUAL_DRIVE_DIRECTORY_REL "/0",
	VIRTUAL_DRIVE_DIRECTORY_REL "/1",
	VIRTUAL_DRIVE_DIRECTORY_REL "/2",
	VIRTUAL_DRIVE_DIRECTORY_REL "/3",
	VIRTUAL_DRIVE_DIRECTORY_REL "/4",
	VIRTUAL_DRIVE_DIRECTORY_REL "/5",
	VIRTUAL_DRIVE_DIRECTORY_REL "/6",
	VIRTUAL_DRIVE_DIRECTORY_REL "/7",
	VIRTUAL_DRIVE_DIRECTORY_REL "/8",
	VIRTUAL_DRIVE_DIRECTORY_REL "/9",
	VIRTUAL_DRIVE_CONTROL_DEVICE_REL,
	NULL
};

int32 api_version = B_CUR_DRIVER_API_VERSION;
extern device_hooks sVirtualDriveHooks;

lock driverlock;

typedef struct device_info {
	int32			open_count;
	int				fd;
	off_t			size;
	bool			unused;
	bool			registered;
	char			file[B_PATH_NAME_LENGTH];
	const char		*device_path;
	device_geometry	geometry;
} device_info;

#define kDeviceCount		11
#define kDataDeviceCount	(kDeviceCount - 1)
#define kControlDevice		(kDeviceCount - 1)
struct device_info			gDeviceInfos[kDeviceCount];

static int32		gRegistrationCount	= 0;
static int			gControlDeviceFD	= -1;

static thread_id	gLockOwner			= -1;
static int32		gLockOwnerNesting	= 0;


// lock_driver
void
lock_driver()
{
	thread_id thread = find_thread(NULL);
	if (gLockOwner != thread) {
		LOCK(driverlock);
		gLockOwner = thread;
	}
	gLockOwnerNesting++;
}


// unlock_driver
void
unlock_driver()
{
	thread_id thread = find_thread(NULL);
	if (gLockOwner == thread && --gLockOwnerNesting == 0) {
		gLockOwner = -1;
		UNLOCK(driverlock);
	}
}


// is_valid_device_index
static inline
bool
is_valid_device_index(int32 index)
{
	return (index >= 0 && index < kDeviceCount);
}


// is_valid_data_device_index
static inline
bool
is_valid_data_device_index(int32 index)
{
	return (is_valid_device_index(index) && index != kControlDevice);
}


// dev_index_for_path
static
int
dev_index_for_path(const char *path)
{
	int i;
	for (i = 0; i < kDeviceCount; i++) {
		if (!strcmp(path, gDeviceInfos[i].device_path))
			return i;
	}
	return -1;
}


// clear_device_info
static
void
clear_device_info(int32 index)
{
	TRACE(("virtualdrive: clear_device_info(%ld)\n", index));

	device_info &info = gDeviceInfos[index];
	info.open_count = 0;
	info.fd = -1;
	info.size = 0;
	info.unused = (index != kDeviceCount - 1);
	info.registered = !info.unused;
	info.file[0] = '\0';
	info.device_path = sVirtualDriveName[index];
	info.geometry.read_only = true;
}


// init_device_info
static
status_t
init_device_info(int32 index, virtual_drive_info *initInfo)
{
	if (!is_valid_data_device_index(index) || !initInfo)
		return B_BAD_VALUE;

	device_info &info = gDeviceInfos[index];
	if (!info.unused)
		return B_BAD_VALUE;

	bool readOnly = (initInfo->use_geometry && initInfo->geometry.read_only);

	// open the file
	int fd = open(initInfo->file_name, (readOnly ? O_RDONLY : O_RDWR));
	if (fd < 0)
		return errno;

	status_t error = B_OK;

	// get the file size
	off_t fileSize = 0;
	struct stat st;
	if (fstat(fd, &st) == 0)
		fileSize = st.st_size;
	else
		error = errno;

	// If we shall use the supplied geometry, we enlarge the file, if
	// necessary. Otherwise we fill in the geometry according to the size of the file.
	off_t size = 0;
	if (error == B_OK) {
		if (initInfo->use_geometry) {
			// use the supplied geometry
			info.geometry = initInfo->geometry;
			size = (off_t)info.geometry.bytes_per_sector
				* info.geometry.sectors_per_track
				* info.geometry.cylinder_count
				* info.geometry.head_count;
			if (size > fileSize) {
				if (!readOnly) {
					if (ftruncate(fd, size) != 0)
						error = errno;
				} else
					error = B_NOT_ALLOWED;
			}
		} else {
			// fill in the geometry
			// default to 512 bytes block size
			uint32 blockSize = 512;
			// Optimally we have only 1 block per sector and only one head.
			// Since we have only a uint32 for the cylinder count, this won't work
			// for files > 2TB. So, we set the head count to the minimally possible
			// value.
			off_t blocks = fileSize / blockSize;
			uint32 heads = (blocks + ULONG_MAX - 1) / ULONG_MAX;
			if (heads == 0)
				heads = 1;
			info.geometry.bytes_per_sector = blockSize;
		    info.geometry.sectors_per_track = 1;
		    info.geometry.cylinder_count = blocks / heads;
		    info.geometry.head_count = heads;
		    info.geometry.device_type = B_DISK;	// TODO: Add a new constant.
		    info.geometry.removable = false;
		    info.geometry.read_only = false;
		    info.geometry.write_once = false;
			size = (off_t)info.geometry.bytes_per_sector
				* info.geometry.sectors_per_track
				* info.geometry.cylinder_count
				* info.geometry.head_count;
		}
	}

	if (error == B_OK) {
		// Disable caching for underlying file! (else this driver will deadlock)
		// We probably cannot resize the file once the cache has been disabled!

		// This applies to BeOS only:
		// Work around a bug in BFS: the file is not synced before the cache is
		// turned off, and thus causing possible inconsistencies.
		// Unfortunately, this only solves one half of the issue; there is
		// no way to remove the blocks in the cache, so changes made to the
		// image have the chance to get lost.
		fsync(fd);

		// This is a special reserved ioctl() opcode not defined anywhere in
		// the Be headers.
		if (ioctl(fd, 10000) != 0) {
			dprintf("virtualdrive: disable caching ioctl failed\n");
			return errno;
		}
	}

	// fill in the rest of the device_info structure
	if (error == B_OK) {
		// open_count doesn't have to be changed here (virtualdrive_open() will do that for us)
		info.fd = fd;
		info.size = size;
		info.unused = false;
		info.registered = true;
		strcpy(info.file, initInfo->file_name);
		info.device_path = sVirtualDriveName[index];
	} else {
		// cleanup on error
		close(fd);
		if (info.open_count == 0)
			clear_device_info(index);
	}
	return error;
}


// uninit_device_info
static
status_t
uninit_device_info(int32 index)
{
	if (!is_valid_data_device_index(index))
		return B_BAD_VALUE;

	device_info &info = gDeviceInfos[index];
	if (info.unused)
		return B_BAD_VALUE;

	close(info.fd);
	clear_device_info(index);
	return B_OK;
}


//	#pragma mark -
//	public driver API


status_t
init_hardware(void)
{
	TRACE(("virtualdrive: init_hardware\n"));
	return B_OK;
}


status_t
init_driver(void)
{
	TRACE(("virtualdrive: init\n"));

	new_lock(&driverlock, "virtualdrive lock");

	// init the device infos
	for (int32 i = 0; i < kDeviceCount; i++)
		clear_device_info(i);

	return B_OK;
}


void
uninit_driver(void)
{
	TRACE(("virtualdrive: uninit\n"));
	free_lock(&driverlock);
}


const char **
publish_devices(void)
{
	TRACE(("virtualdrive: publish_devices\n"));
	return sVirtualDriveName;
}


device_hooks *
find_device(const char* name)
{
	TRACE(("virtualdrive: find_device(%s)\n", name));
	return &sVirtualDriveHooks;
}


//	#pragma mark -
//	the device hooks


static status_t
virtualdrive_open(const char *name, uint32 flags, void **cookie)
{
	TRACE(("virtualdrive: open %s\n",name));

	*cookie = (void *)-1;

	lock_driver();

	int32 devIndex = dev_index_for_path(name);

	TRACE(("virtualdrive: devIndex %ld!\n", devIndex));

	if (!is_valid_device_index(devIndex)) {
		TRACE(("virtualdrive: wrong index!\n"));
		unlock_driver();
		return B_ERROR;
	}

	if (gDeviceInfos[devIndex].unused) {
		TRACE(("virtualdrive: device is unused!\n"));
		unlock_driver();
		return B_ERROR;
	}

	if (!gDeviceInfos[devIndex].registered) {
		TRACE(("virtualdrive: device has been unregistered!\n"));
		unlock_driver();
		return B_ERROR;
	}

	// store index in cookie
	*cookie = (void *)devIndex;

	gDeviceInfos[devIndex].open_count++;

	unlock_driver();
	return B_OK;
}


static status_t
virtualdrive_close(void *cookie)
{
	int32 devIndex = (int)cookie;

	TRACE(("virtualdrive: close() devIndex = %ld\n", devIndex));
	if (!is_valid_data_device_index(devIndex))
		return B_OK;

	lock_driver();

	gDeviceInfos[devIndex].open_count--;
	if (gDeviceInfos[devIndex].open_count == 0 && !gDeviceInfos[devIndex].registered) {
		// The last FD is closed and the device has been unregistered. Free its info.
		uninit_device_info(devIndex);
	}

	unlock_driver();

	return B_OK;
}


static status_t
virtualdrive_read(void *cookie, off_t position, void *buffer, size_t *numBytes)
{
	TRACE(("virtualdrive: read pos = 0x%08Lx, bytes = 0x%08lx\n", position, *numBytes));

	// check parameters
	int devIndex = (int)cookie;
	if (devIndex == kControlDevice) {
		TRACE(("virtualdrive: reading from control device not allowed\n"));
		return B_NOT_ALLOWED;
	}
	if (position < 0)
		return B_BAD_VALUE;

	lock_driver();
	device_info &info = gDeviceInfos[devIndex];
	// adjust position and numBytes according to the file size
	if (position > info.size)
		position = info.size;
	if (position + *numBytes > info.size)
		*numBytes = info.size - position;
	// read
	status_t error = B_OK;
	ssize_t bytesRead = read_pos(info.fd, position, buffer, *numBytes);
	if (bytesRead < 0)
		error = errno;
	else
		*numBytes = bytesRead;
	unlock_driver();
	return error;
}


static status_t
virtualdrive_write(void *cookie, off_t position, const void *buffer, size_t *numBytes)
{
	TRACE(("virtualdrive: write pos = 0x%08Lx, bytes = 0x%08lx\n", position, *numBytes));

	// check parameters
	int devIndex = (int)cookie;
	if (devIndex == kControlDevice) {
		TRACE(("virtualdrive: writing to control device not allowed\n"));
		return B_NOT_ALLOWED;
	}
	if (position < 0)
		return B_BAD_VALUE;

	lock_driver();
	device_info &info = gDeviceInfos[devIndex];
	// adjust position and numBytes according to the file size
	if (position > info.size)
		position = info.size;
	if (position + *numBytes > info.size)
		*numBytes = info.size - position;
	// read
	status_t error = B_OK;
	ssize_t bytesRead = write_pos(info.fd, position, buffer, *numBytes);
	if (bytesRead < 0)
		error = errno;
	else
		*numBytes = bytesRead;
	unlock_driver();
	return error;
}


static status_t
virtualdrive_control(void *cookie, uint32 op, void *arg, size_t len)
{
	TRACE(("virtualdrive: ioctl\n"));

	int devIndex = (int)cookie;
	device_info &info = gDeviceInfos[devIndex];

	if (devIndex == kControlDevice || info.unused) {
		// control device or unused data device
		switch (op) {
			case B_GET_DEVICE_SIZE:
			case B_SET_NONBLOCKING_IO:
			case B_SET_BLOCKING_IO:
			case B_GET_READ_STATUS:
			case B_GET_WRITE_STATUS:		
			case B_GET_ICON:
			case B_GET_GEOMETRY:
			case B_GET_BIOS_GEOMETRY:
			case B_GET_MEDIA_STATUS:
			case B_SET_UNINTERRUPTABLE_IO:
			case B_SET_INTERRUPTABLE_IO:
			case B_FLUSH_DRIVE_CACHE:
			case B_GET_BIOS_DRIVE_ID:
			case B_GET_DRIVER_FOR_DEVICE:
			case B_SET_DEVICE_SIZE:
			case B_SET_PARTITION:
			case B_FORMAT_DEVICE:
			case B_EJECT_DEVICE:
			case B_LOAD_MEDIA:
			case B_GET_NEXT_OPEN_DEVICE:
				TRACE(("virtualdrive: another ioctl: %lx (%lu)\n", op, op));
				return B_BAD_VALUE;

			case VIRTUAL_DRIVE_REGISTER_FILE:
			{
				TRACE(("virtualdrive: VIRTUAL_DRIVE_REGISTER_FILE\n"));

				virtual_drive_info *driveInfo = (virtual_drive_info *)arg;
				if (devIndex != kControlDevice || driveInfo == NULL
					|| driveInfo->magic != VIRTUAL_DRIVE_MAGIC
					|| driveInfo->drive_info_size != sizeof(virtual_drive_info))
					return B_BAD_VALUE;

				status_t error = B_ERROR;
				int32 i;

				lock_driver();				

				// first, look if we already have opened that file and see
				// if it's available to us which happens when it has been
				// halted but is still in use by other components
				for (i = 0; i < kDataDeviceCount; i++) {
					if (!gDeviceInfos[i].unused
						&& gDeviceInfos[i].fd == -1
						&& !gDeviceInfos[i].registered
						&& !strcmp(gDeviceInfos[i].file, driveInfo->file_name)) {
						// mark device as unused, so that init_device_info() will succeed
						gDeviceInfos[i].unused = true;
						error = B_OK;
						break;
					}
				}

				if (error != B_OK) {
					// find an unused data device
					for (i = 0; i < kDataDeviceCount; i++) {
						if (gDeviceInfos[i].unused) {
							error = B_OK;
							break;
						}
					}
				}

				if (error == B_OK) {
					// we found a device slot, let's initialize it
					error = init_device_info(i, driveInfo);
					if (error == B_OK) {
						// return the device path
						strcpy(driveInfo->device_name, "/dev/");
						strcat(driveInfo->device_name, gDeviceInfos[i].device_path);

						// on the first registration we need to open the
						// control device to stay loaded
						if (gRegistrationCount++ == 0) {
							char path[B_PATH_NAME_LENGTH];
							strcpy(path, "/dev/");
							strcat(path, info.device_path);
							gControlDeviceFD = open(path, O_RDONLY);
						}
					}
				}

				unlock_driver();
				return error;
			}
			case VIRTUAL_DRIVE_UNREGISTER_FILE:
			case VIRTUAL_DRIVE_GET_INFO:
				TRACE(("virtualdrive: VIRTUAL_DRIVE_UNREGISTER_FILE/"
					  "VIRTUAL_DRIVE_GET_INFO on control device\n"));
				// these are called on used data files only!
				return B_BAD_VALUE;

			default:
				TRACE(("virtualdrive: unknown ioctl: %lx (%lu)\n", op, op));
				return B_BAD_VALUE;
		}
	} else {
		// used data device
		switch (op) {
			case B_GET_DEVICE_SIZE:
				TRACE(("virtualdrive: B_GET_DEVICE_SIZE\n"));
				*(size_t*)arg = info.size;
				return B_OK;

			case B_SET_NONBLOCKING_IO:
				TRACE(("virtualdrive: B_SET_NONBLOCKING_IO\n"));
				return B_OK;

			case B_SET_BLOCKING_IO:
				TRACE(("virtualdrive: B_SET_BLOCKING_IO\n"));
				return B_OK;

			case B_GET_READ_STATUS:
				TRACE(("virtualdrive: B_GET_READ_STATUS\n"));
				*(bool*)arg = true;
				return B_OK;

			case B_GET_WRITE_STATUS:		
				TRACE(("virtualdrive: B_GET_WRITE_STATUS\n"));
				*(bool*)arg = true;
				return B_OK;

			case B_GET_ICON:
			{
				TRACE(("virtualdrive: B_GET_ICON\n"));
				device_icon *icon = (device_icon *)arg;

				if (icon->icon_size == kPrimaryImageWidth) {
					memcpy(icon->icon_data, kPrimaryImageBits, kPrimaryImageWidth * kPrimaryImageHeight);
				} else if (icon->icon_size == kSecondaryImageWidth) {
					memcpy(icon->icon_data, kSecondaryImageBits, kSecondaryImageWidth * kSecondaryImageHeight);
				} else
					return B_ERROR;

				return B_OK;
			}

			case B_GET_GEOMETRY:
				TRACE(("virtualdrive: B_GET_GEOMETRY\n"));
				*(device_geometry *)arg = info.geometry;
				return B_OK;

			case B_GET_BIOS_GEOMETRY:
			{
				TRACE(("virtualdrive: B_GET_BIOS_GEOMETRY\n"));
				device_geometry *dg = (device_geometry *)arg;
				dg->bytes_per_sector = 512;
				dg->sectors_per_track = info.size / (512 * 1024);
				dg->cylinder_count = 1024;
				dg->head_count = 1;
				dg->device_type = info.geometry.device_type;
				dg->removable = info.geometry.removable;
				dg->read_only = info.geometry.read_only;
				dg->write_once = info.geometry.write_once;
				return B_OK;
			}

			case B_GET_MEDIA_STATUS:
				TRACE(("virtualdrive: B_GET_MEDIA_STATUS\n"));
				*(status_t*)arg = B_NO_ERROR;
				return B_OK;

			case B_SET_UNINTERRUPTABLE_IO:
				TRACE(("virtualdrive: B_SET_UNINTERRUPTABLE_IO\n"));
				return B_OK;

			case B_SET_INTERRUPTABLE_IO:
				TRACE(("virtualdrive: B_SET_INTERRUPTABLE_IO\n"));
				return B_OK;

			case B_FLUSH_DRIVE_CACHE:
				TRACE(("virtualdrive: B_FLUSH_DRIVE_CACHE\n"));
				return B_OK;

			case B_GET_BIOS_DRIVE_ID:
				TRACE(("virtualdrive: B_GET_BIOS_DRIVE_ID\n"));
				*(uint8*)arg = 0xF8;
				return B_OK;

			case B_GET_DRIVER_FOR_DEVICE:
			case B_SET_DEVICE_SIZE:
			case B_SET_PARTITION:
			case B_FORMAT_DEVICE:
			case B_EJECT_DEVICE:
			case B_LOAD_MEDIA:
			case B_GET_NEXT_OPEN_DEVICE:
				TRACE(("virtualdrive: another ioctl: %lx (%lu)\n", op, op));
				return B_BAD_VALUE;

			case VIRTUAL_DRIVE_REGISTER_FILE:
				TRACE(("virtualdrive: VIRTUAL_DRIVE_REGISTER_FILE (data)\n"));
				return B_BAD_VALUE;
			case VIRTUAL_DRIVE_UNREGISTER_FILE:
			{
				TRACE(("virtualdrive: VIRTUAL_DRIVE_UNREGISTER_FILE\n"));
				lock_driver();

				bool immediately = (bool)arg;
				bool wasRegistered = info.registered;

				info.registered = false;

				// on the last unregistration we need to close the
				// control device
				if (wasRegistered && --gRegistrationCount == 0) {
					close(gControlDeviceFD);
					gControlDeviceFD = -1;
				}

				// if we "immediately" is true, we will stop our service immediately
				// and close the underlying file, open it for other uses
				if (immediately) {
					TRACE(("virtualdrive: close file descriptor\n"));
					// we cannot use uninit_device_info() here, since that does
					// a little too much and would open the device for other
					// uses.
					close(info.fd);
					info.fd = -1;
				}

				unlock_driver();
				return B_OK;
			}
			case VIRTUAL_DRIVE_GET_INFO:
			{
				TRACE(("virtualdrive: VIRTUAL_DRIVE_GET_INFO\n"));

				virtual_drive_info *driveInfo = (virtual_drive_info *)arg;
				if (driveInfo == NULL
					|| driveInfo->magic != VIRTUAL_DRIVE_MAGIC
					|| driveInfo->drive_info_size != sizeof(virtual_drive_info))
					return B_BAD_VALUE;

				strcpy(driveInfo->file_name, info.file);
				strcpy(driveInfo->device_name, "/dev/");
				strcat(driveInfo->device_name, info.device_path);
				driveInfo->geometry = info.geometry;
				driveInfo->use_geometry = true;
				driveInfo->halted = info.fd == -1;
				return B_OK;
			}

			default:
				TRACE(("virtualdrive: unknown ioctl: %lx (%lu)\n", op, op));
				return B_BAD_VALUE;
		}
	}

}


static status_t
virtualdrive_free(void *cookie)
{
	TRACE(("virtualdrive: free cookie()\n"));
	return B_OK;
}


/* -----
	function pointers for the device hooks entry points
----- */

device_hooks sVirtualDriveHooks = {
	virtualdrive_open, 			/* -> open entry point */
	virtualdrive_close, 		/* -> close entry point */
	virtualdrive_free,			/* -> free cookie */
	virtualdrive_control, 		/* -> control entry point */
	virtualdrive_read,			/* -> read entry point */
	virtualdrive_write			/* -> write entry point */
};

