/* ----------------------------------------------------------------------- *
 *
 *   Copyright 2009 Pierre-Alexandre Meyer
 *
 *   Some parts borrowed from chain.c32:
 *
 *   Copyright 2003-2009 H. Peter Anvin - All Rights Reserved
 *   Copyright 2009 Intel Corporation; author: H. Peter Anvin
 *
 *   This file is part of Syslinux, and is made available under
 *   the terms of the GNU General Public License version 2.
 *
 * ----------------------------------------------------------------------- */

#include <com32.h>
#include <string.h>
#include <stdio.h>
#include <disk/geom.h>

#include <stdio.h>

/**
 * lba_to_chs - split given lba into cylinders/heads/sectors
 **/
void lba_to_chs(const struct driveinfo* drive_info, const int lba,
		unsigned int* cylinder, unsigned int* head,
		unsigned int* sector)
{
	unsigned int track;

	/* Use EDD, if valid */
	if (drive_info->edd_params.sectors_per_track > 0 &&
	    drive_info->edd_params.heads > 0) {
		*cylinder = (lba % drive_info->edd_params.sectors_per_track) + 1;
		track = lba / drive_info->edd_params.sectors_per_track;
		*head = track % drive_info->edd_params.heads;
		*sector = track / drive_info->edd_params.heads;
	} else if (drive_info->cbios) {
		*cylinder = (lba % drive_info->legacy_sectors_per_track) + 1;
		track = lba / drive_info->legacy_sectors_per_track;
		*head = track % (drive_info->legacy_max_head + 1);
		*sector = track / (drive_info->legacy_max_head + 1);
	}
}

/**
 * detect_extensions - detect if we can use extensions
 *
 * INT 13 - IBM/MS INT 13 Extensions - INSTALLATION CHECK
 *    AH = 41h
 *    BX = 55AAh
 *    DL = drive (80h-FFh)
 *
 * Return: CF set on error (extensions not supported)
 *    AH = 01h (invalid function)
 *    CF clear if successful
 *    BX = AA55h if installed
 *    AH = major version of extensions
 *        01h = 1.x
 *        20h = 2.0 / EDD-1.0
 *        21h = 2.1 / EDD-1.1
 *        30h = EDD-3.0
 *    AL = internal use
 *    CX = API subset support bitmap (see #00271)
 *    DH = extension version (v2.0+ ??? -- not present in 1.x)
 *
 * Note: the Phoenix Enhanced Disk Drive Specification v1.0 uses version 2.0 of
 *       the INT 13 Extensions API
 *
 * Bitfields for IBM/MS INT 13 Extensions API support bitmap:
 * Bit(s)    Description    (Table 00271)
 *     0 extended disk access functions (AH=42h-44h,47h,48h) supported
 *     1 removable drive controller functions (AH=45h,46h,48h,49h,INT 15/AH=52h)
 *       supported
 *     2 enhanced disk drive (EDD) functions (AH=48h,AH=4Eh) supported
 *       extended drive parameter table is valid (see #00273,#00278)
 *     3-15    reserved (0)
 **/
static int detect_extensions(struct driveinfo* drive_info)
{
	com32sys_t getebios, ebios;

	memset(&getebios, 0, sizeof getebios);
	memset(&ebios, 0, sizeof ebios);

	getebios.eflags.b[0] = 0x3;	/* CF set */
	getebios.ebx.w[0] = 0x55aa;
	getebios.edx.b[0] = drive_info->disk;
	getebios.eax.b[1] = 0x41;

	__intcall(0x13, &getebios, &ebios);

	if ( !(ebios.eflags.l & EFLAGS_CF) &&
	     ebios.ebx.w[0] == 0xaa55 ) {
		drive_info->ebios = 1;
		drive_info->edd_version = ebios.eax.b[1];
		drive_info->edd_functionality_subset = ebios.ecx.w[0];
		return 0;
	} else
		return -1; /* Drive does not exist? */
}

/**
 * get_drive_parameters_with_extensions - retrieve disk parameters via AH=48h
 *
 * INT 13 - IBM/MS INT 13 Extensions - GET DRIVE PARAMETERS
 *     AH = 48h
 *     DL = drive (80h-FFh)
 *     DS:SI -> buffer for drive parameters
 * Return: CF clear if successful
 *     AH = 00h
 *     DS:SI buffer filled
 *     CF set on error
 *     AH = error code (see #00234)
 * BUG: several different Compaq BIOSes incorrectly report high-numbered
 *     drives (such as 90h, B0h, D0h, and F0h) as present, giving them the
 *     same geometry as drive 80h; as a workaround, scan through disk
 *     numbers, stopping as soon as the number of valid drives encountered
 *     equals the value in 0040h:0075h
 **/
static int get_drive_parameters_with_extensions(struct driveinfo* drive_info)
{
	com32sys_t inreg, outreg;
	struct edd_device_parameters *dp = __com32.cs_bounce;

	memset(&inreg, 0, sizeof inreg);

	/*
	 * The caller shall set this value to the maximum Result Buffer
	 * length, in bytes. If the length of this buffer is less than 30
	 * bytes, this function shall not return the pointer to Drive Parameter
	 * Table (DPT) extension. If the buffer length is 30 or greater on
	 * entry, it shall be set to 30 on exit. If the buffer length is
	 * between 26 and 29, it shall be set to 26 on exit.
	 * If the buffer length is less than 26 on entry an error shall be
	 * returned.
	 */
	dp->len = sizeof(struct edd_device_parameters);

	inreg.esi.w[0] = OFFS(dp);
	inreg.ds = SEG(dp);
	inreg.edx.b[0] = drive_info->disk;
	inreg.eax.b[1] = 0x48;

	__intcall(0x13, &inreg, &outreg);

	/* CF set on error */
	if ( outreg.eflags.l & EFLAGS_CF )
		return outreg.eax.b[1];

	memcpy(&drive_info->edd_params, dp, sizeof drive_info->edd_params);

	return 0;
}

/**
 * get_drive_parameters_without_extensions - retrieve drive parameters via AH=08h
 *
 * INT 13 - DISK - GET DRIVE PARAMETERS (PC,XT286,CONV,PS,ESDI,SCSI)
 *     AH = 08h
 *     DL = drive (bit 7 set for hard disk)
 *
 * Return: CF set on error
 *     AH = status (07h) (see #00234)
 *     CF clear if successful
 *     AH = 00h
 *     AL = 00h on at least some BIOSes
 *     BL = drive type (AT/PS2 floppies only) (see #00242)
 *     CH = low eight bits of maximum cylinder number
 *     CL = maximum sector number (bits 5-0)
 *          high two bits of maximum cylinder number (bits 7-6)
 *     DH = maximum head number
 *     DL = number of drives
 *     ES:DI -> drive parameter table (floppies only)
 *
 * Notes:
 *   - may return successful even though specified drive is greater than the
 *     number of attached drives of that type (floppy/hard); check DL to
 *     ensure validity
 *   - for systems predating the IBM AT, this call is only valid for hard
 *     disks, as it is implemented by the hard disk BIOS rather than the
 *     ROM BIOS
 *   - Toshiba laptops with HardRAM return DL=02h when called with DL=80h,
 *     but fail on DL=81h. The BIOS data at 40h:75h correctly reports 01h.
 *     may indicate only two drives present even if more are attached; to
 *     ensure a correct count, one can use AH=15h to scan through possible
 *     drives
 *   - for BIOSes which reserve the last cylinder for testing purposes, the
 *     cylinder count is automatically decremented
 *     on PS/1s with IBM ROM DOS 4, nonexistent drives return CF clear,
 *     BX=CX=0000h, and ES:DI = 0000h:0000h
 *   - the PC-Tools PCFORMAT program requires that AL=00h before it will
 *     proceed with the formatting
 *
 * BUG: several different Compaq BIOSes incorrectly report high-numbered
 *      drives (such as 90h, B0h, D0h, and F0h) as present, giving them the
 *      same geometry as drive 80h; as a workaround, scan through disk
 *      numbers, stopping as soon as the number of valid drives encountered
 *      equals the value in 0040h:0075h
 *
 * SeeAlso: AH=06h"Adaptec",AH=13h"SyQuest",AH=48h,AH=15h,INT 1E
 * SeeAlso: INT 41"HARD DISK 0"
 **/
static int get_drive_parameters_without_extensions(struct driveinfo* drive_info)
{
	com32sys_t getparm, parm;

	memset(&getparm, 0, sizeof getparm);
	memset(&parm, 0, sizeof parm);

	/* Ralf Brown recommends setting ES:DI to 0:0 */
	getparm.esi.w[0] = 0;
	getparm.ds = 0;
	getparm.edx.b[0] = drive_info->disk;
	getparm.eax.b[1] = 0x08;

	__intcall(0x13, &getparm, &parm);

	/* CF set on error */
	if ( parm.eflags.l & EFLAGS_CF )
		return parm.eax.b[1];

	/* DL contains the maximum drive number (it starts at 0) */
	drive_info->legacy_max_drive = parm.edx.b[0];

	// XXX broken
	/* Drive specified greater than the bumber of attached drives */
	//if (drive_info->disk > drive_info->drives)
	//	return -1;

	drive_info->legacy_type = parm.ebx.b[0];

	/* DH contains the maximum head number (it starts at 0) */
	drive_info->legacy_max_head = parm.edx.b[1];

	/* Maximum sector number (bits 5-0) per track */
	drive_info->legacy_sectors_per_track = parm.ecx.b[0] & 0x3f;

	/*
	 * Maximum cylinder number:
	 *     CH = low eight bits of maximum cylinder number
	 *     CL = high two bits of maximum cylinder number (bits 7-6)
	 */
	drive_info->legacy_max_cylinder = parm.ecx.b[1] +
			       ((parm.ecx.b[0] & 0xc0) << 2);

	if ( drive_info->legacy_sectors_per_track > 0 )
		drive_info->cbios = 1;	/* Valid geometry */

	return 0;
}

/**
 * get_drive_parameters - retrieve drive parameters
 * @drive_info:		driveinfo structure to fill
 **/
int get_drive_parameters(struct driveinfo *drive_info)
{
	int return_code;

	if (detect_extensions(drive_info))
		return -1;

	return_code = get_drive_parameters_without_extensions(drive_info);

	/* If geometry isn't valid, no need to try to get more info about the drive*/
	/* Looks like in can confuse some optical drives */
	if (drive_info->ebios && drive_info->cbios)
		get_drive_parameters_with_extensions(drive_info);

	return return_code;
}
