#include "qjs_soloud.h"
#include "qjs_blob.h"

#define STB_HEXWAVE_IMPLEMENTATION
#include "soloud_c.h"
#include <stdlib.h>

#define countof(x) (sizeof(x)/sizeof((x)[0]))

#define FNSIG (JSContext *js, JSValueConst this_val, int argc, JSValue *argv)
#define GETSIG (JSContext *js, JSValueConst this_val)
#define SETSIG (JSContext *js, JSValueConst this_val, JSValue val)

#define JSCLASS(TYPE, FINALIZER) \
static JSClassID js_##TYPE##_class_id; \
static inline TYPE *js2##TYPE(JSContext *js, JSValue v) { \
  return JS_GetOpaque(v, js_##TYPE##_class_id); \
} \
static inline JSValue TYPE##2js(JSContext *js, TYPE *data) { \
  JSValue obj = JS_NewObjectClass(js, js_##TYPE##_class_id); \
  JS_SetOpaque(obj, data); \
  return obj; \
} \
static inline void js_##TYPE##_finalizer(JSRuntime *rt, JSValue val) { \
  FINALIZER(JS_GetOpaque(val, js_##TYPE##_class_id)); \
} \
static JSClassDef js_##TYPE##_class = { \
  #TYPE, \
  .finalizer = js_##TYPE##_finalizer \
}; \

#define JS_GETPROP(C, PROP, VAL, TYPE) \
{ \
  JSValue tmp = JS_GetPropertyStr(js, VAL, #PROP); \
  JS_To##TYPE(js, &C, tmp); \
  JS_FreeValue(js, tmp); \
} \

static double js2number(JSContext *js, JSValue v)
{
  double ret;
  JS_ToFloat64(js, &ret, v);
  return ret;
}

static JSValue number2js(JSContext *js, double num)
{
  return JS_NewFloat64(js, num);
}

static int js2bool(JSContext *js, JSValue v)
{
  int b;
  JS_ToInt32(js, &b, v);
  return b;
}

static JSValue bool2js(JSContext *js, int b)
{
  return JS_NewBool(js,b);
}

typedef unsigned int voice;

static Soloud *soloud;

void voice_free(unsigned int *voice)
{
  Soloud_stop(soloud, *voice);
  free(voice);
}

JSCLASS(Wav, Wav_destroy)
JSCLASS(voice, voice_free)
JSCLASS(Bus, Bus_destroy)

static JSValue js_soloud_make(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  soloud = Soloud_create();
  Soloud_initEx(soloud, SOLOUD_CLIP_ROUNDOFF, SOLOUD_NULLDRIVER, 44100, SOLOUD_AUTO, 2);
  JSValue obj = JS_NewObject(js);
  JS_SetPropertyStr(js, obj, "channels", JS_NewFloat64(js, Soloud_getBackendChannels(soloud)));
  JS_SetPropertyStr(js, obj, "samplerate", JS_NewFloat64(js, Soloud_getBackendSamplerate(soloud)));
  return obj;
}

static JSValue js_soloud_play(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  Wav *wav = js2Wav(js,argv[0]);
  unsigned int *ret = malloc(sizeof(*ret));
  *ret = Soloud_play(soloud, wav);
  JSValue voice = voice2js(js,ret);
  return voice;
}

static JSValue js_soloud_mix(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  int req;
  JS_ToInt32(js, &req, argv[0]);
  float *buf = malloc(2*req*sizeof(float));
  Soloud_mix(soloud, buf, req);
  return js_new_blob_stoned_copy(js, buf, 2*req*sizeof(float));
}

// Create a voice from a WAV file
static JSValue js_load_wav_mem(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  size_t len;
  void *data = js_get_blob_data(js, &len, argv[0]);
  Wav *wav = Wav_create();
  if (Wav_loadMemEx(wav, data, len, 1, 1)) {
    Wav_destroy(wav);
    return JS_ThrowReferenceError(js, "buffer data not wav data");
  }
  return Wav2js(js, wav);
}

// Create a voice from pure PWM data
static JSValue js_load_pwm(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  size_t len;
  void *data = js_get_blob_data(js, &len, argv[0]);
  Wav *wav = Wav_create();
  Wav_loadRawWaveEx(wav, data, len, js2number(js,argv[1]), js2number(js,argv[2]), 1, 1);
  return Wav2js(js, wav);
}

static JSValue js_soloud_profile(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  JSValue prof = JS_NewObject(js);
  JS_SetPropertyStr(js, prof, "active_voices", JS_NewFloat64(js, Soloud_getActiveVoiceCount(soloud)));
  JS_SetPropertyStr(js, prof, "voices", JS_NewFloat64(js, Soloud_getVoiceCount(soloud)));
}

static const JSCFunctionListEntry js_soloud_funcs[] = {
  JS_CFUNC_DEF("init", 3, js_soloud_make),
  JS_CFUNC_DEF("mix", 1, js_soloud_mix),
  JS_CFUNC_DEF("load_pwm", 3, js_load_pwm),
  JS_CFUNC_DEF("load_wav_mem", 1, js_load_wav_mem),
  JS_CFUNC_DEF("play", 1, js_soloud_play),
  JS_CFUNC_DEF("profile", 0, js_soloud_profile),
};

static const JSCFunctionListEntry *js_Wav_funcs;

#define SOLOUD_GETSET(ENTRY, TYPE) \
static JSValue js_voice_set_##ENTRY (JSContext *js, JSValueConst self, JSValue val) { \
  unsigned int voice = *js2voice(js, self); \
  Soloud_set##ENTRY(soloud, voice, js2##TYPE(js, val)); \
  return JS_NULL; \
} \
static JSValue js_voice_get_##ENTRY (JSContext *js, JSValueConst self) { \
  unsigned int voice = *js2voice(js,self); \
  return TYPE##2js(js, Soloud_get##ENTRY(soloud, voice)); \
} \

static JSValue js_voice_seek(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  unsigned int voice = *js2voice(js, self);
  Soloud_seek(soloud, voice, js2number(js, argv[0]));
  return JS_NULL;
}

static JSValue js_voice_stop(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  unsigned int voice = *js2voice(js, self);
  Soloud_stop(soloud, voice);
  return JS_NULL;
}

static JSValue js_voice_setInaudibleBehavior(JSContext *js, JSValueConst self, int argc, JSValue *argv)
{
  unsigned int voice = *js2voice(js, self);
  int mustTick = js2bool(js, argv[0]);
  int kill = js2bool(js, argv[1]);
  Soloud_setInaudibleBehavior(soloud, voice, mustTick, kill);
  return JS_NULL;
}

static JSValue js_voice_valid(JSContext *js, JSValueConst self, int argc, JSValue *argv)
{
  unsigned int voice = *js2voice(js, self);
  return JS_NewBool(js, Soloud_isValidVoiceHandle(soloud, voice));
}

SOLOUD_GETSET(Volume, number);
SOLOUD_GETSET(Pan, number)
SOLOUD_GETSET(Samplerate, number)
SOLOUD_GETSET(RelativePlaySpeed, number)
SOLOUD_GETSET(LoopPoint, number)
SOLOUD_GETSET(Looping, bool)
SOLOUD_GETSET(AutoStop, bool)
SOLOUD_GETSET(ProtectVoice, bool)

static const JSCFunctionListEntry js_voice_funcs[] = {
  JS_CFUNC_DEF("seek", 1, js_voice_seek),
  JS_CFUNC_DEF("stop", 0, js_voice_stop),
  JS_CGETSET_DEF("volume", js_voice_get_Volume, js_voice_set_Volume),
  JS_CGETSET_DEF("pan", js_voice_get_Pan, js_voice_set_Pan),
  JS_CGETSET_DEF("samplerate", js_voice_get_Samplerate, js_voice_set_Samplerate),
  JS_CGETSET_DEF("relativePlaySpeed", js_voice_get_RelativePlaySpeed, js_voice_set_RelativePlaySpeed),
  JS_CGETSET_DEF("loopPoint", js_voice_get_LoopPoint, js_voice_set_LoopPoint),
  JS_CGETSET_DEF("loop", js_voice_get_Looping, js_voice_set_Looping),
  JS_CGETSET_DEF("autoStop", js_voice_get_AutoStop, js_voice_set_AutoStop),
  JS_CGETSET_DEF("protect", js_voice_get_ProtectVoice, js_voice_set_ProtectVoice),    
  JS_CFUNC_DEF("is_valid", 0, js_voice_valid),
};

static const JSCFunctionListEntry *js_Bus_funcs;

#define INITCLASS(TYPE) \
JS_NewClassID(&js_##TYPE##_class_id); \
JS_NewClass(JS_GetRuntime(js), js_##TYPE##_class_id, &js_##TYPE##_class); \
JSValue TYPE##_proto = JS_NewObject(js); \
JS_SetPropertyFunctionList(js, TYPE##_proto, js_##TYPE##_funcs, countof(js_##TYPE##_funcs)); \
JS_SetClassProto(js, js_##TYPE##_class_id, TYPE##_proto); \

JSValue js_soloud_use(JSContext *js)
{
  INITCLASS(Wav)
  INITCLASS(voice)
  INITCLASS(Bus)
  JSValue export = JS_NewObject(js);
  JS_SetPropertyFunctionList(js, export, js_soloud_funcs, sizeof(js_soloud_funcs)/sizeof(JSCFunctionListEntry));
  return export;
}
