// sysTickCounter is supposed to be a quick way to get time differences for timeout purposes
// TODO P2 need to check to see if it's accurate
// TODO P1 need to decide if there's a better way

#include "bsp.hpp"
#include "_bsp_private.hpp"
#include "stm32h7xx_hal.h"
#include <system_time.hpp>
#include <PowerManager_Interface.hpp>
#include <iostream>
#include <cassert>

extern "C" void SysTick_Handler(void);
extern "C" void RTC_Alarm_IRQHandler(void);
extern "C" void RTC_WKUP_IRQHandler(void);

#define RTC_ASYNCH_PREDIV 0x7F
#define RTC_SYNCH_PREDIV 0x0F9

static RTC_HandleTypeDef RTCHandle = {};
static uint64_t sysTickCounter = 0;
static bool timeIsSet = false;

// TODO need to check this calculation
static uint16_t timeGetMilliSeconds(RTC_TimeTypeDef& timeNow)
{
	return static_cast<uint16_t>(1000 * (timeNow.SecondFraction - timeNow.SubSeconds) /
								 (timeNow.SecondFraction + 1));
}

/*
 * This configures the RTC to use the Low-Speed External oscillator, which makes the RTC available
 * in the Stop and Standby power modes. It does, however, assume a 32.768kHz crystal is attached
 * to the LSE pins, as it is on the Nucleo board
 */
void bsp_private::initializeRTC() // TODO Why does the class name explicitly call out private?
{
	RTCHandle.Instance = RTC;
	RTCHandle.Init.HourFormat = RTC_HOURFORMAT_24;
	RTCHandle.Init.AsynchPrediv = RTC_ASYNCH_PREDIV;
	RTCHandle.Init.SynchPrediv = RTC_SYNCH_PREDIV;
	RTCHandle.Init.OutPut = RTC_OUTPUT_DISABLE;
	RTCHandle.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
	RTCHandle.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
	__HAL_RTC_RESET_HANDLE_STATE(&RTCHandle);
	auto status = HAL_RTC_Init(&RTCHandle);
	assert(status == HAL_OK);
}

void RTC_Alarm_IRQHandler(void)
{
  HAL_RTC_AlarmIRQHandler(&RTCHandle);
}

void RTC_WKUP_IRQHandler(void)
{
  HAL_RTCEx_WakeUpTimerIRQHandler(&RTCHandle);
}

void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
	assert(hrtc);
	HAL_RTC_DeactivateAlarm(hrtc, RTC_ALARM_A);
}

void PowerManager::setWakeupAlarm(systime::DateTimeStruct wakeup)
{
	RTC_AlarmTypeDef alarm{};
	alarm.Alarm = RTC_ALARM_A;
	alarm.AlarmTime.Hours =  wakeup.hour;
	alarm.AlarmTime.Minutes = wakeup.minute;
	alarm.AlarmTime.Seconds = wakeup.second;
	alarm.AlarmTime.SubSeconds = 0;
	alarm.AlarmTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
	alarm.AlarmTime.StoreOperation = RTC_STOREOPERATION_RESET;
	// This confused me. MASK in this context means that we are IGNORING
	// this aspect of the alarm. So, MASK_DATEWEEKDAY means that we are
	// going to trigger when hours, minutes, seconds matches.
	alarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY;

	// configure RTC to generate an internal ALARM A interrupt
	auto status = HAL_RTC_DeactivateAlarm(&RTCHandle, RTC_ALARM_A);
	assert(status == HAL_OK);
	status = HAL_RTC_SetAlarm_IT(&RTCHandle, &alarm, RTC_FORMAT_BIN);
}

// Function that allows you to set a wakeup alarm with the STM32 RTC
void PowerManager::setWakeupAlarm(std::chrono::seconds wakeup_in)
{
	// the RTC alarm implementation above doesn't currently support waking up
	// more than 24 hours in the future
	assert(wakeup_in < std::chrono::hours(24));
	auto current_time = systime::getDateTimeNow();
	auto adjusted_time = systime::adjustTime(current_time, wakeup_in);
	PowerManager::setWakeupAlarm(adjusted_time);
}

void SysTick_Handler(void)
{
	HAL_IncTick();
	sysTickCounter++;
}

uint64_t systime::getUptimeMilliseconds()
{
	// HAL SysTick is usually configured for 1ms tick increments.
	return sysTickCounter;
}

systime::DateTimeStruct systime::getDateTimeNow()
{
	systime::DateTimeStruct dtNow{};

	if(timeIsSet)
	{
		RTC_DateTypeDef dateNow;
		RTC_TimeTypeDef timeNow;

		HAL_RTC_GetTime(&RTCHandle, &timeNow, RTC_FORMAT_BIN);
		HAL_RTC_GetDate(&RTCHandle, &dateNow, RTC_FORMAT_BIN);

		dtNow.year = static_cast<uint16_t>(dateNow.Year + 2000);
		dtNow.month = dateNow.Month;
		dtNow.date = dateNow.Date;
		dtNow.hour = timeNow.Hours;
		dtNow.minute = timeNow.Minutes;
		dtNow.second = timeNow.Seconds;
		// TODO need to check this calculation
		dtNow.milliSecond = timeGetMilliSeconds(timeNow);
	}

	return dtNow;
}

void systime::delay(uint32_t delay_in_ms)
{
	HAL_Delay(delay_in_ms);
}

void setTime(systime::DateTimeStruct inTime, uint64_t input_capture_tickstamp)
{
	RTC_DateTypeDef RTCDateSet;
	RTC_TimeTypeDef RTCTimeSet;

	if(input_capture_tickstamp)
	{
		auto delta = sysTickCounter - input_capture_tickstamp;
		inTime = systime::adjustTime(inTime, std::chrono::milliseconds(delta));
	}

	RTCTimeSet.Hours = inTime.hour;
	RTCTimeSet.Minutes = inTime.minute;
	RTCTimeSet.Seconds = inTime.second;
	RTCTimeSet.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
	RTCTimeSet.TimeFormat = RTC_HOURFORMAT_24;

	RTCDateSet.Year = static_cast<uint8_t>(inTime.year - 2000);
	RTCDateSet.Date = inTime.date;
	RTCDateSet.Month = inTime.month;
	RTCDateSet.WeekDay = 0; // TODO: Need to set this to something or the year can be wrong.

	if(HAL_RTC_SetDate(&RTCHandle, &RTCDateSet, RTC_FORMAT_BIN) != HAL_OK)
	{
		std::cout << "Error in HAL_RTC_SetDate" << std::endl;
	}

	if(HAL_RTC_SetTime(&RTCHandle, &RTCTimeSet, RTC_FORMAT_BIN) != HAL_OK)
	{
		std::cout << "Error_Handler();" << std::endl;
	}

	timeIsSet = true;
}
