/*
 *   cda - Command-line CD Audio Player
 *
 *   Copyright (C) 1993-2001  Ti Kan
 *   E-mail: xmcd@amb.org
 *
 *   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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */
#ifndef LINT
static char *_cda_c_ident_ = "@(#)cda.c	6.191 01/07/02";
#endif

#include "common_d/appenv.h"
#include "common_d/util.h"
#include "common_d/patchlevel.h"
#include "libdi_d/libdi.h"
#include "cdinfo_d/cdinfo.h"
#include "cda_d/cda.h"
#include "cda_d/visual.h"
#include "cda_d/userreg.h"

#if !defined(USE_TERMIOS) && !defined(USE_TERMIO) && !defined(USE_SGTTY)
#define USE_TERMIOS	/* Make USE_TERMIOS the default if not specified */
#endif

#ifdef USE_TERMIOS
#include <termios.h>
#endif
#ifdef USE_TERMIO
#include <termio.h>
#endif
#ifdef USE_SGTTY
#include <sgtty.h>
#endif

#define PGM_SEPCHAR	','			/* Program seq separator */
#define MAX_PKT_RETRIES	200			/* Max retries reading pkt */
#define PIPE_TIMEOUT	5			/* 5 seconds to time out */


extern char		*ttyname();

appdata_t		app_data;		/* Option settings */
cdinfo_incore_t		*dbp;			/* CD information pointer */
curstat_t		status;			/* Current CD player status */
char			*emsgp,			/* Error msg for use on exit */
			**cda_devlist,		/* Device list table */
			errmsg[ERR_BUF_SZ],	/* Error message buffer */
			spipe[FILE_PATH_SZ],	/* Send pipe path */
			rpipe[FILE_PATH_SZ];	/* Receive pipe path */
int			cda_sfd[2] = {-1,-1},	/* Send pipe file desc */
			cda_rfd[2] = {-1,-1};	/* Receive pipe file desc */
pid_t			daemon_pid;		/* CDA daemon pid */
FILE			*errfp;			/* Error message stream */

STATIC int		cont_delay = 1,		/* Status display interval */
			inetoffln;		/* Inet Offline cmd line arg */
STATIC dev_t		cd_rdev;		/* CD device number */
STATIC bool_t		isdaemon,		/* Am I the daemon process */
			stat_cont,		/* Continuous display status */
			batch,			/* Non-interactive */
			hist_initted;		/* History list initialized */
STATIC FILE		*ttyfp;			/* /dev/tty */
STATIC cdinfo_client_t	cdinfo_cldata;		/* Client info for libcdinfo */
STATIC di_client_t	di_cldata;		/* Client info for libdi */
STATIC char		dlock[FILE_PATH_SZ];	/* Daemon lock file path */
STATIC cdapkt_t		*curr_pkt;		/* Current received packet */
#ifndef NOVISUAL
STATIC bool_t		visual;			/* Visual (curses) mode */
#endif


/***********************
 *  internal routines  *
 ***********************/


/*
 * onsig0
 *	Signal handler to shut down application.
 *
 * Args:
 *	signo - The signal number.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
onsig0(int signo)
{
	cda_quit(&status);
	exit(3);
}


/*
 * onsig1
 *	SIGPIPE signal handler.
 *
 * Args:
 *	signo - The signal number.
 *
 * Return:
 *	Nothing.
 */
STATIC void
onsig1(int signo)
{
	/* Do nothing except re-arm the handler */
	(void) signal(signo, onsig1);
}


/*
 * onintr
 *	Signal handler to stop continuous status display.
 *
 * Args:
 *	signo - The signal number.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
onintr(int signo)
{
	(void) signal(signo, SIG_IGN);
	stat_cont = FALSE;
	(void) printf("\r");
}


/*
 * cda_pgm_parse
 *	Parse the shuffle/program mode play sequence text string, and
 *	update the playorder table in the curstat_t structure.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	TRUE=success, FALSE=error.
 */
/*ARGSUSED*/
STATIC bool_t
cda_pgm_parse(curstat_t *s)
{
	int		i,
			progtot,
			retcode;
	char		*p,
			*q,
			*tmpbuf;
	word32_t	*arg;
	bool_t		last = FALSE,
			ret = TRUE;

	if (dbp->playorder == NULL)
		/* Nothing to do */
		return TRUE;

	arg = (word32_t *) MEM_ALLOC(
		"pgm_parse_arg",
		CDA_NARGS * sizeof(word32_t)
	);
	tmpbuf = (char *) MEM_ALLOC(
		"pgm_parse_tmpbuf",
		strlen(dbp->playorder) + 1
	);
	if (arg == NULL || tmpbuf == NULL) {
		CDA_FATAL(app_data.str_nomemory);
		return FALSE;
	}

	(void) strcpy(tmpbuf, dbp->playorder);

	(void) memset(arg, 0, CDA_NARGS * sizeof(word32_t));
	progtot = 0;

	for (i = 0, p = q = tmpbuf; i < MAXTRACK; i++, p = ++q) {
		/* Skip p to the next digit */
		for (; !isdigit((int) *p) && *p != '\0'; p++)
			;

		if (*p == '\0')
			/* No more to do */
			break;

		/* Skip q to the next non-digit */
		for (q = p; isdigit((int) *q); q++)
			;

		if (*q == PGM_SEPCHAR)
			*q = '\0';
		else if (*q == '\0')
			last = TRUE;
		else {
			MEM_FREE(arg);
			MEM_FREE(tmpbuf);

			return FALSE;
		}

		if (q > p) {
			arg[i+1] = (word32_t) atoi(p);
			progtot++;
		}

		if (last)
			break;
	}

	if (progtot > 0) {
		arg[0] = -progtot;
		ret = cda_sendcmd(CDA_PROGRAM, arg, &retcode);
	}

	MEM_FREE(arg);
	MEM_FREE(tmpbuf);

	return (ret);
}


/*
 * cda_mkdirs
 *	Called at startup time to create some needed directories, if
 *	they aren't already there.
 *	Currently these are:
 *		$HOME/.xmcdcfg
 *		$HOME/.xmcdcfg/prog
 *		/tmp/.cdaudio
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_mkdirs(void)
{
	int		ret;
	pid_t		cpid;
	waitret_t	stat_val;
	char		*errmsg,
			*homepath,
			path[FILE_PATH_SZ + 16];
	struct stat	stbuf;

#ifndef NOMKTMPDIR
	errmsg = (char *) MEM_ALLOC(
		"errmsg",
		strlen(app_data.str_tmpdirerr) + strlen(TEMP_DIR)
	);
	if (errmsg == NULL) {
		CDA_FATAL(app_data.str_nomemory);
		return;
	}

	/* Make temporary directory, if needed */
	(void) sprintf(errmsg, app_data.str_tmpdirerr, TEMP_DIR);
	if (LSTAT(TEMP_DIR, &stbuf) < 0) {
		if (!util_mkdir(TEMP_DIR, 0777)) {
			CDA_FATAL(errmsg);
			return;
		}
	}
	else if (!S_ISDIR(stbuf.st_mode)) {
		CDA_FATAL(errmsg);
		return;
	}

	MEM_FREE(errmsg);
#endif	/* NOMKTMPDIR */

	homepath = util_homedir(util_get_ouid());
	if (homepath == NULL) {
		/* No home directory: shrug */
		return;
	}
	if ((int) strlen(homepath) >= FILE_PATH_SZ) {
		CDA_FATAL(app_data.str_longpatherr);
		return;
	}

	switch (cpid = FORK()) {
	case 0:
		/* Child process */
		DBGPRN(DBG_GEN)(errfp, "\nSetting uid to %d, gid to %d\n",
			(int) util_get_ouid(), (int) util_get_ogid());

		/* Force uid and gid to original setting */
		if (setuid(util_get_ouid()) < 0 ||
		    setgid(util_get_ogid()) < 0)
			exit(1);

		/* Create the per-user config directory */
		(void) sprintf(path, USR_CFG_PATH, homepath);
		if (LSTAT(path, &stbuf) < 0) {
			if (errno == ENOENT && !util_mkdir(path, 0755)) {
				DBGPRN(DBG_GEN)(errfp,
					"cd_mkdirs: cannot mkdir %s.\n", path);
			}
			else {
				DBGPRN(DBG_GEN)(errfp,
					"cd_mkdirs: cannot stat %s.\n", path);
			}
		}
		else if (!S_ISDIR(stbuf.st_mode)) {
			DBGPRN(DBG_GEN)(errfp,
				"cd_mkdirs: %s is not a directory.\n", path);
		}

		/* Create the per-user track program directory */
		(void) sprintf(path, USR_PROG_PATH, homepath);
		if (LSTAT(path, &stbuf) < 0) {
			if (errno == ENOENT && !util_mkdir(path, 0755)) {
				DBGPRN(DBG_GEN)(errfp,
					"cd_mkdirs: cannot mkdir %s.\n", path);
			}
			else {
				DBGPRN(DBG_GEN)(errfp,
					"cd_mkdirs: cannot stat %s.\n", path);
			}
		}
		else if (!S_ISDIR(stbuf.st_mode)) {
			DBGPRN(DBG_GEN)(errfp,
				"cd_mkdirs: %s is not a directory.\n", path);
		}

		exit(0);
		/*NOTREACHED*/

	case -1:
		/* fork failed */
		break;

	default:
		/* Parent: wait for child to finish */
		while ((ret = WAITPID(cpid, &stat_val, 0)) != cpid) {
			if (ret < 0)
				break;
		}
		break;
	}
}


/*
 * cda_init
 *	Initialize the CD interface subsystem.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_init(curstat_t *s)
{
	char	*bdevname,
		str[FILE_PATH_SZ * 2];

	bdevname = util_basename(app_data.device);

	/* Set address to cdinfo incore structure */
	dbp = cdinfo_addr();

	/* Get system-wide device-specific configuration parameters */
	(void) sprintf(str, SYS_DSCFG_PATH, app_data.libdir, bdevname);
	di_devspec_parmload(str, TRUE);

	/* Get user device-specific configuration parameters */
	(void) sprintf(str, USR_DSCFG_PATH, util_homedir(util_get_ouid()),
		       bdevname);
	di_devspec_parmload(str, FALSE);

	/* Initialize CD info */
	cda_dbclear(s, FALSE);

	if (isdaemon) {
		/* Initialize the CD hardware */
		di_cldata.curstat_addr = curstat_addr;
		di_cldata.quit = cda_quit;
		di_cldata.dbclear = cda_dbclear;
		di_cldata.fatal_msg = cda_fatal_msg;
		di_cldata.warning_msg = cda_warning_msg;
		di_init(&di_cldata);

		/* Set default modes */
		di_repeat(s, app_data.repeat_mode);
		di_shuffle(s, app_data.shuffle_mode);
	}
	else
		di_reset_curstat(s, TRUE, TRUE);
}


/*
 * cda_start
 *	Secondary startup functions.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_start(curstat_t *s)
{
	/* Start up libutil */
	util_start();

	/* Start up I/O interface */
	di_start(s);

	/* Open FIFOs - daemon side */
	if ((cda_sfd[0] = open(spipe, O_RDONLY)) < 0) {
		perror("CD audio daemon: cannot open send pipe");
		cda_quit(s);
		exit(4);
	}
	if ((cda_rfd[0] = open(rpipe, O_WRONLY)) < 0) {
		perror("CD audio daemon: cannot open recv pipe");
		cda_quit(s);
		exit(5);
	}
}


/*
 * cda_daemonlock
 *	Check and set a lock to prevent multiple CD audio daemon
 *	processes.
 *
 * Args:
 *	None.
 *
 * Return:
 *	TRUE - Lock successful
 *	FALSE - Daemon already running
 */
STATIC bool_t
cda_daemonlock(void)
{
	int		fd;
	pid_t		pid,
			mypid;
	char		buf[12];

	(void) sprintf(dlock, "%s/cdad.%x", TEMP_DIR, (int) cd_rdev);
	mypid = getpid();

	for (;;) {
		fd = open(dlock, O_CREAT | O_EXCL | O_WRONLY);
		if (fd < 0) {
			if (errno == EEXIST) {
				if ((fd = open(dlock, O_RDONLY)) < 0)
					return FALSE;

				if (read(fd, buf, 12) > 0)
					pid = (pid_t) atoi(buf);
				else {
					(void) close(fd);
					return FALSE;
				}

				(void) close(fd);

				if (pid == mypid)
					/* Our own lock */
					return TRUE;

				if (pid <= 0 ||
				    (kill(pid, 0) < 0 && errno == ESRCH)) {
					/* Pid died, steal its lockfile */
					(void) UNLINK(dlock);
				}
				else {
					/* Pid still running: clash */
					return FALSE;
				}
			}
			else
				return FALSE;
		}
		else {
			(void) sprintf(buf, "%d\n", (int) mypid);
			(void) write(fd, buf, strlen(buf));

			(void) close(fd);
			(void) chmod(dlock, 0644);

			return TRUE;
		}
	}
}


/*
 * cda_poll
 *	Periodic polling function - used to manage program, shuffle,
 *	and repeat modes.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_poll(curstat_t *s)
{
	static int	n = 0;

	if (++n > 100)
		n = 0;

	if (n % 3)
		return;

	if (s->mode == MOD_NODISC || s->mode == MOD_BUSY ||
	    (s->mode == MOD_STOP && app_data.load_play))
		(void) di_check_disc(s);
	else if (s->mode != MOD_STOP && s->mode != MOD_PAUSE &&
		 s->mode != MOD_NODISC)
		di_status_upd(s);
}


/*
 * cda_sendpkt
 *	Write a CDA packet down the pipe.
 *
 * Args:
 *	name - The text string describing the caller module
 *	fd - Pipe file descriptor
 *	s - Pointer to the packet data
 *
 * Return:
 *	TRUE - pipe write successful
 *	FALSE - pipe write failed
 */
STATIC bool_t
cda_sendpkt(char *name, int fd, cdapkt_t *s)
{
	byte_t	*p = (byte_t *) s;
	int	i,
		ret;

	if (fd < 0)
		return FALSE;

	/* Brand packet with magic number */
	s->magic = CDA_MAGIC;

	/* Set timeout */
	(void) signal(SIGALRM, onsig1);
	(void) alarm(PIPE_TIMEOUT);

	/* Send a packet */
	i = sizeof(cdapkt_t);
	while ((ret = write(fd, p, i)) < i) {
		if (ret < 0) {
			switch (errno) {
			case EINTR:
			case EPIPE:
			case EBADF:
				(void) alarm(0);

				if (isdaemon)
					return FALSE;

				if (!cda_daemon_alive())
					return FALSE;
				
				(void) signal(SIGALRM, onsig1);
				(void) alarm(PIPE_TIMEOUT);
				break;
			default:
				(void) alarm(0);
				(void) sprintf(errmsg,
					"%s: packet write error (errno=%d)\n",
					name, errno);
				CDA_WARNING(errmsg);
				return FALSE;
			}
		}
		else if (ret == 0) {
			(void) alarm(0);
			(void) sleep(1);
			(void) signal(SIGALRM, onsig1);
			(void) alarm(PIPE_TIMEOUT);
		}
		else {
			i -= ret;
			p += ret;
		}
	}

	(void) alarm(0);
	return TRUE;
}


/*
 * cda_getpkt
 *	Read a CDA packet from the pipe.
 *
 * Args:
 *	name - The text string describing the caller module
 *	fd - Pipe file descriptor
 *	s - Pointer to the packet data
 *
 * Return:
 *	TRUE - pipe read successful
 *	FALSE - pipe read failed
 */
STATIC bool_t
cda_getpkt(char *name, int fd, cdapkt_t *r)
{
	byte_t	*p = (byte_t *) r;
	int	i,
		ret;

	if (fd < 0)
		return FALSE;

	/* Get a packet */
	i = sizeof(cdapkt_t);
	while ((ret = read(fd, p, i)) < i) {
		if (ret < 0) {
			switch (errno) {
			case EINTR:
			case EPIPE:
			case EBADF:
				if (!isdaemon)
					return FALSE;

				/* Use this occasion to perform
				 * polling function
				 */
				cda_poll(&status);

				(void) sleep(1);
				break;
			default:
				(void) sprintf(errmsg,
					"%s: packet read error (errno=%d)\n",
					name, errno);
				CDA_WARNING(errmsg);
				return FALSE;
			}
		}
		else if (ret == 0) {
			/* Use this occasion to perform polling function */
			if (isdaemon)
				cda_poll(&status);

			(void) sleep(1);
		}
		else {
			i -= ret;
			p += ret;
		}
	}

	/* Check packet for magic number */
	if (r->magic != CDA_MAGIC) {
		(void) sprintf(errmsg, "%s: bad packet magic number.", name);
		CDA_WARNING(errmsg);
		return FALSE;
	}

	return TRUE;
}


/*
 * cda_do_on
 *	Perform the "on" and "off" commands.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
cda_do_onoff(curstat_t *s, cdapkt_t *p)
{
	p->arg[0] = getpid();
}


/*
 * cda_do_disc
 *	Perform the "disc" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_disc(curstat_t *s, cdapkt_t *p)
{
	int	i;

	if (s->mode == MOD_BUSY)
		p->retcode = CDA_INVALID;
	else if (p->arg[0] == 0) {
		/* Load */
		if (s->mode == MOD_NODISC)
			di_load_eject(s);
		else
			p->retcode = CDA_INVALID;
	}
	else if (p->arg[0] == 1) {
		/* Eject */
		if (s->mode != MOD_NODISC)
			di_load_eject(s);
		else
			p->retcode = CDA_INVALID;
	}
	else if (p->arg[0] == 2) {
		/* Next */
		if (s->cur_disc < s->last_disc) {
			s->prev_disc = s->cur_disc;
			s->cur_disc++;
			di_chgdisc(s);
		}
		else
			p->retcode = CDA_INVALID;
	}
	else if (p->arg[0] == 3) {
		/* Prev */
		if (s->cur_disc > s->first_disc) {
			s->prev_disc = s->cur_disc;
			s->cur_disc--;
			di_chgdisc(s);
		}
		else
			p->retcode = CDA_INVALID;
	}
	else if (p->arg[0] == 4) {
		/* disc# */
		i = p->arg[1];
		if (i >= s->first_disc && i <= s->last_disc) {
			if (s->cur_disc != i) {
				s->prev_disc = s->cur_disc;
				s->cur_disc = i;
				di_chgdisc(s);
			}
		}
		else
			p->retcode = CDA_INVALID;
	}
}


/*
 * cda_do_lock
 *	Perform the "lock" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_lock(curstat_t *s, cdapkt_t *p)
{
	if (s->mode == MOD_NODISC || s->mode == MOD_BUSY)
		p->retcode = CDA_INVALID;
	else if (p->arg[0] == 0) {
		/* Unlock */
		if (s->caddy_lock) {
			s->caddy_lock = FALSE;
			di_lock(s, FALSE);
		}
		else
			p->retcode = CDA_INVALID;
	}
	else if (p->arg[0] == 1) {
		/* Lock */
		if (!s->caddy_lock) {
			s->caddy_lock = TRUE;
			di_lock(s, TRUE);
		}
		else
			p->retcode = CDA_INVALID;
	}
	else
		p->retcode = CDA_INVALID;
}


/*
 * cda_do_play
 *	Perform the "play" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_play(curstat_t *s, cdapkt_t *p)
{
	int		i;
	word32_t	blkno;
	bool_t		stopfirst;

	switch (s->mode) {
	case MOD_PLAY:
		stopfirst = TRUE;
		break;
	case MOD_BUSY:
	case MOD_NODISC:
		p->retcode = CDA_INVALID;
		return;
	default:
		stopfirst = FALSE;
		break;
	}

	/* Starting track number */
	i = -1;
	if (p->arg[0] != 0) {
		if (s->shuffle || s->prog_cnt > 0) {
			p->retcode = CDA_INVALID;
			return;
		}

		for (i = 0; i < (int) s->tot_trks; i++) {
			if (s->trkinfo[i].trkno == p->arg[0])
				break;
		}

		if (i >= (int) s->tot_trks) {
			/* Invalid track specified */
			p->retcode = CDA_PARMERR;
			return;
		}

		s->cur_trk = p->arg[0];
	}

	if (stopfirst) {
		/* Stop current playback first */
		di_stop(s, FALSE);

		/*
		 * Restore s->cur_trk value because di_stop() zaps it
		 */
		if (p->arg[0] != 0)
			s->cur_trk = p->arg[0];
	}

	if (p->arg[0] != 0 && (int) p->arg[1] >= 0 && (int) p->arg[2] >= 0) {
		/* Track offset specified */
		if (p->arg[2] > 59) {
			p->retcode = CDA_PARMERR;
			return;
		}

		util_msftoblk(
			(byte_t) p->arg[1],
			(byte_t) p->arg[2],
			0,
			&blkno,
			0
		);

		if (blkno >= (s->trkinfo[i+1].addr - s->trkinfo[i].addr)) {
			p->retcode = CDA_PARMERR;
			return;
		}

		s->curpos_trk.addr = blkno;
		s->curpos_trk.min = (byte_t) p->arg[1];
		s->curpos_trk.sec = (byte_t) p->arg[2];
		s->curpos_trk.frame = 0;

		s->curpos_tot.addr = s->trkinfo[i].addr + s->curpos_trk.addr;
		util_blktomsf(
			s->curpos_tot.addr,
			&s->curpos_tot.min,
			&s->curpos_tot.sec,
			&s->curpos_tot.frame,
			MSF_OFFSET
		);
	}

	/* Start playback */
	di_play_pause(s);
}


/*
 * cda_do_pause
 *	Perform the "pause" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_pause(curstat_t *s, cdapkt_t *p)
{
	if (s->mode == MOD_PLAY)
		di_play_pause(s);
	else
		p->retcode = CDA_INVALID;
}


/*
 * cda_do_stop
 *	Perform the "stop" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_stop(curstat_t *s, cdapkt_t *p)
{
	if (s->mode == MOD_BUSY)
		p->retcode = CDA_INVALID;
	else
		di_stop(s, TRUE);
}


/*
 * cda_do_track
 *	Perform the "track" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_track(curstat_t *s, cdapkt_t *p)
{
	int	i;

	if (p->arg[0] == 0) {
		/* Previous track */
		if (s->mode == MOD_PLAY) {
			if ((i = di_curtrk_pos(s)) > 0)
				s->curpos_tot.addr = s->trkinfo[i].addr;
			di_prevtrk(s);
		}
		else
			p->retcode = CDA_INVALID;
	}
	else {
		/* Next track */
		if (s->mode == MOD_PLAY)
			di_nexttrk(s);
		else
			p->retcode = CDA_INVALID;
	}
}


/*
 * cda_do_index
 *	Perform the "index" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_index(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Previous index */
		if (s->mode == MOD_PLAY && s->prog_tot <= 0) {
			if (s->cur_idx > 1)
				s->curpos_tot.addr = s->sav_iaddr;
			di_previdx(s);
		}
		else
			p->retcode = CDA_INVALID;
	}
	else {
		/* Next index */
		if (s->mode == MOD_PLAY && s->prog_tot <= 0)
			di_nextidx(s);
		else
			p->retcode = CDA_INVALID;
	}
}


/*
 * cda_do_program
 *	Perform the "program" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_program(curstat_t *s, cdapkt_t *p)
{
	int		i,
			j;
	cdinfo_ret_t	ret;

	if (s->mode == MOD_NODISC || s->mode == MOD_BUSY)
		p->retcode = CDA_INVALID;
	else if ((int) p->arg[0] == 1) {
		/* Query */
		if ((int) s->prog_tot > (CDA_NARGS - 2)) {
			p->retcode = CDA_INVALID;
			return;
		}
		p->arg[0] = (word32_t) s->prog_tot;
		p->arg[1] = (word32_t) s->prog_cnt;
		p->arg[2] = (word32_t) -1;

		for (i = 0; i < (int) s->prog_tot; i++) {
			p->arg[i+2] = (word32_t)
				s->trkinfo[s->playorder[i]].trkno;
		}
	}
	else if ((int) p->arg[0] == 2) {
		/* Save */
		if (s->shuffle) {
			p->retcode = CDA_INVALID;
			return;
		}

		p->arg[0] = (word32_t) 0;
		p->arg[2] = (word32_t) -2;
		if (!s->program) {
			p->arg[2] = (word32_t) -1;
			return;
		}

		if ((ret = cdinfo_save_prog()) != 0) {
			DBGPRN(DBG_CDI)(errfp,
				"cdinfo_save_prog: status=%d arg=%d\n",
				CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
		}
	}
	else if ((int) p->arg[0] == 0) {
		/* Clear */
		if (s->shuffle) {
			/* program and shuffle modes are mutually-exclusive */
			p->retcode = CDA_INVALID;
			return;
		}

		p->arg[1] = (word32_t) 0;
		p->arg[2] = (word32_t) -3;
		s->prog_tot = 0;
		s->program = FALSE;

		if ((ret = cdinfo_del_prog()) != 0) {
			DBGPRN(DBG_CDI)(errfp,
				"cdinfo_del_prog: status=%d arg=%d\n",
				CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
		}

		if (dbp->playorder != NULL) {
			MEM_FREE(dbp->playorder);
			dbp->playorder = NULL;
		}
	}
	else if ((int) p->arg[0] < 0) {
		/* Define */
		if (s->shuffle) {
			/* program and shuffle modes are mutually-exclusive */
			p->retcode = CDA_INVALID;
			return;
		}

		s->prog_tot = -(p->arg[0]);
		s->program = TRUE;

		if (dbp->playorder != NULL) {
			MEM_FREE(dbp->playorder);
			dbp->playorder = NULL;
		}

		for (i = 0; i < (int) s->prog_tot; i++) {
			char	num[8];

			for (j = 0; j < (int) s->tot_trks; j++) {
				if (s->trkinfo[j].trkno == p->arg[i+1])
					break;
			}

			if (j >= (int) s->tot_trks) {
				s->prog_tot = 0;
				s->program = FALSE;
				p->retcode = CDA_PARMERR;
				return;
			}

			s->playorder[i] = j;

			(void) sprintf(num, "%d", (int) s->trkinfo[j].trkno);
			if (dbp->playorder == NULL) {
				if (!util_newstr(&dbp->playorder, num)) {
					p->retcode = CDA_PARMERR;
					return;
				}
			}
			else {
				dbp->playorder = (char *) MEM_REALLOC(
				    "playorder",
				    dbp->playorder,
				    strlen(dbp->playorder) + strlen(num) + 2
				);
				if (dbp->playorder == NULL) {
					p->retcode = CDA_PARMERR;
					return;
				}
				(void) sprintf(dbp->playorder, "%s,%s",
					       dbp->playorder, num);
			}
		}
	}
	else
		p->retcode = CDA_PARMERR;
}


/*
 * cda_do_shuffle
 *	Perform the "shuffle" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_shuffle(curstat_t *s, cdapkt_t *p)
{
	if (s->program) {
		p->retcode = CDA_INVALID;
		return;
	}

	if (s->mode != MOD_NODISC && s->mode != MOD_STOP) {
		p->retcode = CDA_INVALID;
		return;
	}

	di_shuffle(s, (bool_t) (p->arg[0] == 1));
}


/*
 * cda_do_repeat
 *	Perform the "repeat" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_repeat(curstat_t *s, cdapkt_t *p)
{
	di_repeat(s, (bool_t) (p->arg[0] == 1));
}


/*
 * cda_do_volume
 *	Perform the "volume" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_volume(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0)
		/* Query */
		p->arg[1] = (word32_t) s->level;
	else if ((int) p->arg[1] >= 0 && (int) p->arg[1] <= 100)
		/* Set */
		di_level(s, (byte_t) p->arg[1], FALSE);
	else
		p->retcode = CDA_PARMERR;
}


/*
 * cda_do_balance
 *	Perform the "balance" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_balance(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Query */
		p->arg[1] = (word32_t)
		    ((int) (s->level_right - s->level_left) / 2) + 50;
	}
	else if ((int) p->arg[1] == 50) {
		/* Center setting */
		s->level_left = s->level_right = 100;
		di_level(s, (byte_t) s->level, FALSE);
	}
	else if ((int) p->arg[1] < 50 && (int) p->arg[1] >= 0) {
		/* Attenuate the right channel */
		s->level_left = 100;
		s->level_right = 100 + (((int) p->arg[1] - 50) * 2);
		di_level(s, (byte_t) s->level, FALSE);
	}
	else if ((int) p->arg[1] > 50 && (int) p->arg[1] <= 100) {
		/* Attenuate the left channel */
		s->level_left = 100 - (((int) p->arg[1] - 50) * 2);
		s->level_right = 100;
		di_level(s, (byte_t) s->level, FALSE);
	}
	else
		p->retcode = CDA_PARMERR;
}


/*
 * cda_do_route
 *	Perform the "route" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_route(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Query */
		p->arg[1] = (word32_t) app_data.ch_route;
	}
	else if ((int) p->arg[1] >= 0 && (int) p->arg[1] <= 4) {
		/* Set */
		app_data.ch_route = (int) p->arg[1];
		di_route(s);
	}
	else
		p->retcode = CDA_PARMERR;
}


/*
 * cda_do_status
 *	Perform the "status" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_status(curstat_t *s, cdapkt_t *p)
{
	word32_t	discid;

	/* Initialize */
	(void) memset(p->arg, 0, CDA_NARGS * sizeof(word32_t));

	WR_ARG_MODE(p->arg[0], s->mode);

	if (s->caddy_lock)
		WR_ARG_LOCK(p->arg[0]);
	if (s->shuffle)
		WR_ARG_SHUF(p->arg[0]);
	if (s->repeat)
		WR_ARG_REPT(p->arg[0]);
	if (s->program)
		WR_ARG_PROG(p->arg[0]);

	WR_ARG_TRK(p->arg[1], s->cur_trk);
	WR_ARG_IDX(p->arg[1], s->cur_idx);
	WR_ARG_MIN(p->arg[1], s->curpos_trk.min);
	WR_ARG_SEC(p->arg[1], s->curpos_trk.sec);

	if (s->repeat && s->mode == MOD_PLAY)
		p->arg[2] = (word32_t) s->rptcnt;
	else
		p->arg[2] = (word32_t) -1;

	p->arg[3] = (word32_t) s->level;
	p->arg[4] = (word32_t)
		    ((int) (s->level_right - s->level_left) / 2) + 50;
	p->arg[5] = (word32_t) app_data.ch_route;
	if (s->mode == MOD_NODISC || s->mode == MOD_BUSY)
		discid = 0;
	else
		discid = cdinfo_discid(s);

	dbp->discid = discid;
	p->arg[7] = s->cur_disc;
	p->arg[6] = discid;
}


/*
 * cda_do_toc
 *	Perform the "toc" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_toc(curstat_t *s, cdapkt_t *p)
{
	int		i,
			j;
	word32_t	min,
			sec;
	bool_t		offsets;

	if (s->mode == MOD_NODISC || s->mode == MOD_BUSY) {
		p->retcode = CDA_INVALID;
		return;
	}

	offsets = (p->arg[0] == 1);

	/* Initialize */
	(void) memset(p->arg, 0, CDA_NARGS * sizeof(word32_t));

	p->arg[0] = cdinfo_discid(s);

	if (offsets) {
		for (i = 0; i < (int) s->tot_trks; i++) {
			WR_ARG_TOC(p->arg[i+1], s->trkinfo[i].trkno,
				   s->cur_trk,
				   s->trkinfo[i].min,
				   s->trkinfo[i].sec);
		}
	}
	else {
		for (i = 0; i < (int) s->tot_trks; i++) {
			j = ((s->trkinfo[i+1].min * 60 +
			      s->trkinfo[i+1].sec) - 
			     (s->trkinfo[i].min * 60 +
			      s->trkinfo[i].sec));
			min = j / 60;
			sec = j % 60;

			WR_ARG_TOC(p->arg[i+1], s->trkinfo[i].trkno,
				   s->cur_trk, min, sec);
		}
	}

	/* Lead-out track */
	WR_ARG_TOC(p->arg[i+1], s->trkinfo[i].trkno,
		   0, s->trkinfo[i].min, s->trkinfo[i].sec);
}


/*
 * cda_doc_toc2
 *	Perform the "toc2" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_toc2(curstat_t *s, cdapkt_t *p)
{
	int	i;

	if (s->mode == MOD_NODISC || s->mode == MOD_BUSY) {
		p->retcode = CDA_INVALID;
		return;
	}

	/* Initialize */
	(void) memset(p->arg, 0, CDA_NARGS * sizeof(word32_t));

	p->arg[0] = cdinfo_discid(s);

	for (i = 0; i < (int) s->tot_trks; i++) {
		WR_ARG_TOC2(p->arg[i+1], s->trkinfo[i].trkno,
			   s->trkinfo[i].min,
			   s->trkinfo[i].sec,
			   s->trkinfo[i].frame
		);
	}

	/* Lead-out track */
	WR_ARG_TOC2(p->arg[i+1], s->trkinfo[i].trkno,
		   s->trkinfo[i].min,
		   s->trkinfo[i].sec,
		   s->trkinfo[i].frame
	);
}


/*
 * cda_do_extinfo
 *	Perform the "extinfo" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_extinfo(curstat_t *s, cdapkt_t *p)
{
	int	i,
		j;

	if (s->mode == MOD_NODISC || s->mode == MOD_BUSY) {
		p->retcode = CDA_INVALID;
		return;
	}

	p->arg[0] = cdinfo_discid(s);
	p->arg[2] = (word32_t) -1;

	if ((int) p->arg[1] == -1) {
		if (s->mode != MOD_PLAY) {
			p->arg[1] = p->arg[2] = (word32_t) -1;
			return;
		}

		p->arg[1] = (word32_t) s->cur_trk;
		j = (int) s->cur_trk;
	}
	else
		j = (int) p->arg[1];

	for (i = 0; i < (int) s->tot_trks; i++) {
		if ((int) s->trkinfo[i].trkno == j)
			break;
	}
	if (i < (int) s->tot_trks)
		p->arg[2] = i;
	else
		p->retcode = CDA_PARMERR;
}


/*
 * cda_doc_on_load
 *	Perform the "on-load" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
cda_do_on_load(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Query */
		p->arg[1] = (word32_t) app_data.caddy_lock;
		p->arg[2] = (word32_t) app_data.load_spindown;
		p->arg[3] = (word32_t) app_data.load_play;
	}
	else {
		switch (p->arg[1]) {
		case 0:/* noautolock */
			app_data.caddy_lock = FALSE;
			break;
		case 1: /* autolock */
			app_data.caddy_lock = TRUE;
			break;
		case 2: /* none */
			app_data.load_spindown = FALSE;
			app_data.load_play = FALSE;
			break;
		case 3: /* spindown */
			app_data.load_spindown = TRUE;
			app_data.load_play = FALSE;
			break;
		case 4: /* autoplay */
			app_data.load_spindown = FALSE;
			app_data.load_play = TRUE;
			break;
		default:
			p->retcode = CDA_PARMERR;
			break;
		}
	}
}


/*
 * cda_do_on_exit
 *	Perform the "on-exit" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
cda_do_on_exit(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Query */
		p->arg[1] = (word32_t) app_data.exit_stop;
		p->arg[2] = (word32_t) app_data.exit_eject;
	}
	else {
		switch (p->arg[1]) {
		case 0: /* none */
			app_data.exit_stop = FALSE;
			app_data.exit_eject = FALSE;
			break;
		case 1: /* autostop */
			app_data.exit_stop = TRUE;
			app_data.exit_eject = FALSE;
			break;
		case 2: /* autoeject */
			app_data.exit_stop = FALSE;
			app_data.exit_eject = TRUE;
			break;
		default:
			p->retcode = CDA_PARMERR;
			break;
		}
	}
}


/*
 * cda_do_on_done
 *	Perform the "on-done" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
cda_do_on_done(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Query */
		p->arg[1] = (word32_t) app_data.done_eject;
		p->arg[2] = (word32_t) app_data.done_exit;
	}
	else {
		switch (p->arg[1]) {
		case 0:	/* noautoeject */
			app_data.done_eject = FALSE;
			break;
		case 1: /* autoeject */
			app_data.done_eject = TRUE;
			break;
		case 2: /* noautoexit */
			app_data.done_exit = FALSE;
			break;
		case 3: /* autoexit */
			app_data.done_exit = TRUE;
			break;
		default:
			p->retcode = CDA_PARMERR;
			break;
		}
	}
}


/*
 * cda_do_on_eject
 *	Perform the "on-eject" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
cda_do_on_eject(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Query */
		p->arg[1] = (word32_t) app_data.eject_exit;
	}
	else {
		/* set [no]autoeject */
		app_data.eject_exit = (p->arg[1] == 0) ? FALSE : TRUE;
	}
}


/*
 * cda_do_chgr
 *	Perform the "changer" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
cda_do_chgr(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Query */
		p->arg[1] = (word32_t) app_data.multi_play;
		p->arg[2] = (word32_t) app_data.reverse;
	}
	else {
		switch (p->arg[1]) {
		case 0:	/* nomultiplay */
			app_data.multi_play = FALSE;
			app_data.reverse = FALSE;
			break;
		case 1: /* multiplay */
			app_data.multi_play = TRUE;
			break;
		case 2: /* noreverse */
			app_data.reverse = FALSE;
			break;
		case 3: /* reverse */
			app_data.multi_play = TRUE;
			app_data.reverse = TRUE;
			break;
		default:
			p->retcode = CDA_PARMERR;
			break;
		}
	}
}


/*
 * cda_do_device
 *	Perform the "device" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
STATIC void
cda_do_device(curstat_t *s, cdapkt_t *p)
{
	(void) sprintf((char *) p->arg, "CD-ROM: %s %s (%s)\n%s",
		    s->vendor, s->prod, s->revnum, di_methodstr());
}


/*
 * cda_do_version
 *	Perform the "version" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
cda_do_version(curstat_t *s, cdapkt_t *p)
{
	(void) sprintf((char *) p->arg, "%s.%s%s PL%d\n%s",
		    VERSION_MAJ, VERSION_MIN, VERSION_EXT,
		    PATCHLEVEL, di_methodstr());
}


/*
 * cda_do_debug
 *	Perform the "debug" command.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
STATIC void
cda_do_debug(curstat_t *s, cdapkt_t *p)
{
	if (p->arg[0] == 0) {
		/* Query */
		p->arg[1] = (word32_t) app_data.debug;
	}
	else {
		/* Set level */
		app_data.debug = (word32_t) p->arg[1];
	}
}


/*
 * prn_program
 *	Print current program sequence, if any.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_program(word32_t arg[])
{
	int	i;

	if ((int) arg[0] > 0) {
		(void) printf("Current program:");
		for (i = 0; i < arg[0]; i++)
			(void) printf(" %d", (int) arg[i+2]);
		(void) printf("\n");

		if ((int) arg[1] > 0) {
			(void) printf("Playing:        ");
			for (i = 0; i < (int) (arg[1] - 1); i++) {
				(void) printf("  %s",
					((int) arg[i+2] > 9) ? " " : "");
			}
			(void) printf(" %s^\n",
				((int) arg[i+2] > 9) ? " " : "");
		}
	}
	else if ((int) arg[0] == 0) {
		if ((int) arg[2] == -1)
			(void) printf("No program sequence defined.\n");
		else if ((int) arg[2] == -2)
			(void) printf("Program sequence saved.\n");
		else if ((int) arg[2] == -3)
			(void) printf("Program sequence cleared.\n");
	}
}


/*
 * prn_volume
 *	Print current volume setting.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_volume(word32_t arg[])
{
	if ((int) arg[0] == 0)
		(void) printf("Current volume: %d (range 0-100)\n",
			      (int) arg[1]);
}


/*
 * prn_balance
 *	Print current balance setting.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_balance(word32_t arg[])
{
	if ((int) arg[0] == 0) {
		(void) printf("Current balance: %d (range 0-100, center:50)\n",
			      (int) arg[1]);
	}
}


/*
 * prn_route
 *	Print current channel routing setting.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_route(word32_t arg[])
{
	if ((int) arg[0] == 0) {
		(void) printf("Current routing: %d ", (int) arg[1]);

		switch ((int) arg[1]) {
		case 0:
			(void) printf("(normal stereo)\n");
			break;
		case 1:
			(void) printf("(reverse stereo)\n");
			break;
		case 2:
			(void) printf("(mono-L)\n");
			break;
		case 3:
			(void) printf("(mono-R)\n");
			break;
		case 4:
			(void) printf("(mono-L+R)\n");
			break;
		}
	}
}


/*
 * prn_stat
 *	Print current CD status.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_stat(word32_t arg[])
{
	unsigned int	disc,
			trk,
			idx,
			min,
			sec;

	if (stat_cont)
		(void) printf("\r");

	switch (RD_ARG_MODE(arg[0])) {
	case MOD_BUSY:
		(void) printf("CD_Busy    disc:-  -- --  --:--");
		break;
	case MOD_NODISC:
		(void) printf("No_Disc    disc:-  -- --  --:--");
		break;
	case MOD_STOP:
		(void) printf("CD_Stopped disc:%u  -- --  --:--",
			      (unsigned int) arg[7]);
		break;
	case MOD_PLAY:
		disc = (unsigned int) arg[7];
		trk = (unsigned int) RD_ARG_TRK(arg[1]);
		idx = (unsigned int) RD_ARG_IDX(arg[1]);
		min = (unsigned int) RD_ARG_MIN(arg[1]);
		sec = (unsigned int) RD_ARG_SEC(arg[1]);

		if (idx == 0 && app_data.subq_lba) {
			/* In LBA mode the time display is not meaningful
			 * while in the lead-in, so just set to 0
			 */
			min = sec = 0;
		}

		(void) printf("CD_Playing disc:%u  %02u %02u %s%02u:%02u",
			      disc, trk, idx,
			      (idx == 0) ? "-" : "+", min, sec);
		break;
	case MOD_PAUSE:
		disc = (unsigned int) arg[7];
		trk = (unsigned int) RD_ARG_TRK(arg[1]);
		idx = (unsigned int) RD_ARG_IDX(arg[1]);
		min = (unsigned int) RD_ARG_MIN(arg[1]);
		sec = (unsigned int) RD_ARG_SEC(arg[1]);

		if (idx == 0 && app_data.subq_lba) {
			/* In LBA mode the time display is not meaningful
			 * while in the lead-in, so just set to 0
			 */
			min = sec = 0;
		}

		(void) printf("CD_Paused  disc:%u  %02u %02u %s%02u:%02u",
			      disc, trk, idx,
			      (idx == 0) ? "-" : "+", min, sec);
		break;
	default:
		(void) printf("Inv_status disc:-  -- --  --:--");
		break;
	}

	(void) printf(" %slock", RD_ARG_LOCK(arg[0]) ? "+" : "-");
	(void) printf(" %sshuf", RD_ARG_SHUF(arg[0]) ? "+" : "-");
	(void) printf(" %sprog", RD_ARG_PROG(arg[0]) ? "+" : "-");
	(void) printf(" %srept", RD_ARG_REPT(arg[0]) ? "+" : "-");

	if ((int) arg[2] >= 0)
		(void) printf(" %d", (int) arg[2]);
	else
		(void) printf(" -");

	if (!stat_cont)
		(void) printf("\n");
}


/*
 * prn_toc
 *	Print current CD Table Of Contents.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_toc(word32_t arg[])
{
	int		i;
	byte_t		ntrks,
			trkno,
			min,
			sec;
	bool_t		playing;
	curstat_t	*s = curstat_addr();

	ntrks = arg[0] & 0xff;

	(void) printf("Genre: %s %s\n",
		      (dbp->disc.genre == NULL) ?
			    "(unknown genre)" :
			    cdinfo_genre_name(dbp->disc.genre),
		      (dbp->disc.notes != NULL ||
		       dbp->disc.credit_list != NULL) ? "*" : "");

	(void) printf("%s%s%s\n\n",
		      (dbp->disc.artist == NULL) ? "" : dbp->disc.artist,
		      (dbp->disc.artist != NULL && dbp->disc.title != NULL) ?
			    " / " : "",
		      (dbp->disc.title == NULL) ?
			    "(unknown title)" : dbp->disc.title);

	for (i = 0; i < (int) ntrks; i++) {
		RD_ARG_TOC(arg[i+1], trkno, playing, min, sec);
		(void) printf("%s%02u %02u:%02u  ",
			      playing ? ">" : " ", trkno, min, sec);
		if (s->qmode == QMODE_MATCH && dbp->track[i].title != NULL)
			(void) printf("%s%s\n", dbp->track[i].title,
				      (dbp->track[i].notes != NULL ||
				       dbp->track[i].credit_list != NULL) ?
					"*" : "");
		else
			(void) printf("??%s\n",
				      (dbp->track[i].notes != NULL ||
				       dbp->track[i].credit_list != NULL) ?
					"*" : "");
	}

	RD_ARG_TOC(arg[i+1], trkno, playing, min, sec);
	(void) printf("\nTotal Time: %02u:%02u\n", min, sec);
}


/*
 * prn_extinfo
 *	Print current Disc and Track extended information.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_extinfo(word32_t arg[])
{
	curstat_t	*s = curstat_addr();
	int		i;
	cdinfo_credit_t	*p;

	if (s->qmode != QMODE_MATCH) {
		(void) printf("No information was found for this CD\n");
		return;
	}

	(void) printf("------- Album Information -------\n");

	(void) printf("Xmcd disc ID: %08x\n", dbp->discid);
	(void) printf("Artist: %s\n",
		      dbp->disc.artist == NULL ? "" : dbp->disc.artist);
	(void) printf("Title: %s\n",
		      dbp->disc.title == NULL ? "" : dbp->disc.title);
	(void) printf("Artist full name: %s%s%s%s%s\n",
		      dbp->disc.artistfname.lastname == NULL ? "" :
			dbp->disc.artistfname.lastname,
		      dbp->disc.artistfname.lastname == NULL ? "" : ", ",
		      dbp->disc.artistfname.firstname == NULL ? "" :
			dbp->disc.artistfname.firstname,
		      dbp->disc.artistfname.the == NULL ? "" : ", ",
		      dbp->disc.artistfname.the == NULL ? "" :
			dbp->disc.artistfname.the);
	(void) printf("Sort title: %s%s%s\n",
		      dbp->disc.sorttitle == NULL ? "" : dbp->disc.sorttitle,
		      dbp->disc.title_the == NULL ? "" : ", ",
		      dbp->disc.title_the == NULL ? "" :
			dbp->disc.title_the);
	(void) printf("Year: %s\n",
		      dbp->disc.year == NULL ? "" : dbp->disc.year);
	(void) printf("Record label: %s\n",
		      dbp->disc.label == NULL ? "" : dbp->disc.label);
	(void) printf("Compilation: %s\n",
		      dbp->disc.compilation ? "Yes" : "No");
	(void) printf("Genre 1: %s\n", cdinfo_genre_name(dbp->disc.genre));
	(void) printf("Genre 2: %s\n", cdinfo_genre_name(dbp->disc.genre2));
	(void) printf("Disc %s of %s\n",
		      dbp->disc.dnum == NULL ? "?" : dbp->disc.dnum,
		      dbp->disc.tnum == NULL ? "?" : dbp->disc.tnum);
	(void) printf("Credits:\n");
	for (p = dbp->disc.credit_list; p != NULL; p = p->next) {
		(void) printf("\t%s (%s)\n",
			      p->crinfo.name == NULL ? "unknown" :
				    p->crinfo.name,
			      p->crinfo.role == NULL ? "unknown" :
				    cdinfo_role_name(p->crinfo.role->id));
	}
	(void) printf("Region: %s\n",
		      dbp->disc.region == NULL ? "" :
		      cdinfo_region_name(dbp->disc.region));
	(void) printf("Revision: %s\n",
		      dbp->disc.revision == NULL ? "" : dbp->disc.revision);
	(void) printf("Certifier: %s\n",
		      dbp->disc.certifier == NULL ? "" : dbp->disc.certifier);

	if ((int) arg[1] < 0)
		return;

	(void) printf("\n------ Track %02d Information ------\n",
		      (int) arg[1]);

	i = (int) arg[2];
	(void) printf("Title: %s\n",
		      dbp->track[i].title == NULL ? "" : dbp->track[i].title);
	(void) printf("Sort Title: %s%s%s\n",
		      dbp->track[i].sorttitle == NULL ? "" :
			dbp->track[i].sorttitle,
		      dbp->track[i].title_the == NULL ? "" : ", ",
		      dbp->track[i].title_the == NULL ? "" :
			dbp->track[i].title_the);
	(void) printf("Artist: %s\n",
		      dbp->track[i].artist == NULL ? "" :
			dbp->track[i].artist);
	(void) printf("Artist full name: %s%s%s%s%s\n",
		      dbp->track[i].artistfname.lastname == NULL ? "" :
			dbp->track[i].artistfname.lastname,
		      dbp->track[i].artistfname.lastname == NULL ? "" : ", ",
		      dbp->track[i].artistfname.firstname == NULL ? "" :
			dbp->track[i].artistfname.firstname,
		      dbp->track[i].artistfname.the == NULL ? "" : ", ",
		      dbp->track[i].artistfname.the == NULL ? "" :
			dbp->track[i].artistfname.the);
	(void) printf("Year: %s\n",
		      dbp->track[i].year == NULL ? "" : dbp->track[i].year);
	(void) printf("Record Label: %s\n",
		      dbp->track[i].label == NULL ? "" : dbp->track[i].label);
	(void) printf("BPM: %s\n",
		      dbp->track[i].bpm == NULL ? "" : dbp->track[i].bpm);
	(void) printf("Genre 1: %s\n",
		      cdinfo_genre_name(dbp->track[i].genre));
	(void) printf("Genre 2: %s\n",
		      cdinfo_genre_name(dbp->track[i].genre2));
	(void) printf("Credits:\n");
	for (p = dbp->track[i].credit_list; p != NULL; p = p->next) {
		(void) printf("\t%s (%s)\n",
			      p->crinfo.name == NULL ? "unknown" :
				    p->crinfo.name,
			      p->crinfo.role == NULL ? "unknown" :
				    cdinfo_role_name(p->crinfo.role->id));
	}
}


/*
 * prn_notes
 *	Print current Disc and Track Notes.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_notes(word32_t arg[])
{
	int		i;
	curstat_t	*s = curstat_addr();

	if (s->qmode != QMODE_MATCH) {
		(void) printf("No information was found for this CD\n");
		return;
	}

	(void) printf("------- Album Notes -------\n");

	if (dbp->disc.notes == NULL)
		(void) printf("(none)\n");
	else {
		if (dbp->disc.title != NULL)
			(void) printf("%s\n\n", dbp->disc.title);
		else
			(void) printf("(unknown disc title)\n\n");

		(void) printf("%s\n", dbp->disc.notes);
	}

	if ((int) arg[1] < 0)
		return;

	(void) printf("\n------ Track %02d Notes ------\n",
		      (int) arg[1]);

	i = (int) arg[2];
	if (dbp->track[i].notes == NULL)
		(void) printf("(none)\n");
	else {
		if (dbp->track[i].title != NULL)
			(void) printf("%s\n\n", dbp->track[i].title);
		else
			(void) printf("(unknown track title)\n\n");

		(void) printf("%s\n", dbp->track[i].notes);
	}
}


 /*
 * prn_on_load
 *	Print on-load mode.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_on_load(word32_t arg[])
{
	if ((int) arg[0] == 0) {
		(void) printf("On load: %s%s%s\n",
			((int) arg[1] == 0) ? "noautolock" : "autolock",
			((int) arg[2] == 0) ? "" : " spindown",
			((int) arg[3] == 0) ? "" : " autoplay");
	}
}


/*
 * prn_on_exit
 *	Print on-exit mode.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_on_exit(word32_t arg[])
{
	if ((int) arg[0] == 0) {
		(void) printf("On exit: %s\n",
			((int) arg[1] == 0 && (int) arg[2] == 0) ? "none" : 
			((int) arg[1] == 0) ? "autoeject" : "autostop");
	}
}


/*
 * prn_on_done
 *	Print on-done mode.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_on_done(word32_t arg[])
{
	if ((int) arg[0] == 0) {
		(void) printf("On done:%s%s%s\n",
			((int) arg[1] == 0 && (int) arg[2] == 0) ?
				" none" : "",
			((int) arg[1] == 0) ? "" : " autoeject",
			((int) arg[2] == 0) ? "" : " autoexit");
	}
}


/*
 * prn_on_eject
 *	Print on-eject mode.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_on_eject(word32_t arg[])
{
	if ((int) arg[0] == 0) {
		(void) printf("On eject: %s\n",
			((int) arg[1] == 0) ? "none" : "autoexit");
	}
}


/*
 * prn_chgr
 *	Print changer mode.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_chgr(word32_t arg[])
{
	if ((int) arg[0] == 0) {
		(void) printf("Changer:%s%s%s\n",
			((int) arg[1] == 0 && (int) arg[2] == 0) ?
				" none" : "",
			((int) arg[1] == 0) ? "" : " multiplay",
			((int) arg[2] == 0) ? "" : " reverse");
	}
}


/*
 * prn_device
 *	Print device information.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_device(word32_t arg[])
{
	(void) printf("Device: %s\n", app_data.device);
	(void) printf("%s\n", (char *) arg);
}


/*
 * prn_version
 *	Print version number and other information.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_version(word32_t arg[])
{
	char	*ctrlver;

	ctrlver = cdinfo_cddbctrl_ver();
	(void) printf("CDA - Command Line CD Audio Player\n\n");
	(void) printf("CD audio        %s.%s%s PL%d\n",
		      VERSION_MAJ, VERSION_MIN, VERSION_EXT, PATCHLEVEL);
	(void) printf("CD audio daemon %s", (char *) arg);
	(void) printf("\n%s\nURL: %s\nE-mail: %s\n\n%s\n\n",
		      COPYRIGHT, XMCD_URL, EMAIL, GNU_BANNER);
	(void) printf("CDDB%s service%s%s\n%s\n",
		(cdinfo_cddb_ver() == 2) ? "\262" : " \"classic\"",
		(ctrlver[0] == '\0') ? "" : ": ",
		(ctrlver[0] == '\0') ? "\n" : ctrlver,
		CDDB_BANNER);
}


/*
 * prn_debug
 *	Print debug mode.
 *
 * Args:
 *	arg - Argument array from CD audio daemon response packet.
 *
 * Return:
 *	Nothing.
 */
STATIC void
prn_debug(word32_t arg[])
{
	(void) printf("Debug level is %s%u\n",
		((int) arg[0] == 0) ? "" : "now ", arg[1]);
}


/* Service function mapping table */
struct {
	word32_t	cmd;
	void		(*srvfunc)(curstat_t *, cdapkt_t *);
	void		(*prnfunc)(word32_t *);
} cmd_fmtab[] = {
	{ CDA_ON,		cda_do_onoff,		NULL		},
	{ CDA_OFF,		cda_do_onoff,		NULL		},
	{ CDA_DISC,		cda_do_disc,		NULL		},
	{ CDA_LOCK,		cda_do_lock,		NULL		},
	{ CDA_PLAY,		cda_do_play,		NULL		},
	{ CDA_PAUSE,		cda_do_pause,		NULL		},
	{ CDA_STOP,		cda_do_stop,		NULL		},
	{ CDA_TRACK,		cda_do_track,		NULL		},
	{ CDA_INDEX,		cda_do_index,		NULL		},
	{ CDA_PROGRAM,		cda_do_program,		prn_program	},
	{ CDA_SHUFFLE,		cda_do_shuffle,		NULL		},
	{ CDA_REPEAT,		cda_do_repeat,		NULL		},
	{ CDA_VOLUME,		cda_do_volume,		prn_volume	},
	{ CDA_BALANCE,		cda_do_balance,		prn_balance	},
	{ CDA_ROUTE,		cda_do_route,		prn_route	},
	{ CDA_STATUS,		cda_do_status,		NULL		},
	{ CDA_TOC,		cda_do_toc,		prn_toc		},
	{ CDA_TOC2,		cda_do_toc2,		NULL		},
	{ CDA_EXTINFO,		cda_do_extinfo,		prn_extinfo	},
	{ CDA_ON_LOAD,		cda_do_on_load,		prn_on_load	},
	{ CDA_ON_EXIT,		cda_do_on_exit,		prn_on_exit	},
	{ CDA_ON_DONE,		cda_do_on_done,		prn_on_done	},
	{ CDA_ON_EJECT,		cda_do_on_eject,	prn_on_eject	},
	{ CDA_CHGR,		cda_do_chgr,		prn_chgr	},
	{ CDA_DEVICE,		cda_do_device,		prn_device	},
	{ CDA_VERSION,		cda_do_version,		prn_version	},
	{ CDA_DEBUG,		cda_do_debug,		prn_debug	},
	{ CDA_NOTES,		cda_do_extinfo,		prn_notes	},
	{ CDA_CDDBREG,		NULL,			NULL		},
	{ CDA_CDDBHINT,		NULL,			NULL		},
	{ 0,			NULL,			NULL		}
};


/*
 * cda_docmd
 *	Perform the command received.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	p - Pointer to the CDA packet structure.
 *
 * Return:
 *	TRUE - Received a CDA_OFF command: daemon should shut down
 *	FALSE - Received normal command.
 */
STATIC bool_t
cda_docmd(curstat_t *s, cdapkt_t *p)
{
	int		i;
	static time_t	cur_time,
			prev_time;

	/* Default status */
	p->retcode = CDA_OK;

	/* Update CD status */
	if (p->cmd != CDA_OFF) {
		if (s->mode == MOD_NODISC || s->mode == MOD_BUSY ||
		    (s->mode == MOD_STOP && app_data.load_play))
			(void) di_check_disc(s);
		else if (s->mode != MOD_STOP && s->mode != MOD_PAUSE) {
			prev_time = cur_time;
			cur_time = time(NULL);

			if (cur_time != prev_time)
				di_status_upd(s);
		}
	}

	p->retcode = CDA_INVALID;

	/* Map the command to its service function and call it */
	for (i = 0; cmd_fmtab[i].cmd != 0; i++) {
		if (p->cmd == cmd_fmtab[i].cmd) {
			p->retcode = CDA_OK;
			(*cmd_fmtab[i].srvfunc)(s, p);
			break;
		}
	}

	return (p->cmd == CDA_OFF);
}


/*
 * usage
 *	Display command line usage syntax
 *
 * Args:
 *	None.
 *
 * Return:
 *	Nothing.
 */
STATIC void
usage(void)
{
	(void) fprintf(errfp, "Usage: %s %s command\n",
		PROGNAME,
		"[-dev device] [-batch] [-online | -offline] [-debug level#]");
	(void) fprintf(errfp,
	    "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
	    "Valid commands are:\n",
	    "\ton\n",
	    "\toff\n",
	    "\tdisc <load | eject | next | prev | disc#>\n",
	    "\tlock <on | off>\n",
	    "\tplay [track# [min:sec]]\n",
	    "\tpause\n",
	    "\tstop\n",
	    "\ttrack <prev | next>\n",
	    "\tindex <prev | next>\n",
	    "\tprogram [clear | save | track# ...]\n",
	    "\tshuffle <on | off>\n",
	    "\trepeat <on | off>\n",
	    "\tvolume [value#]    (value 0-100)\n",
	    "\tbalance [value#]   (value 0-100, center:50)\n",
	    "\troute [stereo | reverse | mono-l | mono-r | mono | value#]\n",
	    "\tstatus [cont [secs#]]\n",
	    "\ttoc [offsets]\n",
	    "\textinfo [track#]\n",
	    "\tnotes [track#]\n",
	    "\ton-load [autolock | noautolock | none | spindown | autoplay]\n",
	    "\ton-exit [none | autostop | autoeject]\n",
	    "\ton-done [autoeject | noautoeject | autoexit | noautoexit]\n",
	    "\ton-eject [autoexit | noautoexit]\n",
	    "\tchanger [multiplay | nomultiplay | reverse | noreverse]\n",
	    "\tdevice\n",
	    "\tversion\n",
	    "\tcddbreg\n",
	    "\tcddbhint\n",
	    "\tdebug [level#]\n",
	    "\tvisual\n");
}


/*
 * parse_time
 *	Parse a string of the form "min:sec" and convert to integer
 *	minute and second values.
 *
 * Args:
 *	str - Pointer to the "min:sec" string.
 *	min - pointer to where the minute value is to be written.
 *	sec - pointer to where the second value is to be written.
 *
 * Return:
 *	TRUE - success
 *	FALSE - failure
 */
STATIC bool_t
parse_time(char *str, int *min, int *sec)
{
	char	*p;

	if ((p = strchr(str, ':')) == NULL)
		return FALSE;
	
	if (!isdigit((int) *str) || !isdigit((int) *(p+1)))
		return FALSE;

	*p = '\0';
	*min = atoi(str);
	*sec = atoi(p+1);
	*p = ':';

	return TRUE;
}


/*
 * cda_parse_args
 *	Parse CDA command line arguments.
 *
 * Args:
 *	argc, argv
 *	cmd - Pointer to the command code.
 *	arg - Command argument array.
 *
 * Return:
 *	TRUE - success
 *	FALSE - failure
 */
STATIC bool_t
cda_parse_args(int argc, char **argv, word32_t *cmd, word32_t arg[])
{
	int	i,
		j,
		min,
		sec;

	/* Default values */
	*cmd = 0;
	(void) memset(arg, 0, CDA_NARGS * sizeof(word32_t));

	/* Command line args handling */
	for (i = 1; i < argc; i++) {
		if (*cmd != 0) {
			/* Multiple commands specified */
			usage();
			return FALSE;
		}

		if (strcmp(argv[i], "-dev") == 0) {
			if (++i < argc) {
				if (!di_isdemo())
					app_data.device = argv[i];
			}
			else {
				usage();
				return FALSE;
			}
		}
		else if (strcmp(argv[i], "-debug") == 0) {
			if (++i < argc && isdigit((int) argv[i][0]))
				app_data.debug = (word32_t) atoi(argv[i]);
			else {
				usage();
				return FALSE;
			}
		}
		else if (strcmp(argv[i], "-batch") == 0) {
			batch = TRUE;
		}
		else if (strcmp(argv[i], "-online") == 0) {
			inetoffln = 1;
		}
		else if (strcmp(argv[i], "-offline") == 0) {
			inetoffln = 2;
		}
		else if (strcmp(argv[i], "on") == 0) {
			*cmd = CDA_ON;
		}
		else if (strcmp(argv[i], "off") == 0) {
			*cmd = CDA_OFF;
		}
		else if (strcmp(argv[i], "disc") == 0) {
			/* <load | eject | next | prev | disc#> */
			if (++i < argc) {
				if (strcmp(argv[i], "load") == 0)
					arg[0] = 0;
				else if (strcmp(argv[i], "eject") == 0)
					arg[0] = 1;
				else if (strcmp(argv[i], "next") == 0)
					arg[0] = 2;
				else if (strcmp(argv[i], "prev") == 0)
					arg[0] = 3;
				else if (isdigit((int) argv[i][0])) {
					arg[0] = 4;
					arg[1] = atoi(argv[i]);
				}
				else {
					/* Wrong arg */
					usage();
					return FALSE;
				}
			}
			else {
				/* Missing arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_DISC;
		}
		else if (strcmp(argv[i], "lock") == 0) {
			/* <on | off> */
			if (++i < argc) {
				if (strcmp(argv[i], "off") == 0)
					arg[0] = 0;
				else if (strcmp(argv[i], "on") == 0)
					arg[0] = 1;
				else {
					/* Wrong arg */
					usage();
					return FALSE;
				}
			}
			else {
				/* Missing arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_LOCK;
		}
		else if (strcmp(argv[i], "play") == 0) {
			/* [track# [min:sec]] */
			if ((i+1) < argc && isdigit((int) argv[i+1][0])) {
				/* The user specified the track number */
				if ((arg[0] = atoi(argv[++i])) == 0) {
					/* Wrong arg */
					usage();
					return FALSE;
				}

				if ((i+1) < argc &&
				    parse_time(argv[i+1], &min, &sec)) {
					/* The user specified a time offset */
					arg[1] = min;
					arg[2] = sec;
					i++;
				}
				else {
					arg[1] = arg[2] = (word32_t) -1;
				}
			}
			*cmd = CDA_PLAY;
		}
		else if (strcmp(argv[i], "pause") == 0) {
			*cmd = CDA_PAUSE;
		}
		else if (strcmp(argv[i], "stop") == 0) {
			*cmd = CDA_STOP;
		}
		else if (strcmp(argv[i], "track") == 0) {
			/* <prev | next> */
			if (++i < argc) {
				if (strcmp(argv[i], "prev") == 0)
					arg[0] = 0;
				else if (strcmp(argv[i], "next") == 0)
					arg[0] = 1;
				else {
					/* Wrong arg */
					usage();
					return FALSE;
				}
			}
			else {
				/* Missing arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_TRACK;
		}
		else if (strcmp(argv[i], "index") == 0) {
			/* <prev | next> */
			if (++i < argc) {
				if (strcmp(argv[i], "prev") == 0)
					arg[0] = 0;
				else if (strcmp(argv[i], "next") == 0)
					arg[0] = 1;
				else {
					/* Wrong arg */
					usage();
					return FALSE;
				}
			}
			else {
				/* Missing arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_INDEX;
		}
		else if (strcmp(argv[i], "program") == 0) {
			/* [clear | save | track# ...] */
			arg[0] = 1;

			if ((i+1) < argc) {
				if (strcmp(argv[i+1], "clear") == 0) {
					i++;
					arg[0] = 0;
				}
				else if (strcmp(argv[i+1], "save") == 0) {
					i++;
					arg[0] = 2;
				}
				else {
					j = 0;
					while ((i+1) < argc &&
					       isdigit((int) argv[i+1][0]) &&
					       j < (CDA_NARGS-2)) {
						arg[++j] = atoi(argv[++i]);
					}
					if (j > 0)
						arg[0] = (word32_t) -j;
				}
			}
			*cmd = CDA_PROGRAM;
		}
		else if (strcmp(argv[i], "shuffle") == 0) {
			/* <on | off> */
			if (++i < argc) {
				if (strcmp(argv[i], "off") == 0)
					arg[0] = 0;
				else if (strcmp(argv[i], "on") == 0)
					arg[0] = 1;
				else {
					/* Wrong arg */
					usage();
					return FALSE;
				}
			}
			else {
				/* Missing arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_SHUFFLE;
		}
		else if (strcmp(argv[i], "repeat") == 0) {
			/* <on | off> */
			if (++i < argc) {
				if (strcmp(argv[i], "off") == 0)
					arg[0] = 0;
				else if (strcmp(argv[i], "on") == 0)
					arg[0] = 1;
				else {
					/* Wrong arg */
					usage();
					return FALSE;
				}
			}
			else {
				/* Missing arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_REPEAT;
		}
		else if (strcmp(argv[i], "volume") == 0) {
			/* [value#] */
			if ((i+1) >= argc || !isdigit((int) argv[i+1][0]))
				/* Query */
				arg[0] = 0;
			else {
				/* Set */
				arg[0] = 1;
				arg[1] = (word32_t) atoi(argv[++i]);
			}
			*cmd = CDA_VOLUME;
		}
		else if (strcmp(argv[i], "balance") == 0) {
			/* [value#] */
			if ((i+1) >= argc || !isdigit((int) argv[i+1][0]))
				/* Query */
				arg[0] = 0;
			else {
				/* Set */
				arg[0] = 1;
				arg[1] = (word32_t) atoi(argv[++i]);
			}
			*cmd = CDA_BALANCE;
		}
		else if (strcmp(argv[i], "route") == 0) {
			/* [stereo | reverse | mono-l | mono-r | 
			 *  mono | value#] 
			 */
			if ((i+1) >= argc) {
				/* Query */
				arg[0] = 0;
			}
			else if (strcmp(argv[i+1], "stereo") == 0) {
				arg[0] = 1;
				arg[1] = 0;
				i++;
			}
			else if (strcmp(argv[i+1], "reverse") == 0) {
				arg[0] = 1;
				arg[1] = 1;
				i++;
			}
			else if (strcmp(argv[i+1], "mono-l") == 0) {
				arg[0] = 1;
				arg[1] = 2;
				i++;
			}
			else if (strcmp(argv[i+1], "mono-r") == 0) {
				arg[0] = 1;
				arg[1] = 3;
				i++;
			}
			else if (strcmp(argv[i+1], "mono") == 0) {
				arg[0] = 1;
				arg[1] = 4;
				i++;
			}
			else if (isdigit((int) argv[i+1][0])) {
				/* value# */
				arg[0] = 1;
				arg[1] = (word32_t) atoi(argv[++i]);
			}
			else {
				/* Wrong arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_ROUTE;
		}
		else if (strcmp(argv[i], "status") == 0) {
			/* [cont [secs#]] */
			if ((i+1) >= argc || strcmp(argv[i+1], "cont") != 0)
				stat_cont = FALSE;
			else {
				i++;
				stat_cont = TRUE;
				if ((i+1) < argc &&
				    isdigit((int) argv[i+1][0]))
					cont_delay = atoi(argv[++i]);
			}
			*cmd = CDA_STATUS;
		}
		else if (strcmp(argv[i], "toc") == 0) {
			/* [offsets] */
			if ((i+1) >= argc || strcmp(argv[i+1], "offsets") != 0)
				arg[0] = 0;
			else {
				i++;
				arg[0] = 1;
			}
			*cmd = CDA_TOC;
		}
		else if (strcmp(argv[i], "extinfo") == 0) {
			/* [track#] */
			arg[0] = 0;
			if ((i+1) >= argc || !isdigit((int) argv[i+1][0]))
				arg[1] = (word32_t) -1;
			else
				arg[1] = atoi(argv[++i]);

			*cmd = CDA_EXTINFO;
		}
		else if (strcmp(argv[i], "notes") == 0) {
			/* [track#] */
			arg[0] = 0;
			if ((i+1) >= argc || !isdigit((int) argv[i+1][0]))
				arg[1] = (word32_t) -1;
			else
				arg[1] = atoi(argv[++i]);

			*cmd = CDA_NOTES;
		}
		else if (strcmp(argv[i], "on-load") == 0) {
			/* [autolock | noautolock | none | spindown | 
			 *  autoplay] 
			 */
			if ((i+1) >= argc) {
				/* Query */
				arg[0] = 0;
			}
			else if (strcmp(argv[i+1], "noautolock") == 0) {
				arg[0] = 1;
				arg[1] = 0;
				i++;
			}
			else if (strcmp(argv[i+1], "autolock") == 0) {
				arg[0] = 1;
				arg[1] = 1;
				i++;
			}
			else if (strcmp(argv[i+1], "none") == 0) {
				arg[0] = 1;
				arg[1] = 2;
				i++;
			}
			else if (strcmp(argv[i+1], "spindown") == 0) {
				arg[0] = 1;
				arg[1] = 3;
				i++;
			}
			else if (strcmp(argv[i+1], "autoplay") == 0) {
				arg[0] = 1;
				arg[1] = 4;
				i++;
			}
			else {
				/* Wrong arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_ON_LOAD;
		}
		else if (strcmp(argv[i], "on-exit") == 0) {
			/* [none | autostop | autoeject] */
			if ((i+1) >= argc) {
				/* Query */
				arg[0] = 0;
			}
			else if (strcmp(argv[i+1], "none") == 0) {
				arg[0] = 1;
				arg[1] = 0;
				i++;
			}
			else if (strcmp(argv[i+1], "autostop") == 0) {
				arg[0] = 1;
				arg[1] = 1;
				i++;
			}
			else if (strcmp(argv[i+1], "autoeject") == 0) {
				arg[0] = 1;
				arg[1] = 2;
				i++;
			}
			else {
				/* Wrong arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_ON_EXIT;
		}
		else if (strcmp(argv[i], "on-done") == 0) {
			/* [autoeject | noautoeject | autoexit | noautoexit ] 
			 */
			if ((i+1) >= argc) {
				/* Query */
				arg[0] = 0;
			}
			else if (strcmp(argv[i+1], "noautoeject") == 0) {
				arg[0] = 1;
				arg[1] = 0;
				i++;
			}
			else if (strcmp(argv[i+1], "autoeject") == 0) {
				arg[0] = 1;
				arg[1] = 1;
				i++;
			}
			else if (strcmp(argv[i+1], "noautoexit") == 0) {
				arg[0] = 1;
				arg[1] = 2;
				i++;
			}
			else if (strcmp(argv[i+1], "autoexit") == 0) {
				arg[0] = 1;
				arg[1] = 3;
				i++;
			}
			else {
				/* Wrong arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_ON_DONE;
		}
		else if (strcmp(argv[i], "on-eject") == 0) {
			/* [autoexit | noautoexit] */
			if ((i+1) >= argc) {
				/* Query */
				arg[0] = 0;
			}
			else if (strcmp(argv[i+1], "noautoexit") == 0) {
				arg[0] = 1;
				arg[1] = 0;
				i++;
			}
			else if (strcmp(argv[i+1], "autoexit") == 0) {
				arg[0] = 1;
				arg[1] = 1;
				i++;
			}
			else {
				/* Wrong arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_ON_EJECT;
		}
		else if (strcmp(argv[i], "changer") == 0) {
			/* [multiplay | nomultiplay | reverse | noreverse ] 
			 */
			if ((i+1) >= argc) {
				/* Query */
				arg[0] = 0;
			}
			else if (strcmp(argv[i+1], "nomultiplay") == 0) {
				arg[0] = 1;
				arg[1] = 0;
				i++;
			}
			else if (strcmp(argv[i+1], "multiplay") == 0) {
				arg[0] = 1;
				arg[1] = 1;
				i++;
			}
			else if (strcmp(argv[i+1], "noreverse") == 0) {
				arg[0] = 1;
				arg[1] = 2;
				i++;
			}
			else if (strcmp(argv[i+1], "reverse") == 0) {
				arg[0] = 1;
				arg[1] = 3;
				i++;
			}
			else {
				/* Wrong arg */
				usage();
				return FALSE;
			}
			*cmd = CDA_CHGR;
		}
		else if (strcmp(argv[i], "device") == 0) {
			*cmd = CDA_DEVICE;
		}
		else if (strcmp(argv[i], "version") == 0) {
			*cmd = CDA_VERSION;
		}
		else if (strcmp(argv[i], "cddbreg") == 0) {
			*cmd = CDA_CDDBREG;
		}
		else if (strcmp(argv[i], "cddbhint") == 0) {
			*cmd = CDA_CDDBHINT;
		}
		else if (strcmp(argv[i], "debug") == 0) {
			/* [level] */
			if ((i+1) < argc && isdigit((int) argv[i+1][0])) {
				/* Set debug level */
				arg[0] = 1;
				arg[1] = (word32_t) atoi(argv[++i]);
			}
			else {
				/* Query */
				arg[0] = 0;
			}
			*cmd = CDA_DEBUG;
		}
		else if (strcmp(argv[i], "visual") == 0) {
#ifdef NOVISUAL
			(void) fprintf(errfp, "%s %s\n",
				       "Cannot start visual mode:",
				       "curses support is not compiled in!");
			return FALSE;
#else
			visual = TRUE;
			*cmd = CDA_STATUS;
			/* Make sure simulator/debug output is redirectable */
			ttyfp = stderr;
#endif
		}
		else {
			usage();
			return FALSE;
		}
	}

	if (*cmd == 0) {
		/* User did not specify a command */
		usage();
		return FALSE;
	}

	return TRUE;
}


/*
 * cda_match_select
 *	Ask the user to select from a list of fuzzy CDDB matches.
 *
 * Args:
 *	matchlist - List of fuzzy matches
 *
 * Return:
 *	User selection number, or 0 for no selection.
 */
STATIC int
cda_match_select(cdinfo_match_t *matchlist)
{
	int		i,
			n;
	cdinfo_match_t	*p;
	char		input[64];

	(void) fprintf(ttyfp, "\n\n%s\n%s\n\n",
		"CDDB has found the following potential matches for the album",
		"that you have.  Please choose an appropriate entry:");

	/* Display list */
	for (p = matchlist, i = 1; p != NULL; p = p->next, i++)
		(void) fprintf(ttyfp, "  %2d. %.30s / %-40s\n", i,
				(p->artist == NULL) ?
					"(unknown artist)" : p->artist,
				(p->title == NULL) ?
					"(unknown title)" : p->title);

	(void) fprintf(ttyfp, "  %2d. None of the above\n\n", i);

	for (;;) {
		(void) fprintf(ttyfp, "Choose one (1-%d): ", i);
		if (fgets(input, 63, stdin) == NULL) {
			(void) fprintf(ttyfp, "\n");
			(void) fflush(ttyfp);
			return 0;
		}

		input[strlen(input)-1] = '\0';

		n = atoi(input);
		if (n > 0 && n <= i)
			break;
	}

	(void) fprintf(ttyfp, "\n");
	(void) fflush(ttyfp);
	return ((n == i) ? 0 : n);
}


/*
 * cda_auth
 *	Ask the user to enter user and password for proxy authorization.
 *
 * Args:
 *	retrycnt - How many times has the user retried this
 *
 * Return:
 *	0 for failure
 *	1 for success
 */
STATIC int
cda_auth(int retrycnt)
{
	char	input[64];

	if (retrycnt == 0)
		(void) fprintf(ttyfp, "Proxy Authorization is required.\n");
	else {
		(void) fprintf(ttyfp, "Proxy Authorization failed.");

		if (retrycnt < 3)
			(void) fprintf(ttyfp, "  Try again.\n");
		else {
			(void) fprintf(ttyfp, "  Too many tries.\n\n");
			(void) fflush(ttyfp);
			return 0;
		}
	}

	(void) fprintf(ttyfp, "%s\n\n",
		"Please enter your proxy user name and password.");

	/* Get user name */
	(void) fprintf(ttyfp, "Username: ");
	(void) fflush(ttyfp);
	if (fgets(input, 63, stdin) == NULL) {
		(void) fprintf(ttyfp, "\n");
		(void) fflush(ttyfp);
		return 0;
	}
	input[strlen(input)-1] = '\0';
	if (input[0] == '\0')
		return 0;

	if (!util_newstr(&dbp->proxy_user, input)) {
		CDA_FATAL(app_data.str_nomemory);
		return 0;
	}

	/* Get password */
	(void) fprintf(ttyfp, "Password: ");
	(void) fflush(ttyfp);
	cda_echo(ttyfp, FALSE);
	if (fgets(input, 63, stdin) == NULL) {
		cda_echo(ttyfp, TRUE);
		(void) fprintf(ttyfp, "\n");
		(void) fflush(ttyfp);
		return 0;
	}
	cda_echo(ttyfp, TRUE);
	input[strlen(input)-1] = '\0';

	if (!util_newstr(&dbp->proxy_passwd, input)) {
		CDA_FATAL(app_data.str_nomemory);
		return 0;
	}

	(void) fprintf(ttyfp, "\n");
	(void) fflush(ttyfp);
	return 1;
}


/***********************
 *   public routines   *
 ***********************/


/*
 * cda_echo
 *	Turn off/on echo mode.  It is assumed that the program starts
 *	with echo mode enabled.
 *
 * Args:
 *	fp - Opened file stream to the user's terminal
 *	doecho - Whether echo should be enabled
 *
 * Return:
 *	Nothing.
 */
void
cda_echo(FILE *fp, bool_t doecho)
{
	int			fd = fileno(fp);
#ifdef USE_TERMIOS
	struct termios		tio;

	(void) tcgetattr(fd, &tio);

	if (doecho)
		tio.c_lflag |= ECHO;
	else
		tio.c_lflag &= ~ECHO;

	(void) tcsetattr(fd, TCSADRAIN, &tio);
#endif	/* USE_TERMIOS */

#ifdef USE_TERMIO
	struct termio		tio;

	(void) ioctl(fd, TCGETA, &tio);

	if (doecho)
		tio.c_lflag |= ECHO;
	else
		tio.c_lflag &= ~ECHO;

	(void) ioctl(fd, TCSETAW, &tio);
#endif	/* USE_TERMIO */

#ifdef USE_SGTTY
	struct sgttyb		tio;

	(void) ioctl(fd, TIOCGETP, &tio);

	if (doecho)
		tio.sg_flags |= ECHO;
	else
		tio.sg_flags &= ~ECHO;

	(void) ioctl(fd, TIOCSETP, &tio);
#endif	/* USE_SGTTY */
}


/*
 * curstat_addr
 *	Return the address of the curstat_t structure.
 *
 * Args:
 *	Nothing.
 *
 * Return:
 *	Nothing.
 */
curstat_t *
curstat_addr(void)
{
	return (&status);
}


/*
 * cda_quit
 *      Shut down CD audio
 *
 * Args:
 *      s - Pointer to the curstat_t structure.
 *
 * Return:
 *      Nothing.
 */
void
cda_quit(curstat_t *s)
{
	if (isdaemon) {
		/* Send response packet to prevent client hang */
		if (curr_pkt != NULL) {
			curr_pkt->retcode = CDA_DAEMON_EXIT;
			(void) cda_sendpkt("CD audio daemon",
					   cda_rfd[0], curr_pkt);
		}

		/* Shut down CD interface subsystem */
		di_halt(s);

		/* Close FIFOs - daemon side */
		if (cda_sfd[0] >= 0)
			(void) close(cda_sfd[0]);
		if (cda_rfd[0] >= 0)
			(void) close(cda_rfd[0]);

		/* Remove FIFOs */
		if (spipe[0] != '\0')
			(void) UNLINK(spipe);
		if (rpipe[0] != '\0')
			(void) UNLINK(rpipe);

		/* Remove lock */
		if (dlock[0] != '\0')
			(void) UNLINK(dlock);
	}
#ifndef NOVISUAL
	else {
		cda_vtidy();
	}
#endif
}


/*
 * cda_warning_msg
 *	Print warning message.
 *
 * Args:
 *	title - Not used.
 *	msg - The warning message text string.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
cda_warning_msg(char *title, char *msg)
{
	(void) fprintf(errfp, "CD audio Warning: %s\n", msg);
}


/*
 * cda_fatal_msg
 *	Print fatal error message.
 *
 * Args:
 *	title - Not used..
 *	msg - The fatal error message text string.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
cda_fatal_msg(char *title, char *msg)
{
	(void) fprintf(errfp, "CD audio Fatal Error:\n%s\n", msg);
	cda_quit(&status);
	exit(6);
}


/*
 * cda_parse_devlist
 *	Parse the app_data.devlist string and create the cda_devlist array.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
cda_parse_devlist(curstat_t *s)
{
	char		*p,
			*q;
	int		i,
			n,
			listsz;

	if (app_data.chg_method < 0 || app_data.chg_method >= MAX_CHG_METHODS)
		/* Fix-up in case of mis-configuration */
		app_data.chg_method = CHG_NONE;

	n = app_data.numdiscs;

	switch (app_data.chg_method) {
	case CHG_SCSI_MEDCHG:
		n = 2;
		/*FALLTHROUGH*/

	case CHG_SCSI_LUN:
		/* SCSI LUN addressing method */
		listsz = n * sizeof(char *);

		cda_devlist = (char **) MEM_ALLOC("cda_devlist", listsz);
		if (cda_devlist == NULL) {
			CDA_FATAL(app_data.str_nomemory);
			return;
		}
		(void) memset(cda_devlist, 0, listsz);

		p = q = app_data.devlist;
		if (p == NULL || *p == '\0') {
			CDA_FATAL(app_data.str_devlist_undef);
			return;
		}

		for (i = 0; i < n; i++) {
			q = strchr(p, ';');

			if (q == NULL && i < (n - 1)) {
				CDA_FATAL(app_data.str_devlist_count);
				return;
			}

			if (q != NULL)
				*q = '\0';

			cda_devlist[i] = NULL;
			if (!util_newstr(&cda_devlist[i], p)) {
				CDA_FATAL(app_data.str_nomemory);
				return;
			}

			if (q != NULL)
				*q = ';';

			p = q + 1;
		}
		break;

	case CHG_OS_IOCTL:
	case CHG_NONE:
	default:
		if (app_data.devlist == NULL) {
			if (!util_newstr(&app_data.devlist, app_data.device)) {
				CDA_FATAL(app_data.str_nomemory);
				return;
			}
		}
		else if (strcmp(app_data.devlist, app_data.device) != 0) {
			MEM_FREE(app_data.devlist);
			if (!util_newstr(&app_data.devlist, app_data.device)) {
				CDA_FATAL(app_data.str_nomemory);
				return;
			}
		}

		cda_devlist = (char **) MEM_ALLOC(
			"cda_devlist",
			sizeof(char *)
		);
		if (cda_devlist == NULL) {
			CDA_FATAL(app_data.str_nomemory);
			return;
		}

		cda_devlist[0] = NULL;
		if (!util_newstr(&cda_devlist[0], app_data.devlist)) {
			CDA_FATAL(app_data.str_nomemory);
			return;
		}
		break;
	}

	/* Initialize to the first device */
	s->curdev = cda_devlist[0];
}


/*
 * cda_hist_init
 *	Initialize history list.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
cda_hist_init(curstat_t *s)
{
	if (!hist_initted) {
		hist_initted = TRUE;
		cdinfo_hist_init();
	}
}


/*
 * cda_hist_new
 *	Add current CD to the history list.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	Nothing.
 */
void
cda_hist_new(curstat_t *s)
{
	cdinfo_dlist_t	h;

	if (dbp->discid == 0)
		/* Don't add to history if no disc */
		return;

	h.device = s->curdev;
	h.discno = (int) s->cur_disc;
	h.type = ((dbp->flags & CDINFO_MATCH) == 0) ? 0 :
		((dbp->flags & CDINFO_FROMLOC) ?
			CDINFO_DLIST_LOCAL : CDINFO_DLIST_REMOTE);
	h.discid = dbp->discid;
	h.genre = dbp->disc.genre;
	h.artist = dbp->disc.artist;
	h.title = dbp->disc.title;
	h.time = time(NULL);

	/* Add to in-core history list */
	(void) cdinfo_hist_addent(&h, TRUE);
}


/*
 * cda_dbclear
 *	Clear in-core CD information.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *	reload - Whether we are going to be re-loading the CD info entry.
 *
 * Return:
 *	Nothing.
 */
/*ARGSUSED*/
void
cda_dbclear(curstat_t *s, bool_t reload)
{
	/* Clear CD information structure */
	cdinfo_clear(reload);
}


/*
 * cda_dbload
 *	Load the CD information pertaining to the current disc, if available.
 *
 * Args:
 *	id - The xmcd disc ID
 *	ifunc - Function pointer to a fuzzy CD info match handling function.
 *	pfunc - Function pointer to a proxy authorization handling function.
 *	depth - recursion depth
 *
 * Return:
 *	Nothing.
 */
bool_t
cda_dbload(
	word32_t	id,
	int		(*ifunc)(cdinfo_match_t *),
	int		(*pfunc)(int),
	int		depth
)
{
	int		i,
			n,
			retcode,
			ret;
	curstat_t	*s;
	word32_t	arg[CDA_NARGS];

	s = &status;
	(void) memset(arg, 0, CDA_NARGS * sizeof(word32_t));

	if (!cda_sendcmd(CDA_TOC2, arg, &retcode)) {
		(void) printf("\n%s\n", emsgp);
		return FALSE;
	}

	s->tot_trks = id & 0xff;

	/* Update curstat with essential info */
	for (i = 0; i < (int) s->tot_trks; i++) {
		RD_ARG_TOC2(arg[i+1], s->trkinfo[i].trkno,
			s->trkinfo[i].min,
			s->trkinfo[i].sec,
			s->trkinfo[i].frame
		);

		util_msftoblk(
			s->trkinfo[i].min,
			s->trkinfo[i].sec,
			s->trkinfo[i].frame,
			&s->trkinfo[i].addr,
			MSF_OFFSET
		);
	}

	/* Lead-out track */
	RD_ARG_TOC2(arg[i+1], s->trkinfo[i].trkno,
		s->trkinfo[i].min,
		s->trkinfo[i].sec,
		s->trkinfo[i].frame
	);
	util_msftoblk(
		s->trkinfo[i].min,
		s->trkinfo[i].sec,
		s->trkinfo[i].frame,
		&s->trkinfo[i].addr,
		MSF_OFFSET
	);

	/* Load user-defined track program, if available */
	if ((ret = cdinfo_load_prog(s)) != 0) {
		DBGPRN(DBG_CDI)(errfp, "cdinfo_load_prog: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
	}

	/* Parse play order string and set the play order */
	if (!cda_pgm_parse(s))
		CDA_WARNING(app_data.str_seqfmterr);

	/* Set appropriate offline mode */
	if ((ret = cdinfo_offline(s)) != 0) {
		DBGPRN(DBG_CDI)(errfp,
			"cdinfo_offline: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
	}

	/* Load CD information */
	if ((ret = cdinfo_load(s)) != 0) {
		/* Failed to load */
		DBGPRN(DBG_CDI)(errfp, "\ncdinfo_load: status=%d arg=%d\n",
			CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));

		switch (CDINFO_GET_STAT(ret)) {
		case AUTH_ERR:
			if (pfunc == NULL || (n = (*pfunc)(depth)) == 0)
				return FALSE;

			/* Recursion */
			return (cda_dbload(id, ifunc, pfunc, depth + 1));

		default:
			/* Add to history list */
			cda_hist_new(s);
			return FALSE;
		}
	}

	if (dbp->matchlist != NULL) {
		/* Got a list of "fuzzy matches": Prompt user
		 * for a selection.
		 */
		if (ifunc == NULL || (n = (*ifunc)(dbp->matchlist)) == 0) {
			/* Add to history list */
			cda_hist_new(s);
			return TRUE;
		}

		dbp->match_tag = (long) n;

		/* Load CD information */
		if ((ret = cdinfo_load_matchsel(s)) != 0) {
			/* Failed to load */
			DBGPRN(DBG_CDI)(errfp,
				"\ncdinfo_load_matchsel: status=%d arg=%d\n",
				CDINFO_GET_STAT(ret), CDINFO_GET_ARG(ret));
			return FALSE;
		}
		else if ((dbp->flags & CDINFO_MATCH) != 0) {
			/* Exact match */

			/* Add to history list */
			cda_hist_new(s);

			if (app_data.discog_mode & DISCOG_GEN_CDINFO) {
				/* Generate new local discography */
				if ((ret = cdinfo_gen_discog(s)) != 0) {
				    DBGPRN(DBG_CDI)(errfp,
					"cdinfo_gen_discog: status=%d arg=%d\n",
					CDINFO_GET_STAT(ret),
					CDINFO_GET_ARG(ret));
				}
			}

			return TRUE;
		}
		else {
			/* Add to history list */
			cda_hist_new(s);
			return FALSE;
		}
	}
	else if ((dbp->flags & CDINFO_NEEDREG) != 0) {
		/* User registration required */
		(void) fprintf(ttyfp,
		    "\n\nYou must register with CDDB to access the CDDB2 "
		    "service.\nTo do that, run the \"cda cddbreg\" command.");
		return FALSE;
	}
	else if ((dbp->flags & CDINFO_MATCH) != 0) {
		/* Exact match */

		/* Add to history list */
		cda_hist_new(s);

		if (app_data.discog_mode & DISCOG_GEN_CDINFO) {
			/* Generate new local discography */
			if ((ret = cdinfo_gen_discog(s)) != 0) {
				DBGPRN(DBG_CDI)(errfp,
					"cdinfo_gen_discog: status=%d arg=%d\n",
					CDINFO_GET_STAT(ret),
					CDINFO_GET_ARG(ret));
			}
		}

		return TRUE;
	}

	/* Add to history list */
	cda_hist_new(s);
	return TRUE;
}


/*
 * cda_sendcmd
 *	Send command down the pipe and handle response.
 *
 * Args:
 *	cmd - The command code
 *	arg - Command arguments
 *	retcode - Return code
 *
 * Return:
 *	Status code
 */
bool_t
cda_sendcmd(word32_t cmd, word32_t arg[], int *retcode)
{
	int		i;
	cdapkt_t	p,
			r;

	/* Fill in command packet */
	(void) memset(&p, 0, sizeof(cdapkt_t));
	p.pktid = getpid();
	p.cmd = cmd;
	(void) memcpy(p.arg, arg, CDA_NARGS * sizeof(word32_t));

	/* Send command packet */
	if (!cda_sendpkt("CD audio", cda_sfd[1], &p)) {
		emsgp = "\nCannot send packet to CD audio daemon.";
		*retcode = -1;
		return FALSE;
	}

	for (i = 0; ; i++) {
		/* Get response packet */
		if (!cda_getpkt("CD audio", cda_rfd[1], &r)) {
			emsgp = "Cannot get packet from CD audio daemon.";
			*retcode = -1;
			return FALSE;
		}

		/* Sanity check */
		if (p.pktid == r.pktid)
			break;

		/* This is not our packet */

		if (i >= MAX_PKT_RETRIES) {
			emsgp =
			"Retry overflow reading packet from CD audio daemon.";
			*retcode = -1;
			return FALSE;
		}

		/* Increment packet reject count */
		r.rejcnt++;

		/* If packet has not reached reject limit, put it back
		 * into the pipe.
		 */
		if (r.rejcnt < MAX_PKT_RETRIES &&
		    !cda_sendpkt("CD audio", cda_rfd[0], &r)) {
			emsgp = "Cannot send packet to CD audio daemon.";
			*retcode = -1;
			return FALSE;
		}

		/* Introduce some randomness to shuffle
		 * the order of packets in the pipe
		 */
		if ((rand() & 0x80) != 0)
			util_delayms(100);
	}

	/* Return args */
	(void) memcpy(arg, r.arg, CDA_NARGS * sizeof(word32_t));

	*retcode = r.retcode;

	/* Check return code */
	switch (r.retcode) {
	case CDA_OK:
		return TRUE;
	case CDA_INVALID:
		emsgp = "This command is not valid in the current state.";
		return FALSE;
	case CDA_PARMERR:
		emsgp = "Command argument error.";
		return FALSE;
	case CDA_FAILED:
		emsgp = "The CD audio daemon does not support this command.";
		return FALSE;
	case CDA_DAEMON_EXIT:
		emsgp = "CD audio daemon exited.";
		return FALSE;
	default:
		emsgp = "The CD audio daemon returned an invalid status.";
		return FALSE;
	}
	/*NOTREACHED*/
}


/*
 * cda_daemon_alive
 *	Check if the cda daemon is running.
 *
 * Args:
 *	None.
 *
 * Return:
 *	TRUE - daemon is alive
 *	FALSE - daemon is dead
 */
bool_t
cda_daemon_alive(void)
{
	int		fd;
	pid_t		pid;
	char		dlock[FILE_PATH_SZ],
			buf[12];

	(void) sprintf(dlock, "%s/cdad.%x", TEMP_DIR, (int) cd_rdev);

	if ((fd = open(dlock, O_RDONLY)) < 0)
		return FALSE;

	if (read(fd, buf, 12) < 0) {
		(void) close(fd);
		return FALSE;
	}
	(void) close(fd);

	pid = (pid_t) atoi(buf);
	
	if (kill(pid, 0) < 0)
		return FALSE;

	daemon_pid = pid;

	return TRUE;
}


/*
 * cda_daemon
 *	CD audio daemon main loop function.
 *
 * Args:
 *	s - Pointer to the curstat_t structure.
 *
 * Return:
 *	The CD audio daemon exit status.
 */
int
cda_daemon(curstat_t *s)
{
	int		i;
	cdapkt_t	p;
	bool_t		done = FALSE;

	/* Make some needed directories */
	cda_mkdirs();

	(void) signal(SIGCHLD, SIG_IGN);

	/* Become a daemon process */
	switch (FORK()) {
	case -1:
		perror("Cannot fork CD audio daemon");
		cda_quit(s);
		return 1;
	case 0:
		/* Child process */
		if (ttyfp != stderr) {
			errfp = ttyfp;
			(void) fclose(stdin);
			(void) fclose(stdout);
			(void) fclose(stderr);
		}

		break;
	default:
		/* Parent process */
		/* Make sure the daemon is running */
		for (i = 0; i < 5; i++) {
			(void) sleep(1);
			if (cda_daemon_alive())
				break;
		}
		if (i >= 5) {
			CDA_FATAL("CD audio daemon not started.");
			return 1;
		}

		return 0;
	}

	/* Check for duplicate daemon processes */
	if (!cda_daemonlock()) {
		CDA_FATAL("CD audio daemon already running.");
		return 1;
	}

	/* Set daemon flag */
	isdaemon = TRUE;

	/* Create FIFOs */
	if (MKFIFO(spipe, 0600) < 0) {
		if (errno != EEXIST) {
			perror("CD audio: Cannot create send pipe");
			cda_quit(s);
			return 1;
		}

		/* Try removing and re-creating the FIFO */
		if (UNLINK(spipe) < 0) {
			perror("CD audio: Cannot unlink old send pipe");
			cda_quit(s);
			return 1;
		}
		if (MKFIFO(spipe, 0600) < 0) {
			perror("CD audio: Cannot create send pipe");
			cda_quit(s);
			return 1;
		}
	}
	if (MKFIFO(rpipe, 0600) < 0) {
		if (errno != EEXIST) {
			perror("CD audio: Cannot create recv pipe");
			cda_quit(s);
			return 1;
		}

		/* Try removing and re-creating the FIFO */
		if (UNLINK(rpipe) < 0) {
			perror("CD audio: Cannot unlink old recv pipe");
			cda_quit(s);
			return 1;
		}
		if (MKFIFO(rpipe, 0600) < 0) {
			perror("CD audio: Cannot create recv pipe");
			cda_quit(s);
			return 1;
		}
	}

	/* Initialize and start drive interfaces */
	cda_init(s);
	cda_start(s);

	/* Handle some signals */
	if (signal(SIGHUP, onsig0) == SIG_IGN)
		(void) signal(SIGHUP, SIG_IGN);
	(void) signal(SIGTERM, SIG_IGN);
	(void) signal(SIGINT, SIG_IGN);
	(void) signal(SIGQUIT, SIG_IGN);
#if defined(SIGTSTP) && defined(SIGCONT)
	(void) signal(SIGTSTP, SIG_IGN);
	(void) signal(SIGCONT, SIG_IGN);
#endif

	/* Main command handling loop */
	while (!done) {
		/* Get command packet */
		if (!cda_getpkt("CD audio daemon", cda_sfd[0], &p)) {
			cda_quit(s);
			return 1;
		}

		/* HACK: Set current packet pointer.  In case
		 * cda_docmd() leads to cda_quit() due to exitOnEject
		 * or exitOnDone, cda_quit() can send a response
		 * packet back to the client before exiting.
		 */
		curr_pkt = &p;

		/* Interpret and carry out command */
		done = cda_docmd(s, &p);

		/* Clear current packet pointer */
		curr_pkt = NULL;

		/* Send response packet */
		if (!cda_sendpkt("CD audio daemon", cda_rfd[0], &p)) {
			cda_quit(s);
			return 1;
		}
	}

	/* Stop the drive */
	cda_quit(s);

	exit(0);

	/*NOTREACHED*/
}


/*
 * main
 *	The main function
 */
int
main(int argc, char **argv)
{
	int		i,
			retcode,
			ret;
	word32_t	cmd,
			*arg1,
			*arg2;
	struct stat	stbuf;
	char		*ttypath,
			*cp,
			*hd,
			str[FILE_PATH_SZ * 2];
	bool_t		loaddb = TRUE;
	curstat_t	*s;

	errfp = stderr;
	s = &status;

	/* Hack: Aside from stdin, stdout, stderr, there shouldn't
	 * be any other files open, so force-close them.  This is
	 * necessary in case xmcd was inheriting any open file
	 * descriptors from a parent process which is for the
	 * CD-ROM device, and violating exclusive-open requirements
	 * on some platforms.
	 */
	for (i = 3; i < 10; i++)
		(void) close(i);

	/* Initialize */
	if ((ttypath = ttyname(2)) == NULL)
		ttypath = "/dev/tty";
	if ((ttyfp = fopen(ttypath, "w")) != NULL)
		setbuf(ttyfp, NULL);
	else
		ttyfp = stderr;

	/* Initialize error messages */
	app_data.str_nomethod = STR_NOMETHOD;
	app_data.str_novu = STR_NOVU;
	app_data.str_nomemory = STR_NOMEMORY;
	app_data.str_nocfg = STR_NOCFG;
	app_data.str_notrom = STR_NOTROM;
	app_data.str_notscsi2 = STR_NOTSCSI2;
	app_data.str_moderr = STR_MODERR;
	app_data.str_staterr = STR_STATERR;
	app_data.str_noderr = STR_NODERR;
	app_data.str_recoverr = STR_RECOVERR;
	app_data.str_maxerr = STR_MAXERR;
	app_data.str_tmpdirerr = STR_TMPDIRERR;
	app_data.str_libdirerr = STR_LIBDIRERR;
	app_data.str_seqfmterr = STR_SEQFMTERR;
	app_data.str_longpatherr = STR_LONGPATHERR;
	app_data.str_devlist_undef = STR_DEVLIST_UNDEF;
	app_data.str_devlist_count = STR_DEVLIST_COUNT;
	app_data.str_medchg_noinit = STR_MEDCHG_NOINIT;

	/* Initialize other app_data parameters */
	app_data.stat_interval = 260;
	app_data.ins_interval = 4000;
	app_data.startup_vol = -1;
	app_data.aux = (void *) &status;

	/* Allocate arg arrays */
	arg1 = (word32_t *) MEM_ALLOC("arg1", CDA_NARGS * sizeof(word32_t));
	arg2 = (word32_t *) MEM_ALLOC("arg2", CDA_NARGS * sizeof(word32_t));
	if (arg1 == NULL || arg2 == NULL)
		CDA_FATAL(app_data.str_nomemory);

	/* Parse command line args */
	if (!cda_parse_args(argc, argv, &cmd, arg1))
		exit(1);

	/* Seed random number generator */
	srand((unsigned) time(NULL));
	for (i = 0, cp = PROGNAME; i < 4 && *cp != '\0'; i++, cp++)
		s->aux[i] = (byte_t) *cp ^ 0xff;

	/* Debug mode start-up banner */
	DBGPRN(DBG_ALL)(errfp, "CDA %s.%s%s PL%d DEBUG MODE\n\n",
		VERSION_MAJ, VERSION_MIN, VERSION_EXT, PATCHLEVEL);

	/* Initialize libutil */
	util_init();

	/* Set library directory path */
	if ((cp = (char *) getenv("XMCD_LIBDIR")) == NULL)
		CDA_FATAL(app_data.str_libdirerr);

	if (!util_newstr(&app_data.libdir, cp))
		CDA_FATAL(app_data.str_nomemory);

	/* Paranoia: avoid overflowing buffers */
	if ((int) strlen(app_data.libdir) >= FILE_PATH_SZ)
		CDA_FATAL(app_data.str_longpatherr);

	hd = util_homedir(util_get_ouid());
	if ((int) strlen(hd) >= FILE_PATH_SZ)
		CDA_FATAL(app_data.str_longpatherr);

	/* Set up defaults */
	app_data.cdinfo_maxhist = 100;

	/* Get system common configuration parameters */
	(void) sprintf(str, SYS_CMCFG_PATH, app_data.libdir);
	di_common_parmload(str, TRUE);

	/* Get user common configuration parameters */
	(void) sprintf(str, USR_CMCFG_PATH, hd);
	di_common_parmload(str, FALSE);

	/* Set up basic wwwWarp structure */
	cdinfo_wwwwarp_parmload();

	if (app_data.device != NULL) {
		if ((int) strlen(app_data.device) >= FILE_PATH_SZ ||
		    (int) strlen(util_basename(app_data.device)) >=
		    FILE_BASE_SZ) {
			CDA_FATAL(app_data.str_longpatherr);
		}
	}

	/* Check validity of device */
	if (di_isdemo())
		cd_rdev = 0;
	else {
		if (stat(app_data.device, &stbuf) < 0) {
			(void) sprintf(errmsg, "Cannot stat %s.",
				       app_data.device);
			CDA_FATAL(errmsg);
		}
		cd_rdev = stbuf.st_rdev;
	}

	/* FIFO paths */
	(void) sprintf(spipe, "%s/send.%x", TEMP_DIR, (int) cd_rdev);
	(void) sprintf(rpipe, "%s/recv.%x", TEMP_DIR, (int) cd_rdev);

	/* Initialize CD information services */
	if (inetoffln > 0)
		app_data.cdinfo_inetoffln = (inetoffln == 1) ? FALSE : TRUE;
	(void) strcpy(cdinfo_cldata.prog, PROGNAME);
	(void) strcpy(cdinfo_cldata.user, util_loginname());
	cdinfo_cldata.isdemo = di_isdemo;
	cdinfo_cldata.curstat_addr = curstat_addr;
	cdinfo_cldata.fatal_msg = cda_fatal_msg;
	cdinfo_cldata.warning_msg = cda_warning_msg;
	cdinfo_init(&cdinfo_cldata);

#ifndef NOVISUAL
	if (visual) {
		/* Handle some signals */
		if (signal(SIGINT, onsig0) == SIG_IGN)
			(void) signal(SIGINT, SIG_IGN);
		if (signal(SIGQUIT, onsig0) == SIG_IGN)
			(void) signal(SIGQUIT, SIG_IGN);
		if (signal(SIGTERM, onsig0) == SIG_IGN)
			(void) signal(SIGTERM, SIG_IGN);
		(void) signal(SIGPIPE, onsig1);

		/* Initialize subsystems */
		cda_init(s);

		/* Start visual mode */
		cda_visual();
	}
#endif

	if (cmd == CDA_ON) {
		/* Start CDA daemon */
		(void) printf("Initializing...\n");
		if ((ret = cda_daemon(&status)))
			exit(ret);
	}

	/* Initialize subsystems */
	cda_init(s);

	/* Create device list */
	cda_parse_devlist(s);

	/* Open FIFOs - command side */
	if ((cda_sfd[1] = open(spipe, O_WRONLY)) < 0) {
		perror("CD audio: cannot open send pipe");
		CDA_FATAL(
			"Run \"cda on\" first to initialize CD audio daemon."
		);
	}
	if ((cda_rfd[1] = open(rpipe, O_RDONLY)) < 0) {
		perror("CD audio: cannot open recv pipe for reading");
		CDA_FATAL(
			"Run \"cda on\" first to initialize CD audio daemon."
		);
	}
	if ((cda_rfd[0] = open(rpipe, O_WRONLY)) < 0) {
		perror("CD audio: cannot open recv pipe for writing");
		CDA_FATAL(
			"Run \"cda on\" first to initialize CD audio daemon."
		);
	}

	/* Handle some signals */
	(void) signal(SIGINT, onintr);
	(void) signal(SIGQUIT, onintr);
	(void) signal(SIGTERM, onintr);

	ret = 0;
	for (;;) {
		/* Send the command */
		switch (cmd) {
		case CDA_ON:
			/* Send command to cda daemon */
			if (!cda_sendcmd(cmd, arg1, &retcode)) {
				(void) printf("%s\n", emsgp);
				ret = 2;
				break;
			}

			daemon_pid = (pid_t) arg1[0];

			(void) fprintf(errfp,
				    "CD audio daemon pid=%d dev=%s started.\n",
				    (int) daemon_pid, app_data.device);
			break;

		case CDA_OFF:
			/* Send command to cda daemon */
			if (!cda_sendcmd(cmd, arg1, &retcode)) {
				(void) printf("%s\n", emsgp);
				ret = 2;
				break;
			}

			daemon_pid = (pid_t) arg1[0];

			(void) fprintf(errfp,
				    "CD audio daemon pid=%d dev=%s exited.\n",
				    (int) daemon_pid, app_data.device);
			break;

		case CDA_STATUS:
			/* Send command to cda daemon */
			if (!cda_sendcmd(cmd, arg1, &retcode)) {
				(void) printf("%s\n", emsgp);
				ret = 2;
				break;
			}

			if (RD_ARG_MODE(arg1[0]) == MOD_NODISC)
				loaddb = TRUE;

			dbp->discid = arg1[6];
			s->cur_disc = arg1[7];

			switch (app_data.chg_method) {
			case CHG_SCSI_LUN:
				s->curdev = cda_devlist[s->cur_disc - 1];
				break;
			case CHG_SCSI_MEDCHG:
			case CHG_OS_IOCTL:
			case CHG_NONE:
			default:
				s->curdev = cda_devlist[0];
				break;
			}

			/* Load CD information */
			if (loaddb &&
			    RD_ARG_MODE(arg1[0]) != MOD_NODISC &&
			    dbp->discid != 0) {
				/* Initialize history list */
				cda_hist_init(s);

				loaddb = FALSE;
				if (cda_dbload(dbp->discid, NULL, NULL, 0))
					s->qmode = QMODE_MATCH;
				else
					s->qmode = QMODE_NONE;
			}

			/* Print status */
			prn_stat(arg1);
			break;

		default:	/* All other commands */
			/* Check current status */
			if (!cda_sendcmd(CDA_STATUS, arg2, &retcode)) {
				(void) printf("%s\n", emsgp);
				ret = 2;
				break;
			}

			if (RD_ARG_MODE(arg2[0]) == MOD_NODISC)
				loaddb = TRUE;

			dbp->discid = arg2[6];
			s->cur_disc = arg2[7];

			switch (app_data.chg_method) {
			case CHG_SCSI_LUN:
				s->curdev = cda_devlist[s->cur_disc - 1];
				break;
			case CHG_SCSI_MEDCHG:
			case CHG_OS_IOCTL:
			case CHG_NONE:
			default:
				s->curdev = cda_devlist[0];
				break;
			}

			/* Load CD information */
			if (loaddb && RD_ARG_MODE(arg2[0]) != MOD_NODISC &&
			    dbp->discid != 0) {
				/* Initialize history list */
				cda_hist_init(s);

				loaddb = FALSE;
				if ((cmd == CDA_TOC || cmd == CDA_EXTINFO ||
				     cmd == CDA_NOTES) && !batch) {
					(void) printf("Accessing CDDB... ");
					(void) fflush(stdout);

					if (cda_dbload(dbp->discid,
						       cda_match_select,
						       cda_auth, 0))
						s->qmode = QMODE_MATCH;
					else
						s->qmode = QMODE_NONE;

					(void) printf("\n\n");
				}
				else {
					if (cda_dbload(dbp->discid,
						       NULL, NULL, 0))
						s->qmode = QMODE_MATCH;
					else
						s->qmode = QMODE_NONE;
				}
			}

			if (cmd == CDA_CDDBREG) {
				if (!cda_userreg(ttyfp))
					ret = 2;
				break;
			}
			else if (cmd == CDA_CDDBHINT) {
				if (!cda_cddbhint(ttyfp))
					ret = 2;
				break;
			}

			/* Send command to cda daemon */
			if (!cda_sendcmd(cmd, arg1, &retcode) &&
			    retcode != CDA_DAEMON_EXIT) {
				(void) printf("%s\n", emsgp);
				ret = 2;
				break;
			}

			/* Map the command to its print function and
			 * call it
			 */
			for (i = 0; cmd_fmtab[i].cmd != 0; i++) {
				if (cmd == cmd_fmtab[i].cmd) {
					if (cmd_fmtab[i].prnfunc != NULL)
						(*cmd_fmtab[i].prnfunc)(arg1);
					break;
				}
			}
			break;
		}

		(void) fflush(stdout);

		if (!stat_cont)
			break;

		if (cont_delay > 0)
			(void) sleep(cont_delay);
	}

	/* Close FIFOs - command side */
	if (cda_sfd[1] >= 0)
		(void) close(cda_sfd[1]);
	if (cda_rfd[1] >= 0)
		(void) close(cda_rfd[1]);
	if (cda_rfd[0] >= 0)
		(void) close(cda_rfd[0]);

	exit(ret);

	/*NOTREACHED*/
}


