/*  cdrdao - write audio CD-Rs in disc-at-once mode
 *
 *  Copyright (C) 1998-2001 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 <string.h>
#include <assert.h>

#include "GenericMMCraw.h"
#include "PQSubChannel16.h"
#include "PWSubChannel96.h"
#include "CdTextEncoder.h"

#include "Toc.h"
#include "util.h"
#include "port.h"

GenericMMCraw::GenericMMCraw(ScsiIf *scsiIf, unsigned long options) 
  : GenericMMC(scsiIf, options), PQChannelEncoder()
{
  driverName_ = "Generic SCSI-3/MMC (raw writing) - Version 2.0";

  encodingMode_ = 0;

  subChannelMode_ = 0;

  leadInLen_ = leadOutLen_ = 0;
  subChannel_ = NULL;
  encSubChannel_ = NULL;
  encodeBuffer_ = NULL;

  // CD-TEXT dynamic data
  cdTextStartLba_ = 0;
  cdTextEndLba_ = 0;
  cdTextSubChannels_ = NULL;
  cdTextSubChannelCount_ = 0;
  cdTextSubChannelAct_ = 0;
}

GenericMMCraw::~GenericMMCraw()
{
  delete subChannel_, subChannel_ = NULL;
  delete[] encodeBuffer_, encodeBuffer_ = NULL;

  delete[] encSubChannel_;
  encSubChannel_ = NULL;

  cdTextStartLba_ = 0;
  cdTextEndLba_ = 0;
  cdTextSubChannels_ = NULL;
  cdTextSubChannelCount_ = 0;
  cdTextSubChannelAct_ = 0;
}

int GenericMMCraw::multiSession(int m)
{
  if (m != 0) {
    return 1; // multi session mode is currently not support for raw writing
  }

  return 0;
}

// static constructor
CdrDriver *GenericMMCraw::instance(ScsiIf *scsiIf, unsigned long options)
{
  return new GenericMMCraw(scsiIf, options);
}

int GenericMMCraw::subChannelEncodingMode(TrackData::SubChannelMode sm) const
{
  int ret = 0;

  
  if (subChannelMode_ == 0) {
    // The supported sub-channel writing mode has not been determined, yet,
    // so just return the plain mode here. 'initDao' will finally check if
    // writing of the sub-channel data defined in 'toc_' is supported by the
    // drive.
    return 0;
  }

  switch (sm) {
  case TrackData::SUBCHAN_NONE:
    ret = 0;
    break;

  case TrackData::SUBCHAN_RW:
    switch (subChannelMode_) {
    case 2:
      ret = 0; // plain
      break;
    case 3:
      ret = -1; // currently not supported
      //ret = 1; // have to create parity and perform interleaving
      break;
    default:
      ret = -1; // not supported
      break;
    }
    break;

  case TrackData::SUBCHAN_RW_RAW:
    if (subChannelMode_ == 3)
      ret = 1;
    else 
      ret = -1;
    break;
  }

  return ret;
}

// Sets write parameters via mode page 0x05.
// return: 0: OK
//         1: scsi command failed
int GenericMMCraw::setWriteParameters(int dataBlockType)
{
  unsigned char mp[0x38];

  if (getModePage(5/*write parameters mode page*/, mp, 0x38,
		  NULL, NULL, 1) != 0) {
    message(-2, "Cannot retrieve write parameters mode page.");
    return 1;
  }

  mp[0] &= 0x7f; // clear PS flag

  mp[2] &= 0xe0;
  mp[2] |= 0x03; // write type: raw
  if (simulate_) {
    mp[2] |= 1 << 4; // test write
  }

  DriveInfo *di;
  if ((di = driveInfo(1)) != NULL && di->burnProof) {
    // This drive has BURN-Proof function.
    // Enable it unless explicitly disabled.
    if (options_ & OPT_MMC_NO_BURNPROOF) {
      message(2, "Turning BURN-Proof off");
      mp[2] &= ~0x40;
    } else {
      message(2, "Turning BURN-Proof on");
      mp[2] |= 0x40;
    }
  }

  mp[3] &= 0x3f; // Multi-session: No B0 pointer, next session not allowed
  mp[3] = 0;

  mp[4] &= 0xf0;
  mp[4] = dataBlockType & 0x0f;  // Data Block Type:
                                 // 1: raw data, block size: 2368 PQ sub chan
                                 // 2: raw data, block size: 2448
                                 // 3: raw data, block size: 2448

  mp[8] = 0; // session format: CD-DA or CD-ROM

  if (setModePage(mp, NULL, NULL, 0) != 0) {
    //message(-2, "Cannot set write parameters mode page.");
    return 1;
  }

  return 0;
}

int GenericMMCraw::getMultiSessionInfo(int sessionNr, int multi,
				       SessionInfo *info)
{
  int err = 0;

  memset(info, 0, sizeof(SessionInfo));

  info->sessionNr = 1;

  if (getSessionInfo() != 0) 
    return 1;

  info->leadInStart = leadInStart_.lba() - 150;

  if (leadInStart_.min() >= 80) {
    info->leadInStart = leadInStart_.lba() - 450000;
  }

  info->leadInLen = leadInLen_;
  info->leadOutLen = leadOutLen_;

  if (multi) {
    unsigned char cmd[10];
    unsigned char data[4];
    unsigned char *buf = NULL;
    long dataLen;
 
    // read ATIP data
    memset(cmd, 0, 10);
    memset(data, 0, 4);

    cmd[0] = 0x43; // READ TOC/PMA/ATIP
    cmd[1] = 0x00;
    cmd[2] = 4; // get ATIP
    cmd[7] = 0;
    cmd[8] = 4; // data length
    
    if (sendCmd(cmd, 10, NULL, 0, data, 4, 0) != 0) {
      message(-2, "Cannot read ATIP data.");
      return 1;
    }
    
    dataLen = (data[0] << 8) | data[1];
    dataLen += 2;

    message(4, "ATIP data len: %ld", dataLen);

    if (sessionNr == 1) {
      if (dataLen < 19) {
	message(-2, "Cannot read ATIP data.");
	return 1;
      }
    }
    else {
      if (dataLen < 15) {
	message(-2, "Cannot read ATIP data.");
	return 1;
      }
    }      
    
    buf = new unsigned char[dataLen];
    memset(buf, 0, dataLen);
    
    cmd[7] = dataLen >> 8;
    cmd[8] = dataLen;
    
    if (sendCmd(cmd, 10, NULL, 0, buf, dataLen, 0) != 0) {
      message(-2, "Cannot read ATIP data.");
      delete[] buf;
      return 1;
    }

    info->lastLeadoutStart = Msf(buf[12], buf[13], buf[14]);
    
    if (sessionNr == 1) {
      info->optimumRecordingPower = buf[4];

      if (buf[8] >= 80 && buf[8] <= 99) {
	info->atipLeadinStart = Msf(buf[8], buf[9], buf[10]);
      }
      else {
	message(-2, "Invalid start time of lead-in in ATIP.");
	err = 1;
      }

      info->cdrw = (buf[6] & 0x40) ? 1 : 0;
      
      if (info->cdrw) {
	if (buf[6] & 0x04) {
	  info->atipA1[0] = buf[16];
	  info->atipA1[1] = buf[17];
	  info->atipA1[2] = buf[18];
	}
	else {
	  message(-2, "ATIP data does not contain point A1 data.");
	  err = 1;
	}
      }
    }

    delete[] buf;
    buf = NULL;
  }

  message(4, "SI: session nr: %d", info->sessionNr);
  message(4, "SI: lead-in start: %ld", info->leadInStart);
  message(4, "SI: lead-in len: %ld", info->leadInLen);
  message(4, "SI: lead-out len: %ld", info->leadOutLen);
  message(4, "SI: last lead-out start: %d %d %d", info->lastLeadoutStart.min(),
	  info->lastLeadoutStart.sec(), info->lastLeadoutStart.frac());
  message(4, "SI: cdrw: %d", info->cdrw);
  message(4, "SI: atip lead-in start: %d %d %d", info->atipLeadinStart.min(),
	  info->atipLeadinStart.sec(), info->atipLeadinStart.frac());
  message(4, "SI: optimum recording power: %u", info->optimumRecordingPower);
  message(4, "SI: atip A1: %u %u %u", info->atipA1[0], info->atipA1[1],
	  info->atipA1[2]);

  return err;
}

int GenericMMCraw::getSubChannelModeFromToc()
{
  TrackIterator itr(CdrDriver::toc_);
  const Track *tr;
  int mode = 0;
  
  for (tr = itr.first(); tr != NULL; tr = itr.next()) {
    switch (tr->subChannelType()) {
    case TrackData::SUBCHAN_NONE:
      break;

    case TrackData::SUBCHAN_RW:
      // we need at least packed RW writing
      if (mode < 2)
	mode = 2;
      break;

    case TrackData::SUBCHAN_RW_RAW:
      // we need raw PW writing
      mode = 3;
      break;
    }
  }

  message(5, "Sub-channel mode requested by toc: %d", mode);

  return mode;
}

int GenericMMCraw::setSubChannelMode()
{
  delete subChannel_;
  subChannel_ = NULL;

  subChannelMode_ = 0;
  
#if 1
  if (cdTextEncoder_ != NULL) {
    if (setWriteParameters(3) == 0) {
      subChannel_ = new PWSubChannel96;
      subChannelMode_ = 3;
    }
    else {
      delete cdTextEncoder_;
      cdTextEncoder_ = NULL;

      message(force() ? -1 : -2,
	      "Cannot write CD-TEXT data because the 96 byte raw P-W sub-channel data mode is not supported.");

      if (force()) {
	message(-1, "Ignored because of --force option.");
      }
      else {
	message(-2, "Use option --force to ignore this error.");
	return 1;
      }
    }
  }

  // check if the toc requires a certain sub-channel mode and try to set it
  if (subChannel_ == NULL) {
    int tocMode = getSubChannelModeFromToc();

    if (tocMode > 0) {
      for (; tocMode <= 3 && subChannel_ == NULL; tocMode++) {
	if (setWriteParameters(tocMode) == 0) {
	  if (tocMode == 1)
	    subChannel_ = new PQSubChannel16;
	  else
	    subChannel_ = new PWSubChannel96;

	  subChannelMode_ = tocMode;
	}
      }

      if (subChannel_ == NULL) {
	message(-2, "Cannot setup sub-channel writing mode for sub-channel data defined in the toc-file.");
	return 1;
      }
    }
  }

  // select any available sub-channel mode
  if (subChannel_ == NULL) {
    if (setWriteParameters(1) == 0) {
      subChannel_ = new PQSubChannel16;
      subChannelMode_ = 1;
    }
    else if (setWriteParameters(3) == 0) {
      subChannel_ = new PWSubChannel96;
      subChannelMode_ = 3;
    }
    else if (setWriteParameters(2) == 0) {
      subChannel_ = new PWSubChannel96;
      subChannelMode_ = 2;
    }
    else {
      message(-2, "Cannot setup disk-at-once writing for this drive.");
      return 1;
    }
  }

#else

  //subChannel_ = new PWSubChannel96;
  subChannel_ = new PQSubChannel16;
  subChannelMode_ = 1;
#endif

  switch (subChannelMode_) {
  case 1:
    message(2, "Using 16 byte P-Q sub-channel data mode.");
    break;
  case 2:
    message(2, "Using 96 byte packed P-W sub-channel data mode.");
    break;
  case 3:
    if (cdTextEncoder_ != NULL)
      message(2, "Using 96 byte raw P-W sub-channel data mode for CD-TEXT.");
    else
      message(2, "Using 96 byte raw P-W sub-channel data mode.");
    break;
  }

  return 0;
}

int GenericMMCraw::initDao(const Toc *toc)
{
  long n;

  CdrDriver::toc_ = toc;

  if (selectSpeed(0) != 0 ||
      getSessionInfo() != 0) {
    return 1;
  }

  delete cdTextEncoder_;
  cdTextEncoder_ = new CdTextEncoder(toc);
  if (cdTextEncoder_->encode() != 0) {
    message(-2, "CD-TEXT encoding failed.");
    return 1;
  }

  if (cdTextEncoder_->getSubChannels(&n) == NULL || n == 0) {
    delete cdTextEncoder_;
    cdTextEncoder_ = NULL;
  }

  if (setSubChannelMode() != 0)
    return 1; 

  blockLength_ = AUDIO_BLOCK_LEN + subChannel_->dataLength();
  blocksPerWrite_ = scsiIf_->maxDataLen() / blockLength_;
  assert(blocksPerWrite_ > 0);
  message(4, "Block length: %ld", blockLength_);

  long cueSheetLen;
  unsigned char *cueSheet = createCueSheet(0, &cueSheetLen);

  if (cueSheet == NULL) {
    return 1;
  }
  
  if (setCueSheet(subChannel_, sessionFormat(), cueSheet, cueSheetLen,
		  leadInStart_) != 0) {
    return 1;
  }

  if (cdTextEncoder_ != NULL) {
    cdTextStartLba_ = leadInStart_.lba() - 450150;
    cdTextEndLba_ = cdTextStartLba_ + leadInLen_;
    cdTextSubChannels_ = cdTextEncoder_->getSubChannels(&cdTextSubChannelCount_);
    cdTextSubChannelAct_ = 0;
  }
  else {
    cdTextStartLba_ = 0;
    cdTextEndLba_ = 0;
    cdTextSubChannels_ = NULL;
    cdTextSubChannelCount_ = 0;
    cdTextSubChannelAct_ = 0;
  }

  // allocate buffer for write zeros
  n = blocksPerWrite_ * (AUDIO_BLOCK_LEN + subChannel_->dataLength());
  delete[] zeroBuffer_;
  zeroBuffer_ = new char[n];
  memset(zeroBuffer_, 0, n);

  // allocate buffer for sub-channel encoding
  n = blocksPerWrite_ * blockLength_;
  delete[] encodeBuffer_;
  encodeBuffer_ = new (unsigned char)[n];

  delete[] encSubChannel_;
  encSubChannel_ = new (unsigned char)[blocksPerWrite_ * subChannel_->dataLength()];

  /*
  SessionInfo sessInfo;
  
  getMultiSessionInfo(1, 1, &sessInfo);

  return 1;
  */

  if (!simulate_) {
    if (performPowerCalibration() != 0) {
      if (!force()) {
	message(-2, "Use option --force to ignore this error.");
	return 1;
      }
      else {
	message(-2, "Ignored because of option --force.");
      }
    }
  }

  return 0;
}

int GenericMMCraw::startDao()
{
  message(2, "Writing lead-in and gap...");

  long lba = leadInStart_.lba() - 450150;
  
  if (writeZeros(CdrDriver::toc_->leadInMode(), TrackData::SUBCHAN_NONE,
		 lba, 0, leadInLen_) != 0) {
    return 1;
  }

  TrackData::SubChannelMode subChanMode = TrackData::SUBCHAN_NONE;
  TrackIterator itr(CdrDriver::toc_);
  const Track *tr;

  if ((tr = itr.first()) != NULL) {
    subChanMode = tr->subChannelType();
  }

  if (writeZeros(CdrDriver::toc_->leadInMode(), subChanMode, lba, 0, 150)
      != 0) {
    return 1;
  }

  return 0;
}

int GenericMMCraw::finishDao()
{
  int ret;

  message(2, "Writing lead-out...");

  long lba = CdrDriver::toc_->length().lba();

  writeZeros(CdrDriver::toc_->leadOutMode(), TrackData::SUBCHAN_NONE,
	     lba, lba + 150, leadOutLen_);

  message(2, "\nFlushing cache...");
  
  if (flushCache() != 0) {
    return 1;
  }

  while ((ret = checkDriveReady()) == 2)
    mSleep(2000);

  if (ret != 0)
    message(-1, "TEST UNIT READY failed after recording.");

  delete cdTextEncoder_, cdTextEncoder_ = NULL;
  delete[] zeroBuffer_, zeroBuffer_ = NULL;
  delete[] encodeBuffer_, encodeBuffer_ = NULL;

  return 0;
}

long GenericMMCraw::nextWritableAddress()
{
  unsigned char cmd[10];
  unsigned char data[28];
  long lba = 0xffffffff;

  memset(cmd, 0, 10);
  memset(data, 0, 28);

  cmd[0] = 0x52; // READ TRACK INFORMATION
  cmd[1] = 0;
  cmd[2] = lba >> 24;
  cmd[3] = lba >> 16;
  cmd[4] = lba >> 8;
  cmd[5] = lba;
  cmd[8] = 28;

  if (sendCmd(cmd, 10, NULL, 0, data, 28) != 0) {
    message(-2, "Cannt get track information.");
    return 0;
  }

  long adr = (data[12] << 24) | (data[13] << 16) | (data[14] << 8) |
    data[15];

  return adr;

}

// Writes data to target. The encoded sub-channel data is appended to each
// block.
// return: 0: OK
//         1: scsi command failed
int GenericMMCraw::writeData(TrackData::Mode mode,
			     TrackData::SubChannelMode sm,
			     long &lba, const char *buf, long len)
{
  assert(blockLength_ > 0);
  assert(blocksPerWrite_ > 0);
  assert(mode == TrackData::AUDIO);
  int writeLen = 0;
  unsigned char cmd[10];
  int i, j;

  long iblen = blockSize(mode, sm);
  long slen = subChannel_->dataLength();

  /*
  message(0, "lba: %ld, len: %ld, bpc: %d, bl: %d ", lba, len, blocksPerCmd,
	 blockLength_);
   */

  memset(cmd, 0, 10);
  cmd[0] = 0x2a; // WRITE1
  
  while (len > 0) {
    writeLen = (len > blocksPerWrite_ ? blocksPerWrite_ : len);

    cmd[2] = lba >> 24;
    cmd[3] = lba >> 16;
    cmd[4] = lba >> 8;
    cmd[5] = lba;

    cmd[7] = writeLen >> 8;
    cmd[8] = writeLen;

    // encode the PQ sub-channel data
    encode(lba, encSubChannel_, writeLen);

    for (i = 0; i < writeLen; i++) {
      memcpy(encodeBuffer_ + i * blockLength_, buf + i * iblen,
	     AUDIO_BLOCK_LEN);

      memcpy(encodeBuffer_ + i * blockLength_ + AUDIO_BLOCK_LEN,
	     encSubChannel_ + i * slen, slen);

      if (cdTextSubChannels_ != NULL && lba >= cdTextStartLba_ &&
	  lba + i < cdTextEndLba_) {

	const unsigned char *data = cdTextSubChannels_[cdTextSubChannelAct_]->data();
	long dataLen = cdTextSubChannels_[cdTextSubChannelAct_]->dataLength();

	unsigned char *actBuf = encodeBuffer_ + i * blockLength_ + AUDIO_BLOCK_LEN;

	//message(0, "Adding CD-TEXT channel %ld for LBA %ld", cdTextSubChannelAct_, lba + i);
	for (j = 0; j < dataLen; j++) {
	  *actBuf |= (*data & 0x3f);
	  actBuf++;
	  data++;
	}

	cdTextSubChannelAct_++;
	if (cdTextSubChannelAct_ >= cdTextSubChannelCount_)
	  cdTextSubChannelAct_ = 0;
      }
      else {
	switch (sm) {
	case TrackData::SUBCHAN_NONE:
	  break;

	case TrackData::SUBCHAN_RW:
	case TrackData::SUBCHAN_RW_RAW:
	  {
	    unsigned char *oBuf = encodeBuffer_ + i * blockLength_ + AUDIO_BLOCK_LEN;
	    const char *iBuf = buf + i * iblen + AUDIO_BLOCK_LEN;

	    for (j = 0; j < PW_SUBCHANNEL_LEN; j++) {
	      *oBuf |= (*iBuf & 0x3f);
	      oBuf++;
	      iBuf++;
	    }
	  }
	  break;
	}
      }
    }


#if 0
    // consistency checks
    long sum1, sum2;
    int n;
    const char *p;
    for (i = 0; i < writeLen; i++) {
      message(0, "%ld: ", lba + i);
      SubChannel *chan = subChannel_->makeSubChannel(encodeBuffer_ + i * blockLength_ + AUDIO_BLOCK_LEN);
      chan->print();
      delete chan;

      sum1 = 0;
      for (p = buf + i * iblen, n = 0;
	   n < AUDIO_BLOCK_LEN; n++, p++) {
	sum1 += *p;
      }

      sum2 = 0;
      for (n = 0; n < AUDIO_BLOCK_LEN; n++) {
	sum2 += *(char *)(encodeBuffer_ + i * blockLength_ + n);
      }

      //message(0, "%ld - %ld", sum1, sum2);
      assert(sum1 == sum2);
    }
#endif

#if 1
    if (sendCmd(cmd, 10, encodeBuffer_, writeLen * blockLength_,
		NULL, 0) != 0) {
      message(-2, "Write data failed.");
      return 1;
    }
#endif
    //message(0, ". ");

    lba += writeLen;
    len -= writeLen;
    buf += writeLen * iblen;
  }

  //message(0, "");

  return 0;
}
