/*
 * Copyright (c) 2020, Seagate Technology LLC
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nxp_lpc11u6x_syscon

#include <zephyr/devicetree.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>

#include <zephyr/drivers/clock_control/lpc11u6x_clock_control.h>

#include "clock_control_lpc11u6x.h"

static void syscon_power_up(struct lpc11u6x_syscon_regs *syscon,
			    uint32_t bit, bool enable)
{
	if (enable) {
		syscon->pd_run_cfg = (syscon->pd_run_cfg & ~bit)
			| LPC11U6X_PDRUNCFG_MASK;
	} else {
		syscon->pd_run_cfg = syscon->pd_run_cfg | bit
			| LPC11U6X_PDRUNCFG_MASK;
	}
}

static void syscon_set_pll_src(struct lpc11u6x_syscon_regs *syscon,
			       uint32_t src)
{
	syscon->sys_pll_clk_sel = src;
	syscon->sys_pll_clk_uen = 0;
	syscon->sys_pll_clk_uen = 1;
}

static void set_flash_access_time(uint32_t nr_cycles)
{
	uint32_t *reg = (uint32_t *) LPC11U6X_FLASH_TIMING_REG;

	*reg = (*reg & (~LPC11U6X_FLASH_TIMING_MASK)) | nr_cycles;
}

static void syscon_setup_pll(struct lpc11u6x_syscon_regs *syscon,
			     uint32_t msel, uint32_t psel)
{
	uint32_t val = msel & LPC11U6X_SYS_PLL_CTRL_MSEL_MASK;

	val |= (psel & LPC11U6X_SYS_PLL_CTRL_PSEL_MASK) <<
		LPC11U6X_SYS_PLL_CTRL_PSEL_SHIFT;
	syscon->sys_pll_ctrl = val;
}

static bool syscon_pll_locked(struct lpc11u6x_syscon_regs *syscon)
{
	return (syscon->sys_pll_stat & 0x1) != 0;
}

static void syscon_set_main_clock_source(struct lpc11u6x_syscon_regs *syscon,
					 uint32_t src)
{
	syscon->main_clk_sel = src;
	syscon->main_clk_uen = 0;
	syscon->main_clk_uen = 1;
}

static void syscon_ahb_clock_enable(struct lpc11u6x_syscon_regs *syscon,
				    uint32_t mask, bool enable)
{
	if (enable) {
		syscon->sys_ahb_clk_ctrl |= mask;
	} else {
		syscon->sys_ahb_clk_ctrl &= ~mask;
	}
}

static void syscon_peripheral_reset(struct lpc11u6x_syscon_regs *syscon,
				    uint32_t mask, bool reset)
{
	if (reset) {
		syscon->p_reset_ctrl &= ~mask;
	} else {
		syscon->p_reset_ctrl |= mask;
	}
}
static void syscon_frg_init(struct lpc11u6x_syscon_regs *syscon)
{
	uint32_t div;

	div = CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC / LPC11U6X_USART_CLOCK_RATE;
	if (!div) {
		div = 1;
	}
	syscon->frg_clk_div = div;

	syscon_peripheral_reset(syscon, LPC11U6X_PRESET_CTRL_FRG, false);
	syscon->uart_frg_div = 0xFF;
	syscon->uart_frg_mult = ((CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC / div)
				 * 256) / LPC11U6X_USART_CLOCK_RATE;
}

static void syscon_frg_deinit(struct lpc11u6x_syscon_regs *syscon)
{
	syscon->uart_frg_div = 0x0;
	syscon_peripheral_reset(syscon, LPC11U6X_PRESET_CTRL_FRG, true);
}

static int lpc11u6x_clock_control_on(const struct device *dev,
				     clock_control_subsys_t sub_system)
{
	const struct lpc11u6x_syscon_config *cfg = dev->config;
	struct lpc11u6x_syscon_data *data = dev->data;
	uint32_t clk_mask = 0, reset_mask = 0;
	int ret = 0, init_frg = 0;

	k_mutex_lock(&data->mutex, K_FOREVER);

	switch ((int) sub_system) {
	case LPC11U6X_CLOCK_I2C0:
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_I2C0;
		reset_mask = LPC11U6X_PRESET_CTRL_I2C0;
		break;
	case LPC11U6X_CLOCK_I2C1:
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_I2C1;
		reset_mask = LPC11U6X_PRESET_CTRL_I2C1;
		break;
	case LPC11U6X_CLOCK_GPIO:
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_GPIO |
			LPC11U6X_SYS_AHB_CLK_CTRL_PINT;
		break;
	case LPC11U6X_CLOCK_USART0:
		cfg->syscon->usart0_clk_div = 1;
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART0;
		break;
	case LPC11U6X_CLOCK_USART1:
		if (!data->frg_in_use++) {
			init_frg = 1;
		}
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART1;
		reset_mask = LPC11U6X_PRESET_CTRL_USART1;
		break;
	case LPC11U6X_CLOCK_USART2:
		if (!data->frg_in_use++) {
			init_frg = 1;
		}
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART2;
		reset_mask = LPC11U6X_PRESET_CTRL_USART2;
		break;
	case LPC11U6X_CLOCK_USART3:
		if (!data->frg_in_use++) {
			init_frg = 1;
		}
		data->usart34_in_use++;
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART3_4;
		reset_mask = LPC11U6X_PRESET_CTRL_USART3;
		break;
	case LPC11U6X_CLOCK_USART4:
		if (!data->frg_in_use++) {
			init_frg = 1;
		}
		data->usart34_in_use++;
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART3_4;
		reset_mask = LPC11U6X_PRESET_CTRL_USART4;
		break;
	default:
		k_mutex_unlock(&data->mutex);
		return -EINVAL;
	}

	syscon_ahb_clock_enable(cfg->syscon, clk_mask, true);
	if (init_frg) {
		syscon_frg_init(cfg->syscon);
	}
	syscon_peripheral_reset(cfg->syscon, reset_mask, false);
	k_mutex_unlock(&data->mutex);

	return ret;
}

static int lpc11u6x_clock_control_off(const struct device *dev,
				      clock_control_subsys_t sub_system)
{
	const struct lpc11u6x_syscon_config *cfg = dev->config;
	struct lpc11u6x_syscon_data *data = dev->data;
	uint32_t clk_mask = 0, reset_mask = 0;
	int ret = 0, deinit_frg = 0;

	k_mutex_lock(&data->mutex, K_FOREVER);

	switch ((int) sub_system) {
	case LPC11U6X_CLOCK_I2C0:
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_I2C0;
		reset_mask = LPC11U6X_PRESET_CTRL_I2C0;
		break;
	case LPC11U6X_CLOCK_I2C1:
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_I2C1;
		reset_mask = LPC11U6X_PRESET_CTRL_I2C1;
		break;
	case LPC11U6X_CLOCK_GPIO:
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_GPIO |
			LPC11U6X_SYS_AHB_CLK_CTRL_PINT;
		break;
	case LPC11U6X_CLOCK_USART0:
		cfg->syscon->usart0_clk_div = 0;
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART0;
		break;
	case LPC11U6X_CLOCK_USART1:
		if (!(--data->frg_in_use)) {
			deinit_frg = 1;
		}
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART1;
		reset_mask = LPC11U6X_PRESET_CTRL_USART1;
		break;
	case LPC11U6X_CLOCK_USART2:
		if (!(--data->frg_in_use)) {
			deinit_frg = 1;
		}
		clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART2;
		reset_mask = LPC11U6X_PRESET_CTRL_USART2;
		break;
	case LPC11U6X_CLOCK_USART3:
		if (!(--data->frg_in_use)) {
			deinit_frg = 1;
		}
		if (!(--data->usart34_in_use)) {
			clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART3_4;
		}
		reset_mask = LPC11U6X_PRESET_CTRL_USART3;
		break;
	case LPC11U6X_CLOCK_USART4:
		if (!(--data->frg_in_use)) {
			deinit_frg = 1;
		}
		if (!(--data->usart34_in_use)) {
			clk_mask = LPC11U6X_SYS_AHB_CLK_CTRL_USART3_4;
		}
		reset_mask = LPC11U6X_PRESET_CTRL_USART4;
		break;
	default:
		k_mutex_unlock(&data->mutex);
		return -EINVAL;
	}

	syscon_ahb_clock_enable(cfg->syscon, clk_mask, false);
	if (deinit_frg) {
		syscon_frg_deinit(cfg->syscon);
	}
	syscon_peripheral_reset(cfg->syscon, reset_mask, true);
	k_mutex_unlock(&data->mutex);
	return ret;

}

static int lpc11u6x_clock_control_get_rate(const struct device *dev,
					   clock_control_subsys_t sub_system,
					   uint32_t *rate)
{
	switch ((int) sub_system) {
	case LPC11U6X_CLOCK_I2C0:
	case LPC11U6X_CLOCK_I2C1:
	case LPC11U6X_CLOCK_GPIO:
	case LPC11U6X_CLOCK_USART0:
		*rate = CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC;
		break;
	case LPC11U6X_CLOCK_USART1:
	case LPC11U6X_CLOCK_USART2:
	case LPC11U6X_CLOCK_USART3:
	case LPC11U6X_CLOCK_USART4:
		*rate = LPC11U6X_USART_CLOCK_RATE;
		break;
	default:
		return -EINVAL;
	}
	return 0;
}

static int lpc11u6x_syscon_init(const struct device *dev)
{
	const struct lpc11u6x_syscon_config *cfg = dev->config;
	struct lpc11u6x_syscon_data *data = dev->data;
	uint32_t val;

	k_mutex_init(&data->mutex);
	data->frg_in_use = 0;
	data->usart34_in_use = 0;
	/* Enable SRAM1 and USB ram if needed */
	val = 0;
#ifdef CONFIG_CLOCK_CONTROL_LPC11U6X_ENABLE_SRAM1
	val |= LPC11U6X_SYS_AHB_CLK_CTRL_SRAM1;
#endif /* CONFIG_CLOCK_CONTROL_LPC11U6X_ENABLE_SRAM1 */
#ifdef CONFIG_CLOCK_CONTROL_LPC11U6X_ENABLE_USB_RAM
	val |= LPC11U6X_SYS_AHB_CLK_CTRL_USB_SRAM;
#endif /* CONFIG_CLOCK_CONTROL_LPC11U6X_ENABLE_USB_RAM */

	/* Enable IOCON (I/O Control) clock. */
	val |= LPC11U6X_SYS_AHB_CLK_CTRL_IOCON;

	syscon_ahb_clock_enable(cfg->syscon, val, true);

	/* Configure PLL output as the main clock source, with a frequency of
	 * 48MHz
	 */
#ifdef CONFIG_CLOCK_CONTROL_LPC11U6X_PLL_SRC_SYSOSC
	syscon_power_up(cfg->syscon, LPC11U6X_PDRUNCFG_SYSOSC_PD, true);

	/* Wait ~500us */
	for (int i = 0; i < 2500; i++) {
	}

	/* Configure PLL input */
	syscon_set_pll_src(cfg->syscon, LPC11U6X_SYS_PLL_CLK_SEL_SYSOSC);

	pinctrl_apply_state(cfg->pincfg, PINCTRL_STATE_DEFAULT);

#elif defined(CONFIG_CLOCK_CONTROL_LPC11U6X_PLL_SRC_IRC)
	syscon_power_up(cfg->syscon, LPC11U6X_PDRUNCFG_IRC_PD, true);
	syscon_set_pll_src(cfg->syscon, LPC11U6X_SYS_PLL_CLK_SEL_IRC);
#endif
	/* Flash access takes 3 clock cycles for main clock frequencies
	 * between 40MHz and 50MHz
	 */
	set_flash_access_time(LPC11U6X_FLASH_TIMING_3CYCLES);

	/* Shutdown PLL to change divider/mult ratios */
	syscon_power_up(cfg->syscon, LPC11U6X_PDRUNCFG_PLL_PD, false);

	/* Setup PLL to have 48MHz output */
	syscon_setup_pll(cfg->syscon, 3, 1);

	/* Power up pll and wait */
	syscon_power_up(cfg->syscon, LPC11U6X_PDRUNCFG_PLL_PD, true);

	while (!syscon_pll_locked(cfg->syscon)) {
	}

	cfg->syscon->sys_ahb_clk_div = 1;
	syscon_set_main_clock_source(cfg->syscon, LPC11U6X_MAIN_CLK_SRC_PLLOUT);
	return 0;
}

static DEVICE_API(clock_control, lpc11u6x_clock_control_api) = {
	.on = lpc11u6x_clock_control_on,
	.off = lpc11u6x_clock_control_off,
	.get_rate = lpc11u6x_clock_control_get_rate,
};


PINCTRL_DT_INST_DEFINE(0);

static const struct lpc11u6x_syscon_config syscon_config = {
	.syscon = (struct lpc11u6x_syscon_regs *) DT_INST_REG_ADDR(0),
	.pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(0),
};

static struct lpc11u6x_syscon_data syscon_data;

DEVICE_DT_INST_DEFINE(0,
		    lpc11u6x_syscon_init,
		    NULL,
		    &syscon_data, &syscon_config,
		    PRE_KERNEL_1, CONFIG_CLOCK_CONTROL_INIT_PRIORITY,
		    &lpc11u6x_clock_control_api);
