/*
 * $Id: cdrom.c,v 1.13 1997/12/19 04:42:35 dirk Exp $
 *
 * This file is part of WorkMan, the civilized CD player program
 * (c) 1991-1997 by Steven Grimm (original author)
 * (c) by Dirk F"orsterling (current 'author' = maintainer)
 * The maintainer can be contacted by his e-mail address:
 * milliByte@DeathsDoor.com 
 *
 * 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.
 *
 * Interface between most of WorkMan and the low-level CD-ROM library
 * routines defined in plat_*.c and drv_*.c.  The goal is to have no
 * platform- or drive-dependent code here.
 */

static char cdrom_id[] = "$Id: cdrom.c,v 1.13 1997/12/19 04:42:35 dirk Exp $";

#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/time.h>

#include "struct.h"
#include "config.h"
#include "proto.h"

#ifdef CAN_CLOSE
#include <fcntl.h>
/* extern char *cd_device; this will be dfined as (char *) */
#endif


extern struct wm_drive generic_proto, toshiba_proto, sony_proto,
	toshiba33_proto;

/*
 * The supported drive types are listed here.  NULL means match anything.
 * The first match in the list is used, and substring matches are done (so
 * put long names before their shorter prefixes.)
 */
struct drivelist {
	char		*ven;
	char		*mod;
	char		*rev;
	struct wm_drive	*proto;
} drives[] = {
{	"TOSHIBA",	"XM-3501",	NULL,	&toshiba_proto		},
{	"TOSHIBA",	"XM-3401",	NULL,	&toshiba_proto		},
{	"TOSHIBA",	"XM-3301",	NULL,	&toshiba_proto		},
{	"SONY",		"CDU-8012",	NULL,	&sony_proto		},
{	NULL,		NULL,		NULL,	&generic_proto		}
};

extern struct play *playlist;
extern struct cdinfo_wm thiscd, *cd;

/*
 * Solaris 2.2 will remove the device out from under us.  Getting an ENOENT
 * is therefore sometimes not a problem.
 */
int	intermittent_dev = 0;

/*
 * Do we want to keep the CD device open after quitting by default?
 */
int	keep_open = 0;

#if defined DEFAULT_CD_DEVICE
char	*cd_device = DEFAULT_CD_DEVICE;
#else
char	*cd_device = NULL;
#endif

extern int cur_track, cur_index, cur_lasttrack, cur_firsttrack, cur_pos_abs,	
	cur_frame, cur_pos_rel, cur_tracklen, cur_cdlen, cur_ntracks,	
	cur_nsections, cur_listno, cur_stopmode, exit_on_eject,
	cur_balance;
extern enum cd_modes cur_cdmode;
extern char *cur_artist, *cur_cdname, *cur_trackname;
extern char	cur_contd, cur_avoid;

struct wm_drive	drive = { -1, "", "", NULL, NULL };

/*
 * Figure out which prototype drive structure we should be using based
 * on the vendor, model, and revision of the current drive.
 */
struct wm_drive *
find_drive_struct(vendor, model, rev)
	char	*vendor, *model, *rev;
{
	struct drivelist	*d;

	for (d = drives; d; d++)
	{
		if( ( (d->ven != NULL) && strncmp(d->ven, vendor, strlen(d->ven)) ) ||
		    ( (d->mod != NULL) && strncmp(d->mod, model, strlen(d->mod)) ) ||
		    ( (d->rev != NULL) && strncmp(d->rev, rev, strlen(d->rev)) ) )
			continue;
		
		if (d->proto->vendor[0] == '\0')
			strcpy(d->proto->vendor, vendor);
		if (d->proto->model[0] == '\0')
			strcpy(d->proto->model, model);

		return (d->proto);
	}

	return (NULL);	/* this means the list is badly terminated. */
}

/*
 * read_toc()
 *
 * Read the table of contents from the CD.  Return a pointer to a cdinfo_wm
 * struct containing the relevant information (minus artist/cdname/etc.)
 * This is a static struct.  Returns NULL if there was an error.
 *
 * XXX allocates one trackinfo too many.
 */
struct cdinfo_wm *
read_toc()
{
	struct playlist		*l;
	int			i, pos;

	if ((drive.get_trackcount)(&drive, &thiscd.ntracks) < 0)
	{
		perror("trackcount");
		return (NULL);
	}

	thiscd.artist[0] = thiscd.cdname[0] = '\0';
	thiscd.whichdb = thiscd.otherrc = thiscd.otherdb = thiscd.user = NULL;
	thiscd.length = 0;
	thiscd.autoplay = thiscd.playmode = thiscd.volume = 0;

	/* Free up any left-over playlists. */
	if (thiscd.lists != NULL)
	{
		for (l = thiscd.lists; l->name != NULL; l++)
		{
			free(l->name);
			free(l->list);
		}
		free(thiscd.lists);
		thiscd.lists = NULL;
	}

	if (thiscd.trk != NULL)
		free(thiscd.trk);

	thiscd.trk = malloc((thiscd.ntracks + 1) * sizeof(struct trackinfo));
	if (thiscd.trk == NULL)
	{
		perror("malloc");
		return (NULL);
	}

	for (i = 0; i < thiscd.ntracks; i++)
	{
		if ((drive.get_trackinfo)(&drive, i + 1, &thiscd.trk[i].data,
					&thiscd.trk[i].start) < 0)
		{
			perror("CD track info read");
			return (NULL);
		}

		thiscd.trk[i].avoid = thiscd.trk[i].data;
		thiscd.trk[i].length = thiscd.trk[i].start / 75;

		thiscd.trk[i].songname = thiscd.trk[i].otherrc =
		thiscd.trk[i].otherdb = NULL;
		thiscd.trk[i].contd = 0;
		thiscd.trk[i].volume = 0;
		thiscd.trk[i].track = i + 1;
		thiscd.trk[i].section = 0;
	}

	if ((drive.get_cdlen)(&drive, &thiscd.trk[i].start) < 0)
	{
		perror("CD length read");
		return (NULL);
	}
	thiscd.trk[i].length = thiscd.trk[i].start / 75;

/* Now compute actual track lengths. */
	pos = thiscd.trk[0].length;

	for (i = 0; i < thiscd.ntracks; i++)
	{
		thiscd.trk[i].length = thiscd.trk[i+1].length - pos;
		pos = thiscd.trk[i+1].length;
		if (thiscd.trk[i].data)
			thiscd.trk[i].length = (thiscd.trk[i + 1].start -
				thiscd.trk[i].start) * 2;
		if (thiscd.trk[i].avoid)
			strmcpy(&thiscd.trk[i].songname, "DATA TRACK");
	}

	thiscd.length = thiscd.trk[thiscd.ntracks].length;

	return (&thiscd);
}

/*
 * cd_status()
 *
 * Return values:
 *
 *	0	No CD in drive.
 *	1	CD in drive.
 *	2	CD has just been inserted (TOC has been read)
 *
 * Updates cur_track, cur_pos_rel, cur_pos_abs and other variables.
 */
int
cd_status( void )
{
	static enum cd_modes	oldmode = UNKNOWN;
	enum cd_modes		mode;
	int			status, trackno;
	int			ret = 1;

	/* Open the drive.  This returns 1 if the device isn't ready. */

#if defined(linux) && defined(SBPCD_HACK)
	status = wmcd_open(&drive, 0);
#else
	status = wmcd_open(&drive);
#endif
	if (status < 0)
		return (status);
	if (status > 0)
		return (0);

	/* If the user hit the stop button, don't pass PLAYING as oldmode. */
	if (cur_cdmode == STOPPED)
		oldmode = STOPPED;

	if ((drive.get_drive_status)(&drive, oldmode, &mode, &cur_frame,
					&trackno, &cur_index) < 0)
	{
		perror("CD get drive status");
		return (-1);
	}
	oldmode = mode;

	if (mode == EJECTED || mode == UNKNOWN)
	{
		cur_cdmode = EJECTED;
		cur_track = -1;
		cur_cdlen = cur_tracklen = 1;
		cur_pos_abs = cur_pos_rel = cur_frame = 0;

		if (exit_on_eject)
			exit(0);

		return (0);
	}

	/* If there wasn't a CD before and there is now, learn about it. */
	if (cur_cdmode == EJECTED)
	{
		cur_pos_rel = cur_pos_abs = 0;

#if defined(linux) && defined(SBPCD_HACK)
		/*
		 * new code to re-open device if a new cd is inserted.
		 * this works around sbpcd and mcdx misbehaviour.
		 *
		 * I had to change wmcd_open, because for some reason
		 * closing the device here leads to "bad file number". 
		 * If I find another (more elegant) way to do this 
		 * (or if I changed the driver) I'll spread it.
		 *
		 * inserted by milliByte@DeathsDoor.com on 1997-06-10
		 */
		do {
		  status = wmcd_open( &drive, 1 );
		} while ( status != 0 );
#endif
		if ((cd = read_toc()) == NULL)
			if (exit_on_eject)
				exit(-1);
			else
				return (-1);

		cur_nsections = 0;
		cur_ntracks = cd->ntracks;
		cur_cdlen = cd->length;
		load();
		cur_artist = cd->artist;
		cur_cdname = cd->cdname;
		cur_cdmode = STOPPED;
		ret = 2;
	}

	switch (mode) {
	case PLAYING:
	case PAUSED:
		cur_pos_abs = cur_frame / 75;

		/* Only look up the current track number if necessary. */
		if (cur_track < 1 || cur_frame < cd->trk[cur_track-1].start ||
				cur_frame >= (cur_track >= cur_ntracks ?
				(cur_cdlen + 1) * 75 :
				cd->trk[cur_track].start))
		{
			cur_track = 0;
			while (cur_track < cur_ntracks && cur_frame >=
					cd->trk[cur_track].start)
				cur_track++;
		}
		if (cur_track >= 1 && trackno > cd->trk[cur_track-1].track)
			cur_track++;
		/* Fall through */

	case UNKNOWN:
		if (mode == UNKNOWN)
		{
			mode = STOPPED;
			cur_lasttrack = cur_firsttrack = -1;
		}
		/* Fall through */

	case STOPPED:
		if (cur_track >= 1 && cur_track <= cur_ntracks)
		{
			cur_trackname = cd->trk[cur_track-1].songname;
			cur_avoid = cd->trk[cur_track-1].avoid;
			cur_contd = cd->trk[cur_track-1].contd;
			cur_pos_rel = (cur_frame -
				cd->trk[cur_track-1].start) / 75;
			if (cur_pos_rel < 0)
				cur_pos_rel = -cur_pos_rel;
		}

		if (playlist != NULL && playlist[0].start)
		{
			cur_pos_abs -= cd->trk[playlist[cur_listno-1].
				start - 1].start / 75;
			cur_pos_abs += playlist[cur_listno-1].starttime;
		}
		if (cur_pos_abs < 0)
			cur_pos_abs = cur_frame = 0;

		if (cur_track < 1)
			cur_tracklen = cd->length;
		else
			cur_tracklen = cd->trk[cur_track-1].length;
		/* Fall through */

	case TRACK_DONE:
		cur_cdmode = mode;
		break;
	case FORWARD:
	case EJECTED:
		break;	
	}

	return (ret);
}

#undef CLIF_VOL 
#ifdef CLIF_VOL
/*
 * cd_volume(vol, bal, max)
 *
 * Set the volume levels.  "vol" and "bal" are the volume and balance knob
 * settings, respectively.  "max" is the maximum value of the volume knob
 * (the balance knob is assumed to always go from 0 to 20.)
 */
void
cd_volume(vol, bal, max)
	int	vol, bal, max;
{
	int	left, right, scale;

/*
 * Set "left" and "right" to volume-slider values accounting for the
 * balance setting.
 */
/*	printf("Vol = %d, Bal = %d, Max = %d\n", vol, bal, max);
*/

        vol  = (vol * 100 + max - 16) / max;  
	scale = (vol + 5) / 10;

        if (bal < 9)
	{
                right = vol - scale * (10 - bal);
#ifdef SYMETRIC_BALANCE
		left  = vol + scale * (10 - bal);
#else
		left = vol;
#endif
        }
	else if (bal > 11)
	{
#ifdef SYMETRIC_BALANCE		
                right = vol + scale * (bal - 10);
#else
		right = vol;
#endif
		left  = vol - scale * (bal - 10);
        }
	else
                left = right = vol;

/*
 * some plat_*.c is missing the limitation
 */
	left = left < 0 ? 0 : left > 100 ? 100 : left;
	right = right < 0 ? 0 : right > 100 ? 100 : right;
/*	printf("Left = %d, Right = %d\n", left, right);
*/
	(void) (drive.set_volume)(&drive, left, right);
} /* cd_volume() */

#else

/*
 * cd_volume(vol, bal, max)
 *
 * Set the volume levels.  "vol" and "bal" are the volume and balance knob
 * settings, respectively.  "max" is the maximum value of the volume knob
 * (the balance knob is assumed to always go from 0 to 20.)
 */
void
cd_volume( int vol, int bal, int max )
{
	int	left, right;

/*
 * Set "left" and "right" to volume-slider values accounting for the
 * balance setting.
 *
 * XXX - the maximum volume setting is assumed to be in the 20-30 range.
 */
	if (bal < 9)
		right = vol - (9 - bal) * 2;
	else
		right = vol;
	if (bal > 11)
		left = vol - (bal - 11) * 2;
	else
		left = vol;

	left = (left * 100 + max - 1) / max;
	right = (right * 100 + max - 1) / max;
	if (left > 100)
		left = 100;
	if (right > 100)
		right = 100;

	(void) (drive.set_volume)(&drive, left, right);
} /* cd_volume() */

#endif /* CLIF_VOL */


/*
 * pause_cd()
 *
 * Pause the CD, if it's in play mode.  If it's already paused, go back to
 * play mode.
 */
void
pause_cd( void )
{
	if (cur_cdmode == EJECTED)	/* do nothing if there's no CD! */
		return;

	switch (cur_cdmode) {
	case PLAYING:		/* playing */
		cur_cdmode = PAUSED;
		(drive.pause)(&drive);
		break;

	case PAUSED:		/* paused */
		cur_cdmode = PLAYING;
		(drive.resume)(&drive);
	default: /* */
		break;	
	}
}

/*
 * stop_cd()
 *
 * Stop the CD if it's not already stopped.
 */
void
stop_cd( void )
{
	if (cur_cdmode == EJECTED)
		return;

	if (cur_cdmode != STOPPED)
	{
		cur_lasttrack = cur_firsttrack = -1;
		cur_cdmode = STOPPED;
		(drive.stop)(&drive);
		cur_track = 1;
	}
}

/*
 * play_chunk(start, end)
 *
 * Play the CD from one position to another (both in frames.)
 */
void
play_chunk( int start, int end)
{
	if (cur_cdmode == EJECTED || cd == NULL)
		return;

	end--;
	if (start >= end)
		start = end-1;

	(drive.play)(&drive, start, end);
}

/*
 * play_cd(starttrack, pos, endtrack)
 *
 * Start playing the CD or jump to a new position.  "pos" is in seconds,
 * relative to start of track.
 */
void
play_cd( int start, int pos, int end )
{
	if (cur_cdmode == EJECTED || cd == NULL)
		return;

	cur_firsttrack = start;
	start--;
	end--;
	cur_lasttrack = end;

	play_chunk(cd->trk[start].start + pos * 75, end >= cur_ntracks ?
		cur_cdlen * 75 : cd->trk[end].start - 1);
}

/*
 * Set the offset into the current track and play.  -1 means end of track
 * (i.e., go to next track.)
 */
void
play_from_pos( int pos )
{
	if (pos == -1)
		if (cd)
			pos = cd->trk[cur_track - 1].length - 1;
	if (cur_cdmode == PLAYING)
		play_cd(cur_track, pos, playlist[cur_listno-1].end);
}

/*
 * Eject the current CD, if there is one, and set the mode to 5.
 *
 * Returns 0 on success, 1 if the CD couldn't be ejected, or 2 if the
 * CD contains a mounted filesystem.
 */
int
eject_cd( void )
{
	int	status;

	if (cur_cdmode == EJECTED)	/* Already ejected! */
		{
#ifdef CAN_CLOSE
		/* some CD-ROM drives will close the tray if the device
		   is closed and then re-opened. This works with my
		   CR-562, but YMMV...
		   Dave Holland <dave@zenda.demon.co.uk> */
		close(drive.fd);
		drive.fd=open(cd_device, O_RDONLY | O_NONBLOCK);
#endif
		return (0);
		}

	status = (drive.eject)(&drive);
	if (status < 0)
		if (status == -3)
			return (2);
		else
			return (1);
	
	if (exit_on_eject)
		exit(0);

	cur_track = -1;
	cur_cdlen = cur_tracklen = 1;
	cur_pos_abs = cur_pos_rel = cur_frame = 0;
	cur_cdmode = EJECTED;

	return (0);
}

/*
 * find_trkind(track, index)
 *
 * Start playing at a particular track and index, optionally using a particular
 * frame as a starting position.  Returns a frame number near the start of the
 * index mark if successful, 0 if the track/index didn't exist.
 *
 * This is made significantly more tedious (though probably easier to port)
 * by the fact that CDROMPLAYTRKIND doesn't work as advertised.  The routine
 * does a binary search of the track, terminating when the interval gets to
 * around 10 frames or when the next track is encountered, at which point
 * it's a fair bet the index in question doesn't exist.
 */
int
find_trkind( int track, int index, int start )
{
	int	top = 0, bottom, current, interval, ret = 0, i;

	if( cur_cdmode == EJECTED || cd == NULL )
		return ( 0 ); /* WARNING: was nothing */

	for (i = 0; i < cur_ntracks; i++)
		if (cd->trk[i].track == track)
			break;
	bottom = cd->trk[i].start;

	for (; i < cur_ntracks; i++)
		if (cd->trk[i].track > track)
			break;

	top = i == cur_ntracks ? (cd->length - 1) * 75 : cd->trk[i].start;

	if (start > bottom && start < top)
		bottom = start;

	current = (top + bottom) / 2;
	interval = (top - bottom) / 4;

	do {
		play_chunk(current, current + 75);

		if (cd_status() != 1)
			return (0);
		while (cur_frame < current)
			if (cd_status() != 1 || cur_cdmode != PLAYING)
				return (0);
			else
				susleep(1);

		if (cd->trk[cur_track - 1].track > track)
			break;

		if (cur_index >= index)
		{
			ret = current;
			current -= interval;
		}
		else
			current += interval;
		interval /= 2;
	} while (interval > 2);

	return (ret);
}

/*
 * Simulate usleep() using select().
 */
int
susleep( int usec )
{
	struct timeval	tv;

	timerclear(&tv);
	tv.tv_sec = usec / 1000000;
	tv.tv_usec = usec % 1000000;
	return (select(0, NULL, NULL, NULL, &tv));
}

/*
 * Read the initial volume from the drive, if available.  Set cur_balance to
 * the balance level (0-20, 10=centered) and return the proper setting for
 * the volume knob.
 *
 * "max" is the maximum value of the volume knob.
 */
int
read_initial_volume( int max )
{
	int	left, right;

	if ((drive.get_volume)(&drive, &left, &right) < 0 || left == -1)
		return (max);

	left = (left * max + 99) / 100;
	right = (right * max + 99) / 100;

	if (left < right)
	{
		cur_balance = (right - left) / 2 + 11;
		if (cur_balance > 20)
			cur_balance = 20;

		return (right);
	}
	else if (left == right)
	{
		cur_balance = 10;
		return (left);
	}
	else
	{
		cur_balance = (right - left) / 2 + 9;
		if (cur_balance < 0)
			cur_balance = 0;

		return (left);
	}
}

/*
 * Prototype wm_drive structure, with generic functions.  The generic functions
 * will be replaced with drive-specific functions as appropriate once the drive
 * type has been sensed.
 */
struct wm_drive generic_proto = {
	-1,			/* fd */
	"\0       ",		/* vendor */
	"\0               ",	/* model */
	NULL,			/* aux */
	NULL,			/* daux */

	gen_init,		/* functions... */
	gen_get_trackcount,
	gen_get_cdlen,
	gen_get_trackinfo,
	gen_get_drive_status,
	gen_get_volume,
	gen_set_volume,
	gen_pause,
	gen_resume,
	gen_stop,
	gen_play,
	gen_eject
};
