/*
 * Copyright (c) 2021 NXP
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nxp_pca9420

#include <errno.h>

#include <zephyr/kernel.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/regulator.h>
#include <zephyr/drivers/regulator/pca9420.h>
#include <zephyr/sys/linear_range.h>
#include <zephyr/sys/util.h>

/** Register memory map. See datasheet for more details. */
/** General purpose registers */
/** @brief Top level system ctrl 0 */
#define PCA9420_TOP_CNTL0     0x09U
/** @brief Top level system ctrl 3 */
#define PCA9420_TOP_CNTL3     0x0CU

/** Regulator status indication registers */
/** @brief Mode configuration for mode 0_0 */
#define PCA9420_MODECFG_0_0          0x22U
/** @brief Mode configuration for mode 0_1 */
#define PCA9420_MODECFG_0_1          0x23U
/** @brief Mode configuration for mode 0_2 */
#define PCA9420_MODECFG_0_2          0x24U
/** @brief Mode configuration for mode 0_3 */
#define PCA9420_MODECFG_0_3          0x25U

/** @brief VIN input current limit selection */
#define PCA9420_TOP_CNTL0_VIN_ILIM_SEL_POS 5U
#define PCA9420_TOP_CNTL0_VIN_ILIM_SEL_MASK 0xE0U
#define PCA9420_TOP_CNTL0_VIN_ILIM_SEL_DISABLED 0x7U

/** @brief I2C Mode control mask */
#define PCA9420_TOP_CNTL3_MODE_I2C_POS 3U
#define PCA9420_TOP_CNTL3_MODE_I2C_MASK 0x18U

/*
 * @brief Mode control selection mask. When this bit is set, the external
 * PMIC pins MODESEL0 and MODESEL1 can be used to select the active mode
 */
#define PCA9420_MODECFG_0_X_EN_MODE_SEL_BY_PIN 0x40U

/*
 * @brief Mode configuration upon falling edge applied to ON pin. If set,
 * the device will switch to mode 0 when a valid falling edge is applied.
 * to the ON pin
 */
/** @brief Mode output voltage mask */
#define PCA9420_MODECFG_0_SW1_OUT_MASK       0x3FU
#define PCA9420_MODECFG_0_SW1_OUT_POS        0U
/** @brief SW2_OUT offset and voltage level mask */
#define PCA9420_MODECFG_1_SW2_OUT_MASK       0x3FU
#define PCA9420_MODECFG_1_SW2_OUT_POS        0U
/** @brief LDO1_OUT voltage level mask */
#define PCA9420_MODECFG_2_LDO1_OUT_MASK      0xF0U
#define PCA9420_MODECFG_2_LDO1_OUT_POS       4U
/** @brief SW1 Enable */
#define PCA9420_MODECFG_2_SW1_EN_MASK	     0x08U
#define PCA9420_MODECFG_2_SW1_EN_VAL	     0x08U
/** @brief SW2 Enable */
#define PCA9420_MODECFG_2_SW2_EN_MASK	     0x04U
#define PCA9420_MODECFG_2_SW2_EN_VAL	     0x04U
/** @brief LDO1 Enable */
#define PCA9420_MODECFG_2_LDO1_EN_MASK	     0x02U
#define PCA9420_MODECFG_2_LDO1_EN_VAL	     0x02U
/** @brief LDO2 Enable */
#define PCA9420_MODECFG_2_LDO2_EN_MASK	     0x01U
#define PCA9420_MODECFG_2_LDO2_EN_VAL	     0x01U
/** @brief LDO2_OUT offset and voltage level mask */
#define PCA9420_MODECFG_3_LDO2_OUT_MASK      0x3FU
#define PCA9420_MODECFG_3_LDO2_OUT_POS       0U

/** VIN ILIM resolution, uA/LSB */
#define PCA9420_VIN_ILIM_UA_LSB 170000
/** VIN ILIM minimum value, uA */
#define PCA9420_VIN_ILIM_MIN_UA 85000

/** Number of modes */
#define PCA9420_NUM_MODES 4U
/** Offset applied to MODECFG* registers for a given mode */
#define PCA9420_MODECFG_OFFSET(mode) ((mode) * 4U)

struct regulator_pca9420_desc {
	uint8_t enable_reg;
	uint8_t enable_mask;
	uint8_t enable_val;
	uint8_t vsel_reg;
	uint8_t vsel_mask;
	uint8_t vsel_pos;
	int32_t max_ua;
	uint8_t num_ranges;
	const struct linear_range *ranges;
};

struct regulator_pca9420_common_config {
	struct i2c_dt_spec i2c;
	int32_t vin_ilim_ua;
	bool enable_modesel_pins;
};

struct regulator_pca9420_common_data {
	regulator_dvs_state_t dvs_state;
};

struct regulator_pca9420_config {
	struct regulator_common_config common;
	bool enable_inverted;
	int32_t modes_uv[4];
	const struct regulator_pca9420_desc *desc;
	const struct device *parent;
};

struct regulator_pca9420_data {
	struct regulator_common_data data;
};

static const struct linear_range buck1_ranges[] = {
	LINEAR_RANGE_INIT(500000, 25000U, 0x0U, 0x28U),
	LINEAR_RANGE_INIT(1500000, 0U, 0x29U, 0x3E),
	LINEAR_RANGE_INIT(1800000, 0U, 0x3FU, 0x3FU),
};

static const struct linear_range buck2_ranges[] = {
	LINEAR_RANGE_INIT(1500000, 25000U, 0x0U, 0x18U),
	LINEAR_RANGE_INIT(2100000, 0U, 0x19U, 0x1F),
	LINEAR_RANGE_INIT(2700000, 25000U, 0x20U, 0x38U),
	LINEAR_RANGE_INIT(3300000, 0U, 0x39U, 0x3F),
};

static const struct linear_range ldo1_ranges[] = {
	LINEAR_RANGE_INIT(1700000, 25000U, 0x0U, 0x9U),
	LINEAR_RANGE_INIT(1900000, 0U, 0x9U, 0xFU),
};

static const struct linear_range ldo2_ranges[] = {
	LINEAR_RANGE_INIT(1500000, 25000U, 0x0U, 0x18U),
	LINEAR_RANGE_INIT(2100000, 0U, 0x19U, 0x1FU),
	LINEAR_RANGE_INIT(2700000, 25000U, 0x20U, 0x38U),
	LINEAR_RANGE_INIT(3300000, 0U, 0x39U, 0x3FU),
};

static const struct regulator_pca9420_desc buck1_desc = {
	.enable_reg = PCA9420_MODECFG_0_2,
	.enable_mask = PCA9420_MODECFG_2_SW1_EN_MASK,
	.enable_val = PCA9420_MODECFG_2_SW1_EN_VAL,
	.vsel_mask = PCA9420_MODECFG_0_SW1_OUT_MASK,
	.vsel_pos = PCA9420_MODECFG_0_SW1_OUT_POS,
	.vsel_reg = PCA9420_MODECFG_0_0,
	.max_ua = 250000,
	.ranges = buck1_ranges,
	.num_ranges = ARRAY_SIZE(buck1_ranges),
};

static const struct regulator_pca9420_desc buck2_desc = {
	.enable_reg = PCA9420_MODECFG_0_2,
	.enable_mask = PCA9420_MODECFG_2_SW2_EN_MASK,
	.enable_val = PCA9420_MODECFG_2_SW2_EN_VAL,
	.vsel_mask = PCA9420_MODECFG_1_SW2_OUT_MASK,
	.vsel_pos = PCA9420_MODECFG_1_SW2_OUT_POS,
	.vsel_reg = PCA9420_MODECFG_0_1,
	.max_ua = 500000,
	.ranges = buck2_ranges,
	.num_ranges = ARRAY_SIZE(buck2_ranges),
};

static const struct regulator_pca9420_desc ldo1_desc = {
	.enable_reg = PCA9420_MODECFG_0_2,
	.enable_mask = PCA9420_MODECFG_2_LDO1_EN_MASK,
	.enable_val = PCA9420_MODECFG_2_LDO1_EN_VAL,
	.vsel_mask = PCA9420_MODECFG_2_LDO1_OUT_MASK,
	.vsel_pos = PCA9420_MODECFG_2_LDO1_OUT_POS,
	.vsel_reg = PCA9420_MODECFG_0_2,
	.max_ua = 1000,
	.ranges = ldo1_ranges,
	.num_ranges = ARRAY_SIZE(ldo1_ranges),
};

static const struct regulator_pca9420_desc ldo2_desc = {
	.enable_reg = PCA9420_MODECFG_0_2,
	.enable_mask = PCA9420_MODECFG_2_LDO2_EN_MASK,
	.enable_val = PCA9420_MODECFG_2_LDO2_EN_VAL,
	.vsel_reg = PCA9420_MODECFG_0_3,
	.vsel_mask = PCA9420_MODECFG_3_LDO2_OUT_MASK,
	.vsel_pos = PCA9420_MODECFG_3_LDO2_OUT_POS,
	.max_ua = 250000,
	.ranges = ldo2_ranges,
	.num_ranges = ARRAY_SIZE(ldo2_ranges),
};

static unsigned int regulator_pca9420_count_voltages(const struct device *dev)
{
	const struct regulator_pca9420_config *config = dev->config;

	return linear_range_group_values_count(config->desc->ranges,
					       config->desc->num_ranges);
}

static int regulator_pca9420_list_voltage(const struct device *dev,
					  unsigned int idx, int32_t *volt_uv)
{
	const struct regulator_pca9420_config *config = dev->config;

	return linear_range_group_get_value(config->desc->ranges,
					    config->desc->num_ranges, idx,
					    volt_uv);
}

static int regulator_pca9420_set_voltage(const struct device *dev,
					 int32_t min_uv, int32_t max_uv)
{
	const struct regulator_pca9420_config *config = dev->config;
	const struct regulator_pca9420_common_config *cconfig = config->parent->config;
	struct regulator_pca9420_common_data *cdata = config->parent->data;
	uint16_t idx;
	int ret;

	ret = linear_range_group_get_win_index(config->desc->ranges,
					       config->desc->num_ranges, min_uv,
					       max_uv, &idx);
	if (ret == -EINVAL) {
		return ret;
	}

	idx <<= config->desc->vsel_pos;

	return i2c_reg_update_byte_dt(&cconfig->i2c, config->desc->vsel_reg +
				      PCA9420_MODECFG_OFFSET(cdata->dvs_state),
				      config->desc->vsel_mask, (uint8_t)idx);
}

static int regulator_pca9420_get_voltage(const struct device *dev,
					 int32_t *volt_uv)
{
	const struct regulator_pca9420_config *config = dev->config;
	const struct regulator_pca9420_common_config *cconfig = config->parent->config;
	struct regulator_pca9420_common_data *cdata = config->parent->data;
	int ret;
	uint8_t raw_reg;

	ret = i2c_reg_read_byte_dt(&cconfig->i2c, config->desc->vsel_reg +
				   PCA9420_MODECFG_OFFSET(cdata->dvs_state),
				   &raw_reg);
	if (ret < 0) {
		return ret;
	}

	raw_reg = (raw_reg & config->desc->vsel_mask) >> config->desc->vsel_pos;

	return linear_range_group_get_value(config->desc->ranges,
					    config->desc->num_ranges, raw_reg,
					    volt_uv);
}

static int regulator_pca9420_get_current_limit(const struct device *dev,
					       int32_t *curr_ua)
{
	const struct regulator_pca9420_config *config = dev->config;
	const struct regulator_pca9420_common_config *cconfig = config->parent->config;

	if (cconfig->vin_ilim_ua == 0U) {
		*curr_ua = config->desc->max_ua;
	} else {
		*curr_ua = MIN(config->desc->max_ua, cconfig->vin_ilim_ua);
	}

	return 0;
}

static int regulator_pca9420_enable(const struct device *dev)
{
	const struct regulator_pca9420_config *config = dev->config;
	const struct regulator_pca9420_common_config *cconfig = config->parent->config;
	struct regulator_pca9420_common_data *cdata = config->parent->data;
	uint8_t en_val;

	en_val = config->enable_inverted ? 0 : config->desc->enable_val;
	return i2c_reg_update_byte_dt(&cconfig->i2c, config->desc->enable_reg
				      + PCA9420_MODECFG_OFFSET(cdata->dvs_state),
				      config->desc->enable_mask, en_val);
}

static int regulator_pca9420_disable(const struct device *dev)
{
	const struct regulator_pca9420_config *config = dev->config;
	const struct regulator_pca9420_common_config *cconfig = config->parent->config;
	struct regulator_pca9420_common_data *cdata = config->parent->data;
	uint8_t dis_val;

	dis_val = config->enable_inverted ? config->desc->enable_val : 0;
	return i2c_reg_update_byte_dt(&cconfig->i2c, config->desc->enable_reg
				      + PCA9420_MODECFG_OFFSET(cdata->dvs_state),
				      config->desc->enable_mask, dis_val);
}

static const struct regulator_driver_api api = {
	.enable = regulator_pca9420_enable,
	.disable = regulator_pca9420_disable,
	.count_voltages = regulator_pca9420_count_voltages,
	.list_voltage = regulator_pca9420_list_voltage,
	.set_voltage = regulator_pca9420_set_voltage,
	.get_voltage = regulator_pca9420_get_voltage,
	.get_current_limit = regulator_pca9420_get_current_limit,
};

static int regulator_pca9420_init(const struct device *dev)
{
	const struct regulator_pca9420_config *config = dev->config;
	const struct regulator_pca9420_common_config *cconfig = config->parent->config;

	regulator_common_data_init(dev);

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

	/* configure mode voltages */
	for (uint8_t i = 0U; i < ARRAY_SIZE(config->modes_uv); i++) {
		int ret;

		if (config->modes_uv[i] == 0) {
			/* disable mode if voltage is 0 */
			ret = i2c_reg_update_byte_dt(
				&cconfig->i2c,
				config->desc->enable_reg + PCA9420_MODECFG_OFFSET(i),
				config->desc->enable_mask, 0U);
			if (ret < 0) {
				return ret;
			}
		} else if (config->modes_uv[i] > 0) {
			uint16_t idx;

			/* program mode voltage */
			ret = linear_range_group_get_win_index(
				config->desc->ranges, config->desc->num_ranges,
				config->modes_uv[i], config->modes_uv[i], &idx);
			if (ret == -EINVAL) {
				return ret;
			}

			idx <<= config->desc->vsel_pos;

			ret = i2c_reg_update_byte_dt(
				&cconfig->i2c,
				config->desc->vsel_reg + PCA9420_MODECFG_OFFSET(i),
				config->desc->vsel_mask, (uint8_t)idx);
			if (ret < 0) {
				return ret;
			}
		}
	}

	return regulator_common_init(dev, false);
}

int regulator_pca9420_dvs_state_set(const struct device *dev,
				    regulator_dvs_state_t state)
{
	const struct regulator_pca9420_common_config *config = dev->config;
	struct regulator_pca9420_common_data *data = dev->data;
	int ret;

	if (state >= PCA9420_NUM_MODES) {
		return -ENOTSUP;
	}

	if (config->enable_modesel_pins) {
		/*
		 * The user cannot set DVS state via this API,
		 * but they may want to query/set voltages for another mode.
		 * Return -EPERM to indicate change failed, but change the
		 * dvs_state variable so the user can access the alternative
		 * dvs mode settings.
		 */
		data->dvs_state = state;
		return -EPERM;
	}

	ret = i2c_reg_update_byte_dt(&config->i2c, PCA9420_TOP_CNTL3,
				      PCA9420_TOP_CNTL3_MODE_I2C_MASK,
				      state << PCA9420_TOP_CNTL3_MODE_I2C_POS);
	if (ret < 0) {
		return ret;
	}
	/* Record new DVS state */
	data->dvs_state = state;
	return 0;
}

static const struct regulator_parent_driver_api parent_api = {
	.dvs_state_set = regulator_pca9420_dvs_state_set,
};

static int regulator_pca9420_common_init(const struct device *dev)
{
	const struct regulator_pca9420_common_config *config = dev->config;
	uint8_t reg_val = PCA9420_TOP_CNTL0_VIN_ILIM_SEL_DISABLED;
	int ret;

	if (!device_is_ready(config->i2c.bus)) {
		return -ENODEV;
	}

	if (config->enable_modesel_pins) {
		/* enable MODESEL0/1 pins for each mode */
		for (uint8_t i = 0U; i < PCA9420_NUM_MODES; i++) {
			ret = i2c_reg_update_byte_dt(
				&config->i2c,
				PCA9420_MODECFG_0_0 +
				PCA9420_MODECFG_OFFSET(i),
				PCA9420_MODECFG_0_X_EN_MODE_SEL_BY_PIN,
				PCA9420_MODECFG_0_X_EN_MODE_SEL_BY_PIN);
			if (ret < 0) {
				return ret;
			}
		}
	}

	/* configure VIN current limit */
	if (config->vin_ilim_ua != 0U) {
		reg_val = (config->vin_ilim_ua - PCA9420_VIN_ILIM_MIN_UA) /
			  PCA9420_VIN_ILIM_UA_LSB;
	}

	return i2c_reg_update_byte_dt(
		&config->i2c, PCA9420_TOP_CNTL0,
		PCA9420_TOP_CNTL0_VIN_ILIM_SEL_MASK,
		reg_val << PCA9420_TOP_CNTL0_VIN_ILIM_SEL_POS);
}

#define REGULATOR_PCA9420_DEFINE(node_id, id, name, _parent)                   \
	static struct regulator_pca9420_data data_##id;                        \
                                                                               \
	static const struct regulator_pca9420_config config_##id = {           \
		.common = REGULATOR_DT_COMMON_CONFIG_INIT(node_id),            \
		.enable_inverted = DT_PROP(node_id, enable_inverted),          \
		.modes_uv = {                                                  \
			DT_PROP_OR(node_id, nxp_mode0_microvolt, -1),          \
			DT_PROP_OR(node_id, nxp_mode1_microvolt, -1),          \
			DT_PROP_OR(node_id, nxp_mode2_microvolt, -1),          \
			DT_PROP_OR(node_id, nxp_mode3_microvolt, -1),          \
		},                                                             \
		.desc = &name ## _desc,                                        \
		.parent = _parent,                                             \
	};                                                                     \
                                                                               \
	DEVICE_DT_DEFINE(node_id, regulator_pca9420_init, NULL, &data_##id,    \
			 &config_##id, POST_KERNEL,                            \
			 CONFIG_REGULATOR_PCA9420_INIT_PRIORITY, &api);

#define REGULATOR_PCA9420_DEFINE_COND(inst, child, parent)                     \
	COND_CODE_1(DT_NODE_EXISTS(DT_INST_CHILD(inst, child)),                \
		    (REGULATOR_PCA9420_DEFINE(DT_INST_CHILD(inst, child),      \
					      child ## inst, child, parent)),  \
		    ())

#define REGULATOR_PCA9420_DEFINE_ALL(inst)                                     \
	static const struct regulator_pca9420_common_config config_##inst = {  \
		.i2c = I2C_DT_SPEC_INST_GET(inst),                             \
		.vin_ilim_ua = DT_INST_PROP(inst, nxp_vin_ilim_microamp),      \
		.enable_modesel_pins =                                         \
			DT_INST_PROP(inst, nxp_enable_modesel_pins),           \
	};                                                                     \
                                                                               \
	static struct regulator_pca9420_common_data data_##inst;               \
                                                                               \
	DEVICE_DT_INST_DEFINE(inst, regulator_pca9420_common_init, NULL,       \
			      &data_##inst,                                    \
			      &config_##inst, POST_KERNEL,                     \
			      CONFIG_REGULATOR_PCA9420_COMMON_INIT_PRIORITY,   \
			      &parent_api);                                    \
                                                                               \
	REGULATOR_PCA9420_DEFINE_COND(inst, buck1, DEVICE_DT_INST_GET(inst))   \
	REGULATOR_PCA9420_DEFINE_COND(inst, buck2, DEVICE_DT_INST_GET(inst))   \
	REGULATOR_PCA9420_DEFINE_COND(inst, ldo1, DEVICE_DT_INST_GET(inst))    \
	REGULATOR_PCA9420_DEFINE_COND(inst, ldo2, DEVICE_DT_INST_GET(inst))

DT_INST_FOREACH_STATUS_OKAY(REGULATOR_PCA9420_DEFINE_ALL)
