/*
 * Copyright 2025 BayLibre SAS
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT x_powers_axp2101_charger

#include <errno.h>

#include <zephyr/device.h>
#include <zephyr/drivers/charger.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(charger_axp2101, CONFIG_CHARGER_LOG_LEVEL);

#define AXP2101_PMU_STATUS1			0x00
	#define VBUS_GOOD_INDICATION		BIT(5)

#define AXP2101_PMU_STATUS2			0x01
	#define CHARGING_STATUS			GENMASK(2, 0)
	#define TRICKLE_CHARGE			0x0
	#define PRE_CHARGE			0x1
	#define CONSTANT_CURRENT		0x2
	#define CONSTANT_VOLTAGE		0x3
	#define CHARGE_DONE			0x4
	#define NOT_CHARGING			0x5

#define AXP2101_CHARGER_CONTROL			0x18
	#define BUTTON_BATTERY_CHARGE_ENABLE	BIT(2)
	#define CELL_BATTERY_CHARGE_ENABLE	BIT(1)

#define AXP2101_IPRECH_CHARGER_SETTING		0x61
	#define PRE_CHARGE_CURRENT_STEP_UA	25000

#define AXP2101_ICC_CHARGER_SETTING		0x62
#define AXP2101_ITERM_CHARGER_SETTING		0x63
	#define CHARGE_TERMINATION_ENABLE	BIT(4)
	#define TERMINATION_CURRENT_LIMIT	GENMASK(3, 0)
	#define TERMINATION_CURRENT_STEP_UA	25000

#define AXP2101_CV_CHARGER_VOLTAGE		0x64

struct axp2101_config {
	struct i2c_dt_spec i2c;
	bool vbackup_enable;
};

struct axp2101_data {
	uint32_t cc_current_ua;
	uint32_t cc_voltage_uv;
	uint32_t termination_current_ua;
};

static const uint32_t constant_charge_current_lut[] = {
	[0] = 0,
	[1] = 0,
	[2] = 0,
	[3] = 0,
	[4] = 100000,
	[5] = 125000,
	[6] = 150000,
	[7] = 175000,
	[8] = 200000,
	[9] = 300000,
	[10] = 400000,
	[11] = 500000,
	[12] = 600000,
	[13] = 700000,
	[14] = 800000,
	[15] = 900000,
	[16] = 1000000,
};

static const uint32_t constant_charge_voltage_lut[] = {
	[0] = 0,
	[1] = 4000000,
	[2] = 4100000,
	[3] = 4200000,
	[4] = 4350000,
	[5] = 4400000,
};

static int get_index_in_lut(uint32_t value, const uint32_t *lut, size_t lut_len)
{
	int i;

	for (i = 0; i < (int) lut_len; i++) {
		if (value == lut[i]) {
			return i;
		}
	}

	return -1;
}

static enum charger_online is_charger_online(const struct device *dev, union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	uint8_t tmp;
	int ret;

	ret = i2c_reg_read_byte_dt(&config->i2c, AXP2101_PMU_STATUS1, &tmp);
	if (ret < 0) {
		return ret;
	}

	val->online = (tmp & VBUS_GOOD_INDICATION) ?
		      CHARGER_ONLINE_FIXED : CHARGER_ONLINE_OFFLINE;

	return 0;
}

static int get_constant_charge_current_ua(const struct device *dev, union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	uint8_t tmp;
	int ret;

	ret = i2c_reg_read_byte_dt(&config->i2c, AXP2101_ICC_CHARGER_SETTING, &tmp);
	if (ret < 0) {
		return ret;
	}

	val->const_charge_current_ua = constant_charge_current_lut[tmp];

	return 0;
}

static int set_constant_charge_current_ua(const struct device *dev,
					  const union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	struct axp2101_data *data = dev->data;
	int lut_index;

	lut_index = get_index_in_lut(val->const_charge_current_ua,
				     constant_charge_current_lut,
				     ARRAY_SIZE(constant_charge_current_lut));
	if (lut_index < 0) {
		return -EINVAL;
	}

	data->cc_current_ua = constant_charge_current_lut[lut_index];

	return i2c_reg_write_byte_dt(&config->i2c, AXP2101_ICC_CHARGER_SETTING,
				     (uint8_t) lut_index);
}

static int get_pre_charge_current_ua(const struct device *dev, union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	uint8_t tmp;
	int ret;

	ret = i2c_reg_read_byte_dt(&config->i2c, AXP2101_IPRECH_CHARGER_SETTING, &tmp);
	if (ret < 0) {
		return ret;
	}
	val->precharge_current_ua = PRE_CHARGE_CURRENT_STEP_UA * ((uint32_t) tmp);

	return 0;
}

static int get_termination_current_ua(const struct device *dev,
				      union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	uint8_t tmp;
	int ret;

	ret = i2c_reg_read_byte_dt(&config->i2c, AXP2101_ITERM_CHARGER_SETTING, &tmp);
	if (ret < 0) {
		return ret;
	}

	if ((tmp & CHARGE_TERMINATION_ENABLE) == 0) {
		val->charge_term_current_ua = 0;
	} else {
		tmp = tmp & TERMINATION_CURRENT_LIMIT;
		val->charge_term_current_ua = TERMINATION_CURRENT_STEP_UA * ((uint32_t) tmp);
	}

	return 0;
}

static int set_termination_current_ua(const struct device *dev,
				      const union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	uint32_t mask, tmp = 0;

	tmp = val->charge_term_current_ua;
	if ((tmp > 200000) || ((tmp % TERMINATION_CURRENT_STEP_UA) != 0)) {
		return -EINVAL;
	}

	if (tmp != 0) {
		tmp = (uint8_t) (tmp / TERMINATION_CURRENT_STEP_UA);
		tmp |= CHARGE_TERMINATION_ENABLE;
	}

	mask = TERMINATION_CURRENT_LIMIT | CHARGE_TERMINATION_ENABLE;

	return i2c_reg_update_byte_dt(&config->i2c, AXP2101_ITERM_CHARGER_SETTING,
				      mask, tmp);
}

static int get_constant_charge_voltage_uv(const struct device *dev, union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	uint8_t tmp;
	int ret;

	ret = i2c_reg_read_byte_dt(&config->i2c, AXP2101_CV_CHARGER_VOLTAGE, &tmp);
	if (ret < 0) {
		return ret;
	}

	if (tmp >= ARRAY_SIZE(constant_charge_voltage_lut)) {
		return -EINVAL;
	}

	val->const_charge_voltage_uv = constant_charge_voltage_lut[tmp];

	return 0;
}

static int set_constant_charge_voltage_uv(const struct device *dev,
					  const union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	int lut_index;

	lut_index = get_index_in_lut(val->const_charge_voltage_uv,
				     constant_charge_voltage_lut,
				     ARRAY_SIZE(constant_charge_voltage_lut));
	if (lut_index < 0) {
		return -EINVAL;
	}

	return i2c_reg_write_byte_dt(&config->i2c, AXP2101_CV_CHARGER_VOLTAGE, (uint8_t) lut_index);
}

static int get_status(const struct device *dev, union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	uint8_t tmp;
	int ret;

	ret = i2c_reg_read_byte_dt(&config->i2c, AXP2101_PMU_STATUS2, &tmp);
	if (ret < 0) {
		return ret;
	}

	tmp = tmp & CHARGING_STATUS;
	if (tmp <= CONSTANT_VOLTAGE) {
		val->status = CHARGER_STATUS_CHARGING;
	} else if (tmp == CHARGE_DONE) {
		val->status = CHARGER_STATUS_FULL;
	} else if (tmp == NOT_CHARGING) {
		val->status = CHARGER_STATUS_NOT_CHARGING;
	} else {
		return -EINVAL;
	}

	return 0;
}

static int get_charge_type(const struct device *dev, union charger_propval *val)
{
	const struct axp2101_config *const config = dev->config;
	uint8_t tmp;
	int ret;

	ret = i2c_reg_read_byte_dt(&config->i2c, AXP2101_PMU_STATUS2, &tmp);
	if (ret < 0) {
		return ret;
	}

	tmp = tmp & CHARGING_STATUS;
	if ((tmp == TRICKLE_CHARGE) || (tmp == PRE_CHARGE)) {
		val->charge_type = CHARGER_CHARGE_TYPE_TRICKLE;
	} else if ((tmp >= CONSTANT_CURRENT) && (tmp <= CHARGE_DONE)) {
		val->charge_type = CHARGER_CHARGE_TYPE_STANDARD;
	} else if (tmp == NOT_CHARGING) {
		val->charge_type = CHARGER_CHARGE_TYPE_UNKNOWN;
	} else {
		return -EINVAL;
	}

	return 0;
}

static int axp2101_get_prop(const struct device *dev, charger_prop_t prop,
			    union charger_propval *val)
{
	switch (prop) {
	case CHARGER_PROP_ONLINE:
		return is_charger_online(dev, val);
	case CHARGER_PROP_CONSTANT_CHARGE_CURRENT_UA:
		return get_constant_charge_current_ua(dev, val);
	case CHARGER_PROP_CONSTANT_CHARGE_VOLTAGE_UV:
		return get_constant_charge_voltage_uv(dev, val);
	case CHARGER_PROP_PRECHARGE_CURRENT_UA:
		return get_pre_charge_current_ua(dev, val);
	case CHARGER_PROP_CHARGE_TERM_CURRENT_UA:
		return get_termination_current_ua(dev, val);
	case CHARGER_PROP_CHARGE_TYPE:
		return get_charge_type(dev, val);
	case CHARGER_PROP_STATUS:
		return get_status(dev, val);
	default:
		return -ENOTSUP;
	}
}

static int axp2101_set_prop(const struct device *dev, charger_prop_t prop,
			    const union charger_propval *val)
{
	switch (prop) {
	case CHARGER_PROP_CONSTANT_CHARGE_CURRENT_UA:
		return set_constant_charge_current_ua(dev, val);
	case CHARGER_PROP_CONSTANT_CHARGE_VOLTAGE_UV:
		return set_constant_charge_voltage_uv(dev, val);
	case CHARGER_PROP_CHARGE_TERM_CURRENT_UA:
		return set_termination_current_ua(dev, val);
	default:
		return -ENOTSUP;
	}
}

static int axp2101_charge_enable(const struct device *dev, const bool enable)
{
	const struct axp2101_config *const config = dev->config;

	return i2c_reg_update_byte_dt(&config->i2c, AXP2101_CHARGER_CONTROL,
				      CELL_BATTERY_CHARGE_ENABLE,
				      (enable) ? CELL_BATTERY_CHARGE_ENABLE : 0);
}

static int axp2101_init(const struct device *dev)
{
	const struct axp2101_config *const config = dev->config;
	struct axp2101_data *data = dev->data;
	union charger_propval val;
	int ret;

	if (config->vbackup_enable) {
		ret = i2c_reg_update_byte_dt(&config->i2c, AXP2101_CHARGER_CONTROL,
					     BUTTON_BATTERY_CHARGE_ENABLE,
					     BUTTON_BATTERY_CHARGE_ENABLE);
		if (ret < 0) {
			return ret;
		}
	}

	val.const_charge_current_ua = data->cc_current_ua;
	ret = set_constant_charge_current_ua(dev, &val);
	if (ret < 0) {
		return ret;
	}

	val.const_charge_voltage_uv = data->cc_voltage_uv;
	ret = set_constant_charge_voltage_uv(dev, &val);
	if (ret < 0) {
		return ret;
	}

	val.charge_term_current_ua = data->termination_current_ua;
	ret = set_termination_current_ua(dev, &val);
	if (ret < 0) {
		return ret;
	}

	return 0;
}

static DEVICE_API(charger, axp2101_driver_api) = {
	.get_property = axp2101_get_prop,
	.set_property = axp2101_set_prop,
	.charge_enable = axp2101_charge_enable,
};

#define AXP2101_INIT(inst)                                                                         \
	static const struct axp2101_config axp2101_config_##inst = {                               \
		.i2c = I2C_DT_SPEC_GET(DT_PARENT(DT_INST(inst, DT_DRV_COMPAT))),                   \
		.vbackup_enable = DT_INST_PROP(inst, vbackup_enable),                              \
	};                                                                                         \
	static struct axp2101_data axp2101_data_##inst = {                                         \
		.cc_current_ua = DT_INST_PROP(inst, constant_charge_current_max_microamp),         \
		.cc_voltage_uv = DT_INST_PROP(inst, constant_charge_voltage_max_microvolt),        \
		.termination_current_ua = DT_INST_PROP_OR(inst, charge-term-current-microamp,      \
							  125000),                                 \
	};                                                                                         \
	DEVICE_DT_INST_DEFINE(inst, axp2101_init, NULL, &axp2101_data_##inst,                      \
			      &axp2101_config_##inst, POST_KERNEL,                                 \
			      CONFIG_CHARGER_INIT_PRIORITY,                                        \
			      &axp2101_driver_api);

DT_INST_FOREACH_STATUS_OKAY(AXP2101_INIT)
