/*
 * Adplug - Replayer for many OPL2/OPL3 audio file formats.
 * Copyright (C) 1999 - 2007 Simon Peter, <dn.tlp@gmx.net>, et al.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * d00.c - D00 Player by Simon Peter <dn.tlp@gmx.net>
 *
 * NOTES:
 * Sorry for the goto's, but the code looks so much nicer now.
 * I tried it with while loops but it was just a mess. If you
 * can come up with a nicer solution, just tell me.
 *
 * BUGS:
 * Hard restart SR is sometimes wrong
 */

#include <string.h>
#include <stdio.h>
#include <inttypes.h>

#include "debug.h"
#include "d00.h"

#define HIBYTE(val)	(val >> 8)
#define LOBYTE(val)	(val & 0xff)

static const unsigned short notetable[12] = // D00 note table
{ 340, 363, 385, 408, 432, 458, 485, 514, 544, 577, 611, 647 };

static inline uint16_t
LE_WORD (const uint16_t * val)
{
  const uint8_t *b = (const uint8_t *) val;
  return (b[1] << 8) + b[0];
}

/*** public methods *************************************/

CPlayer *
Cd00Player::factory (Copl * newopl)
{
  return new Cd00Player (newopl);
}

bool
Cd00Player::load (VFSFile * fd, const CFileProvider & fp)
{
  binistream *f = fp.open (fd);
  if (!f)
    return false;
  d00header *checkhead;
  d00header1 *ch;
  unsigned long filesize;
  int i, ver1 = 0;
  char *str;
  std::string filename (fd->uri);

  // file validation section
  checkhead = new d00header;
  f->readString ((char *) checkhead, sizeof (d00header));

  // Check for version 2-4 header
  if (strncmp (checkhead->id, "JCH\x26\x02\x66", 6) || checkhead->type ||
      !checkhead->subsongs || checkhead->soundcard)
  {
    // Check for version 0 or 1 header (and .d00 file extension)
    delete checkhead;
    if (!fp.extension (filename, ".d00"))
    {
      fp.close (f);
      return false;
    }
    ch = new d00header1;
    f->seek (0);
    f->readString ((char *) ch, sizeof (d00header1));
    if (ch->version > 1 || !ch->subsongs)
    {
      delete ch;
      fp.close (f);
      return false;
    }
    delete ch;
    ver1 = 1;
  }
  else
    delete checkhead;

  AdPlug_LogWrite
    ("Cd00Player::load(f,\"%s\"): %s format D00 file detected!\n",
     filename.c_str (), ver1 ? "Old" : "New");

  // load section
  filesize = fp.filesize (f);
  f->seek (0);
  filedata = new char[filesize + 1];    // 1 byte is needed for old-style DataInfo block
  f->readString ((char *) filedata, filesize);
  fp.close (f);
  if (!ver1)
  {                             // version 2 and above
    header = (struct d00header *) filedata;
    version = header->version;
    datainfo = (char *) filedata + LE_WORD (&header->infoptr);
    inst = (struct Sinsts *) ((char *) filedata + LE_WORD (&header->instptr));
    seqptr =
      (unsigned short *) ((char *) filedata + LE_WORD (&header->seqptr));
    for (i = 31; i >= 0; i--)   // erase whitespace
      if (header->songname[i] == ' ')
        header->songname[i] = '\0';
      else
        break;
    for (i = 31; i >= 0; i--)
      if (header->author[i] == ' ')
        header->author[i] = '\0';
      else
        break;
  }
  else
  {                             // version 1
    header1 = (struct d00header1 *) filedata;
    version = header1->version;
    datainfo = (char *) filedata + LE_WORD (&header1->infoptr);
    inst =
      (struct Sinsts *) ((char *) filedata + LE_WORD (&header1->instptr));
    seqptr =
      (unsigned short *) ((char *) filedata + LE_WORD (&header1->seqptr));
  }
  switch (version)
  {
  case 0:
    levpuls = 0;
    spfx = 0;
    header1->speed = 70;        // v0 files default to 70Hz
    break;
  case 1:
    levpuls =
      (struct Slevpuls *) ((char *) filedata + LE_WORD (&header1->lpulptr));
    spfx = 0;
    break;
  case 2:
    levpuls =
      (struct Slevpuls *) ((char *) filedata + LE_WORD (&header->spfxptr));
    spfx = 0;
    break;
  case 3:
    spfx = 0;
    levpuls = 0;
    break;
  case 4:
    spfx = (struct Sspfx *) ((char *) filedata + LE_WORD (&header->spfxptr));
    levpuls = 0;
    break;
  }
  if ((str = strstr (datainfo, "\xff\xff")))
    while ((*str == '\xff' || *str == ' ') && str >= datainfo)
    {
      *str = '\0';
      str--;
    }
  else                          // old-style block
    memset ((char *) filedata + filesize, 0, 1);

  rewind (0);
  return true;
}

bool
Cd00Player::update ()
{
  unsigned char c, cnt, trackend = 0, fx, note;
  unsigned short ord, *patt, buf, fxop, pattpos;

  // effect handling (timer dependant)
  for (c = 0; c < 9; c++)
  {
    channel[c].slideval += channel[c].slide;
    setfreq (c);                // sliding
    vibrato (c);                // vibrato

    if (channel[c].spfx != 0xffff)
    {                           // SpFX
      if (channel[c].fxdel)
        channel[c].fxdel--;
      else
      {
        channel[c].spfx = LE_WORD (&spfx[channel[c].spfx].ptr);
        channel[c].fxdel = spfx[channel[c].spfx].duration;
        channel[c].inst = LE_WORD (&spfx[channel[c].spfx].instnr) & 0xfff;
        if (spfx[channel[c].spfx].modlev != 0xff)
          channel[c].modvol = spfx[channel[c].spfx].modlev;
        setinst (c);
        if (LE_WORD (&spfx[channel[c].spfx].instnr) & 0x8000)   // locked frequency
          note = spfx[channel[c].spfx].halfnote;
        else                    // unlocked frequency
          note = spfx[channel[c].spfx].halfnote + channel[c].note;
        channel[c].freq = notetable[note % 12] + ((note / 12) << 10);
        setfreq (c);
      }
      channel[c].modvol += spfx[channel[c].spfx].modlevadd;
      channel[c].modvol &= 63;
      setvolume (c);
    }

    if (channel[c].levpuls != 0xff) // Levelpuls
    {
      if (channel[c].frameskip)
        channel[c].frameskip--;
      else
      {
        channel[c].frameskip = inst[channel[c].inst].timer;
        if (channel[c].fxdel)
          channel[c].fxdel--;
        else
        {
          channel[c].levpuls = levpuls[channel[c].levpuls].ptr - 1;
          channel[c].fxdel = levpuls[channel[c].levpuls].duration;
          if (levpuls[channel[c].levpuls].level != 0xff)
            channel[c].modvol = levpuls[channel[c].levpuls].level;
        }
        channel[c].modvol += levpuls[channel[c].levpuls].voladd;
        channel[c].modvol &= 63;
        setvolume (c);
      }
    }
  }

  // song handling
  for (c = 0; c < 9; c++)
    if (version < 3 ? channel[c].del : channel[c].del <= 0x7f)
    {
      if (version == 4)         // v4: hard restart SR
        if (channel[c].del == inst[channel[c].inst].timer)
          if (channel[c].nextnote)
            opl->write (0x83 + op_table[c], inst[channel[c].inst].sr);
      if (version < 3)
        channel[c].del--;
      else if (channel[c].speed)
        channel[c].del += channel[c].speed;
      else
      {
        channel[c].seqend = 1;
        continue;
      }
    }
    else
    {
      if (channel[c].speed)
      {
        if (version < 3)
          channel[c].del = channel[c].speed;
        else
        {
          channel[c].del &= 0x7f;
          channel[c].del += channel[c].speed;
        }
      }
      else
      {
        channel[c].seqend = 1;
        continue;
      }
      if (channel[c].rhcnt)
      {                         // process pending REST/HOLD events
        channel[c].rhcnt--;
        continue;
      }
    readorder:                 // process arrangement (orderlist)
      ord = LE_WORD (&channel[c].order[channel[c].ordpos]);
      switch (ord)
      {
      case 0xfffe:
        channel[c].seqend = 1;
        continue;               // end of arrangement stream
      case 0xffff:             // jump to order
        channel[c].ordpos =
          LE_WORD (&channel[c].order[channel[c].ordpos + 1]);
        channel[c].seqend = 1;
        goto readorder;
      default:
        if (ord >= 0x9000)
        {                       // set speed
          channel[c].speed = ord & 0xff;
          ord = LE_WORD (&channel[c].order[channel[c].ordpos - 1]);
          channel[c].ordpos++;
        }
        else if (ord >= 0x8000)
        {                       // transpose track
          channel[c].transpose = ord & 0xff;
          if (ord & 0x100)
            channel[c].transpose = -channel[c].transpose;
          ord = LE_WORD (&channel[c].order[++channel[c].ordpos]);
        }
        patt =
          (unsigned short *) ((char *) filedata + LE_WORD (&seqptr[ord]));
        break;
      }
      channel[c].fxflag = 0;
    readseq:                   // process sequence (pattern)
      if (!version)             // v0: always initialize rhcnt
        channel[c].rhcnt = channel[c].irhcnt;
      pattpos = LE_WORD (&patt[channel[c].pattpos]);
      if (pattpos == 0xffff)
      {                         // pattern ended?
        channel[c].pattpos = 0;
        channel[c].ordpos++;
        goto readorder;
      }
      cnt = HIBYTE (pattpos);
      note = LOBYTE (pattpos);
      fx = pattpos >> 12;
      fxop = pattpos & 0x0fff;
      channel[c].pattpos++;
      pattpos = LE_WORD (&patt[channel[c].pattpos]);
      channel[c].nextnote = LOBYTE (pattpos) & 0x7f;
      if (version ? cnt < 0x40 : !fx)
      {                         // note event
        switch (note)
        {
        case 0:                // REST event
        case 0x80:
          if (!note || version)
          {
            channel[c].key = 0;
            setfreq (c);
          }
          // fall through...
        case 0x7e:             // HOLD event
          if (version)
            channel[c].rhcnt = cnt;
          channel[c].nextnote = 0;
          break;
        default:               // play note
          // restart fx
          if (!(channel[c].fxflag & 1))
            channel[c].vibdepth = 0;
          if (!(channel[c].fxflag & 2))
            channel[c].slideval = channel[c].slide = 0;

          if (version)
          {                     // note handling for v1 and above
            if (note > 0x80)    // locked note (no channel transpose)
              note -= 0x80;
            else                // unlocked note
              note += channel[c].transpose;
            channel[c].note = note; // remember note for SpFX

            if (channel[c].ispfx != 0xffff && cnt < 0x20)
            {                   // reset SpFX
              channel[c].spfx = channel[c].ispfx;
              if (LE_WORD (&spfx[channel[c].spfx].instnr) & 0x8000) // locked frequency
                note = spfx[channel[c].spfx].halfnote;
              else              // unlocked frequency
                note += spfx[channel[c].spfx].halfnote;
              channel[c].inst =
                LE_WORD (&spfx[channel[c].spfx].instnr) & 0xfff;
              channel[c].fxdel = spfx[channel[c].spfx].duration;
              if (spfx[channel[c].spfx].modlev != 0xff)
                channel[c].modvol = spfx[channel[c].spfx].modlev;
              else
                channel[c].modvol = inst[channel[c].inst].data[7] & 63;
            }

            if (channel[c].ilevpuls != 0xff && cnt < 0x20)
            {                   // reset LevelPuls
              channel[c].levpuls = channel[c].ilevpuls;
              channel[c].fxdel = levpuls[channel[c].levpuls].duration;
              channel[c].frameskip = inst[channel[c].inst].timer;
              if (levpuls[channel[c].levpuls].level != 0xff)
                channel[c].modvol = levpuls[channel[c].levpuls].level;
              else
                channel[c].modvol = inst[channel[c].inst].data[7] & 63;
            }

            channel[c].freq = notetable[note % 12] + ((note / 12) << 10);
            if (cnt < 0x20)     // normal note
              playnote (c);
            else
            {                   // tienote
              setfreq (c);
              cnt -= 0x20;      // make count proper
            }
            channel[c].rhcnt = cnt;
          }
          else
          {                     // note handling for v0
            if (cnt < 2)        // unlocked note
              note += channel[c].transpose;
            channel[c].note = note;

            channel[c].freq = notetable[note % 12] + ((note / 12) << 10);
            if (cnt == 1)       // tienote
              setfreq (c);
            else                // normal note
              playnote (c);
          }
          break;
        }
        continue;               // event is complete
      }
      else
      {                         // effect event
        switch (fx)
        {
        case 6:                // Cut/Stop Voice
          buf = channel[c].inst;
          channel[c].inst = 0;
          playnote (c);
          channel[c].inst = buf;
          channel[c].rhcnt = fxop;
          continue;             // no note follows this event
        case 7:                // Vibrato
          channel[c].vibspeed = fxop & 0xff;
          channel[c].vibdepth = fxop >> 8;
          channel[c].trigger = fxop >> 9;
          channel[c].fxflag |= 1;
          break;
        case 8:                // v0: Duration
          if (!version)
            channel[c].irhcnt = fxop;
          break;
        case 9:                // New Level
          channel[c].vol = fxop & 63;
          if (channel[c].vol + channel[c].cvol < 63)    // apply channel volume
            channel[c].vol += channel[c].cvol;
          else
            channel[c].vol = 63;
          setvolume (c);
          break;
        case 0xb:              // v4: Set SpFX
          if (version == 4)
            channel[c].ispfx = fxop;
          break;
        case 0xc:              // Set Instrument
          channel[c].ispfx = 0xffff;
          channel[c].spfx = 0xffff;
          channel[c].inst = fxop;
          channel[c].modvol = inst[fxop].data[7] & 63;
          if (version < 3 && version && inst[fxop].tunelev) // Set LevelPuls
            channel[c].ilevpuls = inst[fxop].tunelev - 1;
          else
          {
            channel[c].ilevpuls = 0xff;
            channel[c].levpuls = 0xff;
          }
          break;
        case 0xd:              // Slide up
          channel[c].slide = fxop;
          channel[c].fxflag |= 2;
          break;
        case 0xe:              // Slide down
          channel[c].slide = -fxop;
          channel[c].fxflag |= 2;
          break;
        }
        goto readseq;           // event is incomplete, note follows
      }
    }

  for (c = 0; c < 9; c++)
    if (channel[c].seqend)
      trackend++;
  if (trackend == 9)
    songend = 1;

  return !songend;
}

void
Cd00Player::rewind (int subsong)
{
  struct Stpoin
  {
    unsigned short ptr[9];
    unsigned char volume[9], dummy[5];
  } *tpoin;
  int i;

  if(subsong == -1) subsong = cursubsong;

  if (version > 1)
  {                             // do nothing if subsong > number of subsongs
    if (subsong >= header->subsongs)
      return;
  }
  else if (subsong >= header1->subsongs)
    return;

  memset (channel, 0, sizeof (channel));
  if (version > 1)
    tpoin = (struct Stpoin *) ((char *) filedata + LE_WORD (&header->tpoin));
  else
    tpoin = (struct Stpoin *) ((char *) filedata + LE_WORD (&header1->tpoin));
  for (i = 0; i < 9; i++)
  {
    if (LE_WORD (&tpoin[subsong].ptr[i]))
    {                           // track enabled
      channel[i].speed = LE_WORD ((unsigned short *)
                                  ((char *) filedata +
                                   LE_WORD (&tpoin[subsong].ptr[i])));
      channel[i].order =
        (unsigned short *) ((char *) filedata +
                            LE_WORD (&tpoin[subsong].ptr[i]) + 2);
    }
    else
    {                           // track disabled
      channel[i].speed = 0;
      channel[i].order = 0;
    }
    channel[i].ispfx = 0xffff;
    channel[i].spfx = 0xffff;   // no SpFX
    channel[i].ilevpuls = 0xff;
    channel[i].levpuls = 0xff;  // no LevelPuls
    channel[i].cvol = tpoin[subsong].volume[i] & 0x7f;  // our player may savely ignore bit 7
    channel[i].vol = channel[i].cvol;   // initialize volume
  }
  songend = 0;
  opl->init ();
  opl->write (1, 32);           // reset OPL chip
  cursubsong = subsong;
}

std::string Cd00Player::gettype ()
{
  char
    tmpstr[40];

  sprintf (tmpstr, "EdLib packed (version %d)",
           version > 1 ? header->version : header1->version);
  return std::string (tmpstr);
}

float
Cd00Player::getrefresh ()
{
  if (version > 1)
    return header->speed;
  else
    return header1->speed;
}

unsigned int
Cd00Player::getsubsongs ()
{
  if (version <= 1)             // return number of subsongs
    return header1->subsongs;
  else
    return header->subsongs;
}

/*** private methods *************************************/

void
Cd00Player::setvolume (unsigned char chan)
{
  unsigned char op = op_table[chan];
  unsigned short insnr = channel[chan].inst;

  opl->write (0x43 + op,
              (int) (63 -
                     ((63 - (inst[insnr].data[2] & 63)) / 63.0) * (63 -
                                                                   channel
                                                                   [chan].
                                                                   vol)) +
              (inst[insnr].data[2] & 192));
  if (inst[insnr].data[10] & 1)
    opl->write (0x40 + op,
                (int) (63 -
                       ((63 - channel[chan].modvol) / 63.0) * (63 -
                                                               channel[chan].
                                                               vol)) +
                (inst[insnr].data[7] & 192));
  else
    opl->write (0x40 + op,
                channel[chan].modvol + (inst[insnr].data[7] & 192));
}

void
Cd00Player::setfreq (unsigned char chan)
{
  unsigned short freq = channel[chan].freq;

  if (version == 4)             // v4: apply instrument finetune
    freq += inst[channel[chan].inst].tunelev;

  freq += channel[chan].slideval;
  opl->write (0xa0 + chan, freq & 255);
  if (channel[chan].key)
    opl->write (0xb0 + chan, ((freq >> 8) & 31) | 32);
  else
    opl->write (0xb0 + chan, (freq >> 8) & 31);
}

void
Cd00Player::setinst (unsigned char chan)
{
  unsigned char op = op_table[chan];
  unsigned short insnr = channel[chan].inst;

  // set instrument data
  opl->write (0x63 + op, inst[insnr].data[0]);
  opl->write (0x83 + op, inst[insnr].data[1]);
  opl->write (0x23 + op, inst[insnr].data[3]);
  opl->write (0xe3 + op, inst[insnr].data[4]);
  opl->write (0x60 + op, inst[insnr].data[5]);
  opl->write (0x80 + op, inst[insnr].data[6]);
  opl->write (0x20 + op, inst[insnr].data[8]);
  opl->write (0xe0 + op, inst[insnr].data[9]);
  if (version)
    opl->write (0xc0 + chan, inst[insnr].data[10]);
  else
    opl->write (0xc0 + chan,
                (inst[insnr].data[10] << 1) + (inst[insnr].tunelev & 1));
}

void
Cd00Player::playnote (unsigned char chan)
{
  // set misc vars & play
  opl->write (0xb0 + chan, 0);  // stop old note
  setinst (chan);
  channel[chan].key = 1;
  setfreq (chan);
  setvolume (chan);
}

void
Cd00Player::vibrato (unsigned char chan)
{
  if (!channel[chan].vibdepth)
    return;

  if (channel[chan].trigger)
    channel[chan].trigger--;
  else
  {
    channel[chan].trigger = channel[chan].vibdepth;
    channel[chan].vibspeed = -channel[chan].vibspeed;
  }
  channel[chan].freq += channel[chan].vibspeed;
  setfreq (chan);
}
