#include "alutInternal.h"
#include <ctype.h>

/****************************************************************************/

typedef enum
{
  LittleEndian,
  BigEndian,
  UnknwonEndian                 /* has anybody still a PDP11? :-) */
} Endianess;

/* test from Harbison & Steele, "C - A Reference Manual", section 6.1.2 */
static Endianess
endianess (void)
{
  union
  {
    long l;
    char c[sizeof (long)];
  } u;

  u.l = 1;
  return (u.c[0] == 1) ? LittleEndian :
    ((u.c[sizeof (long) - 1] == 1) ? BigEndian : UnknwonEndian);
}

/****************************************************************************/

static int
safeToLower (int c)
{
  return isupper (c) ? tolower (c) : c;
}

static int
hasSuffixIgnoringCase (const char *string, const char *suffix)
{
  const char *stringPointer = string;
  const char *suffixPointer = suffix;

  if (suffix[0] == '\0')
    {
      return 1;
    }

  while (*stringPointer != '\0')
    {
      stringPointer++;
    }

  while (*suffixPointer != '\0')
    {
      suffixPointer++;
    }

  if (stringPointer - string < suffixPointer - suffix)
    {
      return 0;
    }

  while (safeToLower (*--suffixPointer) == safeToLower (*--stringPointer))
    {
      if (suffixPointer == suffix)
        {
          return 1;
        }
    }

  return 0;
}

static BufferData *
loadWavFile (InputStream *stream)
{
  ALboolean found_header = AL_FALSE;
  UInt32LittleEndian chunkLength;
  Int32BigEndian magic;
  UInt16LittleEndian audioFormat;
  UInt16LittleEndian numChannels;
  UInt32LittleEndian sampleFrequency;
  UInt32LittleEndian byteRate;
  UInt16LittleEndian blockAlign;
  UInt16LittleEndian bitsPerSample;
  Codec *codec = _alutCodecLinear;

  if (!_alutInputStreamReadUInt32LE (stream, &chunkLength) ||
      !_alutInputStreamReadInt32BE (stream, &magic))
    {
      return NULL;
    }

  if (magic != 0x57415645)      /* "WAVE" */
    {
      _alutSetError (ALUT_ERROR_UNSUPPORTED_FILE_SUBTYPE);
      return NULL;
    }

  while (1)
    {
      if (!_alutInputStreamReadInt32BE (stream, &magic) ||
          !_alutInputStreamReadUInt32LE (stream, &chunkLength))
        {
          return NULL;
        }

      if (magic == 0x666d7420)  /* "fmt " */
        {
          found_header = AL_TRUE;

          if (chunkLength < 16)
            {
              _alutSetError (ALUT_ERROR_CORRUPT_OR_TRUNCATED_DATA);
              return NULL;
            }

          if (!_alutInputStreamReadUInt16LE (stream, &audioFormat) ||
              !_alutInputStreamReadUInt16LE (stream, &numChannels) ||
              !_alutInputStreamReadUInt32LE (stream, &sampleFrequency) ||
              !_alutInputStreamReadUInt32LE (stream, &byteRate) ||
              !_alutInputStreamReadUInt16LE (stream, &blockAlign) ||
              !_alutInputStreamReadUInt16LE (stream, &bitsPerSample))
            {
              return NULL;
            }

          if (!_alutInputStreamSkip (stream, chunkLength - 16))
            {
              return NULL;
            }

          switch (audioFormat)
            {
            case 1:            /* PCM */
              codec = (bitsPerSample == 8
                       || endianess () ==
                       LittleEndian) ? _alutCodecLinear : _alutCodecPCM16;
              break;
            case 7:            /* uLaw */
              bitsPerSample *= 2;       /* ToDo: ??? */
              codec = _alutCodecULaw;
              break;
            default:
              _alutSetError (ALUT_ERROR_UNSUPPORTED_FILE_SUBTYPE);
              return NULL;
            }
        }
      else if (magic == 0x64617461)     /* "data" */
        {
          ALvoid *data;
          if (!found_header)
            {
              /* ToDo: A bit wrong to check here, fmt chunk could come later... */
              _alutSetError (ALUT_ERROR_CORRUPT_OR_TRUNCATED_DATA);
              return NULL;
            }
          data = _alutInputStreamRead (stream, chunkLength);
          if (data == NULL)
            {
              return NULL;
            }
          return codec (data, chunkLength, numChannels, bitsPerSample,
                        (ALfloat) sampleFrequency);
        }
      else
        {
          if (!_alutInputStreamSkip (stream, chunkLength))
            {
              return NULL;
            }
        }

      if ((chunkLength & 1) && !_alutInputStreamEOF (stream)
          && !_alutInputStreamSkip (stream, 1))
        {
          return NULL;
        }
    }
}

static BufferData *
loadAUFile (InputStream *stream)
{
  Int32BigEndian dataOffset;    /* byte offset to data part, minimum 24 */
  Int32BigEndian len;           /* number of bytes in the data part, -1 = not known */
  Int32BigEndian encoding;      /* encoding of the data part, see AUEncoding */
  Int32BigEndian sampleFrequency;       /* number of samples per second */
  Int32BigEndian numChannels;   /* number of interleaved channels */
  size_t length;
  Codec *codec;
  char *data;
  ALint bitsPerSample;

  if (!_alutInputStreamReadInt32BE (stream, &dataOffset) ||
      !_alutInputStreamReadInt32BE (stream, &len) ||
      !_alutInputStreamReadInt32BE (stream, &encoding) ||
      !_alutInputStreamReadInt32BE (stream, &sampleFrequency) ||
      !_alutInputStreamReadInt32BE (stream, &numChannels))
    {
      return AL_FALSE;
    }

  length = (len == -1) ?
    (_alutInputStreamGetRemainingLength (stream) - AU_HEADER_SIZE -
     dataOffset) : (size_t) len;

  if (!
      (dataOffset >= AU_HEADER_SIZE && length > 0 && sampleFrequency >= 1
       && numChannels >= 1))
    {
      _alutSetError (ALUT_ERROR_CORRUPT_OR_TRUNCATED_DATA);
      return AL_FALSE;
    }

  if (!_alutInputStreamSkip (stream, dataOffset - AU_HEADER_SIZE))
    {
      return AL_FALSE;
    }

  switch (encoding)
    {
    case AU_ULAW_8:
      bitsPerSample = 16;
      codec = _alutCodecULaw;
      break;
    case AU_PCM_8:
      bitsPerSample = 8;
      codec = _alutCodecPCM8s;
      break;
    case AU_PCM_16:
      bitsPerSample = 16;
      codec =
        (endianess () == BigEndian) ? _alutCodecLinear : _alutCodecPCM16;
      break;
    case AU_ALAW_8:
      bitsPerSample = 16;
      codec = _alutCodecALaw;
      break;
    default:
      _alutSetError (ALUT_ERROR_UNSUPPORTED_FILE_SUBTYPE);
      return AL_FALSE;
    }

  data = _alutInputStreamRead (stream, length);
  if (data == NULL)
    {
      return NULL;
    }
  return codec (data, length, numChannels, bitsPerSample,
                (ALfloat) sampleFrequency);
}

static BufferData *
loadRawFile (InputStream *stream)
{
  size_t length = _alutInputStreamGetRemainingLength (stream);
  ALvoid *data = _alutInputStreamRead (stream, length);
  if (data == NULL)
    {
      return NULL;
    }
  /* Guesses */
  return _alutCodecLinear (data, length, 1, 8, 8000);
}

static BufferData *
loadFile (InputStream *stream)
{
  const char *fileName;
  Int32BigEndian magic;

  /* Raw files have no magic number - so use the fileName extension */

  fileName = _alutInputStreamGetFileName (stream);
  if (fileName != NULL && hasSuffixIgnoringCase (fileName, ".raw"))
    {
      return loadRawFile (stream);
    }

  /* For other file formats, read the quasi-standard four byte magic number */
  if (!_alutInputStreamReadInt32BE (stream, &magic))
    {
      return AL_FALSE;
    }

  /* Magic number 'RIFF' == Microsoft '.wav' format */
  if (magic == 0x52494646)
    {
      return loadWavFile (stream);
    }

  /* Magic number '.snd' == Sun & Next's '.au' format */
  if (magic == 0x2E736E64)
    {
      return loadAUFile (stream);
    }

  _alutSetError (ALUT_ERROR_UNSUPPORTED_FILE_TYPE);
  return AL_FALSE;
}

ALuint
_alutCreateBufferFromInputStream (InputStream *stream)
{
  BufferData *bufferData;
  ALuint buffer;

  if (stream == NULL)
    {
      return AL_NONE;
    }

  bufferData = loadFile (stream);
  _alutInputStreamDestroy (stream);
  if (bufferData == NULL)
    {
      return AL_NONE;
    }

  buffer = _alutPassBufferData (bufferData);
  _alutBufferDataDestroy (bufferData);

  return buffer;
}

ALuint
alutCreateBufferFromFile (const char *fileName)
{
  InputStream *stream;
  if (!_alutSanityCheck ())
    {
      return AL_NONE;
    }
  stream = _alutInputStreamConstructFromFile (fileName);
  return _alutCreateBufferFromInputStream (stream);
}

ALuint
alutCreateBufferFromFileImage (const ALvoid *data, ALsizei length)
{
  InputStream *stream;
  if (!_alutSanityCheck ())
    {
      return AL_NONE;
    }
  stream = _alutInputStreamConstructFromMemory (data, length);
  return _alutCreateBufferFromInputStream (stream);
}

void *
_alutLoadMemoryFromInputStream (InputStream *stream, ALenum *format,
                                ALsizei *size, ALfloat *frequency)
{
  BufferData *bufferData;
  ALenum fmt;
  void *data;

  if (stream == NULL)
    {
      return NULL;
    }

  bufferData = loadFile (stream);
  if (bufferData == NULL)
    {
      _alutInputStreamDestroy (stream);
      return NULL;
    }
  _alutInputStreamDestroy (stream);

  if (!_alutGetFormat (bufferData, &fmt))
    {
      _alutBufferDataDestroy (bufferData);
      return NULL;
    }

  if (size != NULL)
    {
      *size = (ALsizei) _alutBufferDataGetLength (bufferData);
    }

  if (format != NULL)
    {
      *format = fmt;
    }

  if (frequency != NULL)
    {
      *frequency = _alutBufferDataGetSampleFrequency (bufferData);
    }

  data = _alutBufferDataGetData (bufferData);
  _alutBufferDataDetachData (bufferData);
  _alutBufferDataDestroy (bufferData);
  return data;
}

ALvoid *
alutLoadMemoryFromFile (const char *fileName, ALenum *format,
                        ALsizei *size, ALfloat *frequency)
{
  InputStream *stream;
  if (!_alutSanityCheck ())
    {
      return NULL;
    }
  stream = _alutInputStreamConstructFromFile (fileName);
  return _alutLoadMemoryFromInputStream (stream, format, size, frequency);
}

ALvoid *
alutLoadMemoryFromFileImage (const ALvoid *data, ALsizei length,
                             ALenum *format, ALsizei *size,
                             ALfloat *frequency)
{
  InputStream *stream;
  if (!_alutSanityCheck ())
    {
      return NULL;
    }
  stream = _alutInputStreamConstructFromMemory (data, length);
  return _alutLoadMemoryFromInputStream (stream, format, size, frequency);
}

/*
  Yukky backwards compatibility crap.
*/

void
alutLoadWAVFile (ALbyte *fileName, ALenum *format, void **data, ALsizei *size,
                 ALsizei *frequency
#if !defined(__APPLE__)
                 , ALboolean *loop
#endif
  )
{
  InputStream *stream;
  ALfloat freq;

  /* Don't do an _alutSanityCheck () because it's not required in ALUT 0.x.x */

  stream = _alutInputStreamConstructFromFile (fileName);
  *data = _alutLoadMemoryFromInputStream (stream, format, size, &freq);
  if (*data == NULL)
    {
      return;
    }

  if (frequency)
    {
      *frequency = (ALsizei) freq;
    }

#if !defined(__APPLE__)
  if (loop)
    {
      *loop = AL_FALSE;
    }
#endif
}

void
alutLoadWAVMemory (ALbyte *buffer, ALenum *format, void **data, ALsizei *size,
                   ALsizei *frequency
#if !defined(__APPLE__)
                   , ALboolean *loop
#endif
  )
{
  InputStream *stream;
  ALfloat freq;

  /* Don't do an _alutSanityCheck () because it's not required in ALUT 0.x.x */

  /* ToDo: Can we do something less insane than passing 0x7FFFFFFF? */
  stream = _alutInputStreamConstructFromMemory (buffer, 0x7FFFFFFF);
  _alutLoadMemoryFromInputStream (stream, format, size, &freq);
  if (*data == NULL)
    {
      return;
    }

  if (frequency)
    {
      *frequency = (ALsizei) freq;
    }

#if !defined(__APPLE__)
  if (loop)
    {
      *loop = AL_FALSE;
    }
#endif
}

void
alutUnloadWAV (ALenum UNUSED (format), ALvoid *data, ALsizei UNUSED (size),
               ALsizei UNUSED (frequency))
{
  /* Don't do an _alutSanityCheck () because it's not required in ALUT 0.x.x */

  free (data);
}

const char *
alutGetMIMETypes (ALenum loader)
{
  if (!_alutSanityCheck ())
    {
      return NULL;
    }

  /* We do not distinguish the loaders yet... */
  switch (loader)
    {
    case ALUT_LOADER_BUFFER:
      return "audio/basic,audio/x-raw,audio/x-wav";

    case ALUT_LOADER_MEMORY:
      return "audio/basic,audio/x-raw,audio/x-wav";

    default:
      _alutSetError (ALUT_ERROR_INVALID_ENUM);
      return NULL;
    }
}
