#include <stdio.h>
#include <stdlib.h>
#include <AL/al.h>
#include <AL/alc.h>
#include <assert.h>
#include <stdint.h>
#include <unistd.h>
#include <math.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

#ifndef EMSCRIPTEN_KEEPALIVE
#define EMSCRIPTEN_KEEPALIVE
#endif

extern "C"
{
void EMSCRIPTEN_KEEPALIVE test_finished()
{
#ifdef REPORT_RESULT
  REPORT_RESULT(1);
#endif

}

#if defined(TEST_ALC_SOFT_PAUSE_DEVICE)
  typedef void (*ALC_DEVICE_PAUSE_SOFT)(ALCdevice *);
  typedef void (*ALC_DEVICE_RESUME_SOFT)(ALCdevice *);

  ALC_DEVICE_PAUSE_SOFT alcDevicePauseSOFT;
  ALC_DEVICE_RESUME_SOFT alcDeviceResumeSOFT;
#endif
}

void playSource(void* arg)
{
  ALuint source = static_cast<ALuint>(reinterpret_cast<intptr_t>(arg));
  ALint state;

  alGetSourcei(source, AL_SOURCE_STATE, &state);
  assert(state == AL_PLAYING);
  alSourcePause(source);
  alGetSourcei(source, AL_SOURCE_STATE, &state);
  assert(state == AL_PAUSED);
  alSourcePlay(source);
  alGetSourcei(source, AL_SOURCE_STATE, &state);
  assert(state == AL_PLAYING);
#ifndef TEST_LOOPED_PLAYBACK
  alSourceStop(source);
  alGetSourcei(source, AL_SOURCE_STATE, &state);
  assert(state == AL_STOPPED);
  test_finished();
#endif
}

void main_tick(void *arg)
{
  ALuint source = static_cast<ALuint>(reinterpret_cast<intptr_t>(arg));
  double t = emscripten_get_now() * 0.001;

#if defined(TEST_LOOPED_SEEK_PLAYBACK)
  int offset = 0;
  alGetSourcei(source, AL_SAMPLE_OFFSET, &offset);
  if (offset < 44100 * 3 / 2) {
    alSourcei(source, AL_SAMPLE_OFFSET, 44100 * 3 / 2);
  }
#elif defined(TEST_ANIMATED_LOOPED_PITCHED_PLAYBACK)
  double pitch = sin(t) * 0.5 + 1.0;
  alSourcef(source, AL_PITCH, pitch);
#elif defined(TEST_ANIMATED_LOOPED_DISTANCE_PLAYBACK)
  double pos = (sin(t) - 1.0) * 100.0;
  ALfloat listenerPos[] = {0.0, 0.0, pos};
  alListenerfv(AL_POSITION, listenerPos);
#elif defined(TEST_ANIMATED_LOOPED_DOPPLER_PLAYBACK)
  double vel = sin(t) * (343.3 / 2.0);
  ALfloat listenerVel[] = {0.0, 0.0, vel};
  alListenerfv(AL_VELOCITY, listenerVel);
#elif defined(TEST_ANIMATED_LOOPED_PANNED_PLAYBACK) || defined(TEST_ANIMATED_LOOPED_RELATIVE_PLAYBACK) || defined(TEST_AL_SOFT_SOURCE_SPATIALIZE)
  ALfloat listenerPos[] = {cos(t), 0.0, sin(t)};
  alListenerfv(AL_POSITION, listenerPos);
#elif defined(TEST_ALC_SOFT_PAUSE_DEVICE)
  ALCcontext *ctx = alcGetCurrentContext();
  ALCdevice *dev = alcGetContextsDevice(ctx);
  if (fmod(t, 2.0) < 1.0) {
    alcDeviceResumeSOFT(dev);
  } else {
    alcDevicePauseSOFT(dev);
  }
#endif
}

int main() {
  int major, minor;
  alcGetIntegerv(NULL, ALC_MAJOR_VERSION, 1, &major);
  alcGetIntegerv(NULL, ALC_MINOR_VERSION, 1, &minor);

  assert(major == 1);

  printf("ALC version: %i.%i\n", major, minor);
  printf("Default device: %s\n", alcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER));

  ALCdevice* device = alcOpenDevice(NULL);
#if defined(TEST_ANIMATED_LOOPED_PANNED_PLAYBACK)
  ALCint attrs[] = {0x1992 /* ALC_HRTF_SOFT */, ALC_TRUE, 0x1996 /* ALC_HRTF_ID_SOFT */, 0, 0};
  ALCcontext* context = alcCreateContext(device, attrs);
#else
  ALCcontext* context = alcCreateContext(device, NULL);
#endif
  alcMakeContextCurrent(context);

  assert(alGetString(AL_VERSION));

  printf("OpenAL version: %s\n", alGetString(AL_VERSION));
  printf("OpenAL vendor: %s\n", alGetString(AL_VENDOR));
  printf("OpenAL renderer: %s\n", alGetString(AL_RENDERER));

  ALfloat listenerPos[] = {0.0, 0.0, 1.0};
  ALfloat listenerVel[] = {0.0, 0.0, 0.0};
  ALfloat listenerOri[] = {0.0, 0.0, -1.0, 0.0, 1.0, 0.0};

  alListenerfv(AL_POSITION, listenerPos);
  alListenerfv(AL_VELOCITY, listenerVel);
  alListenerfv(AL_ORIENTATION, listenerOri);

  // check getting and setting global gain
  ALfloat volume;
  alGetListenerf(AL_GAIN, &volume);
  assert(volume == 1.0);
  alListenerf(AL_GAIN, 0.0);
  alGetListenerf(AL_GAIN, &volume);
  assert(volume == 0.0);

  alListenerf(AL_GAIN, 1.0); // reset gain to default

  ALuint buffers[1];

  alGenBuffers(1, buffers);

#ifdef __EMSCRIPTEN__
  FILE* source = fopen("audio.wav", "rb");
#else
  FILE* source = fopen("sounds/audio.wav", "rb");
#endif
  fseek(source, 0, SEEK_END);
  int size = ftell(source);
  fseek(source, 0, SEEK_SET);

  unsigned char* buffer = (unsigned char*) malloc(size);
  fread(buffer, size, 1, source);

  unsigned offset = 12; // ignore the RIFF header
  offset += 8; // ignore the fmt header
  offset += 2; // ignore the format type

  unsigned channels = buffer[offset + 1] << 8;
  channels |= buffer[offset];
  offset += 2;
  printf("Channels: %u\n", channels);

  unsigned frequency = buffer[offset + 3] << 24;
  frequency |= buffer[offset + 2] << 16;
  frequency |= buffer[offset + 1] << 8;
  frequency |= buffer[offset];
  offset += 4;
  printf("Frequency: %u\n", frequency);

  offset += 6; // ignore block size and bps

  unsigned bits = buffer[offset + 1] << 8;
  bits |= buffer[offset];
  offset += 2;
  printf("Bits: %u\n", bits);

  ALenum format = 0;
  if(bits == 8)
  {
    if(channels == 1)
      format = AL_FORMAT_MONO8;
    else if(channels == 2)
      format = AL_FORMAT_STEREO8;
  }
  else if(bits == 16)
  {
    if(channels == 1)
      format = AL_FORMAT_MONO16;
    else if(channels == 2)
      format = AL_FORMAT_STEREO16;
  }

  offset += 8; // ignore the data chunk

  printf("Start offset: %d\n", offset);

  alBufferData(buffers[0], format, &buffer[offset], size - offset, frequency);

#if defined(TEST_AL_SOFT_LOOP_POINTS)
  ALint loopPoints[] = {44100, 44100 * 2};
  ALint alLoopPointsSoft = alGetEnumValue("AL_LOOP_POINTS_SOFT");
  alBufferiv(buffers[0], alLoopPointsSoft, loopPoints);
#endif

  ALint val;
  alGetBufferi(buffers[0], AL_FREQUENCY, &val);
  assert(val == frequency);
  alGetBufferi(buffers[0], AL_SIZE, &val);
  assert(val == size - offset);
  alGetBufferi(buffers[0], AL_BITS, &val);
  assert(val == bits);
  alGetBufferi(buffers[0], AL_CHANNELS, &val);
  assert(val == channels);

  ALuint sources[1];
  alGenSources(1, sources);

  assert(alIsSource(sources[0]));

  alSourcei(sources[0], AL_BUFFER, buffers[0]);

  ALint state;
  alGetSourcei(sources[0], AL_SOURCE_STATE, &state);
  assert(state == AL_INITIAL);

  alSourcePlay(sources[0]);

  alGetSourcei(sources[0], AL_SOURCE_STATE, &state);
  assert(state == AL_PLAYING);

#ifdef TEST_LOOPED_PLAYBACK
  alSourcei(sources[0], AL_LOOPING, AL_TRUE);
#if defined(TEST_LOOPED_SEEK_PLAYBACK)
  printf("You should hear a continuously looping ~1.5 second half of a clip of the 1902 piano song \"The Entertainer\". If you hear a full 3 second clip, the test has failed. Press OK when confirmed.\n");
#elif defined(TEST_ANIMATED_LOOPED_PITCHED_PLAYBACK)
  printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" played back at a dynamic playback rate that smoothly varies its pitch according to a sine wave. Press OK when confirmed.\n");
#elif defined(TEST_ANIMATED_LOOPED_DISTANCE_PLAYBACK)
  alSourcef(sources[0], AL_REFERENCE_DISTANCE, 25.0);
  printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" fade in and out. Press OK when confirmed.\n");
#elif defined(TEST_ANIMATED_LOOPED_DOPPLER_PLAYBACK)
  printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" played back at a dynamic playback rate that smoothly varies its pitch according to a sine wave doppler shift. Press OK when confirmed.\n");
#elif defined(TEST_ANIMATED_LOOPED_PANNED_PLAYBACK)
  printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" smoothly panning around the listener. Press OK when confirmed.\n");
#elif defined(TEST_ANIMATED_LOOPED_RELATIVE_PLAYBACK)
  alSourcei(sources[0], AL_SOURCE_RELATIVE, AL_TRUE);
  printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" centered at the listener. If it is panning, then the test failed. Press OK when confirmed.\n");
#elif defined(TEST_ALC_SOFT_PAUSE_DEVICE)
  alcDevicePauseSOFT = reinterpret_cast<ALC_DEVICE_PAUSE_SOFT>(alcGetProcAddress(device, "alcDevicePauseSOFT"));
  alcDeviceResumeSOFT = reinterpret_cast<ALC_DEVICE_RESUME_SOFT>(alcGetProcAddress(device, "alcDeviceResumeSOFT"));
  assert(alcDevicePauseSOFT && alcDeviceResumeSOFT);
  printf("You should hear a looping clip of the 1902 piano song \"The Entertainer\" That pauses for 1 second every second. Press OK when confirmed.\n");
#elif defined(TEST_AL_SOFT_LOOP_POINTS)
  printf("You should hear a clip of the 1902 piano song \"The Entertainer\" start normally, then begin looping the same 3 notes repeatedly. If you hear the entire clip, then the test failed. Press OK when confirmed.\n");
#elif defined(TEST_AL_SOFT_SOURCE_SPATIALIZE)
  alSourcei(sources[0], 0x1214 /* AL_SOURCE_SPATIALIZE_SOFT */, AL_FALSE);
  printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" centered at the listener. If it is panning, then the test failed. Press OK when confirmed.\n");
#else
  alSourcef(sources[0], AL_PITCH, 1.5f);
  printf("You should hear a continuously looping clip of the 1902 piano song \"The Entertainer\" played back at a high playback rate (high pitch). Press OK when confirmed.\n");
#endif
  EM_ASM(
    var btn = document.createElement('input');
    btn.type = 'button';
    btn.name = btn.value = 'OK';
    btn.onclick = function() {
      _test_finished();
    };
    document.body.appendChild(btn);
  );
#else
  printf("You should hear a short audio clip playing back.\n");
#endif

#ifdef __EMSCRIPTEN__

#if defined(TEST_LOOPED_PLAYBACK)
  emscripten_set_main_loop_arg(main_tick, (void*)sources[0], 0, 0);
#else
  emscripten_async_call(playSource, reinterpret_cast<void*>(sources[0]), 700);
#endif
#else
  usleep(700000);
  playSource(reinterpret_cast<void*>(sources[0]));
#endif

  return 0;
}
