/*
 * Copyright (c) 2024 Juliane Schulze, deveritec GmbH
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT vishay_vcnl36825t

#include "vcnl36825t.h"

#include <stdlib.h>

#include <zephyr/drivers/i2c.h>
#include <zephyr/logging/log.h>
#include <zephyr/pm/device.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>

LOG_MODULE_REGISTER(VCNL36825T, CONFIG_SENSOR_LOG_LEVEL);

static int vcnl36825t_read(const struct i2c_dt_spec *spec, uint8_t reg_addr, uint16_t *value)
{
	uint8_t rx_buf[2];
	int rc;

	rc = i2c_write_read_dt(spec, &reg_addr, sizeof(reg_addr), rx_buf, sizeof(rx_buf));
	if (rc < 0) {
		return rc;
	}

	*value = sys_get_le16(rx_buf);

	return 0;
}

static int vcnl36825t_write(const struct i2c_dt_spec *spec, uint8_t reg_addr, uint16_t value)
{
	uint8_t tx_buf[3] = {reg_addr};

	sys_put_le16(value, &tx_buf[1]);
	return i2c_write_dt(spec, tx_buf, sizeof(tx_buf));
}

static int vcnl36825t_update(const struct i2c_dt_spec *spec, uint8_t reg_addr, uint16_t mask,
			     uint16_t value)
{
	int rc;
	uint16_t old_value, new_value;

	rc = vcnl36825t_read(spec, reg_addr, &old_value);
	if (rc < 0) {
		return rc;
	}

	new_value = (old_value & ~mask) | (value & mask);

	if (new_value == old_value) {
		return 0;
	}

	return vcnl36825t_write(spec, reg_addr, new_value);
}

#if CONFIG_PM_DEVICE
static int vcnl36825t_pm_action(const struct device *dev, enum pm_device_action action)
{
	const struct vcnl36825t_config *config = dev->config;
	int rc = 0;

	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		rc = vcnl36825t_update(&config->i2c, VCNL36825T_REG_PS_CONF1, VCNL36825T_PS_ON_MSK,
				       VCNL36825T_PS_ON);
		if (rc < 0) {
			return rc;
		}

		rc = vcnl36825t_update(&config->i2c, VCNL36825T_REG_PS_CONF2, VCNL36825T_PS_ST_MSK,
				       VCNL36825T_PS_ST_START);
		if (rc < 0) {
			return rc;
		}
		break;
	case PM_DEVICE_ACTION_SUSPEND:
		rc = vcnl36825t_update(&config->i2c, VCNL36825T_REG_PS_CONF2, VCNL36825T_PS_ST_MSK,
				       VCNL36825T_PS_ST_STOP);
		if (rc < 0) {
			return rc;
		}

		rc = vcnl36825t_update(&config->i2c, VCNL36825T_REG_PS_CONF1, VCNL36825T_PS_ON_MSK,
				       VCNL36825T_PS_OFF);
		if (rc < 0) {
			return rc;
		}
		break;
	default:
		LOG_ERR("action %d not supported", (int)action);
		return -ENOTSUP;
	}

	return 0;
}

#endif

static int vcnl36825t_sample_fetch(const struct device *dev, enum sensor_channel chan)
{
	const struct vcnl36825t_config *config = dev->config;
	struct vcnl36825t_data *data = dev->data;
	int rc;

#if CONFIG_PM_DEVICE
	enum pm_device_state state;

	(void)pm_device_state_get(dev, &state);
	if (state != PM_DEVICE_STATE_ACTIVE) {
		return -EBUSY;
	}
#endif

	switch (chan) {
	case SENSOR_CHAN_ALL:
	case SENSOR_CHAN_PROX:
		if (config->operation_mode == VCNL36825T_OPERATION_MODE_FORCE) {
			rc = vcnl36825t_update(&config->i2c, VCNL36825T_REG_PS_CONF3,
					       VCNL36825T_PS_TRIG_MSK, VCNL36825T_PS_TRIG_ONCE);
			if (rc < 0) {
				LOG_ERR("could not trigger proximity measurement %d", rc);
				return rc;
			}

			k_usleep(data->meas_timeout_us);
		}

		rc = vcnl36825t_read(&config->i2c, VCNL36825T_REG_PS_DATA, &data->proximity);
		if (rc < 0) {
			LOG_ERR("could not fetch proximity measurement %d", rc);
			return rc;
		}

		break;
	default:
		LOG_ERR("invalid sensor channel");
		return -EINVAL;
	}

	return 0;
}

static int vcnl36825t_channel_get(const struct device *dev, enum sensor_channel chan,
				  struct sensor_value *val)
{
	struct vcnl36825t_data *data = dev->data;

	switch (chan) {
	case SENSOR_CHAN_ALL:
	case SENSOR_CHAN_PROX:
		val->val1 = data->proximity & VCNL36825T_OS_DATA_MSK;
		val->val2 = 0;
		break;
	default:
		return -ENOTSUP;
	}

	return 0;
}

/**
 * @brief helper function to configure the registers
 *
 * @param dev pointer to the VCNL36825T instance
 */
static int vcnl36825t_init_registers(const struct device *dev)
{
	const struct vcnl36825t_config *config = dev->config;
	struct vcnl36825t_data *data = dev->data;

	int rc;
	uint16_t reg_value;

	/* reset registers as defined by the datasheet */
	const uint16_t resetValues[][2] = {
		{VCNL36825T_REG_PS_CONF1, VCNL36825T_CONF1_DEFAULT},
		{VCNL36825T_REG_PS_CONF2, VCNL36825T_CONF2_DEFAULT},
		{VCNL36825T_REG_PS_CONF3, VCNL36825T_CONF3_DEFAULT},
		{VCNL36825T_REG_PS_THDL, VCNL36825T_THDL_DEFAULT},
		{VCNL36825T_REG_PS_THDH, VCNL36825T_THDH_DEFAULT},
		{VCNL36825T_REG_PS_CANC, VCNL36825T_CANC_DEFAULT},
		{VCNL36825T_REG_PS_CONF4, VCNL36825T_CONF4_DEFAULT},
	};

	for (size_t i = 0; i < ARRAY_SIZE(resetValues); ++i) {
		vcnl36825t_write(&config->i2c, resetValues[i][0], resetValues[i][1]);
	}

	data->meas_timeout_us = 1;

	/* PS_CONF1 */
	reg_value = 0x01; /* must be set according to datasheet */
	reg_value |= VCNL36825T_PS_ON;

	rc = vcnl36825t_write(&config->i2c, VCNL36825T_REG_PS_CONF1, reg_value);
	if (rc < 0) {
		LOG_ERR("I2C for PS_ON returned %d", rc);
		return -EIO;
	}

	reg_value |= VCNL36825T_PS_CAL;
	reg_value |= FIELD_PREP(1 << 9, 1); /* reserved, must be set by datasheet */

	rc = vcnl36825t_write(&config->i2c, VCNL36825T_REG_PS_CONF1, reg_value);
	if (rc < 0) {
		LOG_ERR("I2C for PS_CAL returned %d", rc);
	}

	/* PS_CONF2 */
	reg_value = 0;

	switch (config->period) {
	case VCNL36825T_MEAS_PERIOD_10MS:
		reg_value |= VCNL36825T_PS_PERIOD_10MS;
		break;
	case VCNL36825T_MEAS_PERIOD_20MS:
		reg_value |= VCNL36825T_PS_PERIOD_20MS;
		break;
	case VCNL36825T_MEAS_PERIOD_40MS:
		reg_value |= VCNL36825T_PS_PERIOD_40MS;
		break;
	case VCNL36825T_MEAS_PERIOD_80MS:
		__fallthrough;
	default:
		reg_value |= VCNL36825T_PS_PERIOD_80MS;
		break;
	}

	reg_value |= VCNL36825T_PS_PERS_1;
	reg_value |= VCNL36825T_PS_ST_STOP;

	switch (config->proximity_it) {
	case VCNL36825T_PROXIMITY_INTEGRATION_1T:
		reg_value |= VCNL36825T_PS_IT_1T;
		data->meas_timeout_us *= 1 * VCNL36825T_FORCED_FACTOR_SCALE;
		break;
	case VCNL36825T_PROXIMITY_INTEGRATION_1_5T:
		reg_value |= VCNL36825T_PS_IT_1_5T;
		data->meas_timeout_us *= 1.5 * VCNL36825T_FORCED_FACTOR_SCALE;
		break;
	case VCNL36825T_PROXIMITY_INTEGRATION_2T:
		reg_value |= VCNL36825T_PS_IT_2T;
		data->meas_timeout_us *= 2 * VCNL36825T_FORCED_FACTOR_SCALE;
		break;
	case VCNL36825T_PROXIMITY_INTEGRATION_2_5T:
		reg_value |= VCNL36825T_PS_IT_2_5T;
		data->meas_timeout_us *= 2.5 * VCNL36825T_FORCED_FACTOR_SCALE;
		break;
	case VCNL36825T_PROXIMITY_INTEGRATION_3T:
		reg_value |= VCNL36825T_PS_IT_3T;
		data->meas_timeout_us *= 3 * VCNL36825T_FORCED_FACTOR_SCALE;
		break;
	case VCNL36825T_PROXIMITY_INTEGRATION_3_5T:
		reg_value |= VCNL36825T_PS_IT_3_5T;
		data->meas_timeout_us *= 3.5 * VCNL36825T_FORCED_FACTOR_SCALE;
		break;
	case VCNL36825T_PROXIMITY_INTEGRATION_4T:
		reg_value |= VCNL36825T_PS_IT_4T;
		data->meas_timeout_us *= 4 * VCNL36825T_FORCED_FACTOR_SCALE;
		break;
	case VCNL36825T_PROXIMITY_INTEGRATION_8T:
		__fallthrough;
	default:
		reg_value |= VCNL36825T_PS_IT_8T;
		data->meas_timeout_us *= 8 * VCNL36825T_FORCED_FACTOR_SCALE;
		break;
	}

	switch (config->multi_pulse) {
	case VCNL38652T_MULTI_PULSE_1:
		reg_value |= VCNL36825T_MPS_PULSES_1;
		break;
	case VCNL38652T_MULTI_PULSE_2:
		reg_value |= VCNL36825T_MPS_PULSES_2;
		break;
	case VCNL38652T_MULTI_PULSE_4:
		reg_value |= VCNL36825T_MPS_PULSES_4;
		break;
	case VCNL38652T_MULTI_PULSE_8:
		__fallthrough;
	default:
		reg_value |= VCNL36825T_MPS_PULSES_8;
		break;
	}

	switch (config->proximity_itb) {
	case VCNL36825T_PROXIMITY_INTEGRATION_DURATION_25us:
		reg_value |= VCNL36825T_PS_ITB_25us;
		data->meas_timeout_us *= 25;
		break;
	case VCNL36825T_PROXIMITY_INTEGRATION_DURATION_50us:
		__fallthrough;
	default:
		reg_value |= VCNL36825T_PS_ITB_50us;
		data->meas_timeout_us *= 50;
		break;
	}

	if (config->high_gain) {
		reg_value |= VCNL36825T_PS_HG_HIGH;
	}

	rc = vcnl36825t_write(&config->i2c, VCNL36825T_REG_PS_CONF2, reg_value);
	if (rc < 0) {
		LOG_ERR("I2C for setting PS_CONF2 returned %d", rc);
		return -EIO;
	}

	/* PS_CONF3 */
	reg_value = 0;

	if (config->operation_mode == VCNL36825T_OPERATION_MODE_FORCE) {
		reg_value |= VCNL36825T_PS_AF_FORCE;
	}

	switch (config->laser_current) {
	case VCNL36825T_LASER_CURRENT_10MS:
		reg_value |= VCNL36825T_PS_I_VCSEL_10MA;
		break;
	case VCNL36825T_LASER_CURRENT_12MS:
		reg_value |= VCNL36825T_PS_I_VCSEL_12MA;
		break;
	case VCNL36825T_LASER_CURRENT_14MS:
		reg_value |= VCNL36825T_PS_I_VCSEL_14MA;
		break;
	case VCNL36825T_LASER_CURRENT_16MS:
		reg_value |= VCNL36825T_PS_I_VCSEL_16MA;
		break;
	case VCNL36825T_LASER_CURRENT_18MS:
		reg_value |= VCNL36825T_PS_I_VCSEL_18MA;
		break;
	case VCNL36825T_LASER_CURRENT_20MS:
		__fallthrough;
	default:
		reg_value |= VCNL36825T_PS_I_VCSEL_20MA;
		break;
	}

	if (config->high_dynamic_output) {
		reg_value |= VCNL36825T_PS_HD_16BIT;
	}

	if (config->sunlight_cancellation) {
		reg_value |= VCNL36825T_PS_SC_ENABLED;
	}

	rc = vcnl36825t_write(&config->i2c, VCNL36825T_REG_PS_CONF3, reg_value);
	if (rc < 0) {
		LOG_ERR("I2C for setting PS_CONF3 returned %d", rc);
		return -EIO;
	}

	/* PS_CONF4 */
	reg_value = 0;

	if (config->low_power) {
		reg_value |= VCNL36825T_PS_LPEN_ENABLED;
	}

	switch (config->period) {
	case VCNL36825T_MEAS_PERIOD_40MS:
		reg_value |= VCNL36825T_PS_LPPER_40MS;
		break;
	case VCNL36825T_MEAS_PERIOD_80MS:
		reg_value |= VCNL36825T_PS_LPPER_80MS;
		break;
	case VCNL36825T_MEAS_PERIOD_160MS:
		reg_value |= VCNL36825T_PS_LPPER_160MS;
		break;
	case VCNL36825T_MEAS_PERIOD_320MS:
		__fallthrough;
	default:
		reg_value |= VCNL36825T_PS_LPPER_320MS;
		break;
	}

	rc = vcnl36825t_write(&config->i2c, VCNL36825T_REG_PS_CONF4, reg_value);
	if (rc < 0) {
		LOG_ERR("I2C for setting PS_CONF4 returned %d", rc);
		return -EIO;
	}

	/* calculate measurement timeout
	 *  Note: always add 1 to prevent corner case losses due to precision.
	 */
	data->meas_timeout_us =
		(data->meas_timeout_us * VCNL36825T_FORCED_FACTOR_SUM) /
			(VCNL36825T_FORCED_FACTOR_SCALE * VCNL36825T_FORCED_FACTOR_SCALE) +
		1;

	return 0;
}

static int vcnl36825t_init(const struct device *dev)
{
	const struct vcnl36825t_config *config = dev->config;
	int rc;
	uint16_t reg_value;

	if (!i2c_is_ready_dt(&config->i2c)) {
		LOG_ERR("device is not ready");
		return -ENODEV;
	}

	rc = vcnl36825t_read(&config->i2c, VCNL36825T_REG_DEV_ID, &reg_value);
	if (rc < 0) {
		LOG_ERR("could not read device id");
		return rc;
	}

	if ((reg_value & VCNL36825T_ID_MSK) != VCNL36825T_DEVICE_ID) {
		LOG_ERR("incorrect device id (%d)", reg_value);
		return -EIO;
	}

	LOG_INF("version code: 0x%X",
		(unsigned int)FIELD_GET(VCNL36825T_VERSION_CODE_MSK, reg_value));

	rc = vcnl36825t_init_registers(dev);
	if (rc < 0) {
		return rc;
	}

	if (config->operation_mode == VCNL36825T_OPERATION_MODE_AUTO) {
		rc = vcnl36825t_update(&config->i2c, VCNL36825T_REG_PS_CONF2, VCNL36825T_PS_ST_MSK,
				       VCNL36825T_PS_ST_START);
		if (rc < 0) {
			LOG_ERR("error starting measurement");
			return -EIO;
		}
	}

	return 0;
}

static const struct sensor_driver_api vcnl36825t_driver_api = {
	.sample_fetch = vcnl36825t_sample_fetch,
	.channel_get = vcnl36825t_channel_get,
};

#define VCNL36825T_DEFINE(inst)                                                                    \
	BUILD_ASSERT(!DT_INST_PROP(inst, low_power) || (DT_INST_PROP(inst, measurement_period) >=  \
							VCNL36825T_PS_LPPER_VALUE_MIN_MS),         \
		     "measurement-period must be greater/equal 40 ms in low-power mode");          \
	BUILD_ASSERT(                                                                              \
		DT_INST_PROP(inst, low_power) || (DT_INST_PROP(inst, measurement_period) <=        \
						  VCNL36825T_PS_PERIOD_VALUE_MAX_MS),              \
		"measurement-period must be less/equal 80 ms with deactivated low-power mode");    \
	static struct vcnl36825t_data vcnl36825t_data_##inst;                                      \
	static const struct vcnl36825t_config vcnl36825t_config_##inst = {                         \
		.i2c = I2C_DT_SPEC_INST_GET(inst),                                                 \
		.operation_mode = DT_INST_ENUM_IDX(inst, operation_mode),                          \
		.period = DT_INST_ENUM_IDX(inst, measurement_period),                              \
		.proximity_it = DT_INST_ENUM_IDX(inst, proximity_it),                              \
		.proximity_itb = DT_INST_ENUM_IDX(inst, proximity_itb),                            \
		.multi_pulse = DT_INST_ENUM_IDX(inst, multi_pulse),                                \
		.low_power = DT_INST_PROP(inst, low_power),                                        \
		.high_gain = DT_INST_PROP(inst, high_gain),                                        \
		.laser_current = DT_INST_ENUM_IDX(inst, laser_current),                            \
		.high_dynamic_output = DT_INST_PROP(inst, high_dynamic_output),                    \
		.sunlight_cancellation = DT_INST_PROP(inst, sunlight_cancellation),                \
	};                                                                                         \
	IF_ENABLED(CONFIG_PM_DEVICE, (PM_DEVICE_DT_INST_DEFINE(inst, vcnl36825t_pm_action)));      \
	SENSOR_DEVICE_DT_INST_DEFINE(                                                              \
		inst, vcnl36825t_init,                                                             \
		COND_CODE_1(CONFIG_PM_DEVICE, (PM_DEVICE_DT_INST_GET(inst)), (NULL)),              \
		&vcnl36825t_data_##inst, &vcnl36825t_config_##inst, POST_KERNEL,                   \
		CONFIG_SENSOR_INIT_PRIORITY, &vcnl36825t_driver_api);

DT_INST_FOREACH_STATUS_OKAY(VCNL36825T_DEFINE)
