#include <AL/al.h>
#include <AL/alc.h>
#include <vorbis/vorbisfile.h>
#include <SDL.h>

#include "llua.h"
#include "lluadef.h"
#include "misn_lua.h"

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

#define MUSIC_STOPPED   (1<<1)
#define MUSIC_PLAYING   (1<<2)
#define MUSIC_KILL      (1<<9)
#define music_is(f)     (music_state  & f)
#define music_set(f)    (music_state |= f)
#define music_rm(f)     (music_state ^= f)

#define MUSIC_PREFIX    "../snd/music/"
#define MUSIC_SUFFIX    ".ogg"

#define BUFFER_SIZE     (4096 * 8)

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

#define musicLock()     SDL_mutexP(music_vorbis_lock)
#define musicUnlock()   SDL_mutexV(music_vorbis_lock)

#define MUSIC_LUA_PATH  "../snd/music.lua"

/* Gobal sound mutex. */
extern SDL_mutex* sound_lock;

/* Global music lua. */
static lua_State* music_lua = NULL;
/* Functions. */
static int musicL_load(lua_State* L);
static int musicL_play(lua_State* L);
static int musicL_stop(lua_State* L);
static int musicL_get(lua_State* L);
static const luaL_reg music_methods[] = {
  { "load",   musicL_load },
  { "play",   musicL_play },
  { "stop",   musicL_stop },
  { "get",    musicL_get  },
  {0, 0}
};

/* Saves the music to ram in this structure. */
typedef struct alMusic_ {
  char name[64]; /* Name. */
  Packfile file;
  OggVorbis_File stream;
  vorbis_info* info;
  ALenum format;
} alMusic;

/* Song currently playing. */
static SDL_mutex* music_vorbis_lock;
static alMusic music_vorbis;
static ALuint music_buffer[2]; /* Front and back buffer. */
static ALuint music_source = 0;

/* What is available? */
static char** music_selection = NULL;
static int nmusic_selection = 0;

/* Volume */
static ALfloat mvolume = 1.;

/* Vorbis suff. */
static size_t ovpack_read(void* ptr, size_t size,
                          size_t nmemb, void* datasource) {
  return (ssize_t) pack_read(datasource, ptr, size*nmemb);
}

static int ovpack_retneg(void) { return -1; } /* Must return -1. */
static int ovpack_retzero(void) { return 0; } /* Must return 0. */
ov_callbacks ovcall = {
  .read_func  = ovpack_read,
  .seek_func  = (int(*)(void*, ogg_int64_t, int)) ovpack_retneg,
  .close_func = (int(*)(void*))ovpack_retzero,
  .tell_func = (long(*)(void*))ovpack_retneg
};

/* Music stuff. */
static int stream_loadBuffer(ALuint buffer);
static int music_find(void);
static int music_loadOGG(const char* filename);
static void music_free(void);
/* Lua stuff. */
static int music_luaInit(void);
static void music_luaQuit(void);

/* The thread. */
static unsigned int music_state = 0;
int music_thread(void* unused) {
  (void)unused;

  int active; /* Active buffer. */
  ALint state;

  /* Main loop. */
  while(!music_is(MUSIC_KILL)) {
    if(music_is(MUSIC_PLAYING)) {
      if(music_vorbis.file.end == 0)
        music_rm(MUSIC_PLAYING);
      else {
        music_rm(MUSIC_STOPPED);

        musicLock(); /* Lock the mutex. */
        soundLock();

        /* Start playing current song. */
        active = 0; /* Load first buffer. */
        if(stream_loadBuffer(music_buffer[active])) music_rm(MUSIC_PLAYING);
        alSourceQueueBuffers(music_source, 1, &music_buffer[active]);

        /* Start playing with buffer laoaded. */
        alSourcePlay(music_source);

        active = 1; /* Load the second buffer. */
        if(stream_loadBuffer(music_buffer[active])) music_rm(MUSIC_PLAYING);
        alSourceQueueBuffers(music_source, 1, &music_buffer[active]);

        soundUnlock();

        active = 0;
      }
      while(music_is(MUSIC_PLAYING)) {
        soundLock();

        alGetSourcei(music_source, AL_BUFFERS_PROCESSED, &state);

        if(state > 0) {
          /* Refill active buffer. */
          alSourceUnqueueBuffers(music_source, 1, &music_buffer[active]);
          if(stream_loadBuffer(music_buffer[active])) music_rm(MUSIC_PLAYING);
          alSourceQueueBuffers(music_source, 1, &music_buffer[active]);

          active = 1 - active;
        }
        soundUnlock();

        SDL_Delay(0);
      }
      soundLock();

      alSourceStop(music_source);
      alSourceUnqueueBuffers(music_source, 2, music_buffer);

      soundUnlock();
      musicUnlock();
    }
    music_set(MUSIC_STOPPED);
    SDL_Delay(0); /* We must not kill resources. */
  }
  return 0;
}

static int stream_loadBuffer(ALuint buffer) {
  int size, section, result;
  char dat[BUFFER_SIZE]; /* Buffer to hold the data. */

  size = 0;
  while(size < BUFFER_SIZE) {
    /* Fill up the entire data buffer. */
    result = ov_read(
          &music_vorbis.stream,   /* Stream. */
          dat + size,             /* Data. */
          BUFFER_SIZE - size,     /* Amount to read. */
          0,                      /* Big endian?. */
          2,                      /* 16 bit. */
          1,                      /* Signed. */
          &section);              /* Current bitstream. */

    if(result == 0) return 1;
    else if(result == OV_HOLE) {
      WARN("OGG: Vorbis hole detected in music!");
      return 0;
    }
    else if(result == OV_EBADLINK) {
      WARN("OGG: Invalid stream section or corrupt link in music!");
      return -1;
    }

    size += result;
    if(size == BUFFER_SIZE) break; /* Buffer is full. */
  }
  /* Load the buffer. */
  alBufferData(buffer, music_vorbis.format, dat, BUFFER_SIZE,
               music_vorbis.info->rate);

  return 0;
}

/* Init/Exit. */
int music_init(void) {
  music_vorbis_lock = SDL_CreateMutex();
  music_vorbis.file.end = 0; /* Indication that it's not loaded.. */

  soundLock();

  alGenBuffers(2, music_buffer);
  alGenSources(1, &music_source);
  alSourcef(music_source, AL_GAIN, mvolume);
  alSourcef(music_source, AL_ROLLOFF_FACTOR, 0.);
  alSourcei(music_source, AL_SOURCE_RELATIVE, AL_FALSE);

  /* Start the lua music stuff. */
  music_luaInit();

  soundUnlock();

  return 0;
}

int music_makeList(void) {
  return music_find();
}

void music_exit(void) {
  int i;

  /* Free the music. */
  alDeleteBuffers(2, music_buffer);
  alDeleteSources(1, &music_source);
  music_free();

  /* Free selection */
  for(i = 0; i < nmusic_selection; i++)
    free(music_selection[i]);
  free(music_selection);

  /* Bye bye Lua. */
  music_luaQuit();

  SDL_DestroyMutex(music_vorbis_lock);
}

/* Internal music loading ruitines. */
static int music_loadOGG(const char* filename) {
  /* Free currently loaded ogg. */
  music_free();

  musicLock();

  /* Set the new name. */
  strncpy(music_vorbis.name, filename, 64);
  music_vorbis.name[64-1] = '\0';

  /* Load the new ogg. */
  pack_open(&music_vorbis.file, DATA, filename);
  ov_open_callbacks(&music_vorbis.file, &music_vorbis.stream, NULL, 0, ovcall);
  music_vorbis.info = ov_info(&music_vorbis.stream, -1);

  if(music_vorbis.info->channels == 1) music_vorbis.format = AL_FORMAT_MONO16;
  else music_vorbis.format = AL_FORMAT_STEREO16;

  musicUnlock();

  return 0;
}

static int music_find(void) {
  char** files;
  uint32_t nfiles, i;
  char tmp[64];
  int len;

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

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

      /* Grow the selection size. */
      music_selection = realloc(music_selection,++nmusic_selection*sizeof(char*));

      /* Remove the prefix and suffix. */
      len = strlen(files[i]) - strlen(MUSIC_SUFFIX MUSIC_PREFIX);
      strncpy(tmp, files[i]  + strlen(MUSIC_PREFIX), len);
      tmp[MIN(len, 64-1)] = '\0';

      music_selection[nmusic_selection-1] = strdup(tmp);
    }
  /* Free the char* allocated by pack. */
  for(i = 0; i < nfiles; i++)
    free(files[i]);
  free(files);

  DEBUG("Loaded %d song%c", nmusic_selection, (nmusic_selection==1)?' ':'s');

  return 0;
}

static void music_free(void) {
  musicLock();

  if(music_vorbis.file.end != 0) {
    ov_clear(&music_vorbis.stream);
    pack_close(&music_vorbis.file);
    music_vorbis.file.end = 0; /* Somewhat ended. */
  }

  musicUnlock();
}

void music_volume(const double vol) {
  if(sound_lock == NULL) return;

  /* Sanity check! */
  ALfloat fvol = ABS(vol);
  if(fvol > 1.) fvol = 1.;

  mvolume = fvol;

  /* Only needed if playing! */
  if(music_set(MUSIC_PLAYING)) {
    soundLock();
    alSourcef(music_source, AL_GAIN, fvol);
    soundUnlock();
  }
}

/* Music control functions. */
void music_load(const char* name) {
  if(sound_lock == NULL) return;

  int i;
  char tmp[64];

  music_stop();
  while(!music_is(MUSIC_STOPPED)) SDL_Delay(0);

  for(i = 0; i < nmusic_selection; i++)
    if(strcmp(music_selection[i], name)==0) {
      snprintf(tmp, 64, MUSIC_PREFIX"%s"MUSIC_SUFFIX, name);
      music_loadOGG(tmp);
      return;
    }
  WARN("Requested load song '%s' but it can't be found in the music stack",name);
}

void music_play(void) {
  if(!music_is(MUSIC_PLAYING)) music_set(MUSIC_PLAYING);
}

void music_stop(void) {
  if(music_is(MUSIC_PLAYING)) music_rm(MUSIC_PLAYING);
}

void music_kill(void) {
  if(!music_is(MUSIC_KILL)) music_set(MUSIC_KILL);
}

/* Music lua stuff. */

/* Initialize. */
static int music_luaInit(void) {
  char* buf;
  uint32_t bufsize;

  if(music_lua != NULL)
    music_luaQuit();

  music_lua = llua_newState();

  /*luaL_openlibs(music_lua); */

  lua_loadSpace(music_lua, 1);  /* Space and time are readonly. */
  lua_loadTime(music_lua, 1);
  lua_loadRnd(music_lua);
  lua_loadVar(music_lua, 1);    /* Also read only. */
  lua_loadMusic(music_lua, 0);  /* Write it. */

  /* Load the actual lua music code. */
  buf = pack_readfile(DATA, MUSIC_LUA_PATH, &bufsize);
  if(luaL_dobuffer(music_lua, buf, bufsize, MUSIC_LUA_PATH) != 0) {
    ERR("Error loading music file: %s" MUSIC_LUA_PATH);
    ERR("%s", lua_tostring(music_lua, -1));
    WARN("Most likely lua file has improper syntax, please check");
    return -1;
  }
  free(buf);

  return 0;
}

/* Destroy. */
static void music_luaQuit(void) {
  if(music_lua == NULL)
    return;

  lua_close(music_lua);
  music_lua = NULL;
}

int lua_loadMusic(lua_State* L, int read_only) {
  (void)read_only; /* Future proof. */
  luaL_register(L, "music", music_methods);
  return 0;
}

int music_choose(char* situation) {
  if(sound_lock == NULL) return 0;

  lua_getglobal(music_lua, "choose");
  lua_pushstring(music_lua, situation);
  if(lua_pcall(music_lua, 1, 0, 0)) {
    /* Error occured. */
    WARN("Error while choosing music: %s", (char*)lua_tostring(music_lua, -1));
    return -1;
  }
  return 0;
}

/* The music lua functions. */
static int musicL_load(lua_State* L) {
  char* str;

  /* Check parameters. */
  LLUA_MIN_ARGS(1);
  if(lua_isstring(L, 1)) str = (char*)lua_tostring(L, 1);
  else LLUA_INVALID_PARAMETER();

  music_load(str);
  return 0;
}

static int musicL_play(lua_State* L) {
  (void)L;
  music_play();
  return 0;
}

static int musicL_stop(lua_State* L) {
  (void)L;
  music_stop();
  return 0;
}

static int musicL_get(lua_State* L) {
  musicLock();
  lua_pushstring(L, music_vorbis.name);
  musicUnlock();

  return 1;
}