/*
    $Id: _cdio_osx.c,v 1.4 2005/01/01 02:43:57 rockyb Exp $

    Copyright (C) 2003, 2004 Rocky Bernstein <rocky@panix.com> 
    from vcdimager code: 
    Copyright (C) 2001 Herbert Valerio Riedel <hvr@gnu.org>
    and VideoLAN code Copyright (C) 1998-2001 VideoLAN
      Authors: Johan Bilien <jobi@via.ecp.fr>
               Gildas Bazin <gbazin@netcourrier.com>
               Jon Lech Johansen <jon-vl@nanocrew.net>
               Derk-Jan Hartman <hartman at videolan.org>
               Justin F. Hallett <thesin@southofheaven.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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/* This file contains OSX-specific code and implements low-level 
   control of the CD drive.
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

static const char _rcsid[] = "$Id: _cdio_osx.c,v 1.4 2005/01/01 02:43:57 rockyb Exp $";

#include <cdio/logging.h>
#include <cdio/sector.h>
#include <cdio/util.h>
#include "cdio_assert.h"
#include "cdio_private.h"

#include <string.h>

#ifdef HAVE_DARWIN_CDROM
#undef VERSION 

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/storage/IOStorageDeviceCharacteristics.h>

#include <mach/mach.h>
#include <Carbon/Carbon.h>
#include <IOKit/scsi-commands/SCSITaskLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <mach/mach_error.h>

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>


#include <paths.h>
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOBSD.h>
#include <IOKit/scsi-commands/IOSCSIMultimediaCommandsDevice.h>
#include <IOKit/storage/IOCDTypes.h>
#include <IOKit/storage/IODVDTypes.h>
#include <IOKit/storage/IOMedia.h>
#include <IOKit/storage/IOCDMedia.h>
#include <IOKit/storage/IODVDMedia.h>
#include <IOKit/storage/IOCDMediaBSDClient.h>
#include <IOKit/storage/IODVDMediaBSDClient.h>
#include <IOKit/storage/IOStorageDeviceCharacteristics.h>

#define kIOCDBlockStorageDeviceClassString		"IOCDBlockStorageDevice"

/* Note leadout is normally defined 0xAA, But on OSX 0xA0 is "lead in" while
   0xA2 is "lead out". Don't ask me why. */
#define	OSX_CDROM_LEADOUT_TRACK 0xA2

#define TOTAL_TRACKS    (p_env->i_last_track - p_env->gen.i_first_track + 1)

#define CDROM_CDI_TRACK 0x1
#define CDROM_XA_TRACK  0x2

typedef enum {
  _AM_NONE,
  _AM_OSX,
} access_mode_t;

#define MAX_SERVICE_NAME 1000
typedef struct {
  /* Things common to all drivers like this. 
     This must be first. */
  generic_img_private_t gen; 

  access_mode_t access_mode;

  /* Track information */
  CDTOC *pTOC;
  int i_descriptors;
  track_t i_last_track;      /* highest track number */
  track_t i_last_session;    /* highest session number */
  track_t i_first_session;   /* first session number */
  lsn_t   *pp_lba;
  io_service_t MediaClass_service;
  char    psz_MediaClass_service[MAX_SERVICE_NAME];
  SCSITaskDeviceInterface **pp_scsiTaskDeviceInterface;

} _img_private_t;

static bool read_toc_osx (void *p_user_data);

/****
 * GetRegistryEntryProperties - Gets the registry entry properties for
 *  an io_service_t.
 *****/

static CFMutableDictionaryRef
GetRegistryEntryProperties ( io_service_t service )
{
  IOReturn			err	= kIOReturnSuccess;
  CFMutableDictionaryRef	dict	= 0;
  
  err = IORegistryEntryCreateCFProperties (service, &dict, kCFAllocatorDefault, 0); 
  if ( err != kIOReturnSuccess )
    cdio_warn( "IORegistryEntryCreateCFProperties: 0x%08x", err );

  return dict;
}


static bool 
init_osx(_img_private_t *p_env) {
  mach_port_t port;
  char *psz_devname;
  kern_return_t ret;
  io_iterator_t iterator;
  
  p_env->gen.fd = open( p_env->gen.source_name, O_RDONLY | O_NONBLOCK );
  if (-1 == p_env->gen.fd) {
    cdio_warn("Failed to open %s: %s", p_env->gen.source_name,
	       strerror(errno));
    return false;
  }

  /* get the device name */
  psz_devname = strrchr( p_env->gen.source_name, '/');
  if( NULL != psz_devname )
    ++psz_devname;
  else
    psz_devname = p_env->gen.source_name;
  
  /* unraw the device name */
  if( *psz_devname == 'r' )
    ++psz_devname;
  
  /* get port for IOKit communication */
  ret = IOMasterPort( MACH_PORT_NULL, &port );
  
  if( ret != KERN_SUCCESS )
    {
      cdio_warn( "IOMasterPort: 0x%08x", ret );
      return false;
    }
  
  ret = IOServiceGetMatchingServices( port, 
				      IOBSDNameMatching(port, 0, psz_devname),
				      &iterator );
  
  /* get service iterator for the device */
  if( ret != KERN_SUCCESS )
    {
        cdio_warn( "IOServiceGetMatchingServices: 0x%08x", ret );
        return false;
    }
  
  /* first service */
  p_env->MediaClass_service = IOIteratorNext( iterator );
  IOObjectRelease( iterator );
  
  /* search for kIOCDMediaClass or kIOCDVDMediaClass */ 
  while( p_env->MediaClass_service && 
	 (!IOObjectConformsTo(p_env->MediaClass_service, kIOCDMediaClass)) &&
	 (!IOObjectConformsTo(p_env->MediaClass_service, kIODVDMediaClass)) )
    {

      ret = IORegistryEntryGetParentIterator( p_env->MediaClass_service, 
					      kIOServicePlane, 
					      &iterator );
      if( ret != KERN_SUCCESS )
        {
	  cdio_warn( "IORegistryEntryGetParentIterator: 0x%08x", ret );
	  IOObjectRelease( p_env->MediaClass_service );
	  return false;
        }
      
      IOObjectRelease( p_env->MediaClass_service );
      p_env->MediaClass_service = IOIteratorNext( iterator );
      IOObjectRelease( iterator );
    }
  
  if ( 0 == p_env->MediaClass_service )     {
    cdio_warn( "search for kIOCDMediaClass/kIODVDMediaClass came up empty" );
    return false;
  }

  /* Save the name so we can compare against this in case we have to do
     another scan. FIXME: this is hoaky and there's got to be a better
     variable to test or way to do.
   */
  IORegistryEntryGetPath(p_env->MediaClass_service, kIOServicePlane, 
			 p_env->psz_MediaClass_service);
  return true;
}

/*!
  Run a SCSI MMC command. 
 
  cdio	        CD structure set by cdio_open().
  i_timeout     time in milliseconds we will wait for the command
                to complete. If this value is -1, use the default 
		time-out value.
  p_buf	        Buffer for data, both sending and receiving
  i_buf	        Size of buffer
  e_direction	direction the transfer is to go.
  cdb	        CDB bytes. All values that are needed should be set on 
                input. We'll figure out what the right CDB length should be.

  We return true if command completed successfully and false if not.
 */
static int
run_scsi_cmd_osx( const void *p_user_data, 
		  unsigned int i_timeout_ms,
		  unsigned int i_cdb, const scsi_mmc_cdb_t *p_cdb, 
		  scsi_mmc_direction_t e_direction, 
		  unsigned int i_buf, /*in/out*/ void *p_buf )
{

#ifndef SCSI_MMC_FIXED
  return 2;
#else 
  const _img_private_t *p_env = p_user_data;
  SCSITaskDeviceInterface **sc;
  SCSITaskInterface **cmd = NULL;
  IOVirtualRange iov;
  SCSI_Sense_Data senseData;
  SCSITaskStatus status;
  UInt64 bytesTransferred;
  IOReturn ioReturnValue;
  int ret = 0;

  if (NULL == p_user_data) return 2;

  /* Make sure pp_scsiTaskDeviceInterface is initialized. FIXME: The code
     should probably be reorganized better for this. */
  if (!p_env->gen.toc_init) read_toc_osx (p_user_data) ;

  sc = p_env->pp_scsiTaskDeviceInterface;

  if (NULL == sc) return 3;

  cmd = (*sc)->CreateSCSITask(sc);
  if (cmd == NULL) {
    cdio_warn("Failed to create SCSI task");
    return -1;
  }

  iov.address = (IOVirtualAddress) p_buf;
  iov.length = i_buf;

  ioReturnValue = (*cmd)->SetCommandDescriptorBlock(cmd, (UInt8 *) p_cdb, 
						    i_cdb);
  if (ioReturnValue != kIOReturnSuccess) {
    cdio_warn("SetCommandDescriptorBlock failed with status %x", 
	      ioReturnValue);
    return -1;
  }

  ioReturnValue = (*cmd)->SetScatterGatherEntries(cmd, &iov, 1, i_buf,
						  (SCSI_MMC_DATA_READ == e_direction ) ? 
						  kSCSIDataTransfer_FromTargetToInitiator :
						  kSCSIDataTransfer_FromInitiatorToTarget);
  if (ioReturnValue != kIOReturnSuccess) {
    cdio_warn("SetScatterGatherEntries failed with status %x", ioReturnValue);
    return -1;
  }

  ioReturnValue = (*cmd)->SetTimeoutDuration(cmd, i_timeout_ms );
  if (ioReturnValue != kIOReturnSuccess) {
    cdio_warn("SetTimeoutDuration failed with status %x", ioReturnValue);
    return -1;
  }

  memset(&senseData, 0, sizeof(senseData));

  ioReturnValue = (*cmd)->ExecuteTaskSync(cmd,&senseData, &status, &
					  bytesTransferred);

  if (ioReturnValue != kIOReturnSuccess) {
    cdio_warn("Command execution failed with status %x", ioReturnValue);
    return -1;
  }

  if (cmd != NULL) {
    (*cmd)->Release(cmd);
  }

  return (ret);
#endif
}

/***************************************************************************
 * GetDeviceIterator - Gets an io_iterator_t for our class type
 ***************************************************************************/

static io_iterator_t
GetDeviceIterator ( const char * deviceClass )
{
  
  IOReturn	err	 = kIOReturnSuccess;
  io_iterator_t	iterator = MACH_PORT_NULL;
  
  err = IOServiceGetMatchingServices ( kIOMasterPortDefault,
				       IOServiceMatching ( deviceClass ),
				       &iterator );
  check ( err == kIOReturnSuccess );
  
  return iterator;
  
}

/***************************************************************************
 * GetFeaturesFlagsForDrive -Gets the bitfield which represents the
 * features flags.
 ***************************************************************************/

static bool
GetFeaturesFlagsForDrive ( CFDictionaryRef dict,
			   uint32_t *i_cdFlags,
			   uint32_t *i_dvdFlags )
{
  CFDictionaryRef propertiesDict = 0;
  CFNumberRef     flagsNumberRef = 0;
  
  *i_cdFlags = 0;
  *i_dvdFlags= 0;
  
  propertiesDict = ( CFDictionaryRef ) 
    CFDictionaryGetValue ( dict, 
			   CFSTR ( kIOPropertyDeviceCharacteristicsKey ) );

  if ( propertiesDict == 0 ) return false;
  
  /* Get the CD features */
  flagsNumberRef = ( CFNumberRef ) 
    CFDictionaryGetValue ( propertiesDict, 
			   CFSTR ( kIOPropertySupportedCDFeatures ) );
  if ( flagsNumberRef != 0 ) {
    CFNumberGetValue ( flagsNumberRef, kCFNumberLongType, i_cdFlags );
  }
  
  /* Get the DVD features */
  flagsNumberRef = ( CFNumberRef ) 
    CFDictionaryGetValue ( propertiesDict, 
			   CFSTR ( kIOPropertySupportedDVDFeatures ) );
  if ( flagsNumberRef != 0 ) {
    CFNumberGetValue ( flagsNumberRef, kCFNumberLongType, i_dvdFlags );
  }

  return true;
}

/*! 
  Get disc type associated with the cd object.
*/
static discmode_t
get_discmode_osx (void *p_user_data)
{
  _img_private_t *p_env = p_user_data;
  char str[10];
  int32_t i_discmode = CDIO_DISC_MODE_ERROR;
  CFDictionaryRef propertiesDict = 0;
  CFStringRef data;

  propertiesDict  = GetRegistryEntryProperties ( p_env->MediaClass_service );

  if ( propertiesDict == 0 ) return i_discmode;

  data = ( CFStringRef ) 
    CFDictionaryGetValue ( propertiesDict, CFSTR ( kIODVDMediaTypeKey ) );

  if( CFStringGetCString( data, str, sizeof(str),
			  kCFStringEncodingASCII ) ) {
    if (0 == strncmp(str, "DVD+R", strlen(str)) )
      i_discmode = CDIO_DISC_MODE_DVD_PR;
    else if (0 == strncmp(str, "DVD+RW", strlen(str)) ) 
      i_discmode = CDIO_DISC_MODE_DVD_PRW;
    else if (0 == strncmp(str, "DVD-R", strlen(str)) ) 
      i_discmode = CDIO_DISC_MODE_DVD_R;
    else if (0 == strncmp(str, "DVD-RW", strlen(str)) ) 
      i_discmode = CDIO_DISC_MODE_DVD_RW;
    else if (0 == strncmp(str, "DVD-ROM", strlen(str)) ) 
      i_discmode = CDIO_DISC_MODE_DVD_ROM;
    else if (0 == strncmp(str, "DVD-RAM", strlen(str)) ) 
      i_discmode = CDIO_DISC_MODE_DVD_RAM;
    else if (0 == strncmp(str, "CD-ROM", strlen(str)) )
      i_discmode = CDIO_DISC_MODE_CD_DATA;
    else if (0 == strncmp(str, "CDR", strlen(str)) ) 
      i_discmode = CDIO_DISC_MODE_CD_DATA;
    else if (0 == strncmp(str, "CDRW", strlen(str)) ) 
      i_discmode = CDIO_DISC_MODE_CD_DATA;
    //??  Handled by below? CFRelease( data );
  }
  CFRelease( propertiesDict );    
  if (CDIO_DISC_MODE_CD_DATA == i_discmode) {
    /* Need to do more classification */
    return get_discmode_cd_generic(p_user_data);
  }
  return i_discmode;

}

static io_service_t
get_drive_service_osx(const _img_private_t *p_env)
{
  io_service_t  service;
  io_iterator_t service_iterator;
  
  service_iterator = GetDeviceIterator ( kIOCDBlockStorageDeviceClassString );

  if( service_iterator == MACH_PORT_NULL ) return 0;
  
  service = IOIteratorNext( service_iterator );
  if( service == 0 ) return 0;

  do
    {
      char psz_service[MAX_SERVICE_NAME];
      IORegistryEntryGetPath(service, kIOServicePlane, psz_service);
      psz_service[MAX_SERVICE_NAME-1] = '\0';
      
      /* FIXME: This is all hoaky. Here we need info from a parent class,
	 psz_service of what we opened above. We are relying on the
	 fact that the name  will be a substring of the name we
	 openned with.
      */
      if (0 == strncmp(psz_service, p_env->psz_MediaClass_service, 
		       strlen(psz_service))) {
	/* Found our device */
	IOObjectRelease( service_iterator );
	return service;
      }
      
      IOObjectRelease( service );
      
    } while( ( service = IOIteratorNext( service_iterator ) ) != 0 );

  IOObjectRelease( service_iterator );
  return service;
}

static void
get_drive_cap_osx(const void *p_user_data,
		  /*out*/ cdio_drive_read_cap_t  *p_read_cap,
		  /*out*/ cdio_drive_write_cap_t *p_write_cap,
		  /*out*/ cdio_drive_misc_cap_t  *p_misc_cap)
{
  const _img_private_t *p_env = p_user_data;
  uint32_t i_cdFlags;
  uint32_t i_dvdFlags;

  io_service_t  service = get_drive_service_osx(p_env);
  
  if( service == 0 ) goto err_exit;

  /* Found our device */
  {
    CFDictionaryRef  properties = GetRegistryEntryProperties ( service );
    
    if (! GetFeaturesFlagsForDrive ( properties, &i_cdFlags, 
				     &i_dvdFlags ) ) {
      IOObjectRelease( service );
      goto err_exit;
    }
    
    /* Reader */
    
    if ( 0 != (i_cdFlags & kCDFeaturesAnalogAudioMask) )
      *p_read_cap  |= CDIO_DRIVE_CAP_READ_AUDIO;      
    
    if ( 0 != (i_cdFlags & kCDFeaturesWriteOnceMask) ) 
      *p_write_cap |= CDIO_DRIVE_CAP_WRITE_CD_R;
    
    if ( 0 != (i_cdFlags & kCDFeaturesCDDAStreamAccurateMask) )
      *p_read_cap  |= CDIO_DRIVE_CAP_READ_CD_DA;
    
    if ( 0 != (i_dvdFlags & kDVDFeaturesReadStructuresMask) )
      *p_read_cap  |= CDIO_DRIVE_CAP_READ_DVD_ROM;
    
    if ( 0 != (i_cdFlags & kCDFeaturesReWriteableMask) )
      *p_write_cap |= CDIO_DRIVE_CAP_WRITE_CD_RW;
    
    if ( 0 != (i_dvdFlags & kDVDFeaturesWriteOnceMask) ) 
      *p_write_cap |= CDIO_DRIVE_CAP_WRITE_DVD_R;
    
    if ( 0 != (i_dvdFlags & kDVDFeaturesRandomWriteableMask) )
      *p_write_cap |= CDIO_DRIVE_CAP_WRITE_DVD_RAM;
    
    if ( 0 != (i_dvdFlags & kDVDFeaturesReWriteableMask) )
      *p_write_cap |= CDIO_DRIVE_CAP_WRITE_DVD_RW;
    
    /***
	if ( 0 != (i_dvdFlags & kDVDFeaturesPlusRMask) )
	*p_write_cap |= CDIO_DRIVE_CAP_WRITE_DVD_PR;
	
	if ( 0 != (i_dvdFlags & kDVDFeaturesPlusRWMask )
	*p_write_cap |= CDIO_DRIVE_CAP_WRITE_DVD_PRW;
	***/

    /* FIXME: fill out. For now assume CD-ROM is relatively modern. */
      *p_misc_cap = (
		     CDIO_DRIVE_CAP_MISC_CLOSE_TRAY 
		     | CDIO_DRIVE_CAP_MISC_EJECT
		     | CDIO_DRIVE_CAP_MISC_LOCK
		     | CDIO_DRIVE_CAP_MISC_SELECT_SPEED
		     | CDIO_DRIVE_CAP_MISC_MULTI_SESSION
		     | CDIO_DRIVE_CAP_MISC_MEDIA_CHANGED
		     | CDIO_DRIVE_CAP_MISC_RESET
		     | CDIO_DRIVE_CAP_MCN
		     | CDIO_DRIVE_CAP_ISRC
		     );

    IOObjectRelease( service );
  }
  
  return;

 err_exit:
  *p_misc_cap = *p_write_cap = *p_read_cap = CDIO_DRIVE_CAP_UNKNOWN;
  return;
}

#if 1
/****************************************************************************
 * GetDriveDescription - Gets drive description. 
 ****************************************************************************/

static bool
get_hwinfo_osx ( const CdIo *p_cdio, /*out*/ cdio_hwinfo_t *hw_info)
{
  _img_private_t *p_env = (_img_private_t *) p_cdio->env;
  io_service_t  service = get_drive_service_osx(p_env);

  if ( service == 0 ) return false;
  
  /* Found our device */
  {
    CFStringRef      vendor      = NULL;
    CFStringRef      product     = NULL;
    CFStringRef      revision    = NULL;
  
    CFDictionaryRef  properties  = GetRegistryEntryProperties ( service );
    CFDictionaryRef  deviceDict  = ( CFDictionaryRef ) 
      CFDictionaryGetValue ( properties, 
			     CFSTR ( kIOPropertyDeviceCharacteristicsKey ) );
    
    if ( deviceDict == 0 ) return false;
    
    vendor = ( CFStringRef ) 
      CFDictionaryGetValue ( deviceDict, CFSTR ( kIOPropertyVendorNameKey ) );
    
    if ( CFStringGetCString( vendor,
			     (char *) &(hw_info->psz_vendor),
			     sizeof(hw_info->psz_vendor),
			     kCFStringEncodingASCII ) )
      CFRelease( vendor );
    
    product = ( CFStringRef ) 
      CFDictionaryGetValue ( deviceDict, CFSTR ( kIOPropertyProductNameKey ) );
    
    if ( CFStringGetCString( product,
			     (char *) &(hw_info->psz_model),
			     sizeof(hw_info->psz_model),
			     kCFStringEncodingASCII ) )
      CFRelease( product );
    
    revision = ( CFStringRef ) 
      CFDictionaryGetValue ( deviceDict, 
			     CFSTR ( kIOPropertyProductRevisionLevelKey ) );
    
    if ( CFStringGetCString( product,
			     (char *) &(hw_info->psz_revision),
			     sizeof(hw_info->psz_revision),
			     kCFStringEncodingASCII ) )
      CFRelease( revision );
  }
  return true;
  
}
#endif

/*!
  Return the media catalog number MCN.

  Note: string is malloc'd so caller should free() then returned
  string when done with it.

 */
static const cdtext_t *
get_cdtext_osx (void *p_user_data, track_t i_track) 
{
  return NULL;
}

static void 
_free_osx (void *p_user_data) {
  _img_private_t *p_env = p_user_data;
  if (NULL == p_env) return;
  cdio_generic_free(p_env);
  if (NULL != p_env->pp_lba)  free((void *) p_env->pp_lba);
  if (NULL != p_env->pTOC)    free((void *) p_env->pTOC);
  IOObjectRelease( p_env->MediaClass_service );

  if (NULL != p_env->pp_scsiTaskDeviceInterface) 
    ( *(p_env->pp_scsiTaskDeviceInterface) )->
      Release ( (p_env->pp_scsiTaskDeviceInterface) );

}

/*!
   Reads nblocks of mode2 form2 sectors from cd device into data starting
   from lsn.
   Returns 0 if no error. 
 */
static int
_get_read_mode1_sectors_osx (void *user_data, void *data, lsn_t lsn, 
			     bool b_form2, unsigned int nblocks)
{
  _img_private_t *env = user_data;
  dk_cd_read_t cd_read;
  
  memset( &cd_read, 0, sizeof(cd_read) );
  
  cd_read.sectorArea  = kCDSectorAreaUser;
  cd_read.buffer      = data;
  cd_read.sectorType  = kCDSectorTypeMode1;
  
  if (b_form2) {
    cd_read.offset       = lsn * kCDSectorSizeMode2;
    cd_read.bufferLength = kCDSectorSizeMode2 * nblocks;
  } else {
    cd_read.offset       = lsn * kCDSectorSizeMode1;
    cd_read.bufferLength = kCDSectorSizeMode1 * nblocks;
  }
  
   if( ioctl( env->gen.fd, DKIOCCDREAD, &cd_read ) == -1 )
  {
    cdio_info( "could not read block %d, %s", lsn, strerror(errno) );
    return -1;
  }
  return 0;
}

  
/*!
   Reads nblocks of mode2 form2 sectors from cd device into data starting
   from lsn.
   Returns 0 if no error. 
 */
static int
_get_read_mode2_sectors_osx (void *user_data, void *data, lsn_t lsn, 
			     bool b_form2, unsigned int nblocks)
{
  _img_private_t *env = user_data;
  dk_cd_read_t cd_read;
  
  memset( &cd_read, 0, sizeof(cd_read) );
  
  cd_read.sectorArea = kCDSectorAreaUser;
  cd_read.buffer = data;
  
  if (b_form2) {
    cd_read.offset       = lsn * kCDSectorSizeMode2Form2;
    cd_read.sectorType   = kCDSectorTypeMode2Form2;
    cd_read.bufferLength = kCDSectorSizeMode2Form2 * nblocks;
  } else {
    cd_read.offset       = lsn * kCDSectorSizeMode2Form1;
    cd_read.sectorType   = kCDSectorTypeMode2Form1;
    cd_read.bufferLength = kCDSectorSizeMode2Form1 * nblocks;
  }
  
  if( ioctl( env->gen.fd, DKIOCCDREAD, &cd_read ) == -1 )
  {
    cdio_info( "could not read block %d, %s", lsn, strerror(errno) );
    return -1;
  }
  return 0;
}

  
/*!
   Reads a single audio sector from CD device into data starting from lsn.
   Returns 0 if no error. 
 */
static int
_get_read_audio_sectors_osx (void *user_data, void *data, lsn_t lsn, 
			     unsigned int nblocks)
{
  _img_private_t *env = user_data;
  dk_cd_read_t cd_read;
  
  memset( &cd_read, 0, sizeof(cd_read) );
  
  cd_read.offset       = lsn * kCDSectorSizeCDDA;
  cd_read.sectorArea   = kCDSectorAreaUser;
  cd_read.sectorType   = kCDSectorTypeCDDA;
  
  cd_read.buffer       = data;
  cd_read.bufferLength = kCDSectorSizeCDDA * nblocks;
  
  if( ioctl( env->gen.fd, DKIOCCDREAD, &cd_read ) == -1 )
  {
    cdio_info( "could not read block %d", lsn );
    return -1;
  }
  return 0;
}

/*!
   Reads a single mode2 sector from cd device into data starting
   from lsn. Returns 0 if no error. 
 */
static int
_get_read_mode1_sector_osx (void *user_data, void *data, lsn_t lsn, 
			    bool b_form2)
{
  return _get_read_mode1_sectors_osx(user_data, data, lsn, b_form2, 1);
}

/*!
   Reads a single mode2 sector from cd device into data starting
   from lsn. Returns 0 if no error. 
 */
static int
_get_read_mode2_sector_osx (void *user_data, void *data, lsn_t lsn, 
			    bool b_form2)
{
  return _get_read_mode2_sectors_osx(user_data, data, lsn, b_form2, 1);
}

/*!
  Set the key "arg" to "value" in source device.
*/
static int
_set_arg_osx (void *user_data, const char key[], const char value[])
{
  _img_private_t *env = user_data;

  if (!strcmp (key, "source"))
    {
      if (!value)
	return -2;

      free (env->gen.source_name);
      
      env->gen.source_name = strdup (value);
    }
  else if (!strcmp (key, "access-mode"))
    {
      if (!strcmp(value, "OSX"))
	env->access_mode = _AM_OSX;
      else
	cdio_warn ("unknown access type: %s. ignored.", value);
    }
  else 
    return -1;

  return 0;
}

#if 0
static void TestDevice(_img_private_t *p_env, io_service_t service)
{
  SInt32                          score;
  HRESULT                         herr;
  kern_return_t                   err;
  IOCFPlugInInterface             **plugInInterface = NULL;
  MMCDeviceInterface              **mmcInterface = NULL;

  /* Create the IOCFPlugIn interface so we can query it. */

  err = IOCreatePlugInInterfaceForService ( service,
					    kIOMMCDeviceUserClientTypeID,
					    kIOCFPlugInInterfaceID,
					    &plugInInterface,
					    &score );
  if ( err != noErr ) {
    printf("IOCreatePlugInInterfaceForService returned %d\n", err);
    return;
  }
  
  /* Query the interface for the MMCDeviceInterface. */
  
  herr = ( *plugInInterface )->QueryInterface ( plugInInterface,
						CFUUIDGetUUIDBytes ( kIOMMCDeviceInterfaceID ),
						( LPVOID ) &mmcInterface );
  
  if ( herr != S_OK )     {
    printf("QueryInterface returned %ld\n", herr);
    return;
  }
  
  p_env->pp_scsiTaskDeviceInterface = 
    ( *mmcInterface )->GetSCSITaskDeviceInterface ( mmcInterface );
  
  if ( NULL == p_env->pp_scsiTaskDeviceInterface )  {
    printf("GetSCSITaskDeviceInterface returned NULL\n");
    return;
  }
  
  ( *mmcInterface )->Release ( mmcInterface );
  IODestroyPlugInInterface ( plugInInterface );
}
#endif

/*! 
  Read and cache the CD's Track Table of Contents and track info.
  Return false if successful or true if an error.
*/
static bool
read_toc_osx (void *p_user_data) 
{
  _img_private_t *p_env = p_user_data;
  CFDictionaryRef propertiesDict = 0;
  CFDataRef data;

  /* create a CF dictionary containing the TOC */
  propertiesDict = GetRegistryEntryProperties( p_env->MediaClass_service );

  if ( 0 == propertiesDict )     {
    return false;
  }

  /* get the TOC from the dictionary */
  data = (CFDataRef) CFDictionaryGetValue( propertiesDict,
					   CFSTR(kIOCDMediaTOCKey) );
  if ( data  != NULL ) {
    CFRange range;
    CFIndex buf_len;
    
    buf_len = CFDataGetLength( data ) + 1;
    range = CFRangeMake( 0, buf_len );
    
    if( ( p_env->pTOC = (CDTOC *)malloc( buf_len ) ) != NULL ) {
      CFDataGetBytes( data, range, (u_char *) p_env->pTOC );
    } else {
      cdio_warn( "Trouble allocating CDROM TOC" );
      return false;
    }
  } else     {
    cdio_warn( "Trouble reading TOC" );
    return false;
  }

  /* TestDevice(p_env, service); */
  CFRelease( propertiesDict );    

  p_env->i_descriptors = CDTOCGetDescriptorCount ( p_env->pTOC );

  /* Read in starting sectors. There may be non-tracks mixed in with
     the real tracks.  So find the first and last track number by
     scanning. Also find the lead-out track position.
   */
  {
    int i, i_leadout = -1;
    
    CDTOCDescriptor *pTrackDescriptors;
    
    p_env->pp_lba = malloc( p_env->i_descriptors * sizeof(int) );
    if( p_env->pp_lba == NULL )
      {
	cdio_warn("Out of memory in allocating track starting LSNs" );
	free( p_env->pTOC );
	return false;
      }
    
    pTrackDescriptors = p_env->pTOC->descriptors;

    p_env->gen.i_first_track   = CDIO_CD_MAX_TRACKS+1;
    p_env->i_last_track    = CDIO_CD_MIN_TRACK_NO;
    p_env->i_first_session = CDIO_CD_MAX_TRACKS+1;
    p_env->i_last_session  = CDIO_CD_MIN_TRACK_NO;
    
    for( i = 0; i <= p_env->i_descriptors; i++ )
      {
	track_t i_track     = pTrackDescriptors[i].point;
	session_t i_session = pTrackDescriptors[i].session;

	cdio_debug( "point: %d, tno: %d, session: %d, adr: %d, control:%d, "
		    "address: %d:%d:%d, p: %d:%d:%d", 
		    i_track,
		    pTrackDescriptors[i].tno, i_session,
		    pTrackDescriptors[i].adr, pTrackDescriptors[i].control,
		    pTrackDescriptors[i].address.minute,
		    pTrackDescriptors[i].address.second,
		    pTrackDescriptors[i].address.frame, 
		    pTrackDescriptors[i].p.minute,
		    pTrackDescriptors[i].p.second, 
		    pTrackDescriptors[i].p.frame );

	/* track information has adr = 1 */
	if ( 0x01 != pTrackDescriptors[i].adr ) 
	  continue;

	if( i_track == OSX_CDROM_LEADOUT_TRACK )
	  i_leadout = i;

	if( i_track > CDIO_CD_MAX_TRACKS || i_track < CDIO_CD_MIN_TRACK_NO )
	  continue;

	if (p_env->gen.i_first_track > i_track) 
	  p_env->gen.i_first_track = i_track;
	
	if (p_env->i_last_track < i_track) 
	  p_env->i_last_track = i_track;
	
	if (p_env->i_first_session > i_session) 
	  p_env->i_first_session = i_session;
	
	if (p_env->i_last_session < i_session) 
	  p_env->i_last_session = i_session;
      }

    /* Now that we know what the first track number is, we can make sure
       index positions are ordered starting at 0.
     */
    for( i = 0; i <= p_env->i_descriptors; i++ )
      {
	track_t i_track = pTrackDescriptors[i].point;

	if( i_track > CDIO_CD_MAX_TRACKS || i_track < CDIO_CD_MIN_TRACK_NO )
	  continue;

	/* Note what OSX calls a LBA we call an LSN. So below re we 
	   really have have MSF -> LSN -> LBA.
	 */
	p_env->pp_lba[i_track - p_env->gen.i_first_track] =
	  cdio_lsn_to_lba(CDConvertMSFToLBA( pTrackDescriptors[i].p ));
      }
    
    if( i_leadout == -1 )
      {
	cdio_warn( "CD leadout not found" );
	free( p_env->pp_lba );
	free( (void *) p_env->pTOC );
	return false;
      }
    
    /* Set leadout sector. 
       Note what OSX calls a LBA we call an LSN. So below re we 
       really have have MSF -> LSN -> LBA.
    */
    p_env->pp_lba[TOTAL_TRACKS] =
      cdio_lsn_to_lba(CDConvertMSFToLBA( pTrackDescriptors[i_leadout].p ));
    p_env->gen.i_tracks = TOTAL_TRACKS;
  }

  p_env->gen.toc_init   = true;

  return( true ); 

}

/*!  
  Return the starting LSN track number
  i_track in obj.  Track numbers start at 1.
  The "leadout" track is specified either by
  using i_track LEADOUT_TRACK or the total tracks+1.
  False is returned if there is no track entry.
*/
static lsn_t
get_track_lba_osx(void *p_user_data, track_t i_track)
{
  _img_private_t *p_env = p_user_data;

  if (!p_env->gen.toc_init) read_toc_osx (p_env) ;
  if (!p_env->gen.toc_init) return CDIO_INVALID_LSN;

  if (i_track == CDIO_CDROM_LEADOUT_TRACK) i_track = p_env->i_last_track+1;

  if (i_track > p_env->i_last_track + 1 || i_track < p_env->gen.i_first_track) {
    return CDIO_INVALID_LSN;
  } else {
    return p_env->pp_lba[i_track - p_env->gen.i_first_track];
  }
}

/*!
  Eject media . Return 1 if successful, 0 otherwise.

  The only way to cleanly unmount the disc under MacOS X is to use the
  'disktool' command line utility. It uses the non-public Disk
  Arbitration API, which can not be used by Cocoa or Carbon
  applications.

 */

static int 
_eject_media_osx (void *user_data) {

  _img_private_t *p_env = user_data;

  FILE *p_eject;
  char *psz_disk;
  char sz_cmd[32];

  if( ( psz_disk = (char *)strstr( p_env->gen.source_name, "disk" ) ) != NULL &&
      strlen( psz_disk ) > 4 )
    {
#define EJECT_CMD "/usr/sbin/hdiutil eject %s"
      snprintf( sz_cmd, sizeof(sz_cmd), EJECT_CMD, psz_disk );
#undef EJECT_CMD
      
      if( ( p_eject = popen( sz_cmd, "r" ) ) != NULL )
        {
	  char psz_result[0x200];
	  int i_ret = fread( psz_result, 1, sizeof(psz_result) - 1, p_eject );
	  
	  if( i_ret == 0 && ferror( p_eject ) != 0 )
            {
	      pclose( p_eject );
	      return 0;
            }
	  
	  pclose( p_eject );
	  
	  psz_result[ i_ret ] = 0;
	  
	  if( strstr( psz_result, "Disk Ejected" ) != NULL )
            {
	      return 1;
            }
        }
    }
  
  return 0;
}

/*!
   Return the size of the CD in logical block address (LBA) units.
 */
static uint32_t 
_stat_size_osx (void *user_data)
{
  return get_track_lba_osx(user_data, CDIO_CDROM_LEADOUT_TRACK);
}

/*!
  Return the value associated with the key "arg".
*/
static const char *
_get_arg_osx (void *user_data, const char key[])
{
  _img_private_t *p_env = user_data;

  if (!strcmp (key, "source")) {
    return p_env->gen.source_name;
  } else if (!strcmp (key, "access-mode")) {
    switch (p_env->access_mode) {
    case _AM_OSX:
      return "OS X";
    case _AM_NONE:
      return "no access method";
    }
  } 
  return NULL;
}

/*!
  Return the media catalog number MCN.
 */
static char *
get_mcn_osx (const void *user_data) {
  const _img_private_t *p_env = user_data;
  dk_cd_read_mcn_t cd_read;

  memset( &cd_read, 0, sizeof(cd_read) );

  if( ioctl( p_env->gen.fd, DKIOCCDREADMCN, &cd_read ) < 0 )
  {
    cdio_debug( "could not read MCN, %s", strerror(errno) );
    return NULL;
  }
  return strdup((char*)cd_read.mcn);
}


/*!  
  Get format of track. 
*/
static track_format_t
get_track_format_osx(void *user_data, track_t i_track) 
{
  _img_private_t *p_env = user_data;
  dk_cd_read_track_info_t cd_read;
  CDTrackInfo a_track;

  if (!p_env->gen.toc_init) read_toc_osx (p_env) ;

  if (i_track > p_env->i_last_track || i_track < p_env->gen.i_first_track)
    return TRACK_FORMAT_ERROR;
    
  memset( &cd_read, 0, sizeof(cd_read) );

  cd_read.address = i_track;
  cd_read.addressType = kCDTrackInfoAddressTypeTrackNumber;
  
  cd_read.buffer = &a_track;
  cd_read.bufferLength = sizeof(CDTrackInfo);
  
  if( ioctl( p_env->gen.fd, DKIOCCDREADTRACKINFO, &cd_read ) == -1 )
  {
    cdio_warn( "could not read trackinfo for track %d", i_track );
    return TRACK_FORMAT_ERROR;
  }

  cdio_debug( "%d: trackinfo trackMode: %x dataMode: %x", i_track, a_track.trackMode, a_track.dataMode );

  if (a_track.trackMode == CDIO_CDROM_DATA_TRACK) {
    if (a_track.dataMode == CDROM_CDI_TRACK) {
      return TRACK_FORMAT_CDI;
    } else if (a_track.dataMode == CDROM_XA_TRACK) {
      return TRACK_FORMAT_XA;
    } else {
      return TRACK_FORMAT_DATA;
    }
  } else {
    return TRACK_FORMAT_AUDIO;
  }

}

/*!
  Return true if we have XA data (green, mode2 form1) or
  XA data (green, mode2 form2). That is track begins:
  sync - header - subheader
  12     4      -  8

  FIXME: there's gotta be a better design for this and get_track_format?
*/
static bool
get_track_green_osx(void *user_data, track_t i_track) 
{
  _img_private_t *p_env = user_data;
  CDTrackInfo a_track;

  if (!p_env->gen.toc_init) read_toc_osx (p_env) ;

  if ( i_track > p_env->i_last_track || i_track < p_env->gen.i_first_track )
    return false;

  else {

    dk_cd_read_track_info_t cd_read;
    
    memset( &cd_read, 0, sizeof(cd_read) );
    
    cd_read.address      = i_track;
    cd_read.addressType  = kCDTrackInfoAddressTypeTrackNumber;
    
    cd_read.buffer       = &a_track;
    cd_read.bufferLength = sizeof(CDTrackInfo);
    
    if( ioctl( p_env->gen.fd, DKIOCCDREADTRACKINFO, &cd_read ) == -1 ) {
      cdio_warn( "could not read trackinfo for track %d", i_track );
      return false;
    }
    return ((a_track.trackMode & CDIO_CDROM_DATA_TRACK) != 0);
  }
}

#endif /* HAVE_DARWIN_CDROM */

/*!
  Return a string containing the default CD device if none is specified.
 */
char **
cdio_get_devices_osx(void)
{
#ifndef HAVE_DARWIN_CDROM
  return NULL;
#else
  io_object_t   next_media;
  mach_port_t   master_port;
  kern_return_t kern_result;
  io_iterator_t media_iterator;
  CFMutableDictionaryRef classes_to_match;
  char        **drives = NULL;
  unsigned int  num_drives=0;
  
  kern_result = IOMasterPort( MACH_PORT_NULL, &master_port );
  if( kern_result != KERN_SUCCESS )
    {
      return( NULL );
    }
  
  classes_to_match = IOServiceMatching( kIOCDMediaClass );
  if( classes_to_match == NULL )
    {
      return( NULL );
    }
  
  CFDictionarySetValue( classes_to_match, CFSTR(kIOMediaEjectableKey),
			kCFBooleanTrue );
  
  kern_result = IOServiceGetMatchingServices( master_port, 
					      classes_to_match,
					      &media_iterator );
  if( kern_result != KERN_SUCCESS )
    {
      return( NULL );
    }
  
  next_media = IOIteratorNext( media_iterator );
  if( next_media != 0 )
    {
      char psz_buf[0x32];
      size_t dev_path_length;
      CFTypeRef str_bsd_path;
      
      do
	{
	  str_bsd_path = IORegistryEntryCreateCFProperty( next_media,
							  CFSTR( kIOBSDNameKey ),
							  kCFAllocatorDefault,
							  0 );
	  if( str_bsd_path == NULL )
	    {
	      IOObjectRelease( next_media );
	      continue;
	    }
	  
	  snprintf( psz_buf, sizeof(psz_buf), "%s%c", _PATH_DEV, 'r' );
	  dev_path_length = strlen( psz_buf );
	  
	  if( CFStringGetCString( str_bsd_path,
				  (char*)&psz_buf + dev_path_length,
				  sizeof(psz_buf) - dev_path_length,
				  kCFStringEncodingASCII ) )
	    {
	      CFRelease( str_bsd_path );
	      IOObjectRelease( next_media );
	      IOObjectRelease( media_iterator );
	      cdio_add_device_list(&drives, strdup(psz_buf), &num_drives);
	    }
	  
	  CFRelease( str_bsd_path );
	  IOObjectRelease( next_media );
	  
	} while( ( next_media = IOIteratorNext( media_iterator ) ) != 0 );
    }
  IOObjectRelease( media_iterator );
  cdio_add_device_list(&drives, NULL, &num_drives);
  return drives;
#endif /* HAVE_DARWIN_CDROM */
}

/*!
  Return a string containing the default CD device if none is specified.
 */
char *
cdio_get_default_device_osx(void)
{
#ifndef HAVE_DARWIN_CDROM
  return NULL;
#else
  io_object_t   next_media;
  mach_port_t   master_port;
  kern_return_t kern_result;
  io_iterator_t media_iterator;
  CFMutableDictionaryRef classes_to_match;
  
  kern_result = IOMasterPort( MACH_PORT_NULL, &master_port );
  if( kern_result != KERN_SUCCESS )
    {
      return( NULL );
    }
  
  classes_to_match = IOServiceMatching( kIOCDMediaClass );
  if( classes_to_match == NULL )
    {
      return( NULL );
    }
  
  CFDictionarySetValue( classes_to_match, CFSTR(kIOMediaEjectableKey),
			kCFBooleanTrue );
  
  kern_result = IOServiceGetMatchingServices( master_port, 
					      classes_to_match,
					      &media_iterator );
  if( kern_result != KERN_SUCCESS )
    {
      return( NULL );
    }
  
  next_media = IOIteratorNext( media_iterator );
  if( next_media != 0 )
    {
      char psz_buf[0x32];
      size_t dev_path_length;
      CFTypeRef str_bsd_path;
      
      do
	{
	  str_bsd_path = IORegistryEntryCreateCFProperty( next_media,
							  CFSTR( kIOBSDNameKey ),
							  kCFAllocatorDefault,
							  0 );
	  if( str_bsd_path == NULL )
	    {
	      IOObjectRelease( next_media );
	      continue;
	    }
	  
	  snprintf( psz_buf, sizeof(psz_buf), "%s%c", _PATH_DEV, 'r' );
	  dev_path_length = strlen( psz_buf );
	  
	  if( CFStringGetCString( str_bsd_path,
				  (char*)&psz_buf + dev_path_length,
				  sizeof(psz_buf) - dev_path_length,
				  kCFStringEncodingASCII ) )
	    {
	      CFRelease( str_bsd_path );
	      IOObjectRelease( next_media );
	      IOObjectRelease( media_iterator );
	      return strdup( psz_buf );
	    }
	  
	  CFRelease( str_bsd_path );
	  IOObjectRelease( next_media );
	  
	} while( ( next_media = IOIteratorNext( media_iterator ) ) != 0 );
    }
  IOObjectRelease( media_iterator );
  return NULL;
#endif /* HAVE_DARWIN_CDROM */
}

/*!
  Initialization routine. This is the only thing that doesn't
  get called via a function pointer. In fact *we* are the
  ones to set that up.
 */
CdIo *
cdio_open_am_osx (const char *psz_source_name, const char *psz_access_mode)
{

  if (psz_access_mode != NULL)
    cdio_warn ("there is only one access mode for OS X. Arg %s ignored",
	       psz_access_mode);
  return cdio_open_osx(psz_source_name);
}


/*!
  Initialization routine. This is the only thing that doesn't
  get called via a function pointer. In fact *we* are the
  ones to set that up.
 */
CdIo *
cdio_open_osx (const char *psz_orig_source)
{

#ifdef HAVE_DARWIN_CDROM
  CdIo *ret;
  _img_private_t *_data;
  char *psz_source;

  cdio_funcs _funcs = {
    .eject_media        = _eject_media_osx,
    .free               = _free_osx,
    .get_arg            = _get_arg_osx,
    .get_cdtext         = get_cdtext_osx,
    .get_default_device = cdio_get_default_device_osx,
    .get_devices        = cdio_get_devices_osx,
    .get_discmode       = get_discmode_osx,
    .get_drive_cap      = get_drive_cap_osx,
    .get_first_track_num= get_first_track_num_generic,
    .get_hwinfo         = get_hwinfo_osx,
    .get_mcn            = get_mcn_osx,
    .get_num_tracks     = get_num_tracks_generic,
    .get_track_format   = get_track_format_osx,
    .get_track_green    = get_track_green_osx,
    .get_track_lba      = get_track_lba_osx,
    .get_track_msf      = NULL,
    .lseek              = cdio_generic_lseek,
    .read               = cdio_generic_read,
    .read_audio_sectors = _get_read_audio_sectors_osx,
    .read_mode1_sector  = _get_read_mode1_sector_osx,
    .read_mode1_sectors = _get_read_mode1_sectors_osx,
    .read_mode2_sector  = _get_read_mode2_sector_osx,
    .read_mode2_sectors = _get_read_mode2_sectors_osx,
    .read_toc           =  read_toc_osx,
    .run_scsi_mmc_cmd   =  run_scsi_cmd_osx,
    .set_arg            = _set_arg_osx,
    .stat_size          = _stat_size_osx
  };

  _data                 = _cdio_malloc (sizeof (_img_private_t));
  _data->access_mode    = _AM_OSX;
  _data->MediaClass_service = 0;
  _data->gen.init           = false;
  _data->gen.fd             = -1;
  _data->gen.toc_init       = false;
  _data->gen.b_cdtext_init  = false;
  _data->gen.b_cdtext_error = false;

  if (NULL == psz_orig_source) {
    psz_source=cdio_get_default_device_osx();
    if (NULL == psz_source) return NULL;
    _set_arg_osx(_data, "source", psz_source);
    free(psz_source);
  } else {
    if (cdio_is_device_generic(psz_orig_source))
      _set_arg_osx(_data, "source", psz_orig_source);
    else {
      /* The below would be okay if all device drivers worked this way. */
#if 0
      cdio_info ("source %s is a not a device", psz_orig_source);
#endif
      return NULL;
    }
  }

  ret = cdio_new ((void *)_data, &_funcs);
  if (ret == NULL) return NULL;

  if (cdio_generic_init(_data) && init_osx(_data))
    return ret;
  else {
    cdio_generic_free (_data);
    return NULL;
  }
  
#else 
  return NULL;
#endif /* HAVE_DARWIN_CDROM */

}

bool
cdio_have_osx (void)
{
#ifdef HAVE_DARWIN_CDROM
  return true;
#else 
  return false;
#endif /* HAVE_DARWIN_CDROM */
}
