/* hardware.c
 *
 * Copyright 1994 thomas insel 
 * Copyright 1995 sven oliver moll
 *
 * 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; version 2 dated June, 1991.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pwd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>
#include <errno.h>

#include "config.h"
#include "database.h"
#include "hardware.h"
#include "util.h"

/* returns first track (1-based) on disk */
int first_track(const cdhw_t *hw) {
    return hw->tochdr.cdth_trk0;
}

/* returns last track (1-based) on disk */
/* note that this is the last *real* track; a LEADOUT track follows */
int last_track(const cdhw_t *hw) {
    return hw->tochdr.cdth_trk1;
}

/* track is 1-based! */
int is_audio_track(const cdhw_t *hw, int track) {
    return !(hw->tocentries[track - 1].cdte_ctrl & CDROM_DATA_TRACK);
}

/************************************************************************/
/* Procedure:  read_hw
 * Purpose:    allocate, read hardware info
 * 
 * Inputs:     program name, CD file descriptor
 * Outputs:    to cdhw_t structure
 * Returns:    pointer to cdhw_t structure
 *
 ************************************************************************/

cdhw_t *read_hw(int fd)
{
    cdhw_t *hw;
    hw = malloc(sizeof(cdhw_t));
    if (!hw)
        return hw;
 
    if (update_hw(hw, fd) < 0) {
        free(hw);
        hw = NULL;
    }
 
    return hw;
}
 
/************************************************************************
 * Procedure:  update_hw
 * Returns:    0 success, -1 fatal error
 * Purpose:    read subchnl, tochdr and tocentries and write status
 *             into hw->iStatus. 
 ************************************************************************/
 
int update_hw(cdhw_t *hw, int fd)
{
    int i;
    struct cdrom_tocentry *tocentry;
    memset(hw, 0, sizeof(cdhw_t));

    /* try to get subchannel information. some reasons this can fail:
     * non-CDROM device (EINVAL). drive not capable of audio (ENOSYS),
     * tray open (EIO), no disc inserted (EIO). 
     */
    hw->subchnl.cdsc_format = CDROM_MSF;
    if (ioctl(fd, CDROMSUBCHNL, &hw->subchnl) < 0) {
        debugmsg("%P: ioctl subchnl: %s", strerror(errno));

        if (errno == EINVAL || errno == ENOSYS) {
            errormsg("%P: drive is not audio-capable.");
            hw->iStatus = CD_STATUS_ERROR;
            hw->subchnl.cdsc_audiostatus = CDROM_AUDIO_ERROR;
            return(-1);
        }
        
        hw->iStatus = CD_STATUS_NO_CD;
        return(0);
    }
 
    /* try to read the TOC header */
    if (ioctl(fd, CDROMREADTOCHDR, &hw->tochdr) < 0) {
        debugmsg("%P: ioctl readtochdr: %s", strerror(errno));
        hw->iStatus = CD_STATUS_NO_CD;
        return(0);
    }
     
    /* check bounds of tracks */
    if (hw->tochdr.cdth_trk1 > MAX_TRACKS) {
        errormsg("%P: unexpected end track %d in TOC", hw->tochdr.cdth_trk1);
        hw->tochdr.cdth_trk1 = MAX_TRACKS;
    }

    /* by now subchnl and tochdr have been read; should only get here
     * when there is a CD inserted */
    hw->iStatus = CD_STATUS_CD_IN;
    hw->iData = hw->iAudio = 0;

    /* read individual track entries (including leadout) */
    for (i = first_track(hw)-1; i <= last_track(hw); i++) {
        tocentry = &hw->tocentries[i];
        tocentry->cdte_format = CDROM_MSF;
        tocentry->cdte_track = (i==last_track(hw)) ? CDROM_LEADOUT : i+1;

        debugmsg("%P: reading track %d tocentry", tocentry->cdte_track);

        if (ioctl(fd, CDROMREADTOCENTRY, tocentry) == -1) {
            errormsg("%P: ioctl readtocentry track %d: %s", i, strerror(errno));
            hw->iStatus = CD_STATUS_ERROR;
            hw->subchnl.cdsc_audiostatus = CDROM_AUDIO_ERROR;
            return(-1);
        }

        /* determine if data or audio track, skip CDROM_LEADOUT */
        if (i <= last_track(hw)) {
            if (is_audio_track(hw, i + 1))
                hw->iAudio++;
            else
                hw->iData++;
        }
    }

    /* if no audio tracks, i.e., all-data disc, set status to DATA_DISC */
    if (hw->iAudio == 0) {
        hw->iStatus = CD_STATUS_DATA_DISC;
        hw->subchnl.cdsc_audiostatus = CDROM_DATA_DISC;
    }

    return(0);
}

/************************************************************************
 * Procedure:  check_audio_range()
 * Purpose:    determine last audio track in the requested range
 * Returns:    >0  last audio track, -1  no audio tracks in range
 *
 ************************************************************************/

int check_audio_range(cdhw_t *hw, int trk0, int trk1)
{
    int newlast, rest, warned;

    for (; trk0 <= trk1; trk0++)
        if (is_audio_track(hw, trk0))
            break;

    if (trk0 > trk1)
        /* no audio tracks within requested range */
        return(-1);

    for (newlast = trk0; newlast < trk1; newlast++)
        if (!is_audio_track(hw, newlast + 1))
            break;

    for (rest=newlast+1, warned=0; rest <= trk1; rest++)
        if (is_audio_track(hw, rest)) {
            if (!warned) {
                errormsg("%P: warning, only playing to first "
                  "data track in range");
                fprintf(stderr, "   audio tracks");
                warned = 1;
            }
            fprintf(stderr, " %d", rest);
        }

    if (warned)
        errormsg(" will be missed");

    return newlast;
}

/*************************************************************************
 * Procedure:  check_disc 
 * Purpose:    determine if there may be an audio disc inserted
 ************************************************************************/
int check_disc(cdhw_t *hw)
{
    int status;
    switch (hw->iStatus) {
        case CD_STATUS_CD_IN:
        case CD_STATUS_INSERTED:
            status = 1;
            break;
        
	case CD_STATUS_ERROR:
        case CD_STATUS_NO_CD:
        case CD_STATUS_REMOVED:
        case CD_STATUS_MOUNTED:
        case CD_STATUS_DATA_DISC:
	default:
            status = 0;
    }
    return(status);
}

/************************************************************************/
/* Procedure:  cd_status_text
 * Purpose:    to get status name in text
 * 
 * Inputs:     status code
 * Outputs:    to buffer
 * Returns:    pointer to status text string
 * Notes:  
 *   1. Input values:
 *
 *	0	No CD in drive.
 *	1	CD in drive.
 *	2	CD has just been inserted (TOC has been read)
 *	3	CD has just been removed
 *	4	CD is mounted
 *
 */
/************************************************************************/
char *cd_status_text (int iStatus)
{
    char *pszResult;
    switch (iStatus) 
    {
        case CD_STATUS_ERROR:
            pszResult = "error";
            break;
        case CD_STATUS_NO_CD:
            pszResult = "no_disc";
            break;
        case CD_STATUS_CD_IN:
            pszResult = "disc_in";
            break;
        case CD_STATUS_INSERTED:
            pszResult = "inserted";
            break;
        case CD_STATUS_REMOVED:
            pszResult = "removed";
            break;
        case CD_STATUS_MOUNTED:
            pszResult = "mounted";
            break;
        case CD_STATUS_DATA_DISC:
            pszResult = "data_disc";
            break;
        default:
            pszResult = "unknown";
            break;
    }
    return (pszResult);
}

/* convert track offset to MSF format */
struct cdrom_msf *
msf_from_tracks(cdhw_t *hw, int first, int last)
{
    struct cdrom_msf *msf;

    if (first < 1 || first > hw->tochdr.cdth_trk1) {
        errormsg("%P: invalid first track %d", first);
        return NULL;
    }

    msf = malloc(sizeof(*msf));
    if (!msf)
        return NULL;

    /* use MSF start of the first track (one-based) */
    msf->cdmsf_min0 = hw->tocentries[first-1].cdte_addr.msf.minute;
    msf->cdmsf_sec0 = hw->tocentries[first-1].cdte_addr.msf.second;
    msf->cdmsf_frame0 = hw->tocentries[first-1].cdte_addr.msf.frame;

    if (last < 1 || last > hw->tochdr.cdth_trk1) {
        errormsg("%P: invalid last track %d", last);
	free(msf);
        return NULL;
    }

    /* use MSF stop from the start of the track _after_ "last" */
    msf->cdmsf_min1 = hw->tocentries[last].cdte_addr.msf.minute;
    msf->cdmsf_sec1 = hw->tocentries[last].cdte_addr.msf.second;
    msf->cdmsf_frame1 = hw->tocentries[last].cdte_addr.msf.frame;
 
    return msf;
}

/************************************************************************/
/* Procedure:  do_play_ti
 * Purpose:    to start playing
 * Inputs:     track index
 * Returns:    0< on error; 0 on success
 * 
 ************************************************************************/
int do_play_ti(int fd, cdhw_t *hw, struct cdrom_ti *ti)
{
#ifdef NOSCSI
    if (ioctl(fd, CDROMPLAYTRKIND, ti) < 0) {
        /* patch by <Ingo.Wilken@Informatik.Uni-Oldenburg.DE> for Plextor */
        /* PX-32TSI drive: retry with all indices set to 1 */
        ti->cdti_ind1 = 1;
        if (ioctl(fd, CDROMPLAYTRKIND, ti) < 0) {
            errormsg("%P: ioctl cdromplaytrkind: %s", strerror(errno));
            return(-1);
        }
    }
#else
    struct cdrom_msf *msf;

    if (ioctl(fd, CDROMSTART) < 0) {
        errormsg("%P: ioctl cdromstart: %s", strerror(errno));
        return(-1);
    }

    /* convert track index into MSF format */
    msf = msf_from_tracks(hw, ti->cdti_trk0, ti->cdti_trk1);
    if (!msf)
        return(-1);
    
    if (ioctl(fd, CDROMPLAYMSF, msf) == -1) {
        errormsg("%P: ioctl cdromplaymsf: %s", strerror(errno));
        return(-1);
    }

    free(msf);
#endif
    return(0);
}

