/*
 * Copyright (c) 2025 Core Devices LLC
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT sifli_sf32lb_gpio

#include <stdint.h>

#include <zephyr/arch/cpu.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/gpio/gpio_utils.h>
#include <zephyr/drivers/clock_control/sf32lb.h>
#include <zephyr/irq.h>
#include <zephyr/sys/math_extras.h>

#include <register.h>

#define GPIO1_DIRX   offsetof(GPIO1_TypeDef, DIR0)
#define GPIO1_DORX   offsetof(GPIO1_TypeDef, DOR0)
#define GPIO1_DOSRX  offsetof(GPIO1_TypeDef, DOSR0)
#define GPIO1_DOCRX  offsetof(GPIO1_TypeDef, DOCR0)
#define GPIO1_DOERX  offsetof(GPIO1_TypeDef, DOER0)
#define GPIO1_DOESRX offsetof(GPIO1_TypeDef, DOESR0)
#define GPIO1_DOECRX offsetof(GPIO1_TypeDef, DOECR0)
#define GPIO1_IESRX  offsetof(GPIO1_TypeDef, IESR0)
#define GPIO1_IECRX  offsetof(GPIO1_TypeDef, IECR0)
#define GPIO1_ISRX   offsetof(GPIO1_TypeDef, ISR0)
#define GPIO1_ITSRX  offsetof(GPIO1_TypeDef, ITSR0)
#define GPIO1_ITCRX  offsetof(GPIO1_TypeDef, ITCR0)
#define GPIO1_IPHCRX offsetof(GPIO1_TypeDef, IPHCR0)
#define GPIO1_IPLCRX offsetof(GPIO1_TypeDef, IPLCR0)
#define GPIO1_IPHSRX offsetof(GPIO1_TypeDef, IPHSR0)
#define GPIO1_IPLSRX offsetof(GPIO1_TypeDef, IPLSR0)

#define PINMUX_PAD_XXYY_PE      BIT(4)
#define PINMUX_PAD_XXYY_PS_PUP  BIT(5)
#define PINMUX_PAD_XXYY_IE      BIT(6)
#define PINMUX_PAD_XXYY_SR_SLOW BIT(8)

struct gpio_sf32lb_config {
	struct gpio_driver_config common;
	uintptr_t gpio;
	uintptr_t pinmux;
};

struct gpio_sf32lb_data {
	struct gpio_driver_data common;
	sys_slist_t callbacks;
	gpio_port_pins_t od;
};

static bool shared_initialized;
static const struct device *controllers[] = {
	DT_FOREACH_CHILD_STATUS_OKAY_SEP(DT_INST_PARENT(0), DEVICE_DT_GET, (,)),
};

BUILD_ASSERT((DT_NODE_HAS_COMPAT(DT_INST_PARENT(0), sifli_sf32lb_gpio_parent)) &&
		     (DT_NUM_INST_STATUS_OKAY(sifli_sf32lb_gpio_parent) == 1),
	     "Only one parent instance is supported");

static void gpio_sf32lb_irq(const void *arg)
{
	for (size_t c = 0U; c < ARRAY_SIZE(controllers); c++) {
		const struct gpio_sf32lb_config *config = controllers[c]->config;
		struct gpio_sf32lb_data *data = controllers[c]->data;
		uint8_t min, max;
		uint32_t val;

		min = u32_count_trailing_zeros(config->common.port_pin_mask);
		max = 32 - u32_count_leading_zeros(config->common.port_pin_mask);

		val = sys_read32(config->gpio + GPIO1_ISRX);
		for (uint8_t i = min; i < max; i++) {
			if ((val & BIT(i)) != 0U) {
				gpio_fire_callbacks(&data->callbacks, controllers[c], BIT(i));
			}
		}
		sys_write32(val, config->gpio + GPIO1_ISRX);
	}
}

static inline int gpio_sf32lb_configure(const struct device *port, gpio_pin_t pin,
					gpio_flags_t flags)
{
	const struct gpio_sf32lb_config *config = port->config;
	struct gpio_sf32lb_data *data = port->data;
	uint32_t val;

	if ((flags & GPIO_OUTPUT) != 0U) {
		/* disable ISR */
		sys_write32(BIT(pin), config->gpio + GPIO1_IECRX);

		if ((flags & GPIO_SINGLE_ENDED) != 0U) {
			if ((flags & GPIO_LINE_OPEN_DRAIN) == 0U) {
				return -ENOTSUP;
			}

			data->od |= BIT(pin);

			/* disable O */
			sys_write32(BIT(pin), config->gpio + GPIO1_DOCRX);

			/* set initial state (OE) */
			if ((flags & GPIO_OUTPUT_INIT_HIGH) != 0U) {
				sys_write32(BIT(pin), config->gpio + GPIO1_DOESRX);
			} else if ((flags & GPIO_OUTPUT_INIT_LOW) != 0U) {
				sys_write32(BIT(pin), config->gpio + GPIO1_DOECRX);
			}
		} else {
			data->od &= ~BIT(pin);

			/* enable OE */
			sys_write32(BIT(pin), config->gpio + GPIO1_DOESRX);

			/* set initial state (O) */
			if ((flags & GPIO_OUTPUT_INIT_HIGH) != 0U) {
				sys_write32(BIT(pin), config->gpio + GPIO1_DOSRX);
			} else if ((flags & GPIO_OUTPUT_INIT_LOW) != 0U) {
				sys_write32(BIT(pin), config->gpio + GPIO1_DOCRX);
			}
		}
	} else if ((flags & GPIO_INPUT) != 0U) {
		data->od &= ~BIT(pin);

		/* disable OE */
		sys_write32(BIT(pin), config->gpio + GPIO1_DOECRX);
	} else {
		return -ENOTSUP;
	}

	/* configure pad settings in PINMUX */
	val = PINMUX_PAD_XXYY_SR_SLOW;

	if ((flags & GPIO_INPUT) != 0U) {
		val |= PINMUX_PAD_XXYY_IE;
	}

	if ((flags & GPIO_PULL_UP) != 0U) {
		val |= PINMUX_PAD_XXYY_PE | PINMUX_PAD_XXYY_PS_PUP;
	} else if ((flags & GPIO_PULL_DOWN) != 0U) {
		val |= PINMUX_PAD_XXYY_PE;
	}

	sys_write32(val, config->pinmux + (pin * 4U));

	return 0;
}

static int gpio_sf32lb_port_get_raw(const struct device *port, uint32_t *value)
{
	const struct gpio_sf32lb_config *config = port->config;

	*value = sys_read32(config->gpio + GPIO1_DIRX);

	return 0;
}

static int gpio_sf32lb_port_set_masked_raw(const struct device *port, gpio_port_pins_t mask,
					   gpio_port_value_t value)
{
	const struct gpio_sf32lb_config *config = port->config;
	struct gpio_sf32lb_data *data = port->data;
	gpio_port_pins_t pp_mask, od_mask;
	uint32_t val;

	pp_mask = mask & ~data->od;
	if (pp_mask != 0U) {
		val = sys_read32(config->gpio + GPIO1_DORX);
		val = (val & ~pp_mask) | (value & pp_mask);
		sys_write32(val, config->gpio + GPIO1_DORX);
	}

	od_mask = mask & data->od;
	if (od_mask != 0U) {
		val = sys_read32(config->gpio + GPIO1_DOERX);
		val = (val & ~od_mask) | (value & od_mask);
		sys_write32(val, config->gpio + GPIO1_DOERX);
	}

	return 0;
}

static int gpio_sf32lb_port_set_bits_raw(const struct device *port, gpio_port_pins_t pins)
{
	const struct gpio_sf32lb_config *config = port->config;
	struct gpio_sf32lb_data *data = port->data;
	gpio_port_pins_t pp_pins, od_pins;

	pp_pins = pins & ~data->od;
	sys_write32(pp_pins, config->gpio + GPIO1_DOSRX);

	od_pins = pins & data->od;
	sys_write32(od_pins, config->gpio + GPIO1_DOESRX);

	return 0;
}

static int gpio_sf32lb_port_clear_bits_raw(const struct device *port, gpio_port_pins_t pins)
{
	const struct gpio_sf32lb_config *config = port->config;
	struct gpio_sf32lb_data *data = port->data;
	gpio_port_pins_t pp_pins, od_pins;

	pp_pins = pins & ~data->od;
	sys_write32(pp_pins, config->gpio + GPIO1_DOCRX);

	od_pins = pins & data->od;
	sys_write32(od_pins, config->gpio + GPIO1_DOECRX);

	return 0;
}

static int gpio_sf32lb_port_toggle_bits(const struct device *port, gpio_port_pins_t pins)
{
	const struct gpio_sf32lb_config *config = port->config;
	struct gpio_sf32lb_data *data = port->data;
	gpio_port_pins_t pp_pins, od_pins;
	uint32_t val;

	pp_pins = pins & ~data->od;
	if (pp_pins != 0U) {
		val = sys_read32(config->gpio + GPIO1_DORX);
		val ^= pp_pins;
		sys_write32(val, config->gpio + GPIO1_DORX);
	}

	od_pins = pins & data->od;
	if (od_pins != 0U) {
		val = sys_read32(config->gpio + GPIO1_DOERX);
		val ^= od_pins;
		sys_write32(val, config->gpio + GPIO1_DOERX);
	}

	return 0;
}

static int gpio_sf32lb_pin_interrupt_configure(const struct device *port, gpio_pin_t pin,
					       enum gpio_int_mode mode, enum gpio_int_trig trig)
{
	const struct gpio_sf32lb_config *config = port->config;

	if (mode == GPIO_INT_MODE_DISABLED) {
		sys_write32(BIT(pin), config->gpio + GPIO1_IECRX);
	} else if ((mode == GPIO_INT_MODE_EDGE) || (mode == GPIO_INT_MODE_LEVEL)) {
		if (mode == GPIO_INT_MODE_EDGE) {
			sys_write32(BIT(pin), config->gpio + GPIO1_ITSRX);
		} else {
			sys_write32(BIT(pin), config->gpio + GPIO1_ITCRX);
		}

		switch (trig) {
		case GPIO_INT_TRIG_LOW:
			sys_write32(BIT(pin), config->gpio + GPIO1_IPHCRX);
			sys_write32(BIT(pin), config->gpio + GPIO1_IPLSRX);
			break;
		case GPIO_INT_TRIG_HIGH:
			sys_write32(BIT(pin), config->gpio + GPIO1_IPHSRX);
			sys_write32(BIT(pin), config->gpio + GPIO1_IPLCRX);
			break;
		case GPIO_INT_TRIG_BOTH:
			sys_write32(BIT(pin), config->gpio + GPIO1_IPHSRX);
			sys_write32(BIT(pin), config->gpio + GPIO1_IPLSRX);
			break;
		default:
			return -ENOTSUP;
		}

		sys_write32(BIT(pin), config->gpio + GPIO1_IESRX);
	} else {
		return -ENOTSUP;
	}

	return 0;
}

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

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

static DEVICE_API(gpio, gpio_sf32lb_api) = {
	.pin_configure = gpio_sf32lb_configure,
	.port_get_raw = gpio_sf32lb_port_get_raw,
	.port_set_masked_raw = gpio_sf32lb_port_set_masked_raw,
	.port_set_bits_raw = gpio_sf32lb_port_set_bits_raw,
	.port_clear_bits_raw = gpio_sf32lb_port_clear_bits_raw,
	.port_toggle_bits = gpio_sf32lb_port_toggle_bits,
	.pin_interrupt_configure = gpio_sf32lb_pin_interrupt_configure,
	.manage_callback = gpio_sf32lb_manage_callback,
};

static int gpio_sf32lb_init(const struct device *dev)
{
	if (!shared_initialized) {
		struct sf32lb_clock_dt_spec clk = SF32LB_CLOCK_DT_SPEC_GET(DT_INST_PARENT(0));

		if (!sf32lb_clock_is_ready_dt(&clk)) {
			return -ENODEV;
		}

		(void)sf32lb_clock_control_on_dt(&clk);

		IRQ_CONNECT(DT_IRQN(DT_INST_PARENT(0)), DT_IRQ(DT_INST_PARENT(0), priority),
			    gpio_sf32lb_irq, NULL, 0);
		irq_enable(DT_IRQN(DT_INST_PARENT(0)));

		shared_initialized = true;
	}

	return 0;
}

#define GPIO_SF32LB_DEFINE(n)                                                                      \
	static const struct gpio_sf32lb_config gpio_sf32lb_config##n = {                           \
		.common =                                                                          \
			{                                                                          \
				.port_pin_mask = GPIO_PORT_PIN_MASK_FROM_DT_INST(n),               \
			},                                                                         \
		.gpio = DT_INST_REG_ADDR(n),                                                       \
		.pinmux = DT_REG_ADDR_BY_IDX(DT_INST_PHANDLE(n, sifli_pinmuxs),                    \
					     DT_INST_PHA(n, sifli_pinmuxs, port)) +                \
			  DT_INST_PHA(n, sifli_pinmuxs, offset),                                   \
	};                                                                                         \
                                                                                                   \
	static struct gpio_sf32lb_data gpio_sf32lb_data##n;                                        \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(n, gpio_sf32lb_init, NULL, &gpio_sf32lb_data##n,                     \
			      &gpio_sf32lb_config##n, PRE_KERNEL_1, CONFIG_GPIO_INIT_PRIORITY,     \
			      &gpio_sf32lb_api);

DT_INST_FOREACH_STATUS_OKAY(GPIO_SF32LB_DEFINE)
