/*
 * cd_sdl.c
 *
 * Copyright (C) 1996-1997  Id Software, Inc.
 * Taken from the Twilight project with modifications
 * to make it work with Hexen II: Hammer of Thyrion.
 *
 * 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.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */


#if defined(SDL_FRAMEWORK) || defined(NO_SDL_CONFIG)
#if defined(USE_SDL2)
#include <SDL2/SDL.h>
#else
#include <SDL/SDL.h>
#endif
#else
#include "SDL.h"
#endif

#ifndef	SDL_INIT_CDROM

/* SDL dropped support for
   cd audio since v1.3.0 */
#pragma message("Warning: SDL CDAudio support disabled")
#include "cd_null.c"

#else	/* SDL_INIT_CDROM */

#include "quakedef.hpp"

static bool cdValid = false;
static bool	playing = false;
static bool	wasPlaying = false;
static bool	enabled = true;
static bool playLooping = false;
static byte	remap[100];
static byte	playTrack;
static double	endOfTrack = -1.0, pausetime = -1.0;
static SDL_CD	*cd_handle;
static int	cd_dev = -1;
static float	old_cdvolume;
static bool	hw_vol_works = true;


static void CDAudio_Eject(void)
{
	if (!cd_handle || !enabled)
		return;

#ifdef __linux__
	SDL_CDStop(cd_handle);	/* see CDAudio_Stop() */
#endif
	if (SDL_CDEject(cd_handle) < 0)
		console::info ("Unable to eject CD-ROM: %s\n", SDL_GetError ());
}

static int CDAudio_GetAudioDiskInfo(void)
{
	cdValid = false;

	if (!cd_handle)
		return -1;

	if ( ! CD_INDRIVE(SDL_CDStatus(cd_handle)) )
		return -1;

	cdValid = true;

	return 0;
}

int CDAudio_Play(byte track, bool looping)
{
	int	len_m, len_s, len_f;

	if (!cd_handle || !enabled)
		return -1;

	if (!cdValid)
	{
		CDAudio_GetAudioDiskInfo();
		if (!cdValid)
			return -1;
	}

	track = remap[track];

	if (track < 1 || track > cd_handle->numtracks)
	{
		console::info ("CDAudio_Play: Bad track number %d.\n", track);
		return -1;
	}

	if (cd_handle->track[track-1].type == SDL_DATA_TRACK)
	{
		console::info ("CDAudio_Play: track %d is not audio\n", track);
		return -1;
	}

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

	if (SDL_CDPlay(cd_handle, cd_handle->track[track-1].offset, cd_handle->track[track-1].length) < 0)
	{
		console::info ("CDAudio_Play: Unable to play track %d: %s\n", track, SDL_GetError ());
		return -1;
	}

	playLooping = looping;
	playTrack = track;
	playing = true;

	FRAMES_TO_MSF(cd_handle->track[track-1].length, &len_m, &len_s, &len_f);
	endOfTrack = realtime + ((double)len_m * 60.0) + (double)len_s + (double)len_f / (double)CD_FPS;

	/* Add the pregap for the next track.  This means that disc-at-once CDs
	 * won't loop smoothly, but they wouldn't anyway so it doesn't really
	 * matter.  SDL doesn't give us pregap information anyway, so you'll
	 * just have to live with it.  */
	endOfTrack += 2.0;
	pausetime = -1.0;

	if (bgmvolume.value == 0) /* don't bother advancing */
		CDAudio_Pause ();

	return 0;
}

void CDAudio_Stop(void)
{
	if (!cd_handle || !enabled)
		return;

	if (!playing)
		return;

#ifdef __linux__
	/* Don't really stop, but just pause: On some devices, the CDROMSTOP
	 * ioctl causes any followup ioctls to fail for a considerable time.
	 * observed with a TSSTcorp CDW/DVD SH-M522C drive with TS05 and TS08
	 * firmware versions running under a 2.6.27.25 kernel, and with a
	 * Samsung DVD r/w drive running under 2.6.35.6 kernel.
	 * Therefore, avoid dead stops if playback may be resumed shortly. */
	if (SDL_CDPause(cd_handle) < 0)
		console::info ("CDAudio_Stop: Unable to stop CD-ROM (%s)\n", SDL_GetError());
#else
	if (SDL_CDStop(cd_handle) < 0)
		console::info ("CDAudio_Stop: Unable to stop CD-ROM (%s)\n", SDL_GetError());
#endif

	wasPlaying = false;
	playing = false;
	pausetime = -1.0;
	endOfTrack = -1.0;
}

void CDAudio_Pause(void)
{
	if (!cd_handle || !enabled)
		return;

	if (!playing)
		return;

	if (SDL_CDPause(cd_handle) < 0)
		console::info ("Unable to pause CD-ROM: %s\n", SDL_GetError());

	wasPlaying = playing;
	playing = false;
	pausetime = realtime;
}

void CDAudio_Resume(void)
{
	if (!cd_handle || !enabled)
		return;

	if (!cdValid)
		return;

	if (!wasPlaying)
		return;

	if (SDL_CDResume(cd_handle) < 0)
		console::info ("Unable to resume CD-ROM: %s\n", SDL_GetError());
	playing = true;
	endOfTrack += realtime - pausetime;
	pausetime = -1.0;
}

static int get_first_audiotrk (void)
{
	int i;
	for (i = 0; i < cd_handle->numtracks; ++i)
		if (cd_handle->track[i].type != SDL_DATA_TRACK)
			return ++i;
	return 1;
}

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

	if (command::argc() < 2)
	{
		console::info("commands:");
		console::info("on, off, reset, remap, \n");
		console::info("play, stop, loop, pause, resume\n");
		console::info("eject, info, next, prev\n");
		return;
	}

	command = command::argv (1)->c_str();

	if (std::is_eq(common::strcasecmp(command, "on")))
	{
		enabled = true;
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "off")))
	{
		if (playing)
			CDAudio_Stop();
		enabled = false;
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "reset")))
	{
		enabled = true;
		if (playing)
			CDAudio_Stop();
		for (n = 0; n < 100; n++)
			remap[n] = n;
		CDAudio_GetAudioDiskInfo();
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "remap")))
	{
		ret = command::argc() - 2;
		if (ret <= 0)
		{
			for (n = 1; n < 100; n++)
				if (remap[n] != n)
					console::info("  %u -> %u\n", n, remap[n]);
			return;
		}
		for (n = 1; n <= ret; n++)
			remap[n] = atoi(command::argv (n + 1)->c_str());
		return;
	}

	if (!cdValid)
	{
		CDAudio_GetAudioDiskInfo();
		if (!cdValid)
		{
			console::info("No CD in player.\n");
			return;
		}
	}

	if (std::is_eq(common::strcasecmp(command, "play")))
	{
		n = atoi(command::argv (2)->c_str());
		if (n == 0)
			n = 1;
		CDAudio_Play((byte)n, false);
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "loop")))
	{
		CDAudio_Play((byte)atoi(command::argv (2)->c_str()), true);
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "stop")))
	{
		CDAudio_Stop();
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "pause")))
	{
		CDAudio_Pause();
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "resume")))
	{
		CDAudio_Resume();
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "eject")))
	{
		if (playing)
			CDAudio_Stop();
		CDAudio_Eject();
		cdValid = false;
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "info")))
	{
		int	current_min, current_sec, current_frame;
		int	length_min, length_sec, length_frame;

		console::info ("%u tracks\n", cd_handle->numtracks);

		if (playing)
			console::info("Currently %s track %u\n", playLooping ? "looping" : "playing", playTrack);
		else if (wasPlaying)
			console::info("Paused %s track %u\n", playLooping ? "looping" : "playing", playTrack);

		if (playing || wasPlaying)
		{
			SDL_CDStatus(cd_handle);
			FRAMES_TO_MSF(cd_handle->cur_frame, &current_min, &current_sec, &current_frame);
			FRAMES_TO_MSF(cd_handle->track[playTrack-1].length, &length_min, &length_sec, &length_frame);

			console::info ("Current position: %d:%02d.%02d (of %d:%02d.%02d)\n",
						current_min, current_sec, current_frame * 60 / CD_FPS,
						length_min, length_sec, length_frame * 60 / CD_FPS);
		}
		console::info("Volume is %f\n", bgmvolume.value);

		return;
	}

	if (std::is_eq(common::strcasecmp(command, "next")))
	{
		if (playTrack == cd_handle->numtracks)
			playTrack = get_first_audiotrk() - 1;
		CDAudio_Play(playTrack + 1, playLooping);
		return;
	}

	if (std::is_eq(common::strcasecmp(command, "prev")))
	{
		if (playTrack == get_first_audiotrk())
			playTrack = cd_handle->numtracks + 1;
		CDAudio_Play(playTrack - 1, playLooping);
		return;
	}

	console::info ("cd: unknown command \"%s\"\n", command);
}

static bool CD_GetVolume (void *unused)
{
/* FIXME: write proper code in here when SDL
   supports cdrom volume control some day. */
	return false;
}

static bool CD_SetVolume (void *unused)
{
/* FIXME: write proper code in here when SDL
   supports cdrom volume control some day. */
	return false;
}

static bool CDAudio_SetVolume (float value)
{
	if (!cd_handle || !enabled)
		return false;

	old_cdvolume = value;

	if (value == 0.0f)
		CDAudio_Pause ();
	else
		CDAudio_Resume();

	if (!hw_vol_works)
	{
		return false;
	}
	else
	{
/* FIXME: write proper code in here when SDL
   supports cdrom volume control some day. */
		return CD_SetVolume (NULL);
	}
}

void CDAudio_Update(void)
{
	CDstatus	curstat;
/*	static double	lastchk;*/

	if (!cd_handle || !enabled)
		return;

	if (old_cdvolume != bgmvolume.value)
		CDAudio_SetVolume (bgmvolume.value);

/*	if (playing && realtime > lastchk)*/
	if (playing && realtime > endOfTrack)
	{
	/*	lastchk = realtime + 2;*/ /* two seconds between chks */
		curstat = SDL_CDStatus(cd_handle);
		if (curstat != CD_PLAYING && curstat != CD_PAUSED)
		{
			playing = false;
			endOfTrack = -1.0;
			if (playLooping)
				CDAudio_Play(playTrack, true);
		}
	}
}

static const char *get_cddev_arg (const char *arg)
{
#if defined(_WIN32)
/* arg should be like "D:\", make sure it is so,
 * but tolerate args like "D" or "D:", as well. */
	static char drive[4];
	if (!arg || ! *arg)
		return NULL;
	if (arg[1] != '\0')
	{
		if (arg[1] != ':')
			return NULL;
		if (arg[2] != '\0')
		{
			if (arg[2] != '\\' &&
			    arg[2] != '/')
				return NULL;
			if (arg[3] != '\0')
				return NULL;
		}
	}
	if (*arg >= 'A' && *arg <= 'Z')
	{
		drive[0] = *arg;
		drive[1] = ':';
		drive[2] = '\\';
		drive[3] = '\0';
		return drive;
	}
	else if (*arg >= 'a' && *arg <= 'z')
	{
	/* make it uppercase for SDL */
		drive[0] = *arg - ('a' - 'A');
		drive[1] = ':';
		drive[2] = '\\';
		drive[3] = '\0';
		return drive;
	}
	return NULL;
#else
	if (!arg || ! *arg)
		return NULL;
	return arg;
#endif
}

static void export_cddev_arg (void)
{
/* Bad ugly hack to workaround SDL's cdrom device detection.
 * not needed for windows due to the way SDL_cdrom works. */
#if !defined(_WIN32)
	auto i = common::check_param("-cddev");
	if (i.has_value() && i.value() < common::_argv.size() - 1 && common::_argv[i.value()+1][0] != '\0')
	{
		static char arg[64];
		q_snprintf(arg, sizeof(arg), "SDL_CDROM=%s", common::_argv[i.value()+1].c_str());
		putenv(arg);
	}
#endif
}

int CDAudio_Init(void)
{
	if (common::safe_mode || common::check_param("-nocdaudio").has_value())
		return -1;

	export_cddev_arg();

	if (SDL_InitSubSystem(SDL_INIT_CDROM) < 0)
	{
		console::info("Couldn't init SDL cdrom: %s\n", SDL_GetError());
		return -1;
	}

	const int sdl_num_drives = SDL_CDNumDrives();
	console::info ("SDL detected %d CD-ROM drive%c\n", sdl_num_drives,
					sdl_num_drives == 1 ? ' ' : 's');

	if (sdl_num_drives < 1)
		return -1;

	if (const auto dev = common::check_param("-cddev"); dev.has_value() && dev.value() < common::_argv.size() - 1)
	{
		const char *userdev = get_cddev_arg(common::_argv[dev.value()+1].c_str());
		if (!userdev)
		{
			console::info("Invalid argument to -cddev\n");
			return -1;
		}
		for (auto i = 0; i < sdl_num_drives; i++)
		{
			if (std::is_eq(common::strcasecmp(SDL_CDName(i), userdev)))
			{
				cd_dev = i;
				break;
			}
		}
		if (cd_dev == -1)
		{
			console::info("SDL couldn't find cdrom device %s\n", userdev);
			return -1;
		}
	}

	if (cd_dev == -1)
		cd_dev = 0;	/* default drive */

	cd_handle = SDL_CDOpen(cd_dev);
	if (!cd_handle)
	{
		console::info ("CDAudio_Init: Unable to open CD-ROM drive %s (%s)\n",
				SDL_CDName(cd_dev), SDL_GetError());
		return -1;
	}

	for (auto i = 0; i < 100; i++)
		remap[i] = i;
	enabled = true;
	old_cdvolume = bgmvolume.value;

	console::info("CDAudio initialized (SDL, using %s)\n", SDL_CDName(cd_dev));

	if (CDAudio_GetAudioDiskInfo())
	{
		console::info("CDAudio_Init: No CD in drive\n");
		cdValid = false;
	}

	command::add ("cd", CD_f);

	hw_vol_works = CD_GetVolume (NULL); /* no SDL support at present. */
	if (hw_vol_works)
		hw_vol_works = CDAudio_SetVolume (bgmvolume.value);

	return 0;
}

void CDAudio_Shutdown(void)
{
	if (!cd_handle)
		return;
	CDAudio_Stop();
	if (hw_vol_works)
		CD_SetVolume (NULL); /* no SDL support at present. */
#ifdef __linux__
	SDL_CDStop(cd_handle);	/* see CDAudio_Stop() */
#endif
	SDL_CDClose(cd_handle);
	cd_handle = NULL;
	cd_dev = -1;
	SDL_QuitSubSystem(SDL_INIT_CDROM);
}

#endif	/* SDL_INIT_CDROM */

