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

#include "PQChannelEncoder.h"

#include "Msf.h"
#include "Track.h"
#include "util.h"


PQChannelEncoder::PQChannelEncoder() 
{
  cueSheet_ = NULL;
  cueSheetLen_ = 0;
  toc_ = NULL;
  tocLen_ = 0;
  discType_ = 0;
  current_ = NULL;
  catalog_ = NULL;
  isrc_ = NULL;
}

PQChannelEncoder::~PQChannelEncoder()
{
  int i;

  for (i = 0; i < tocLen_; i++) {
    delete toc_[i];
  }
  delete[] toc_;

  delete[] cueSheet_;
  delete current_;
  delete catalog_;
  delete isrc_;
}


int PQChannelEncoder::setCueSheet(SubChannel *chan, unsigned char discType, 
				  const unsigned char *sheet, int len,
				  const Msf &leadInStart)
{
  int tocEnt, i;

  subChannel_ = chan;

  // Convert toc type to decimal numbers so that they look like the
  // corresponding hex value when stored as BCD in the sub-channel
  switch (discType) {
  case 0:
    break;
  case 0x10:
    discType = 10;
    break;
  case 0x20:
    discType = 20;
    break;
  default:
    message(-3, "Illegal disc type.");
    return 1;
  }

  if ((len % sizeof(CueSheetEntry)) != 0) {
    message(-3, "Illegal cue sheet length.");
    return 1;
  }

  discType_ = discType;

  cueSheetLen_ = len / sizeof(CueSheetEntry);
  cueSheet_ = new CueSheetEntry[cueSheetLen_ + 1];
  memcpy(cueSheet_, sheet, len);
  // mark end of queue sheet with a zeroed entry
  memset(cueSheet_ + cueSheetLen_, 0, sizeof(CueSheetEntry));

  // create some sub channel objects
  catalog_ = subChannel_->makeSubChannel(SubChannel::QMODE2);
  isrc_ = subChannel_->makeSubChannel(SubChannel::QMODE3);
  current_ = subChannel_->makeSubChannel(SubChannel::QMODE1DATA);
  
  if (analyzeCueSheet() != 0) {
    return 1;
  }

  // create PQ sub channels for toc in lead-in
  tocLen_ = lastTrackNr_ - firstTrackNr_ + 1 + 3/*A0, A1, A2*/;
  toc_ = new SubChannel*[tocLen_];

  for (i = 0; i < tocLen_; i++) {
    toc_[i] = subChannel_->makeSubChannel(SubChannel::QMODE1TOC);
  }

  CueSheetEntry *run;
  for (run = actCueSheetEntry_, tocEnt = 0; 
       (run->ctlAdr & 0x0f) != 1 || run->trackNr != 0xaa; run++) {
    if ((run->ctlAdr & 0x0f) == 1 && run->trackNr > 0 && run->trackNr <= 99 &&
	run->indexNr == 1) {
      toc_[tocEnt]->ctl(run->ctlAdr & 0xf0);
      toc_[tocEnt]->point(run->trackNr);
      toc_[tocEnt]->pmin(run->min);
      toc_[tocEnt]->psec(run->sec);
      toc_[tocEnt]->pframe(run->frame);
      tocEnt++;
    }
  }

  toc_[tocEnt]->point(0xa0);
  toc_[tocEnt]->pmin(firstTrackNr_);
  toc_[tocEnt]->psec(discType_);
  toc_[tocEnt]->ctl(firstTrackCtlAdr_ & 0xf0);
  tocEnt++;

  toc_[tocEnt]->point(0xa1);
  toc_[tocEnt]->pmin(lastTrackNr_);
  toc_[tocEnt]->ctl(lastTrackCtlAdr_ & 0xf0);
  tocEnt++;

  toc_[tocEnt]->point(0xa2);
  toc_[tocEnt]->pmin(leadOutStart_.min());
  toc_[tocEnt]->psec(leadOutStart_.sec());
  toc_[tocEnt]->pframe(leadOutStart_.frac());
  toc_[tocEnt]->ctl(leadOutCtlAdr_ & 0xf0);
  tocEnt++;

  assert(tocEnt == tocLen_);

  // setup encoder dynamic data
  abslba_ = leadInStart.lba() - 450150;
  trlba_ = 0;
  writeIsrc_ = 0;
  deferredCatalog_ = 0;
  deferredIsrc_ = 0;

  actTocEnt_ = actTocCount_ = 0;

  run = nextCueSheetEntry(actCueSheetEntry_, 1);
  assert(run != NULL);
  nextTransitionLba_ = Msf(run->min, run->sec, run->frame).lba() - 150;

  run = nextCueSheetEntry(actCueSheetEntry_, firstTrackNr_, 1);
  assert(run != NULL);
  nextTrackStartLba_ = Msf(run->min, run->sec, run->frame).lba() - 150;

  return 0;
}


int PQChannelEncoder::analyzeCueSheet()
{
  int i;
  CueSheetEntry *ent;
  int prevTrackNr = -1;
  long prevLba = -1;
  long lba;

  firstTrackNr_ = 0;
  firstTrackCtlAdr_ = 0;
  lastTrackNr_ = 0;
  lastTrackCtlAdr_ = 0;
  leadOutStart_ = 0;
  leadOutCtlAdr_ = 0;
  actCueSheetEntry_ = NULL;
  writeCatalog_ = 0;

  for (ent = cueSheet_, i = 0; i < cueSheetLen_; ent++, i++) {

    switch (ent->ctlAdr & 0x0f) {
    case 1:
      if (ent->min > 99 || ent->sec > 59 || ent->frame > 74) {
	message(-3, "Illegal time field value at cue sheet entry: %d",
		i);
	return 1;
      }
	
      if (ent->trackNr == 0) { // indicates lead-in
	if (i == 0 || (writeCatalog_ && i == 2)) { // must be first entry
	  actCueSheetEntry_ = ent;
	}
	else {
	  message(-3, "Illegal track number at cue sheet entry: %d", i);
	  return 1;
	}
      }
      else if (ent->trackNr == 0xaa) { // indicates lead-out
	if (i == cueSheetLen_ - 1) { // must be last entry
	  leadOutStart_ = Msf(ent->min, ent->sec, ent->frame);
	  leadOutCtlAdr_ = ent->ctlAdr;
	}
	else {
	  message(-3, "Illegal track number at cue sheet entry: %d", i);
	  return 1;
	}
      }
      else if (ent->trackNr <= 99) { // data track
	if (firstTrackNr_ == 0) {
	  firstTrackNr_ = ent->trackNr;
	  firstTrackCtlAdr_ = ent->ctlAdr;
	  prevTrackNr = ent->trackNr;
	}
	else {
	  if (ent->trackNr != prevTrackNr && ent->trackNr != prevTrackNr + 1) {
	    message(-3, 
		    "Wrong track number sequence at cue sheet entry: %d", i);
	    return 1;
	  }
	  prevTrackNr = ent->trackNr;
	}
	lastTrackNr_ = ent->trackNr;
	lastTrackCtlAdr_ = ent->ctlAdr;
      }
      else {
	message(-3, "Illegal track number at cue sheet entry: %d", i);
	return 1;
      }

      if (ent->trackNr != 0) {
	lba = Msf(ent->min, ent->sec, ent->frame).lba();
	if (lba <= prevLba) {
	  message(-3, 
		  "Time field does not increase at cue sheet entry: %d", i);
	  return 1;
	}
	prevLba = lba;
      }
      break;

    case 2:
      if (i != 0) {
	message(-3, "Catalog number must be first cue sheet entry.");
	return 1;
      }
      if ((cueSheet_[1].ctlAdr & 0x0f) != 2) {
	message(-3, "Missing second catalog number entry.");
	return 1;
      }
      writeCatalog_ = 1;
      catalog_->catalog(ent->trackNr, ent->indexNr, ent->dataForm, ent->scms,
			ent->min, ent->sec, ent->frame, cueSheet_[1].trackNr,
			cueSheet_[1].indexNr, cueSheet_[1].dataForm,
			cueSheet_[1].scms, cueSheet_[1].min, cueSheet_[1].sec);
      // skip next entry
      ent++;
      i++;
      break;

    case 3:
      if (((ent + 1)->ctlAdr & 0x0f) != 3) {
	message(-3, "Missing second ISRC code entry.");
	return 1;
      }

      // skip next entry
      ent++;
      i++;
      break;
    default:
      message(-3, "Illegal adr field at cue sheet entry: %d.", i);
      return 1;
      break;
    }
  }

  if (actCueSheetEntry_ == NULL) {
    message(-3, "Cue sheet contains no lead-in entry.");
    return 1;
  }

  if (leadOutStart_.lba() == 0) {
    message(-3, "Cue sheet contains no lead-out entry.");
    return 1;
  }

  if (firstTrackNr_ == 0) {
    message(-3, "Cue sheet contains no data track.");
    return 1;
  }

  return 0;
}


void PQChannelEncoder::encode(long lba, unsigned char *out, long blocks)
{
  long clen = subChannel_->dataLength();
  const SubChannel *chan;
  long i;
  
  for (i = 0; i < blocks; i++, lba++) {
    chan = encodeSubChannel(lba);
    memcpy(out, chan->data(), clen);
    out += clen;
  }
}



const SubChannel *PQChannelEncoder::encodeSubChannel(long lba)
{
  int newTransition = 0;
  SubChannel *chan = NULL;
  
  // check consistency of internal lba with external lba
  assert(lba == abslba_);

  if (lba == nextTransitionLba_) {
    // switch to next transition
    //message(3, "Switching to next transition at lba: %ld", lba);
    nextTransition();
    newTransition = 1;
  }

  if (actCueSheetEntry_->trackNr == 0) {
    // lead-in sector
    chan = toc_[actTocEnt_];

    Msf m(trlba_);
    chan->min(m.min());
    chan->sec(m.sec());
    chan->frame(m.frac());

    // advance to next to entry
    if (++actTocCount_ == 3) {
      actTocCount_ = 0;
      if (++actTocEnt_ == tocLen_) {
	actTocEnt_ = 0;
      }
    }
  }
  else {
    // data or lead-out sector

    // Q channel setup

    // catalog number
    if (writeCatalog_ && 
	(deferredCatalog_ || (abslba_ % 90) == 0)) {
      if (newTransition) {
	deferredCatalog_ = 1;
      }
      else {
	deferredCatalog_ = 0;
	chan = catalog_;
	chan->aframe(Msf(abslba_ + 150).frac());
      }
    }

    // ISRC code
    if (writeIsrc_ && 
	(deferredIsrc_ || (abslba_ % 90) == 50 || (abslba_ % 90) == -40)) {
      if (newTransition) {
	deferredIsrc_ = 1;
      }
      else {
	deferredIsrc_ = 0;
	chan = isrc_;
	chan->aframe(Msf(abslba_ + 150).frac());
      }
    }

    if (chan == NULL) {
      // Current position
      chan = current_;

      Msf m(trlba_ < 0 ? -trlba_ : trlba_);
      chan->min(m.min());
      chan->sec(m.sec());
      chan->frame(m.frac());
      
      m = Msf(abslba_ + 150);
      chan->amin(m.min());
      chan->asec(m.sec());
      chan->aframe(m.frac());
    }


    // P channel setup
    if (trlba_ <= 0 || 
	(abslba_ >= nextTrackStartLba_ - 150 && 
	 abslba_ <= nextTrackStartLba_)) {
      // set P channel flag in pre-gap and 2 seconds before next track starts
      chan->pChannel(1);
    }
    else if (abslba_ >= leadOutStart_.lba() ) {
      // P channel flag is 0 2 secs from start of lead-out, after that
      // flag alternates at 2 Hz.
      chan->pChannel(((trlba_ - 150) % 38) < 19 ? 1 : 0);
    }
    else {
      chan->pChannel(0);
    }
  }

  abslba_++;
  trlba_++;

  assert(chan != NULL);
  chan->calcCrc();

  return chan;
}

void PQChannelEncoder::nextTransition()
{
  CueSheetEntry *nextEnt, *ent , *ent1;

  nextEnt = nextCueSheetEntry(actCueSheetEntry_, 1);
  assert(nextEnt != NULL);

  if (nextEnt->trackNr != actCueSheetEntry_->trackNr) {
    // new track started

    // check for ISRC code cue sheet entry
    if ((ent = nextCueSheetEntry(actCueSheetEntry_, 3)) != NULL &&
	ent->trackNr == nextEnt->trackNr) {
      ent1 = ent + 1;
      isrc_->isrc(ent->indexNr, ent->dataForm, ent->scms, ent->min, ent->sec,
		  ent->frame, ent1->indexNr, ent1->dataForm, ent1->scms,
		  ent1->min, ent1->sec, ent1->frame);
      writeIsrc_ = 1;
    }
    else {
      writeIsrc_ = 0;
    }
    deferredIsrc_ = 0;

    // setup 'trlba'
    if (nextEnt->indexNr == 0) {
      // track has a pre-gap -> determine length
      ent = nextCueSheetEntry(nextEnt, nextEnt->trackNr, 1);
      assert(ent != NULL);
      
      trlba_ = Msf(nextEnt->min, nextEnt->sec, nextEnt->frame).lba() -
	Msf(ent->min, ent->sec, ent->frame).lba();
    }
    else {
      trlba_ = 0;
    }
  }
    
  actCueSheetEntry_ = nextEnt;
  
  current_->ctl(actCueSheetEntry_->ctlAdr & 0xf0);
  current_->trackNr(actCueSheetEntry_->trackNr);
  current_->indexNr(actCueSheetEntry_->indexNr);

  if (writeCatalog_)
    catalog_->ctl(actCueSheetEntry_->ctlAdr & 0xf0);

  if (writeIsrc_)
    isrc_->ctl(actCueSheetEntry_->ctlAdr & 0xf0);

  if (actCueSheetEntry_->trackNr != 0xaa) {
    // find next transition point

    ent = nextCueSheetEntry(actCueSheetEntry_, 1);
    assert(ent != NULL);
    nextTransitionLba_ = Msf(ent->min, ent->sec, ent->frame).lba() - 150;

    if (actCueSheetEntry_->indexNr == 1) {
      // find next track start lba
      if (actCueSheetEntry_->trackNr == lastTrackNr_)
	ent = nextCueSheetEntry(actCueSheetEntry_, 0xaa, 1);
      else
	ent = nextCueSheetEntry(actCueSheetEntry_,
				actCueSheetEntry_->trackNr + 1, 1);
      assert(ent != NULL);

      nextTrackStartLba_ = Msf(ent->min, ent->sec, ent->frame).lba() - 150;
    }
  }
}

CueSheetEntry *PQChannelEncoder::nextCueSheetEntry(CueSheetEntry *act,
						   int adr)
{
  if (act->trackNr == 0xaa) {
    return NULL;
  }

  act++;

  while (act->ctlAdr != 0) {
    if ((act->ctlAdr & 0x0f) == adr) {
      return act;
    }
    act++;
  }

  return NULL;
}

CueSheetEntry *PQChannelEncoder::nextCueSheetEntry(CueSheetEntry *act,
						   int trackNr, int indexNr)
{
  if (act->trackNr == 0xaa) {
    return NULL;
  }

  act++;

  while (act->ctlAdr != 0) {
    if ((act->ctlAdr & 0x0f) == 1 && act->trackNr == trackNr &&
	act->indexNr == indexNr) {
      return act;
    }
    act++;
  }

  return NULL;
}


