// used for syncing audio from Paula writes to tracker visuals

#include <math.h>
#include <stdint.h>
#include <stdbool.h>
#include "pt2_audio.h"
#include "pt2_visuals_sync.h"
#include "pt2_scopes.h"
#include "pt2_visuals.h"
#include "pt2_tables.h"

typedef struct syncVoice_t
{
	const int8_t *newData, *data;
	uint8_t flags, volume;
	uint16_t period;
	uint16_t newLength, length;
} syncVoice_t;

static volatile bool chQueueClearing;
static uint32_t audLatencyPerfValInt;
static uint64_t audLatencyPerfValFrac;
static uint64_t tickTime64, tickTime64Frac;
static uint32_t tickTimeLenInt;
static uint64_t tickTimeLenFrac;
static syncVoice_t syncVoice[PAULA_VOICES];
static chSyncData_t *chSyncEntry;
static chSync_t chSync;

static void startDMA(int32_t ch)
{
	syncVoice_t *sv = &syncVoice[ch];

	if (editor.songPlaying)
	{
		sv->data = sv->newData;
		sv->length = sv->newLength;
		sv->flags |= TRIGGER_SCOPE;
	}
	else
	{
		scope_t *s = &scope[ch];
		s->data = sv->newData;
		s->length = sv->newLength * 2;
		scopeTrigger(ch);
	}
}

static void stopDMA(int32_t ch)
{
	if (editor.songPlaying)
		syncVoice[ch].flags |= STOP_SCOPE;
	else
		scope[ch].active = false;
}

void setVisualsDMACON(uint16_t bits)
{
	if (bits & 0x8000)
	{
		// set
		if (bits & 1) startDMA(0);
		if (bits & 2) startDMA(1);
		if (bits & 4) startDMA(2);
		if (bits & 8) startDMA(3);
	}
	else
	{
		// clear
		if (bits & 1) stopDMA(0);
		if (bits & 2) stopDMA(1);
		if (bits & 4) stopDMA(2);
		if (bits & 8) stopDMA(3);
	}
}

void setVisualsVolume(int32_t ch, uint16_t vol)
{
	int32_t realVol = vol & 127;
	if (realVol > 64)
		realVol = 64;

	if (editor.songPlaying)
	{
		syncVoice_t *sv = &syncVoice[ch];

		sv->volume = (uint8_t)realVol;
		sv->flags |= SET_SCOPE_VOLUME;
	}
	else
	{
		scope[ch].volume = (uint8_t)realVol;
	}
}

void setVisualsPeriod(int32_t ch, uint16_t period)
{
	if (period == 0)
		period = 65535; // On Amiga: period 0 = one full cycle with period 65536, then period 65535 for the rest
	else if (period < 113)
		period = 113; // close to what happens on real Amiga (and needed for BLEP synthesis)

	if (editor.songPlaying)
	{
		syncVoice_t *sv = &syncVoice[ch];

		sv->period = period;
		sv->flags |= SET_SCOPE_PERIOD;
	}
	else
	{
		scopeSetPeriod(ch, period);
	}
}

void setVisualsLength(int32_t ch, uint16_t len)
{
	syncVoice_t *sv = &syncVoice[ch];

	sv->newLength = len;

	if (editor.songPlaying)
		sv->flags |= SET_SCOPE_LENGTH;
	else
		scope[ch].newLength = len * 2;
}

void setVisualsDataPtr(int32_t ch, const int8_t *src)
{
	syncVoice_t *sv = &syncVoice[ch];

	if (src == NULL)
		src = paulaGetNullSamplePtr();

	sv->newData = src;

	if (editor.songPlaying)
		sv->flags |= SET_SCOPE_DATA;
	else
		scope[ch].newData = src;
}

void calcAudioLatencyVars(int32_t audioBufferSize, int32_t audioFreq)
{
	if (audioFreq == 0)
		return;

	const double dAudioLatencyTime = (audioBufferSize / (double)audioFreq) * (double)hpcFreq.freq64;
	double dAudioLatencyTimeInt, dAudioLatencyTimeFrac = modf(dAudioLatencyTime, &dAudioLatencyTimeInt);

	audLatencyPerfValInt = (uint32_t)dAudioLatencyTimeInt;
	audLatencyPerfValFrac = (uint64_t)(dAudioLatencyTimeFrac * TICK_TIME_FRAC_SCALE);
}

void setSyncTickTimeLen(uint32_t timeLenInt, uint64_t timeLenFrac)
{
	tickTimeLenInt = timeLenInt;
	tickTimeLenFrac = timeLenFrac;
}

void resetChSyncQueue(void)
{
	chSync.data[0].timestamp = 0;
	chSync.writePos = 0;
	chSync.readPos = 0;
}

static int32_t chQueueReadSize(void)
{
	while (chQueueClearing);

	if (chSync.writePos > chSync.readPos)
		return chSync.writePos - chSync.readPos;
	else if (chSync.writePos < chSync.readPos)
		return chSync.writePos - chSync.readPos + SYNC_QUEUE_LEN + 1;
	else
		return 0;
}

static int32_t chQueueWriteSize(void)
{
	int32_t size;

	if (chSync.writePos > chSync.readPos)
	{
		size = chSync.readPos - chSync.writePos + SYNC_QUEUE_LEN;
	}
	else if (chSync.writePos < chSync.readPos)
	{
		chQueueClearing = true;

		/* Buffer is full, reset the read/write pos. This is actually really nasty since
		** read/write are two different threads, but because of timestamp validation it
		** shouldn't be that dangerous.
		** It will also create a small visual stutter while the buffer is getting filled,
		** though that is barely noticable on normal buffer sizes, and it takes a minute
		** or two at max BPM between each time (when queue size is default, 8191)
		*/
		chSync.data[0].timestamp = 0;
		chSync.readPos = 0;
		chSync.writePos = 0;

		size = SYNC_QUEUE_LEN;

		chQueueClearing = false;
	}
	else
	{
		size = SYNC_QUEUE_LEN;
	}

	return size;
}

bool chQueuePush(chSyncData_t t)
{
	if (!chQueueWriteSize())
		return false;

	ASSERT(chSync.writePos <= SYNC_QUEUE_LEN);
	chSync.data[chSync.writePos] = t;
	chSync.writePos = (chSync.writePos + 1) & SYNC_QUEUE_LEN;

	return true;
}

static bool chQueuePop(void)
{
	if (!chQueueReadSize())
		return false;

	chSync.readPos = (chSync.readPos + 1) & SYNC_QUEUE_LEN;
	ASSERT(chSync.readPos <= SYNC_QUEUE_LEN);

	return true;
}

static chSyncData_t *chQueuePeek(void)
{
	if (!chQueueReadSize())
		return NULL;

	ASSERT(chSync.readPos <= SYNC_QUEUE_LEN);
	return &chSync.data[chSync.readPos];
}

static uint64_t getChQueueTimestamp(void)
{
	if (!chQueueReadSize())
		return 0;

	ASSERT(chSync.readPos <= SYNC_QUEUE_LEN);
	return chSync.data[chSync.readPos].timestamp;
}

void fillVisualsSyncBuffer(void)
{
	chSyncData_t chSyncData;

	if (audio.resetSyncTickTimeFlag)
	{
		audio.resetSyncTickTimeFlag = false;

		tickTime64 = SDL_GetPerformanceCounter() + audLatencyPerfValInt;
		tickTime64Frac = audLatencyPerfValFrac;
	}

	if (song != NULL)
	{
		moduleChannel_t *ch = song->channels;
		syncVoice_t *sv = syncVoice;
		syncedChannel_t *sc = chSyncData.channels;

		for (int32_t i = 0; i < PAULA_VOICES; i++, ch++, sc++, sv++)
		{
			sc->flags = sv->flags | ch->syncFlags;
			ch->syncFlags = sv->flags = 0; // clear sync flags

			sc->volume = sv->volume;
			sc->period = sv->period;
			sc->data = sv->data;
			sc->length = sv->length;
			sc->newData = sv->newData;
			sc->newLength = sv->newLength;
			sc->vuVolume = ch->syncVuVolume;
			sc->analyzerVolume = ch->syncAnalyzerVolume;
			sc->analyzerPeriod = ch->syncAnalyzerPeriod;
		}

		chSyncData.timestamp = tickTime64;
		chQueuePush(chSyncData);
	}

	tickTime64 += tickTimeLenInt;

	tickTime64Frac += tickTimeLenFrac;
	if (tickTime64Frac >= TICK_TIME_FRAC_SCALE)
	{
		tickTime64Frac &= TICK_TIME_FRAC_MASK;
		tickTime64++;
	}
}

void updateChannelSyncBuffer(void)
{
	uint8_t updateFlags[PAULA_VOICES];

	chSyncEntry = NULL;

	*(uint32_t *)updateFlags = 0; // clear all channel update flags (this is needed)

	const uint64_t frameTime64 = SDL_GetPerformanceCounter();

	// handle channel sync queue

	while (chQueueClearing);
	while (chQueueReadSize() > 0)
	{
		if (frameTime64 < getChQueueTimestamp())
			break; // we have no more stuff to render for now

		chSyncEntry = chQueuePeek();
		if (chSyncEntry == NULL)
			break;

		for (int32_t i = 0; i < PAULA_VOICES; i++)
			updateFlags[i] |= chSyncEntry->channels[i].flags; // yes, OR the status

		if (!chQueuePop())
			break;
	}

	/* Extra validation because of possible issues when the buffer is full
	** and positions are being reset, which is not entirely thread safe.
	*/
	if (chSyncEntry != NULL && chSyncEntry->timestamp == 0)
		chSyncEntry = NULL;

	// do actual updates
	if (chSyncEntry != NULL)
	{
		scope_t *s = scope;
		syncedChannel_t *c = chSyncEntry->channels;
		for (int32_t ch = 0; ch < PAULA_VOICES; ch++, s++, c++)
		{
			const uint8_t flags = updateFlags[ch];
			if (flags == 0)
				continue;

			if (flags & SET_SCOPE_VOLUME)
				scope[ch].volume = c->volume;

			if (flags & SET_SCOPE_PERIOD)
				scopeSetPeriod(ch, c->period);

			// the following handling order is important, don't change it!

			if (flags & STOP_SCOPE)
				scope[ch].active = false;

			if (flags & TRIGGER_SCOPE)
			{
				s->data = c->data;
				s->length = c->length * 2;
				scopeTrigger(ch);
			}

			if (flags & SET_SCOPE_DATA)
				scope[ch].newData = c->newData;

			if (flags & SET_SCOPE_LENGTH)
				scope[ch].newLength = c->newLength * 2;

			// ---------------------------------------------------------------

			if (flags & UPDATE_SPECTRUM_ANALYZER)
				updateSpectrumAnalyzer(c->analyzerVolume, c ->analyzerPeriod);

			if (flags & UPDATE_VUMETER) // for fake VU-meters only
			{
				if (c->vuVolume <= 64)
					editor.vuMeterVolumes[ch] = vuMeterHeights[c->vuVolume];
			}
		}
	}
}
