#include "max2871.hpp"
#include <string.h>
#include <algorithm.hpp>

#include "delay.hpp"
#include <cmath>
#define LOG_LEVEL	LOG_LEVEL_ERR
#define LOG_MODULE	"MAX2871"
#include "Log.h"

bool MAX2871::Init() {
	return Init(10000000, true, 1, false);
}

bool MAX2871::Init(uint32_t f_ref, bool doubler, uint16_t r, bool div2) {
	for (uint8_t i = 0; i < 6; i++) {
		regs[i] = 0;
	}

	ChipEnable(false);
	RFEnable(false);

	SetReference(f_ref, doubler, r, div2);

	// non-inverting loop filter
	regs[2] |= (1UL << 6);
	// select digital lock detect
	regs[5] |= (0x1UL << 22);

	// fundamental VCO feedback
	regs[4] |= (1UL << 23);

	// reserved, set to 0x03
	regs[4] |= (0x3UL << 29);

	// enable double buffering for register 4
	regs[2] |= (1UL << 13);

	// automatically switch to integer mode if F = 0
	regs[5] |= (1UL << 24);

	// recommended phase setting
	regs[1] |= (1UL << 15);

	SetMode(Mode::LowNoise);
	// for all other CP modes the PLL reports unlock condition (output signal appears to be locked)
	SetCPMode(CPMode::CP20);
	SetCPCurrent(15);
	SetFrequency(1000000000);

	// initial register write according to datasheet timing
	ChipEnable(true);
	Write(5, regs[5]);
	Delay::ms(20);
	Write(4, regs[4]);
	Write(3, regs[3]);
	Write(2, regs[2]);
	Write(1, regs[1]);
	Write(0, regs[0]);
	Write(5, regs[5]);
	Delay::ms(20);
	Write(4, regs[4]);
	Write(3, regs[3]);
	Write(2, regs[2]);
	Write(1, regs[1]);
	Write(0, regs[0]);

	return true;
}

void MAX2871::ChipEnable(bool on) {
	if(!CE) {
		return;
	}
	if (on) {
		CE->BSRR = CEpin;
	} else {
		CE->BSRR = CEpin << 16;
	}
}

void MAX2871::RFEnable(bool on) {
	if(!RF_EN) {
		return;
	}
	if (on) {
		RF_EN->BSRR = RF_ENpin;
		Read();
	} else {
		RF_EN->BSRR = RF_ENpin << 16;
	}
}

bool MAX2871::Locked() {
	return LD->IDR & LDpin;
}

void MAX2871::SetPowerOutA(Power p, bool enabled) {
	// only set power of port A
	regs[4] &= ~0x38;
	regs[4] |= ((uint16_t) p << 3);
	if (enabled) {
		regs[4] |= 0x20;
	}
}

void MAX2871::SetPowerOutB(Power p, bool enabled) {
	// only set power of port B
	regs[4] &= ~0x1C0;
	regs[4] |= ((uint16_t) p << 6);
	if (enabled) {
		regs[4] |= 0x100;
	}
}

void MAX2871::SetMode(Mode m) {
	regs[2] &= ~0x60000000;
	regs[2] |= ((uint32_t) m << 29);
}

void MAX2871::SetCPMode(CPMode m) {
	regs[1] &= ~0x60000000;
	regs[1] |= ((uint32_t) m << 29);
}

void MAX2871::SetCPCurrent(uint8_t mA) {
	if(mA > 15) {
		LOG_WARN("Clipping charge pump current to 15mA");
		mA = 15;
	}
	regs[2] &= ~0x1E00;
	regs[2] |= ((uint16_t) mA << 9);
}

bool MAX2871::SetFrequency(uint64_t f) {
	if (f < 23500000 || f > MaxFreq) {
		LOG_ERR("Frequency must be between 23.5MHz and 6GHz, requested %lu%06luHz", (uint32_t ) (f / 1000000),
				(uint32_t ) (f % 1000000));
		return false;
	}
	LOG_DEBUG("Setting frequency to %lu%06luHz...", (uint32_t ) (f / 1000000),
			(uint32_t ) (f % 1000000));
	// select divider
	uint64_t f_vco = f;
	uint8_t div = 0;
	if (f < 46875000) {
		div = 0x07;
		f_vco *= 128;
	} else if (f < 93750000) {
		div = 0x06;
		f_vco *= 64;
	} else if (f < 187500000) {
		div = 0x05;
		f_vco *= 32;
	} else if (f < 375000000) {
		div = 0x04;
		f_vco *= 16;
	} else if (f < 750000000) {
		div = 0x03;
		f_vco *= 8;
	} else if (f < 1500000000) {
		div = 0x02;
		f_vco *= 4;
	} else if (f < 3000000000) {
		div = 0x01;
		f_vco *= 2;
	}
	LOG_DEBUG("F_VCO: %lu%06luHz",
			(uint32_t ) (f_vco / 1000000), (uint32_t ) (f_vco % 1000000));
	if (gotVCOMap) {
		// manual VCO selection for lock time improvement
		uint16_t compare = f_vco / 100000;
		uint8_t vco = 0;
		for (; vco < 64; vco++) {
			if (VCOmax[vco] >= compare) {
				break;
			}
		} LOG_DEBUG("Manually selected VCO %d", vco);
		regs[3] &= ~0xFC000000;
		regs[3] |= (uint32_t) vco << 26;
	}
	uint16_t N = f_vco / f_PFD;
	if (N < 19 || N > 4091) {
		LOG_ERR("Invalid N value, should be between 19 and 4091, got %lu", N);
		return false;
	}
	uint32_t rem_f = f_vco - N * f_PFD;
	LOG_DEBUG("Remaining fractional frequency: %lu", rem_f);
	LOG_DEBUG("Looking for best fractional match");
	float fraction = (float) rem_f / f_PFD;

	Algorithm::RationalApproximation ratio;
	ratio.num = rem_f;
	ratio.denom = f_PFD;
	auto approx = Algorithm::BestRationalApproximation(ratio, 4095);

	if (approx.denom == approx.num) {
		// got an impossible result due to floating point limitations(?)
		// Set fractional part to zero, increase integer part instead
		approx.num = 0;
		approx.denom = 2;
		N++;
	}

	if(approx.denom == 1) {
		// M value must be at least 2
		approx.denom = 2;
	}

	int32_t rem_approx = ((uint64_t) f_PFD * approx.num) / approx.denom;
	if(rem_approx != rem_f) {
		LOG_WARN("Best match is F=%u/M=%u, deviation of %luHz",
				approx.num, approx.denom, abs(rem_f - rem_approx));
	}

	uint64_t f_set = (uint64_t) N * f_PFD + rem_approx;
	f_set /= (1UL << div);

	// write values to registers
	regs[4] &= ~0x00700000;
	regs[4] |= ((uint32_t) div << 20);
	regs[0] &= ~0x7FFFFFF8;
	regs[0] |= ((uint32_t) N << 15) | ((uint32_t) approx.num << 3);
	regs[1] &= ~0x00007FF8;
	regs[1] |= ((uint32_t) approx.denom << 3);

	LOG_DEBUG("Set frequency to %lu%06luHz...",
			(uint32_t ) (f_set / 1000000), (uint32_t ) (f_set % 1000000));
	outputFrequency = f_set;
	return true;
}

bool MAX2871::SetReference(uint32_t f_ref, bool doubler, uint16_t r,
		bool div2) {
	if (f_ref < 10000000) {
		LOG_ERR("Reference frequency must be >=10MHz, is %lu", f_ref);
		return false;
	} else if (f_ref > 105000000 && doubler) {
		LOG_ERR(
				"Reference frequency must be <=105MHz when used with doubler, is %lu",
				f_ref);
		return false;
	} else if (f_ref > 210000000) {
		LOG_ERR("Reference frequency must be <=210MHz, is %lu", f_ref);
		return false;
	}
	if (r < 1 || r > 1023) {
		LOG_ERR("Reference divider must be between 1 and 1023, is %d", r);
		return false;
	}
	// calculate PFD frequency
	uint32_t pfd = f_ref;
	if (doubler) {
		pfd *= 2;
	}
	pfd /= r;
	if (div2) {
		pfd /= 2;
	}
	if (pfd > 125000000) {
		LOG_ERR("PFD frequency must be <=125MHz, is %d",
				pfd);
		return false;
	}
	if(pfd > 32000000) {
		regs[2] |= (1UL << 31);
	} else {
		regs[2] &= ~(1UL << 31);
	}
	// input values are valid, adjust registers
	regs[2] &= ~0x03FFC000;
	if (doubler) {
		regs[2] |= (1UL << 25);
	}
	if (div2) {
		regs[2] |= (1UL << 24);
	}
	regs[2] |= (r << 14);
	f_PFD = pfd;
	LOG_INFO("Set PFD frequency to %lu", f_PFD);

	// updating VAS state machine clock
	uint16_t BS = f_PFD / 50000;
	if (BS > 1023) {
		BS = 1023;
	} else if (BS < 1) {
		BS = 1;
	}
	LOG_DEBUG("BS set to %lu", BS);
	regs[4] &= ~0x030FF000;
	regs[4] |= ((BS & 0xFF) << 12);
	regs[4] |= (((BS >> 8) & 0x03) << 24);

	// update ADC clock
	uint16_t cdiv = f_PFD/100000;
	LOG_DEBUG("CDIV set to %u", cdiv);
	regs[3] &= ~0x00007FF8;
	regs[3] |= (cdiv & 0xFFF) << 3;
	return true;
}

void MAX2871::Update() {
	Write(5, regs[5]);
	Write(4, regs[4]);
	Write(3, regs[3]);
	Write(2, regs[2]);
	Write(1, regs[1]);
	Write(0, regs[0]);
}

void MAX2871::UpdateFrequency() {
	Write(4, regs[4]);
	Write(3, regs[3]);
	Write(1, regs[1]);
	Write(0, regs[0]);
}

void MAX2871::Write(uint8_t reg, uint32_t val) {
	uint8_t data[4];
	// split value into two 16 bit words
	data[0] = (val >> 24) & 0xFF;
	data[1] = (val >> 16) & 0xFF;
	data[2] = (val >> 8) & 0xFF;
	data[3] = (val & 0xF8) | reg;
	Delay::us(1);
	HAL_SPI_Transmit(hspi, (uint8_t*) data, 4, 20);
	LE->BSRR = LEpin;
	Delay::us(1);
	LE->BSRR = LEpin << 16;
}

// Assumes that the MUX pin is already configured as "Read register 6" and connected to MISO
uint32_t MAX2871::Read() {
	uint8_t transmit[4] = {0x00, 0x00, 0x00, 0x06};
	HAL_SPI_Transmit(hspi, (uint8_t*) transmit, 4, 20);
	LE->BSRR = LEpin;
	memset(transmit, 0, sizeof(transmit));
	uint8_t recv[4];
	HAL_SPI_TransmitReceive(hspi, (uint8_t*) transmit, (uint8_t*) recv, 4, 20);
	LE->BSRR = LEpin << 16;
	// assemble readback result
	uint32_t result = ((uint32_t) recv[0] << 24) | ((uint32_t) recv[1] << 16
	) | ((uint32_t) recv[2] << 8) | (recv[3] & 0xFF);
	result <<= 2;
	LOG_DEBUG("Readback: 0x%08x", result);
	return result;
}

bool MAX2871::BuildVCOMap() {
	memset(VCOmax, 0, sizeof(VCOmax));
	// save output frequency
	uint64_t oldFreq = outputFrequency;
	constexpr uint32_t step = 10000000;
	for (uint64_t freq = 3000000000; freq <= MaxFreq; freq += step) {
		SetFrequency(freq);
		UpdateFrequency();
		uint32_t start = HAL_GetTick();
		// set MUX to LD
		regs[2] &= ~(7UL << 26);
		regs[5] &= ~(1UL << 18);
		regs[2] |= (6UL << 26);
		Write(5, regs[5]);
		Write(2, regs[2]);
		while (!(MUX->IDR & MUXpin)) {
			if (HAL_GetTick() - start > 100) {
				LOG_ERR(
						"Failed to lock during VCO map build process, aborting (f=%lu%06luHz)",
						(uint32_t )(freq / 1000000),
						(uint32_t ) (freq % 1000000));
				gotVCOMap = false;
				// revert back to previous frequency
				SetFrequency(oldFreq);
				LE->BSRR = LEpin << 16;
				// Mux pin back to high impedance
				regs[2] &= ~(7UL << 26);
				regs[5] &= ~(1UL << 18);
				Update();
				return false;
			}
		}
		// set MUX to SPI read
		regs[2] &= ~(7UL << 26);
		regs[5] &= ~(1UL << 18);
		regs[2] |= (4UL << 26);
		regs[5] |= (1UL << 18);
		Write(5, regs[5]);
		Write(2, regs[2]);
		auto readback = Read();
		uint8_t vco = (readback & 0x01F8) >> 3;
		VCOmax[vco] = freq / 100000;
		LOG_INFO("VCO map: %lu%06luHz uses VCO %d",
			(uint32_t ) (freq / 1000000), (uint32_t ) (freq % 1000000), vco);
	}
	gotVCOMap = true;
	// revert back to previous frequency
	SetFrequency(oldFreq);
	// Mux pin back to high impedance
	regs[2] &= ~(7UL << 26);
	regs[5] &= ~(1UL << 18);

	// Turn off VAS, select VCO manually from now on
	regs[3] |= (1UL << 25);
	Update();
	return true;
}

uint8_t MAX2871::GetTemp() {
	// select temperature channel and start ADC
	regs[5] &= ~0x00000078;
	regs[5] |= 0x00000048;
	Write(5, regs[5]);
	Delay::us(100);
	// set MUX to SPI read
	regs[2] &= ~(7UL << 26);
	regs[5] &= ~(1UL << 18);
	regs[2] |= (4UL << 26);
	regs[5] |= (1UL << 18);
	Write(5, regs[5]);
	Write(2, regs[2]);
	uint8_t ADC_raw = (Read() >> 16) & 0x7F;
	LOG_DEBUG("Raw temp ADC: %d", ADC_raw);
	// Disable ADC
	regs[5] &= ~0x00000078;
	// Mux pin back to high impedance
	regs[2] &= ~(7UL << 26);
	regs[5] &= ~(1UL << 18);
	Write(5, regs[5]);
	Write(2, regs[2]);
	// convert to celsius and return
	return 95 - 1.14f * ADC_raw;
}
