/*  cdrdao - write audio CD-Rs in disc-at-once mode
 *
 *  Copyright (C) 1998-2002  Andreas Mueller <andreas@daneb.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <config.h>

#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <errno.h>

#ifdef linux
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/../scsi/scsi.h>  /* cope with silly includes */
#include <linux/../scsi/sg.h>    /* cope with silly includes */
#endif

#include "ScsiIf.h"
#include "util.h"

#include "xconfig.h"
#include "standard.h"
#include "scg/scgcmd.h"
#include "scg/scsitransp.h"

static void printVersionInfo(SCSI *scgp);

#define MAX_DATALEN_LIMIT (63 * 1024)
#define MAX_SCAN_DATA_LEN 30

static int VERSION_INFO_PRINTED = 0;

class ScsiIfImpl {
public:
  char *dev_;
  
  int maxSendLen_;
  SCSI *scgp_;

  long timeout_;

  unsigned int pageSize_;
  char *pageAlignedBuffer_;

#ifdef linux
  const char *makeDevName(int k, int do_numeric);
  const char *openScsiDevAsSg(const char *devname);
#endif
};

ScsiIf::ScsiIf(const char *dev)
{
  impl_ = new ScsiIfImpl;

  impl_->dev_ = strdupCC(dev);
  impl_->scgp_ = NULL;
  impl_->pageAlignedBuffer_ = NULL;

  vendor_[0] = 0;
  product_[0] = 0;
  revision_[0] = 0;

  //set_progname("cdrdao");


#if defined(HAVE_GETPAGESIZE)
  impl_->pageSize_ = getpagesize();
#elif defined(_SC_PAGESIZE)
  impl_->pageSize_ = sysconf(_SC_PAGESIZE);
#elif defined(_SC_PAGE_SIZE) // saw this on HPUX 9.05
  impl_->pageSize_ = sysconf(_SC_PAGE_SIZE);
#else
  impl_->pageSize_ = 0;
#endif

  if (impl_->pageSize_ == 0) {
    message(-3, "Cannot determine page size.");
    impl_->pageSize_ = 1;
  }

}

ScsiIf::~ScsiIf()
{
  if (impl_->scgp_ != NULL) {
    scg_close(impl_->scgp_);
    impl_->scgp_ = NULL;
  }

  impl_->pageAlignedBuffer_ = NULL;
  
  delete[] impl_->dev_;
  impl_->dev_ = NULL;

  delete impl_;
}

// Opens the scsi device. Most of the code originates from cdrecord's 
// initialization function.
// return: 0: OK
//         1: device could not be opened
//         2: inquiry failed
int ScsiIf::init()
{
  char errstr[80];

  scg_remote();

#ifdef linux
  if (impl_->dev_[0] == '/' && strchr(impl_->dev_, ':') == NULL) {
    // we have a device name, check if it is a generic SCSI device or
    // try to map it a generic SCSI device
    const char *fname = impl_->openScsiDevAsSg(impl_->dev_);

    if (fname != NULL) {
      char *old = impl_->dev_;
      impl_->dev_ = strdupCC(fname);
      delete[] old;
    }
  }
#endif

  if ((impl_->scgp_ = scg_open(impl_->dev_, errstr, sizeof(errstr), 0, 0))
	 == NULL) {
    message(-2, "Cannot open SCSI device '%s': %s", impl_->dev_, errstr);
    return 1;
  }

  impl_->timeout_ = impl_->scgp_->deftimeout;

  maxDataLen_ = scg_bufsize(impl_->scgp_, MAX_DATALEN_LIMIT);

  message(5, "SCSI: max DMA: %ld", maxDataLen_);

  if (maxDataLen_ > MAX_DATALEN_LIMIT)
    maxDataLen_ = MAX_DATALEN_LIMIT;

  impl_->pageAlignedBuffer_ = (char *)scg_getbuf(impl_->scgp_, maxDataLen_);

  if (impl_->pageAlignedBuffer_ == NULL) {
    message(-2, "Cannot get SCSI buffer.");
    return 1;
  }

  printVersionInfo(impl_->scgp_);

  if (inquiry() != 0)
    return 2;

  return 0;

}

// Sets given timeout value in seconds and returns old timeout.
// return: old timeout
int ScsiIf::timeout(int t)
{
  long oldTimeout = impl_->timeout_;

  if (t > 0) 
    impl_->timeout_ = t;

  return oldTimeout;
}

// sends a scsi command and receives data
// return 0: OK
//        1: scsi command failed (os level, no sense data available)
//        2: scsi command failed (sense data available)
int ScsiIf::sendCmd(const unsigned char *cmd, int cmdLen, 
		    const unsigned char *dataOut, int dataOutLen,
		    unsigned char *dataIn, int dataInLen,
		    int showMessage)
{
  assert(cmdLen > 0 && cmdLen <= 12);

  assert(dataOutLen == 0 || dataInLen == 0);

  assert(dataOutLen <= maxDataLen_);
  assert(dataInLen <= maxDataLen_);

  struct scg_cmd *scmd = impl_->scgp_->scmd;
  int usedPageAlignedBuffer = 0;

  memset(scmd, 0, sizeof(*scmd));

  memcpy(scmd->cdb.cmd_cdb, cmd, cmdLen);
  scmd->cdb_len = cmdLen;

  if (dataOutLen > 0) {
    if (((unsigned)dataOut % impl_->pageSize_) != 0) {
      //message(0, "Use SCSI buffer for data out.");
      memcpy(impl_->pageAlignedBuffer_, dataOut, dataOutLen);
      scmd->addr = impl_->pageAlignedBuffer_;
    }
    else {
      //message(0, "Data out is page aligned.");
      scmd->addr = (char*)dataOut;
    }

    scmd->size = dataOutLen;
  }
  else if (dataInLen > 0) {
    if (((unsigned)dataIn % impl_->pageSize_) != 0) {
      //message(0, "Use SCSI buffer for data in.");
      scmd->addr = impl_->pageAlignedBuffer_;
      usedPageAlignedBuffer = 1;
    }
    else {
      //message(0, "Data in is page aligned.");
      scmd->addr = (char*)dataIn;
    }

    scmd->size = dataInLen;
    scmd->flags = SCG_RECV_DATA;
  }

  scmd->flags |= SCG_DISRE_ENA;
  scmd->timeout = impl_->timeout_;

  scmd->sense_len = CCS_SENSE_LEN;
  //scmd->target = impl_->scgp_->target;
	
  impl_->scgp_->cmdname = " ";
  impl_->scgp_->verbose = 0;
  impl_->scgp_->silent = showMessage ? 0 : 1;
  
  if (scg_cmd(impl_->scgp_) < 0) {
    return scmd->sense_count > 0 ?  2 : 1;
  }

  if (usedPageAlignedBuffer)
    memcpy(dataIn, impl_->pageAlignedBuffer_, dataInLen);

  return 0;
}

const unsigned char *ScsiIf::getSense(int &len) const
{
  len = impl_->scgp_->scmd->sense_count;

  return impl_->scgp_->scmd->u_sense.cmd_sense;
}

void ScsiIf::printError()
{
  scg_printerr(impl_->scgp_);
}

int ScsiIf::inquiry()
{
  unsigned char cmd[6];
  unsigned char result[0x2c];
  int i;

  cmd[0] = 0x12; // INQUIRY
  cmd[1] = cmd[2] = cmd[3] = 0;
  cmd[4] = 0x2c;
  cmd[5] = 0;


  if (sendCmd(cmd, 6, NULL, 0, result, 0x2c, 1) != 0) {
    message (-2, "Inquiry command failed on '%s'.", impl_->dev_);
    return 1;
  }

  strncpy(vendor_, (char *)(result + 0x08), 8);
  vendor_[8] = 0;

  strncpy(product_, (char *)(result + 0x10), 16);
  product_[16] = 0;

  strncpy(revision_, (char *)(result + 0x20), 4);
  revision_[4] = 0;

  for (i = 7; i >= 0 && vendor_[i] == ' '; i--) {
    vendor_[i] = 0;
  }

  for (i = 15; i >= 0 && product_[i] == ' '; i--) {
    product_[i] = 0;
  }

  for (i = 3; i >= 0 && revision_[i] == ' '; i--) {
    revision_[i] = 0;
  }

  return 0;
}

static int scanInquiry(SCSI *scgp, unsigned char *buf, ScsiIf::ScanData *sdata)
{
  struct scg_cmd *cmd = scgp->scmd;
  int i;

  memset(buf, 0, 36);
  memset(cmd, 0, sizeof(struct scg_cmd));

  cmd->cdb.g0_cdb.cmd = 0x12; // INQUIRY
  cmd->cdb.g0_cdb.count = 36;

  cmd->addr = (char*)buf;
  cmd->size = 36;
  cmd->flags = SCG_RECV_DATA|SCG_DISRE_ENA;
  cmd->cdb_len = SC_G0_CDBLEN;
  cmd->sense_len = CCS_SENSE_LEN;
  scgp->silent = 1;
  scgp->cmdname = "inquiry";

  if (scg_cmd(scgp) == 0) {
    if ((buf[0] & 0x1f) == 0x04 || (buf[0] & 0x1f) == 0x05) {
      sdata->bus = scg_scsibus(scgp);
      sdata->id = scg_target(scgp);
      sdata->lun = scg_lun(scgp);

      strncpy(sdata->vendor, (char *)(buf + 0x08), 8);
      sdata->vendor[8] = 0;

      strncpy(sdata->product, (char *)(buf + 0x10), 16);
      sdata->product[16] = 0;

      strncpy(sdata->revision, (char *)(buf + 0x20), 4);
      sdata->revision[4] = 0;

      for (i = 7; i >= 0 && sdata->vendor[i] == ' '; i--) {
	sdata->vendor[i] = 0;
      }

      for (i = 15; i >= 0 && sdata->product[i] == ' '; i--) {
	sdata->product[i] = 0;
      }

      for (i = 3; i >= 0 && sdata->revision[i] == ' '; i--) {
	sdata->revision[i] = 0;
      }

      return 1;
    }
  }

  return 0;
}

ScsiIf::ScanData *ScsiIf::scan(int *len)
{
  ScanData *sdata = NULL;
  SCSI *scgp;
  unsigned char *buf;
  char errstr[80];

  *len = 0;

  //set_progname("cdrdao");
  scg_remote();

  if ((scgp = scg_open(NULL, errstr, sizeof(errstr), 0, 0)) == NULL)
    return NULL;

  printVersionInfo(scgp);

  // allocate buffer for inquiry data
  if ((buf = (unsigned char *)scg_getbuf(scgp, 100)) == NULL) {
    scg_close(scgp);
    return NULL;
  }

  sdata = new ScanData[MAX_SCAN_DATA_LEN];
  
  for (int bus = 0; bus < 16 && *len < MAX_SCAN_DATA_LEN; bus++) {

    if (scg_havebus(scgp, bus)) {
      int lun = 0;

      for (int target=0; target < 16 && *len < MAX_SCAN_DATA_LEN; target++) {
	scg_settarget(scgp, bus, target, lun);
	if (scanInquiry(scgp, buf, &(sdata[*len])))
	  *len += 1;
      }
    }
  }
  
  
  scg_close(scgp);

  return sdata;
}

static void printVersionInfo(SCSI *scgp)
{
  if (!VERSION_INFO_PRINTED) {
    VERSION_INFO_PRINTED = 1;

    const char *vers = scg_version(0, SCG_VERSION);
    const char *auth = scg_version(0, SCG_AUTHOR);
    message(2, "Using libscg version '%s-%s'", auth, vers);
  
    vers = scg_version(scgp, SCG_VERSION);
    auth = scg_version(scgp, SCG_AUTHOR);
  
    message(3, "Using libscg transport code version '%s-%s'", auth, vers);

    vers = scg_version(scgp, SCG_RVERSION);
    auth = scg_version(scgp, SCG_RAUTHOR);

    if (vers != NULL && auth != NULL) {
      message(3, "Using remote transport code version '%s-%s'", auth, vers);
    }

    message(2, "");
  }
}

#include "ScsiIf-common.cc"

#ifdef linux

/* Function for mapping any SCSI device to the corresponding SG device.
 * Taken from D. Gilbert's example code.
 */

#define MAX_SG_DEVS 26

#define SCAN_ALPHA 0
#define SCAN_NUMERIC 1
#define DEF_SCAN SCAN_ALPHA

const char *ScsiIfImpl::makeDevName(int k, int do_numeric)
{
  static char filename[100];
  char buf[20];

  strcpy(filename, "/dev/sg");

  if (do_numeric) {
    sprintf(buf, "%d", k);
    strcat(filename, buf);
  }
  else {
    if (k <= 26) {
      buf[0] = 'a' + (char)k;
      buf[1] = '\0';
      strcat(filename, buf);
    }
    else {
      strcat(filename, "xxxx");
    }
  }

  return filename;
}

const char *ScsiIfImpl::openScsiDevAsSg(const char *devname)
{
  int fd, bus, bbus, k;
  int do_numeric = DEF_SCAN;
  const char *fname = devname;
  struct {
    int mux4;
    int hostUniqueId;
  } m_idlun, mm_idlun;

  if ((fd = open(fname, O_RDONLY | O_NONBLOCK)) < 0) {
    if (EACCES == errno)
      fd = open(fname, O_RDWR | O_NONBLOCK);
  }

  if (fd < 0) {
    message(-2, "Cannot open \"%s\": %s", fname, strerror(errno));
    return NULL;
  }

  if (ioctl(fd, SG_GET_TIMEOUT, 0) < 0) { /* not a sg device ? */

#ifdef SCSI_IOCTL_GET_BUS_NUMBER
    if (ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, &bus) < 0) {
      message(-2, "%s: Need a filename that resolves to a SCSI device.",
	      fname);
      close(fd);
      return NULL;
    }
#else
    bus = 0;
#endif


    if (ioctl(fd, SCSI_IOCTL_GET_IDLUN, &m_idlun) < 0) {
      message(-2, "%s: Need a filename that resolves to a SCSI device (2).",
	      fname);
      close(fd);
      return NULL;
    }
    close(fd);
    
    for (k = 0; k < MAX_SG_DEVS; k++) {
      fname = makeDevName(k, do_numeric);
      if ((fd = open(fname, O_RDONLY | O_NONBLOCK)) < 0) {
	if (EACCES == errno) 
	  fd = open(fname, O_RDWR | O_NONBLOCK);
	if (fd < 0) {
	  if ((ENOENT == errno) && (0 == k) && (do_numeric == DEF_SCAN)) {
	    do_numeric = ! DEF_SCAN;
	    fname = makeDevName(k, do_numeric);
	    if ((fd = open(fname, O_RDONLY | O_NONBLOCK)) < 0) {
	      if (EACCES == errno) 
		fd = open(fname, O_RDWR | O_NONBLOCK);
	    }
	  }
	  if (fd < 0) {
	    if (EBUSY == errno)
	      continue;  /* step over if O_EXCL already on it */
	    else
	      break;
	  }
	}
      }

#ifdef SCSI_IOCTL_GET_BUS_NUMBER
      if (ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, &bbus) < 0) {
	message(-2, "%s: SG: ioctl SCSI_IOCTL_GET_BUS_NUMBER failed: %s",
		fname, strerror(errno));
	close(fd);
	fd = -9999;
      }
#else
      bbus = 0;
#endif
      
      if (ioctl(fd, SCSI_IOCTL_GET_IDLUN, &mm_idlun) < 0) {
	message(-2, "%s: SG: ioctl SCSI_IOCTL_GET_IDLUN failed: %s", 
		fname, strerror(errno));
	close(fd);
	fd = -9999;
      }
      if ((bus == bbus) && 
	  ((m_idlun.mux4 & 0xff) == (mm_idlun.mux4 & 0xff)) &&
	  (((m_idlun.mux4 >> 8) & 0xff) == 
	   ((mm_idlun.mux4 >> 8) & 0xff)) &&
	  (((m_idlun.mux4 >> 16) & 0xff) == 
	   ((mm_idlun.mux4 >> 16) & 0xff))) {
	message(4, "Mapping %s to sg device: %s", devname, fname);
	break;
      }
      else {
	close(fd);
	fd = -9999;
      }
    }
  }

  if (fd >= 0) {
    close(fd);
    return fname;
  }
  else {
    message(-2, "Cannot map \"%s\" to a SG device.", devname);
    return NULL;
  }
}

#endif /* linux */
