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

    Copyright (C) 2004 Rocky Bernstein <rocky@panix.com>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    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 FreeBSD-specific code and implements low-level
   control of the CD drive via SCSI emulation.
*/

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

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

#ifdef HAVE_FREEBSD_CDROM

#include "freebsd.h"
#include <cdio/scsi_mmc.h>

/* Default value in seconds we will wait for a command to 
   complete. */
#define DEFAULT_TIMEOUT_MSECS 10000

/*!
  Run a SCSI MMC command. 
 
  p_user_data   internal CD structure.
  i_timeout_ms  time in milliseconds we will wait for the command
                to complete. If this value is -1, use the default 
		time-out value.
  i_cdb	        Size of p_cdb
  p_cdb	        CDB bytes. 
  e_direction	direction the transfer is to go.
  i_buf	        Size of buffer
  p_buf	        Buffer for data, both sending and receiving

  Return 0 if no error.
 */
int
run_scsi_cmd_freebsd_cam( 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 )
{
  const _img_private_t *p_env = p_user_data;
  int   i_status;
  int direction = CAM_DEV_QFRZDIS;
  union ccb ccb;

  if (!p_env || !p_env->cam) return -2;
    
  memset(&ccb, 0, sizeof(ccb));

  ccb.ccb_h.path_id    = p_env->cam->path_id;
  ccb.ccb_h.target_id  = p_env->cam->target_id;
  ccb.ccb_h.target_lun = p_env->cam->target_lun;
  ccb.ccb_h.timeout    = i_timeout_ms;

   if (!i_cdb)
      direction |= CAM_DIR_NONE;
   else
      direction |= (e_direction == SCSI_MMC_DATA_READ)?CAM_DIR_IN : CAM_DIR_OUT;
  cam_fill_csio (&(ccb.csio), 1, NULL, 
		 direction | CAM_DEV_QFRZDIS, MSG_SIMPLE_Q_TAG, p_buf, i_buf, 
		 sizeof(ccb.csio.sense_data), 0, 30*1000);

  memcpy(ccb.csio.cdb_io.cdb_bytes, p_cdb, i_cdb);
  
  ccb.csio.cdb_len = 
    scsi_mmc_get_cmd_len(ccb.csio.cdb_io.cdb_bytes[0]);
  
  if ((i_status = cam_send_ccb(p_env->cam, &ccb)) < 0)
    {
      cdio_warn ("transport failed: %d", i_status);
      return -1;
    }
  if ((ccb.ccb_h.status & CAM_STATUS_MASK) == CAM_REQ_CMP)
    {
      return 0;
    }
  errno = EIO;
  i_status = ERRCODE(((unsigned char *)&ccb.csio.sense_data));
  if (i_status == 0)
    i_status = -1;
  else
    CREAM_ON_ERRNO(((unsigned char *)&ccb.csio.sense_data));
  cdio_warn ("transport failed: %d", i_status);
  return i_status;
}

bool
init_freebsd_cam (_img_private_t *p_env)
{
  char pass[100];
  
  p_env->cam=NULL;
  memset (&p_env->ccb, 0, sizeof(p_env->ccb));
  p_env->ccb.ccb_h.func_code = XPT_GDEVLIST;

  if (-1 == p_env->gen.fd) 
    p_env->gen.fd = open (p_env->device, O_RDONLY, 0);

  if (p_env->gen.fd < 0)
    {
      cdio_warn ("open (%s): %s", p_env->device, strerror (errno));
      return false;
    }

  if (ioctl (p_env->gen.fd, CAMGETPASSTHRU, &p_env->ccb) < 0)
    {
      cdio_warn ("open: %s", strerror (errno));
      return false;
    }
  sprintf (pass,"/dev/%.15s%u",
	   p_env->ccb.cgdl.periph_name,
	   p_env->ccb.cgdl.unit_number);
  p_env->cam = cam_open_pass (pass,O_RDWR,NULL);
  if (!p_env->cam) return false;
  
  p_env->gen.init   = true;
  p_env->b_cam_init = true;
  return true;
}

void
free_freebsd_cam (void *user_data)
{
  _img_private_t *p_env = user_data;

  if (NULL == p_env) return;

  if (p_env->gen.fd > 0)
    close (p_env->gen.fd);
  p_env->gen.fd = -1;

  if(p_env->cam)
    cam_close_device(p_env->cam);

  free (p_env);
}

static int 
_set_bsize (_img_private_t *p_env, unsigned int bsize)
{
  scsi_mmc_cdb_t cdb = {{0, }};

  struct
  {
    uint8_t reserved1;
    uint8_t medium;
    uint8_t reserved2;
    uint8_t block_desc_length;
    uint8_t density;
    uint8_t number_of_blocks_hi;
    uint8_t number_of_blocks_med;
    uint8_t number_of_blocks_lo;
    uint8_t reserved3;
    uint8_t block_length_hi;
    uint8_t block_length_med;
    uint8_t block_length_lo;
  } mh;

  memset (&mh, 0, sizeof (mh));
  mh.block_desc_length = 0x08;
  mh.block_length_hi   = (bsize >> 16) & 0xff;
  mh.block_length_med  = (bsize >>  8) & 0xff;
  mh.block_length_lo   = (bsize >>  0) & 0xff;

  memset (&cdb, 0, sizeof (cdb));
  
  CDIO_MMC_SET_COMMAND(cdb.field, CDIO_MMC_GPCMD_MODE_SELECT_6);

  cdb.field[1] = 1 << 4;
  cdb.field[4] = 12;
  
  return run_scsi_cmd_freebsd_cam (p_env, DEFAULT_TIMEOUT_MSECS,
				   scsi_mmc_get_cmd_len(cdb.field[0]), 
				   &cdb, SCSI_MMC_DATA_WRITE, 
				   sizeof(mh), &mh);
}

int
read_mode2_sector_freebsd_cam (_img_private_t *p_env, void *data, lsn_t lsn, 
			       bool b_form2)
{
  if ( b_form2 )
    return read_mode2_sectors_freebsd_cam(p_env, data, lsn, 1);
  else {
    /* Need to pick out the data portion from a mode2 form2 frame */
    char buf[M2RAW_SECTOR_SIZE] = { 0, };
    int retval = read_mode2_sectors_freebsd_cam(p_env, buf, lsn, 1);
    if ( retval ) return retval;
    memcpy (((char *)data), buf + CDIO_CD_SUBHEADER_SIZE, CDIO_CD_FRAMESIZE);
    return 0;
  }
}

/*!
   Reads nblocks of mode2 sectors from cd device into data starting
   from lsn.
   Returns 0 if no error. 
 */
int
read_mode2_sectors_freebsd_cam (_img_private_t *p_env, void *p_buf, 
				lsn_t lsn, unsigned int nblocks)
{
  scsi_mmc_cdb_t cdb = {{0, }};

  bool b_read_10 = false;

  CDIO_MMC_SET_READ_LBA(cdb.field, lsn);
  
  if (b_read_10) {
    int retval;
    
    CDIO_MMC_SET_COMMAND(cdb.field, CDIO_MMC_GPCMD_READ_10);
    CDIO_MMC_SET_READ_LENGTH16(cdb.field, nblocks);
    if ((retval = _set_bsize (p_env, M2RAW_SECTOR_SIZE)))
      return retval;
    
    if ((retval = run_scsi_cmd_freebsd_cam (p_env, 0, 
						scsi_mmc_get_cmd_len(cdb.field[0]), 
						&cdb, 
						SCSI_MMC_DATA_READ,
						M2RAW_SECTOR_SIZE * nblocks, 
						p_buf)))
      {
	_set_bsize (p_env, CDIO_CD_FRAMESIZE);
	return retval;
      }
    
    if ((retval = _set_bsize (p_env, CDIO_CD_FRAMESIZE)))
      return retval;
  } else
    CDIO_MMC_SET_COMMAND(cdb.field, CDIO_MMC_GPCMD_READ_CD);
    CDIO_MMC_SET_READ_LENGTH24(cdb.field, nblocks);
    cdb.field[1] = 0; /* sector size mode2 */
    cdb.field[9] = 0x58; /* 2336 mode2 */
    return run_scsi_cmd_freebsd_cam (p_env, 0, 
					 scsi_mmc_get_cmd_len(cdb.field[0]), 
					 &cdb, 
					 SCSI_MMC_DATA_READ,
					 M2RAW_SECTOR_SIZE * nblocks, p_buf);

  return 0;
}

/*!
   Return the size of the CD in logical block address (LBA) units.
 */
uint32_t
stat_size_freebsd_cam (_img_private_t *p_env)
{
  scsi_mmc_cdb_t cdb = {{0, }};
  uint8_t buf[12] = { 0, };

  uint32_t retval;
  int i_status;

  /* Operation code */
  CDIO_MMC_SET_COMMAND(cdb.field, CDIO_MMC_GPCMD_READ_TOC);

  cdb.field[1] = 0; /* lba; msf: 0x2 */

  /* Format */
  cdb.field[2] = CDIO_MMC_READTOC_FMT_TOC;

  CDIO_MMC_SET_START_TRACK(cdb.field, CDIO_CDROM_LEADOUT_TRACK);

  CDIO_MMC_SET_READ_LENGTH16(cdb.field, sizeof(buf));
  
  p_env->ccb.csio.data_ptr = buf;
  p_env->ccb.csio.dxfer_len = sizeof (buf);

  i_status = run_scsi_cmd_freebsd_cam(p_env, DEFAULT_TIMEOUT_MSECS,
					  scsi_mmc_get_cmd_len(cdb.field[0]), 
					  &cdb, SCSI_MMC_DATA_READ, 
					  sizeof(buf), buf);
  if (0 != i_status)
    return 0;

  {
    int i;

    retval = 0;
    for (i = 8; i < 12; i++)
      {
        retval <<= 8;
        retval += buf[i];
      }
  }

  return retval;
}

/*!
 * Eject using SCSI MMC commands. Return 0 if successful.
 */
int
eject_media_freebsd_cam (_img_private_t *p_env) 
{
  int i_status;
  scsi_mmc_cdb_t cdb = {{0, }};
  uint8_t buf[1];
  
  CDIO_MMC_SET_COMMAND(cdb.field, CDIO_MMC_GPCMD_ALLOW_MEDIUM_REMOVAL);

  i_status = run_scsi_cmd_freebsd_cam (p_env, DEFAULT_TIMEOUT_MSECS,
					   scsi_mmc_get_cmd_len(cdb.field[0]), 
					   &cdb, SCSI_MMC_DATA_WRITE, 0, &buf);
  if (0 != i_status)
    return i_status;
  
  cdb.field[4] = 1;
  i_status = run_scsi_cmd_freebsd_cam (p_env, DEFAULT_TIMEOUT_MSECS,
				 scsi_mmc_get_cmd_len(cdb.field[0]), &cdb, 
				 SCSI_MMC_DATA_WRITE, 0, &buf);
  if (0 != i_status)
    return i_status;
  
  CDIO_MMC_SET_COMMAND(cdb.field, CDIO_MMC_GPCMD_START_STOP);
  cdb.field[4] = 2; /* eject */

  return run_scsi_cmd_freebsd_cam (p_env, DEFAULT_TIMEOUT_MSECS,
				       scsi_mmc_get_cmd_len(cdb.field[0]), 
				       &cdb, 
				       SCSI_MMC_DATA_WRITE, 0, &buf);
}

#endif /* HAVE_FREEBSD_CDROM */
