/*
 * audio_disk.c
 * Copyright (C) 2002 P.Everett <peter@everett9981.freeserve.co.uk>
 *
 * This file is part of KinoAMP, a free RISCOS MPEG program stream decoder.
 *
 * KinoAMP 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.
 *
 * KinoAMP 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "kernel.h"
#include "swis.h"

#include "audiocodec.h"
#include "config.h"
#include "ka_codecs.h"
#include "ka_error.h"
#include "ka_mem.h"
#include "ka_log.h"
#include "timer1.h"

extern ka_config_t config;

// SWI's
#define DiskSample_Version          0x52EC0
#define DiskSample_Configure        0x52EC1
#define DiskSample_StreamClose      0x52EC3
#define DiskSample_StreamCreate     0x52EC4
#define DiskSample_StreamSource     0x52EC5
#define DiskSample_StreamPlay       0x52EC8
#define DiskSample_StreamPause      0x52EC9
#define DiskSample_StreamPosition   0x52ECB
#define DiskSample_StreamVolume     0x52ECC
#define DiskSample_StreamStatus     0x52ECE
#define DiskSample_StreamInfo       0x52ED0
#define DiskSample_StreamTexts      0x52ED1
#define DiskSample_StreamParam      0x52ED2
#define DiskSample_StreamDecoding   0x52ED3

typedef struct
{
  // buffer header
  int   size;     // read only, size of the input buffer in bytes
  int   start;    // read only, offset from start of inbuf.data,
                  // gives the beginning of the filled part of the buffer
                  // this value change in the background while DiskSample
                  // decodes the data and empties parts of the buffer
  int   free;     // writable, offset from start of inbuf.data,
                  // gives the end of the filled part of the buffer
                  // you should update if as you fill the buffer
  int   finished; // writable, tells DiskSample if more data is to be expected
                  // 0: more data will follow, 1: no more data will follow
  int   dummy[12];
  // buffer data
  char  data[1];  // actually it is data[inbuf.size]
} inbuf;

#define DiskSample_Desc_UseTypeString 0x00000001
#define DiskSample_Desc_BigEndian     0x00000002
#define DiskSample_Desc_Unsigned      0x00000004

typedef struct
{
  int   filesize;
  char* filetype;
  int   flags;
  struct
  {
    int channels;
    int samplerate;
    int bitspersample;
    int blocksize;
  } pcm;
} typdesc;

// The buffer is circular and the following conditions apply:
// 0 <= start < size
// 0 <= free < size
// if (start = free) the buffer is empty
// if (start < free) the buffer is filled from [start, free[
// if (start > free) the buffer is filled from [start, size[ and [0, free[
// the buffer may never be completely filled

typedef struct
{
  ka_audiocodec_t hdr;
  int      handle;
  inbuf*   inb;
  typdesc  desc;
  int      opened;
  int      pause;
  uint32_t pts;
  uint32_t addedsincepts;
  ka_block_t* translated;
  ka_block_id typ;
  int      version;
} data_t;

#define OP_BUFF_DELAY 2 // buffers at least 1 sec before playing
#define OP_BUFF 512  // 2*1.49 secs of 44.1k

/**
 * Allocates and initializes codec data, links to player module.
 *
 * @param  ppcodec  Pointer to fill with a pointer to the codec's data.
 *
 * @returns Pointer to codec data if OK, else NULL and error block is filled.
 */
static ka_audiocodec_t* disk_open(ka_error_t* pErrorBlock)
{
  int rc;
  data_t* pcodec;


  // check that we have the required modules
  rc = rm_ensure(pErrorBlock, "DiskSample", 33, "System:Modules.Audio.DiskSample");
  if (rc)
    return NULL;

  if ((pcodec = ka_mem_calloc(sizeof(*pcodec))) == NULL)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return NULL;
  }

  pcodec->hdr.vptr = &kav_disk_codec;
  pcodec->hdr.pErrorBlock = pErrorBlock;

  pcodec->desc.filesize = 0; // unknown

  return &pcodec->hdr;
}

static int disk_allow(const ka_aparams_t* params)
{
  switch(params->typ)
  {
    case KA_ABLOCK_MPEG:
    case KA_ABLOCK_AC3:
    case KA_ABLOCK_LPCM_LE:
    case KA_ABLOCK_LPCM_LE_U:
    case KA_ABLOCK_LPCM_BE:
    case KA_ABLOCK_LPCM_BE_U:
    case KA_ABLOCK_LPCM_DVD:
    case KA_ABLOCK_LPCM_BR:
      return 1;
    break;
    case KA_ABLOCK_DTS:
    default:
      return 0;
  }
}

/**
 * Connects to the player module and checks the description
 * of the type of audio packets that we will provide it.
 *
 * @param  p      Pointer to the codec's data.
 * @param  params Type of audio packet to decode.
 *
 * @returns 0 if OK, else error block is filled.
 */
static int disk_start(ka_audiocodec_t* p, const ka_aparams_t* params)
{
  data_t* pcodec = (data_t*) p;
  int in_size; // buffer size in KB

  pcodec->desc.flags = DiskSample_Desc_UseTypeString;
  pcodec->pause = 1;

  switch(params->typ)
  {
    case KA_ABLOCK_MPEG:
    {
      pcodec->desc.filetype = "MPEG";
      in_size = 24;
    }
    break;
    case KA_ABLOCK_AC3:
    {
      pcodec->desc.filetype = "AC3";
      in_size = 128;
    }
    break;
    case KA_ABLOCK_DTS:
    {
      pcodec->desc.filetype = "DTS";
      in_size = 256;
    }
    break;
    case KA_ABLOCK_LPCM_LE:
    case KA_ABLOCK_LPCM_LE_U:
    {
      pcodec->desc.filetype = "PCM";
      pcodec->desc.pcm.channels = params->channels;
      pcodec->desc.pcm.samplerate = params->samplerate;
      pcodec->desc.pcm.bitspersample = params->bitspersample;
      pcodec->desc.pcm.blocksize = params->blocksize;
      in_size = pcodec->desc.pcm.samplerate
              * pcodec->desc.pcm.channels
              * pcodec->desc.pcm.bitspersample
              / (500 * 8);
      if (params->typ == KA_ABLOCK_LPCM_LE_U)
        pcodec->desc.flags |= DiskSample_Desc_Unsigned;
    }
    break;
    case KA_ABLOCK_LPCM_BE:
    case KA_ABLOCK_LPCM_BE_U:
    case KA_ABLOCK_LPCM_DVD:
    case KA_ABLOCK_LPCM_BR:
    {
      pcodec->desc.flags |= DiskSample_Desc_BigEndian;
      pcodec->desc.filetype = "PCM";
      pcodec->desc.pcm.channels = params->channels;
      pcodec->desc.pcm.samplerate = params->samplerate;
      pcodec->desc.pcm.bitspersample = params->bitspersample;
      pcodec->desc.pcm.blocksize = params->blocksize;
      in_size = pcodec->desc.pcm.samplerate
              * pcodec->desc.pcm.channels
              * pcodec->desc.pcm.bitspersample
              / (500 * 8);
      if (params->typ == KA_ABLOCK_LPCM_LE_U)
        pcodec->desc.flags |= DiskSample_Desc_Unsigned;
    }
    break;
    default:
      if (config.debug & (cfg_printwarnings | cfg_printaudiostats))
        ka_log(ka_log_error | ka_log_audio, "Format not supported by DiskSample");

      ka_error_fill(pcodec->hdr.pErrorBlock, "error33:%s %s", ka_block_to_name(params->typ), "DiskSample");
      return -1;
  }
  pcodec->typ = params->typ;

  _swix(DiskSample_Version, _OUT(0), &pcodec->version);
  _swix(DiskSample_Configure, _INR(0,1), 257, OP_BUFF);
  _swix(DiskSample_Configure, _INR(0,1), 260, 1); // Set buffering to minimal (.5 sec)

  // input buffer must remain small to avoid sync problems with VBR streams
  if (_swix(DiskSample_StreamCreate, _INR(0,1)|_OUT(0)
           , 0, in_size
           , &pcodec->handle))
  {
    ka_error_fill(pcodec->hdr.pErrorBlock, "error30"); // (Cannot create DiskSample stream)
    return -1;
  }

  if (_swix(DiskSample_StreamSource, _INR(0,2)|_OUT(1)
           , pcodec->handle, 0, &pcodec->desc
           , &pcodec->inb))
  {
    _swix(DiskSample_StreamClose, _IN(0), pcodec->handle);
    ka_error_fill(pcodec->hdr.pErrorBlock, "error31:%4s", pcodec->desc.filetype); // (Cannot create DiskSample input source)
    return -1;
  }
  pcodec->opened = 1;

  return 0;
}

static int Quality_FromFrequency(int freq)
{
  int val = 1000000/freq;
  int res = val & 7;

  val = val - res;
  if (res > 3) val += 8;
  if (val <= 24) return 24;
  if (val >= 72) return 72;

  return val;
}

/**
 * Starts or pauses the replay.
 *
 * @param  p      Pointer to the codec's data.
 * @param  pause  1 to pause, 0 to resume.
 */
static void disk_pause(ka_audiocodec_t* p, int pause)
{
  data_t* pcodec = (data_t*) p;

  if (!pcodec->opened) return;
  pcodec->pause = pause;

  if (pause)
    _swix(DiskSample_StreamPause, _IN(0), pcodec->handle);
  else
  {
    int freq, samp_rate, val;

    // Start playing
    _swix(DiskSample_StreamPlay, _IN(0), pcodec->handle);

    // Activate interpolation
    _swix(DiskSample_Configure, _INR(0,1), 1, 1);

    // Try to match output/stream frequencies, with a minimum of 44100 Hz
    _swix(DiskSample_StreamParam, _INR(0,1)|_OUT(2)
         , pcodec->handle, 1
         , &samp_rate);

    // Is 16-bit disabled ?
    _swix(DiskSample_Configure, _INR(0,1)|_OUT(1), 4, -1, &val);
    if (val)
    {
      _swix(DiskSample_Configure, _INR(0,1)|_OUT(1), 0, -1, &val);
      freq = 1000000/val;
      if (freq > samp_rate) freq = samp_rate;
      if (freq < 44100) freq = 44100;
      val = Quality_FromFrequency(freq);
      _swix(DiskSample_Configure, _INR(0,1), 0, val);
    }
    else
    {
      _swix(DiskSample_Configure, _INR(0,1)|_OUT(1), 5, -1, &freq);
      if (freq > samp_rate) freq = samp_rate;
      if (freq < 44100) freq = 44100;
      _swix(DiskSample_Configure, _INR(0,1), 5, freq);
    }
  }
}

/**
 * Stops the replay and decoding,
 * closes the connection with the player module.
 *
 * @param  p     Pointer to the codec's data.
 */
static void disk_stop(ka_audiocodec_t* p)
{
  data_t* pcodec = (data_t*) p;

  if (pcodec->opened)
  {
    _swix(DiskSample_StreamClose, _IN(0), pcodec->handle);
    pcodec->inb = NULL;
    pcodec->opened = 0;
  }
}

/**
 * Tries to update audio information block (parameters and position).
 * Sets info->pts if a new position is provided for audio/video sync.
 *
 * @param  p     Pointer to the codec's data.
 * @param  info  Pointer to the audio information block to update.
 *
 * @returns One or more of the following flags.
 *          AUDIOCODEC_STATE_READY     if the information was successully updated
 *                                     and player is ready to play.
 *          AUDIOCODEC_STATE_NOWORK    if the audio was entirely decoded
 *          AUDIOCODEC_STATE_COMPLETED if the audio was entirely decoded and no future input.
 *          AUDIOCODEC_STATE_INERROR   if the player is in error.
 */
static int disk_getPos(ka_audiocodec_t* p, audio_info_t* info)
{
  const _kernel_oserror* err;
  data_t* pcodec = (data_t*) p;
  unsigned int status;
  int state = 0;

  info->pts = 0;

  // Stream is ready to provide output information ?
  status = 0;
  err = _swix(DiskSample_StreamStatus, _INR(0,2)|_OUT(1)
            , pcodec->handle, 0, 0
            , &status);
  if (err != NULL)
  {
    ka_log(ka_log_error | ka_log_audio, "DiskSample error: %s", err->errmess);
    return AUDIOCODEC_STATE_INERROR;
  }

  if (!(status & 1)) return 0;

  state |= AUDIOCODEC_STATE_READY;

  // Not playing nor paused ?
  if ((((status & 0x33) == 0x33) || ((status & 0x33) == 0x11)) && !pcodec->pause)
  {
    state |= AUDIOCODEC_STATE_NOWORK;
    if (pcodec->inb->finished)
      state |= AUDIOCODEC_STATE_COMPLETED;
  }

  // Get audio parameters
  if (info->channels == 0)
  {
    char* string;

    _swix(DiskSample_StreamTexts, _IN(0)|_OUT(4)
         , pcodec->handle, &string);
    snprintf(info->name, sizeof(info->name), "%s", string);
    _swix(DiskSample_StreamParam, _INR(0,1)|_OUT(2)
         , pcodec->handle, 1
         , &info->samp_rate);
    _swix(DiskSample_StreamParam, _INR(0,1)|_OUT(2)
         , pcodec->handle, 2
         , &info->bitrate);
    _swix(DiskSample_StreamParam, _INR(0,1)|_OUT(2)
         , pcodec->handle, 0
         , &info->channels);

    if (config.debug & cfg_printaudiostats)
      ka_log(ka_log_audio, "Info: %d ch, %d Hz, %d kbits/s"
         , info->channels, info->samp_rate, info->bitrate);
  }

  // Use timing info only if current output not completely played
  // to cope with gaps in audio of corrupted streams
/*  if ((pcodec->version > 55) && !(status & 0x14))
  {
    uint64_t out;
    uint32_t pos;
    err = _swix(DiskSample_StreamPosition, _INR(0,1)|_OUT(1), pcodec->handle, -1, &pos);
    if (err != NULL)
    {
      if (config.debug & (cfg_printwarnings | cfg_printreplaystats))
        ka_log(ka_log_error | ka_log_audio, "DiskSample error: %s", err->errmess);
      return AUDIOCODEC_STATE_INERROR;
    }

    out = pos;
    out *= timer_getrate();
    out /= 1000;
    info->pts = (uint32_t) out;
  }
  else
*/  {
    if (pcodec->pts && !(status & 0x14))
    {
      uint64_t out;

      // Get estimated total remaining time, doesn't cope well with VBR streams
      out = 0;
      err = _swix(DiskSample_StreamDecoding, _INR(0,2), pcodec->handle, 9, &out);
      if (err != NULL)
      {
        if (config.debug & (cfg_printwarnings | cfg_printreplaystats))
          ka_log(ka_log_error | ka_log_audio, "DiskSample error: %s", err->errmess);
        return AUDIOCODEC_STATE_INERROR;
      }
      else
      {
        // Only if still something significant to play
        if (out > 10)
        {
#if 1
          // Correct code
          out *= timer_getrate();
          out /= 1000;

          int64_t delta = (uint32_t)(pcodec->addedsincepts*8);
          delta *= timer_getrate();
          delta /= (uint32_t)(info->bitrate*1000);
          info->pts = pcodec->pts + (int32_t) delta;

          if (info->pts <= out)
            info->pts = 0; // pcodec->pts dropped back to low value ?
          else
            info->pts -= (uint32_t) out;
#else
          // Approximation for StubG <= 0.04
          uint32_t tmp;
          tmp = ((uint32_t) out)*90;
          info->pts = pcodec->pts - tmp;
          tmp = pcodec->addedsincepts*8*90;
          tmp /= info->bitrate;
          info->pts += tmp;
#endif
        }
      }
    }
  }

  if ((status & 0x12) == 0x12)
  {
    if (config.debug & cfg_printaudiostats)
      ka_log(ka_log_audio, "No output");
  }

  return state;
}

/**
 * Tries to push the audio packet into the player's input buffers.
 *
 * @param  p     Pointer to the codec's data.
 * @param  block Pointer to audio block or NULL to signal no more blocks are to follow.
 *
 * @returns EAUDIOCODEC_DECODE_TRYLATER  if the buffer is full and the packet should be added later.
 *          EAUDIOCODEC_DECODE_PROCESSED if the packed was successully added.
 *          EAUDIOCODEC_DECODE_INERROR   if the player is in error.
 */
static ka_eaudiocodec_decode disk_decode(ka_audiocodec_t* p, ka_block_t* block)
{
  data_t* pcodec = (data_t*) p;
  inbuf* inb = pcodec->inb;
  int start = inb->start; // Because value may change in the background
  int len, n;

  if (!block)
  {
    inb->finished = 1;
    pcodec->pts = 0;
    pcodec->translated = NULL;
    return EAUDIOCODEC_DECODE_PROCESSED;
  }
  else
  {
    inb->finished = 0;
  }

  len = block->eod - block->sod;

  // transform packet content
  if (!pcodec->translated)
  {
    pcodec->translated = block;
/*
    if (block->pts)
    {
      uint64_t out;

      out = block->pts / 90ll;
      _swix(DiskSample_StreamDecoding, _INR(0,2), pcodec->handle, 10, &out);
    }
*/
    if ((pcodec->typ == KA_ABLOCK_LPCM_DVD)
    &&  (pcodec->desc.pcm.bitspersample == 24))
    {
      uint8_t tmp[4*3];
      uint8_t* p = (uint8_t*) block->sod;

      for (int i = len - 12; i >= 0; i -= 12, p += 12)
      {
        tmp[0]  = p[0];
        tmp[1]  = p[1];
        tmp[3]  = p[2];
        tmp[4]  = p[3];
        tmp[6]  = p[4];
        tmp[7]  = p[5];
        tmp[9]  = p[6];
        tmp[10] = p[7];
        tmp[2]  = p[8];
        tmp[5]  = p[9];
        tmp[8]  = p[10];
        tmp[11] = p[11];
        memmove(p, tmp, 12);
      }
    }
  }

  n = start - inb->free;
  if (n <= 0)
    n += inb->size;

  // Beware of never filling the buffer completely
  // but split large buffers into smaller ones if they don't fit in
  if (n <= len)
  {
    if (n <= 4*1024)
      return EAUDIOCODEC_DECODE_TRYLATER;
    else
      len = n-1;
  }

  if (config.debug & cfg_printaudiostats)
    ka_log(ka_log_audio, "Decode pts %10u, len %d", block->pts, len);

  start = inb->free;
  // Free part may actually wrap from end to beginning of the buffer
  if (len > (inb->size - start))
    n = inb->size - start;
  else
    n = len;

  // If enough free space inf buffer, read a block of data
  memcpy(&inb->data[start], block->sod, n);
  // Update position of free pointer but use an intermediate variable
  // to avoid temporary corruption of free pointer (AudioMPEG
  // is working in the background and may work on the buffer
  // in the middle of the computation of the new free position
  start += n;
  if (start >= inb->size)
    start = 0;
  len -= n;
  if (len)
  {
    memcpy(&inb->data[start], ((char*) block->sod) + n, len);
    start += len;
  }
  inb->free = start;
  len += n;

  if (block->pts)
  {
    pcodec->pts = block->pts;
    pcodec->addedsincepts = len;
  }
  else
  {
    pcodec->addedsincepts += len;
  }

  // If data was splitted, we will have to process the end of the block later
  if ((block->eod - block->sod) > len)
  {
    block->pts = 0;
    block->sod += len;
    return EAUDIOCODEC_DECODE_TRYLATER;
  }

  pcodec->translated = NULL;
  return EAUDIOCODEC_DECODE_PROCESSED;
}

/**
 * Alters or read the audio volume.
 *
 * @param  p     Pointer to the codec's data.
 * @param  vol   Volume to set [0 - 127] or -1 to just read current value
 *
 * @returns new volume.
 */
static int disk_volume(ka_audiocodec_t* p, int vol)
{
  data_t* pcodec = (data_t*) p;

  if (vol == -1)
  {
    vol = 256;
    _swix(DiskSample_StreamVolume, _INR(0,1)|_OUT(1), pcodec->handle, -1, &vol);
  }
  else
  {
    vol <<= 1;
    _swix(DiskSample_StreamVolume, _INR(0,1)|_OUT(1), pcodec->handle, vol, &vol);
  }

  vol >>= 1;
  if (vol > 127) vol = 127;

  return vol;
}

/**
 * Unlinks from the player module and releases the codec's data.
 *
 * @param  p  Pointer to address of the codec's data.
 */
static void disk_close(ka_audiocodec_t** p)
{
  data_t* pcodec = (data_t*) *p;
  *p = NULL;

  if (!pcodec)
    return;

  disk_stop(&pcodec->hdr);

  ka_mem_free(pcodec);
}

const kav_audiocodec_t kav_disk_codec =
{ .name = "DiskSample"
, .allow  = disk_allow
, .open   = disk_open
, .close  = disk_close
, .start  = disk_start
, .stop   = disk_stop
, .getPos = disk_getPos
, .decode = disk_decode
, .pause  = disk_pause
, .volume = disk_volume
};
