// Copyright 2020 Embedded Artistry LLC
// SPDX-License-Identifier: MIT

#include "STM32Timer.hpp"
#include "STM32ClockControl.hpp"
#include <array>
#include <nvic.hpp>
#include <stm32h7xx_ll_bus.h>
#include <stm32h7xx_ll_tim.h>
#include <volatile.hpp>

// TODO: decouple RCC from this class, handle instead in the hardware platform?

extern "C" void TIM1_CC_IRQHandler();
extern "C" void TIM2_IRQHandler();
extern "C" void TIM3_IRQHandler();
extern "C" void TIM4_IRQHandler();
extern "C" void TIM5_IRQHandler();
extern "C" void TIM6_DAC_IRQHandler();
extern "C" void TIM7_IRQHandler();
extern "C" void TIM8_CC_IRQHandler();

namespace
{
constexpr std::array<TIM_TypeDef* const, STM32Timer::Device::COUNT> timer_instance = {
	TIM1, TIM2, TIM3, TIM4, TIM5, TIM6, TIM7, TIM8};

static std::array<Timer::cb_t, STM32Timer::Device::COUNT> tim_driver_callbacks = {};
static std::array<Timer::cb_t, STM32Timer::Device::COUNT> tim_user_callbacks = {};

constexpr std::array<uint8_t, STM32Timer::Device::COUNT> irq_num = {
	TIM1_CC_IRQn, // Note that other options are: TIM1_UP_TIM16_IRQN, TIM1_BRK_TIM15_IRQn,
				  // TIM1_TRG_COM_TIM17_IRQn, TIM1_CC_IRQn, LPTIM1_IRQn
	TIM2_IRQn, TIM3_IRQn, TIM4_IRQn, TIM5_IRQn, TIM6_DAC_IRQn, TIM7_IRQn,
	TIM8_CC_IRQn, // Other ptions are: TIM8_BRK_IRQn, TIM8_UP_IRQn, TIM8_TRG_COM_IRQn, TIM8_CC_IRQn
};
} // namespace

static void timer_interrupt_handler(STM32Timer::Device ch)
{
	// Right now we are just blanket-clearing flags.
	// This can be updated with particular flag checks for finer-grained
	// clock interrupt support. Also, if you need multiple channels, you need
	// to do that anyway - to determine if CC1, CC2, CC3, CC4 fired, for example.
	volatile TIM_TypeDef* const reg = timer_instance[ch];
	embutil::volatile_store(&(reg->SR), UINT32_C(0));

	auto user_cb = tim_user_callbacks[ch];
	auto driver_cb = tim_driver_callbacks[ch];

	if(user_cb)
	{
		user_cb();
	}

	if(driver_cb)
	{
		driver_cb();
	}
}

extern "C" void TIM1_CC_IRQHandler()
{
	timer_interrupt_handler(STM32Timer::Device::CH1);
}

extern "C" void TIM2_IRQHandler()
{
	timer_interrupt_handler(STM32Timer::Device::CH2);
}

extern "C" void TIM3_IRQHandler()
{
	timer_interrupt_handler(STM32Timer::Device::CH3);
}

extern "C" void TIM4_IRQHandler()
{
	timer_interrupt_handler(STM32Timer::Device::CH4);
}

extern "C" void TIM5_IRQHandler()
{
	timer_interrupt_handler(STM32Timer::Device::CH5);
}

extern "C" void TIM6_DAC_IRQHandler()
{
	timer_interrupt_handler(STM32Timer::Device::CH6);
}

extern "C" void TIM7_IRQHandler()
{
	timer_interrupt_handler(STM32Timer::Device::CH7);
}

extern "C" void TIM8_CC_IRQHandler()
{
	timer_interrupt_handler(STM32Timer::Device::CH8);
}

Timer::period_t STM32Timer::count() const noexcept
{
	auto count = LL_TIM_GetCounter(timer_instance[channel_]);
	return std::chrono::microseconds(count);
}

void STM32Timer::start() noexcept
{
	STM32ClockControl::timerEnable(static_cast<uint8_t>(channel_));
	auto inst = timer_instance[channel_];
	assert(inst);
	uint32_t div =
		(tick_ == TickRate::microsecond) ? LL_TIM_CLOCKDIVISION_DIV1 : LL_TIM_CLOCKDIVISION_DIV2;
	uint32_t clockrate = (tick_ == TickRate::microsecond) ? SystemCoreClock : SystemCoreClock >> 1;

	// We need to coerce the tick count to the appropriate units
	uint32_t period =
		(tick_ == TickRate::microsecond) ?
			static_cast<uint32_t>(period_.count()) :
			static_cast<uint32_t>(
				std::chrono::duration_cast<std::chrono::milliseconds>(period_).count());

	auto prescaler = __LL_TIM_CALC_PSC(clockrate, static_cast<uint32_t>(tick_));

	// If this fails, you're trying to set a value > 16 bits in size on a 16-bit timer
	// You need to a) change the prescaler, or b) change the period.
	assert(IS_TIM_32B_COUNTER_INSTANCE(inst) || (period < UINT16_MAX));

	LL_TIM_InitTypeDef initializer = {
		.Prescaler = static_cast<uint16_t>(prescaler),
		.CounterMode = LL_TIM_COUNTERMODE_UP,
		.Autoreload = period,
		.ClockDivision = div,
		.RepetitionCounter = UINT8_C(0),
	};

	auto r = LL_TIM_Init(inst, &initializer);
	assert(r == 0);

	/* Enable TIM2_ARR register preload. Writing to or reading from the         */
	/* auto-reload register accesses the preload register. The content of the   */
	/* preload register are transferred into the shadow register at each update */
	/* event (UEV).                                                             */
	LL_TIM_EnableARRPreload(inst);

	/* Enable TIM2_CCR1 register preload. Read/Write operations access the      */
	/* preload register. TIM2_CCR1 preload value is loaded in the active        */
	/* at each update event.                                                    */
	LL_TIM_OC_EnablePreload(inst, LL_TIM_CHANNEL_CH1);
	// TODO: support multiple channels for each timer, not just channel 1

	if(config_ == Timer::Config::oneshot)
	{
		tim_driver_callbacks[channel_] = [this]() noexcept {
			stop();
		};

		LL_TIM_SetOnePulseMode(inst, LL_TIM_ONEPULSEMODE_SINGLE);
	}
	else
	{
		tim_driver_callbacks[channel_] = {};
		LL_TIM_SetOnePulseMode(inst, LL_TIM_ONEPULSEMODE_REPETITIVE);
	}

	/**********************************/
	/* Start output signal generation */
	/**********************************/
	/* Enable output channel 1 */
	LL_TIM_CC_EnableChannel(inst, LL_TIM_CHANNEL_CH1);
	// TODO: support multiple channels for each timer, not just channel 1

	/* Enable counter */
	LL_TIM_EnableCounter(inst);

	/* Force update generation */
	// LL_TIM_GenerateEvent_UPDATE(inst);

	enableInterrupts();

	state_ = Timer::State::armed;
}

void STM32Timer::registerCallback(const Timer::cb_t& cb) noexcept
{
	tim_user_callbacks[channel_] = cb;
}

void STM32Timer::registerCallback(Timer::cb_t&& cb) noexcept
{
	tim_user_callbacks[channel_] = std::move(cb);
}

void STM32Timer::stop() noexcept
{
	auto inst = timer_instance[channel_];
	assert(inst);
	disableInterrupts();
	LL_TIM_DeInit(inst);
	STM32ClockControl::timerDisable(static_cast<uint8_t>(channel_));
	state_ = Timer::State::stopped;
}

void STM32Timer::enableInterrupts() noexcept
{
	auto inst = timer_instance[channel_];
	assert(inst);

	/* Enable the capture/compare interrupt for channel*/
	// TODO: will need to adjust to CCx to select the appropriate channel
	// if multiple channels are used
	LL_TIM_EnableIT_CC1(inst);
	if(channel_ == STM32Timer::Device::CH6 || channel_ == STM32Timer::Device::CH7)
	{
		LL_TIM_EnableIT_UPDATE(inst);
	}

	auto irq = irq_num[channel_];
	NVICControl::priority(irq, 0); // TODO: how to configure priority for the driver?
	NVICControl::enable(irq);
}

void STM32Timer::disableInterrupts() noexcept
{
	auto inst = timer_instance[channel_];
	assert(inst);
	// Clear all pending flags
	embutil::volatile_store(&(inst->SR), UINT32_C(0));

	// TODO: select proper channel if multi capture-compare is used
	LL_TIM_DisableIT_CC1(inst);
	if(channel_ == STM32Timer::Device::CH6 || channel_ == STM32Timer::Device::CH7)
	{
		LL_TIM_DisableIT_UPDATE(inst);
	}

	auto irq = irq_num[channel_];
	NVICControl::disable(irq);
}

STM32Timer::~STM32Timer()
{
	stop();
}
