/*
 *  Squeezelite - lightweight headless squeezebox emulator
 *
 *  (c) Adrian Smith 2012-2015, triode1@btinternet.com
 *      Ralph Irving 2015-2023, ralph_irving@hotmail.com
 *	Philippe 2018-2023, philippe_44@outlook.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "squeezelite.h"
#if OPUS

/* 
*  with some low-end CPU, the decode call takes a fair bit of time and if the outputbuf is locked during that
*  period, the output_thread (or equivalent) will be locked although there is plenty of samples available.
*  Normally, with PRIO_INHERIT, that thread should increase decoder priority and get the lock quickly but it
*  seems that when the streambuf has plenty of data, the decode thread grabs the CPU to much, even it the output
*  thread has a higher priority. Using an interim buffer where opus decoder writes the output is not great from
*  an efficiency (one extra memory copy) point of view, but it allows the lock to not be kept for too long
*/
#if EMBEDDED
#define FRAME_BUF 2048
#endif

#if BYTES_PER_FRAME == 4		
#define ALIGN(n) 	(n)
#else
#define ALIGN(n) 	(n << 16)		
#endif

#include <opusfile.h>

struct opus {
	struct OggOpusFile *of;
#if FRAME_BUF
	u8_t *write_buf;
#endif
#if !LINKALL
	// opus symbols to be dynamically loaded
	void (*op_free)(OggOpusFile *_of);
	int  (*op_read)(OggOpusFile *_of, opus_int16 *_pcm, int _buf_size, int *_li);
	const OpusHead* (*op_head)(OggOpusFile *_of, int _li);
	OggOpusFile*  (*op_open_callbacks) (void *_source, OpusFileCallbacks *_cb, unsigned char *_initial_data, size_t _initial_bytes, int *_error);
#endif
};

static struct opus *u;

extern log_level loglevel;

extern struct buffer *streambuf;
extern struct buffer *outputbuf;
extern struct streamstate stream;
extern struct outputstate output;
extern struct decodestate decode;
extern struct processstate process;

#define LOCK_S   mutex_lock(streambuf->mutex)
#define UNLOCK_S mutex_unlock(streambuf->mutex)
#define LOCK_O   mutex_lock(outputbuf->mutex)
#define UNLOCK_O mutex_unlock(outputbuf->mutex)
#if PROCESS
#define LOCK_O_direct   if (decode.direct) mutex_lock(outputbuf->mutex)
#define UNLOCK_O_direct if (decode.direct) mutex_unlock(outputbuf->mutex)
#define LOCK_O_not_direct   if (!decode.direct) mutex_lock(outputbuf->mutex)
#define UNLOCK_O_not_direct if (!decode.direct) mutex_unlock(outputbuf->mutex)
#define IF_DIRECT(x)    if (decode.direct) { x }
#define IF_PROCESS(x)   if (!decode.direct) { x }
#else
#define LOCK_O_direct   mutex_lock(outputbuf->mutex)
#define UNLOCK_O_direct mutex_unlock(outputbuf->mutex)
#define LOCK_O_not_direct
#define UNLOCK_O_not_direct
#define IF_DIRECT(x)    { x }
#define IF_PROCESS(x)
#endif

#if LINKALL
#define OP(h, fn, ...) (op_ ## fn)(__VA_ARGS__)
#else
#define OP(h, fn, ...) (h)->op_ ## fn(__VA_ARGS__)
#endif

// called with mutex locked within vorbis_decode to avoid locking O before S
static int _read_cb(void *datasource, char *ptr, int size) {
	size_t bytes;

	while (1) {
		LOCK_S;
		bytes = min(_buf_used(streambuf), _buf_cont_read(streambuf));
		bytes = min(bytes, size);
		if (bytes || stream.state <= DISCONNECT) break;

		UNLOCK_S;
		usleep(50 * 1000);
	}

	memcpy(ptr, streambuf->readp, bytes);
	_buf_inc_readp(streambuf, bytes);

	UNLOCK_S;

	return bytes;
}

static decode_state opus_decompress(void) {
	frames_t frames;
	int n;
	static int channels;
	u8_t *write_buf;

	if (decode.new_stream) {
		struct OpusFileCallbacks cbs;
		const struct OpusHead *info;
		int err;

		cbs.read = (op_read_func) _read_cb;
		cbs.seek = NULL; cbs.tell = NULL; cbs.close = NULL;

		if ((u->of = OP(u, open_callbacks, streambuf, &cbs, NULL, 0, &err)) == NULL) {
			LOG_WARN("open_callbacks error: %d", err);
			return DECODE_COMPLETE;
		}

		info = OP(u, head, u->of, -1);

		LOCK_O;
		output.next_sample_rate = decode_newstream(48000, output.supported_rates);
		IF_DSD(	output.next_fmt = PCM; )
		output.track_start = outputbuf->writep;
		if (output.fade_mode) _checkfade(true);
		decode.new_stream = false;
		UNLOCK_O;

		channels = info->channel_count;

		LOG_INFO("setting track_start");
	}

#if FRAME_BUF
	IF_DIRECT(
		frames = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME;
		frames = min(frames, FRAME_BUF);
		write_buf = u->write_buf;
	);
#else
	LOCK_O_direct;
	IF_DIRECT(
		frames = min(_buf_space(outputbuf), _buf_cont_write(outputbuf)) / BYTES_PER_FRAME;
		write_buf = outputbuf->writep;
	);
#endif
	IF_PROCESS(
		frames = process.max_in_frames;
		write_buf = process.inbuf;
	);
	
	// write the decoded frames into outputbuf then unpack them (they are 16 bits)
	n = OP(u, read, u->of, (opus_int16*) write_buf, frames * channels, NULL);
			
#if FRAME_BUF
	LOCK_O_direct;
#endif

	if (n > 0) {
		frames_t count;
		s16_t *iptr;
		ISAMPLE_T *optr;

		frames = n;
		count = frames * channels;

		// work backward to unpack samples (if needed)
		iptr = (s16_t *) write_buf + count;
		IF_DIRECT(
			optr = (ISAMPLE_T *) outputbuf->writep + frames * 2;
		)
		IF_PROCESS(
			optr = (ISAMPLE_T *) write_buf + frames * 2;
		)
		
		if (channels == 2) {
#if BYTES_PER_FRAME == 4
#if FRAME_BUF
			// copy needed only when DIRECT and FRAME_BUF
			IF_DIRECT(
				memcpy(outputbuf->writep, write_buf, frames * BYTES_PER_FRAME);
			)	
#endif			
#else
			while (count--) {
				*--optr = ALIGN(*--iptr);
			}
#endif
		} else if (channels == 1) {
			while (count--) {
				*--optr = ALIGN(*--iptr);
				*--optr = ALIGN(*iptr);
			}
		}

		IF_DIRECT(
			_buf_inc_writep(outputbuf, frames * BYTES_PER_FRAME);
		);
		IF_PROCESS(
			process.in_frames = frames;
		);

		LOG_SDEBUG("wrote %u frames", frames);

	} else if (n == 0) {

		if (stream.state <= DISCONNECT) {
			LOG_INFO("end of decode");
			UNLOCK_O_direct;
			return DECODE_COMPLETE;
		} else {
			LOG_INFO("no frame decoded");
        }

	} else if (n == OP_HOLE) {

		// recoverable hole in stream, seen when skipping
		LOG_DEBUG("hole in stream");

	} else {

		LOG_INFO("op_read error: %d", n);
		UNLOCK_O_direct;
		return DECODE_COMPLETE;
	}

	UNLOCK_O_direct;

	return DECODE_RUNNING;
}


static void opus_open(u8_t size, u8_t rate, u8_t chan, u8_t endianness) {
	if (!u->of) {
#if FRAME_BUF
		if (!u->write_buf) u->write_buf = malloc(FRAME_BUF * BYTES_PER_FRAME);
#endif
	} else {
		OP(u, free, u->of);
		u->of = NULL;
	}	
}

static void opus_close(void) {
	if (u->of) {
		OP(u, free, u->of);
		u->of = NULL;
	}
#if FRAME_BUF
	free(u->write_buf);
	u->write_buf = NULL;
#endif
}

static bool load_opus(void) {
#if !LINKALL
	void *handle = dlopen(LIBOPUS, RTLD_NOW);
	char *err;

	if (!handle) {
		LOG_INFO("dlerror: %s", dlerror());
		return false;
	}

	u->op_free = dlsym(handle, "op_free");
	u->op_read = dlsym(handle, "op_read");
	u->op_head = dlsym(handle, "op_head");
	u->op_open_callbacks = dlsym(handle, "op_open_callbacks");
	
	if ((err = dlerror()) != NULL) {
		LOG_INFO("dlerror: %s", err);
		return false;
	}

	LOG_INFO("loaded "LIBOPUS);
#endif

	return true;
}

struct codec *register_opus(void) {
	static struct codec ret = {
		'u',          // id
		"ops",        // types
		4*1024,       // min read
		32*1024,       // min space
		opus_open, 	  // open
		opus_close,   // close
		opus_decompress,  // decode
	};

	u = malloc(sizeof(struct opus));
	if (!u) {
		return NULL;
	}

	u->of = NULL;
#if FRAME_BUF
	u->write_buf = NULL;
#endif
	if (!load_opus()) {
		return NULL;
	}

	LOG_INFO("using opus to decode ops");
	return &ret;
}
#endif /* OPUS */

