/* coding: utf-8; */
#include "soundio-class.h"
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>

#define PRINT_STDERR(handle, tag) do {					\
	void *_buf;							\
	if (loop_read_all((handle)->_err, &_buf, NULL, false) > 0) {	\
		fprintf(stderr, "%s: %s\n", (tag), (char *) _buf);	\
		free(_buf);						\
	}								\
} while (0)

#define PRINT_EXIT(handle, tag) do {					\
	if ((handle)->status > 0)					\
		fprintf(stderr,						\
			"%s: daemon exited with a non-zero value: %d\n",\
			(tag), (handle)->status);			\
	else if ((handle)->status < 0)					\
		fprintf(stderr, "%s: daemon was killed by %s\n",	\
			(tag), strsignal(-(handle)->status));		\
} while (0)

bool SoundIn_init(tagSoundIn *in)
{
	memset(in, 0, sizeof(*in));

	return true;
}

bool SoundIn_uninit(tagSoundIn *in)
{

	return true;
}

static int SoundIn_thread(tagSoundIn *in)
{
	int r;
	WorkerHandle *h = &in->handle;

	while (true) {
		void *data;
		size_t nbytes;
		r = loop_read_all(h->_out, &data, &nbytes, true);
		PRINT_STDERR(h, "SoundIn");
		/* usleep(in->record_interval); */

		if (r <= 0)
			break;

		if (in->WaveInCallback)
			in->WaveInCallback(data, nbytes, in->user_data);
		free(data);
	}

	return r;
}

bool SoundIn_start(tagSoundIn *in)
{
	int r;

	WorkerHandle *h = &in->handle;
	worker_handle_init(h);
	h->stop_wait = in->stop_wait;

	if ((r = record_start(h)) < 0) {
		fprintf(stderr,
			"SoundIn: failed to start daemon: %s\n",
			strerror(-r));
		goto error;
	}

	r = pthread_create(&in->thread, NULL, (void *(*)(void *)) SoundIn_thread, in);
	if (r != 0) {
		fprintf(stderr,
			"SoundIn: failed to create thread: %s\n",
			strerror(r));

		if ((r = record_stop(h)) < 0) {
			fprintf(stderr,
				"SoundIn: failed to cleanup daemon: %s\n",
				strerror(-r));
		}

		goto error;
	}

	return true;
error:
	worker_handle_uninit(h);
	return false;
}

/* Should call in *the same* thread as SoundIn_start */
bool SoundIn_stop(tagSoundIn *in)
{
	int r;
	intptr_t retval;
	WorkerHandle *h = &in->handle;

	if ((r = record_stop(h)) < 0) {
		fprintf(stderr,
			"SoundIn: failed to stop daemon: %s\n",
			strerror(-r));

		return false;
	}
	PRINT_STDERR(h,"SoundIn:Record:");
	PRINT_EXIT(h, "SoundIn:Record:");

	if ((r = pthread_join(in->thread, (void **) &retval)) != 0) {
		fprintf(stderr,
			"SoundIn: failed to join thread %s\n",
			strerror(r));

		return false;
	}

	if ((int)retval != 0)
		fprintf(stderr,
			"SoundIn: thread exited abnormally: %s\n",
			strerror(-((int)retval)));

	worker_handle_uninit(h);
	return true;
}

bool SoundOut_init(tagSoundOut *out)
{
	int r;

	memset(out, 0, sizeof(*out));

	if ((r = pthread_mutex_init(&out->mutex, NULL)) != 0) {
		fprintf(stderr, "SoundOut:init: failed to init mutex: %s\n",
			strerror(r));
		return false;
	}

	if ((r = pthread_cond_init(&out->wq_cond, NULL)) != 0) {
		fprintf(stderr, "SoundOut:init: failed to init cond: %s\n",
			strerror(r));
		pthread_mutex_destroy(&out->mutex);

		return false;
	}

	return true;
}

bool SoundOut_uninit(tagSoundOut *out)
{
	int r;

	if ((r = pthread_cond_destroy(&out->wq_cond)) != 0) {
		fprintf(stderr,
			"SoundOut:uninit: failed to destroy cond: %s\n",
			strerror(r));

		return false;
	}

	if ((r = pthread_mutex_destroy(&out->mutex)) != 0) {
		fprintf(stderr,
			"SoundOut:uninit: failed to destroy mutex: %s\n",
			strerror(r));

		return false;
	}

	return true;
}

static int SoundOut_thread(tagSoundOut *out)
{
	int r = 0;

	while (true) {
		WorkerHandle *h = &out->handle;
		void *data, *tag;
		int data_len;

		assert(pthread_mutex_lock(&out->mutex) == 0);
		while (!out->n_works && !out->quit)
			assert(pthread_cond_wait(
				&out->wq_cond, &out->mutex) == 0);

		if (out->quit) {
			assert(pthread_mutex_unlock(&out->mutex) == 0);
			break;
		}

		data = out->ring[out->wq_idx].data;
		data_len = out->ring[out->wq_idx].data_len;
		tag = out->ring[out->wq_idx].tag;
		memset(&out->ring[out->wq_idx], 0, sizeof(out->ring[out->wq_idx]));
		out->wq_idx = (out->wq_idx + 1) % OUT_RING_SZ;
		out->n_works--;
		assert(pthread_mutex_unlock(&out->mutex) == 0);

		worker_handle_init(h);
		h->stop_wait = out->stop_wait;

		if ((r = play_start(h)) < 0) {
			fprintf(stderr, "SoundOut_thread: play_start failed: %s\n", strerror(-r));
		} else {
			siginfo_t siginfo;

			r = loop_write(h->_in, data, data_len, true);
			PRINT_STDERR(h, "SoundOut_thread:Play:");
			if (r < 0)
				fprintf(stderr, "SoundOut_thread: failed to play: %s\n",
					r == -EPIPE ? "already quit" : strerror(-r));
			close(h->_in); /* EOF to notify quit */

			while ((r = worker_check_stopped(h)) == 0)
				waitid(P_PID, h->pid, &siginfo, WEXITED | WNOWAIT);

			if (r == 1) {
				PRINT_STDERR(h, "SoundOut_thread:Play:");
				PRINT_EXIT(h, "SoundOut_thread:Play:");
			} else
				fprintf(stderr, "SoundOut_thread:"
					"failed to check worker stop: %s\n",
					strerror(-r));
		}

		if (out->WaveOutCallback)
			out->WaveOutCallback(tag);
		worker_handle_uninit(h);
		free(data);
	}

	return 0;
}

bool SoundOut_start(tagSoundOut *out)
{
	int r;
	WorkerHandle *mixer = &out->mixer;

	worker_handle_init(mixer);
	mixer->stop_wait = out->stop_wait;
	if ((r = mixer_start(mixer)) < 0) {
		fprintf(stderr,
			"SoundOut:start: failed to start mixer: %s\n",
			strerror(-r));

		goto error;
	}

	out->n_works = 0;
	out->quit = false;
	r = pthread_create(&out->thread, NULL, (void *(*)(void *)) SoundOut_thread, out);
	if (r != 0) {
		fprintf(stderr,
			"SoundOut: failed to create thread: %s\n",
			strerror(r));

		if ((r = mixer_stop(mixer)) < 0) {
			fprintf(stderr,
				"SoundOut: failed to cleanup mixer daemon: %s\n",
				strerror(-r));
		}

		goto error;
	}

	return true;
error:
	worker_handle_uninit(mixer);
	return false;
}

bool SoundOut_stop(tagSoundOut *out)
{
	bool res = true;
	int r, i;
	WorkerHandle *mixer = &out->mixer;
	intptr_t retval;

	if ((r = mixer_stop(mixer)) < 0) {
		fprintf(stderr,
			"SoundOut: failed to stop mixer: %s\n",
			strerror(-r));

		res = false;
	} else {
		PRINT_STDERR(mixer,"SoundOut:Mixer:");
		PRINT_EXIT(mixer, "SoundOut:Mixer");
	}
	worker_handle_uninit(mixer);

	assert(pthread_mutex_lock(&out->mutex) == 0);
	out->quit = true;
	assert(pthread_cond_signal(&out->wq_cond) == 0);
	assert(pthread_mutex_unlock(&out->mutex) == 0);

	if ((r = pthread_join(out->thread, (void **) &retval)) != 0) {
		fprintf(stderr,
			"SoundIn: failed to join thread %s\n",
			strerror(r));

		res = false;
	}

	for (i = 0; i < out->n_works; i++)
		free(out->ring[(out->wq_idx + i) % OUT_RING_SZ].data);

	return res;
}

bool SoundOut_play_data(tagSoundOut *out, void *data, int data_len, void *tag)
{
	bool res = false;
	int next_idx;

	assert(pthread_mutex_lock(&out->mutex) == 0);
	assert(!out->quit);
	if (out->n_works == OUT_RING_SZ) {
		fprintf(stderr,
			"SoundOut: work queue is full(%d items)!\n",
			out->n_works);

		goto quit;
	}

	next_idx = (out->wq_idx + out->n_works) % OUT_RING_SZ;
	out->ring[next_idx].data = data;
	out->ring[next_idx].data_len = data_len;
	out->ring[next_idx].tag = tag;
	out->n_works++;
	assert(pthread_cond_signal(&out->wq_cond) == 0);
	res = true;

quit:
	assert(pthread_mutex_unlock(&out->mutex) == 0);

	return res;
}

bool SoundOut_set_volume(tagSoundOut *out, int _volume)
{
	WorkerHandle *h = &out->mixer;
	int volume = _volume;
	int r;

	if ((r = mixer_set_play_vol(h, &volume)) < 0) {
		fprintf(stderr, "SoundOut:SetVolume: failed to set to %d: %s\n",
			_volume, strerror(-r));

		return false;
	}
	PRINT_STDERR(h, "SoundOut:SetVolume:");

	fprintf(stdout, "SoundOut:SetVolume: set to %d(actually set to %d)\n",
			_volume, volume);
	return true;
}

int SoundOut_get_volume(tagSoundOut *out)
{
	WorkerHandle *h = &out->mixer;
	int r, left, right;

	if ((r = mixer_get_play_vol(h, &left, &right)) < 0) {
		fprintf(stderr, "SoundOut:GetVolume: failed to get volume: %s\n",
			strerror(-r));

		return -1;
	}

	return left;
}

