/*
 * Copyright (c) 2023 Kurtis Dinelle
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT ams_tsl2591

#include <zephyr/device.h>
#include <zephyr/pm/device.h>
#include <zephyr/sys/byteorder.h>
#include "tsl2591.h"

LOG_MODULE_REGISTER(TSL2591, CONFIG_SENSOR_LOG_LEVEL);

static int tsl2591_reg_read(const struct device *dev, uint8_t reg, uint8_t *buf, uint8_t size)
{
	const struct tsl2591_config *config = dev->config;
	uint8_t cmd = TSL2591_NORMAL_CMD | reg;

	return i2c_write_read_dt(&config->i2c, &cmd, 1U, buf, size);
}

static int tsl2591_reg_write(const struct device *dev, uint8_t reg, uint8_t val)
{
	const struct tsl2591_config *config = dev->config;
	uint8_t cmd[2] = {TSL2591_NORMAL_CMD | reg, val};

	return i2c_write_dt(&config->i2c, cmd, 2U);
}

int tsl2591_reg_update(const struct device *dev, uint8_t reg, uint8_t mask, uint8_t val)
{
	uint8_t old_value, new_value;
	int ret;

	ret = tsl2591_reg_read(dev, reg, &old_value, 1U);
	if (ret < 0) {
		return ret;
	}

	new_value = (old_value & ~mask) | (val & mask);
	if (new_value == old_value) {
		return 0;
	}

	return tsl2591_reg_write(dev, reg, new_value);
}

static int tsl2591_sample_fetch(const struct device *dev, enum sensor_channel chan)
{
	struct tsl2591_data *data = dev->data;
	uint8_t als_data[4];
	int ret;

#ifdef CONFIG_TSL2591_FETCH_WAIT
	uint8_t status;

	ret = tsl2591_reg_read(dev, TSL2591_REG_STATUS, &status, 1U);
	if (ret < 0) {
		LOG_ERR("Failed to read status register");
		return ret;
	}

	/* Check if ALS has completed an integration cycle since AEN asserted.
	 * If not, sleep for the duration of an integration cycle to ensure valid reading.
	 */
	if (!(status & TSL2591_AVALID_MASK)) {
		k_msleep((data->atime / 100) * TSL2591_MAX_TIME_STEP);
	}

	/* Reassert AEN to determine if next reading is valid */
	ret = tsl2591_reg_update(dev, TSL2591_REG_ENABLE, TSL2591_AEN_MASK, TSL2591_AEN_OFF);
	if (ret < 0) {
		LOG_ERR("Failed to disable ALS");
		return ret;
	}

	ret = tsl2591_reg_update(dev, TSL2591_REG_ENABLE, TSL2591_AEN_MASK, TSL2591_AEN_ON);
	if (ret < 0) {
		LOG_ERR("Failed to re-enable ALS");
		return ret;
	}
#endif

	switch (chan) {
	case SENSOR_CHAN_ALL:
		ret = tsl2591_reg_read(dev, TSL2591_REG_C0DATAL, als_data, 4U);
		if (ret < 0) {
			LOG_ERR("Failed to read ALS data");
			return ret;
		}

		data->vis_count = sys_get_le16(als_data);
		data->ir_count = sys_get_le16(als_data + 2);
		break;
	case SENSOR_CHAN_LIGHT:
		ret = tsl2591_reg_read(dev, TSL2591_REG_C0DATAL, als_data, 2U);
		if (ret < 0) {
			LOG_ERR("Failed to read ALS visible light data");
			return ret;
		}

		data->vis_count = sys_get_le16(als_data);
		break;
	case SENSOR_CHAN_IR:
		ret = tsl2591_reg_read(dev, TSL2591_REG_C1DATAL, als_data, 2U);
		if (ret < 0) {
			LOG_ERR("Failed to read ALS infrared data");
			return ret;
		}

		data->ir_count = sys_get_le16(als_data);
		break;
	default:
		LOG_ERR("Unsupported sensor channel");
		return -ENOTSUP;
	}

#ifdef CONFIG_TSL2591_WARN_SATURATED
	uint16_t max_count = data->atime == 100 ? TSL2591_MAX_ADC_100 : TSL2591_MAX_ADC;
	bool vis_saturated = (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_LIGHT) &&
			     (data->vis_count >= max_count);
	bool ir_saturated = (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_IR) &&
			    (data->ir_count >= max_count);
	if (vis_saturated || ir_saturated) {
		LOG_WRN("Sensor ADC potentially saturated, reading may be invalid");
		return -EOVERFLOW;
	}
#endif

	return 0;
}

static int tsl2591_channel_get(const struct device *dev, enum sensor_channel chan,
			       struct sensor_value *val)
{
	const struct tsl2591_data *data = dev->data;
	int64_t cpl = data->atime * data->again;
	int64_t strength;

	/* Unfortunately, datasheet does not provide a lux conversion formula for this particular
	 * device. There is still ongoing discussion about the proper formula, though this
	 * implementation uses a slightly modified version of the Adafruit library formula:
	 * https://github.com/adafruit/Adafruit_TSL2591_Library/
	 *
	 * Since the device relies on both visible and IR readings to calculate lux,
	 * read SENSOR_CHAN_ALL to get a closer approximation of lux. Reading SENSOR_CHAN_LIGHT or
	 * SENSOR_CHAN_IR individually can be more closely thought of as relative strength
	 * as opposed to true lux.
	 */
	switch (chan) {
	case SENSOR_CHAN_ALL:
		if (data->vis_count > 0) {
			cpl *= 1000000;
			strength =
				(data->vis_count - data->ir_count) *
				(1000000 - (((int64_t)data->ir_count * 1000000) / data->vis_count));
		} else {
			strength = 0;
		}
		break;
	case SENSOR_CHAN_LIGHT:
		strength = data->vis_count;
		break;
	case SENSOR_CHAN_IR:
		strength = data->ir_count;
		break;
	default:
		LOG_ERR("Unsupported sensor channel");
		return -ENOTSUP;
	}

	strength *= TSL2591_LUX_DF;
	val->val1 = strength / cpl;
	val->val2 = ((strength % cpl) * 1000000) / cpl;

	return 0;
}

#ifdef CONFIG_TSL2591_TRIGGER
static int tsl2591_set_threshold(const struct device *dev, enum sensor_attribute attr,
				 const struct sensor_value *val)
{
	const struct tsl2591_data *data = dev->data;
	const struct tsl2591_config *config = dev->config;
	uint64_t cpl;
	uint32_t raw;
	uint16_t thld;
	uint8_t thld_reg;
	uint8_t cmd[3];
	int ret;

	/* Convert from relative strength of visible light to raw value */
	cpl = data->atime * data->again;
	raw = ((val->val1 * cpl) / TSL2591_LUX_DF) +
	      ((val->val2 * cpl) / (1000000U * TSL2591_LUX_DF));

	if (raw > TSL2591_MAX_ADC) {
		LOG_ERR("Given value would overflow threshold register");
		return -EOVERFLOW;
	}

	thld = sys_cpu_to_le16(raw);
	thld_reg = attr == SENSOR_ATTR_LOWER_THRESH ? TSL2591_REG_AILTL : TSL2591_REG_AIHTL;

	cmd[0] = TSL2591_NORMAL_CMD | thld_reg;
	bytecpy(cmd + 1, &thld, 2U);

	ret = i2c_write_dt(&config->i2c, cmd, 3U);
	if (ret < 0) {
		LOG_ERR("Failed to set interrupt threshold");
	}

	return ret;
}

static int tsl2591_set_persist(const struct device *dev, int32_t persist_filter)
{
	uint8_t persist_mode;
	int ret;

	switch (persist_filter) {
	case 0:
		persist_mode = TSL2591_PERSIST_EVERY;
		break;
	case 1:
		persist_mode = TSL2591_PERSIST_1;
		break;
	case 2:
		persist_mode = TSL2591_PERSIST_2;
		break;
	case 3:
		persist_mode = TSL2591_PERSIST_3;
		break;
	case 5:
		persist_mode = TSL2591_PERSIST_5;
		break;
	case 10:
		persist_mode = TSL2591_PERSIST_10;
		break;
	case 15:
		persist_mode = TSL2591_PERSIST_15;
		break;
	case 20:
		persist_mode = TSL2591_PERSIST_20;
		break;
	case 25:
		persist_mode = TSL2591_PERSIST_25;
		break;
	case 30:
		persist_mode = TSL2591_PERSIST_30;
		break;
	case 35:
		persist_mode = TSL2591_PERSIST_35;
		break;
	case 40:
		persist_mode = TSL2591_PERSIST_40;
		break;
	case 45:
		persist_mode = TSL2591_PERSIST_45;
		break;
	case 50:
		persist_mode = TSL2591_PERSIST_50;
		break;
	case 55:
		persist_mode = TSL2591_PERSIST_55;
		break;
	case 60:
		persist_mode = TSL2591_PERSIST_60;
		break;
	default:
		LOG_ERR("Invalid persist filter");
		return -EINVAL;
	}

	ret = tsl2591_reg_write(dev, TSL2591_REG_PERSIST, persist_mode);
	if (ret < 0) {
		LOG_ERR("Failed to set persist filter");
	}

	return ret;
}
#endif

static int tsl2591_set_gain(const struct device *dev, enum sensor_gain_tsl2591 gain)
{
	struct tsl2591_data *data = dev->data;
	uint8_t gain_mode;
	int ret;

	switch (gain) {
	case TSL2591_SENSOR_GAIN_LOW:
		data->again = TSL2591_GAIN_SCALE_LOW;
		gain_mode = TSL2591_GAIN_MODE_LOW;
		break;
	case TSL2591_SENSOR_GAIN_MED:
		data->again = TSL2591_GAIN_SCALE_MED;
		gain_mode = TSL2591_GAIN_MODE_MED;
		break;
	case TSL2591_SENSOR_GAIN_HIGH:
		data->again = TSL2591_GAIN_SCALE_HIGH;
		gain_mode = TSL2591_GAIN_MODE_HIGH;
		break;
	case TSL2591_SENSOR_GAIN_MAX:
		data->again = TSL2591_GAIN_SCALE_MAX;
		gain_mode = TSL2591_GAIN_MODE_MAX;
		break;
	default:
		LOG_ERR("Invalid gain mode");
		return -EINVAL;
	}

	ret = tsl2591_reg_update(dev, TSL2591_REG_CONFIG, TSL2591_AGAIN_MASK, gain_mode);
	if (ret < 0) {
		LOG_ERR("Failed to set gain mode");
	}

	return ret;
}

static int tsl2591_set_integration(const struct device *dev, int32_t integration_time)
{
	struct tsl2591_data *data = dev->data;
	uint8_t atime_mode;
	int ret;

	switch (integration_time) {
	case 100:
		atime_mode = TSL2591_INTEGRATION_100MS;
		break;
	case 200:
		atime_mode = TSL2591_INTEGRATION_200MS;
		break;
	case 300:
		atime_mode = TSL2591_INTEGRATION_300MS;
		break;
	case 400:
		atime_mode = TSL2591_INTEGRATION_400MS;
		break;
	case 500:
		atime_mode = TSL2591_INTEGRATION_500MS;
		break;
	case 600:
		atime_mode = TSL2591_INTEGRATION_600MS;
		break;
	default:
		LOG_ERR("Invalid integration time");
		return -EINVAL;
	}

	ret = tsl2591_reg_update(dev, TSL2591_REG_CONFIG, TSL2591_ATIME_MASK, atime_mode);
	if (ret < 0) {
		LOG_ERR("Failed to set integration time");
		return ret;
	}

	data->atime = integration_time;

	return 0;
}

static int tsl2591_attr_set(const struct device *dev, enum sensor_channel chan,
			    enum sensor_attribute attr, const struct sensor_value *val)
{
	const struct tsl2591_data *data = dev->data;
	int ret;

	ret = tsl2591_reg_update(dev, TSL2591_REG_ENABLE, TSL2591_POWER_MASK, TSL2591_POWER_OFF);
	if (ret < 0) {
		LOG_ERR("Unable to power down device");
		return ret;
	}

#ifdef CONFIG_TSL2591_TRIGGER
	if (attr == SENSOR_ATTR_UPPER_THRESH || attr == SENSOR_ATTR_LOWER_THRESH) {
		if (chan == SENSOR_CHAN_LIGHT) {
			ret = tsl2591_set_threshold(dev, attr, val);
		} else {
			LOG_ERR("Attribute not supported for channel");
			ret = -ENOTSUP;
		}
		goto exit;
	}
#endif

	switch ((enum sensor_attribute_tsl2591)attr) {
	case SENSOR_ATTR_GAIN_MODE:
		ret = tsl2591_set_gain(dev, (enum sensor_gain_tsl2591)val->val1);
		break;
	case SENSOR_ATTR_INTEGRATION_TIME:
		ret = tsl2591_set_integration(dev, val->val1);
		break;

#ifdef CONFIG_TSL2591_TRIGGER
	case SENSOR_ATTR_INT_PERSIST:
		ret = tsl2591_set_persist(dev, val->val1);
		break;
#endif
	default:
		LOG_ERR("Invalid sensor attribute");
		ret = -EINVAL;
		goto exit; /* So the compiler doesn't warn if triggers not enabled */
	}

exit:
	if (data->powered_on) {
		ret = tsl2591_reg_update(dev, TSL2591_REG_ENABLE, TSL2591_POWER_MASK,
					 TSL2591_POWER_ON);
	}

	return ret;
}

static int tsl2591_setup(const struct device *dev)
{
	struct tsl2591_data *data = dev->data;
	uint8_t device_id;
	int ret;

	ret = tsl2591_reg_write(dev, TSL2591_REG_CONFIG, TSL2591_SRESET);
	if (ret < 0) {
		LOG_ERR("Failed to reset device");
		return ret;
	}

	ret = tsl2591_reg_read(dev, TSL2591_REG_ID, &device_id, 1U);
	if (ret < 0) {
		LOG_ERR("Failed to read device ID");
		return ret;
	}

	if (device_id != TSL2591_DEV_ID) {
		LOG_ERR("Device with ID 0x%02x is not supported", device_id);
		return -ENOTSUP;
	}

	/* Set initial values to match sensor values on reset */
	data->again = TSL2591_GAIN_SCALE_LOW;
	data->atime = 100U;

	ret = tsl2591_reg_write(dev, TSL2591_REG_ENABLE, TSL2591_POWER_ON);
	if (ret < 0) {
		LOG_ERR("Failed to perform initial power up of device");
		return ret;
	}

	data->powered_on = true;

	return 0;
}

static int tsl2591_init(const struct device *dev)
{
	const struct tsl2591_config *config = dev->config;
	int ret;

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

	ret = tsl2591_setup(dev);
	if (ret < 0) {
		LOG_ERR("Failed to setup device");
		return ret;
	}

#ifdef CONFIG_TSL2591_TRIGGER
	ret = tsl2591_initialize_int(dev);
	if (ret < 0) {
		LOG_ERR("Failed to initialize interrupt!");
		return ret;
	}
#endif

	return 0;
}

static const struct sensor_driver_api tsl2591_driver_api = {
#ifdef CONFIG_TSL2591_TRIGGER
	.trigger_set = tsl2591_trigger_set,
#endif
	.attr_set = tsl2591_attr_set,
	.sample_fetch = tsl2591_sample_fetch,
	.channel_get = tsl2591_channel_get};

#ifdef CONFIG_PM_DEVICE
static int tsl2591_pm_action(const struct device *dev, enum pm_device_action action)
{
	struct tsl2591_data *data = dev->data;
	int ret;

	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		ret = tsl2591_reg_update(dev, TSL2591_REG_ENABLE, TSL2591_POWER_MASK,
					 TSL2591_POWER_ON);
		if (ret < 0) {
			LOG_ERR("Failed to power on device");
			return ret;
		}

		data->powered_on = true;
		break;
	case PM_DEVICE_ACTION_SUSPEND:
		ret = tsl2591_reg_update(dev, TSL2591_REG_ENABLE, TSL2591_POWER_MASK,
					 TSL2591_POWER_OFF);
		if (ret < 0) {
			LOG_ERR("Failed to power off device");
			return ret;
		}

		data->powered_on = false;
		break;
	default:
		LOG_ERR("Unsupported PM action");
		return -ENOTSUP;
	}

	return 0;
}
#endif

#define TSL2591_INIT_INST(n)                                                                       \
	static struct tsl2591_data tsl2591_data_##n;                                               \
	static const struct tsl2591_config tsl2591_config_##n = {                                  \
		.i2c = I2C_DT_SPEC_INST_GET(n),                                                    \
		IF_ENABLED(CONFIG_TSL2591_TRIGGER,                                                 \
			   (.int_gpio = GPIO_DT_SPEC_INST_GET_OR(n, int_gpios, {0}),))};           \
	PM_DEVICE_DT_INST_DEFINE(n, tsl2591_pm_action);                                            \
	SENSOR_DEVICE_DT_INST_DEFINE(n, tsl2591_init, PM_DEVICE_DT_INST_GET(n), &tsl2591_data_##n, \
				     &tsl2591_config_##n, POST_KERNEL,                             \
				     CONFIG_SENSOR_INIT_PRIORITY, &tsl2591_driver_api);

DT_INST_FOREACH_STATUS_OKAY(TSL2591_INIT_INST)
