
#define DT_DRV_COMPAT cd74hc4067

#include <errno.h>
#include <stdbool.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>

#include "cd74hc4067.h"

LOG_MODULE_REGISTER(cd74hc4067, CONFIG_CD74HC4067_LOG_LEVEL);

#define CD74HC4067_CHANNEL_COUNT 16U
#define CD74HC4067_SELECTION_PIN_COUNT 4U

struct cd74hc4067_config {
	struct gpio_dt_spec sel[CD74HC4067_SELECTION_PIN_COUNT];
	struct gpio_dt_spec enable;
	bool has_enable;
};

struct cd74hc4067_data {
	struct k_mutex lock;
	uint8_t current_channel;
	bool enabled;
};

static int cd74hc4067_sync_enable_state(const struct device *dev, bool enable)
{
	const struct cd74hc4067_config *cfg = dev->config;
	struct cd74hc4067_data *data = dev->data;
	int ret = 0;

	if (!cfg->has_enable) {
		if (!enable) {
			return -ENOTSUP;
		}

		data->enabled = true;
		return 0;
	}

	if (!device_is_ready(cfg->enable.port)) {
		LOG_ERR("%s enable GPIO not ready", dev->name);
		return -ENODEV;
	}

	ret = gpio_pin_set_dt(&cfg->enable, enable ? 0 : 1);
	if (ret == 0) {
		data->enabled = enable;
	}

	return ret;
}

static int cd74hc4067_configure_gpios(const struct device *dev)
{
	const struct cd74hc4067_config *cfg = dev->config;
	int ret;

	for (size_t i = 0; i < ARRAY_SIZE(cfg->sel); i++) {
		const struct gpio_dt_spec *sel = &cfg->sel[i];

		if (!device_is_ready(sel->port)) {
			LOG_ERR("%s sel%zu GPIO not ready", dev->name, i);
			return -ENODEV;
		}

		ret = gpio_pin_configure_dt(sel, GPIO_OUTPUT_INACTIVE);
		if (ret != 0) {
			LOG_ERR("%s failed to configure sel%zu GPIO (%d)", dev->name, i, ret);
			return ret;
		}
	}

	if (cfg->has_enable) {
		if (!device_is_ready(cfg->enable.port)) {
			LOG_ERR("%s enable GPIO not ready", dev->name);
			return -ENODEV;
		}

		ret = gpio_pin_configure_dt(&cfg->enable, GPIO_OUTPUT_HIGH);
		if (ret != 0) {
			LOG_ERR("%s failed to configure enable GPIO (%d)", dev->name, ret);
			return ret;
		}
	}

	return 0;
}

static int cd74hc4067_init(const struct device *dev)
{
	const struct cd74hc4067_config *cfg = dev->config;
	struct cd74hc4067_data *data = dev->data;
	int ret;

	k_mutex_init(&data->lock);
	data->current_channel = 0U;
	data->enabled = !cfg->has_enable;

	ret = cd74hc4067_configure_gpios(dev);
	if (ret != 0) {
		return ret;
	}

	/* Latch the default channel (0) */
	ret = cd74hc4067_select_channel(dev, 0U);
	if (ret != 0) {
		return ret;
	}

	LOG_INF("%s initialized (enable pin %s)", dev->name, cfg->has_enable ? "present" : "absent");

	return 0;
}

int cd74hc4067_select_channel(const struct device *dev, uint8_t channel)
{
	const struct cd74hc4067_config *cfg = dev->config;
	struct cd74hc4067_data *data = dev->data;
	int ret = 0;

	if (channel >= CD74HC4067_CHANNEL_COUNT) {
		return -EINVAL;
	}

	ret = k_mutex_lock(&data->lock, K_FOREVER);
	if (ret != 0) {
		return ret;
	}

	for (size_t i = 0; i < ARRAY_SIZE(cfg->sel); i++) {
		ret = gpio_pin_set_dt(&cfg->sel[i], (channel >> i) & 0x1);
		if (ret != 0) {
			LOG_ERR("%s failed to drive sel%zu GPIO (%d)", dev->name, i, ret);
			goto out;
		}
	}

	data->current_channel = channel;

out:
	k_mutex_unlock(&data->lock);
	return ret;
}

int cd74hc4067_enable(const struct device *dev)
{
	struct cd74hc4067_data *data = dev->data;
	int ret;

	ret = k_mutex_lock(&data->lock, K_FOREVER);
	if (ret != 0) {
		return ret;
	}

	ret = cd74hc4067_sync_enable_state(dev, true);

	k_mutex_unlock(&data->lock);
	return ret;
}

int cd74hc4067_disable(const struct device *dev)
{
	struct cd74hc4067_data *data = dev->data;
	int ret;

	ret = k_mutex_lock(&data->lock, K_FOREVER);
	if (ret != 0) {
		return ret;
	}

	ret = cd74hc4067_sync_enable_state(dev, false);

	k_mutex_unlock(&data->lock);
	return ret;
}

uint8_t cd74hc4067_get_current_channel(const struct device *dev)
{
	struct cd74hc4067_data *data = dev->data;
	uint8_t channel;

	k_mutex_lock(&data->lock, K_FOREVER);
	channel = data->current_channel;
	k_mutex_unlock(&data->lock);

	return channel;
}

#define CD74HC4067_DEFINE(inst)                                                              \
	BUILD_ASSERT(DT_PROP_LEN(DT_DRV_INST(inst), sel_gpios) == CD74HC4067_SELECTION_PIN_COUNT, \
		     "cd74hc4067 requires exactly 4 selection GPIOs");                             \
	static const struct cd74hc4067_config cd74hc4067_config_##inst = {                        \
		.sel = {                                                                             \
			GPIO_DT_SPEC_GET_BY_IDX(DT_DRV_INST(inst), sel_gpios, 0),                    \
			GPIO_DT_SPEC_GET_BY_IDX(DT_DRV_INST(inst), sel_gpios, 1),                    \
			GPIO_DT_SPEC_GET_BY_IDX(DT_DRV_INST(inst), sel_gpios, 2),                    \
			GPIO_DT_SPEC_GET_BY_IDX(DT_DRV_INST(inst), sel_gpios, 3),                    \
		},                                                                                    \
		.enable = COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, enable_gpios),                     \
				      (GPIO_DT_SPEC_GET(DT_DRV_INST(inst), enable_gpios)),              \
				      ((struct gpio_dt_spec){0})),                                          \
		.has_enable = DT_INST_NODE_HAS_PROP(inst, enable_gpios),                              \
	};                                                                                       \
	static struct cd74hc4067_data cd74hc4067_data_##inst;                                     \
	DEVICE_DT_INST_DEFINE(inst, cd74hc4067_init, NULL, &cd74hc4067_data_##inst,               \
			      &cd74hc4067_config_##inst, POST_KERNEL,                                   \
			      CONFIG_CD74HC4067_INIT_PRIORITY, NULL);

DT_INST_FOREACH_STATUS_OKAY(CD74HC4067_DEFINE)

