/*
 * This file is derived from dos_mscdex.cpp which is
 *  Copyright (C) 2002-2005  The DOSBox Team
 *
 * All modifications in this file to the original code are
 * (C) Copyright 1992, ..., 2014 the "DOSEMU-Development-Team".
 *
 * for details see file COPYING in the DOSEMU distribution
 */

#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <limits.h>
#include <sys/types.h>
#include "emu.h"
#include "int.h"
#include "cpu.h"
#include "mfs.h"
#include "mangle.h"
#include "utilities.h"
#include "dos2linux.h"

#define CALC_PTR(PTR,OFFSET,RESULT_TYPE) ((RESULT_TYPE *)(PTR+OFFSET))

#define MSCD_READ_ADRESSING       13
#define MSCD_READ_STARTSECTOR     20
#define MSCD_READ_NUMSECTORS      18

#define MSCDEX_VERSION_HIGH		2
#define MSCDEX_VERSION_LOW		23

#define MSCDEX_ERROR_BAD_FORMAT		11
#define MSCDEX_ERROR_UNKNOWN_DRIVE	15
#define MSCDEX_ERROR_DRIVE_NOT_READY	21

#define CD_FRAMESIZE       2048

static int numDrives = 0;
static int cd_drives[4] = { -1, -1, -1, -1 };	/* drive letter (A=0) for each CDROM */

static int GetDriver(int drive)
{
	int i = 0;
	for (i = 0; i < 4; i++)
		if (cd_drives[i] == drive)
			break;
	return i;
}

void register_cdrom(int drive, int device)
{
	device--;
	if (device < 0 || device >= 4)
		return;
	if (cd_drives[device] == -1)
		numDrives++;
	cd_drives[device] = drive;
}

void unregister_cdrom(int drive)
{
	int device = GetDriver(drive);
	if (device >= 4)
		return;
	cd_drives[device] = -1;
	numDrives--;
}

static int ReadSectors(int drive, Bit32u sector, Bit16u num, unsigned char *buf,
		       unsigned int dosbuf)
{
	unsigned char req_buf[24];
	unsigned save_ax = _AX;
	int err;
	Bit8u driver = GetDriver(drive);
	if (driver >= 4)
		return MSCDEX_ERROR_UNKNOWN_DRIVE;
	*CALC_PTR(req_buf, MSCD_READ_STARTSECTOR, u_int) = sector;
	*CALC_PTR(req_buf, MSCD_READ_ADRESSING, u_char) = 0;
	*CALC_PTR(req_buf, MSCD_READ_NUMSECTORS, u_short) = num;
	_AH = 2 | (driver << 6);
	cdrom_helper(req_buf, buf, dosbuf);
	err = _AL == 0 ? 0 : MSCDEX_ERROR_DRIVE_NOT_READY;
	_AX = save_ax;
	return err;
}

static int ReadVTOC(int drive, Bit16u volume, unsigned char *buf, unsigned int dosbuf)
{
	char id[5];
	Bit8u type;
	int err = ReadSectors(drive, 16 + volume, 1, buf, dosbuf);
	if (err)
		return err;
	if (buf) {
		if (memcmp("CD001", buf + 1, 5) != 0)
			return MSCDEX_ERROR_BAD_FORMAT;
		type = *buf;
	} else {
		MEMCPY_2UNIX(id, dosbuf + 1, 5);
		if (memcmp("CD001", id, 5) != 0)
			return MSCDEX_ERROR_BAD_FORMAT;
		type = READ_BYTE(dosbuf);
	}
	return (type == 1) ? 1 : (type == 0xFF) ? 0xFF : 0;
}

int get_volume_label_cdrom(int drive, char *buf)
{
	char *p;
	unsigned char readbuf[CD_FRAMESIZE];
	if (ReadVTOC(drive, 0x00, readbuf, 0))
		return 0;
	memcpy(buf, readbuf + 40, 31);
	buf[31] = '\0';
	p = strchr(buf, '\0');
	while (--p >= buf && isspace((unsigned char)*p)) ;
	p[1] = '\0';
	return 1;
}

static int fill_buffer(int copyFlag, Bit32u buffer, unsigned char *readBuf,
		       unsigned entryLength)
{
	// TO DO : name gefunden, Daten in den Buffer kopieren
	if (copyFlag) {
		Bit8u writeBuf[256];
		C_printf
		    ("MSCDEX: GetDirEntry: Copyflag structure not entirely "
		     "accurate maybe\n");
		if (entryLength > 256)
			return MSCDEX_ERROR_BAD_FORMAT;
		// 00h  BYTE    length of XAR in Logical Block Numbers
		writeBuf[0] = readBuf[1];
		// 01h  DWORD   Logical Block Number of file start
		memcpy(&writeBuf[1], &readBuf[0x2], 4);
		// 05h  WORD    size of disk in logical blocks
		writeBuf[5] = 0;
		writeBuf[6] = 8;
		// 07h  DWORD   file length in bytes
		memcpy(&writeBuf[7], &readBuf[0xa], 4);
		// 0bh  DWORD   date and time
		memcpy(&writeBuf[0xb], &readBuf[0x12], 7);
		// 12h  BYTE    bit flags
		writeBuf[0x12] = readBuf[0x19];
		// 13h  BYTE    interleave size
		writeBuf[0x13] = readBuf[0x1a];
		// 14h  BYTE    interleave skip factor
		writeBuf[0x14] = readBuf[0x1b];
		// 15h  WORD    volume set sequence number
		memcpy(&writeBuf[0x15], &readBuf[0x1c], 2);
		writeBuf[0x17] = readBuf[0x20];
		memcpy(&writeBuf[0x18], &readBuf[21],
		       readBuf[0x20] <= 38 ? readBuf[0x20] : 38);
		MEMCPY_2DOS(buffer, writeBuf, 0x18 + 40);
	} else {
		// Direct copy
		MEMCPY_2DOS(buffer, readBuf, entryLength);
	}
	return 1;		/* ISO 9660 */
}

static int namecomp(const unsigned char *name1, size_t len1,
		    const char *name2, size_t len2)
{
	if (len1 > 1 && name1[len1 - 1] == '.')
		len1--;
	C_printf("MSCDEX: %zu %.*s\n", len1, (int)len1, name1);
	if (len1 == len2 && memcmp(name1, name2, len1) == 0) {
		C_printf("MSCDEX: Get DirEntry : Found : %s\n", name1);
		return 1;
	}
	return 0;
}

static int GetDirectoryEntry(int drive, int copyFlag, Bit32u pathname,
			     Bit32u buffer)
{
	char searchName[256];
	int err, dirSize;
	unsigned char defBuffer[CD_FRAMESIZE];
	unsigned dirEntrySector, entryLength, index;
	char *searchPos = searchName;
	size_t searchlen;

	/* skip initial \ */
	MEMCPY_2UNIX(searchName, pathname + 1, 255);
	searchName[255] = '\0';
	strupperDOS(searchName);

	//strip of tailing . (XCOM APOCALIPSE)
	searchlen = strlen(searchName);
	if (searchlen > 1 && strcmp(searchName, ".."))
		if (searchName[searchlen - 1] == '.')
			searchName[searchlen - 1] = 0;

	C_printf("MSCDEX: Get DirEntry : Find : %s\n", searchName);
	// read vtoc
	err = ReadSectors(drive, 16, 1, defBuffer, 0);
	if (err)
		return err;
	// TODO: has to be iso 9660
	if (memcmp("CD001", defBuffer + 1, 5) != 0)
		return MSCDEX_ERROR_BAD_FORMAT;
	// get directory position
	dirEntrySector = *(unsigned *)(defBuffer + 156 + 2);
	dirSize = *(int *)(defBuffer + 156 + 10);
	do {
		// Get string part
		char *useName = searchPos;
		size_t useNameLength;

		searchPos = strchr(searchPos, '\\');

		if (searchPos) {
			useNameLength = searchPos - useName;
			searchPos++;
		} else
			useNameLength = strlen(useName);

		while (1) {
			err =
			    ReadSectors(drive, dirEntrySector, 1, defBuffer, 0);
			if (err)
				return err;
			index = 0;

			do {
				unsigned char *entryName, *longername;
				unsigned nameLength;

				entryLength = defBuffer[index];
				C_printf("MSCDEX: entryLength=%u, index=%d\n",
					 entryLength, index);
				if (entryLength == 0)
					break;
				nameLength = defBuffer[index + 32];
				entryName = defBuffer + index + 33;
				if (namecomp
				    (entryName, nameLength, useName,
				     useNameLength))
					break;
				/* Xcom Apocalipse searches for MUSIC.
				 * and expects to find MUSIC;1
				 * All Files on the CDROM are of the kind
				 * blah;1
				 */
				longername = memchr(entryName, ';', nameLength);
				if (longername &&
				    namecomp(entryName, longername - entryName,
					     useName, useNameLength))
					break;
				index += entryLength;
			} while (index + 33 <= CD_FRAMESIZE);
			if (index + 33 <= CD_FRAMESIZE)
				break;
			// continue search in next sector
			dirSize -= 2048;
			if (dirSize <= 0)
				return 2;	// file not found
			dirEntrySector++;
		}
		// directory wechseln
		dirEntrySector = *(unsigned *)(defBuffer + index + 2);
		dirSize = *(unsigned *)(defBuffer + index + 10);
	} while (searchPos);
	return fill_buffer(copyFlag, buffer, defBuffer + index, entryLength);
};

int mscdex(void)
{
	dosaddr_t buf = SEGOFF2LINEAR(_ES, _BX);
	dosaddr_t dev;
	unsigned strat, intr;
	int err;
	int i;
	char devname[] = "MSCD0001";

	if (numDrives == 0)
		return 0;

	switch (_AL) {
	case 0x00:		/* install check */
	{
		int firstdrive = -1;
		int cnt = 0;
		for (i = 0; i < 4; i++) {
			if (cd_drives[i] == -1)
				continue;
			devname[7] = i + '1';
			if (!is_dos_device(devname))
				continue;
			cnt++;
			if (cd_drives[i] < firstdrive || firstdrive == -1)
				firstdrive = cd_drives[i];
		}
		_BX = cnt;
		_CX = firstdrive;
		break;
	}
	case 0x01:		/* driver info */
		if (!buf) {
			CARRY;
			break;
		}
		for (i = 0; i < 4; i++) {
			if (cd_drives[i] == -1)
				continue;
			devname[7] = i + '1';
			dev = is_dos_device(devname);
			if (!dev)
				continue;
			/* subunit: always 0 for cdrom.sys */
			WRITE_BYTE(buf, 0x00);
			WRITE_DWORD(buf + 1, dev);
			buf += 5;
		}
		break;
	case 0x02:		/* copyright file name */
	case 0x03:		/* abstract file name */
	case 0x04:		/* documentation file name */
		{
			unsigned char readbuf[CD_FRAMESIZE];
			if (!buf) {
				CARRY;
				break;
			}
			if (ReadVTOC(_CX, 0x00, readbuf, 0) == 0) {
				MEMCPY_2DOS(buf, readbuf + 702 + (_AL - 2) * 37,
					    37);
				WRITE_BYTE(buf + 37, 0);
				NOCARRY;
			} else {
				_AX = MSCDEX_ERROR_UNKNOWN_DRIVE;
				CARRY;
			}
			break;
		}
	case 0x05:		/* read vtoc */
		if (!buf) {
			CARRY;
			break;
		}
		NOCARRY;
		err = ReadVTOC(_CX, _DX, NULL, buf);
		if (err) {
			_AL = err;
			CARRY;
		};
		break;
	case 0x08:		/* read sectors */
		if (!buf) {
			CARRY;
			break;
		}
		NOCARRY;
		err = ReadSectors(_CX, (_SI << 16) + _DI, _DX, NULL, buf);
		if (err) {
			_AL = err;
			CARRY;
		};
		break;
	case 0x09:		/* write sectors - not supported */
		_AL = MSCDEX_ERROR_DRIVE_NOT_READY;
		CARRY;
		break;
	case 0x0B:		/* CD-ROM drive check */
		_AX = 0;
		for (i = 0; i < 4; i++)
			if (_CX == cd_drives[i]) {
				_AX = 1;
				break;
			}
		_BX = 0xadad;
		break;
	case 0x0C:
		_BX = (MSCDEX_VERSION_HIGH << 8) + MSCDEX_VERSION_LOW;
		break;
	case 0x0D:		/* get drives */
		if (!buf) {
			CARRY;
			break;
		}
		for (i = 0; i < 4; i++)
			if (cd_drives[i] != -1)
				WRITE_BYTE(buf++, cd_drives[i]);
		break;
	case 0x0F:		/* Get directory entry */
		if (!buf) {
			CARRY;
			break;
		}
		CARRY;
		_AX =
		    GetDirectoryEntry(_CL, _CH & 1, buf,
				      SEGOFF2LINEAR(_SI, _DI));
		if (_AX == 0 || _AX == 1)
			NOCARRY;
		break;
	case 0x10:
		{
			uint16_t seg;
			int driver = GetDriver(_CX);
			if (!buf) {
				_AX = 0xf;
				CARRY;
				break;
			}
			CARRY;
			if (driver >= 4) {
#if 0
				_AX = 0xf;
#else
				/* Ultimate Domain game puts junk into CH,
				 * which leads to a wrong drive. We can
				 * either clean CH ourselves or not return
				 * an error here. */
				NOCARRY;
#endif
				break;
			}
			devname[7] = driver + '1';
			dev = is_dos_device(devname);
			if (!dev) {
				_AX = 0xf;
				break;
			}
			seg = dev >> 16;
			dev = SEGOFF2LINEAR(seg, dev & 0xffff);
			WRITE_BYTE(buf + 1, driver);  // set SubUnit
			/* call driver with req buf in ES:BX */
			strat = READ_WORD(dev + 6);
			intr = READ_WORD(dev + 8);
			fake_call_to(seg, intr);
			fake_call_to(seg, strat);
			NOCARRY;
			break;
		}
	default:
		C_printf("unknown mscdex\n");
		return 0;
	}
	return 1;
}
