/*
 * Copyright (c) 2022 ITE Technology Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT ite_it8xxx2_vcmp

#include <zephyr/device.h>
#include <zephyr/devicetree/io-channels.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/drivers/sensor/it8xxx2_vcmp.h>
#include <zephyr/dt-bindings/dt-util.h>
#include <zephyr/dt-bindings/sensor/it8xxx2_vcmp.h>
#include <errno.h>
#include <soc.h>
#include <soc_dt.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(vcmp_ite_it8xxx2, CONFIG_SENSOR_LOG_LEVEL);

#define VCMP_REG_MASK		0x7
#define VCMP_RESOLUTION		BIT(10)
#ifdef CONFIG_ADC_IT8XXX2_VOL_FULL_SCALE
#define VCMP_MAX_MVOLT		3300
#else
#define VCMP_MAX_MVOLT		3000
#endif

/* Device config */
struct vcmp_it8xxx2_config {
	/* Voltage comparator x control register */
	volatile uint8_t *reg_vcmpxctl;
	/* Voltage comparator x channel select MSB register */
	volatile uint8_t *reg_vcmpxcselm;
	/* Voltage comparator scan period register */
	volatile uint8_t *reg_vcmpscp;
	/* Voltage comparator x threshold data buffer MSB register */
	volatile uint8_t *reg_vcmpxthrdatm;
	/* Voltage comparator x threshold data buffer LSB register */
	volatile uint8_t *reg_vcmpxthrdatl;
	/* Voltage comparator status register */
	volatile uint8_t *reg_vcmpsts;
	/* Voltage comparator status 2 register */
	volatile uint8_t *reg_vcmpsts2;
	/* Voltage comparator module irq */
	int irq;
	/* Voltage comparator channel */
	int vcmp_ch;
	/* Scan period for "all voltage comparator channel" */
	int scan_period;
	/*
	 * Determines the condition between ADC data and threshold_mv
	 * that will trigger voltage comparator interrupt.
	 */
	int comparison;
	/* Threshold assert value in mv */
	int threshold_mv;
	/* Pointer of ADC device that will be performing measurement */
	const struct device *adc;
};

/* Driver data */
struct vcmp_it8xxx2_data {
	/* ADC channel config */
	struct adc_channel_cfg adc_ch_cfg;
	/* Work queue to be notified when threshold assertion happens */
	struct k_work work;
	/* Sensor trigger hanlder to notify user of assetion */
	sensor_trigger_handler_t handler;
	const struct sensor_trigger *trig;
	/* Pointer of voltage comparator device */
	const struct device *vcmp;
};

/* Voltage comparator work queue address */
static uint32_t vcmp_work_addr[VCMP_CHANNEL_CNT];
#ifdef CONFIG_VCMP_IT8XXX2_WORKQUEUE
/*
 * Pointer of work queue thread to be notified when threshold assertion
 * occurs.
 */
struct k_work_q *work_q;
#endif

static void clear_vcmp_status(const struct device *dev, int vcmp_ch)
{
	const struct vcmp_it8xxx2_config *const config = dev->config;
	volatile uint8_t *reg_vcmpsts = config->reg_vcmpsts;
	volatile uint8_t *reg_vcmpsts2 = config->reg_vcmpsts2;

	/* W/C voltage comparator specific channel interrupt status */
	if (vcmp_ch <= VCMP_CHANNEL_2) {
		*reg_vcmpsts = BIT(vcmp_ch);
	} else {
		*reg_vcmpsts2 = BIT(vcmp_ch - VCMP_CHANNEL_3);
	}
}

static void vcmp_enable(const struct device *dev, int enable)
{
	const struct vcmp_it8xxx2_config *const config = dev->config;
	volatile uint8_t *reg_vcmpxctl = config->reg_vcmpxctl;

	if (enable) {
		/* Enable voltage comparator specific channel interrupt */
		*reg_vcmpxctl |= IT8XXX2_VCMP_CMPINTEN;
		/* Start voltage comparator specific channel */
		*reg_vcmpxctl |= IT8XXX2_VCMP_CMPEN;
	} else {
		/* Stop voltage comparator specific channel */
		*reg_vcmpxctl &= ~IT8XXX2_VCMP_CMPEN;
		/* Disable voltage comparator specific channel interrupt */
		*reg_vcmpxctl &= ~IT8XXX2_VCMP_CMPINTEN;
	}
}

static int vcmp_set_threshold(const struct device *dev,
			      enum sensor_attribute attr,
			      int32_t reg_val)
{
	const struct vcmp_it8xxx2_config *const config = dev->config;
	volatile uint8_t *reg_vcmpxthrdatm = config->reg_vcmpxthrdatm;
	volatile uint8_t *reg_vcmpxthrdatl = config->reg_vcmpxthrdatl;
	volatile uint8_t *reg_vcmpxctl = config->reg_vcmpxctl;

	if (reg_val >= VCMP_RESOLUTION) {
		LOG_ERR("Vcmp%d threshold only support 10-bits", config->vcmp_ch);
		return -ENOTSUP;
	}

	/* Set threshold raw value */
	*reg_vcmpxthrdatl = (uint8_t)(reg_val & 0xff);
	*reg_vcmpxthrdatm = (uint8_t)((reg_val >> 8) & 0xff);

	/* Set lower or higher threshold */
	if ((attr == SENSOR_ATTR_UPPER_THRESH) ||
	    (attr == (uint16_t) SENSOR_ATTR_UPPER_VOLTAGE_THRESH)) {
		*reg_vcmpxctl |= IT8XXX2_VCMP_GREATER_THRESHOLD;
	} else {
		*reg_vcmpxctl &= ~IT8XXX2_VCMP_GREATER_THRESHOLD;
	}

	return 0;
}

static void it8xxx2_vcmp_trigger_work_handler(struct k_work *item)
{
	struct vcmp_it8xxx2_data *data =
			CONTAINER_OF(item, struct vcmp_it8xxx2_data, work);

	if (data->handler) {
		data->handler(data->vcmp, data->trig);
	}
}

static int vcmp_ite_it8xxx2_attr_set(const struct device *dev,
				     enum sensor_channel chan,
				     enum sensor_attribute attr,
				     const struct sensor_value *val)
{
	const struct vcmp_it8xxx2_config *const config = dev->config;
	int32_t reg_val, ret = 0;

	if (chan != SENSOR_CHAN_VOLTAGE) {
		return -ENOTSUP;
	}

	switch ((uint16_t)attr) {
	case SENSOR_ATTR_LOWER_THRESH:
	case SENSOR_ATTR_UPPER_THRESH:
		ret = vcmp_set_threshold(dev, attr, val->val1);
		break;
	case SENSOR_ATTR_LOWER_VOLTAGE_THRESH:
	case SENSOR_ATTR_UPPER_VOLTAGE_THRESH:
		/*
		 * Tranfrom threshold from mv to raw
		 * NOTE: CMPXTHRDAT[9:0] = threshold(mv) * 1024 / 3000(mv)
		 */
		reg_val = (val->val1 * VCMP_RESOLUTION / VCMP_MAX_MVOLT);
		ret = vcmp_set_threshold(dev, attr, reg_val);
		break;
	case SENSOR_ATTR_ALERT:
		if (!!val->val1) {
			clear_vcmp_status(dev, config->vcmp_ch);
			vcmp_enable(dev, 1);
		} else {
			vcmp_enable(dev, 0);
			clear_vcmp_status(dev, config->vcmp_ch);
		}

		break;
	default:
		ret = -ENOTSUP;
	}

	return ret;
}

static int vcmp_ite_it8xxx2_trigger_set(const struct device *dev,
					const struct sensor_trigger *trig,
					sensor_trigger_handler_t handler)
{
	const struct vcmp_it8xxx2_config *const config = dev->config;
	struct vcmp_it8xxx2_data *const data = dev->data;

	if (trig->type != SENSOR_TRIG_THRESHOLD ||
	    trig->chan != SENSOR_CHAN_VOLTAGE) {
		return -ENOTSUP;
	}

	data->handler = handler;
	data->trig = trig;

	vcmp_work_addr[config->vcmp_ch] = (uint32_t) &data->work;

	return 0;
}

static int vcmp_it8xxx2_channel_get(const struct device *dev,
				    enum sensor_channel chan,
				    struct sensor_value *val)
{
	const struct vcmp_it8xxx2_config *const config = dev->config;

	if (chan != SENSOR_CHAN_VOLTAGE) {
		return -ENOTSUP;
	}

	/*
	 * It8xxx2 adc and comparator module read automatically, according to
	 * {ADCCTS1, ADCCTS2} and VCMPSCP register setting.
	 */
	val->val1 = config->vcmp_ch;

	return 0;
}

/*
 * All voltage comparator channels share one irq interrupt, so we
 * need to handle all channels, when the interrupt fired.
 */
static void vcmp_it8xxx2_isr(const struct device *dev)
{
	const struct vcmp_it8xxx2_config *const config = dev->config;
	volatile uint8_t *reg_vcmpsts = config->reg_vcmpsts;
	volatile uint8_t *reg_vcmpsts2 = config->reg_vcmpsts2;
	int idx, status;

	/* Find out which voltage comparator triggered */
	status = *reg_vcmpsts & VCMP_REG_MASK;
	status |= (*reg_vcmpsts2 & VCMP_REG_MASK) << 3;

	for (idx = VCMP_CHANNEL_0; idx < VCMP_CHANNEL_CNT; idx++) {
		if (status & BIT(idx)) {
			/* Call triggered channel callback function in work queue */
			if (vcmp_work_addr[idx]) {
#ifdef CONFIG_VCMP_IT8XXX2_WORKQUEUE
				k_work_submit_to_queue(work_q,
						       (struct k_work *) vcmp_work_addr[idx]);
#else
				k_work_submit((struct k_work *) vcmp_work_addr[idx]);
#endif
			}
			/* W/C voltage comparator specific channel interrupt status */
			clear_vcmp_status(dev, idx);
		}
	}

	/* W/C voltage comparator irq interrupt status */
	ite_intc_isr_clear(config->irq);
}

static int vcmp_it8xxx2_init(const struct device *dev)
{
	const struct vcmp_it8xxx2_config *const config = dev->config;
	struct vcmp_it8xxx2_data *const data = dev->data;
	volatile uint8_t *reg_vcmpxctl = config->reg_vcmpxctl;
	volatile uint8_t *reg_vcmpxcselm = config->reg_vcmpxcselm;
	volatile uint8_t *reg_vcmpscp = config->reg_vcmpscp;

	/* Disable voltage comparator specific channel before init */
	vcmp_enable(dev, 0);

	/*
	 * ADC channel signal output to voltage comparator,
	 * so we need to set ADC channel to alternate mode first.
	 */
	if (!device_is_ready(config->adc)) {
		LOG_ERR("ADC device not ready");
		return -ENODEV;
	}
	adc_channel_setup(config->adc, &data->adc_ch_cfg);

	/* Select which ADC channel output voltage into comparator */
	if (data->adc_ch_cfg.channel_id <= 7) {
		/* ADC channel 0~7 map to value 0x0~0x7 */
		*reg_vcmpxctl |= data->adc_ch_cfg.channel_id & VCMP_REG_MASK;
		*reg_vcmpxcselm &= ~IT8XXX2_VCMP_VCMPXCSELM;
	} else {
		/* ADC channel 13~16 map to value 0x8~0xb */
		*reg_vcmpxctl |= (data->adc_ch_cfg.channel_id - 5) & VCMP_REG_MASK;
		*reg_vcmpxcselm |= IT8XXX2_VCMP_VCMPXCSELM;
	}

	/* Set minimum scan period for "all voltage comparator channel" */
	if (*reg_vcmpscp > config->scan_period) {
		*reg_vcmpscp = config->scan_period;
	}

	/* Data must keep device reference for worker handler */
	data->vcmp = dev;

	/* Init and set work item to enable notifications */
	k_work_init(&data->work, it8xxx2_vcmp_trigger_work_handler);
	vcmp_work_addr[config->vcmp_ch] = (uint32_t) &data->work;

	/* Set threshold and comparison if set on device tree */
	if ((config->threshold_mv != IT8XXX2_VCMP_UNDEFINED) &&
	     (config->comparison != IT8XXX2_VCMP_UNDEFINED)) {
		enum sensor_attribute attr;
		struct sensor_value val;

		if (config->comparison == IT8XXX2_VCMP_LESS_OR_EQUAL) {
			attr = SENSOR_ATTR_LOWER_VOLTAGE_THRESH;
		} else {
			attr = SENSOR_ATTR_UPPER_VOLTAGE_THRESH;
		}

		val.val1 = config->threshold_mv;
		val.val2 = 0;

		vcmp_ite_it8xxx2_attr_set(dev, SENSOR_CHAN_VOLTAGE, attr, &val);
	}

	/*
	 * All voltage comparator channels share one irq interrupt,
	 * so if the irq is enabled before, we needn't to enable again.
	 * And we will figure out the triggered channel in vcmp_it8xxx2_isr().
	 */
	if (!irq_is_enabled(config->irq)) {
		ite_intc_isr_clear(config->irq);

		irq_connect_dynamic(config->irq, 0,
				    (void (*)(const void *))vcmp_it8xxx2_isr,
				    (const void *)dev, 0);

		irq_enable(config->irq);
	}

	return 0;
}

static const struct sensor_driver_api vcmp_ite_it8xxx2_api = {
	.attr_set = vcmp_ite_it8xxx2_attr_set,
	.trigger_set = vcmp_ite_it8xxx2_trigger_set,
	.channel_get = vcmp_it8xxx2_channel_get,
};

#ifdef CONFIG_VCMP_IT8XXX2_WORKQUEUE
struct k_work_q vcmp_it8xxx2_work_q;

static K_KERNEL_STACK_DEFINE(vcmp_it8xxx2_work_q_stack,
			     CONFIG_VCMP_IT8XXX2_WORKQUEUE_STACK_SIZE);

static int vcmp_it8xxx2_init_work_q(void)
{
	struct k_work_queue_config cfg = {
		.name = "vcmp_work",
		.no_yield = false,
	};

	k_work_queue_start(&vcmp_it8xxx2_work_q,
			   vcmp_it8xxx2_work_q_stack,
			   K_KERNEL_STACK_SIZEOF(vcmp_it8xxx2_work_q_stack),
			   CONFIG_VCMP_IT8XXX2_WORKQUEUE_PRIORITY, &cfg);

	work_q = &vcmp_it8xxx2_work_q;
	return 0;
}

SYS_INIT(vcmp_it8xxx2_init_work_q, POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY);
#endif

#define VCMP_IT8XXX2_INIT(inst)								\
	static const struct vcmp_it8xxx2_config vcmp_it8xxx2_cfg_##inst = {		\
		.reg_vcmpxctl = (uint8_t *)DT_INST_REG_ADDR_BY_IDX(inst, 0),		\
		.reg_vcmpxcselm = (uint8_t *)DT_INST_REG_ADDR_BY_IDX(inst, 1),		\
		.reg_vcmpscp = (uint8_t *)DT_INST_REG_ADDR_BY_IDX(inst, 2),		\
		.reg_vcmpxthrdatm = (uint8_t *)DT_INST_REG_ADDR_BY_IDX(inst, 3),	\
		.reg_vcmpxthrdatl = (uint8_t *)DT_INST_REG_ADDR_BY_IDX(inst, 4),	\
		.reg_vcmpsts = (uint8_t *)DT_INST_REG_ADDR_BY_IDX(inst, 5),		\
		.reg_vcmpsts2 = (uint8_t *)DT_INST_REG_ADDR_BY_IDX(inst, 6),		\
		.irq = DT_INST_IRQN(inst),						\
		.vcmp_ch = DT_INST_PROP(inst, vcmp_ch),					\
		.scan_period = DT_INST_PROP(inst, scan_period),				\
		.comparison = DT_INST_PROP(inst, comparison),				\
		.threshold_mv = DT_INST_PROP(inst, threshold_mv),			\
		.adc = DEVICE_DT_GET(DT_INST_IO_CHANNELS_CTLR(inst)),			\
	};										\
											\
	static struct vcmp_it8xxx2_data vcmp_it8xxx2_data_##inst = {			\
		.adc_ch_cfg.gain = ADC_GAIN_1,						\
		.adc_ch_cfg.reference = ADC_REF_INTERNAL,				\
		.adc_ch_cfg.acquisition_time = ADC_ACQ_TIME_DEFAULT,			\
		.adc_ch_cfg.channel_id = (uint8_t)DT_INST_IO_CHANNELS_INPUT(inst),	\
	};										\
											\
	SENSOR_DEVICE_DT_INST_DEFINE(inst,						\
			      vcmp_it8xxx2_init,					\
			      NULL,							\
			      &vcmp_it8xxx2_data_##inst,				\
			      &vcmp_it8xxx2_cfg_##inst,					\
			      POST_KERNEL,						\
			      CONFIG_VCMP_IT8XXX2_INIT_PRIORITY,			\
			      &vcmp_ite_it8xxx2_api);

DT_INST_FOREACH_STATUS_OKAY(VCMP_IT8XXX2_INIT)
#ifdef CONFIG_VCMP_IT8XXX2_WORKQUEUE
BUILD_ASSERT(CONFIG_SENSOR_INIT_PRIORITY < CONFIG_VCMP_IT8XXX2_INIT_PRIORITY,
	"CONFIG_SENSOR_INIT_PRIORITY must be less than CONFIG_VCMP_IT8XXX2_INIT_PRIORITY");
#endif
