/*
 * Copyright (c) 2022 T-Mobile USA, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT ams_tsl2540

#include "tsl2540.h"

#include <stdlib.h>

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

#define TSL2540_INTEGRATION_TIME_MS	(2.81)
#define TSL2540_DEVICE_FACTOR		(53.0)

#define FIXED_ATTENUATION_TO_DBL(x)	(x * 0.00001)

LOG_MODULE_REGISTER(tsl2540, CONFIG_SENSOR_LOG_LEVEL);

static int tsl2540_sample_fetch(const struct device *dev, enum sensor_channel chan)
{
	const struct tsl2540_config *cfg = dev->config;
	struct tsl2540_data *data = dev->data;
	int ret = 0;

	__ASSERT_NO_MSG(chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_LIGHT ||
			chan == SENSOR_CHAN_IR);
	k_sem_take(&data->sem, K_FOREVER);

	if (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_LIGHT) {
		uint16_t le16_buffer;

		ret = i2c_burst_read_dt(&cfg->i2c_spec, TSL2540_REG_VIS_LOW,
					(uint8_t *)&le16_buffer, sizeof(le16_buffer));
		if (ret) {
			LOG_ERR("Could not fetch ambient light (visible)");
			k_sem_give(&data->sem);
			return -EIO;
		}

		data->count_vis = sys_le16_to_cpu(le16_buffer);
	}

	if (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_IR) {
		uint16_t le16_buffer;

		ret = i2c_burst_read_dt(&cfg->i2c_spec, TSL2540_REG_IR_LOW, (uint8_t *)&le16_buffer,
					sizeof(le16_buffer));
		if (ret) {
			LOG_ERR("Could not fetch ambient light (IR)");
			k_sem_give(&data->sem);
			return -EIO;
		}

		data->count_ir = sys_le16_to_cpu(le16_buffer);
	}

	k_sem_give(&data->sem);

	return ret;
}

static int tsl2540_channel_get(const struct device *dev, enum sensor_channel chan,
			       struct sensor_value *val)
{
	const struct tsl2540_config *cfg = dev->config;
	struct tsl2540_data *data = dev->data;
	int ret = 0;
	double cpl;
	double glass_attenuation = FIXED_ATTENUATION_TO_DBL(cfg->glass_attenuation);
	double glass_ir_attenuation = FIXED_ATTENUATION_TO_DBL(cfg->glass_ir_attenuation);

	k_sem_take(&data->sem, K_FOREVER);

	cpl = (data->integration_time + 1) * TSL2540_INTEGRATION_TIME_MS;
	cpl *= data->again;

	switch (chan) {
	case SENSOR_CHAN_LIGHT:
		sensor_value_from_double(val, data->count_vis / cpl *
					 TSL2540_DEVICE_FACTOR * glass_attenuation);
		break;
	case SENSOR_CHAN_IR:
		sensor_value_from_double(val, data->count_ir / cpl *
					 TSL2540_DEVICE_FACTOR * glass_ir_attenuation);
		break;
	default:
		ret = -ENOTSUP;
	}

	k_sem_give(&data->sem);

	return ret;
}

static int tsl2540_attr_set_gain(const struct device *dev, enum sensor_gain_tsl2540 gain)
{
	const struct tsl2540_config *cfg = dev->config;
	struct tsl2540_data *data = dev->data;
	uint8_t value = 0;
	double again = 0.0;

	switch (gain) {
	case TSL2540_SENSOR_GAIN_1_2:
		value = TSL2540_CFG1_G1_2;
		again = TSL2540_AGAIN_S1_2;
		break;
	case TSL2540_SENSOR_GAIN_1:
		value = TSL2540_CFG1_G1;
		again = TSL2540_AGAIN_S1;
		break;
	case TSL2540_SENSOR_GAIN_4:
		value = TSL2540_CFG1_G4;
		again = TSL2540_AGAIN_S4;
		break;
	case TSL2540_SENSOR_GAIN_16:
		value = TSL2540_CFG1_G16;
		again = TSL2540_AGAIN_S16;
		break;
	case TSL2540_SENSOR_GAIN_64:
		value = TSL2540_CFG1_G64;
		again = TSL2540_AGAIN_S64;
		break;
	case TSL2540_SENSOR_GAIN_128:
		value = TSL2540_CFG1_G128;
		again = TSL2540_CFG2_G128;
		break;
	}

	if (i2c_reg_write_byte_dt(&cfg->i2c_spec, TSL2540_REG_CFG_1, value) < 0) {
		return -EIO;
	}

	if (i2c_reg_write_byte_dt(&cfg->i2c_spec, TSL2540_REG_CFG_2, value) < 0) {
		return -EIO;
	}

	data->again = again;

	return 0;
}

static int tsl2540_attr_set(const struct device *dev, enum sensor_channel chan,
				enum sensor_attribute attr, const struct sensor_value *val)
{
	const struct tsl2540_config *cfg = dev->config;
	struct tsl2540_data *data = dev->data;
	int ret = 0;
	uint8_t temp;
	double it;

	if ((chan != SENSOR_CHAN_IR) & (chan != SENSOR_CHAN_LIGHT)) {
		return -ENOTSUP;
	}

	k_sem_take(&data->sem, K_FOREVER);

	ret = i2c_reg_write_byte_dt(&cfg->i2c_spec, TSL2540_ENABLE_ADDR, TSL2540_ENABLE_MASK &
				    ~TSL2540_ENABLE_CONF);
	if (ret) {
		k_sem_give(&data->sem);
		return ret;
	}

#if CONFIG_TSL2540_TRIGGER
	if (chan == SENSOR_CHAN_LIGHT) {
		if (attr == SENSOR_ATTR_UPPER_THRESH) {
			double cpl;
			uint16_t thld, le16_buffer;
			double glass_attenuation = FIXED_ATTENUATION_TO_DBL(cfg->glass_attenuation);

			cpl = ((data->integration_time + 1) * TSL2540_INTEGRATION_TIME_MS);
			cpl *= data->again;
			cpl /= (TSL2540_DEVICE_FACTOR * glass_attenuation);
			thld = sensor_value_to_double(val) * cpl;
			LOG_DBG("attr: %d, cpl: %g, thld: %x\n", attr, cpl, thld);

			le16_buffer = sys_cpu_to_le16(thld);
			ret = i2c_burst_write_dt(
				&((const struct tsl2540_config *)dev->config)->i2c_spec,
				TSL2540_REG_AIHT_LOW, (uint8_t *)&le16_buffer, sizeof(le16_buffer));

			goto exit;
		}
		if (attr == SENSOR_ATTR_LOWER_THRESH) {
			double cpl;
			uint16_t thld, le16_buffer;
			double glass_attenuation = FIXED_ATTENUATION_TO_DBL(cfg->glass_attenuation);

			cpl = ((data->integration_time + 1) * TSL2540_INTEGRATION_TIME_MS);
			cpl *= data->again;
			cpl /= (TSL2540_DEVICE_FACTOR * glass_attenuation);
			thld = sensor_value_to_double(val) * cpl;
			LOG_DBG("attr: %d, cpl: %g, thld: %x\n", attr, cpl, thld);

			le16_buffer = sys_cpu_to_le16(sys_cpu_to_le16(thld));

			ret = i2c_burst_write_dt(
				&((const struct tsl2540_config *)dev->config)->i2c_spec,
				TSL2540_REG_AILT_LOW, (uint8_t *)&le16_buffer, sizeof(le16_buffer));

			goto exit;
		}

	}
#endif /* CONFIG_TSL2540_TRIGGER */

	switch ((enum sensor_attribute_tsl2540)attr) {
	case SENSOR_ATTR_GAIN:
		tsl2540_attr_set_gain(dev, (enum sensor_gain_tsl2540)val->val1);
		break;
	case SENSOR_ATTR_INT_APERS:
		temp = (uint8_t)val->val1;

		if (temp > 15) {
			ret = -EINVAL;
			goto exit;
		}

		if (i2c_reg_write_byte_dt(&cfg->i2c_spec, TSL2540_REG_PERS, temp)) {
			ret = -EIO;
			goto exit;
		}
		break;
	case SENSOR_ATTR_INTEGRATION_TIME:
		it = sensor_value_to_double(val);
		it /= TSL2540_INTEGRATION_TIME_MS;
		if (it < 1 || it > 256) {
			ret = -EINVAL;
			goto exit;
		}
		it -= 1;
		temp = (uint8_t)it;
		if (i2c_reg_write_byte_dt(&cfg->i2c_spec, TSL2540_REG_ATIME, temp)) {
			ret = -EIO;
			goto exit;
		}

		data->integration_time = temp;
		ret = 0;
		break;
	case SENSOR_ATTR_TSL2540_SHUTDOWN_MODE:
		data->enable_mode = TSL2540_ENABLE_DISABLE;
		ret = i2c_reg_update_byte_dt(&cfg->i2c_spec, TSL2540_CFG3_ADDR, TSL2540_CFG3_MASK,
						TSL2540_CFG3_CONF);
		break;
	case SENSOR_ATTR_TSL2540_CONTINUOUS_MODE:
		data->enable_mode = TSL2540_ENABLE_CONF;
		ret = i2c_reg_update_byte_dt(&cfg->i2c_spec, TSL2540_CFG3_ADDR, TSL2540_CFG3_MASK,
						TSL2540_CFG3_CONF);
		break;
	case SENSOR_ATTR_TSL2540_CONTINUOUS_NO_WAIT_MODE:
		data->enable_mode = TSL2540_ENABLE_AEN_PON;
		ret = i2c_reg_update_byte_dt(&cfg->i2c_spec, TSL2540_CFG3_ADDR, TSL2540_CFG3_MASK,
						TSL2540_CFG3_DFLT);
		break;
	}

exit:
	i2c_reg_update_byte_dt(&cfg->i2c_spec, TSL2540_ENABLE_ADDR, TSL2540_ENABLE_MASK,
				data->enable_mode);

	k_sem_give(&data->sem);

	return ret;
}

static int tsl2540_setup(const struct device *dev)
{
	struct sensor_value integration_time;

	/* Set ALS integration time */
	tsl2540_attr_set(dev, (enum sensor_channel)SENSOR_CHAN_LIGHT,
			 (enum sensor_attribute)SENSOR_ATTR_GAIN,
			 &(struct sensor_value){.val1 = TSL2540_SENSOR_GAIN_1_2, .val2 = 0});

	sensor_value_from_double(&integration_time, 500.0);
	tsl2540_attr_set(dev, (enum sensor_channel)SENSOR_CHAN_LIGHT,
			 (enum sensor_attribute)SENSOR_ATTR_INTEGRATION_TIME, &integration_time);

	return 0;
}

static int tsl2540_init(const struct device *dev)
{
	const struct tsl2540_config *cfg = dev->config;
	struct tsl2540_data *data = dev->data;
	int ret;

	data->enable_mode = TSL2540_ENABLE_DISABLE;

	k_sem_init(&data->sem, 1, K_SEM_MAX_LIMIT);

	if (!i2c_is_ready_dt(&cfg->i2c_spec)) {
		LOG_ERR("I2C dev %s not ready", cfg->i2c_spec.bus->name);
		return -ENODEV;
	}

	ret = i2c_reg_write_byte_dt(&cfg->i2c_spec, TSL2540_REG_PERS, 1);
	if (ret) {
		LOG_ERR("Failed to setup interrupt persistence filter");
		return ret;
	}

	ret = i2c_reg_update_byte_dt(&cfg->i2c_spec, TSL2540_CFG3_ADDR, TSL2540_CFG3_MASK,
				     TSL2540_CFG3_DFLT);
	if (ret) {
		LOG_ERR("Failed to set configuration");
		return ret;
	}

	if (tsl2540_setup(dev)) {
		LOG_ERR("Failed to setup ambient light functionality");
		return -EIO;
	}

#if CONFIG_TSL2540_TRIGGER
	if (tsl2540_trigger_init(dev)) {
		LOG_ERR("Could not initialize interrupts");
		return -EIO;
	}
#endif

	LOG_DBG("Init complete");

	return 0;
}

static const struct sensor_driver_api tsl2540_driver_api = {
	.sample_fetch = tsl2540_sample_fetch,
	.channel_get = tsl2540_channel_get,
	.attr_set = tsl2540_attr_set,
#ifdef CONFIG_TSL2540_TRIGGER
	.trigger_set = tsl2540_trigger_set,
#endif
};

#ifdef CONFIG_PM_DEVICE
static int tsl2540_pm_action(const struct device *dev, enum pm_device_action action)
{

	const struct tsl2540_config *cfg = dev->config;
	struct tsl2540_data *data = dev->data;
	int ret = 0;

	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		ret = i2c_reg_update_byte_dt(&cfg->i2c_spec, TSL2540_ENABLE_ADDR,
					    TSL2540_ENABLE_MASK, data->enable_mode);
		break;
	case PM_DEVICE_ACTION_SUSPEND:
		ret = i2c_reg_update_byte_dt(&cfg->i2c_spec, TSL2540_ENABLE_ADDR,
					    TSL2540_ENABLE_MASK, TSL2540_ENABLE_DISABLE);
		break;
	default:
		return -ENOTSUP;
	}

	return ret;
}
#endif

#define TSL2540_GLASS_ATTEN(inst)						\
	.glass_attenuation = DT_INST_PROP(inst, glass_attenuation),		\
	.glass_ir_attenuation = DT_INST_PROP(inst, glass_ir_attenuation),	\

#define TSL2540_DEFINE(inst)									\
	static struct tsl2540_data tsl2540_prv_data_##inst;					\
	static const struct tsl2540_config tsl2540_config_##inst = {				\
		.i2c_spec = I2C_DT_SPEC_INST_GET(inst),						\
		IF_ENABLED(CONFIG_TSL2540_TRIGGER,						\
		(.int_gpio = GPIO_DT_SPEC_INST_GET(inst, int_gpios),))				\
		TSL2540_GLASS_ATTEN(inst)							\
	};											\
	PM_DEVICE_DT_INST_DEFINE(inst, tsl2540_pm_action);					\
	SENSOR_DEVICE_DT_INST_DEFINE(inst, &tsl2540_init, PM_DEVICE_DT_INST_GET(inst),		\
				&tsl2540_prv_data_##inst, &tsl2540_config_##inst, POST_KERNEL,	\
				CONFIG_SENSOR_INIT_PRIORITY, &tsl2540_driver_api);

DT_INST_FOREACH_STATUS_OKAY(TSL2540_DEFINE)
