/*
 * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
 *
 * This file is part of PortaPack.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

#ifndef __SI5351_H__
#define __SI5351_H__

#include <cstdint>
#include <array>
#include <algorithm>

#include "ch.h"
#include "hal.h"

#include "i2c_pp.hpp"

namespace si5351 {

namespace Register {
	enum {
		DeviceStatus = 0,
		InterruptStatusSticky = 1,
		InterruptStatusMask = 2,
		OutputEnableControl = 3,
		OEBPinEnableControlMask = 9,
		PLLInputSource = 15,
		CLKControl_Base = 16,
		CLKControl0 = 16,
		CLKControl1 = 17,
		CLKControl2 = 18,
		CLKControl3 = 19,
		CLKControl4 = 20,
		CLKControl5 = 21,
		CLKControl6 = 22,
		CLKControl7 = 23,
		CLK3_0DisableState = 24,
		CLK7_4DisableState = 25,
		MultisynthNAParameters_Base = 26,
		MultisynthNBParameters_Base = 34,
		Multisynth0Parameters_Base = 42,
		Multisynth1Parameters_Base = 50,
		Multisynth2Parameters_Base = 58,
		Multisynth3Parameters_Base = 66,
		Multisynth4Parameters_Base = 74,
		Multisynth5Parameters_Base = 82,
		Multisynth6Parameters = 90,
		Multisynth7Parameters = 91,
		Clock6And7OutputDivider = 92,
		SpreadSpectrumParameters_Base = 149,
		VCXOParameters_Base = 162,
		CLKInitialPhaseOffset_Base = 165,
		PLLReset = 177,
		CrystalInternalLoadCapacitance = 183,
		FanoutEnable = 187,
	};
}

namespace DeviceStatus {
	using Type = uint8_t;

	enum {
		REVID_Mask              = (0b11 << 0),

		LOS_Mask                = (1 << 4),
		LOS_ValidClockAtCLKIN   = (0 << 4),
		LOS_LossOfSignalAtCLKIN = (1 << 4),

		LOL_A_Mask              = (1 << 5),
		LOL_A_PLLALocked        = (0 << 5),
		LOL_A_PLLAUnlocked      = (1 << 5),

		LOL_B_Mask              = (1 << 6),
		LOL_B_PLLBLocked        = (0 << 6),
		LOL_B_PLLBUnlocked      = (1 << 6),

		SYS_INIT_Mask           = (1 << 7),
		SYS_INIT_Complete       = (0 << 7),
		SYS_INIT_Initializing   = (1 << 7),
	};
}

namespace ClockControl {
	using Type = uint8_t;

	enum {
		CLK_IDRV_Mask     = (0b11 << 0),
		CLK_IDRV_2mA      = (0b00 << 0),
		CLK_IDRV_4mA      = (0b01 << 0),
		CLK_IDRV_6mA      = (0b10 << 0),
		CLK_IDRV_8mA      = (0b11 << 0),

		CLK_SRC_Mask      = (0b11 << 2),
		CLK_SRC_XTAL      = (0b00 << 2),
		CLK_SRC_CLKIN     = (0b01 << 2),
		CLK_SRC_MS_Group  = (0b10 << 2),
		CLK_SRC_MS_Self   = (0b11 << 2),

		CLK_INV_Mask      = (1 << 4),
		CLK_INV_Normal    = (0 << 4),
		CLK_INV_Invert    = (1 << 4),

		MS_SRC_Mask       = (1 << 5),
		MS_SRC_PLLA       = (0 << 5),
		MS_SRC_PLLB       = (1 << 5),

		MS_INT_Mask       = (1 << 6),
		MS_INT_Fractional = (0 << 6),
		MS_INT_Integer    = (1 << 6),

		CLK_PDN_Mask      = (1 << 7),
		CLK_PDN_Power_On  = (0 << 7),
		CLK_PDN_Power_Off = (1 << 7),
	};
}

using ClockControls = std::array<ClockControl::Type, 8>;

namespace CrystalInternalLoadCapacitance {
	using Type = uint8_t;

	enum {
		XTAL_CL_Mask      = (0b11 << 6),
		XTAL_CL_6pF       = (0b01 << 6),
		XTAL_CL_8pF       = (0b10 << 6),
		XTAL_CL_10pF      = (0b11 << 6),
	};
}

namespace PLLInputSource {
	using Type = uint8_t;

	enum {
		PLLA_Source_Mask  = (1 << 2),
		PLLA_Source_XTAL  = (0 << 2),
		PLLA_Source_CLKIN = (1 << 2),

		PLLB_Source_Mask  = (1 << 3),
		PLLB_Source_XTAL  = (0 << 3),
		PLLB_Source_CLKIN = (1 << 3),

		CLKIN_Div_Mask    = (0b11 << 6),
		CLKIN_Div1        = (0b00 << 6),
		CLKIN_Div2        = (0b01 << 6),
		CLKIN_Div4        = (0b10 << 6),
		CLKIN_Div8        = (0b11 << 6),
	};
}

struct Inputs {
	const uint32_t f_xtal;
	const uint32_t f_clkin;
	const uint32_t clkin_div;

	constexpr uint32_t f_clkin_out() const {
		return f_clkin / clkin_div;
	}
};

using PLLReg = std::array<uint8_t, 9>;

struct PLL {
	const uint32_t f_in;
	const uint32_t a;
	const uint32_t b;
	const uint32_t c;

	constexpr uint32_t f_vco() const {
		return f_in * (a + (float)b / (float)c);
	}

	constexpr uint32_t p1() const {
		return 128 * a + (uint32_t)(128 * (float)b / (float)c) - 512;
	}

	constexpr uint32_t p2() const {
		return 128 * b - c * (uint32_t)(128 * (float)b / (float)c);
	}

	constexpr uint32_t p3() const {
		return c;
	}

	constexpr PLLReg reg(const uint8_t pll_n) const {
		return {
			uint8_t(26 + (pll_n * 8)),
			uint8_t((p3() >>  8) & 0xff),
			uint8_t((p3() >>  0) & 0xff),
			uint8_t((p1() >> 16) & 0x03),
			uint8_t((p1() >>  8) & 0xff),
			uint8_t((p1() >>  0) & 0xff),
			uint8_t(
				  (((p3() >> 16) & 0x0f) << 4)
				|  ((p2() >> 16) & 0x0f)
			),
			uint8_t((p2() >>  8) & 0xff),
			uint8_t((p2() >>  0) & 0xff),
		};
	}
};

using MultisynthFractionalReg = std::array<uint8_t, 9>;

struct MultisynthFractional {
	const uint32_t f_src;
	const uint32_t a;
	const uint32_t b;
	const uint32_t c;
	const uint32_t r_div;

	constexpr uint32_t p1() const {
		return 128 * a + (uint32_t)(128 * (float)b / (float)c) - 512;
	}

	constexpr uint32_t p2() const {
		return 128 * b - c * (uint32_t)(128 * (float)b / (float)c);
	}

	constexpr uint32_t p3() const {
		return c;
	}

	constexpr uint32_t f_out() const {
		return f_src / (a + (float)b / (float)c) / (1 << r_div);
	}

	constexpr MultisynthFractionalReg reg(const uint8_t multisynth_n) const {
		return {
			uint8_t(42 + (multisynth_n * 8)),
			uint8_t((p3() >> 8) & 0xFF),
			uint8_t((p3() >> 0) & 0xFF),
			uint8_t((r_div << 4) | (0 << 2) | ((p1() >> 16) & 0x3)),
			uint8_t((p1() >> 8) & 0xFF),
			uint8_t((p1() >> 0) & 0xFF),
			uint8_t((((p3() >> 16) & 0xF) << 4) | (((p2() >> 16) & 0xF) << 0)),
			uint8_t((p2() >> 8) & 0xFF),
			uint8_t((p2() >> 0) & 0xFF)
		};
	}
};

struct MultisynthInteger {
	const uint32_t f_src;
	const uint32_t a;
	const uint32_t r_div;

	constexpr uint8_t p1() const {
		return a;
	}

	constexpr uint32_t f_out() const {
		return f_src / a / (1 << r_div);
	}
};

using Multisynth6And7Reg = std::array<uint8_t, 4>;

constexpr Multisynth6And7Reg ms6_7_reg(
	const MultisynthInteger& ms6,
	const MultisynthInteger& ms7
) {
	return {
		Register::Multisynth6Parameters,
		uint8_t(ms6.p1() & 0xff),
		uint8_t(ms7.p1() & 0xff),
		uint8_t(((ms7.r_div & 7) << 4) | ((ms6.r_div & 7) << 0)),
	};
}

class Si5351 {
public:
	using regvalue_t = uint8_t;

	constexpr Si5351(I2C& bus, I2C::address_t address) :
		_clock_control({
			ClockControl::CLK_PDN_Power_Off, ClockControl::CLK_PDN_Power_Off,
			ClockControl::CLK_PDN_Power_Off, ClockControl::CLK_PDN_Power_Off,
			ClockControl::CLK_PDN_Power_Off, ClockControl::CLK_PDN_Power_Off,
			ClockControl::CLK_PDN_Power_Off, ClockControl::CLK_PDN_Power_Off
		}),
		_bus(bus),
		_address(address),
		_output_enable(0x00)
	{
	}

	void reset();

	void wait_for_device_ready() {
		while(device_status() & 0x80);
	}

	void enable_fanout() {
		write_register(Register::FanoutEnable, 0b11010000);
	}

	void reset_plls() {
		write_register(Register::PLLReset, 0xa0);
	}

	regvalue_t read_register(const uint8_t reg);

	template<size_t N>
	void write(const std::array<uint8_t, N>& values) {
		_bus.transmit(_address, values.data(), values.size());
	}

	void write_register(const uint8_t reg, const regvalue_t value) {
		write(std::array<uint8_t, 2>{
			reg, value
		});
	}

	void write(const size_t ms_number, const MultisynthFractional& config) {
		write(config.reg(ms_number));
	}

	void set_ms_frequency(
		const size_t ms_number,
		const uint32_t frequency,
		const uint32_t vco_frequency,
		const size_t r_div
	);

	void set_crystal_internal_load_capacitance(const CrystalInternalLoadCapacitance::Type xtal_cl) {
		write_register(Register::CrystalInternalLoadCapacitance, xtal_cl);
	}

	void set_pll_input_sources(const PLLInputSource::Type value) {
		write_register(Register::PLLInputSource, value);
	}

	void enable_output_mask(const uint8_t mask) {
		_output_enable |= mask;
		update_output_enable_control();
	}

	void enable_output(const size_t n) {
		enable_output_mask(1 << n);
	}

	void disable_output_mask(const uint8_t mask) {
		_output_enable &= ~mask;
		update_output_enable_control();
	}

	void disable_output(const size_t n) {
		disable_output_mask(1 << n);
	}

	void set_clock_control(const ClockControls& clock_control) {
		_clock_control = clock_control;
		update_all_clock_control();
	}

	void enable_clock(const size_t n) {
		_clock_control[n] &= ~ClockControl::CLK_PDN_Mask;
		write_register(Register::CLKControl_Base + n, _clock_control[n]);
	}

	void disable_clock(const size_t n) {
		_clock_control[n] |= ClockControl::CLK_PDN_Mask;
		write_register(Register::CLKControl_Base + n, _clock_control[n]);
	}
	
	bool clkin_status() {
		return ((device_status() & DeviceStatus::LOS_Mask) == DeviceStatus::LOS_ValidClockAtCLKIN);
	}
	
	template<size_t N>
	void write_registers(const uint8_t reg, const std::array<uint8_t, N>& values) {
		std::array<uint8_t, N + 1> data;
		data[0] = reg;
		std::copy(values.cbegin(), values.cend(), data.begin() + 1);
		write(data);
	}

private:
	std::array<uint8_t, 8> _clock_control;
	I2C& _bus;
	const I2C::address_t _address;
	uint8_t _output_enable;

	uint8_t device_status() {
		return read_register(Register::DeviceStatus);
	}
	
	void update_output_enable_control() {
		write_register(Register::OutputEnableControl, ~_output_enable);
	}

	void update_all_clock_control() {
		write_registers(Register::CLKControl_Base, _clock_control);
	}
};

}

#endif/*__SI5351_H__*/
