#pragma once

#include <stdint.h>

#include "alarms.hpp"
#include "flags.hpp"

namespace rtc {

namespace detail {

//////////////////////////////////////////////////////////////////////////

template <uint8_t Mask = 0xFF>
static inline uint8_t toBcd(const uint8_t &data)
{
	return ((data / 10 * 16) + (data % 10)) & Mask;
}

template <uint8_t Mask = 0xFF>
static inline uint8_t fromBcd(const uint8_t &data)
{
	const auto maskedData = data & Mask;
	return ((maskedData / 16 * 10) + (maskedData % 16));
}

static inline uint8_t convertTo24Hour(const uint8_t &hoursReg)
{
	constexpr auto FLAG_12_HOUR = 6;
	constexpr auto FLAG_PM = 5;

	const bool time12HourFormat = (hoursReg >> FLAG_12_HOUR) & 1;

	if (time12HourFormat) {
		const auto pmFlag = (hoursReg >> FLAG_PM) & 1;
		constexpr auto HOUR_12_MASK = 0b00011111;
		const auto hour12 = fromBcd<HOUR_12_MASK>(hoursReg);
		if (hour12 == 12 && !pmFlag)
			return 0;
		return hour12 + pmFlag ? 12 : 0;
	} else // 24 hour format
	{
		constexpr auto HOUR_MASK = 0b00111111;
		return fromBcd<HOUR_MASK>(hoursReg);
	}
}

template <uint8_t Mask = 0b01111111>
static inline uint8_t getMaskedBcd(const uint8_t &reg)
{
	return fromBcd<Mask>(reg);
}

template <uint8_t Mask = 0b01111111>
static inline void setMaskedBcd(uint8_t &reg, const uint8_t &value)
{
	reg &= ~Mask;
	reg |= toBcd<Mask>(value);
}

static inline bool getEncodedDay(uint8_t &value, const uint8_t &reg)
{
	constexpr auto DAY_FLAG = 6;
	if ((reg >> DAY_FLAG) & 1) {
		constexpr auto DAY_MASK = 0b00001111;
		value = reg & DAY_MASK;
		return true;
	}

	return false;
}

static inline bool getEncodedDate(uint8_t &value, const uint8_t &reg)
{
	constexpr auto DAY_FLAG = 6;
	if (!((reg >> DAY_FLAG) & 1)) {
		value = getMaskedBcd<0b00111111>(reg);
		return true;
	}

	return false;
}

static inline void setEncodedDay(uint8_t &reg, const uint8_t &value)
{
	constexpr auto DAY_MASK = 0b11110000;
	reg &= DAY_MASK;
	reg |= value & DAY_MASK;
	constexpr auto DAY_FLAG = 6;
	reg |= (1 << DAY_FLAG);
}

static inline void setEncodedDate(uint8_t &reg, const uint8_t &value)
{
	setMaskedBcd<0b00111111>(reg, value);
	constexpr auto DAY_FLAG = 6;
	reg &= ~(1 << DAY_FLAG);
}

//////////////////////////////////////////////////////////////////////////

struct [[gnu::packed]] TimeReg
{
	uint8_t seconds = 0;
	uint8_t minutes = 0;
	uint8_t hours = 0;
	uint8_t day = 1; // Range 1-7 according to datasheet
	uint8_t date = 0;
	uint8_t month_century = 0;
	uint8_t year = 0;

	//////////////////////////////////////////////////////////////////////////

	inline uint8_t getSeconds() const
	{
		return getMaskedBcd(seconds);
	}
	inline uint8_t getMinutes() const
	{
		return getMaskedBcd(minutes);
	}
	inline uint8_t getHours() const
	{
		return convertTo24Hour(hours);
	}
	inline uint8_t getDay() const
	{
		return getMaskedBcd<0b00000111>(day);
	}
	inline uint8_t getDate() const
	{
		return getMaskedBcd<0b00111111>(date);
	}
	inline uint8_t getMonth() const
	{
		return getMaskedBcd<0b00011111>(month_century);
	}
	inline bool getCentury() const
	{
		constexpr auto CENTURY_FLAG = 7;
		return (month_century >> CENTURY_FLAG) & 1;
	}
	inline uint16_t getYear() const
	{
		return 2000 + fromBcd(year);
	}

	//////////////////////////////////////////////////////////////////////////

	inline void setSeconds(uint8_t seconds)
	{
		setMaskedBcd(this->seconds, seconds);
	}
	inline void setMinutes(uint8_t minutes)
	{
		setMaskedBcd(this->minutes, minutes);
	}
	inline void setHours(uint8_t hours)
	{
		setMaskedBcd(this->hours, hours);
	}
	inline void setDay(uint8_t day)
	{
		this->day = day & 0b111;
	}
	inline void setDate(uint8_t date)
	{
		setMaskedBcd<0b00111111>(this->date, date);
	}
	inline void setMonth(uint8_t month)
	{
		setMaskedBcd<0b00011111>(month_century, month);
	}
	inline void setCentury(bool century)
	{
		constexpr auto CENTURY_POS = 7;
		month_century &= ~(1 << CENTURY_POS);
		month_century |= (century << CENTURY_POS);
	}
	inline void setYear(uint16_t year)
	{
		year = year % 100;
		this->year = toBcd(year);
	}
};

static_assert(sizeof(TimeReg) == 7, "Invalid time register size");

//////////////////////////////////////////////////////////////////////////

struct [[gnu::packed]] Alarm1Reg
{
	uint8_t seconds = 0;
	uint8_t minutes = 0;
	uint8_t hours = 0;
	uint8_t day_date = 0;

	//////////////////////////////////////////////////////////////////////////

	inline uint8_t getSeconds() const
	{
		return getMaskedBcd(seconds);
	}
	inline uint8_t getMinutes() const
	{
		return getMaskedBcd(minutes);
	}
	inline uint8_t getHours() const
	{
		return convertTo24Hour(hours);
	}
	inline bool getDay(uint8_t & day) const
	{
		return getEncodedDay(day, day_date);
	}
	inline bool getDate(uint8_t & date) const
	{
		return getEncodedDate(date, day_date);
	}
	inline Alarm1Rate getAlarmRate() const
	{
		constexpr auto M_FLAG = 7;
		const auto m1 = (seconds >> M_FLAG) & 1;
		const auto m2 = (minutes >> M_FLAG) & 1;
		const auto m3 = (hours >> M_FLAG) & 1;
		const auto m4 = (day_date >> M_FLAG) & 1;
		const auto m = (m4 << 3) | (m3 << 2) | (m2 << 1) | (m1 << 0);
		if (m == 0) {
			constexpr auto DAY_FLAG = 6;
			const auto dayFormat = ((day_date >> DAY_FLAG) & 1) << 4;
			return static_cast<Alarm1Rate>(dayFormat);
		}
		return static_cast<Alarm1Rate>(m);
	}

	//////////////////////////////////////////////////////////////////////////

	inline void setSeconds(uint8_t seconds)
	{
		setMaskedBcd(this->seconds, seconds);
	}
	inline void setMinutes(uint8_t minutes)
	{
		setMaskedBcd(this->minutes, minutes);
	}
	inline void setHours(uint8_t hours)
	{
		setMaskedBcd(this->hours, hours);
	}
	inline void setDay(uint8_t day)
	{
		setEncodedDay(day_date, day);
	}
	inline void setDate(uint8_t date)
	{
		setEncodedDate(day_date, date);
	}
	inline void setAlarmRate(const Alarm1Rate &alarmRate)
	{
		const auto alarmRateFlags = static_cast<uint8_t>(alarmRate);
		constexpr auto M_FLAG = 7;
		seconds &= ~(1 << M_FLAG);
		seconds |= (alarmRateFlags & 1) << M_FLAG;
		minutes &= ~(1 << M_FLAG);
		minutes |= ((alarmRateFlags >> 1) & 1) << M_FLAG;
		hours &= ~(1 << M_FLAG);
		hours |= ((alarmRateFlags >> 2) & 1) << M_FLAG;
		day_date &= ~(1 << M_FLAG);
		day_date |= ((alarmRateFlags >> 3) & 1) << M_FLAG;
	}
};

static_assert(sizeof(Alarm1Reg) == 4, "Invalid alarm1 register size");

//////////////////////////////////////////////////////////////////////////

struct [[gnu::packed]] Alarm2Reg
{
	uint8_t minutes = 0;
	uint8_t hours = 0;
	uint8_t day_date = 0;

	//////////////////////////////////////////////////////////////////////////

	inline uint8_t getMinutes() const
	{
		return getMaskedBcd(minutes);
	}
	inline uint8_t getHours() const
	{
		return convertTo24Hour(hours);
	}
	inline bool getDay(uint8_t & day) const
	{
		return getEncodedDay(day, day_date);
	}
	inline bool getDate(uint8_t & date) const
	{
		return getEncodedDate(date, day_date);
	}
	inline Alarm2Rate getAlarmRate() const
	{
		constexpr auto M_FLAG = 7;
		const auto m2 = (minutes >> M_FLAG) & 1;
		const auto m3 = (hours >> M_FLAG) & 1;
		const auto m4 = (day_date >> M_FLAG) & 1;
		const auto m = (m4 << 2) | (m3 << 1) | (m2 << 0);
		if (m == 0) {
			constexpr auto DAY_FLAG = 6;
			const auto dayFormat = ((day_date >> DAY_FLAG) & 1) << 3;
			return static_cast<Alarm2Rate>(dayFormat);
		}
		return static_cast<Alarm2Rate>(m);
	}

	//////////////////////////////////////////////////////////////////////////

	inline void setMinutes(uint8_t minutes)
	{
		setMaskedBcd(this->minutes, minutes);
	}
	inline void setHours(uint8_t hours)
	{
		setMaskedBcd(this->hours, hours);
	}
	inline void setDay(uint8_t day)
	{
		setEncodedDay(day_date, day);
	}
	inline void setDate(uint8_t date)
	{
		setEncodedDate(day_date, date);
	}
	inline void setAlarmRate(const Alarm2Rate &alarmRate)
	{
		const auto alarmRateFlags = static_cast<uint8_t>(alarmRate);
		constexpr auto M_FLAG = 7;
		minutes &= ~(1 << M_FLAG);
		minutes |= (alarmRateFlags & 1) << M_FLAG;
		hours &= ~(1 << M_FLAG);
		hours |= ((alarmRateFlags >> 1) & 1) << M_FLAG;
		day_date &= ~(1 << M_FLAG);
		day_date |= ((alarmRateFlags >> 2) & 1) << M_FLAG;
	}
};

static_assert(sizeof(Alarm2Reg) == 3, "Invalid alarm2 register size");

//////////////////////////////////////////////////////////////////////////

enum class ControlRegFlags : uint8_t {
	N_EOSC = 1 << 7,
	BBSQW = 1 << 6,
	CONV = 1 << 5,
	RS2 = 1 << 4,
	RS1 = 1 << 3,
	INTCN = 1 << 2,
	A2IE = 1 << 1,
	A1IE = 1 << 0,
};

static inline uint8_t operator~(const ControlRegFlags &flag)
{
	return ~static_cast<uint8_t>(flag);
}

struct [[gnu::packed]] ControlReg : FlagsImpl<ControlRegFlags>{};

static_assert(sizeof(ControlReg) == 1, "Invalid control register size");

//////////////////////////////////////////////////////////////////////////

enum class ControlStatusRegFlags : uint8_t {
	OSF = 1 << 7,
	EN32KHZ = 1 << 3,
	BSY = 1 << 2,
	A2F = 1 << 1,
	A1F = 1 << 0,
};

static inline uint8_t operator~(const ControlStatusRegFlags &flag)
{
	return ~static_cast<uint8_t>(flag);
}

struct [[gnu::packed]] ControlStatusReg : FlagsImpl<ControlStatusRegFlags>{};

static_assert(sizeof(ControlStatusReg) == 1, "Invalid control/status register size");

//////////////////////////////////////////////////////////////////////////

struct [[gnu::packed]] AgingOffsetReg
{
	uint8_t data = 0;
};

static_assert(sizeof(AgingOffsetReg) == 1, "Invalid aging offset register size");

//////////////////////////////////////////////////////////////////////////

struct [[gnu::packed]] TempReg
{
	uint8_t msb_temp = 0;
	uint8_t lsb_temp = 0;
};

static_assert(sizeof(TempReg) == 2, "Invalid temperature register size");

//////////////////////////////////////////////////////////////////////////

} // namespace detail

} // namespace rtc