/*
 * Copyright (c) 2025 Michael Estes
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nxp_pcal9722

#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(pcal9722, CONFIG_GPIO_LOG_LEVEL);

#include <zephyr/drivers/gpio/gpio_utils.h>

/* PCAL9722 Register addresses */
#define PCAL9722_INPUT_PORT0     0x00
#define PCAL9722_OUTPUT_PORT0    0x04
#define PCAL9722_CONFIG_PORT0    0x0C
#define PCAL9722_PULL_EN0        0x4C
#define PCAL9722_PULL_SEL0       0x50
#define PCAL9722_IRQMASK_PORT0   0x54
#define PCAL9722_IRQ_STAT_PORT0  0x58
#define PCAL9722_IRQEDGE_PORT0_A 0x60
#define PCAL9722_IRQ_CLEAR_PORT0 0x68

/* Bit instructs PCAL9722 to auto-increment register address between SPI bytes */
#define PCAL9722_AUTO_INC BIT(7)

/* Base address of the PCAL9722 sent as the first byte in every SPI transceive */
#define PCAL9722_ADDRESS 0x40

/* Set this bit in PCAL9722_ADDRESS to address another PCAL9722 device on the same chip select */
#define ADDRESS_BIT (BIT(1))

#define PCAL9722_READ_CMD BIT(0)
#define NUM_PINS          22
#define ALL_PINS          ((uint32_t)BIT_MASK(NUM_PINS))

#define PCAL9722_ENABLE_IRQ_SUPPORT DT_ANY_INST_HAS_PROP_STATUS_OKAY(irq_gpios)

struct pcal9722_drv_data {
	/* gpio_driver_data needs to be first */
	struct gpio_driver_data common;
	struct k_sem *lock;
	struct gpio_callback gpio_cb;
	struct k_work work;
	const struct device *dev;
	sys_slist_t cb;
};

struct pcal9722_config {
	/* gpio_driver_config needs to be first */
	struct gpio_driver_config common;
	struct spi_dt_spec spi;
#if PCAL9722_ENABLE_IRQ_SUPPORT
	const struct gpio_dt_spec gpio_int;
#endif
	const uint8_t addr;
};

static int gpio_pcal9722_reg_read(const struct pcal9722_config *cfg, uint8_t reg, uint8_t *val,
				  uint8_t len)
{
	uint8_t cmd_buf[2 + len];

	cmd_buf[0] = cfg->addr | PCAL9722_READ_CMD;
	cmd_buf[1] = reg;

	const struct spi_buf tx_buf = {.buf = cmd_buf, .len = ARRAY_SIZE(cmd_buf)};
	const struct spi_buf rx_buf = {.buf = cmd_buf, .len = ARRAY_SIZE(cmd_buf)};
	const struct spi_buf_set tx = {.buffers = &tx_buf, .count = 1};
	const struct spi_buf_set rx = {.buffers = &rx_buf, .count = 1};
	int ret;

	ret = spi_transceive_dt(&cfg->spi, &tx, &rx);
	if (ret < 0) {
		return ret;
	}

	memcpy(val, &cmd_buf[2], len);

	return 0;
}

static int gpio_pcal9722_reg_write(const struct pcal9722_config *cfg, uint8_t reg, uint8_t *val,
				   uint8_t len)
{
	uint8_t cmd_buf[2] = {cfg->addr, PCAL9722_AUTO_INC | reg};
	const struct spi_buf tx_buf[2] = {
		{.buf = cmd_buf, .len = sizeof(cmd_buf)},
		{.buf = val, .len = len},
	};
	const struct spi_buf_set tx = {.buffers = &tx_buf[0], .count = 2};

	return spi_transceive_dt(&cfg->spi, &tx, NULL);
}

static int gpio_pcal9722_config(const struct device *dev, gpio_pin_t pin, gpio_flags_t flags)
{
	const struct pcal9722_config *cfg = dev->config;
	struct pcal9722_drv_data *drv_data = dev->data;
	uint32_t pullen = 0;
	uint32_t pull = 0;
	uint32_t dir = 0;
	uint32_t val = 0;
	int rc = 0;

	/* Can't do SPI bus operations from an ISR */
	if (k_is_in_isr()) {
		return -EWOULDBLOCK;
	}

	/* Single Ended lines (Open drain and open source) not supported */
	if ((flags & GPIO_SINGLE_ENDED) != 0) {
		return -ENOTSUP;
	}

	/* Simultaneous input & output mode not supported */
	if (((flags & GPIO_INPUT) != 0) && ((flags & GPIO_OUTPUT) != 0)) {
		return -ENOTSUP;
	}

	k_sem_take(drv_data->lock, K_FOREVER);

	rc = gpio_pcal9722_reg_read(cfg, PCAL9722_CONFIG_PORT0, (uint8_t *)&dir, sizeof(dir));
	if (rc != 0) {
		goto out;
	}
	dir = sys_le32_to_cpu(dir) & ALL_PINS;

	rc = gpio_pcal9722_reg_read(cfg, PCAL9722_OUTPUT_PORT0, (uint8_t *)&val, sizeof(val));
	if (rc != 0) {
		goto out;
	}
	val = sys_le32_to_cpu(val) & ALL_PINS;

	rc = gpio_pcal9722_reg_read(cfg, PCAL9722_PULL_EN0, (uint8_t *)&pullen, sizeof(pullen));
	if (rc != 0) {
		goto out;
	}
	pullen = sys_le32_to_cpu(pullen) & ALL_PINS;

	rc = gpio_pcal9722_reg_read(cfg, PCAL9722_PULL_SEL0, (uint8_t *)&pull, sizeof(pull));
	if (rc != 0) {
		goto out;
	}
	pull = sys_le32_to_cpu(pull) & ALL_PINS;

	/* Ensure either Output or Input is specified */
	WRITE_BIT(dir, pin, (flags & GPIO_INPUT));
	if (flags & GPIO_OUTPUT_INIT_LOW || flags & GPIO_OUTPUT_INIT_HIGH) {
		WRITE_BIT(val, pin, (flags & GPIO_OUTPUT_INIT_HIGH));
	}

	if (flags & GPIO_PULL_UP || flags & GPIO_PULL_DOWN) {
		WRITE_BIT(pullen, pin, 1);
		WRITE_BIT(pull, pin, (flags & GPIO_PULL_UP));
	} else {
		WRITE_BIT(pullen, pin, 0);
	}

	pull = sys_cpu_to_le32(pull & ALL_PINS);
	pullen = sys_cpu_to_le32(pull & ALL_PINS);
	val = sys_cpu_to_le32(val & ALL_PINS);
	dir = sys_cpu_to_le32(dir & ALL_PINS);

	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_OUTPUT_PORT0, (uint8_t *)&val, 3);
	if (rc != 0) {
		goto out;
	}

	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_CONFIG_PORT0, (uint8_t *)&dir, 3);
	if (rc != 0) {
		goto out;
	}

	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_PULL_SEL0, (uint8_t *)&pull, 3);
	if (rc != 0) {
		goto out;
	}

	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_PULL_EN0, (uint8_t *)&pullen, 3);
	if (rc != 0) {
		goto out;
	}

out:
	k_sem_give(drv_data->lock);
	return rc;
}

static int gpio_pcal9722_port_read(const struct device *dev, gpio_port_value_t *value)
{
	const struct pcal9722_config *cfg = dev->config;
	struct pcal9722_drv_data *drv_data = dev->data;
	uint32_t data = 0;
	int rc = 0;

	/* Can't do SPI bus operations from an ISR */
	if (k_is_in_isr()) {
		return -EWOULDBLOCK;
	}

	k_sem_take(drv_data->lock, K_FOREVER);

	/* Read Input Register */
	rc = gpio_pcal9722_reg_read(cfg, PCAL9722_INPUT_PORT0, (uint8_t *)&data, 3);
	data = sys_le32_to_cpu(data) & ALL_PINS;

	LOG_DBG("read 0x%06X got %d", data, rc);

	if (rc == 0) {
		*value = (gpio_port_value_t)(data);
	}

	k_sem_give(drv_data->lock);
	return rc;
}

static int gpio_pcal9722_port_write(const struct device *dev, gpio_port_pins_t mask,
				    gpio_port_value_t value, gpio_port_value_t toggle)
{
	const struct pcal9722_config *cfg = dev->config;
	struct pcal9722_drv_data *drv_data = dev->data;
	uint32_t data = 0;
	int rc = 0;

	/* Can't do SPI bus operations from an ISR */
	if (k_is_in_isr()) {
		return -EWOULDBLOCK;
	}

	k_sem_take(drv_data->lock, K_FOREVER);

	gpio_pcal9722_reg_read(cfg, PCAL9722_OUTPUT_PORT0, (uint8_t *)&data, 3);
	data = sys_le32_to_cpu(data) & ALL_PINS;
	data = sys_cpu_to_le32((((data & ~mask) | (value & mask)) ^ toggle) & ALL_PINS);

	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_OUTPUT_PORT0, (uint8_t *)&data, 3);

	k_sem_give(drv_data->lock);

	LOG_DBG("write %x msk %08x val %08x => %x: %d", data, mask, value, data, rc);

	return rc;
}

static int gpio_pcal9722_port_set_masked(const struct device *dev, gpio_port_pins_t mask,
					 gpio_port_value_t value)
{
	return gpio_pcal9722_port_write(dev, mask, value, 0);
}

static int gpio_pcal9722_port_set_bits(const struct device *dev, gpio_port_pins_t pins)
{
	return gpio_pcal9722_port_write(dev, pins, pins, 0);
}

static int gpio_pcal9722_port_clear_bits(const struct device *dev, gpio_port_pins_t pins)
{
	return gpio_pcal9722_port_write(dev, pins, 0, 0);
}

static int gpio_pcal9722_port_toggle_bits(const struct device *dev, gpio_port_pins_t pins)
{
	return gpio_pcal9722_port_write(dev, 0, 0, pins);
}

#if PCAL9722_ENABLE_IRQ_SUPPORT
static int gpio_pcal9722_pin_interrupt_configure(const struct device *dev, gpio_pin_t pin,
						 enum gpio_int_mode mode, enum gpio_int_trig trig)
{
	const struct pcal9722_config *cfg = dev->config;
	struct pcal9722_drv_data *drv_data = dev->data;
	uint64_t irqedge = 0;
	uint32_t irqmask = 0;
	int rc = 0;

	k_sem_take(drv_data->lock, K_FOREVER);

	rc = gpio_pcal9722_reg_read(cfg, PCAL9722_IRQEDGE_PORT0_A, (uint8_t *)&irqedge, 6);
	if (rc != 0) {
		goto out;
	}

	rc = gpio_pcal9722_reg_read(cfg, PCAL9722_IRQMASK_PORT0, (uint8_t *)&irqmask, 3);
	if (rc != 0) {
		goto out;
	}

	irqedge = sys_le64_to_cpu(irqedge) & BIT64_MASK(NUM_PINS * 2);
	irqmask = sys_le32_to_cpu(irqmask) & ALL_PINS;

	if (mode == GPIO_INT_MODE_DISABLED) {
		WRITE_BIT(irqmask, pin, 1);
	} else if (mode == GPIO_INT_MODE_LEVEL) {
		WRITE_BIT(irqmask, pin, 0);
		irqedge &= ~(GENMASK64(1, 0) << (pin * 2));
	} else { /* GPIO_INT_MODE_EDGE */
		WRITE_BIT(irqmask, pin, 0);
		if (trig == GPIO_INT_TRIG_BOTH) {
			/* Set both edge trigger bits for this pin */
			irqedge |= (GENMASK64(1, 0) << (pin * 2));
		} else if (trig == GPIO_INT_TRIG_LOW) {
			/* Set the falling-edge bit, clear the rising edge bit */
			irqedge &= ~(GENMASK64(1, 0) << (pin * 2));
			irqedge |= (BIT64(1) << (pin * 2));
		} else { /* GPIO_INT_TRIG_HIGH */
			/* Set the rising-edge bit, clear the falling-edge bit */
			irqedge &= ~(GENMASK64(1, 0) << (pin * 2));
			irqedge |= (BIT64(0) << (pin * 2));
		}
	}

	irqedge = sys_cpu_to_le64(irqedge & BIT64_MASK(NUM_PINS * 2));
	irqmask = sys_cpu_to_le32(irqmask & ALL_PINS);

	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_IRQMASK_PORT0, (uint8_t *)&irqmask, 3);
	if (rc != 0) {
		goto out;
	}
	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_IRQEDGE_PORT0_A, (uint8_t *)&irqedge, 6);

out:
	k_sem_give(drv_data->lock);

	return rc;
}

static int gpio_pcal9722_manage_callback(const struct device *dev, struct gpio_callback *callback,
					 bool set)
{
	struct pcal9722_drv_data *data = dev->data;

	return gpio_manage_callback(&data->cb, callback, set);
}

static void gpio_pcal9722_handle_interrupt(const struct device *dev)
{
	const struct pcal9722_config *cfg = dev->config;
	struct pcal9722_drv_data *drv_data = dev->data;
	int rc = 0;
	uint32_t irq_status = 0;

	k_sem_take(drv_data->lock, K_FOREVER);

	rc = gpio_pcal9722_reg_read(cfg, PCAL9722_IRQ_STAT_PORT0, (uint8_t *)&irq_status, 3);
	if (rc) {
		goto out;
	}

	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_IRQ_CLEAR_PORT0, (uint8_t *)&irq_status, 3);
	if (rc != 0) {
		goto out;
	}

	irq_status = sys_le32_to_cpu(irq_status) & ALL_PINS;

out:
	k_sem_give(drv_data->lock);

	if ((rc == 0) && (irq_status)) {
		gpio_fire_callbacks(&drv_data->cb, dev, irq_status);
	}
}

static void gpio_pcal9722_work_handler(struct k_work *work)
{
	struct pcal9722_drv_data *drv_data = CONTAINER_OF(work, struct pcal9722_drv_data, work);

	gpio_pcal9722_handle_interrupt(drv_data->dev);
}

static void gpio_pcal9722_init_cb(const struct device *dev, struct gpio_callback *gpio_cb,
				  uint32_t pins)
{
	struct pcal9722_drv_data *drv_data =
		CONTAINER_OF(gpio_cb, struct pcal9722_drv_data, gpio_cb);
	ARG_UNUSED(pins);

	k_work_submit(&drv_data->work);
}
#endif /* PCAL9722_ENABLE_IRQ_SUPPORT */

static int gpio_pcal9722_init(const struct device *dev)
{
	const struct pcal9722_config *cfg = dev->config;
#if PCAL9722_ENABLE_IRQ_SUPPORT
	struct pcal9722_drv_data *drv_data = dev->data;
#endif
	uint32_t pins = 0;
	int rc = 0;

	if (!device_is_ready(cfg->spi.bus)) {
		LOG_ERR("SPI bus not ready");
		goto out;
	}

	/* Clear interrupts */
	pins = sys_cpu_to_le32(ALL_PINS);
	rc = gpio_pcal9722_reg_write(cfg, PCAL9722_IRQ_CLEAR_PORT0, (uint8_t *)&pins, 3);
	if (rc != 0) {
		goto out;
	}

#if PCAL9722_ENABLE_IRQ_SUPPORT
	if (!gpio_is_ready_dt(&cfg->gpio_int)) {
		LOG_ERR("Interrupt GPIO not ready");
		rc = -EINVAL;
		goto out;
	}

	drv_data->dev = dev;

	k_work_init(&drv_data->work, gpio_pcal9722_work_handler);

	rc = gpio_pin_configure_dt(&cfg->gpio_int, GPIO_INPUT);
	if (rc) {
		goto out;
	}

	rc = gpio_pin_interrupt_configure_dt(&cfg->gpio_int, GPIO_INT_EDGE_TO_ACTIVE);
	if (rc) {
		goto out;
	}

	gpio_init_callback(&drv_data->gpio_cb, gpio_pcal9722_init_cb, BIT(cfg->gpio_int.pin));
	rc = gpio_add_callback(cfg->gpio_int.port, &drv_data->gpio_cb);

	if (rc) {
		goto out;
	}
#endif /* PCAL9722_ENABLE_IRQ_SUPPORT */

out:
	if (rc) {
		LOG_ERR("%s failed to initialize: %d", dev->name, rc);
	}

	return rc;
}

static DEVICE_API(gpio, api_table) = {
	.pin_configure = gpio_pcal9722_config,
	.port_get_raw = gpio_pcal9722_port_read,
	.port_set_masked_raw = gpio_pcal9722_port_set_masked,
	.port_set_bits_raw = gpio_pcal9722_port_set_bits,
	.port_clear_bits_raw = gpio_pcal9722_port_clear_bits,
	.port_toggle_bits = gpio_pcal9722_port_toggle_bits,
#if PCAL9722_ENABLE_IRQ_SUPPORT
	.pin_interrupt_configure = gpio_pcal9722_pin_interrupt_configure,
	.manage_callback = gpio_pcal9722_manage_callback,
#endif
};

#if PCAL9722_ENABLE_IRQ_SUPPORT
#define IRQ_GPIO(n) .gpio_int = GPIO_DT_SPEC_INST_GET(n, irq_gpios)
#else
#define IRQ_GPIO(n)
#endif

#define GPIO_PCAL9722_INIT(n)                                                                      \
	static const struct pcal9722_config pcal9722_cfg_##n = {                                   \
		.spi = SPI_DT_SPEC_INST_GET(n, SPI_OP_MODE_MASTER | SPI_WORD_SET(8)),              \
		.common =                                                                          \
			{                                                                          \
				.port_pin_mask = GPIO_PORT_PIN_MASK_FROM_DT_INST(n),               \
			},                                                                         \
		.addr = DT_INST_PROP(n, addr),                                                     \
		IRQ_GPIO(n)};                                                                      \
	static K_SEM_DEFINE(pcal9722_drvdata_##n_lock, 1, 1);                                      \
	static struct pcal9722_drv_data pcal9722_drvdata_##n = {                                   \
		.lock = &pcal9722_drvdata_##n_lock};                                               \
	DEVICE_DT_INST_DEFINE(n, gpio_pcal9722_init, NULL, &pcal9722_drvdata_##n,                  \
			      &pcal9722_cfg_##n, POST_KERNEL, CONFIG_GPIO_PCAL9722_INIT_PRIORITY,  \
			      &api_table);

DT_INST_FOREACH_STATUS_OKAY(GPIO_PCAL9722_INIT)
