// TODO: audio stores audio buffers at indicies that the user specifies. (array growth is the next power of two from the idx)
#include "audio.h"

#include <SDL_audio.h>
#include <SDL_error.h>
#include <SDL_stdinc.h>
#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "../error.h"
#include "../util/compat.h"

struct audioplayer {
	uint8_t const* buf;
	int len;
};

struct audiodevice {
	struct audioplayer audio[AUDIO_MAX];
	unsigned id;
	int freq;
	SDL_AudioFormat fmt;
	uint8_t ch; // audio channels
} dev;

/* mixes the audio output stream, using the different audio as sources */
static void audiomixer(void* const userdata, uint8_t* const stream, int const len) {
	(void)userdata;
	memset(stream, 0, len); // clear the playing audio

	// update the counts for the audio array
	for (unsigned i = 0; i < AUDIO_MAX; i++) {
		if (dev.audio[i].len > 0) {
			unsigned mixlen = SDL_min(len, dev.audio[i].len);
			SDL_MixAudioFormat(stream, dev.audio[i].buf, dev.fmt, mixlen, SDL_MIX_MAXVOLUME);
			dev.audio[i].len -= mixlen;
			dev.audio[i].buf += mixlen;
		}
	}
}

/* converts input audio buffer to one that matches the buffer of dev.
 * `len` is a pointer to the current size, the new size will be written to this location.
 * returns the pointer to the audio buffer to use, or NULL, when something went wrong.
 * NULL will never be returned after the conversion */
static uint8_t* audio_cvt(SDL_AudioSpec const* spec, uint8_t* bufptr, unsigned* len) {
	if (!bufptr) return NULL;

	// init the converter
	SDL_AudioCVT cvt;
	if (SDL_BuildAudioCVT(&cvt, spec->format, spec->channels, spec->freq, dev.fmt, dev.ch, dev.freq) < 0) {
		error("could not build the audio converter! SDL Error: %s", SDL_GetError());
		return NULL;
	}
	if (!cvt.needed) return bufptr; // ensure the conversion is necessary

	// grow the inputted buffer for the conversion
	cvt.len = *len;
	cvt.buf = realloc(bufptr, cvt.len * cvt.len_mult);
	if (!cvt.buf) {
		warn("failed to grow the audio buffer to the new size of %u bytes! audio may be bugged", cvt.len);
		return bufptr;
	}

	// convert the audio to the new format
	if (SDL_ConvertAudio(&cvt) < 0) {
		warn("couldn't convert an audio buffer to the set format, audio may be bugged! SDL Error: %s", SDL_GetError());
		assert(cvt.buf);
		return cvt.buf;
	}
	assert(cvt.buf);

	// shrink to reduce memory footprint
	*len = cvt.len_cvt;
	bufptr = realloc(cvt.buf, *len);
	return bufptr ? bufptr : cvt.buf;
}

int audio_device_init(int freq, SDL_AudioFormat fmt, uint8_t channels, uint16_t samples) {
	// define the audio specification
	SDL_AudioSpec spec = {freq, fmt, channels, 0, samples, 0, 0, NULL, NULL};
	spec.callback = audiomixer;
	spec.userdata = NULL;

	unsigned id = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0);

	// create the audio device
	dev = (struct audiodevice){
		{0},
		id,
		freq,
		fmt,
		channels,
	};

	if (id) {
		// default state of the device is paused, so we unpause it here
		SDL_PauseAudioDevice(id, 0);
		return 0;
	} else return 1;
}

void audio_device_free(void) {
	SDL_CloseAudioDevice(dev.id);
	dev = (struct audiodevice){0};
}

void audio_play(audiodata const* audio) {
	if (!dev.id) return;
	if (!audio->buf) return;

	unsigned i = 0;
	while (i < AUDIO_MAX && dev.audio[i].len > 0) i++;
	if (i < AUDIO_MAX)
		dev.audio[i] = (struct audioplayer){audio->buf, audio->len};
}

audiodata audio_wav_load(char const* fpath) {
	debug("loading audio file '%s'...", fpath);

	if (faccess(fpath, FA_R)) {
		error("%s:%u audio file either isn't readable or doesn't exist. path: '%s'!", __FILE_NAME__, __LINE__, fpath);
		return (struct audiodata){0};
	}

	// load the audio
	SDL_AudioSpec spec;
	audiodata audio;
	SDL_LoadWAV(fpath, &spec, &audio.buf, &audio.len);
	if (!audio.buf) return audio;

	// convert the audio data to the format reflecting dev
	void* ptr = audio_cvt(&spec, audio.buf, &audio.len);
	if (!ptr) free(audio.buf); // free the buffer if NULL was returned; failure
	audio.buf = ptr;

	// calculate the time in milliseconds of the audio fragment
	// by dividing the audio bytelength by the format's bitsize, by the audio device's channels and the audio device's frequency
	audio.ms = (((1000 * audio.len) / (SDL_AUDIO_BITSIZE(dev.fmt) / 8)) / dev.ch / dev.freq);

	return audio;
}

void audio_wav_unload(audiodata* audio) {
	free(audio->buf);
	*audio = (audiodata){0}; // zero out all audio data
}
