/*
 * Copyright (c) 2023 Nordic Semiconductor ASA
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nordic_npm1300_regulator

#include <errno.h>
#include <string.h>

#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/regulator.h>
#include <zephyr/drivers/mfd/npm1300.h>
#include <zephyr/dt-bindings/regulator/npm1300.h>
#include <zephyr/sys/linear_range.h>
#include <zephyr/sys/util.h>

/* nPM1300 voltage sources */
enum npm1300_sources {
	NPM1300_SOURCE_BUCK1,
	NPM1300_SOURCE_BUCK2,
	NPM1300_SOURCE_LDO1,
	NPM1300_SOURCE_LDO2,
};

/* nPM1300 gpio control channels */
enum npm1300_gpio_type {
	NPM1300_GPIO_TYPE_ENABLE,
	NPM1300_GPIO_TYPE_RETENTION,
	NPM1300_GPIO_TYPE_PWM
};

/* nPM1300 regulator base addresses */
#define BUCK_BASE 0x04U
#define LDSW_BASE 0x08U
#define SHIP_BASE 0x0BU

/* nPM1300 regulator register offsets */
#define BUCK_OFFSET_EN_SET    0x00U
#define BUCK_OFFSET_EN_CLR    0x01U
#define BUCK_OFFSET_PWM_SET   0x04U
#define BUCK_OFFSET_PWM_CLR   0x05U
#define BUCK_OFFSET_VOUT_NORM 0x08U
#define BUCK_OFFSET_VOUT_RET  0x09U
#define BUCK_OFFSET_EN_CTRL   0x0CU
#define BUCK_OFFSET_VRET_CTRL 0x0DU
#define BUCK_OFFSET_PWM_CTRL  0x0EU
#define BUCK_OFFSET_SW_CTRL   0x0FU
#define BUCK_OFFSET_VOUT_STAT 0x10U
#define BUCK_OFFSET_CTRL0     0x15U

/* nPM1300 ldsw register offsets */
#define LDSW_OFFSET_EN_SET  0x00U
#define LDSW_OFFSET_EN_CLR  0x01U
#define LDSW_OFFSET_STATUS  0x04U
#define LDSW_OFFSET_GPISEL  0x05U
#define LDSW_OFFSET_CONFIG  0x07U
#define LDSW_OFFSET_LDOSEL  0x08U
#define LDSW_OFFSET_VOUTSEL 0x0CU

/* nPM1300 ship register offsets */
#define SHIP_OFFSET_SHIP 0x02U

struct regulator_npm1300_pconfig {
	const struct device *mfd;
	struct gpio_dt_spec dvs_state_pins[5];
};

struct regulator_npm1300_config {
	struct regulator_common_config common;
	const struct device *mfd;
	uint8_t source;
	int32_t retention_uv;
	struct gpio_dt_spec enable_gpios;
	struct gpio_dt_spec retention_gpios;
	struct gpio_dt_spec pwm_gpios;
};

struct regulator_npm1300_data {
	struct regulator_common_data data;
};

/* Linear range for output voltage, common for all bucks and LDOs on this device */
static const struct linear_range buckldo_range = LINEAR_RANGE_INIT(1000000, 100000, 0U, 23U);

unsigned int regulator_npm1300_count_voltages(const struct device *dev)
{
	const struct regulator_npm1300_config *config = dev->config;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
	case NPM1300_SOURCE_BUCK2:
	case NPM1300_SOURCE_LDO1:
	case NPM1300_SOURCE_LDO2:
		return linear_range_values_count(&buckldo_range);
	default:
		return 0;
	}
}

int regulator_npm1300_list_voltage(const struct device *dev, unsigned int idx, int32_t *volt_uv)
{
	const struct regulator_npm1300_config *config = dev->config;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
	case NPM1300_SOURCE_BUCK2:
	case NPM1300_SOURCE_LDO1:
	case NPM1300_SOURCE_LDO2:
		return linear_range_get_value(&buckldo_range, idx, volt_uv);
	default:
		return -EINVAL;
	}
}

static int retention_set_voltage(const struct device *dev, int32_t retention_uv)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint16_t idx;
	uint8_t chan;
	int ret;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
		chan = 0U;
		break;
	case NPM1300_SOURCE_BUCK2:
		chan = 1U;
		break;
	default:
		return -ENOTSUP;
	}

	ret = linear_range_get_win_index(&buckldo_range, retention_uv, retention_uv, &idx);

	if (ret == -EINVAL) {
		return ret;
	}

	return mfd_npm1300_reg_write(config->mfd, BUCK_BASE, BUCK_OFFSET_VOUT_RET + (chan * 2U),
				     idx);
}

static int buck_get_voltage_index(const struct device *dev, uint8_t chan, uint8_t *idx)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint8_t sel;
	int ret;

	ret = mfd_npm1300_reg_read(config->mfd, BUCK_BASE, BUCK_OFFSET_SW_CTRL, &sel);

	if (ret < 0) {
		return ret;
	}

	if ((sel >> chan) & 1U) {
		/* SW control */
		return mfd_npm1300_reg_read(config->mfd, BUCK_BASE,
					    BUCK_OFFSET_VOUT_NORM + (chan * 2U), idx);
	}

	/* VSET pin control */
	return mfd_npm1300_reg_read(config->mfd, BUCK_BASE, BUCK_OFFSET_VOUT_STAT + chan, idx);
}

static int buck_set_voltage(const struct device *dev, uint8_t chan, int32_t min_uv, int32_t max_uv)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint8_t mask;
	uint8_t curr_idx;
	uint16_t idx;
	int ret;

	ret = linear_range_get_win_index(&buckldo_range, min_uv, max_uv, &idx);

	if (ret == -EINVAL) {
		return ret;
	}

	/* Get current setting, and return if current and new index match */
	ret = buck_get_voltage_index(dev, chan, &curr_idx);

	if ((ret < 0) || (idx == curr_idx)) {
		return ret;
	}

	ret = mfd_npm1300_reg_write(config->mfd, BUCK_BASE, BUCK_OFFSET_VOUT_NORM + (chan * 2U),
				    idx);

	if (ret < 0) {
		return ret;
	}

	/* Enable SW control of buck output */
	mask = BIT(chan);
	return mfd_npm1300_reg_update(config->mfd, BUCK_BASE, BUCK_OFFSET_SW_CTRL, mask, mask);
}

static int ldo_set_voltage(const struct device *dev, uint8_t chan, int32_t min_uv, int32_t max_uv)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint16_t idx;
	int ret;

	ret = linear_range_get_win_index(&buckldo_range, min_uv, max_uv, &idx);

	if (ret == -EINVAL) {
		return ret;
	}

	return mfd_npm1300_reg_write(config->mfd, LDSW_BASE, LDSW_OFFSET_VOUTSEL + chan, idx);
}

int regulator_npm1300_set_voltage(const struct device *dev, int32_t min_uv, int32_t max_uv)
{
	const struct regulator_npm1300_config *config = dev->config;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
		return buck_set_voltage(dev, 0, min_uv, max_uv);
	case NPM1300_SOURCE_BUCK2:
		return buck_set_voltage(dev, 1, min_uv, max_uv);
	case NPM1300_SOURCE_LDO1:
		return ldo_set_voltage(dev, 0, min_uv, max_uv);
	case NPM1300_SOURCE_LDO2:
		return ldo_set_voltage(dev, 1, min_uv, max_uv);
	default:
		return -ENODEV;
	}
}

static int buck_get_voltage(const struct device *dev, uint8_t chan, int32_t *volt_uv)
{
	uint8_t idx;
	int ret;

	ret = buck_get_voltage_index(dev, chan, &idx);

	if (ret < 0) {
		return ret;
	}

	return linear_range_get_value(&buckldo_range, idx, volt_uv);
}

static int ldo_get_voltage(const struct device *dev, uint8_t chan, int32_t *volt_uv)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint8_t idx;
	int ret;

	ret = mfd_npm1300_reg_read(config->mfd, LDSW_BASE, LDSW_OFFSET_VOUTSEL + chan, &idx);

	if (ret < 0) {
		return ret;
	}

	return linear_range_get_value(&buckldo_range, idx, volt_uv);
}

int regulator_npm1300_get_voltage(const struct device *dev, int32_t *volt_uv)
{
	const struct regulator_npm1300_config *config = dev->config;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
		return buck_get_voltage(dev, 0, volt_uv);
	case NPM1300_SOURCE_BUCK2:
		return buck_get_voltage(dev, 1, volt_uv);
	case NPM1300_SOURCE_LDO1:
		return ldo_get_voltage(dev, 0, volt_uv);
	case NPM1300_SOURCE_LDO2:
		return ldo_get_voltage(dev, 1, volt_uv);
	default:
		return -ENODEV;
	}
}

static int set_buck_mode(const struct device *dev, uint8_t chan, regulator_mode_t mode)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint8_t pfm_mask = BIT(chan);
	uint8_t pfm_data;
	uint8_t pwm_reg;
	int ret;

	switch (mode) {
	case NPM1300_BUCK_MODE_PWM:
		pfm_data = 0U;
		pwm_reg = BUCK_OFFSET_PWM_SET;
		break;
	case NPM1300_BUCK_MODE_AUTO:
		pfm_data = 0U;
		pwm_reg = BUCK_OFFSET_PWM_CLR;
		break;
	case NPM1300_BUCK_MODE_PFM:
		pfm_data = pfm_mask;
		pwm_reg = BUCK_OFFSET_PWM_CLR;
		break;
	default:
		return -ENOTSUP;
	}

	ret = mfd_npm1300_reg_update(config->mfd, BUCK_BASE, BUCK_OFFSET_CTRL0, pfm_data, pfm_mask);
	if (ret < 0) {
		return ret;
	}

	return mfd_npm1300_reg_write(config->mfd, BUCK_BASE, pwm_reg + (chan * 2U), 1U);
}

static int set_ldsw_mode(const struct device *dev, uint8_t chan, regulator_mode_t mode)
{
	const struct regulator_npm1300_config *config = dev->config;

	switch (mode) {
	case NPM1300_LDSW_MODE_LDO:
		return mfd_npm1300_reg_write(config->mfd, LDSW_BASE, LDSW_OFFSET_LDOSEL + chan, 1U);
	case NPM1300_LDSW_MODE_LDSW:
		return mfd_npm1300_reg_write(config->mfd, LDSW_BASE, LDSW_OFFSET_LDOSEL + chan, 0U);
	default:
		return -ENOTSUP;
	}
}

int regulator_npm1300_set_mode(const struct device *dev, regulator_mode_t mode)
{
	const struct regulator_npm1300_config *config = dev->config;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
		return set_buck_mode(dev, 0, mode);
	case NPM1300_SOURCE_BUCK2:
		return set_buck_mode(dev, 1, mode);
	case NPM1300_SOURCE_LDO1:
		return set_ldsw_mode(dev, 0, mode);
	case NPM1300_SOURCE_LDO2:
		return set_ldsw_mode(dev, 1, mode);
	default:
		return -ENOTSUP;
	}
}

int regulator_npm1300_enable(const struct device *dev)
{
	const struct regulator_npm1300_config *config = dev->config;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
		return mfd_npm1300_reg_write(config->mfd, BUCK_BASE, BUCK_OFFSET_EN_SET, 1U);
	case NPM1300_SOURCE_BUCK2:
		return mfd_npm1300_reg_write(config->mfd, BUCK_BASE, BUCK_OFFSET_EN_SET + 2U, 1U);
	case NPM1300_SOURCE_LDO1:
		return mfd_npm1300_reg_write(config->mfd, LDSW_BASE, LDSW_OFFSET_EN_SET, 1U);
	case NPM1300_SOURCE_LDO2:
		return mfd_npm1300_reg_write(config->mfd, LDSW_BASE, LDSW_OFFSET_EN_SET + 2U, 1U);
	default:
		return 0;
	}
}

int regulator_npm1300_disable(const struct device *dev)
{
	const struct regulator_npm1300_config *config = dev->config;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
		return mfd_npm1300_reg_write(config->mfd, BUCK_BASE, BUCK_OFFSET_EN_CLR, 1U);
	case NPM1300_SOURCE_BUCK2:
		return mfd_npm1300_reg_write(config->mfd, BUCK_BASE, BUCK_OFFSET_EN_CLR + 2U, 1U);
	case NPM1300_SOURCE_LDO1:
		return mfd_npm1300_reg_write(config->mfd, LDSW_BASE, LDSW_OFFSET_EN_CLR, 1U);
	case NPM1300_SOURCE_LDO2:
		return mfd_npm1300_reg_write(config->mfd, LDSW_BASE, LDSW_OFFSET_EN_CLR + 2U, 1U);
	default:
		return 0;
	}
}

static int regulator_npm1300_set_buck_pin_ctrl(const struct device *dev, uint8_t chan, uint8_t pin,
					       uint8_t inv, enum npm1300_gpio_type type)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint8_t ctrl;
	uint8_t mask;

	switch (chan) {
	case 0:
		/* Invert control in bit 6, pin control in bits 2-0 */
		ctrl = (inv << 6U) | (pin + 1U);
		mask = BIT(6U) | BIT_MASK(3U);
		break;
	case 1:
		/* Invert control in bit 7, pin control in bits 5-3 */
		ctrl = (inv << 7U) | ((pin + 1U) << 3U);
		mask = BIT(7U) | (BIT_MASK(3U) << 3U);
		break;
	default:
		return -EINVAL;
	}

	switch (type) {
	case NPM1300_GPIO_TYPE_ENABLE:
		return mfd_npm1300_reg_update(config->mfd, BUCK_BASE, BUCK_OFFSET_EN_CTRL, ctrl,
					      mask);
	case NPM1300_GPIO_TYPE_PWM:
		return mfd_npm1300_reg_update(config->mfd, BUCK_BASE, BUCK_OFFSET_PWM_CTRL, ctrl,
					      mask);
	case NPM1300_GPIO_TYPE_RETENTION:
		return mfd_npm1300_reg_update(config->mfd, BUCK_BASE, BUCK_OFFSET_VRET_CTRL, ctrl,
					      mask);
	default:
		return -ENOTSUP;
	}
}

static int regulator_npm1300_set_ldsw_pin_ctrl(const struct device *dev, uint8_t chan, uint8_t pin,
					       uint8_t inv, enum npm1300_gpio_type type)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint8_t ctrl;

	if (type != NPM1300_GPIO_TYPE_ENABLE) {
		return -ENOTSUP;
	}

	ctrl = (pin + 1U) | (inv << 3U);

	return mfd_npm1300_reg_write(config->mfd, LDSW_BASE, LDSW_OFFSET_GPISEL + chan, type);
}

int regulator_npm1300_set_pin_ctrl(const struct device *dev, const struct gpio_dt_spec *spec,
				   enum npm1300_gpio_type type)
{
	const struct regulator_npm1300_config *config = dev->config;
	uint8_t inv;

	if (spec->port == NULL) {
		return 0;
	}

	inv = (spec->dt_flags & GPIO_ACTIVE_LOW) != 0U;

	switch (config->source) {
	case NPM1300_SOURCE_BUCK1:
		return regulator_npm1300_set_buck_pin_ctrl(dev, 0, spec->pin, inv, type);
	case NPM1300_SOURCE_BUCK2:
		return regulator_npm1300_set_buck_pin_ctrl(dev, 1, spec->pin, inv, type);
	case NPM1300_SOURCE_LDO1:
		return regulator_npm1300_set_ldsw_pin_ctrl(dev, 0, spec->pin, inv, type);
	case NPM1300_SOURCE_LDO2:
		return regulator_npm1300_set_ldsw_pin_ctrl(dev, 1, spec->pin, inv, type);
	default:
		return -ENODEV;
	}
}

int regulator_npm1300_dvs_state_set(const struct device *dev, regulator_dvs_state_t state)
{
	const struct regulator_npm1300_pconfig *pconfig = dev->config;
	const struct gpio_dt_spec *spec;
	int ret;

	for (size_t idx = 0U; idx < 5U; idx++) {
		spec = &pconfig->dvs_state_pins[idx];

		if (spec->port != NULL) {
			ret = gpio_pin_set_dt(spec, ((state >> idx) & 1U) != 0U);

			if (ret != 0) {
				return ret;
			}
		}
	}

	return 0;
}

int regulator_npm1300_ship_mode(const struct device *dev)
{
	const struct regulator_npm1300_pconfig *pconfig = dev->config;

	return mfd_npm1300_reg_write(pconfig->mfd, SHIP_BASE, SHIP_OFFSET_SHIP, 1U);
}

static const struct regulator_parent_driver_api parent_api = {
	.dvs_state_set = regulator_npm1300_dvs_state_set,
	.ship_mode = regulator_npm1300_ship_mode,
};

int regulator_npm1300_common_init(const struct device *dev)
{
	const struct regulator_npm1300_pconfig *pconfig = dev->config;
	const struct gpio_dt_spec *spec;
	int ret;

	for (size_t idx = 0U; idx < 5U; idx++) {
		spec = &pconfig->dvs_state_pins[idx];

		if (spec->port != NULL) {
			if (!gpio_is_ready_dt(spec)) {
				return -ENODEV;
			}

			ret = gpio_pin_configure_dt(spec, GPIO_OUTPUT);
			if (ret != 0) {
				return ret;
			}
		}
	}

	return 0;
}

int regulator_npm1300_init(const struct device *dev)
{
	const struct regulator_npm1300_config *config = dev->config;
	int ret = 0;

	if (!device_is_ready(config->mfd)) {
		return -ENODEV;
	}

	ret = regulator_common_init(dev, false);
	if (ret < 0) {
		return ret;
	}

	/* Configure retention voltage */
	if (config->retention_uv != 0) {
		ret = retention_set_voltage(dev, config->retention_uv);
		if (ret != 0) {
			return ret;
		}
	}

	/* Configure GPIO pin control */
	ret = regulator_npm1300_set_pin_ctrl(dev, &config->enable_gpios, NPM1300_GPIO_TYPE_ENABLE);
	if (ret != 0) {
		return ret;
	}

	ret = regulator_npm1300_set_pin_ctrl(dev, &config->retention_gpios,
					     NPM1300_GPIO_TYPE_RETENTION);
	if (ret != 0) {
		return ret;
	}

	ret = regulator_npm1300_set_pin_ctrl(dev, &config->pwm_gpios, NPM1300_GPIO_TYPE_PWM);
	if (ret != 0) {
		return ret;
	}

	return ret;
}

static const struct regulator_driver_api api = {.enable = regulator_npm1300_enable,
						.disable = regulator_npm1300_disable,
						.count_voltages = regulator_npm1300_count_voltages,
						.list_voltage = regulator_npm1300_list_voltage,
						.set_voltage = regulator_npm1300_set_voltage,
						.get_voltage = regulator_npm1300_get_voltage,
						.set_mode = regulator_npm1300_set_mode};

#define REGULATOR_NPM1300_DEFINE(node_id, id, _source)                                             \
	static struct regulator_npm1300_data data_##id;                                            \
                                                                                                   \
	static const struct regulator_npm1300_config config_##id = {                               \
		.common = REGULATOR_DT_COMMON_CONFIG_INIT(node_id),                                \
		.mfd = DEVICE_DT_GET(DT_GPARENT(node_id)),                                         \
		.source = _source,                                                                 \
		.retention_uv = DT_PROP_OR(node_id, retention_microvolt, 0),                       \
		.enable_gpios = GPIO_DT_SPEC_GET_OR(node_id, enable_gpios, {0}),                   \
		.retention_gpios = GPIO_DT_SPEC_GET_OR(node_id, retention_gpios, {0}),             \
		.pwm_gpios = GPIO_DT_SPEC_GET_OR(node_id, pwm_gpios, {0})};                        \
                                                                                                   \
	DEVICE_DT_DEFINE(node_id, regulator_npm1300_init, NULL, &data_##id, &config_##id,          \
			 POST_KERNEL, CONFIG_REGULATOR_NPM1300_INIT_PRIORITY, &api);

#define REGULATOR_NPM1300_DEFINE_COND(inst, child, source)                                         \
	COND_CODE_1(DT_NODE_EXISTS(DT_INST_CHILD(inst, child)),                                    \
		    (REGULATOR_NPM1300_DEFINE(DT_INST_CHILD(inst, child), child##inst, source)),   \
		    ())

#define REGULATOR_NPM1300_DEFINE_ALL(inst)                                                         \
	static const struct regulator_npm1300_pconfig config_##inst = {                            \
		.mfd = DEVICE_DT_GET(DT_INST_PARENT(inst)),                                        \
		.dvs_state_pins = {GPIO_DT_SPEC_INST_GET_BY_IDX_OR(inst, dvs_gpios, 0, {0}),       \
				   GPIO_DT_SPEC_INST_GET_BY_IDX_OR(inst, dvs_gpios, 1, {0}),       \
				   GPIO_DT_SPEC_INST_GET_BY_IDX_OR(inst, dvs_gpios, 2, {0}),       \
				   GPIO_DT_SPEC_INST_GET_BY_IDX_OR(inst, dvs_gpios, 3, {0}),       \
				   GPIO_DT_SPEC_INST_GET_BY_IDX_OR(inst, dvs_gpios, 4, {0})}};     \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(inst, regulator_npm1300_common_init, NULL, NULL, &config_##inst,     \
			      POST_KERNEL, CONFIG_REGULATOR_NPM1300_COMMON_INIT_PRIORITY,          \
			      &parent_api);                                                        \
                                                                                                   \
	REGULATOR_NPM1300_DEFINE_COND(inst, buck1, NPM1300_SOURCE_BUCK1)                           \
	REGULATOR_NPM1300_DEFINE_COND(inst, buck2, NPM1300_SOURCE_BUCK2)                           \
	REGULATOR_NPM1300_DEFINE_COND(inst, ldo1, NPM1300_SOURCE_LDO1)                             \
	REGULATOR_NPM1300_DEFINE_COND(inst, ldo2, NPM1300_SOURCE_LDO2)

DT_INST_FOREACH_STATUS_OKAY(REGULATOR_NPM1300_DEFINE_ALL)
