/*
 * Copyright (c) 2023 Zephyr Project
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT ti_cc13xx_cc26xx_timer_pwm

#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/drivers/pwm.h>

#include <driverlib/gpio.h>
#include <driverlib/prcm.h>
#include <driverlib/timer.h>
#include <inc/hw_memmap.h>
#include <inc/hw_types.h>
#include <ti/drivers/Power.h>
#include <ti/drivers/power/PowerCC26XX.h>

#include <zephyr/logging/log.h>
#define LOG_MODULE_NAME pwm_cc13xx_cc26xx_timer
LOG_MODULE_REGISTER(LOG_MODULE_NAME, CONFIG_PWM_LOG_LEVEL);

/* TODO: Clock frequency can be settable via KConfig, see TOP:PRCM:GPTCLKDIV */
#define CPU_FREQ ((uint32_t)DT_PROP(DT_PATH(cpus, cpu_0), clock_frequency))

/* GPT peripherals in 16 bit mode have maximum 24 counter bits incl. the
 * prescaler. Count is set to (2^24 - 2) to allow for a glitch free 100% duty
 * cycle at max. period count.
 */
#define PWM_COUNT_MAX      0xFFFFFE
#define PWM_INITIAL_PERIOD PWM_COUNT_MAX
#define PWM_INITIAL_DUTY   0U /* initially off */

struct pwm_cc13xx_cc26xx_data {
};

struct pwm_cc13xx_cc26xx_config {
	const uint32_t gpt_base; /* GPT register base address */
	const struct pinctrl_dev_config *pcfg;

	LOG_INSTANCE_PTR_DECLARE(log);
};

static void write_value(const struct pwm_cc13xx_cc26xx_config *config, uint32_t value,
			uint32_t prescale_register, uint32_t value_register)
{
	/* Upper byte represents the prescaler value. */
	uint8_t prescaleValue = 0xff & (value >> 16);

	HWREG(config->gpt_base + prescale_register) = prescaleValue;

	/* The remaining bytes represent the load / match value. */
	HWREG(config->gpt_base + value_register) = value & 0xffff;
}

static int set_period_and_pulse(const struct pwm_cc13xx_cc26xx_config *config, uint32_t period,
				uint32_t pulse)
{
	uint32_t match_value = pulse;

	if (pulse == 0U) {
		TimerDisable(config->gpt_base, TIMER_B);
#ifdef CONFIG_PM
		Power_releaseConstraint(PowerCC26XX_DISALLOW_STANDBY);
#endif
		match_value = period + 1;
	}

	/* Fail if period is out of range */
	if ((period > PWM_COUNT_MAX) || (period == 0)) {
		LOG_ERR("Period (%d) is out of range.", period);
		return -EINVAL;
	}

	/* Compare to new period and fail if invalid */
	if (period < (match_value - 1) || (match_value < 0)) {
		LOG_ERR("Period (%d) is shorter than pulse (%d).", period, pulse);
		return -EINVAL;
	}

	/* Store new period and update timer */
	write_value(config, period, GPT_O_TBPR, GPT_O_TBILR);
	write_value(config, match_value, GPT_O_TBPMR, GPT_O_TBMATCHR);

	if (pulse > 0U) {
#ifdef CONFIG_PM
		Power_setConstraint(PowerCC26XX_DISALLOW_STANDBY);
#endif
		TimerEnable(config->gpt_base, TIMER_B);
	}

	LOG_DBG("Period and pulse successfully set.");
	return 0;
}

static int set_cycles(const struct device *dev, uint32_t channel, uint32_t period, uint32_t pulse,
		      pwm_flags_t flags)
{
	const struct pwm_cc13xx_cc26xx_config *config = dev->config;

	if (channel != 0) {
		return -EIO;
	}

	set_period_and_pulse(config, period, pulse);

	return 0;
}

static int get_cycles_per_sec(const struct device *dev, uint32_t channel, uint64_t *cycles)
{
	if (channel > 0) {
		return -EIO;
	}

	if (cycles) {
		*cycles = CPU_FREQ;
	}

	return 0;
}

static const struct pwm_driver_api pwm_driver_api = {
	.set_cycles = set_cycles,
	.get_cycles_per_sec = get_cycles_per_sec,
};

#ifdef CONFIG_PM
static int get_timer_inst_number(const struct pwm_cc13xx_cc26xx_config *config)
{
	switch (config->gpt_base) {
	case GPT0_BASE:
		return 0;
	case GPT1_BASE:
		return 1;
	case GPT2_BASE:
		return 2;
	case GPT3_BASE:
		return 3;
	default:
		__ASSERT_UNREACHABLE;
	}
}
#else
static int get_timer_peripheral(const struct pwm_cc13xx_cc26xx_config *config)
{
	switch (config->gpt_base) {
	case GPT0_BASE:
		return PRCM_PERIPH_TIMER0;
	case GPT1_BASE:
		return PRCM_PERIPH_TIMER1;
	case GPT2_BASE:
		return PRCM_PERIPH_TIMER2;
	case GPT3_BASE:
		return PRCM_PERIPH_TIMER3;
	default:
		__ASSERT_UNREACHABLE;
	}
}
#endif /* CONFIG_PM */

static int init_pwm(const struct device *dev)
{
	const struct pwm_cc13xx_cc26xx_config *config = dev->config;
	pinctrl_soc_pin_t pin = config->pcfg->states[0].pins[0];
	int ret;

#ifdef CONFIG_PM
	/* Set dependency on gpio resource to turn on power domains */
	Power_setDependency(get_timer_inst_number(config));
#else
	/* Enable peripheral power domain. */
	PRCMPowerDomainOn(PRCM_DOMAIN_PERIPH);

	/* Enable GPIO peripheral. */
	PRCMPeripheralRunEnable(get_timer_peripheral(config));

	/* Load PRCM settings. */
	PRCMLoadSet();
	while (!PRCMLoadGet()) {
		continue;
	}
#endif /* CONFIG_PM */

	ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
	if (ret < 0) {
		LOG_ERR("failed to setup PWM pinctrl");
		return ret;
	}

	/* Configures the PWM idle output level.
	 *
	 * TODO: Make PWM idle high/low configurable via custom DT PWM flag.
	 */
	GPIO_writeDio(pin.pin, 0);

	GPIO_setOutputEnableDio(pin.pin, GPIO_OUTPUT_ENABLE);

	/* Peripheral should not be accessed until power domain is on. */
	while (PRCMPowerDomainsAllOn(PRCM_DOMAIN_PERIPH) != PRCM_DOMAIN_POWER_ON) {
		continue;
	}

	TimerDisable(config->gpt_base, TIMER_B);

	HWREG(config->gpt_base + GPT_O_CFG) = GPT_CFG_CFG_16BIT_TIMER;
	/* Stall timer when debugging.
	 *
	 * TODO: Make debug stall configurable via custom DT prop.
	 */
	HWREG(config->gpt_base + GPT_O_CTL) |= GPT_CTL_TBSTALL;

	/* TODO: Make PWM polarity configurable via DT PWM flag. */
	HWREG(config->gpt_base + GPT_O_TBMR) = GPT_TBMR_TBAMS_PWM | GPT_TBMR_TBMRSU_TOUPDATE |
					       GPT_TBMR_TBPWMIE_EN | GPT_TBMR_TBMR_PERIODIC;

	set_period_and_pulse(config, PWM_INITIAL_PERIOD, PWM_INITIAL_DUTY);

	return 0;
}

#define DT_TIMER(idx)           DT_INST_PARENT(idx)
#define DT_TIMER_BASE_ADDR(idx) (DT_REG_ADDR(DT_TIMER(idx)))

#define PWM_DEVICE_INIT(idx)                                                                       \
	PINCTRL_DT_INST_DEFINE(idx);                                                               \
	LOG_INSTANCE_REGISTER(LOG_MODULE_NAME, idx, CONFIG_PWM_LOG_LEVEL);                         \
	static const struct pwm_cc13xx_cc26xx_config pwm_cc13xx_cc26xx_##idx##_config = {          \
		.gpt_base = DT_TIMER_BASE_ADDR(idx),                                               \
		.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(idx),                                       \
		LOG_INSTANCE_PTR_INIT(log, LOG_MODULE_NAME, idx)};                                 \
                                                                                                   \
	static struct pwm_cc13xx_cc26xx_data pwm_cc13xx_cc26xx_##idx##_data;                       \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(idx, init_pwm, NULL, &pwm_cc13xx_cc26xx_##idx##_data,                \
			      &pwm_cc13xx_cc26xx_##idx##_config, POST_KERNEL,                      \
			      CONFIG_PWM_INIT_PRIORITY, &pwm_driver_api)

DT_INST_FOREACH_STATUS_OKAY(PWM_DEVICE_INIT);
