#include <dpmi.h>
#include "quakedef.h"
#include "dosisms.h"

extern	cvar_t	bgmvolume;

#define ADDRESS_MODE_HSG		0
#define ADDRESS_MODE_RED_BOOK	1

#define STATUS_ERROR_BIT	0x8000
#define STATUS_BUSY_BIT		0x0200
#define STATUS_DONE_BIT		0x0100
#define STATUS_ERROR_MASK	0x00ff

#define ERROR_WRITE_PROTECT		0
#define ERROR_UNKNOWN_UNIT		1
#define ERROR_DRIVE_NOT_READY	2
#define ERROR_UNKNOWN_COMMAND	3
#define ERROR_CRC_ERROR			4
#define ERROR_BAD_REQUEST_LEN	5
#define ERROR_SEEK_ERROR		6
#define ERROR_UNKNOWN_MEDIA		7
#define ERROR_SECTOR_NOT_FOUND	8
#define ERROR_OUT_OF_PAPER		9
#define ERROR_WRITE_FAULT		10
#define ERROR_READ_FAULT		11
#define ERROR_GENERAL_FAILURE	12
#define ERROR_RESERVED_13		13
#define ERROR_RESERVED_14		14
#define ERROR_BAD_DISK_CHANGE	15

#define COMMAND_READ			3
#define COMMAND_WRITE			12
#define COMMAND_PLAY_AUDIO		132
#define COMMAND_STOP_AUDIO		133
#define COMMAND_RESUME_AUDIO	136

#define READ_REQUEST_AUDIO_CHANNEL_INFO		4
#define READ_REQUEST_DEVICE_STATUS			6
#define READ_REQUEST_MEDIA_CHANGE			9
#define READ_REQUEST_AUDIO_DISK_INFO		10
#define READ_REQUEST_AUDIO_TRACK_INFO		11
#define READ_REQUEST_AUDIO_STATUS			15

#define WRITE_REQUEST_EJECT					0
#define WRITE_REQUEST_RESET					2
#define WRITE_REQUEST_AUDIO_CHANNEL_INFO	3

#define STATUS_DOOR_OPEN					0x00000001
#define STATUS_DOOR_UNLOCKED				0x00000002
#define STATUS_RAW_SUPPORT					0x00000004
#define STATUS_READ_WRITE					0x00000008
#define STATUS_AUDIO_SUPPORT				0x00000010
#define STATUS_INTERLEAVE_SUPPORT			0x00000020
#define STATUS_BIT_6_RESERVED				0x00000040
#define STATUS_PREFETCH_SUPPORT				0x00000080
#define STATUS_AUDIO_MANIPLUATION_SUPPORT	0x00000100
#define STATUS_RED_BOOK_ADDRESS_SUPPORT		0x00000200

#define MEDIA_NOT_CHANGED		1
#define MEDIA_STATUS_UNKNOWN	0
#define MEDIA_CHANGED			-1

#define AUDIO_CONTROL_MASK				0xd0
#define AUDIO_CONTROL_DATA_TRACK		0x40
#define AUDIO_CONTROL_AUDIO_2_TRACK		0x00
#define AUDIO_CONTROL_AUDIO_2P_TRACK	0x10
#define AUDIO_CONTROL_AUDIO_4_TRACK		0x80
#define AUDIO_CONTROL_AUDIO_4P_TRACK	0x90

#define AUDIO_STATUS_PAUSED				0x0001

#pragma pack(1)

struct playAudioRequest
{
	char	addressingMode;
	int		startLocation;
	int		sectors;
};

struct readRequest
{
	char	mediaDescriptor;
	short	bufferOffset;
	short	bufferSegment;
	short	length;
	short	startSector;
	int		volumeID;
};

struct writeRequest
{
	char	mediaDescriptor;
	short	bufferOffset;
	short	bufferSegment;
	short	length;
	short	startSector;
	int		volumeID;
};

struct cd_request
{
	char	headerLength;
	char	unit;
	char	command;
	short	status;
	char	reserved[8];
	union
	{
		struct	playAudioRequest	playAudio;
		struct	readRequest			read;
		struct	writeRequest		write;
	} x;
};


struct audioChannelInfo_s
{
	char	code;
	char	channel0input;
	char	channel0volume;
	char	channel1input;
	char	channel1volume;
	char	channel2input;
	char	channel2volume;
	char	channel3input;
	char	channel3volume;
};

struct deviceStatus_s
{
	char	code;
	int		status;
};

struct mediaChange_s
{
	char	code;
	char	status;
};

struct audioDiskInfo_s
{
	char	code;
	char	lowTrack;
	char	highTrack;
	int		leadOutStart;
};

struct audioTrackInfo_s
{
	char	code;
	char	track;
	int		start;
	char	control;
};

struct audioStatus_s
{
	char	code;
	short	status;
	int		PRstartLocation;
	int		PRendLocation;
};

struct reset_s
{
	char	code;
};

union readInfo_u
{
	struct audioChannelInfo_s	audioChannelInfo;
	struct deviceStatus_s		deviceStatus;
	struct mediaChange_s		mediaChange;
	struct audioDiskInfo_s		audioDiskInfo;
	struct audioTrackInfo_s		audioTrackInfo;
	struct audioStatus_s		audioStatus;
	struct reset_s				reset;
};

#pragma pack()

#define MAXIMUM_TRACKS			32

typedef struct
{
	int			start;
	int			length;
	qboolean	isData;
} track_info;

typedef struct
{
	qboolean	valid;
	int			leadOutAddress;
	track_info	track[MAXIMUM_TRACKS];
	byte		lowTrack;
	byte		highTrack;
} cd_info;

static struct cd_request	*cdRequest;
static union readInfo_u		*readInfo;
static cd_info				cd;

static qboolean	playing = false;
static qboolean	wasPlaying = false;
static qboolean	mediaCheck = false;
static qboolean	initialized = false;
static qboolean	enabled = true;
static qboolean playLooping = false;
static short	cdRequestSegment;
static short	cdRequestOffset;
static short	readInfoSegment;
static short	readInfoOffset;
static byte 	remap[256];
static byte		cdrom;
static byte		playTrack;
static byte		cdvolume;


static int RedBookToSector(int rb)
{
	byte	minute;
	byte	second;
	byte	frame;

	minute = (rb >> 16) & 0xff;
	second = (rb >> 8) & 0xff;
	frame = rb & 0xff;
	return minute * 60 * 75 + second * 75 + frame;
}


static void CDAudio_Reset(void)
{
	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_WRITE;
	cdRequest->status = 0;

	cdRequest->x.write.mediaDescriptor = 0;
	cdRequest->x.write.bufferOffset = readInfoOffset;
	cdRequest->x.write.bufferSegment = readInfoSegment;
	cdRequest->x.write.length = sizeof(struct reset_s);
	cdRequest->x.write.startSector = 0;
	cdRequest->x.write.volumeID = 0;

	readInfo->reset.code = WRITE_REQUEST_RESET;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);
}


static void CDAudio_Eject(void)
{
	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_WRITE;
	cdRequest->status = 0;

	cdRequest->x.write.mediaDescriptor = 0;
	cdRequest->x.write.bufferOffset = readInfoOffset;
	cdRequest->x.write.bufferSegment = readInfoSegment;
	cdRequest->x.write.length = sizeof(struct reset_s);
	cdRequest->x.write.startSector = 0;
	cdRequest->x.write.volumeID = 0;

	readInfo->reset.code = WRITE_REQUEST_EJECT;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);
}


static int CDAudio_GetAudioTrackInfo(byte track, int *start)
{
	byte	control;

	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_READ;
	cdRequest->status = 0;

	cdRequest->x.read.mediaDescriptor = 0;
	cdRequest->x.read.bufferOffset = readInfoOffset;
	cdRequest->x.read.bufferSegment = readInfoSegment;
	cdRequest->x.read.length = sizeof(struct audioTrackInfo_s);
	cdRequest->x.read.startSector = 0;
	cdRequest->x.read.volumeID = 0;

	readInfo->audioTrackInfo.code = READ_REQUEST_AUDIO_TRACK_INFO;
	readInfo->audioTrackInfo.track = track;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	if (cdRequest->status & STATUS_ERROR_BIT)
	{
		Con_DPrintf("CDAudio_GetAudioTrackInfo %04x\n", cdRequest->status & 	0xffff);
		return -1;
	}

	*start = readInfo->audioTrackInfo.start;
	control = readInfo->audioTrackInfo.control & AUDIO_CONTROL_MASK;
	return (control & AUDIO_CONTROL_DATA_TRACK);
}


static int CDAudio_GetAudioDiskInfo(void)
{
	int n;

	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_READ;
	cdRequest->status = 0;

	cdRequest->x.read.mediaDescriptor = 0;
	cdRequest->x.read.bufferOffset = readInfoOffset;
	cdRequest->x.read.bufferSegment = readInfoSegment;
	cdRequest->x.read.length = sizeof(struct audioDiskInfo_s);
	cdRequest->x.read.startSector = 0;
	cdRequest->x.read.volumeID = 0;

	readInfo->audioDiskInfo.code = READ_REQUEST_AUDIO_DISK_INFO;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	if (cdRequest->status & STATUS_ERROR_BIT)
	{
		Con_DPrintf("CDAudio_GetAudioDiskInfo %04x\n", cdRequest->status & 	0xffff);
		return -1;
	}

	cd.valid = true;
	cd.lowTrack = readInfo->audioDiskInfo.lowTrack;
	cd.highTrack = readInfo->audioDiskInfo.highTrack;
	cd.leadOutAddress = readInfo->audioDiskInfo.leadOutStart;

	for (n = cd.lowTrack; n <= cd.highTrack; n++)
	{
		cd.track[n].isData = CDAudio_GetAudioTrackInfo (n, &cd.track[n].start);
		if (n > cd.lowTrack)
		{
			cd.track[n-1].length = RedBookToSector(cd.track[n].start) - RedBookToSector(cd.track[n-1].start);
			if (n == cd.highTrack)
				cd.track[n].length = RedBookToSector(cd.leadOutAddress) - RedBookToSector(cd.track[n].start);
		}
	}

	return 0;
}


static int CDAudio_GetAudioStatus(void)
{
	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_READ;
	cdRequest->status = 0;

	cdRequest->x.read.mediaDescriptor = 0;
	cdRequest->x.read.bufferOffset = readInfoOffset;
	cdRequest->x.read.bufferSegment = readInfoSegment;
	cdRequest->x.read.length = sizeof(struct audioStatus_s);
	cdRequest->x.read.startSector = 0;
	cdRequest->x.read.volumeID = 0;

	readInfo->audioDiskInfo.code = READ_REQUEST_AUDIO_STATUS;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	if (cdRequest->status & STATUS_ERROR_BIT)
		return -1;
	return 0;
}


static int CDAudio_MediaChange(void)
{
	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_READ;
	cdRequest->status = 0;

	cdRequest->x.read.mediaDescriptor = 0;
	cdRequest->x.read.bufferOffset = readInfoOffset;
	cdRequest->x.read.bufferSegment = readInfoSegment;
	cdRequest->x.read.length = sizeof(struct mediaChange_s);
	cdRequest->x.read.startSector = 0;
	cdRequest->x.read.volumeID = 0;

	readInfo->mediaChange.code = READ_REQUEST_MEDIA_CHANGE;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	return readInfo->mediaChange.status;
}


byte CDAudio_GetVolume (void)
{
	return cdvolume;
}


// we set the volume to 0 first and then to the desired volume
// some cd-rom drivers seem to need it done this way
void CDAudio_SetVolume (byte volume)
{
	if (!initialized || !enabled)
		return;

	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_WRITE;
	cdRequest->status = 0;

	cdRequest->x.read.mediaDescriptor = 0;
	cdRequest->x.read.bufferOffset = readInfoOffset;
	cdRequest->x.read.bufferSegment = readInfoSegment;
	cdRequest->x.read.length = sizeof(struct audioChannelInfo_s);
	cdRequest->x.read.startSector = 0;
	cdRequest->x.read.volumeID = 0;

	readInfo->audioChannelInfo.code = WRITE_REQUEST_AUDIO_CHANNEL_INFO;
	readInfo->audioChannelInfo.channel0input = 0;
	readInfo->audioChannelInfo.channel0volume = 0;
	readInfo->audioChannelInfo.channel1input = 1;
	readInfo->audioChannelInfo.channel1volume = 0;
	readInfo->audioChannelInfo.channel2input = 2;
	readInfo->audioChannelInfo.channel2volume = 0;
	readInfo->audioChannelInfo.channel3input = 3;
	readInfo->audioChannelInfo.channel3volume = 0;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	readInfo->audioChannelInfo.channel0volume = volume;
	readInfo->audioChannelInfo.channel1volume = volume;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	cdvolume = volume;
}


void CDAudio_Play(byte track, qboolean looping)
{
	if (!initialized || !enabled)
		return;
	
	if (!cd.valid)
		return;

	track = remap[track];

	if (playing)
	{
		if (playTrack == track)
			return;
		CDAudio_Stop();
	}

	playLooping = looping;

	if (track < cd.lowTrack || track > cd.highTrack)
	{
		Con_DPrintf("CDAudio_Play: Bad track number %u.\n", track);
		return;
	}

	playTrack = track;

	if (cd.track[track].isData)
	{
		Con_DPrintf("CDAudio_Play: Can not play data.\n");
		return;
	}

	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_PLAY_AUDIO;
	cdRequest->status = 0;

	cdRequest->x.playAudio.addressingMode = ADDRESS_MODE_RED_BOOK;
	cdRequest->x.playAudio.startLocation = cd.track[track].start;
	cdRequest->x.playAudio.sectors = cd.track[track].length;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	if (cdRequest->status & STATUS_ERROR_BIT)
	{
		Con_DPrintf("CDAudio_Play: track %u failed\n", track);
		cd.valid = false;
		playing = false;
		return;
	}

	playing = true;
}


void CDAudio_Stop(void)
{
	if (!initialized || !enabled)
		return;
	
	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_STOP_AUDIO;
	cdRequest->status = 0;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	wasPlaying = playing;
	playing = false;
}


void CDAudio_Resume(void)
{
	if (!initialized || !enabled)
		return;
	
	if (!cd.valid)
		return;

	if (!wasPlaying)
		return;
	
	cdRequest->headerLength = 13;
	cdRequest->unit = 0;
	cdRequest->command = COMMAND_RESUME_AUDIO;
	cdRequest->status = 0;

	regs.x.ax = 0x1510;
	regs.x.cx = cdrom;
	regs.x.es = cdRequestSegment;
	regs.x.bx = cdRequestOffset;
	dos_int86 (0x2f);

	playing = true;
}


static void CD_f (void)
{
	char	*command;
	int		ret;
	int		n;
	int		startAddress;

	if (Cmd_Argc() < 2)
		return;

	command = Cmd_Argv (1);

	if (Q_strcasecmp(command, "on") == 0)
	{
		enabled = true;
		return;
	}

	if (Q_strcasecmp(command, "off") == 0)
	{
		if (playing)
			CDAudio_Stop();
		enabled = false;
		return;
	}

	if (Q_strcasecmp(command, "reset") == 0)
	{
		enabled = true;
		if (playing)
			CDAudio_Stop();
		for (n = 0; n < 256; n++)
			remap[n] = n;
		CDAudio_Reset();
		CDAudio_GetAudioDiskInfo();
		return;
	}

	if (Q_strcasecmp(command, "remap") == 0)
	{
		ret = Cmd_Argc() - 2;
		if (ret <= 0)
		{
			for (n = 1; n < 256; n++)
				if (remap[n] != n)
					Con_Printf("  %u -> %u\n", n, remap[n]);
			return;
		}
		for (n = 1; n <= ret; n++)
			remap[n] = Q_atoi(Cmd_Argv (n+1));
		return;
	}

	if (!cd.valid)
	{
		Con_Printf("No CD in player.\n");
		return;
	}

	if (Q_strcasecmp(command, "play") == 0)
	{
		CDAudio_Play(Q_atoi(Cmd_Argv (2)), false);
		return;
	}

	if (Q_strcasecmp(command, "loop") == 0)
	{
		CDAudio_Play(Q_atoi(Cmd_Argv (2)), true);
		return;
	}

	if (Q_strcasecmp(command, "stop") == 0)
	{
		CDAudio_Stop();
		return;
	}

	if (Q_strcasecmp(command, "resume") == 0)
	{
		CDAudio_Resume();
		return;
	}

	if (Q_strcasecmp(command, "eject") == 0)
	{
		if (playing)
			CDAudio_Stop();
		CDAudio_Eject();
		cd.valid = false;
		return;
	}

	if (Q_strcasecmp(command, "info") == 0)
	{
		Con_Printf("%u tracks\n", cd.highTrack - cd.lowTrack + 1);
		for (n = cd.lowTrack; n <= cd.highTrack; n++)
		{
			ret = CDAudio_GetAudioTrackInfo (n, &startAddress);
			Con_Printf("Track %2u: %s at %2u:%02u\n", n, ret ? "data " : "music", (startAddress >> 16) & 0xff, (startAddress >> 8) & 0xff);
		}
		if (playing)
			Con_Printf("Currently %s track %u\n", playLooping ? "looping" : "playing", playTrack);
		Con_Printf("Volume is %u\n", cdvolume);
		CDAudio_MediaChange();
		Con_Printf("Status %04x\n", cdRequest->status & 0xffff);
		return;
	}
}


void CDAudio_Update(void)
{
	int		ret;
	int		newVolume;
	static	double lastUpdate;

	if (!initialized || !enabled)
		return;

	if ((realtime - lastUpdate) < 0.25)
		return;
	lastUpdate = realtime;

	if (mediaCheck)
	{
		static	double lastCheck;

		if ((realtime - lastCheck) < 5.0)
			return;
		lastCheck = realtime;

		ret = CDAudio_MediaChange();
		if (ret == MEDIA_CHANGED)
		{
			Con_DPrintf("CDAudio: media changed\n");
			playing = false;
			wasPlaying = false;
			cd.valid = false;
			CDAudio_GetAudioDiskInfo();
			return;
		}
	}

	newVolume = (int)(bgmvolume.value * 255.0);
	if (newVolume < 0)
	{
		Cvar_SetValue ("bgmvolume", 0.0);
		newVolume = 0;
	}
	else if (newVolume > 255)
	{
		Cvar_SetValue ("bgmvolume", 1.0);
		newVolume = 255;
	}
	if (cdvolume != newVolume)
		CDAudio_SetVolume (newVolume);

	if (playing)
	{
		CDAudio_GetAudioStatus();
		if ((cdRequest->status & STATUS_BUSY_BIT) == 0)
		{
			playing = false;
			if (playLooping)
				CDAudio_Play(playTrack, true);
		}
	}
}


qboolean CDAudio_Playing(void)
{
	return playing;
}


int CDAudio_Init(void)
{
	char	*memory;
	int		n;

	if (cls.state == ca_dedicated)
		return -1;

	if (COM_CheckParm("-nocdaudio"))
		return -1;

	if (COM_CheckParm("-cdmediacheck"))
		mediaCheck = true;

	regs.x.ax = 0x1500;
	regs.x.bx = 0;
	dos_int86 (0x2f);
	if (regs.x.bx == 0)
	{
		Con_NotifyBox (
			"MSCDEX not loaded, music is\n"
			"disabled.  Use \"-nocdaudio\" if you\n"
			"wish to avoid this message in the\n"
			"future.  See README.TXT for help.\n"
			);			
		return -1;
	}
	if (regs.x.bx > 1)
		Con_DPrintf("CDAudio_Init: First CD-ROM drive will be used\n");
	cdrom = regs.x.cx;

	regs.x.ax = 0x150c;
	regs.x.bx = 0;
	dos_int86 (0x2f);
	if (regs.x.bx == 0)
	{
		Con_NotifyBox (
			"MSCDEX version 2.00 or later\n"
			"required for music. See README.TXT\n"
			"for help.\n"
			);			
		Con_DPrintf("CDAudio_Init: MSCDEX version 2.00 or later required.\n");
		return -1;
	}

	memory = dos_getmemory(sizeof(struct cd_request
) + sizeof(union readInfo_u));
	if (memory == NULL)
	{
		Con_DPrintf("CDAudio_Init: Unable to allocate low memory.\n");
		return -1;
	}

	cdRequest = (struct cd_request *)memory;
	cdRequestSegment = ptr2real(cdRequest) >> 4;
	cdRequestOffset = ptr2real(cdRequest) & 0xf;

	readInfo = (union readInfo_u *)(memory + sizeof(struct cd_request));
	readInfoSegment = ptr2real(readInfo) >> 4;
	readInfoOffset = ptr2real(readInfo) & 0xf;

	for (n = 0; n < 256; n++)
		remap[n] = n;
	initialized = true;

	CDAudio_SetVolume (255);
	if (CDAudio_GetAudioDiskInfo())
	{
		Con_Printf("CDAudio_Init: No CD in player.\n");
		enabled = false;
	}

	Cmd_AddCommand ("cd", CD_f);

	Con_Printf("CD Audio Initialized\n");

	return 0;
}


void CDAudio_Shutdown(void)
{
	if (!initialized)
		return;
	CDAudio_Stop();
}
