/* wav.c */
/* Uses Replay codecs to play WAV from file or memory without MovieFS */

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

#include "swis.h"

#include "errors.h"
#include "generic.h"
#include "log.h"
#include "mstream.h"
#include "osfstream.h"
#include "wav.h"
#include "xstr.h"

typedef struct {
  fmstream fm;
  int (*read_word16le)(fmstream, fms_t16 *);
  int (*read_word32le)(fmstream, int *);
  int (*seek)(fmstream, long int);
  int (*close)(fmstream);
  long int (*tell)(fmstream);
  long int fmt_off, data_off;
  int fmt_size, data_size;
  char wavl;
} wav_data;

#define CHUNKIDSTR(c1, c2, c3, c4) (c1 + (c2 << 8) + (c3 << 16) + (c4 << 24))

enum wav_chunk_id {
  wav_cid_RIFF = CHUNKIDSTR('R', 'I', 'F', 'F'),
  wav_cid_WAVE = CHUNKIDSTR('W', 'A', 'V', 'E'),
  wav_cid_fmt  = CHUNKIDSTR('f', 'm', 't', ' '),
  wav_cid_data = CHUNKIDSTR('d', 'a', 't', 'a'),
  wav_cid_wavl = CHUNKIDSTR('w', 'a', 'v', 'l')
};

/* Parses file to find format and data chunks */
static _kernel_oserror *
wav_open(wav_data *wav, const char *file, size_t size)
{
  int id;
  long int offset;
  int csize;
  if (size)
  {
    wav->fm.mp = mstream_open(file, size);
    if (!wav->fm.mp)
    {
      LOG(("wav_open couldn't open mstream\n"));
      return &error_memory;
    }
    wav->read_word16le = mstream_read_word16le;
    wav->read_word32le = mstream_read_word32le;
    wav->seek = mstream_seek;
    wav->close = mstream_close;
    wav->tell = mstream_tell;
  }
  else
  {
    _swix(OS_File, _INR(0,1)|_OUT(4), 5, file, &size);
    wav->fm.fp = osfstream_open(file);
    if (!wav->fm.fp)
    {
      LOG(("wav_open couldn't open osfstream\n"));
      return &error_file;
    }
    wav->read_word16le = osfstream_read_word16le;
    wav->read_word32le = osfstream_read_word32le;
    wav->seek = osfstream_seek;
    wav->close = osfstream_close;
    wav->tell = osfstream_tell;
  }
  LOG(("File size %d\n", size));
  wav->fmt_off = wav->data_off = 0;
  wav->wavl = 0;
  if (wav->read_word32le(wav->fm, &id))
  {
    LOG(("wav_open failed reading RIFF id\n"));
    wav->close(wav->fm);
    return &error_file;
  }
  if (id != wav_cid_RIFF)
  {
    LOG(("wav_open: First word isn't 'RIFF'\n"));
    wav->close(wav->fm);
    return &error_bad_wav;
  }
  else
  {
    LOG(("wav_open: Starts with 'RIFF' OK\n"));
  }
  if (wav->read_word32le(wav->fm, &csize))
  {
    LOG(("wav_open failed reading master chunk size\n"));
    wav->close(wav->fm);
    return &error_file;
  }
  LOG(("Master chunk size %d\n", csize));
  if (csize + 8 != size)
  {
    LOG(("wav_open: Master chunk size doesn't match file size\n"));
  }
  else
  {
    LOG(("wav_open: Master chunk size matches file size OK\n"));
  }
  if (wav->read_word32le(wav->fm, &id))
  {
    LOG(("wav_open failed reading 'WAVE' identifier\n"));
    wav->close(wav->fm);
    return &error_file;
  }
  if (id != wav_cid_WAVE)
  {
    LOG(("wav_open: 'WAVE' identifier missing\n"));
    wav->close(wav->fm);
    return &error_bad_wav;
  }
  else
  {
    LOG(("wav_open: 'WAVE' identifier found OK\n"));
  }
  offset = 12;
  size = csize + 8;
  while (offset < size)
  {
  #ifdef MAKELOG
    char idstr[5];
  #endif
    if (wav->seek(wav->fm, offset))
    {
      LOG(("wav_open unable to seek to next chunk\n"));
      wav->close(wav->fm);
      return &error_file;
    }
    if (wav->read_word32le(wav->fm, &id))
    {
      LOG(("wav_open failed reading chunk id\n"));
      wav->close(wav->fm);
      return &error_file;
    }
  #ifdef MAKELOG
    *((int *) idstr) = id;
    idstr[4] = 0;
    LOG(("wav_open found chunk with id '%s'\n", idstr));
  #endif
    if (wav->read_word32le(wav->fm, &csize))
    {
      LOG(("wav_open failed reading chunk size\n"));
      wav->close(wav->fm);
      return &error_file;
    }
    LOG(("wav_open: Chunk size %d\n", csize));
    if (id == wav_cid_fmt)
    {
      wav->fmt_off = offset;
      wav->fmt_size = csize;
    }
    if (id == wav_cid_data)
    {
      wav->data_off = offset;
      wav->data_size = csize;
    }
    if (id == wav_cid_wavl)
    {
      wav->wavl = 1;
    }
    offset += (long) WORDALIGN16((long) csize) + 8l;
  }
  return 0;
}

/* Return codes as for THSound_CheckWAV, plus file error */
typedef enum {
  wav_check_OK,
  wav_check_CodecMissing,
  wav_check_TooManyChannels,
  wav_check_TooManyBits,
  wav_check_UnknownCompression,
  wav_check_SilentChunks,
  wav_check_FileError
} wav_check_codes;
static int
wav_get_codec(wav_data *wav, char const **name,
              int *channels_out, int *sample_rate_out,
              int *bits_per_sample_out)
{
  _kernel_oserror *e;
  int cot;
  static char codec[20];
  char codec_path[40] = "<ARMovie$SoundDir>.";
  fms_t16 comp_code, channels, bits_per_sample;
  int sample_rate;
  if (wav->seek(wav->fm, wav->fmt_off + 8))
  {
    LOG(("wav_get_codec couldn't seek to fmt chunk\n"));
    return wav_check_FileError;
  }
  if (wav->read_word16le(wav->fm, &comp_code))
  {
    LOG(("wav_get_codec couldn't read compression code\n"));
    return wav_check_FileError;
  }
  LOG(("Compression code %d (0x%x)\n", comp_code, comp_code));
  if (wav->read_word16le(wav->fm, &channels))
  {
    LOG(("wav_get_codec couldn't read number of channels\n"));
    return wav_check_FileError;
  }
  LOG(("%d channels\n", channels));
  if (channels_out)
    *channels_out = channels;
  if (wav->read_word32le(wav->fm, &sample_rate))
  {
    LOG(("wav_get_codec couldn't read sample rate\n"));
    return wav_check_FileError;
  }
  LOG(("Sample rate %d\n", sample_rate));
  if (sample_rate_out)
    *sample_rate_out = sample_rate;
  if (wav->seek(wav->fm, wav->fmt_off + 0x16))
  {
    LOG(("wav_get_codec couldn't seek to bits per sample\n"));
    return wav_check_FileError;
  }
  if (wav->read_word16le(wav->fm, &bits_per_sample))
  {
    LOG(("wav_get_codec couldn't read bits per sample\n"));
    return wav_check_FileError;
  }
  LOG(("Bits per sample %d\n", bits_per_sample));
  if (bits_per_sample_out)
    *bits_per_sample_out = bits_per_sample;
  switch (comp_code)
  {
    case 1:
      strcpy(codec, "Sound");
      if (bits_per_sample <= 8)
        strcat(codec, "U8");
      else
        sprintf(codec + sizeof("Sound") - 1, "S%d", BYTEROUND(bits_per_sample));
      break;
    case 2:
      strcpy(codec, "msadpcm.play");
      break;
    case 6:
      strcpy(codec, "Alaw.play");
      break;
    case 7:
      strcpy(codec, "MuLaw.play");
      break;
    default:
      return wav_check_UnknownCompression;
  }
  if (channels != 1)
    sprintf(codec + strlen(codec), "x%d", channels);
  LOG(("Codec name %s\n", codec));
  if (name)
    *name = codec;
  /* Does the codec exist? */
  strcat(codec_path, codec);
  e = _swix(OS_File, _INR(0,1)|_OUT(0), 5, codec_path, &cot);
  if (e)
  {
    LOG(("OS_File error checking for codec: %s\n", e->errmess));
    return wav_check_CodecMissing;
  }
  if (cot == 1)
    return wav_check_OK;
  LOG(("Codec object type %d is not a file\n", cot));
  /* Find out why it's missing */
  if (comp_code == 1 && bits_per_sample > 16)
  {
    LOG(("Too many bits per sample\n"));
    return wav_check_TooManyBits;
  }
  if (channels != 1 && channels != 2)
  {
    LOG(("Too many channels\n"));
    return wav_check_TooManyChannels;
  }
  return wav_check_CodecMissing;
}

_kernel_oserror *
wav_check(const char *file, size_t size, int *return_code)
{
  wav_data wav;
  _kernel_oserror *e;
  LOG(("\nwav_check on "));
#ifdef MAKELOG
  if (size)
  {
    LOG(("WAV in memory of size %d\n", size));
  }
  else
  {
    LOG(("file '%s'\n", file));
  }
#endif
  e = wav_open(&wav, file, size);
  if (e)
  {
    *return_code = wav_check_FileError;
    return 0;
  }
  *return_code = wav_get_codec(&wav, 0, 0, 0, 0);
  wav.close(wav.fm);
  return 0;
}

_kernel_oserror *
wav_read_chunks(const char *file, size_t size,
                replay_handle *handle, const char **codec_out,
                const char **sndrep_out, int *fps, int *fpc,
                int *channels, int *multiplier, int *chunk_size)
{
  wav_data wav;
  int samp_rate;
  _kernel_oserror *e;
  const char *codec;
  static char sndrep[16];
  LOG(("wav_read_chunks playing "));
#ifdef MAKELOG
  if (size)
  {
    LOG(("WAV in memory of size %d\n", size));
  }
  else
  {
    LOG(("file '%s'\n", file));
  }
#endif
  replay_remove_current();
  e = replay_get_handle(handle);
  if (e)
    return e;
  e = wav_open(&wav, file, size);
  if (e)
    return e;
  if (wav_get_codec(&wav, &codec, channels, &samp_rate, multiplier))
  {
    wav.close(wav.fm);
    return &error_bad_wav;
  }
  replay_master.fm = wav.fm;
  e = generic_prepare_already_open(wav.data_size, wav.data_off - 8,
                                   size == 0,
                                   codec, chunk_size);
  if (e)
    return e;
  xstr_toupper((char *) codec);
  if (strstr(codec, "ADPCM"))
  {
    LOG(("ADPCM, converting multiplier to 16\n"));
    *multiplier = 16;
  }
  *codec_out = replay_master.decomp;
  sprintf(sndrep, "%d", samp_rate);
  *sndrep_out = sndrep;
  *fps = *fpc = 1;
  return 0;
}
