/*
 * Copyright (C) 2009 Juergen Beisert, Pengutronix
 *
 * Mostly stolen from the GRUB2 project
 *  Copyright (C) 1999,2000,2001,2002,2003,2004,2005,2006,2007,2008  Free Software Foundation, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 * MA 02111-1307 USA
 *
 */

/**
 * @file
 * @brief Media communication layer through the standard 16 bit PC-BIOS
 *
 * This communication driver does all accesses to the boot medium via 16 bit
 * real mode calls into the standard BIOS. Due to this method, its possible
 * to use all the medias to boot from that are supported by the BIOS. This
 * also includes emulated only medias.
 *
 * To be able to call the real mode BIOS, this driver must switch back to
 * real mode for each access. This will slow down the access a little bit, but
 * we are a boot loader here, not an operating system...
 *
 * Note: We need scratch memory for the BIOS communication, because the BIOS
 * can only handle memory below 0xA0000. So we must copy all data between
 * the flat mode buffers and realmode buffers.
 *
 * Note: This driver makes no sense on other architectures than x86.
 *
 * Note: This driver does only support LBA addressing. Currently no CHS!
 */

#include <stdio.h>
#include <linux/types.h>
#include <init.h>
#include <driver.h>
#include <string.h>
#include <xfuncs.h>
#include <asm/syslib.h>
#include <ata.h>
#include <errno.h>

/**
 * Sector count handled in one count
 *
 * @todo 127 are always possible, some BIOS manufacturer supports up to 255.
 * Is it's worth to detect Phoenic's restriction?
 */
#define SECTORS_AT_ONCE 64

/** Size of one sector in bytes */
#define SECTOR_SIZE 512

/** Command to read sectors from media */
#define BIOS_READ_CMD 0

/** Command to write sectors to media */
#define BIOS_WRT_CMD 1

/**
 * "Disk Address Packet Structure" to be used when calling
 * BIOS's int13, function 0x42/0x43
 */
struct DAPS
{
	uint8_t size;		/**< always '16' */
	uint8_t res1;		/**< always '0' */
	int8_t count;		/**< number of sectors 0...127 */
	uint8_t res2;		/**< always '0' */
	uint16_t offset;	/**< buffer address: offset */
	uint16_t segment;	/**< buffer address: segment */
	uint64_t lba;		/**< LBA of the start sector */
} __attribute__ ((packed));

/**
 * Collection of data we need to know about the connected drive
 */
struct media_access {
	int drive_no;	/**< drive number used by the BIOS */
	int is_cdrom;	/**< drive is a CDROM e.g. no write support */
};

/**
 * Scratch memory for BIOS communication to handle data in chunks of 32 kiB
 *
 * Note: This variable is located in the .bss segment, assuming it is located
 * below 0xA0000. If not, the BIOS is not able to read or store any data
 * from/to it. The variable must also aligned to a 16 byte boundary to easify
 * linear to segment:offset address conversion.
 */
static uint8_t scratch_buffer[SECTORS_AT_ONCE * SECTOR_SIZE] __attribute__((aligned(16)));

/**
 * Communication buffer for the 16 bit int13 BIOS call
 *
 * Note: This variable is located in the .bss segment, assuming it is located
 * below 0xA0000. If not, the BIOS is not able to read or store any data
 * from/to it. The variable must also aligned to a 16 byte boundary to easify
 * linear to segment:offset conversion.
 */
static struct DAPS bios_daps __attribute__((aligned(16)));

/**
 * @param media our data we need to do the access
 * @param cmd Command to forward to the BIOS
 * @param sector_start LBA of the start sector
 * @param sector_count Sector count
 * @param buffer Buffer to read from or write to (in the low memory area)
 * @return 0 on success, anything else on failure
 */
static int biosdisk_bios_call(struct media_access *media, int cmd, uint64_t sector_start, unsigned sector_count, void *buffer)
{
	int rc;

	/* prepare the DAPS for the int13 call */
	bios_daps.size = sizeof(struct DAPS);
	bios_daps.res1 = 0;
	bios_daps.count = sector_count;	/* always less than 128! */
	bios_daps.res2 = 0;
	bios_daps.segment = (unsigned long)buffer >> 4;
	bios_daps.offset = (unsigned long)buffer - (unsigned long)(bios_daps.segment << 4);
	bios_daps.lba = sector_start;

	if (cmd == BIOS_READ_CMD)
		rc = bios_disk_rw_int13_extensions(0x42, media->drive_no, &bios_daps);
	else if (cmd == BIOS_WRT_CMD)
		rc = bios_disk_rw_int13_extensions(0x43, media->drive_no, &bios_daps);
	else
		return -1;

	return rc;
}

/**
 * Read a chunk of sectors from media
 * @param dev our data we need to do the access
 * @param sector_start Sector's LBA number to start read from
 * @param sector_count Sectors to read
 * @param buffer Buffer to read into
 * @return 0 on success, anything else on failure
 *
 * This routine expects the buffer has the correct size to store all data!
 */
static int biosdisk_read(struct device_d *dev, uint64_t sector_start, unsigned sector_count, void *buffer)
{
	int rc;
	struct ata_interface *intf = dev->platform_data;
	struct media_access *media = intf->priv;

	while (sector_count >= SECTORS_AT_ONCE) {
		rc = biosdisk_bios_call(media, BIOS_READ_CMD, sector_start, SECTORS_AT_ONCE, scratch_buffer);
		if (rc != 0)
			return rc;
		__builtin_memcpy(buffer, scratch_buffer, sizeof(scratch_buffer));
		buffer += sizeof(scratch_buffer);
		sector_start += SECTORS_AT_ONCE;
		sector_count -= SECTORS_AT_ONCE;
	};

	/* Are sectors still remaining? */
	if (sector_count) {
		rc = biosdisk_bios_call(media, BIOS_READ_CMD, sector_start, sector_count, scratch_buffer);
		__builtin_memcpy(buffer, scratch_buffer, sector_count * SECTOR_SIZE);
	} else
		rc = 0;

	return rc;
}

/**
 * Write a chunk of sectors to media
 * @param dev our data we need to do the access
 * @param sector_start Sector's LBA number to start write to
 * @param sector_count Sectors to write
 * @param buffer Buffer to write from
 * @return 0 on success, anything else on failure
 *
 * This routine expects the buffer has the correct size to read all data!
 */
static int biosdisk_write(struct device_d *dev, uint64_t sector_start, unsigned sector_count, const void *buffer)
{
	int rc;
	struct ata_interface *intf = dev->platform_data;
	struct media_access *media = intf->priv;

	while (sector_count >= SECTORS_AT_ONCE) {
		__builtin_memcpy(scratch_buffer, buffer, sizeof(scratch_buffer));
		rc = biosdisk_bios_call(media, BIOS_WRT_CMD, sector_start, SECTORS_AT_ONCE, scratch_buffer);
		if (rc != 0)
			return rc;
		buffer += sizeof(scratch_buffer);
		sector_start += SECTORS_AT_ONCE;
		sector_count -= SECTORS_AT_ONCE;
	};

	/* Are sectors still remaining? */
	if (sector_count) {
		__builtin_memcpy(scratch_buffer, buffer, sector_count * SECTOR_SIZE);
		rc = biosdisk_bios_call(media, BIOS_WRT_CMD, sector_start, sector_count, scratch_buffer);
	} else
		rc = 0;

	return rc;
}

/**
 * Probe for connected drives and register them
 *
 * Detecting if a drive is present is done by simply reading its MBR.
 *
 * FIXME: Relation between BIOS disk numbering scheme and our representation
 * here in barebox (and later on in the linux kernel)
 */
static int biosdisk_probe(struct device_d *dev)
{
	int drive, rc;
	struct media_access media, *m;
	struct device_d *drive_dev;
	struct ata_interface *p;

	for (drive = 0x80; drive < 0x90; drive++) {
		media.drive_no = drive;
		media.is_cdrom = 0;	/* don't know yet */
		rc = biosdisk_bios_call(&media, BIOS_READ_CMD, 0, 1, scratch_buffer);
		if (rc != 0)
			continue;

		printf("BIOSdrive %d seems valid. Registering...\n", media.drive_no);

		drive_dev = xzalloc(sizeof(struct device_d) + sizeof(struct media_access) + sizeof(struct ata_interface));
		if (drive_dev == NULL) {
			dev_err(dev, "Out of memory\n");
			return -1;
		}
		m = (struct media_access*)&drive_dev[1];
		p = (struct ata_interface*)&m[1];

		m->drive_no = drive;
		m->is_cdrom = 0;

		p->write = biosdisk_write;
		p->read = biosdisk_read;
		p->priv = m;

		strcpy(drive_dev->name, "biosdisk");
		drive_dev->id = drive - 0x80;
		drive_dev->resource[0].start = 0;
		drive_dev->platform_data = p;

		register_device(drive_dev);
	}

	return 0;
}

static struct driver_d biosdisk_driver = {
        .name   = "biosdrive",
        .probe  = biosdisk_probe,
};

static int biosdisk_init(void)
{
	/* sanity */
	if (scratch_buffer > (uint8_t*)0x9FFFF) {
		printf("BIOS driver: Scratch memory not in real mode area. Cannot continue!\n");
		return -EIO;
	}
	if (&bios_daps > (struct DAPS*)0x9FFFF) {
		printf("BIOS driver: DAPS memory not in real mode area. Cannot continue!\n");
		return -EIO;
	}

	register_driver(&biosdisk_driver);
	return 0;
}

device_initcall(biosdisk_init);
