// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2019 Fredrik Noring
 */

#include <stdlib.h>

#include <asm/interrupt.h>
#include <asm/io.h>
#include <asm/machine.h>
#include <asm/math.h>
#include <asm/mfp.h>
#include <asm/snd/dma.h>
#include <tos/cookie.h>
#include <tos/system-variable.h>

#include "internal/compare.h"
#include "internal/macro.h"
#include "internal/build-assert.h"
#include "internal/check-compiler.h"
#include "internal/types.h"
#include "internal/limits.h"

#include "psgplay/sndh.h"

struct timer_prescale {
	enum mfp_ctrl ctrl;
	uint8_t divisor;
	uint8_t count;
};

struct system_variables *__system_variables = (struct system_variables *)0x400;

static void (*vblqueue[4])(void);

static struct cookie cookie_jar[16] = {
	{ .uid = COOKIE__CPU, .value = COOKIE__CPU_68000 },
	{ .uid = COOKIE__VDO, .value = COOKIE__VDO_STE   },
	{ .uid = COOKIE__MCH, .value = COOKIE__MCH_STE   },
	{ .uid = COOKIE__SWI, .value = 0xff              },
	{ .uid = COOKIE__SND, .value = COOKIE__SND_PSG
				     | COOKIE__SND_DMA8  },
	{ .uid = COOKIE__FPU, .value = COOKIE__FPU_NONE  },
	{ .value = ARRAY_SIZE(cookie_jar) }
};

struct sndh_file file;

static struct {
	enum sndh_timer_type type;
	struct timer_prescale prescale;
	uint8_t dividend;
	bool play;
} timer_state;

static uint32_t gemdos_malloc(const uint32_t amount)
{
	const uint32_t address = (__system_variables->_membot & ~(uint32_t)0xf) + 0x10;

	__system_variables->_membot = address + amount;

	return address;
}

uint32_t gemdos_trap(uint8_t *sp)
{
	switch (*(uint16_t*)sp) {
		case 72:
			return gemdos_malloc(*(uint32_t*)&sp[2]);
	}

	return 0;
}

uint32_t xbios_trap(void)
{
	return 0;
}

static bool timer_prescale(struct timer_prescale *prescale, int frequency)
{
	static const uint8_t prescale_div[] = {
#define MFP_CTRL_DIV_PRESCALE(div) div,
MFP_CTRL_DIV(MFP_CTRL_DIV_PRESCALE)
	};
	bool valid = false;
	int best = -1;

	if (frequency < 1)
		return false;

	const uint32_t f = frequency;

	for (size_t divisor = 1; divisor <= 8; divisor++)
	for (size_t i = 0; i < ARRAY_SIZE(prescale_div); i++) {
		const uint32_t d = prescale_div[i] * divisor;
		if (f > U32_MAX / d)
			continue;

		/* FIXME: Report unobtainable frequencies. */
		const uint32_t xtal = ATARI_MFP_XTAL;
		const uint32_t count = clamp_t(uint32_t,
			DIV_ROUND_CLOSEST_U32(xtal, d * f), 1, 255);
		/* Prefer lower divisors over higher ones, all else equal. */
		const uint32_t err = 10 * abs(d * count * f - xtal) + divisor;

		if (best >= 0 && err >= best)
			continue;

		best = err;
		valid = true;
		*prescale = (struct timer_prescale) {
			.ctrl = 1 + i,
			.divisor = divisor,
			.count = count & 0xff,
		};
	}

	return valid;
}

static void sndh_play_record(void)
{
	static bool record;

	if (!record) {
		ioor16(0x10, SND_DMA_ADDR_CTRL);
		ioand16(~0x10, SND_DMA_ADDR_CTRL);

		record = true;
	}

	sndh_play(&file);
}

INTERRUPT void vbl_exception(void)
{
	if (timer_state.type == SNDH_TIMER_V &&
	    timer_state.play)
		sndh_play_record();
}

static void install_sndh_vbl(void)
{
	timer_state.type = SNDH_TIMER_V;
}

static bool timer_division(void)
{
	if (timer_state.dividend >= timer_state.prescale.divisor)
		timer_state.dividend = 0;

	return !timer_state.dividend++;
}

#define DEFINE_TIMER_EXCEPTION(name_, type_, ab_)			\
INTERRUPT void timer_##name_##_exception(void)				\
{									\
	if (timer_state.type == SNDH_TIMER_##type_ &&			\
	    timer_state.play && timer_division())			\
		sndh_play_record();					\
									\
	/* Timer is now served. */					\
	mfp_clrs_is##ab_({ .timer_##name_ = true });			\
}									\
									\
static void install_sndh_timer_##name_(void)				\
{									\
	mfp_wrs_t##name_##cr({ .ctrl = timer_state.prescale.ctrl });	\
	mfp_wrs_t##name_##dr({ .count = timer_state.prescale.count });	\
	mfp_sets_ie##ab_({ .timer_##name_ = true }); 			\
	mfp_clrs_ip##ab_({ .timer_##name_ = true }); 			\
	mfp_clrs_is##ab_({ .timer_##name_ = true }); 			\
	mfp_sets_im##ab_({ .timer_##name_ = true }); 			\
									\
	timer_state.type = SNDH_TIMER_##type_;				\
}

DEFINE_TIMER_EXCEPTION(a, A, ra)
DEFINE_TIMER_EXCEPTION(b, B, ra)
DEFINE_TIMER_EXCEPTION(c, C, rb)
DEFINE_TIMER_EXCEPTION(d, D, rb)

static void install_sndh_timer(const struct sndh_timer timer)
{
	(timer.type == SNDH_TIMER_A ? install_sndh_timer_a :
	 timer.type == SNDH_TIMER_B ? install_sndh_timer_b :
	 timer.type == SNDH_TIMER_C ? install_sndh_timer_c :
	 timer.type == SNDH_TIMER_D ? install_sndh_timer_d :
				      install_sndh_vbl)();
}

static void play(void)
{
	barrier();

	timer_state.play = true;

	barrier();
}

static void idle(void)
{
	__asm__ __volatile__ ("stop #0x2200" : : : "cc");
}

static void idle_indefinitely(void)
{
	for (;;)
		idle();
}

void start(size_t size, void *sndh, uint32_t track, uint32_t timer_)
{
	const struct sndh_timer timer = u32_to_sndh_timer(timer_);

	BUILD_BUG_ON(sizeof(struct system_variables) != 0x1b4);

	file = (struct sndh_file) { .size = size, .sndh = sndh };

	__system_variables->_membot = (uint32_t)sndh + size + 1024;
	__system_variables->nvbls = ARRAY_SIZE(vblqueue);
	__system_variables->_vblqueue = vblqueue;
	__system_variables->_p_cookies = cookie_jar;

	if (timer.type && timer.type != SNDH_TIMER_V)
		if (!timer_prescale(&timer_state.prescale, timer.frequency))
			goto err;

	sndh_init(track, &file);

	install_sndh_timer(timer);

	play();

err:
	idle_indefinitely();
}
