#include <sys/stat.h>
#include <AL/alc.h>
#include <AL/alut.h>
#include <SDL.h>
#include <SDL_thread.h>

#include "lephisto.h"
#include "log.h"
#include "pack.h"
#include "music.h"
#include "sound.h"

// ==============================================
// sound.c controls the routines for using a
// virtual voice wrapper system around the openal
// library to get 3D sound.
//
// We only use position sound and no doppler effect
// right now.
// ==============================================

// ==============================================
// Sound Overview:
// ---------------
//
// We use a priority virtual voice system with
// pre-allocated buffers.
//
// Nameing:
//    -- buffer - Sound sample.
//    -- source - openal object that plays sound.
//    -- voice  - Virtual object that wants to play sound.
//
// First we allocate all the buffers based on what
// we find inside the datafile.
// Then we allocate all the possible sources (giving
// the music system what it needs).
// Now we allow the user to dynamically create
// voices, these voices will always try to grab
// a source from the source pool. If they can't,
// they will pretend to play the buffer.
// Every so often we'll check to see if the important
// voices are being played and take away the sources
// from the lesser ones.
// ==============================================

// Sound parameters - TODO: make it variable per source.
#define SOUND_ROLLOFF_FACTOR  1.
#define SOUND_REFERENCE_DIST  500.
#define SOUND_MAX_DIST        1000.


#define SOUND_PREFIX  "../snd/sounds/"
#define SOUND_SUFFIX  ".wav"

#define soundLock()   SDL_mutexP(sound_lock)
#define soundUnlock() SDL_mutexV(sound_lock)

// Give the buffers a name.
typedef struct alSound_ {
  char* name;   // Buffers name.
  ALuint buffer; // Associated OpenAL buffer.
} alSound;

// Voice private flags (public in sound.h).
#define VOICE_PLAYING   (1<<0)  // Voice is playing.
#define VOICE_DONE      (1<<1)  // Voice is done - must remove.
#define voice_set(v,f) ((v)->flags |= f)
#define voice_is(v,f)  ((v)->flags & f)

// Global sound lock.
SDL_mutex* sound_lock = NULL;

// Gobal device and context.
static ALCcontext* al_context = NULL;
static ALCdevice* al_device = NULL;

// Threads.
static SDL_Thread* music_player = NULL;

// List of sounds available (All preloaded into a buffer).
static alSound* sound_list = NULL;
static int nsound_list = 0;

// Struct to hold all the sources and currently attached voice.
static ALuint* source_stack = NULL; // And it's stack.
static int source_nstack = 0;

// Virtual voice.
struct alVoice {
  alVoice* next;      // Yes it's a linked list.

  //ALuint id;        // Unique id for the voice.

  ALuint source;      // Source itself, 0 if not set.
  ALuint buffer;      // Buffer.

  int priority;       // Base priority.

  double px, py;      // Position.
  //double vx, vy;    // Velocity.

  unsigned int start; // time started in ms.
  unsigned int flags; // Flags to set properties.
};
static alVoice* voice_start = NULL;
static alVoice* voice_end   = NULL;

// Volume.
static ALfloat svolume = 0.3;

static int  sound_makeList(void);
static int  sound_load(ALuint* buffer, char* filename);
static void sound_free(alSound* snd);
static int  voice_getSource(alVoice* voc);
static void voice_init(alVoice* voice);
static int  voice_play(alVoice* voice);
static void voice_rm(alVoice* prev, alVoice* voice);
static void voice_parseFlags(alVoice* voice, const unsigned int flags);

int sound_init(void) {
  int mem, ret = 0;
  ALenum err;

  ret = 0;
  
  // We'll need a mutex.
  sound_lock = SDL_CreateMutex();
  soundLock();

  // Initialize alut - I think it's worth it.
  alutInitWithoutContext(NULL, NULL);

  const ALchar* device = alcGetString(NULL, ALC_DEFAULT_DEVICE_SPECIFIER);
  // Open the default device.
  al_device = alcOpenDevice(NULL);
  if(al_device == NULL) {
    WARN("Unable to open default sound device");
    ret =  -1;
    goto snderr_dev;
  }

  // Create the OpenAL context.
  al_context = alcCreateContext(al_device, NULL);
  if(sound_lock == NULL) {
    WARN("Unable to create OpenAL context");
    ret = -2;
    goto snderr_ctx;
  }

  // Clear the errors.
  alGetError();

  // Set active context.
  if(alcMakeContextCurrent(al_context)==AL_FALSE) {
    WARN("Failure to set default context");
    ret = -4;
    goto snderr_act;
  }

  // Set the master gain.
  alListenerf(AL_GAIN, .1);

  // Set the distance model.
  alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);

  // We can unlock now.
  soundUnlock();

  // Start the music server.
  music_init();

  // Start allocating the sources - music has already taken this.
  alGetError(); // Another error clear.
  mem = 0;
  while(((err = alGetError()) == AL_NO_ERROR) && (source_nstack < 128)) {
    if(mem < source_nstack+1) {
      // Allocate more memory.
      mem += 32;
      source_stack = realloc(source_stack, sizeof(ALuint) * mem);
    }
    alGenSources(1, &source_stack[source_nstack]);
    source_nstack++;
  }
  
  // Use minimal ram.
  source_stack = realloc(source_stack, sizeof(ALuint) * source_nstack);

  // Debug magic.
  DEBUG("OpenAL:    %s", device);
  DEBUG("Sources:   %d", source_nstack);
  DEBUG("Renderer:  %s", alGetString(AL_RENDERER));
  DEBUG("Version:   %s", alGetString(AL_VERSION));

  // Load up all the sounds.
  sound_makeList();
  music_makeList(); // And music.

  // Now start the music thread.
  music_player = SDL_CreateThread(music_thread, NULL);

  return 0;

snderr_act:
  alcDestroyContext(al_context);
snderr_ctx:
  al_context = NULL;
  alcCloseDevice(al_device);
snderr_dev:
  al_device = NULL;
  soundUnlock();
  SDL_DestroyMutex(sound_lock);
  sound_lock = NULL;
  ERR("Sound failed to initialize.");
  return ret;
}


// Clean up after the sound system.
void sound_exit(void) {
  int i;
  // Free the sounds.
  for(i = 0; i < nsound_list; i++)
    sound_free(&sound_list[i]);
  free(sound_list);
  sound_list = NULL;
  nsound_list = 0;

  // Must stop the music before killing it,
  // then thread should commit suicide.
  if(music_player) {
    music_stop();
    music_kill();
    SDL_WaitThread(music_player, NULL);
    music_exit();
  }

  // Clean up the voices.
  while(voice_start != NULL)
    voice_rm(NULL, voice_start);

  // Clean up the sources.
  if(source_stack)
    alDeleteSources(source_nstack, source_stack);

  if(sound_lock) {
    soundLock();

    if(al_context) {
      alcMakeContextCurrent(NULL);
      alcDestroyContext(al_context);
    }
    if(al_device) alcCloseDevice(al_device);

    soundUnlock();
    SDL_DestroyMutex(sound_lock);
  }
  // Cya alut!
  alutExit();
}

// Get the buffer to sound of [name].
ALuint sound_get(char* name) {
  if(sound_lock == NULL) return 0;

  int i;
  for(i = 0; i < nsound_list; i++)
    if(strcmp(name, sound_list[i].name)==0)
      return sound_list[i].buffer;
  WARN("Sound '%s' not found in sound list", name);
  return 0;
}

// Make list of available sounds.
static int sound_makeList(void) {
  if(sound_lock == NULL) return 0;

  char** files;
  uint32_t nfiles, i;
  char tmp[64];
  int len;
  int mem;

  // Get the file list.
  files = pack_listfiles(data, &nfiles);

  // Load the profiles.
  mem = 0;
  for(i = 0; i < nfiles; i++)
    if((strncmp(files[i], SOUND_PREFIX, strlen(SOUND_PREFIX))==0) &&
       (strncmp(files[i] + strlen(files[i]) - strlen(SOUND_SUFFIX),
                SOUND_SUFFIX, strlen(SOUND_SUFFIX))==0)) {

      // Expand the selection size.
      nsound_list++;
      if(nsound_list > mem) {
        // We must grow.
        mem += 32;  // We'll overallocate most likely.
        sound_list = realloc(sound_list, mem*sizeof(alSound));
      }

      // Remove the prefix and suffix.
      len = strlen(files[i]) - strlen(SOUND_SUFFIX SOUND_PREFIX);
      strncpy(tmp, files[i] + strlen(SOUND_PREFIX), len);
      tmp[len] = '\0';

      // give it the new name.
      sound_list[nsound_list-1].name = strdup(tmp);
      sound_load(&sound_list[nsound_list-1].buffer, files[i]);
    }

  // Shrink to minimum ram usage.
  sound_list = realloc(sound_list, nsound_list*sizeof(alSound));

  // Free the char* allocated by pack.
  for(i = 0; i < nfiles; i++)
    free(files[i]);
  free(files);

  DEBUG("Loaded %d sound%s", nsound_list, (nsound_list==1)?"":"s");

  return 0;
}

// Loads a sound into the sound_list.
static int sound_load(ALuint* buffer, char* filename) {
  if(sound_lock == NULL) return 0;

  void* wavdata;
  unsigned int size;
  ALenum err;

  // Get the file data buffer from the packfile.
  wavdata = pack_readfile(DATA, filename, &size);

  soundLock();

  // Bind to OpenAL buffer.
  (*buffer) = alutCreateBufferFromFileImage(wavdata, size);
  if((*buffer) == AL_NONE) WARN("FAILURE: %s", alutGetErrorString(alutGetError()));
  //alGenBuffers(1, buffer);
  //alBufferData(*buffer, AL_FORMAT_MONO16, wavdata, size, 22050);

  // Errors?
  if((err = alGetError()) != AL_NO_ERROR) {
    WARN("OpenAL erro '%d' loading sound '%s'.", err, filename);
    return 0;
  }

  soundUnlock();

  // Finish up.
  free(wavdata);
  return 0;
}

static void sound_free(alSound* snd) {
  if(sound_lock) return;

  soundLock();

  // Free the stuff.
  if(snd->name) free(snd->name);
  alDeleteBuffers(1, &snd->buffer);

  soundUnlock();
}

// Update the sounds and prioritize them.
void sound_update(void) {
  ALint stat;
  alVoice* voice, *prev, *next;

  if(sound_lock == NULL)  return; // Sound system is off.
  if(voice_start == NULL) return; // No voices.

  soundLock();

  // Update sound.
  prev = NULL;
  voice = voice_start;
  do {
    next = voice->next;

    // Get status.
    stat = -1;
    if(voice->source != 0)
      alGetSourcei(voice->source, AL_SOURCE_STATE, &stat);

    if(!voice_is(voice, VOICE_DONE)) { // Still working.
      // Voice has a source.
      if(voice->source != 0) {

        // Update position.
        alSource3f(voice->source, AL_POSITION,
            voice->px, voice->py, 0.);
        /*alSource3f(voice->source, AL_VELOCITY,
            voice->vx, voice->vy, 0.);*/
      }

      prev = voice;
    }else {
      // Delete them.
      if(stat != AL_PLAYING)
        voice_rm(prev, voice); // Do not set prev to voice.
      else
        prev = voice;
    }
    voice = next;
  } while(voice != NULL);
 
  soundUnlock();
}

// Remove a voice.
static void voice_rm(alVoice* prev, alVoice* voice) {
  ALint stat;

  if(voice->source != 0) { // Source must exist.
    // Stop it if playing.
    alGetSourcei(voice->source, AL_SOURCE_STATE, &stat);
    if(stat == AL_PLAYING) alSourceStop(voice->source);

    // Clear it and get rid of it.
    source_stack[source_nstack++] = voice->source; // Throw it back.
  }

  // Delete from linked list.
  if(prev == NULL) // Was the first member.
    voice_start = voice->next;
  else // Not first memmber.
    prev->next = voice->next;
  if(voice_end == voice) // Last voice in linked list.
    voice_end = prev;
  free(voice);
}

// Set all the sounds volume to vol.
void sound_volume(const double vol) {
  if(sound_lock == NULL) return;

  svolume = (ALfloat) vol;
}

// Attempt to alloc a source for a voice.
static int voice_getSource(alVoice* voc) {
  int ret;

  // Sound system isn't on.
  if(sound_lock == NULL) return -1;

  ret = 0; // Default return.

  soundLock();

  // Try and grab a source.
  if(source_nstack > 0) { // We have the source.
    // We must pull it from the free source vector.
    voc->source = source_stack[--source_nstack];

    // Initialize and play.
    voice_init(voc);
    ret = voice_play(voc);
  } else
    voc->source = 0;

  soundUnlock();

  return ret;
}

// Must lock becore calling.
static void voice_init(alVoice* voice) {
  // Distance model.
  alSourcef(voice->source, AL_ROLLOFF_FACTOR, SOUND_ROLLOFF_FACTOR);
  alSourcef(voice->source, AL_MAX_DISTANCE, SOUND_MAX_DIST);
  alSourcef(voice->source, AL_REFERENCE_DISTANCE, SOUND_REFERENCE_DIST);

  alSourcef(voice->source, AL_GAIN, svolume);
  alSource3f(voice->source, AL_POSITION, voice->px, voice->py, 0.);
  //alSource3f(voice->source, AL_VELOCITY, voice->vx, voice->vy, 0.);
  if(voice_is(voice, VOICE_LOOPING))
    alSourcei(voice->source, AL_LOOPING, AL_TRUE);
  else
    alSourcei(voice->source, AL_LOOPING, AL_FALSE);
}

// Create a dynamic moving piece.
alVoice* sound_addVoice(int priority, double px, double py,
    double vx, double vy, const ALuint buffer, const unsigned int flags) {

  (void)vx;
  (void)vy;
  alVoice* voc;

  if(sound_lock == NULL) return NULL;

  // Allocate the voice.
  voc = malloc(sizeof(alVoice));

  // Set the data.
  voc->next = NULL;
  voc->priority = priority;
  voc->start = SDL_GetTicks();
  voc->buffer = buffer;

  // Handle positions.
  voc->px = px;
  voc->py = py;
  //voc->vx = vx;
  //voc->vy = vy;
  
  // Handle the flags.
  voice_parseFlags(voc, flags);

  // Get the source.
  voice_getSource(voc);

  if(voice_start == NULL) {
    voice_start = voc;
    voice_end = voc;
  } else {
    if(voice_end != NULL)
      voice_end->next = voc;
    voice_end = voc;
  }

  return voc;
}

// Delete the voice.
void sound_delVoice(alVoice* voice) {
  if(sound_lock == NULL) return;

  voice_set(voice, VOICE_DONE);
}

// Update voice position, should be run once per frame.
void voice_update(alVoice* voice, double px, double py, double vx, double vy) {
  (void) vx;
  (void) vy;

  if(sound_lock == NULL) return;

  voice->px = px;
  voice->py = py;
  //voice->vx = vx;
  //voice->vy = vy;
}

// Changes the voice's buffer.
void voice_buffer(alVoice* voice, const ALuint buffer, const unsigned int flags) {
  voice->buffer = buffer;
  voice_parseFlags(voice, flags);

  // Start playing.
  soundLock();
  voice_play(voice);
  soundUnlock();
}

// Stop playing sound.
void voice_stop(alVoice* voice) {
  soundLock();
  if(voice->source != 0)
    alSourceStop(voice->source);
  soundUnlock();
}

// Handle flags.
static void voice_parseFlags(alVoice* voice, const unsigned int flags) {
  voice->flags = 0; // Defaults.

  // Looping.
  if(flags & VOICE_LOOPING)
    voice_set(voice, VOICE_LOOPING);

  if(flags & VOICE_STATIC)
    alSourcei(voice->source, AL_SOURCE_RELATIVE, AL_TRUE);
  else
    alSourcei(voice->source, AL_SOURCE_RELATIVE, AL_FALSE);
}

// Make a voice play. Must lock before calling.
static int voice_play(alVoice* voice) {
  ALenum err;
  ALint stat;

  // Must have buffer.
  if(voice->buffer != 0) {
    alGetSourcei(voice->source, AL_SOURCE_STATE, &stat);
    if(stat == AL_PLAYING)
      alSourceStop(voice->source);
    // Set buffer.
    alSourcei(voice->source, AL_BUFFER, voice->buffer);

    // Try to play the source.
    alSourcePlay(voice->source);
    err = alGetError();
    if(err == AL_NO_ERROR) voice_set(voice, VOICE_PLAYING);
    else return 2;
  }

  return 0;
}

void sound_listener(double dir, double px, double py, double vx, double vy) {
  (void)vx;
  (void)vy;

  if(sound_lock == NULL) return;

  soundLock();

  // Set orientation.
  ALfloat ori[] = { 0., 0., 0., 0., 0., 1. };
  ori[0] = cos(dir);
  ori[1] = sin(dir);
  alListenerfv(AL_ORIENTATION, ori);
  alListener3f(AL_POSITION, px, py, 1.);
  //alListener3f(AL_VELOCITY, vx, vy, 0.);

  soundUnlock();
}