#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <getopt.h>
#include <errno.h>
#include <curses.h>
#include <dirent.h>
#include <pwd.h>

#include <sys/time.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/utsname.h>

#include <netdb.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <linux/cdrom.h>
/* #include <linux/ucdrom.h> */

int filehandle;                                   /* Handle of /dev/>cdrom< */
int sig = 0;                                         /* set on some signals */

/* cdrom data */
char                       toc_header[2];
int                        first,last;
struct cdrom_tocentry      *toc[CDROM_LEADOUT+1];
struct cdrom_subchnl       sub;      /* subchannel last time read */
int                        data;     /* data tracks present ? */
int                        have_cd = 0;
int                        auto_mode = 0;
int                        start_track = 0;
int                        stop_track = 0;
int                        one_track = 0;
int                        verbose = 0;
int                        debug = 0;
int                        interactive = 1;
int                        todo; /* non-interactive */

int                        have_wdb = 0;       /* wdb database present */
int                        have_cddb = 0;      /* cddb datebase present */
int                        have_cddb_l = 0;    /* local cddb */
int                        have_cddb_r = 0;    /* remote cddb */
int                        have_db = 0;        /* we have a database at all */
int                        have_record = 0;    /* we have a record for
						  the inserted CD */

char                       cddb_path[256] = "/usr/local/lib/cddb";
char                       cddb_server[2][128];

#define PLAY_CD        1
#define PLAY_TRACK     2
#define STOP_PLAYING   3
#define EJECT_CD       4
#define LIST_TRACKS    5
#define PS_LIST_TRACKS 6

#define log_printf if (debug) fprintf

char *devname = "/dev/cdrom";
char *progname;
char line[80];

char *cd_stat[] = {
    "invalid",
    "playing",
    "paused",
    "completed",
    "error",
    "no status"
};

/* workman db */
char *db_data;
int  db_size;
char cdname[80];
char artist[80];
char song[CDROM_LEADOUT+1][80];

/* ---------------------------------------------------------------------- */
/* tty stuff                                                              */

#define LINE_STATUS   0
#define LINE_CDINFO   1

#define LINE_ARTIST   3
#define LINE_CDNAME   4
#define LINE_TRACK    5

#define LINE_ACTION   7
#define LINE_LAST     8

void
tty_raw()
{
    if (!interactive)
	return;

    initscr();
    cbreak();
    noecho();
    keypad(stdscr,1);
    refresh();
}

void
tty_restore()
{
    if (!interactive)
	return;

    endwin();
}

void
ctrlc(int signal)
{
    sig=1;
}

int
select_wait(int sec)
{
    struct timeval  tv;
    fd_set          se;
    
    FD_ZERO(&se);
    FD_SET(0,&se);
    tv.tv_sec = sec;
    tv.tv_usec = 0;
    return select(1,&se,NULL,NULL,&tv);
}

/* ---------------------------------------------------------------------- */

void action(char *txt)
{
    if (!interactive) {
	if (verbose)
	    fprintf(stderr,"action: %s\n",txt);
	return;
    }
    
    if (txt && strlen(txt))
	mvprintw(LINE_ACTION,0,"action : %-70s\n",txt);
    else
	mvprintw(LINE_ACTION,0,"%-79s\n","");
    refresh();
}

void inline
xperror(char *txt)
{
    char line[80];

    if (!interactive) {
	if (verbose) {
	    fprintf(stderr,"error: ");
	    perror(txt);
	}
	return;
    }
    
    if (verbose) {
	sprintf(line,"%s: %s",txt,strerror(errno));
	mvprintw(LINE_ACTION,0,"error  : %-70s",line);
	refresh();
	select_wait(3);
	action(NULL);
    }
}

void oops(char *txt)
{
    if(interactive) {
	mvprintw(LINE_LAST,0,"%s, exiting...\n",txt);
	refresh();
    }
    tty_restore();
    exit(1);
}

/* ---------------------------------------------------------------------- */

void play_track(int t1,int t2);
void display_cdinfo();
void db_lookup();

void
cd_eject()
{
    if (have_cd) {
	action("stop...");
	if (0 != ioctl(filehandle,CDROMSTOP,NULL))
	    xperror("stop");
    }
    action("eject...");
    if (0 != ioctl(filehandle,CDROMEJECT,NULL))
	xperror("eject");
    have_cd = 0;
}

void
cd_stop()
{
    if (have_cd) {
	action("stop...");
	if (0 != ioctl(filehandle,CDROMSTOP,NULL))
	    xperror("stop");
    }
}

void
cd_close()
{
    if (!have_cd) {
	action("close...");
	if (0 != ioctl(filehandle,CDROMCLOSETRAY,NULL))
	    xperror("close");
    }
}

void
read_subchannel()
{
    if (!have_cd)
	return;
    sub.cdsc_format = CDROM_MSF;
    if (ioctl(filehandle,CDROMSUBCHNL,&sub)) {
	xperror("read subchannel");
	have_cd = 0;
    }
    if (auto_mode && sub.cdsc_audiostatus == CDROM_AUDIO_COMPLETED)
	cd_eject();
}

void
read_toc()
{
    int i;
    
    action("read toc...");
    toc_header[0] = toc_header[1] = 0;
    if (0 != ioctl(filehandle,CDROMREADTOCHDR,&toc_header)) {
	xperror("read toc header");
	have_cd = 0;
	have_record = 0;
	cdname[0] = 0;
	artist[0] = 0;
	for (i = 0; i < CDROM_LEADOUT; song[i++][0] = 0);
    } else {
	have_cd = 1;
	data = 0;
	first = toc_header[0];
	last  = CDROM_LEADOUT;
	for (i = toc_header[0]; i <= CDROM_LEADOUT; i++) {
	    if (!toc[i]) {
		if (NULL == (toc[i] = malloc(sizeof(struct cdrom_tocentry))))
		    oops("out of memory");
		memset(toc[i],0,sizeof(struct cdrom_tocentry));
	    }
	    toc[i]->cdte_track  = i;
	    toc[i]->cdte_format = CDROM_MSF;
	    if (0 != ioctl(filehandle,CDROMREADTOCENTRY,toc[i])) {
		xperror("read toc entry");
		have_cd = 0;
		return;
	    }
	    if ((toc[i]->cdte_ctrl & CDROM_DATA_TRACK) &&
		(i != CDROM_LEADOUT)) {
		data++;
		if (i == first) {
		    if (i == (int)toc_header[1])
			first = CDROM_LEADOUT;
		    else
			first++;
		} else if (last == CDROM_LEADOUT)
		    last = i;
	    }
	    
	    /* skip to leadout */
	    if (i == (int)(toc_header[1]))
		i = CDROM_LEADOUT-1;
	}
	read_subchannel();
	if (auto_mode && sub.cdsc_audiostatus != CDROM_AUDIO_PLAY)
	    play_track(1,CDROM_LEADOUT);
	db_lookup();
    }
    display_cdinfo();
}

void
play_track(int t1, int t2)
{
    struct cdrom_msf msf;
    char line[80];

    if (!have_cd) {
	cd_close();
	read_toc();
    }
    
    read_subchannel();
    if (!have_cd || first == CDROM_LEADOUT)
	return;

    if (debug)
	fprintf(stderr,"play tracks: %d-%d => ",t1,t2);
    if (t1 < first)          t1 = first;
    if (t1 > last)           t1 = last;
    if (t1 > toc_header[1])  t1 = CDROM_LEADOUT;
    if (t2 < first)          t2 = first;
    if (t2 > last)           t2 = last;
    if (t2 > toc_header[1])  t2 = CDROM_LEADOUT;
    if (debug)
	fprintf(stderr,"%d-%d\n",t1,t2);

    msf.cdmsf_min0   = toc[t1]->cdte_addr.msf.minute;
    msf.cdmsf_sec0   = toc[t1]->cdte_addr.msf.second;
    msf.cdmsf_frame0 = toc[t1]->cdte_addr.msf.frame;
    msf.cdmsf_min1   = toc[t2]->cdte_addr.msf.minute;
    msf.cdmsf_sec1   = toc[t2]->cdte_addr.msf.second;
    msf.cdmsf_frame1 = toc[t2]->cdte_addr.msf.frame;
    if (sub.cdsc_audiostatus == CDROM_AUDIO_PLAY)
	if (0 != ioctl(filehandle,CDROMPAUSE,NULL))
	    xperror("pause");
    sprintf(line,"play track %d...",t1);
    action(line);
    if (0 != ioctl(filehandle,CDROMPLAYMSF,&msf))
	xperror("play");
}

void
skip(int diff)
{
    struct cdrom_msf msf;
    int    sec;

    read_subchannel();
    if (!have_cd ||  first == CDROM_LEADOUT)
	return;

    sec  = sub.cdsc_absaddr.msf.minute * 60 + sub.cdsc_absaddr.msf.second;
    sec += diff;
    if (sec < 0)
	sec = 0;
    msf.cdmsf_min0   = sec / 60;
    msf.cdmsf_sec0   = sec % 60;
    msf.cdmsf_frame0 = 0;
    msf.cdmsf_min1   = toc[last]->cdte_addr.msf.minute;
    msf.cdmsf_sec1   = toc[last]->cdte_addr.msf.second;
    msf.cdmsf_frame1 = toc[last]->cdte_addr.msf.frame;
    if (sub.cdsc_audiostatus == CDROM_AUDIO_PLAY)
	if (0 != ioctl(filehandle,CDROMPAUSE,NULL))
	    xperror("pause");
    if (0 != ioctl(filehandle,CDROMPLAYMSF,&msf))
	xperror("play");
}

void
toggle_pause()
{
    if (!have_cd)
	return;
    
    if (CDROM_AUDIO_PAUSED == sub.cdsc_audiostatus) {
	if (0 != ioctl(filehandle,CDROMRESUME,NULL))
	    xperror("pause");
    } else {
	if (0 != ioctl(filehandle,CDROMPAUSE,NULL))
	    xperror("pause");
    }
}

void
display_status()
{
    char line[80];

    if (!interactive)
	return;

    if (!have_cd) {
	sprintf(line,"no CD in drive (%s)",devname);

    } else if (first == CDROM_LEADOUT) {
	sprintf(line,"CD has only data tracks");
	
    } else if (sub.cdsc_audiostatus == CDROM_AUDIO_PAUSED ||
	       sub.cdsc_audiostatus == CDROM_AUDIO_PLAY) {
	sprintf(line,"%2d - %02d:%02d (%02d:%02d abs)  %-10s",
		sub.cdsc_trk,
		sub.cdsc_reladdr.msf.minute,
		sub.cdsc_reladdr.msf.second,
		sub.cdsc_absaddr.msf.minute,
		sub.cdsc_absaddr.msf.second,
		cd_stat[sub.cdsc_audiostatus & 0x0f]);

    } else {
	sprintf(line,"%s",cd_stat[sub.cdsc_audiostatus & 0x0f]);
	
    }
    mvprintw(LINE_STATUS,0,"status%s: %-70s",auto_mode ? "*" : " ",line);
    
    if (have_db) {
	if ((sub.cdsc_audiostatus == CDROM_AUDIO_PAUSED ||
	     sub.cdsc_audiostatus == CDROM_AUDIO_PLAY)  &&
	    have_cd)
	    mvprintw(LINE_TRACK,0,"song   : %-70s",song[sub.cdsc_trk]);
	else
	    mvprintw(LINE_TRACK,0,"song   : %-70s","");
    }
    refresh();
    action(NULL);
}

void
display_cdinfo()
{
    int len;
    char line[80];

    if (!interactive)
	return;
    
    if (!have_cd)
	sprintf(line,"-");
    else {
	len = sprintf(line,"%2d tracks  (%02d:%02d min)",
		      (int)toc_header[1],
		      toc[CDROM_LEADOUT]->cdte_addr.msf.minute,
		      toc[CDROM_LEADOUT]->cdte_addr.msf.second);
	if (data && first != CDROM_LEADOUT)
	    sprintf(line+len,", audio=%d-%d", first,
		    last == CDROM_LEADOUT ? (int)toc_header[1] : last-1);
    }
    
    if (have_db) {
	mvprintw(LINE_ARTIST,0,"artist : %-70s",artist);
	mvprintw(LINE_CDNAME,0,"CD     : %-70s",cdname);
    }
    mvprintw(LINE_CDINFO,0,"CD info: %-70s",line);
    refresh();
}

/* ---------------------------------------------------------------------- */
/* workman                                                                */

void
wdb_open()
{
    int fd;
    char filename[256];

    sprintf(filename,"%s/.workmandb",getenv("HOME"));
    fd = open(filename, O_RDONLY);
    if (-1 == fd)
	return;
    have_db = have_wdb = 1;
    db_size = lseek(fd, 0, SEEK_END);
    db_data = mmap(NULL, db_size, PROT_READ, MAP_SHARED, fd, 0);
    close(fd);
}

void
wdb_lookup()
{
    int  i,n,lba1,lba2,len;
    char *pos,d;

    if (!db_data)
	return;
    action("workman db lookup...");

    for (pos = db_data; pos - db_data < db_size; pos = strchr(pos,'\n')+1) {
	if (0 != strncmp(pos,"tracks",6))
	    continue;
	pos += 6;
	sscanf(pos," %d%n",&n,&len);
	pos += len;
	if (n != toc_header[1])
	    continue;
	for (i = toc_header[0]; i <= toc_header[1]; i++) {
	    sscanf(pos," %d%n",&lba1,&len);
	    pos += len;
	    lba2 = toc[i]->cdte_addr.msf.minute *60*75 +
		toc[i]->cdte_addr.msf.second *75 +
		toc[i]->cdte_addr.msf.frame;
	    if (lba1 != lba2)
		break;
	}
	if (i > toc_header[1])
	    break;
    }
    if (pos - db_data >= db_size)
	return;

    pos = strchr(pos,'\n')+1;
    if (2 != sscanf(pos,"cdname%c%80[^\n]\n%n",&d,cdname,&len))
	return;
    pos += len;

    if (2 != sscanf(pos,"artist%c%80[^\n]\n%n",&d,artist,&len))
	return;
    pos += len;

    for (i = toc_header[0]; i <= toc_header[1]; i++) {
	if (2 != sscanf(pos,"track%c%80[^\n]\n%n",&d,song[i],&len))
	    return;
	pos += len;
    }
    have_record = 1;
    return;
}

/* ---------------------------------------------------------------------- */
/* some networking stuff...                                               */

void
string_to_ipaddr(char *name, struct sockaddr_in *addr)
{
    struct hostent *host;

    /* "*" is any address (for bind) */
    if (0 == strcmp(name,"*")) {
	addr->sin_addr.s_addr = htonl (INADDR_ANY);
	return;
    }
    
    /* try to parse it it dotted number ... */
    if (-1 == (addr->sin_addr.s_addr = inet_addr(name))) {

	/* ... failing that do a DNS lookup */
	if (NULL == (host = gethostbyname(name))) {
	    fprintf(stderr,"can't resolve hostname %s\n",name);
	    exit(1);
	}
	addr->sin_addr = *(struct in_addr *) host->h_addr;
    }
}

int 
tcp_connect(char *host, char *port)
{
    int                 s,p;
    struct sockaddr_in  server;
    struct servent*     servinfo;
   
    if (-1 == (s = socket(AF_INET, SOCK_STREAM, 0))) {
	xperror("socket");
	return -1;
    }

    if (!(p=atoi(port))) {
	if (NULL == (servinfo = getservbyname(port,"tcp"))) {
	    xperror("getservbyname");
	    return -1;
	}
	server.sin_port = servinfo->s_port;
    } else
	server.sin_port = htons(p);
    server.sin_family = AF_INET;
    string_to_ipaddr(host,&server);
    
    if (-1 == connect(s, (struct sockaddr*) &server, sizeof(server))) {
	xperror("connect");
	return -1;
    }
    
    return s;
}

/* ---------------------------------------------------------------------- */
/* cddb                                                                   */

int
cddb_sum(int n)
{
    int ret=0;
    
    for (;;) {
	ret += n%10;
	n    = n/10;
	if (!n)
	    return ret;
    }
}

unsigned long
cddb_discid()
{
    int i,t,n=0;

    for (i = 1; i <= toc_header[1]; i++) {
	n += cddb_sum(toc[i]->cdte_addr.msf.minute * 60 +
		      toc[i]->cdte_addr.msf.second);
    }
    
    t = + toc[CDROM_LEADOUT]->cdte_addr.msf.minute * 60 
	+ toc[CDROM_LEADOUT]->cdte_addr.msf.second
	- toc[1]->cdte_addr.msf.minute * 60 
	- toc[1]->cdte_addr.msf.second;

    return ((n % 0xff) << 24 | t << 8 | toc_header[1]);
}

void
cddb_open()
{
    char *h;
    struct stat st;

    if (NULL != (h = getenv("CDDB_PATH")))
	strcpy(cddb_path,h);
    if (NULL != (h = getenv("CDDB_SERVER")))
	sscanf(h,"%127[^:]:%127s",cddb_server[0],cddb_server[1]);

    if (-1 != stat(cddb_path,&st))
	have_db = have_cddb = have_cddb_l = 1;
    if (strlen(cddb_server[0]) > 0 && strlen(cddb_server[1]) > 0)
	have_db = have_cddb = have_cddb_r = 1;
}

void
cddb_lookup()
{
    DIR *d;
    struct dirent *de;
    char line[256],data[256],cat[64],id[10],hostname[128],*h;
    unsigned long ID;
    FILE *fp = NULL;
    int l,i,s = -1;
    struct passwd  *pw;
    struct hostent *he;

    ID = cddb_discid();
    sprintf(line,"cddb lookup (ID %08lx)...",ID);
    action(line);

    /* local lookup */
    if (have_cddb_l && NULL != (d = opendir(cddb_path))) {
	action("cddb: local lookup");
	while (NULL != (de = readdir(d))) {
	    sprintf(line,"%s/%s/%08lx",cddb_path,de->d_name,ID);
	    if (NULL != (fp = fopen(line,"r")))
		break;
	}
    }

    /* remote lookup */
    if (NULL == fp && have_cddb_r) {
	action("cddb: remote lookup");
	if (-1 == (s = tcp_connect(cddb_server[0],cddb_server[1])))
	    return;
	fp = fdopen(s,"r+");

	/* server hello */
	fgets(line,255,fp);
	log_printf(stderr,"<< %s",line);
	if (atoi(line) > 299)
	    goto close_connection;

	/* cddb hello */
	action("cddb: hello");
	gethostname(hostname,127);
	he = gethostbyname(hostname);
	pw = getpwuid(getuid());
	sprintf(line,"cddb hello %s %s player 1.0\n",
		pw->pw_name,he->h_name);
	log_printf(stderr,">> %s",line);
	fputs(line,fp);
	fgets(line,255,fp);
	log_printf(stderr,"<< %s",line);
	if (atoi(line) > 299)
	    goto close_connection;

	/* cddb  */
	action("cddb: query");
	l = sprintf(line,"cddb query %08lx %d ",cddb_discid(),toc_header[1]);
	for (i = toc_header[0]; i <= toc_header[1]; i++)
	    l += sprintf(line+l,"%d ",
			 toc[i]->cdte_addr.msf.minute * 75 * 60 + 
			 toc[i]->cdte_addr.msf.second * 75 +
			 toc[i]->cdte_addr.msf.frame);
	sprintf(line+l,"%d\n",
		toc[CDROM_LEADOUT]->cdte_addr.msf.minute * 60 + 
		toc[CDROM_LEADOUT]->cdte_addr.msf.second);
	log_printf(stderr,">> %s",line);
	fputs(line,fp);
	fgets(line,255,fp);
	log_printf(stderr,"<< %s",line);
	switch (atoi(line)) {
	case 200:  /* found */
	    sscanf(line,"200 %63s %9s %[^\n\r]",cat,id,data);
	    break;
	case 211:  /* multiple */
	    for (;;) {
		fgets(line,255,fp);
		log_printf(stderr,"<< %s",line);
		if (line[0] == '.' && strlen(line) < 4)
		    break;
	    }
	    /* fall (not implemented yet) */
	case 202:  /* no match */
	default:   /* unknown response */
	    goto close_connection;
	}

	/* cddb read */
	sprintf(line,"cddb: read %s (%s)",data,cat);
	action(line);
	sprintf(line,"cddb read %s %s\n",cat,id);
	log_printf(stderr,">> %s",line);
	fputs(line,fp);
	fgets(line,255,fp);
	log_printf(stderr,"<< %s",line);
	if (210 != atoi(line))
	    goto close_connection;
    }

    if (NULL == fp)
	return;

    /* parsing */
    while (NULL != fgets(line, 255, fp)) {
	if (-1 != s)
	    log_printf(stderr,"<< %s",line);
	while (NULL != (h = strchr(line,'\n'))
	       || NULL != (h = strchr(line,'\r')))
	    *h = '\0';
	if (0 == strncmp(line,"DTITLE=",7)) {
	    if (NULL != (h = strchr(line+7,'/'))) {
		strncpy(artist,line+7,h-line-7);
		while (*h == ' ' || *h == '/')
		    h--;
		h++;
		artist[h-line-7]='\0';
		while (*h == ' ' || *h == '/')
		    h++;
		strcpy(cdname,h);
	    } else {
		strcpy(artist,line+7);
		cdname[0]='\0';
	    }
	}
	if (2 == sscanf(line,"TTITLE%d=%255[^\n]",&i,data))
	    strcpy(song[i+1],data);
	if (0 == strcmp(line,"."))
	    break;
    }
    have_record=1;

    if (-1 == s)
	return;

close_connection:
    /* quit */
    strcpy(line,"quit\n");
    log_printf(stderr,">> %s",line);
    fputs(line,fp);
    fgets(line,255,fp);
    log_printf(stderr,"<< %s",line);
    fclose(fp);
}

/* ---------------------------------------------------------------------- */
/* wrapper -- check cddb & workman                                        */

void
db_open()
{
    wdb_open();
    cddb_open();
    if (debug) {
	if (have_wdb)    fprintf(stderr,"db: have workman file\n");
	if (have_cddb_l) fprintf(stderr,"db: have local cddb : %s\n",cddb_path);
	if (have_cddb_r) fprintf(stderr,"db: have remote cddb: %s:%s\n",
				 cddb_server[0],cddb_server[1]);
    }
}

void
db_lookup()
{
#if 1
    /* cddb first */
    if (have_cddb)
	cddb_lookup();
    if (have_wdb && !have_record)
	wdb_lookup();
#else
    /* workman first */
    if (have_wdb)
	wdb_lookup();
    if (have_cddb && !have_record)
	cddb_lookup();
#endif
}

/* ---------------------------------------------------------------------- */

void
usage(char *prog)
{
    fprintf(stderr,
	    "%s is a simple curses CD player.  It can pick up artist,\n"
	    "CD name and song title from a workman database file or\n"
	    "via cddb.\n"
	    "\n"
	    "usage: %s [options] [device]\n"
            "\n"
	    "default for device is \"/dev/cdrom\". You can omit \"/dev/\",\n"
	    "i.e. start it with \"%s sr1\" for example.\n" 
	    "\n"
	    "These command line options available:\n"
	    "  -h      print this help\n"
	    "  -k      print key mapping\n"
	    "  -a      start up in auto-mode\n"
	    "  -v      verbose\n"
	    "\n"
	    "for non-interactive use (only one of them):\n"
	    "  -l      list tracks\n"
	    "  -c      print cover (PostScript to stdout)\n"
	    "  -p      play the whole CD\n"
	    "  -t n    play track >n<\n"
	    "  -t a-b  play all tracks between a and b (inclusive)\n"
	    "  -s      stop playing\n"
	    "  -e      eject cdrom\n"
            "\n"
	    "Thats all. Oh, maybe a few words more about the auto-mode. This\n"
	    "is the 'dont-touch-any-key' feature. You load a CD, player starts\n"
	    "to play it, and when it is done it ejects the CD. Start it that\n"
	    "way on a spare console and forget about it...\n"
	    "\n"
	    "Title database access:\n"
	    "  workman:     checks $HOME/.workmandb\n"
	    "  cddb local:  checks %s by default, you can change this\n"
	    "               using the CDDB_PATH environment variable.\n"
	    "  cddb remote: disabled by default, set CDDB_SERVER to \"server:port\"\n"
	    "               to enable it.\n"
	    "\n"
	    "(c) 1997,98 Gerd Knorr <kraxel@goldbach.in-berlin.de>\n"
	    ,prog,prog,prog,cddb_path);
}

void
keys()
{
    fprintf(stderr,
	    "    right     play / next track\n"
	    "    left      previous track\n"
	    "    up        10 sec forward\n"
	    "    down      10 sec back\n"
	    "    1-9       jump to track 1-9\n"
	    "    0         jump to track 10\n"
	    "    F1-F20    jump to track 11-30\n"
	    "\n"
	    "    e         eject\n"
	    "    c         close tray\n"
	    "    p         pause / resume\n"
	    "    s         stop\n"
	    "    q, ^C     quit\n"
	    "    x         quit and continue playing\n"
	    "    a         toggle auto-mode\n");
}

void
tracklist(void)
{
    int i,j,s;

    if (sub.cdsc_audiostatus == CDROM_AUDIO_PLAY) {
	printf("drive plays track %d (%d:%02d)\n\n",
	       sub.cdsc_trk,
	       sub.cdsc_reladdr.msf.minute,
	       sub.cdsc_reladdr.msf.second);
    }
    printf("%2d  %02d:%02d  |  ", (int)toc_header[1],
	   toc[CDROM_LEADOUT]->cdte_addr.msf.minute,
	   toc[CDROM_LEADOUT]->cdte_addr.msf.second);
    if (have_record)
	printf("%s / %s",artist,cdname);
    printf("\n");
    printf("-----------+--------------------------------------------------\n");
    for (i = toc_header[0]; i <= toc_header[1]; i++) {
	j = (i == toc_header[1]) ? CDROM_LEADOUT : i+1;
	s = (toc[j]->cdte_addr.msf.minute-toc[i]->cdte_addr.msf.minute) * 60 + 
	    toc[j]->cdte_addr.msf.second-toc[i]->cdte_addr.msf.second;
	printf("%2d  %02d:%02d  %s  ", i, s/60, s%60,
	       (sub.cdsc_audiostatus == CDROM_AUDIO_PLAY &&
		sub.cdsc_trk == i) ? "*" : "|");
	if (have_record && strlen(song[i]) > 0)
	    printf("%s",song[i]);
	printf("\n");
    }
}

/*
 * PostScript 8bit latin1 handling
 * stolen from mpage output -- please don't ask me how this works...
 */
#define ENCODING_TRICKS \
	"/reencsmalldict 12 dict def\n"				\
	"/ReEncodeSmall { reencsmalldict begin\n"		\
	"/newcodesandnames exch def /newfontname exch def\n"	\
	"/basefontname exch def\n"				\
	"/basefontdict basefontname findfont def\n"		\
	"/newfont basefontdict maxlength dict def\n"		\
	"basefontdict { exch dup /FID ne { dup /Encoding eq\n"	\
	"{ exch dup length array copy newfont 3 1 roll put }\n"	\
	"{ exch newfont 3 1 roll put }\n"			\
	"ifelse }\n"						\
	"{ pop pop }\n"						\
	"ifelse } forall\n"					\
	"newfont /FontName newfontname put\n"			\
	"newcodesandnames aload pop newcodesandnames length 2 idiv\n"	\
	"{ newfont /Encoding get 3 1 roll put } repeat\n"	\
	"newfontname newfont definefont pop end } def\n"	\
	"/charvec [\n"		\
	"026 /Scaron\n"		\
	"027 /Ydieresis\n"	\
	"028 /Zcaron\n"		\
	"029 /scaron\n"		\
	"030 /trademark\n"	\
	"031 /zcaron\n"		\
	"032 /space\n"		\
	"033 /exclam\n"		\
	"034 /quotedbl\n"	\
	"035 /numbersign\n"	\
	"036 /dollar\n"		\
	"037 /percent\n"	\
	"038 /ampersand\n"	\
	"039 /quoteright\n"	\
	"040 /parenleft\n"	\
	"041 /parenright\n"	\
	"042 /asterisk\n"	\
	"043 /plus\n"		\
	"044 /comma\n"		\
	"045 /minus\n"		\
	"046 /period\n"		\
	"047 /slash\n"		\
	"048 /zero\n"		\
	"049 /one\n"		\
	"050 /two\n"		\
	"051 /three\n"		\
	"052 /four\n"		\
	"053 /five\n"		\
	"054 /six\n"		\
	"055 /seven\n"		\
	"056 /eight\n"		\
	"057 /nine\n"		\
	"058 /colon\n"		\
	"059 /semicolon\n"	\
	"060 /less\n"		\
	"061 /equal\n"		\
	"062 /greater\n"	\
	"063 /question\n"	\
	"064 /at\n"		\
	"065 /A\n"		\
	"066 /B\n"		\
	"067 /C\n"		\
	"068 /D\n"		\
	"069 /E\n"		\
	"070 /F\n"		\
	"071 /G\n"		\
	"072 /H\n"		\
	"073 /I\n"		\
	"074 /J\n"		\
	"075 /K\n"		\
	"076 /L\n"		\
	"077 /M\n"		\
	"078 /N\n"		\
	"079 /O\n"		\
	"080 /P\n"		\
	"081 /Q\n"		\
	"082 /R\n"		\
	"083 /S\n"		\
	"084 /T\n"		\
	"085 /U\n"		\
	"086 /V\n"		\
	"087 /W\n"		\
	"088 /X\n"		\
	"089 /Y\n"		\
	"090 /Z\n"		\
	"091 /bracketleft\n"	\
	"092 /backslash\n"	\
	"093 /bracketright\n"	\
	"094 /asciicircum\n"	\
	"095 /underscore\n"	\
	"096 /quoteleft\n"	\
	"097 /a\n"		\
	"098 /b\n"		\
	"099 /c\n"		\
	"100 /d\n"		\
	"101 /e\n"		\
	"102 /f\n"		\
	"103 /g\n"		\
	"104 /h\n"		\
	"105 /i\n"		\
	"106 /j\n"		\
	"107 /k\n"		\
	"108 /l\n"		\
	"109 /m\n"		\
	"110 /n\n"		\
	"111 /o\n"		\
	"112 /p\n"		\
	"113 /q\n"		\
	"114 /r\n"		\
	"115 /s\n"		\
	"116 /t\n"		\
	"117 /u\n"		\
	"118 /v\n"		\
	"119 /w\n"		\
	"120 /x\n"		\
	"121 /y\n"		\
	"122 /z\n"		\
	"123 /braceleft\n"	\
	"124 /bar\n"		\
	"125 /braceright\n"	\
	"126 /asciitilde\n"	\
	"127 /.notdef\n"	\
	"128 /fraction\n"	\
	"129 /florin\n"		\
	"130 /quotesingle\n"	\
	"131 /quotedblleft\n"	\
	"132 /guilsinglleft\n"	\
	"133 /guilsinglright\n"	\
	"134 /fi\n"		\
	"135 /fl\n"		\
	"136 /endash\n"		\
	"137 /dagger\n"		\
	"138 /daggerdbl\n"	\
	"139 /bullet\n"		\
	"140 /quotesinglbase\n"	\
	"141 /quotedblbase\n"	\
	"142 /quotedblright\n"	\
	"143 /ellipsis\n"	\
	"144 /dotlessi\n"	\
	"145 /grave\n"		\
	"146 /acute\n"		\
	"147 /circumflex\n"	\
	"148 /tilde\n"		\
	"149 /oe\n"		\
	"150 /breve\n"		\
	"151 /dotaccent\n"	\
	"152 /perthousand\n"	\
	"153 /emdash\n"		\
	"154 /ring\n"		\
	"155 /Lslash\n"		\
	"156 /OE\n"		\
	"157 /hungarumlaut\n"	\
	"158 /ogonek\n"		\
	"159 /caron\n"		\
	"160 /lslash\n"		\
	"161 /exclamdown\n"	\
	"162 /cent\n"		\
	"163 /sterling\n"	\
	"164 /currency\n"	\
	"165 /yen\n"		\
	"166 /brokenbar\n"	\
	"167 /section\n"	\
	"168 /dieresis\n"	\
	"169 /copyright\n"	\
	"170 /ordfeminine\n"	\
	"171 /guillemotleft\n"	\
	"172 /logicalnot\n"	\
	"173 /hyphen\n"		\
	"174 /registered\n"	\
	"175 /macron\n"		\
	"176 /degree\n"		\
	"177 /plusminus\n"	\
	"178 /twosuperior\n"	\
	"179 /threesuperior\n"	\
	"180 /acute\n"		\
	"181 /mu\n"		\
	"182 /paragraph\n"	\
	"183 /periodcentered\n"	\
	"184 /cedilla\n"	\
	"185 /onesuperior\n"	\
	"186 /ordmasculine\n"	\
	"187 /guillemotright\n"	\
	"188 /onequarter\n"	\
	"189 /onehalf\n"	\
	"190 /threequarters\n"	\
	"191 /questiondown\n"	\
	"192 /Agrave\n"		\
	"193 /Aacute\n"		\
	"194 /Acircumflex\n"	\
	"195 /Atilde\n"		\
	"196 /Adieresis\n"	\
	"197 /Aring\n"		\
	"198 /AE\n"		\
	"199 /Ccedilla\n"	\
	"200 /Egrave\n"		\
	"201 /Eacute\n"		\
	"202 /Ecircumflex\n"	\
	"203 /Edieresis\n"	\
	"204 /Igrave\n"		\
	"205 /Iacute\n"		\
	"206 /Icircumflex\n"	\
	"207 /Idieresis\n"	\
	"208 /Eth\n"		\
	"209 /Ntilde\n"		\
	"210 /Ograve\n"		\
	"211 /Oacute\n"		\
	"212 /Ocircumflex\n"	\
	"213 /Otilde\n"		\
	"214 /Odieresis\n"	\
	"215 /multiply\n"	\
	"216 /Oslash\n"		\
	"217 /Ugrave\n"		\
	"218 /Uacute\n"		\
	"219 /Ucircumflex\n"	\
	"220 /Udieresis\n"	\
	"221 /Yacute\n"		\
	"222 /Thorn\n"		\
	"223 /germandbls\n"	\
	"224 /agrave\n"		\
	"225 /aacute\n"		\
	"226 /acircumflex\n"	\
	"227 /atilde\n"		\
	"228 /adieresis\n"	\
	"229 /aring\n"		\
	"230 /ae\n"		\
	"231 /ccedilla\n"	\
	"232 /egrave\n"		\
	"233 /eacute\n"		\
	"234 /ecircumflex\n"	\
	"235 /edieresis\n"	\
	"236 /igrave\n"		\
	"237 /iacute\n"		\
	"238 /icircumflex\n"	\
	"239 /idieresis\n"	\
	"240 /eth\n"		\
	"241 /ntilde\n"		\
	"242 /ograve\n"		\
	"243 /oacute\n"		\
	"244 /ocircumflex\n"	\
	"245 /otilde\n"		\
	"246 /odieresis\n"	\
	"247 /divide\n"		\
	"248 /oslash\n"		\
	"249 /ugrave\n"		\
	"250 /uacute\n"		\
	"251 /ucircumflex\n"	\
	"252 /udieresis\n"	\
	"253 /yacute\n"		\
	"254 /thorn\n"		\
	"255 /ydieresis\n"	\
	"] def"


void
ps_tracklist(void)
{
    int i,j,s,y,sy;

    if (!have_record)
	return;
    printf("%%!PS-Adobe-2.0\n");

    /* encoding tricks */
    puts(ENCODING_TRICKS);
    printf("/Times /TimesLatin1 charvec ReEncodeSmall\n");
    printf("/Helvetica /HelveticaLatin1 charvec ReEncodeSmall\n");

    /* Rahmen */
    printf("0 setlinewidth\n");
    printf(" 100 100 moveto\n");
    printf(" 390   0 rlineto\n");
    printf("   0 330 rlineto\n");
    printf("-390   0 rlineto\n");
    printf("closepath stroke\n");

    printf(" 100 100 moveto\n");
    printf("-16    0 rlineto\n");
    printf("  0  330 rlineto\n");
    printf("422    0 rlineto\n");
    printf("  0 -330 rlineto\n");
    printf("closepath stroke\n");

    /* Titel */
    printf("/TimesLatin1 findfont 24 scalefont setfont\n");
    printf("120 400 moveto (%s) show\n",cdname);
    printf("/TimesLatin1 findfont 18 scalefont setfont\n");
    printf("120 375 moveto (%s) show\n",artist);

    /* Liste */
    sy = 250 / toc_header[1];
    if (sy > 14) sy = 14;
    printf("/labelfont /TimesLatin1 findfont %d scalefont def\n",sy-2);
    printf("/timefont /Courier findfont %d scalefont def\n",sy-2);
    for (i = toc_header[0], y = 350; i <= toc_header[1]; i++, y -= sy) {
	j = (i == toc_header[1]) ? CDROM_LEADOUT : i+1;
	s = (toc[j]->cdte_addr.msf.minute-toc[i]->cdte_addr.msf.minute) * 60 + 
	    toc[j]->cdte_addr.msf.second-toc[i]->cdte_addr.msf.second;

	printf("labelfont setfont\n");
	printf("120 %d moveto (%d) show\n",y,i);
	printf("150 %d moveto (%s) show\n",y,song[i]);

	printf("timefont setfont\n");
	printf("420 %d moveto (%2d:%02d) show\n", y, s/60, s%60);
    }

    /* Seitenbanner */
    printf("/HelveticaLatin1 findfont 12 scalefont setfont\n");
    printf(" 97 105 moveto (%s: %s) 90 rotate show -90 rotate\n",
	   artist,cdname);
    printf("493 425 moveto (%s: %s) -90 rotate show 90 rotate\n",
	   artist,cdname);
    printf("showpage\n");
}

int
main(int argc, char *argv[])
{    
    int             c,key,nostop=0;
    char            *h;

    progname = strrchr(argv[0],'/');
    progname = progname ? progname+1 : argv[0];

    /* parse options */
    for (;;) {
	if (-1 == (c = getopt(argc, argv, "xdhkvcpset:la")))
	    break;
	switch (c) {
	case 'v':
	    verbose = 1;
	    break;
	case 'd':
	    debug = 1;
	    break;
	case 'a':
	    auto_mode = 1;
	    break;
	case 't':
	    if (NULL != (h = strchr(optarg,'-'))) {
		*h = 0;
		start_track = atoi(optarg);
		stop_track = atoi(h+1)+1;
 		if (0 == start_track) start_track = 1;
		if (1 == stop_track)  stop_track  = CDROM_LEADOUT;
	    } else {
		start_track = atoi(optarg);
		stop_track = start_track+1;
		one_track = 1;
	    }
	    interactive = 0;
	    todo = PLAY_TRACK;
	    break;
	case 'p':
	    interactive = 0;
	    todo = PLAY_CD;
	    break;
	case 'l':
	    interactive = 0;
	    todo = LIST_TRACKS;
	    break;
	case 'c':
	    interactive = 0;
	    todo = PS_LIST_TRACKS;
	    break;
	case 's':
	    interactive = 0;
	    todo = STOP_PLAYING;
	    break;
	case 'e':
	    interactive = 0;
	    todo = EJECT_CD;
	    break;
	case 'k':
	    keys();
	    exit(1);
	case 'h':
	    usage(progname);
	    exit(1);
	default:
	    usage(progname);
	    exit(1);
	}
    }

    if (argc > optind) {
	if (0 == strncmp(argv[optind],"/dev/",5))
	    devname = argv[optind];
	else {
	    devname=malloc(6+strlen(argv[optind]));
	    sprintf(devname,"/dev/%s",argv[optind]);
	}
    }

    /* open device */
    if (verbose)
	fprintf(stderr,"open %s... ",devname);
    filehandle = open(devname,O_RDONLY | O_NONBLOCK);
    if (filehandle == -1) {
	if (verbose)
	    fprintf(stderr,"error: %s\n",strerror(errno));
	else
	    fprintf(stderr,"open %s: %s\n",devname,strerror(errno));
	exit(1);
    } else
	if (verbose)
	    fprintf(stderr,"ok\n");

    tty_raw();
    signal(SIGINT,ctrlc);
    signal(SIGQUIT,ctrlc);
    signal(SIGTERM,ctrlc);
    signal(SIGHUP,ctrlc);

    db_open();
    if (!interactive) {
	sig=1;
	nostop=1;
	read_toc();
	if (!have_cd && todo != EJECT_CD) {
	    cd_close();
	    read_toc();
	}
	if (have_cd || todo == EJECT_CD) {
	    switch (todo) {
	    case STOP_PLAYING:
		cd_stop();
		break;
	    case EJECT_CD:
		cd_stop();
		cd_eject();
		break;
	    case LIST_TRACKS:
		tracklist();
		break;
	    case PS_LIST_TRACKS:
		ps_tracklist();
		break;
	    case PLAY_TRACK:
		/* play just this one track */
		if (have_record) {
		    printf("%s / %s\n",artist,cdname);
		    if (one_track)
			printf("%s\n",song[start_track]);
		}
		play_track(start_track,stop_track);
		break;
	    case PLAY_CD:
		if (have_record)
		    printf("%s / %s\n",artist,cdname);
		play_track(1,CDROM_LEADOUT);
		break;
	    }
	} else {
	    fprintf(stderr,"no CD in drive (%s)\n",devname);
	}
    }

    for (;!sig;) {
	if (!have_cd)
	    read_toc();
	read_subchannel();
	display_status();

	if (1 == select_wait(have_cd ? 1 : 5)) {
	    switch (key = getch()) {
	    case 'A':
	    case 'a':
		auto_mode = !auto_mode;
		break;
	    case 'X':
	    case 'x':
		nostop=1;
		/* fall */
	    case 'Q':
	    case 'q':
		sig=1;
		break;
	    case 'E':
	    case 'e':
		cd_eject();
		break;
	    case 'S':
	    case 's':
		cd_stop();
		break;
	    case 'C':
	    case 'c':
		cd_close();
		break;
	    case 'P':
	    case 'p':
		toggle_pause();
		break;
	    case KEY_RIGHT:
		if (have_cd &&
		    (sub.cdsc_audiostatus == CDROM_AUDIO_PAUSED ||
		     sub.cdsc_audiostatus == CDROM_AUDIO_PLAY)) 
		    play_track(sub.cdsc_trk+1,CDROM_LEADOUT);
		else
		    play_track(1,CDROM_LEADOUT);
		break;
	    case KEY_LEFT:
		if (have_cd &&
		    (sub.cdsc_audiostatus == CDROM_AUDIO_PAUSED ||
		    sub.cdsc_audiostatus == CDROM_AUDIO_PLAY))
		    play_track(sub.cdsc_trk-1,CDROM_LEADOUT);
		break;
	    case KEY_UP:
		if (have_cd &&
		    sub.cdsc_audiostatus == CDROM_AUDIO_PLAY)
		    skip(10);
		break;
	    case KEY_DOWN:
		if (have_cd &&
		    sub.cdsc_audiostatus == CDROM_AUDIO_PLAY)
		    skip(-10);
		break;
	    case '1':
	    case '2':
	    case '3':
	    case '4':
	    case '5':
	    case '6':
	    case '7':
	    case '8':
	    case '9':
		play_track(key - '0',CDROM_LEADOUT);
		break;
	    case '0':
		play_track(10,CDROM_LEADOUT);
		break;
	    case KEY_F(1):
	    case KEY_F(2):
	    case KEY_F(3):
	    case KEY_F(4):
	    case KEY_F(5):
	    case KEY_F(6):
	    case KEY_F(7):
	    case KEY_F(8):
	    case KEY_F(9):
	    case KEY_F(10):
	    case KEY_F(11):
	    case KEY_F(12):
	    case KEY_F(13):
	    case KEY_F(14):
	    case KEY_F(15):
	    case KEY_F(16):
	    case KEY_F(17):
	    case KEY_F(18):
	    case KEY_F(19):
	    case KEY_F(20):
		play_track(key - KEY_F(1) + 11,CDROM_LEADOUT);
		break;
	    }
	}
    }
    if (!nostop)
	if (0 != ioctl(filehandle,CDROMSTOP,NULL))
	    xperror("stop");
    tty_restore();
    oops("bye");

    return 0; /* keep compiler happy */
}
