/*  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 <string.h>
#include <assert.h>

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

#include "Toc.h"
#include "log.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(bool m)
{
    if (m) {
	// multi session mode is currently not support for raw writing
	return 1;
    }

    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)
{
    u8 mp[0x38];

    if (getModePage(5/*write parameters mode page*/, mp, 0x38,
                    NULL, NULL, 1) != 0) {
        log_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
    }

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

        RicohSetWriteOptions(di);
    }

    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) {
        //log_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) {
        u8 cmd[10];
        u8 data[4];
        u8 *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) {
            log_message(-2, "Cannot read ATIP data.");
            return 1;
        }
    
        dataLen = (data[0] << 8) | data[1];
        dataLen += 2;

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

        if (sessionNr == 1) {
            if (dataLen < 19) {
                log_message(-2, "Cannot read ATIP data.");
                return 1;
            }
        }
        else {
            if (dataLen < 15) {
                log_message(-2, "Cannot read ATIP data.");
                return 1;
            }
        }      
    
        buf = new u8[dataLen];
        memset(buf, 0, dataLen);
    
        cmd[7] = dataLen >> 8;
        cmd[8] = dataLen;
    
        if (sendCmd(cmd, 10, NULL, 0, buf, dataLen, 0) != 0) {
            log_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 {
                log_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 {
                    log_message(-2, "ATIP data does not contain point A1 data.");
                    err = 1;
                }
            }
        }

        delete[] buf;
        buf = NULL;
    }

    log_message(4, "SI: session nr: %d", info->sessionNr);
    log_message(4, "SI: lead-in start: %ld", info->leadInStart);
    log_message(4, "SI: lead-in len: %ld", info->leadInLen);
    log_message(4, "SI: lead-out len: %ld", info->leadOutLen);
    log_message(4, "SI: last lead-out start: %d %d %d", info->lastLeadoutStart.min(),
                info->lastLeadoutStart.sec(), info->lastLeadoutStart.frac());
    log_message(4, "SI: cdrw: %d", info->cdrw);
    log_message(4, "SI: atip lead-in start: %d %d %d", info->atipLeadinStart.min(),
                info->atipLeadinStart.sec(), info->atipLeadinStart.frac());
    log_message(4, "SI: optimum recording power: %u", info->optimumRecordingPower);
    log_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;
        }
    }

    log_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;

            log_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()) {
                log_message(-1, "Ignored because of --force option.");
            }
            else {
                log_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) {
                log_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 {
            log_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:
        log_message(2, "Using 16 byte P-Q sub-channel data mode.");
        break;
    case 2:
        log_message(2, "Using 96 byte packed P-W sub-channel data mode.");
        break;
    case 3:
        if (cdTextEncoder_ != NULL)
            log_message(2, "Using 96 byte raw P-W sub-channel data mode for CD-TEXT.");
        else
            log_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 ||
        getSessionInfo() != 0) {
        return 1;
    }

    delete cdTextEncoder_;
    cdTextEncoder_ = new CdTextEncoder(toc);
    if (cdTextEncoder_->encode() != 0) {
        log_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);
    log_message(4, "Block length: %ld", blockLength_);

    long cueSheetLen;
    u8 *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 u8[n];

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

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

      return 1;
    */

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

    return 0;
}

int GenericMMCraw::startDao()
{
    log_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;

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

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

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

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

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

    if (ret != 0)
        log_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()
{
    u8 cmd[10];
    u8 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) {
        log_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;
    u8 cmd[10];
    int i, j;

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

    /*
      log_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 u8 *data = cdTextSubChannels_[cdTextSubChannelAct_]->data();
                long dataLen = cdTextSubChannels_[cdTextSubChannelAct_]->dataLength();

                u8 *actBuf = encodeBuffer_ + i * blockLength_ + AUDIO_BLOCK_LEN;

                //log_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:
                {
                    u8 *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++) {
            log_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);
            }

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

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

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

    //log_message(0, "");

    return 0;
}
