#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;
  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 state;
  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. */
    state = -1;
    if(voice->source != 0)
      alGetSourcei(voice->source, AL_SOURCE_STATE, &state);

    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(state != 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 state;

  if(voice->source != 0) { /* Source must exist. */
    /* Stop it if playing. */
    alGetSourcei(voice->source, AL_SOURCE_STATE, &state);
    if(state == 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) {
  if(voice == NULL) return;

  voice->buffer = buffer;
  voice_parseFlags(voice, flags);

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

/* Stop playing sound. */
void voice_stop(alVoice* voice) {
  if(voice == NULL) return;

  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 state;

  /* Must have buffer. */
  if(voice->buffer != 0) {
    alGetSourcei(voice->source, AL_SOURCE_STATE, &state);
    if(state == 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();
}

