#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <stdbool.h>
#include <fcntl.h>
#include <time.h>
#include <ios>
#include <fstream>
#include <iostream>
#include <string>
#include <sstream>
#include <sys/stat.h>
#include <libchdr/chd.h>

#include "spi.h"
#include "user_io.h"
#include "file_io.h"
#include "hardware.h"
#include "cd.h"
#include "ide.h"

#if 0
	#define dbg_printf     printf
	#define dbg_print_regs ide_print_regs
	#define dbg_hexdump    hexdump
#else
	#define dbg_printf(...)   void()
	#define dbg_print_regs    void
	#define dbg_hexdump(...)  void()
#endif

#define ide_send_data(databuf, size) ide_sendbuf(ide, 255, (size), (uint16_t*)(databuf))
#define ide_recv_data(databuf, size) ide_recvbuf(ide, 255, (size), (uint16_t*)(databuf))
#define ide_reset_buf() ide_reg_set(ide, 7, 0)

#define BYTES_PER_RAW_REDBOOK_FRAME    2352
#define BYTES_PER_COOKED_REDBOOK_FRAME 2048
#define REDBOOK_FRAMES_PER_SECOND      75
#define REDBOOK_FRAME_PADDING          150

#define CD_FPS 75
#define MSF_TO_FRAMES(M, S, F) ((M)*60*CD_FPS+(S)*CD_FPS+(F))

#define CD_ERR_NO_DISK ((2 << 4) | ATA_ERR_ABRT)


typedef struct
{
	unsigned char   min;
	unsigned char   sec;
	unsigned char   fr;
} TMSF;

static int check_magic(fileTYPE *file, int sectorSize, int mode2)
{
	// Initialize our array in the event file->read() doesn't fully write it
	static uint8_t pvd[BYTES_PER_COOKED_REDBOOK_FRAME];
	memset(pvd, 0, sizeof(pvd));

	uint32_t seek = 16 * sectorSize;  // first vd is located at sector 16
	if (sectorSize == BYTES_PER_RAW_REDBOOK_FRAME && !mode2) seek += 16;
	if (mode2) seek += 24;
	FileSeek(file, seek, SEEK_SET);
	if (!FileReadAdv(file, pvd, BYTES_PER_COOKED_REDBOOK_FRAME)) return 0;

	// pvd[0] = descriptor type, pvd[1..5] = standard identifier,
	// pvd[6] = iso version (+8 for High Sierra)
	return ((pvd[0] == 1 && !strncmp((char*)(&pvd[1]), "CD001", 5) && pvd[6] == 1) ||
			(pvd[8] == 1 && !strncmp((char*)(&pvd[9]), "CDROM", 5) && pvd[14] == 1));
}

static int check_iso_file(fileTYPE *f, uint8_t *mode2, uint16_t *sectorSize)
{
	if (check_magic(f, BYTES_PER_COOKED_REDBOOK_FRAME, false))
	{
		if (sectorSize) *sectorSize = BYTES_PER_COOKED_REDBOOK_FRAME;
		if (mode2) *mode2 = 0;
		return 1;
	}
	else if (check_magic(f, BYTES_PER_RAW_REDBOOK_FRAME, false))
	{
		if (sectorSize) *sectorSize = BYTES_PER_RAW_REDBOOK_FRAME;
		if (mode2) *mode2 = 0;
		return 1;
	}
	else if (check_magic(f, 2336, true))
	{
		if (sectorSize) *sectorSize = 2336;
		if (mode2) *mode2 = 1;
		return 1;
	}
	else if (check_magic(f, BYTES_PER_RAW_REDBOOK_FRAME, true))
	{
		if (sectorSize) *sectorSize = BYTES_PER_RAW_REDBOOK_FRAME;
		if (mode2) *mode2 = 1;
		return 1;
	}

	if (sectorSize) *sectorSize = 0;
	if (mode2) *mode2 = 0;
	return 0;
}

static const char * load_iso_file(drive_t *drv, const char* filename)
{
	fileTYPE f;
	memset(drv->track, 0, sizeof(drv->track));
	drv->track_cnt = 0;

	strcpy(drv->track[0].filename, filename);
	if (!FileOpen(&f, filename))
	{
		printf("Cannot open ISO file!\n");
		return 0;
	}

	if(!check_iso_file(&f, &drv->track[0].mode2, &drv->track[0].sectorSize))
	{
		printf("Fail to parse ISO!\n");
		FileClose(&f);
		return 0;
	}

	drv->track[0].attr = 0x40; //data track
	drv->track[0].length = f.size / drv->track[0].sectorSize;
	drv->track[0].number = 1;

	FileClose(&f);

	// lead-out track (track 2)
	drv->track[1].start = drv->track[0].length;
	drv->track[2].number = 2;

	printf("ISO: mode2 = %d, sectorSize = %d, sectors = %d\n", drv->track[0].mode2, drv->track[0].sectorSize, drv->track[0].length);
	drv->track_cnt = 2;

	drv->data_num = 0;
	return drv->track[0].filename;
}

static int get_word(std::string &keyword, std::istream &in)
{
	in >> keyword;
	for (uint32_t i = 0; i < keyword.size(); i++) keyword[i] = (char)toupper(keyword[i]);
	return keyword.size();
}

static int get_timecode(uint32_t &frames, std::istream &in)
{
	std::string msf;
	in >> msf;
	TMSF tmp = { 0, 0, 0 };
	int success = sscanf(msf.c_str(), "%hhu:%hhu:%hhu", &tmp.min, &tmp.sec, &tmp.fr) == 3;
	frames = (int)MSF_TO_FRAMES(tmp.min, tmp.sec, tmp.fr);
	return success;
}

static int add_track(drive_t *drv, track_t *curr, uint32_t &shift, const int32_t prestart, uint32_t &totalPregap, uint32_t currPregap)
{
	uint32_t skip = 0;
	if (drv->track_cnt >= sizeof(drv->track) / sizeof(drv->track[0]))
	{
		printf("CDROM: too many tracks(%d)\n", drv->track_cnt);
		return 0;
	}

	// frames between index 0(prestart) and 1(curr.start) must be skipped
	if (prestart >= 0)
	{
		if (prestart > static_cast<int>(curr->start))
		{
			printf("CDROM: add_track => prestart %d cannot be > curr.start %u\n", prestart, curr->start);
			return 0;
		}
		skip = static_cast<uint32_t>(static_cast<int>(curr->start) - prestart);
	}

	// Add the first track, if our vector is empty
	if (!drv->track_cnt)
	{
		//assertm(curr.number == 1, "The first track must be labelled number 1 [BUG!]");
		curr->skip = skip * curr->sectorSize;
		curr->start += currPregap;
		totalPregap = currPregap;

		memcpy(&drv->track[drv->track_cnt], curr, sizeof(track_t));
		drv->track_cnt++;
		return 1;
	}

	// Guard against undefined behavior in subsequent tracks.back() call
	//assert(!tracks.empty());
	track_t *prev = &drv->track[drv->track_cnt - 1];

	// current track consumes data from the same file as the previous
	if (!strcmp(prev->filename, curr->filename))
	{
		curr->start += shift;
		if (!prev->length)
		{
			prev->length = curr->start + totalPregap - prev->start - skip;
		}
		curr->skip += prev->skip + prev->length * prev->sectorSize + skip * curr->sectorSize;
		totalPregap += currPregap;
		curr->start += totalPregap;
		// current track uses a different file as the previous track
	}
	else
	{
		uint32_t size = FileLoad(prev->filename, 0, 0);
		const uint32_t tmp = size - prev->skip;
		prev->length = tmp / prev->sectorSize;

		if (tmp % prev->sectorSize != 0) prev->length++; // padding

		curr->start += prev->start + prev->length + currPregap;
		curr->skip = skip * curr->sectorSize;
		shift += prev->start + prev->length;
		totalPregap = currPregap;
	}

	// error checks
	if (curr->number <= 1
		|| prev->number + 1 != curr->number
		|| curr->start < prev->start + prev->length) {
		printf("add_track: failed consistency checks\n"
			"\tcurr.number (%d) <= 1\n"
			"\tprev.number (%d) + 1 != curr.number (%d)\n"
			"\tcurr.start (%d) < prev.start (%d) + prev.length (%d)\n",
			curr->number, prev->number, curr->number,
			curr->start, prev->start, prev->length);
		return 0;
	}

	memcpy(&drv->track[drv->track_cnt], curr, sizeof(track_t));
	drv->track_cnt++;
	return 1;
}

static const char* load_chd_file(drive_t *drv, const char *chdfile)
{

	//Borrow the cd.h "toc_t" and mister_chd* parse function. Then translate the toc_t to drive_t+track_t.
	//TODO: abstract all the bin/cue+chd+iso parsing and reading into a shared class
	//

	const char *ext = chdfile+strlen(chdfile)-4;
	uint32_t total_sector_size = 0;


	if (strncasecmp(".chd", ext, 4))
	{
		//Not a CHD
		return 0;
	}
	toc_t tmpTOC = { };
	memset(drv->track, 0, sizeof(drv->track));
	drv->track_cnt = 0;
	chd_error err = mister_load_chd(chdfile, &tmpTOC);
	if (err != CHDERR_NONE)
	{
		return 0;
	}

	if (drv->chd_hunkbuf)
	{
		free(drv->chd_hunkbuf);
	}

	drv->chd_hunkbuf = (uint8_t *)malloc(CD_FRAME_SIZE * CD_FRAMES_PER_HUNK);
	drv->chd_hunknum = -1;
	drv->chd_f = tmpTOC.chd_f;

	//don't use add_track, just do it ourselves...
	for(int i = 0; i <= tmpTOC.last; i++)
	{
		cd_track_t *chd_track = &tmpTOC.tracks[i];
		track_t *trk = &drv->track[i];
		trk->number = i+1;
		trk->sectorSize = chd_track->sector_size;
		if (chd_track->type)
		{
			trk->attr = 0x40;
			if (chd_track->type == 2)
			{
				trk->mode2 = true;
			}

		}

		trk->chd_offset = chd_track->offset;
		trk->start = chd_track->start;
		trk->length = chd_track->end - chd_track->start;
		drv->track_cnt++;
		total_sector_size += trk->length * trk->sectorSize;
	}

	//Add the lead-out track

	track_t *lead_out = &drv->track[drv->track_cnt];
	lead_out->number = drv->track_cnt+1;
	lead_out->attr = 0;
	lead_out->start = tmpTOC.tracks[tmpTOC.last].end;
	lead_out->length = 0;

	drv->total_sectors = total_sector_size / 512;
	drv->chd_total_size = total_sector_size;

	for(uint8_t i = 0; i < drv->track_cnt; i++)
	{
		if (drv->track[i].attr == 0x40)
		{
			drv->data_num = i;
		}
	}

	return chdfile;
}


static const char* load_cue_file(drive_t *drv, const char *cuefile)
{
	memset(drv->track, 0, sizeof(drv->track));
	drv->track_cnt = 0;

	track_t track = {};
	uint32_t shift = 0;
	uint32_t currPregap = 0;
	uint32_t totalPregap = 0;
	int32_t prestart = -1;
	int track_number;
	int success;
	int canAddTrack = 0;

	std::string pathname(cuefile);
	std::size_t found = pathname.find_last_of('/');
	if (found == std::string::npos) return 0; // no folder name?
	pathname.resize(found + 1);

	std::ifstream in;
	in.open(cuefile, std::ios::in);
	if (in.fail()) return 0;

	while (!in.eof())
	{
		// get next line
		std::string buf;
		std::getline(in, buf);

		if (in.fail() && !in.eof()) return 0;  // probably a binary file

		std::istringstream line(buf);

		std::string command;
		get_word(command, line);

		//printf("command: %s\n", command.c_str());

		if (command == "TRACK")
		{
			if (canAddTrack) success = add_track(drv, &track, shift, prestart, totalPregap, currPregap);
			else success = 1;

			track.start = 0;
			track.skip = 0;
			currPregap = 0;
			prestart = -1;

			line >> track_number; // (cin) read into a true int first

			track.number = static_cast<uint8_t>(track_number);

			std::string type;
			get_word(type, line);

			//printf("  type: %s\n", type.c_str());

			if (type == "AUDIO")
			{
				track.sectorSize = BYTES_PER_RAW_REDBOOK_FRAME;
				track.attr = 0;
				track.mode2 = false;
			}
			else if (type == "MODE1/2048")
			{
				track.sectorSize = BYTES_PER_COOKED_REDBOOK_FRAME;
				track.attr = 0x40;
				track.mode2 = false;
			}
			else if (type == "MODE1/2352")
			{
				track.sectorSize = BYTES_PER_RAW_REDBOOK_FRAME;
				track.attr = 0x40;
				track.mode2 = false;
			}
			else if (type == "MODE2/2336")
			{
				track.sectorSize = 2336;
				track.attr = 0x40;
				track.mode2 = true;
			}
			else if (type == "MODE2/2352")
			{
				track.sectorSize = BYTES_PER_RAW_REDBOOK_FRAME;
				track.attr = 0x40;
				track.mode2 = true;
			}
			else success = 0;

			canAddTrack = 1;
		}
		else if (command == "INDEX")
		{
			int index;
			line >> index;
			uint32_t frame;
			success = get_timecode(frame, line);

			if (index == 1) track.start = frame;
			else if (index == 0) prestart = static_cast<int32_t>(frame);
			// ignore other indices
		}
		else if (command == "FILE")
		{
			if (canAddTrack) success = add_track(drv, &track, shift, prestart, totalPregap, currPregap);
			else success = 1;
			canAddTrack = 0;

			std::string filename;
			std::getline(std::getline(line, filename, '"'), filename, '"');

			strcpy(track.filename, pathname.c_str());
			strcat(track.filename, filename.c_str());

			printf("cue: got new file name: %s\n", track.filename);
		}
		else if (command == "PREGAP") success = get_timecode(currPregap, line);
		// ignored commands
		else if (command == "CATALOG" || command == "CDTEXTFILE" || command == "FLAGS" || command == "ISRC" ||
			command == "PERFORMER" || command == "POSTGAP" || command == "REM" ||
			command == "SONGWRITER" || command == "TITLE" || command.empty())
		{
			success = 1;
		}
		// failure
		else
		{
			success = 0;
		}

		if (!success)
		{
			return 0;
		}
	}

	// add last track
	if (!add_track(drv, &track, shift, prestart, totalPregap, currPregap))
	{
		return 0;
	}

	// add lead-out track
	track.number++;
	track.attr = 0;//sync with load iso
	track.start = FileLoad(track.filename, 0, 0) / track.sectorSize;
	track.length = 0;

	if (!add_track(drv, &track, shift, -1, totalPregap, 0))
	{
		return 0;
	}

	for (uint8_t i = 0; i < drv->track_cnt; i++)
	{
		if (drv->track[i].attr == 0x40)
		{
			drv->data_num = i;
			return drv->track[i].filename;
		}
	}

	return 0;
}

inline TMSF frames_to_msf(uint32_t frames)
{
	TMSF msf = { 0, 0, 0 };
	msf.fr = frames % REDBOOK_FRAMES_PER_SECOND;
	frames /= REDBOOK_FRAMES_PER_SECOND;
	msf.sec = frames % 60;
	frames /= 60;
	msf.min = static_cast<uint8_t>(frames);
	return msf;
}

static int get_tracks(drive_t *drv, int& start_track_num, int& end_track_num, TMSF& lead_out_msf)
{
	if (drv->track_cnt < 2 || !drv->track[0].length) return 0;

	start_track_num = drv->track[0].number;
	end_track_num = drv->track[drv->track_cnt - 2].number;
	lead_out_msf = frames_to_msf(drv->track[drv->track_cnt - 1].start + REDBOOK_FRAME_PADDING);
	return 1;
}

static int get_track_info(drive_t *drv, int requested_track_num, TMSF& start_msf, unsigned char& attr)
{
	if (drv->track_cnt < 2 || requested_track_num < 1 || requested_track_num >= drv->track_cnt)
	{
		return 0;
	}

	start_msf = frames_to_msf(drv->track[requested_track_num - 1].start + REDBOOK_FRAME_PADDING);
	attr = drv->track[requested_track_num - 1].attr;
	return 1;
}

static int read_toc(drive_t *drv, uint8_t *cmdbuf)
{
	/* NTS: The SCSI MMC standards say we're allowed to indicate the return data
	 *      is longer than it's allocation length. But here's the thing: some MS-DOS
	 *      CD-ROM drivers will ask for the TOC but only provide enough room for one
	 *      entry (OAKCDROM.SYS) and if we signal more data than it's buffer, it will
	 *      reject our response and render the CD-ROM drive inaccessible. So to make
	 *      this emulation work, we have to cut our response short to the driver's
	 *      allocation length */
	unsigned int AllocationLength = ((unsigned int)cmdbuf[7] << 8) + cmdbuf[8];
	unsigned char Format = cmdbuf[2] & 0xF;
	unsigned char Track = cmdbuf[6];
	bool TIME = !!(cmdbuf[1] & 2);
	unsigned char *write;
	int first, last, track;
	TMSF leadOut;

	printf("read_toc in:\n");
	hexdump(cmdbuf, 12);

	memset(ide_buf, 0, 8);

	if (!get_tracks(drv, first, last, leadOut))
	{
		printf("WARNING: ATAPI READ TOC failed to get track info\n");
		return 8;
	}

	/* start 2 bytes out. we'll fill in the data length later */
	write = ide_buf + 2;

	if (Format == 1) /* Read multisession info */
	{
		unsigned char attr;
		TMSF start;

		*write++ = (unsigned char)1;        /* @+2 first complete session */
		*write++ = (unsigned char)1;        /* @+3 last complete session */

		if (!get_track_info(drv, first, start, attr))
		{
			printf("WARNING: ATAPI READ TOC unable to read track %u information\n", first);
			attr = 0x41; /* ADR=1 CONTROL=4 */
			start.min = 0;
			start.sec = 0;
			start.fr = 0;
		}

		printf("Track %u attr=0x%02x %02u:%02u:%02u\n", first, attr, start.min, start.sec, start.fr);

		*write++ = 0x00;        /* entry+0 RESERVED */
		*write++ = (attr >> 4) | 0x10;  /* entry+1 ADR=1 CONTROL=4 (DATA) */
		*write++ = (unsigned char)first;/* entry+2 TRACK */
		*write++ = 0x00;        /* entry+3 RESERVED */

		/* then, start address of first track in session */
		if (TIME)
		{
			*write++ = 0x00;
			*write++ = start.min;
			*write++ = start.sec;
			*write++ = start.fr;
		}
		else
		{
			uint32_t sec = (start.min * 60u * 75u) + (start.sec * 75u) + start.fr - 150u;
			*write++ = (unsigned char)(sec >> 24u);
			*write++ = (unsigned char)(sec >> 16u);
			*write++ = (unsigned char)(sec >> 8u);
			*write++ = (unsigned char)(sec >> 0u);
		}
	}
	else if (Format == 0) /* Read table of contents */
	{
		*write++ = (unsigned char)first;    /* @+2 */
		*write++ = (unsigned char)last;     /* @+3 */

		for (track = first; track <= last; track++)
		{
			unsigned char attr;
			TMSF start;

			if (!get_track_info(drv, track, start, attr))
			{
				printf("WARNING: ATAPI READ TOC unable to read track %u information\n", track);
				attr = 0x41; /* ADR=1 CONTROL=4 */
				start.min = 0;
				start.sec = 0;
				start.fr = 0;
			}

			if (track < Track) continue;
			if ((write + 8) > (ide_buf + AllocationLength)) break;

			printf("Track %u attr=0x%02x %02u:%02u:%02u\n", first, attr, start.min, start.sec, start.fr);

			*write++ = 0x00;        /* entry+0 RESERVED */
			*write++ = (attr >> 4) | 0x10; /* entry+1 ADR=1 CONTROL=4 (DATA) */
			*write++ = (unsigned char)track;/* entry+2 TRACK */
			*write++ = 0x00;        /* entry+3 RESERVED */
			if (TIME)
			{
				*write++ = 0x00;
				*write++ = start.min;
				*write++ = start.sec;
				*write++ = start.fr;
			}
			else
			{
				uint32_t sec = (start.min * 60u * 75u) + (start.sec * 75u) + start.fr - 150u;
				*write++ = (unsigned char)(sec >> 24u);
				*write++ = (unsigned char)(sec >> 16u);
				*write++ = (unsigned char)(sec >> 8u);
				*write++ = (unsigned char)(sec >> 0u);
			}
		}

		if ((write + 8) <= (ide_buf + AllocationLength))
		{
			*write++ = 0x00;
			*write++ = 0x14;
			*write++ = 0xAA;/*TRACK*/
			*write++ = 0x00;
			if (TIME)
			{
				*write++ = 0x00;
				*write++ = leadOut.min;
				*write++ = leadOut.sec;
				*write++ = leadOut.fr;
			}
			else
			{
				uint32_t sec = (leadOut.min * 60u * 75u) + (leadOut.sec * 75u) + leadOut.fr - 150u;
				*write++ = (unsigned char)(sec >> 24u);
				*write++ = (unsigned char)(sec >> 16u);
				*write++ = (unsigned char)(sec >> 8u);
				*write++ = (unsigned char)(sec >> 0u);
			}
		}
	}
	else
	{
		printf("WARNING: ATAPI READ TOC Format=%u not supported\n", Format);
		return 8;
	}

	/* update the TOC data length field */
	unsigned int x = (unsigned int)(write - ide_buf) - 2;
	ide_buf[0] = x >> 8;
	ide_buf[1] = x & 0xFF;

	printf("read_toc result:\n");
	hexdump(ide_buf, write - ide_buf);

	return write - ide_buf;
}

static uint16_t mode_sense(int page)
{
	uint8_t *write = ide_buf;
	uint8_t *plen;

	uint32_t x;

	int valid = 0;

	printf("mode_sense page: %X\n", page);

	/* Mode Parameter List MMC-3 Table 340 */
	/* - Mode parameter header */
	/* - Page(s) */

	/* Mode Parameter Header (response for 10-byte MODE SENSE) SPC-2 Table 148 */
	*write++ = 0x00;    /* MODE DATA LENGTH                     (MSB) */
	*write++ = 0x00;    /*                                      (LSB) */
	*write++ = 0x00;    /* MEDIUM TYPE */
	*write++ = 0x00;    /* DEVICE-SPECIFIC PARAMETER */
	*write++ = 0x00;    /* Reserved */
	*write++ = 0x00;    /* Reserved */
	*write++ = 0x00;    /* BLOCK DESCRIPTOR LENGTH              (MSB) */
	*write++ = 0x00;    /*                                      (LSB) */
	/* NTS: MMC-3 Table 342 says that BLOCK DESCRIPTOR LENGTH is zero, where it would be 8 for legacy units */

	/* Mode Page Format MMC-3 Table 341 */
	if (page == 0x01 || page == 0x3F)
	{
		valid = 1;
		*write++ = 1;       /* PS|reserved|Page Code */
		plen = write;
		*write++ = 0x00;    /* Page Length (n - 1) ... Length in bytes of the mode parameters that follow */

		*write++ = 0x00;    /* +2 Error recovery Parameter  AWRE|ARRE|TB|RC|Reserved|PER|DTE|DCR */
		*write++ = 3;       /* +3 Read Retry Count */
		*write++ = 0x00;    /* +4 Reserved */
		*write++ = 0x00;    /* +5 Reserved */
		*write++ = 0x00;    /* +6 Reserved */
		*write++ = 0x00;    /* +7 Reserved */

		*plen = write - plen - 1;
	}

	/* CD-ROM audio control MMC-3 Section 6.3.7 table 354 */
	/* also MMC-1 Section 5.2.3.1 table 97 */
	if (page == 0x0E || page == 0x3F)
	{
		valid = 1;
		*write++ = 0x0E;    /* PS|reserved|Page Code */
		plen = write;
		*write++ = 0x00;    /* Page Length (n - 1) ... Length in bytes of the mode parameters that follow */

		*write++ = 0x04;    /* +2 Reserved|IMMED=1|SOTC=0|Reserved */
		*write++ = 0x00;    /* +3 Reserved */
		*write++ = 0x00;    /* +4 Reserved */
		*write++ = 0x00;    /* +5 Reserved */
		*write++ = 0x00;    /* +6 Obsolete (75) */
		*write++ = 75;      /* +7 Obsolete (75) */
		*write++ = 0x01;    /* +8 output port 0 selection (0001b = channel 0) */
		*write++ = 0xFF;    /* +9 output port 0 volume (0xFF = 0dB atten.) */
		*write++ = 0x02;    /* +10 output port 1 selection (0010b = channel 1) */
		*write++ = 0xFF;    /* +11 output port 1 volume (0xFF = 0dB atten.) */
		*write++ = 0x00;    /* +12 output port 2 selection (none) */
		*write++ = 0x00;    /* +13 output port 2 volume (0x00 = mute) */
		*write++ = 0x00;    /* +14 output port 3 selection (none) */
		*write++ = 0x00;    /* +15 output port 3 volume (0x00 = mute) */

		*plen = write - plen - 1;
	}

	/* CD-ROM mechanical status MMC-3 Section 6.3.11 table 361 */
	if (page == 0x2A || page == 0x3F)
	{
		valid = 1;
		*write++ = 0x2A;    /* PS|reserved|Page Code */
		plen = write;
		*write++ = 0x00;    /* Page Length (n - 1) ... Length in bytes of the mode parameters that follow */

							/*    MSB            |             |             |             |              |               |              |       LSB */
		*write++ = 0x07;    /* +2 Reserved       |Reserved     |DVD-RAM read |DVD-R read   |DVD-ROM read  |   Method 2    | CD-RW read   | CD-R read */
		*write++ = 0x00;    /* +3 Reserved       |Reserved     |DVD-RAM write|DVD-R write  |   Reserved   |  Test Write   | CD-RW write  | CD-R write */
		*write++ = 0x71;    /* +4 Buffer Underrun|Multisession |Mode 2 form 2|Mode 2 form 1|Digital Port 2|Digital Port 1 |  Composite   | Audio play */
		*write++ = 0xFF;    /* +5 Read code bar  |UPC          |ISRC         |C2 Pointers  |R-W deintcorr | R-W supported |CDDA accurate |CDDA support */
		*write++ = 0x2F;    /* +6 Loading mechanism type                     |Reserved     |Eject         |Prevent Jumper |Lock state    |Lock */
							/*      0 (0x00) = Caddy
							 *      1 (0x20) = Tray
							 *      2 (0x40) = Popup
							 *      3 (0x60) = Reserved
							 *      4 (0x80) = Changer with indivually changeable discs
							 *      5 (0xA0) = Changer using a magazine mechanism
							 *      6 (0xC0) = Reserved
							 *      6 (0xE0) = Reserved */
		*write++ = 0x03;    /* +7 Reserved       |Reserved     |R-W in leadin|Side chg cap |S/W slot sel  |Changer disc pr|Sep. ch. mute |Sep. volume levels */

		x = 176 * 8;        /* +8 maximum speed supported in kB: 8X  (obsolete in MMC-3) */
		*write++ = x >> 8;
		*write++ = x;

		x = 256;            /* +10 Number of volume levels supported */
		*write++ = x >> 8;
		*write++ = x;

		x = 6 * 256;        /* +12 buffer size supported by drive in kB */
		*write++ = x >> 8;
		*write++ = x;

		x = 176 * 8;        /* +14 current read speed selected in kB: 8X  (obsolete in MMC-3) */
		*write++ = x >> 8;
		*write++ = x;

		*plen = write - plen - 1;
	}

	if (!valid)
	{
		*write++ = page;    /* PS|reserved|Page Code */
		*write++ = 0x06;    /* Page Length (n - 1) ... Length in bytes of the mode parameters that follow */

		memset(write, 0, 6); write += 6;
		printf("WARNING: MODE SENSE on page 0x%02x not supported\n", page);
	}

	/* mode param header, data length */
	x = (uint32_t)(write - ide_buf) - 2;
	ide_buf[0] = (unsigned char)(x >> 8u);
	ide_buf[1] = (unsigned char)x;

	hexdump(ide_buf, x + 2);
	return x + 2;
}

static int get_subchan(drive_t *drv, unsigned char& attr, unsigned char& track_num, unsigned char& index, TMSF& relative_msf, TMSF& absolute_msf)
{
	attr = 0;
	track_num = 1;
	index = 1;
	relative_msf.min = relative_msf.fr = 0; relative_msf.sec = 2;
	absolute_msf.min = absolute_msf.fr = 0; absolute_msf.sec = 2;

	if (drv->play_start_lba == 0xFFFFFFFF) return 0;

	//TODO: use current play position when audio playback will be implemented
	uint32_t cur_pos = drv->play_start_lba;

	for (int i = 0; i < drv->track_cnt; i++)
	{
		if (cur_pos >= drv->track[i].start && cur_pos < (drv->track[i].start + drv->track[i].length))
		{
			track_num = drv->track[i].number;
			attr = drv->track[i].attr;
			absolute_msf = frames_to_msf(cur_pos + REDBOOK_FRAME_PADDING);
			return 1;
		}
	}

	return 0;
}

static int read_subchannel(drive_t *drv, uint8_t* cmdbuf)
{
	unsigned char paramList = cmdbuf[3];
	unsigned char attr, track, index;
	bool SUBQ = !!(cmdbuf[2] & 0x40);
	bool TIME = !!(cmdbuf[1] & 2);
	unsigned char *write;
	TMSF rel, abs;

	if (paramList == 0 || paramList > 3)
	{
		printf("ATAPI READ SUBCHANNEL unknown param list\n");
		memset(ide_buf, 0, 8);
		return 8;
	}
	else if (paramList == 2)
	{
		printf("ATAPI READ SUBCHANNEL Media Catalog Number not supported\n");
		memset(ide_buf, 0, 8);
		return 8;
	}
	else if (paramList == 3) {
		printf("ATAPI READ SUBCHANNEL ISRC not supported\n");
		memset(ide_buf, 0, 8);
		return 8;
	}

	/* get current subchannel position */
	if (!get_subchan(drv, attr, track, index, rel, abs))
	{
		printf("ATAPI READ SUBCHANNEL unable to read current pos\n");
		memset(ide_buf, 0, 8);
		return 8;
	}

	memset(ide_buf, 0, 8);
	write = ide_buf;
	*write++ = 0x00;
	*write++ = (!drv->playing) ? 0x13 : drv->paused ? 0x12 : 0x11; /* AUDIO STATUS */
	*write++ = 0x00;  /* SUBCHANNEL DATA LENGTH */
	*write++ = 0x00;

	if (SUBQ)
	{
		*write++ = 0x01;    /* subchannel data format code */
		*write++ = (attr >> 4) | 0x10;  /* ADR/CONTROL */
		*write++ = track;
		*write++ = index;
		if (TIME)
		{
			*write++ = 0x00;
			*write++ = abs.min;
			*write++ = abs.sec;
			*write++ = abs.fr;
			*write++ = 0x00;
			*write++ = rel.min;
			*write++ = rel.sec;
			*write++ = rel.fr;
		}
		else
		{
			uint32_t sec;

			sec = (abs.min * 60u * 75u) + (abs.sec * 75u) + abs.fr - 150u;
			*write++ = (unsigned char)(sec >> 24u);
			*write++ = (unsigned char)(sec >> 16u);
			*write++ = (unsigned char)(sec >> 8u);
			*write++ = (unsigned char)(sec >> 0u);

			sec = (rel.min * 60u * 75u) + (rel.sec * 75u) + rel.fr - 150u;
			*write++ = (unsigned char)(sec >> 24u);
			*write++ = (unsigned char)(sec >> 16u);
			*write++ = (unsigned char)(sec >> 8u);
			*write++ = (unsigned char)(sec >> 0u);
		}
	}

	unsigned int x = (unsigned int)(write - ide_buf) - 4;
	ide_buf[2] = x >> 8;
	ide_buf[3] = x;

	dbg_hexdump(ide_buf, write - ide_buf);
	return write - ide_buf;
}

static void pkt_send(ide_config *ide, void *data, uint16_t size)
{
	ide->regs.pkt_io_size = (size + 1) / 2;
	ide_send_data(data, ide->regs.pkt_io_size);

	ide->regs.cylinder = size;
	ide->regs.sector_count = 2;

	ide->regs.status = ATA_STATUS_RDY | ATA_STATUS_DRQ | ATA_STATUS_IRQ;
	ide_set_regs(ide);
	ide->state = IDE_STATE_WAIT_PKT_RD;
}

static void read_cd_sectors(ide_config *ide, int cnt)
{
	drive_t *drv = &ide->drive[ide->regs.drv];
	uint32_t sz = drv->track[drv->data_num].sectorSize;

	if (sz == 2048)
	{
		if (!ide->null) ide->null = (FileReadAdv(drv->f, ide_buf, cnt * sz, -1) <= 0);
		if (ide->null) memset(ide_buf, 0, cnt * sz);
		return;
	}

	uint32_t pre = drv->track[drv->data_num].mode2 ? 24 : 16;
	uint32_t post = sz - pre - 2048;
	uint32_t off = 0;

	while (cnt--)
	{
		if (!ide->null) ide->null = !FileSeek(drv->f, pre, SEEK_CUR);
		if (!ide->null) ide->null = (FileReadAdv(drv->f, ide_buf + off, 2048, -1) <= 0);
		if (ide->null) memset(ide_buf + off, 0, 2048);
		if (!ide->null) ide->null = !FileSeek(drv->f, post, SEEK_CUR);
		off += 2048;
	}
}

void cdrom_read(ide_config *ide)
{
	uint32_t cnt = ide->regs.pkt_cnt;
	drive_t *drive = &ide->drive[ide->regs.drv];

	if ((cnt * 4) > ide_io_max_size) cnt = ide_io_max_size / 4;

	while ((cnt * 2048) > ide->regs.pkt_size_limit)
	{
		if (cnt <= 1) break;
		cnt--;
	}


	if (cnt != ide->regs.pkt_cnt)
	{
		dbg_printf("** partial CD read\n");
	}

	if (ide->state == IDE_STATE_INIT_RW && !drive->chd_f)
	{
		uint32_t pos = ide->regs.pkt_lba * ide->drive[ide->regs.drv].track[ide->drive[ide->regs.drv].data_num].sectorSize;

		//printf("Read from pos: %d\n", pos);
		ide->null = (FileSeek(ide->drive[ide->regs.drv].f, pos, SEEK_SET) < 0);
	}


	if (drive->chd_f) {

		uint32_t hdr = drive->track[drive->data_num].mode2 ? 24 : 16;
		if (drive->track[drive->data_num].sectorSize == 2048)
		{
			hdr = 0;
		}
		uint32_t d_offset = 0;

		if (ide->state == IDE_STATE_INIT_RW)
		{
			drive->chd_last_partial_lba = ide->regs.pkt_lba;
		}

	 	for(uint32_t i = 0; i < cnt; i++)
		{

			if (mister_chd_read_sector(drive->chd_f, drive->chd_last_partial_lba + drive->track[drive->data_num].chd_offset, d_offset, hdr, 2048, ide_buf, drive->chd_hunkbuf, &drive->chd_hunknum) != CHDERR_NONE)
			{
				//I don't think anything else uses this, but set it just in case.
				ide->null = 1;
				memset(ide_buf+d_offset, 0, 2048);
			} else {
				ide->null = 0;
			}
			d_offset += 2048;
			drive->chd_last_partial_lba++;
		}

	} else {
		read_cd_sectors(ide, cnt);
	}

	dbg_printf("\nsector:\n");
	dbg_hexdump(ide_buf, 512, 0);

	ide->regs.pkt_cnt -= cnt;
	pkt_send(ide, ide_buf, cnt * 2048);
}

static int cd_inquiry(uint8_t maxlen)
{
	static const char vendor[] = "MiSTer  ";
	static const char product[] = "CDROM           ";

	memset(ide_buf, 0, 47);
	ide_buf[0] = (0 << 5) | 5;  /* Peripheral qualifier=0   device type=5 (CDROM) */
	ide_buf[1] = 0x80;			/* RMB=1 removable media */
	ide_buf[2] = 0x00;			/* ANSI version */
	ide_buf[3] = 0x21;
	ide_buf[4] = maxlen - 5;    /* additional length */

	for (int i = 0; i < 8; i++) ide_buf[i + 8] = (unsigned char)vendor[i];
	for (int i = 0; i < 16; i++) ide_buf[i + 16] = (unsigned char)product[i];
	for (int i = 0; i < 4; i++) ide_buf[i + 32] = ' ';
	for (int i = 0; i < 11; i++) ide_buf[i + 36] = ' ';

	hexdump(ide_buf, maxlen);
	return maxlen;
}

static void set_sense(uint8_t SK, uint8_t ASC = 0, uint8_t ASCQ = 0)
{
	int len = 18;
	memset(ide_buf, 0, len);

	ide_buf[0] = 0x70;      /* RESPONSE CODE */
	ide_buf[2] = SK & 0xF;  /* SENSE KEY */
	ide_buf[7] = len - 18;  /* additional sense length */
	ide_buf[12] = ASC;
	ide_buf[13] = ASCQ;
}

static int get_sense(drive_t *drv)
{
	switch (drv->load_state)
	{
	case 3:
		set_sense(2, 0x3A);
		break;

	case 2:
		set_sense(2, 4, 1);
		drv->load_state--;
		break;

	case 1:
		set_sense(2, 28, 0);
		drv->load_state--;
		break;

	default:
		set_sense(0);
		break;
	}

	dbg_hexdump(ide_buf, 18);
	return 18;
}

static void pause_resume(drive_t *drv, uint8_t *cmdbuf)
{
	bool resume = !!(cmdbuf[8] & 1);
	if(drv->playing) drv->paused = !resume;
}

static void play_audio_msf(drive_t *drv, uint8_t *cmdbuf)
{
	if (cmdbuf[3] == 0xFF && cmdbuf[4] == 0xFF && cmdbuf[5] == 0xFF)
	{
		drv->play_start_lba = 0xFFFFFFFF;
	}
	else
	{
		drv->play_start_lba = (cmdbuf[3] * 60u * 75u) + (cmdbuf[4] * 75u) + cmdbuf[5];

		if (drv->play_start_lba >= 150u) drv->play_start_lba -= 150u; /* LBA sector 0 == M:S:F sector 0:2:0 */
		else drv->play_end_lba = 0;
	}

	if (cmdbuf[6] == 0xFF && cmdbuf[7] == 0xFF && cmdbuf[8] == 0xFF)
	{
		drv->play_end_lba = 0xFFFFFFFF;
	}
	else
	{
		drv->play_end_lba = (cmdbuf[6] * 60u * 75u) + (cmdbuf[7] * 75u) + cmdbuf[8];
		if (drv->play_end_lba >= 150u) drv->play_end_lba -= 150u; /* LBA sector 0 == M:S:F sector 0:2:0 */
		else drv->play_end_lba = 0;
	}

	if (drv->play_start_lba == drv->play_end_lba)
	{
		drv->playing = 0;
		drv->paused = 0;
		return;
	}

	/* LBA 0xFFFFFFFF means start playing wherever the optics of the CD sit */
	if (drv->play_start_lba != 0xFFFFFFFF)
	{
		drv->playing = 1;
		drv->paused = 0;
	}
	else
	{
		drv->playing = 0;
		drv->paused = 1;
	}
}

void play_audio10(drive_t *drv, uint8_t *cmdbuf)
{
	uint16_t play_length;

	drv->play_start_lba = ((uint32_t)cmdbuf[2] << 24) + ((uint32_t)cmdbuf[3] << 16) + ((uint32_t)cmdbuf[4] << 8) + ((uint32_t)cmdbuf[5] << 0);
	play_length = ((uint16_t)cmdbuf[7] << 8) + ((uint16_t)cmdbuf[8] << 0);
	drv->play_end_lba = drv->play_start_lba + play_length;

	if (play_length == 0)
	{
		drv->playing = 0;
		drv->paused = 0;
		return;
	}

	/* LBA 0xFFFFFFFF means start playing wherever the optics of the CD sit */
	if (drv->play_start_lba != 0xFFFFFFFF)
	{
		drv->playing = 1;
		drv->paused = 0;
	}
	else
	{
		drv->playing = 0;
		drv->paused = 1;
	}
}

static void cdrom_nodisk(ide_config *ide)
{
	drive_t *drv = &ide->drive[ide->regs.drv];
	drv->last_load_state = drv->load_state;
	if (drv->load_state && drv->load_state < 3) drv->load_state--;

	cdrom_reply(ide, CD_ERR_NO_DISK);
}

void cdrom_handle_pkt(ide_config *ide)
{
	drive_t *drv = &ide->drive[ide->regs.drv];
	uint8_t cmdbuf[16];

	ide_recv_data(cmdbuf, 6);
	ide_reset_buf();
	dbg_hexdump(cmdbuf, 12, 0);

	ide->regs.pkt_cnt = 0;
	int err = 0;

	switch (cmdbuf[0])
	{
	case 0xA8: // read(12) sectors
	case 0x28: // read(10) sectors
		dbg_printf("** Read Sectors\n");
		//hexdump(cmdbuf, 12, 0);
		ide->regs.pkt_cnt = (cmdbuf[0] == 0x28) ? ((cmdbuf[7] << 8) | cmdbuf[8]) : ((cmdbuf[6] << 24) | (cmdbuf[7] << 16) | (cmdbuf[8] << 8) | cmdbuf[9]);
		ide->regs.pkt_lba = ((cmdbuf[2] << 24) | (cmdbuf[3] << 16) | (cmdbuf[4] << 8) | cmdbuf[5]);
		if (!ide->regs.pkt_cnt)
		{
			dbg_printf("  length 0 is not a error.\n");
			cdrom_reply(ide, 0);
			break;
		}

		dbg_printf("** par: lba = %d, cnt = %d, load_state = %d\n", ide->regs.pkt_lba, ide->regs.pkt_cnt, drv->load_state);
		ide->state = IDE_STATE_INIT_RW;
		if(!drv->load_state) cdrom_read(ide);
		else cdrom_nodisk(ide);
		break;

	case 0x25: // read capacity
		dbg_printf("** Read Capacity\n");
		if (!drv->load_state)
		{
			uint32_t tmp = 0;

			if (drv->chd_f)
			{
				tmp = drv->chd_total_size / 2048;
			} else {
				tmp = drv->f->size / 2048;
			}
			ide_buf[0] = tmp >> 24;
			ide_buf[1] = tmp >> 16;
			ide_buf[2] = tmp >> 8;
			ide_buf[3] = tmp;

			tmp = 2048;
			ide_buf[4] = tmp >> 24;
			ide_buf[5] = tmp >> 16;
			ide_buf[6] = tmp >> 8;
			ide_buf[7] = tmp;
			//hexdump(buf, 8, 0);
			pkt_send(ide, ide_buf, 8);
		}
		else cdrom_nodisk(ide);
		break;

	case 0x2B: // seek

		dbg_printf("** Seek\n");
		drv->playing = 0;
		drv->paused = 0;
		cdrom_reply(ide, 0);
		break;

	case 0x1E: // lock the cd door - doing nothing.
		dbg_printf("** Lock Door\n");
		cdrom_reply(ide, 0);
		break;

	case 0x5A: // mode sense
		dbg_printf("** Mode Sense\n");
		pkt_send(ide, ide_buf, mode_sense(cmdbuf[2]));
		break;

	case 0x42: // read sub
		dbg_printf("** read sub:\n");
		pkt_send(ide, ide_buf, read_subchannel(drv, cmdbuf));
		break;

	case 0x43: // read TOC
		dbg_printf("** Read TOC\n");
		pkt_send(ide, ide_buf, read_toc(drv, cmdbuf));
		break;

	case 0x12: // inquiry
		dbg_printf("** Inquiry\n");
		pkt_send(ide, ide_buf, cd_inquiry(cmdbuf[4]));
		break;

	case 0x03: // mode sense
		dbg_printf("** get sense:\n");
		pkt_send(ide, ide_buf, get_sense(drv));
		break;

	case 0x55: // mode select
		dbg_printf("** mode select\n");
		ide->regs.cylinder = (cmdbuf[7] << 8) | cmdbuf[8];
		if (ide->regs.cylinder > 512) ide->regs.cylinder = 512;
		ide->regs.pkt_io_size = (ide->regs.cylinder + 1) / 2;
		ide->regs.sector_count = 0;
		ide->regs.status = ATA_STATUS_RDY | ATA_STATUS_DRQ | ATA_STATUS_IRQ;
		ide_set_regs(ide);
		ide->state = IDE_STATE_WAIT_PKT_MODE;
		break;

	case 0x00: // test unit ready
		dbg_printf("** Test Unit Ready\n");
		if (!drv->load_state) cdrom_reply(ide, 0);
		else cdrom_nodisk(ide);
		break;

	case 0x45: // play lba
		dbg_printf("** CD PLAY AUDIO(10)\n");
		play_audio10(drv, cmdbuf);
		cdrom_reply(ide, 0);
		break;

	case 0x47: // play msf
		dbg_printf("** CD PLAY AUDIO MSF\n");
		play_audio_msf(drv, cmdbuf);
		cdrom_reply(ide, 0);
		break;

	case 0x4B: // pause/resume
		dbg_printf("** CD PAUSE/RESUME\n");
		pause_resume(drv, cmdbuf);
		cdrom_reply(ide, 0);
		break;

	default:
		err = 1;
		break;
	}

	if (err)
	{
		printf("(!) Error in packet command %02X\n", cmdbuf[0]);
		hexdump(cmdbuf, 12, 0);
		cdrom_reply(ide, ATA_ERR_ABRT);
	}
}

int cdrom_handle_cmd(ide_config *ide)
{
	uint8_t drv;

	switch (ide->regs.cmd)
	{
	case 0xA1: // identify packet
		//print_regs(&ide->regs);
		dbg_printf("identify packet\n");
		ide_send_data(ide->drive[ide->regs.drv].id, 256);
		drv = ide->regs.drv;
		memset(&ide->regs, 0, sizeof(ide->regs));
		ide->regs.drv = drv;
		ide->regs.pkt_io_size = 256;
		ide->regs.status = ATA_STATUS_RDY | ATA_STATUS_DRQ | ATA_STATUS_IRQ | ATA_STATUS_END;
		ide_set_regs(ide);
		ide->state = IDE_STATE_IDLE;
		break;

	case 0xEC: // identify (fail)
		dbg_printf("identify (CD)\n");
		ide->regs.sector = 1;
		ide->regs.sector_count = 1;
		ide->regs.cylinder = 0xEB14;
		ide->regs.head = 0;
		ide->regs.io_size = 0;
		return 1;

	case 0xA0: // packet
		dbg_printf("cmd A0: %02X\n", ide->regs.features);
		if (ide->regs.features & 1)
		{
			dbg_printf("Cancel A0 DMA transfer\n");
			return 1;
		}
		ide->regs.pkt_size_limit = ide->regs.cylinder;
		if (!ide->regs.pkt_size_limit) ide->regs.pkt_size_limit = ide_io_max_size * 512;
		ide->regs.pkt_io_size = 6;
		ide->regs.sector_count = 1;
		ide->regs.status = ATA_STATUS_RDY | ATA_STATUS_DRQ; // | ATA_STATUS_IRQ;
		ide_set_regs(ide);
		ide->state = IDE_STATE_WAIT_PKT_CMD;
		break;

	case 0x08: // reset
		dbg_printf("cmd 08\n");
		ide->drive[ide->regs.drv].playing = 0;
		ide->drive[ide->regs.drv].paused = 0;
		ide->regs.sector = 1;
		ide->regs.sector_count = 1;
		ide->regs.cylinder = 0xEB14;
		ide->regs.head = 0;
		ide->regs.io_size = 0;
		ide->regs.status = ATA_STATUS_RDY;
		ide_set_regs(ide);
		break;

	case 0x00: // nop
		dbg_printf("cmd 00\n");
		return 1; // must always fail

	default:
		printf("(!) Unsupported command\n");
		ide_print_regs(&ide->regs);
		return 1;
	}

	return 0;
}

void cdrom_reply(ide_config *ide, uint8_t error)
{
	ide->state = IDE_STATE_IDLE;
	ide->regs.sector_count = 3;
	ide->regs.status = ATA_STATUS_RDY | ATA_STATUS_IRQ | (error ? ATA_STATUS_ERR : 0);
	ide->regs.error = error;
	ide_set_regs(ide);
}

void cdrom_close_chd(drive_t *drv)
{

	if (drv->chd_f)
	{
		chd_close(drv->chd_f);
		drv->chd_f = NULL;
	}

	if (drv->chd_hunkbuf)
	{
		free(drv->chd_hunkbuf);
		drv->chd_hunkbuf = NULL;
	}
	drv->chd_hunknum = -1;
}

const char* cdrom_parse(uint32_t num, const char *filename)
{
	const char *res = 0;

	if (strlen(filename))
	{
		const char *path = getFullPath(filename);
		int drv = num & 1;
		num >>= 1;
		cdrom_close_chd(&ide_inst[num].drive[drv]);
		res = load_chd_file(&ide_inst[num].drive[drv], path);
		if (!res) res = load_cue_file(&ide_inst[num].drive[drv], path);
		if (!res) res = load_iso_file(&ide_inst[num].drive[drv], path);
	}
	return res;
}
