/*
 * Copyright (c) 2025 Renesas Electronics Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT renesas_rza2m_gpio

#include <zephyr/drivers/gpio/gpio_utils.h>
#include <zephyr/drivers/interrupt_controller/gic.h>
#include <zephyr/dt-bindings/gpio/renesas-rza2m-gpio.h>
#include <zephyr/logging/log.h>
#include "gpio_renesas_rza2m.h"

LOG_MODULE_REGISTER(rza2m_gpio, CONFIG_GPIO_LOG_LEVEL);

/* clang-format off */
static const struct device *gpio_port_devs[] = {
	DT_FOREACH_CHILD_STATUS_OKAY_SEP(DT_PARENT(DT_DRV_INST(0)), DEVICE_DT_GET, (,))};

static const uint32_t gpio_port_regs[] = {
	DT_FOREACH_CHILD_STATUS_OKAY_SEP(DT_PARENT(DT_DRV_INST(0)), DT_REG_ADDR, (,))};
/* clang-format on */

/* TINT pin map (According to the Table 51.37 of HW manual) */
static const gpio_rza2m_port_tint_map_t gpio_rza2m_port_tint_map[] = {
	{TINT31, {PORT0, UNUSED_PORT}, {0x7F, UNUSED_MASK}},
	{TINT30, {PORT1, UNUSED_PORT}, {0x1F, UNUSED_MASK}},
	{TINT29, {PORT2, PORT3}, {0x0F, 0x01}},
	{TINT28, {PORT3, PORT4}, {0x3E, 0x01}},
	{TINT27, {PORT4, UNUSED_PORT}, {0xFE, UNUSED_MASK}},
	{TINT26, {PORT5, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT25, {PORT5, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT24, {PORT6, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT23, {PORT6, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT22, {PORT7, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT21, {PORT7, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT20, {PORT8, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT19, {PORT8, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT18, {PORT9, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT17, {PORT9, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT16, {PORTA, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT15, {PORTA, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT14, {PORTB, UNUSED_PORT}, {0x3F, UNUSED_MASK}},
	{TINT13, {PORTC, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT12, {PORTC, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT11, {PORTD, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT10, {PORTD, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT9, {PORTE, UNUSED_PORT}, {0x7F, UNUSED_MASK}},
	{TINT8, {PORTF, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT7, {PORTF, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT6, {PORTG, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT5, {PORTG, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT4, {PORTH, UNUSED_PORT}, {0x7F, UNUSED_MASK}},
	{TINT3, {PORTJ, UNUSED_PORT}, {0x0F, UNUSED_MASK}},
	{TINT2, {PORTJ, UNUSED_PORT}, {0xF0, UNUSED_MASK}},
	{TINT1, {PORTK, UNUSED_PORT}, {0x3F, UNUSED_MASK}},
	{TINT0, {PORTL, PORTM}, {0x1F, 0x01}}};

static int gpio_rza2m_get_pin_interrupt_line(uint32_t port, uint8_t pin)
{
	int i, j;

	for (i = 0; i < ARRAY_SIZE(gpio_rza2m_port_tint_map); i++) {
		for (j = 0; j < RZA2M_MAX_PORTS_PER_TINT; j++) {
			if (port == gpio_rza2m_port_tint_map[i].ports[j] &&
			    (gpio_rza2m_port_tint_map[i].masks[j] & BIT(pin))) {
				return gpio_rza2m_port_tint_map[i].tint;
			}
		}
	}

	return -ENOTSUP;
}

static uint8_t gpio_rza2m_port_get_output(const struct device *port_dev)
{
	const struct gpio_rza2m_port_config *config = port_dev->config;
	const struct device *int_dev = config->int_dev;
	uint8_t port = config->port;

	return sys_read8(RZA2M_PODR(int_dev, port));
}

static void gpio_rza2m_port_write(const struct device *port_dev, uint8_t value)
{
	const struct gpio_rza2m_port_config *config = port_dev->config;
	const struct device *int_dev = config->int_dev;
	uint8_t port = config->port;

	sys_write8(value, RZA2M_PODR(int_dev, port));
}

static int gpio_rza2m_port_set_bits_raw(const struct device *port_dev, gpio_port_pins_t pins)
{
	uint8_t base_value = gpio_rza2m_port_get_output(port_dev);

	gpio_rza2m_port_write(port_dev, (base_value | pins));

	return 0;
}

static int gpio_rza2m_port_clear_bits_raw(const struct device *port_dev, gpio_port_pins_t pins)
{
	uint8_t base_value = gpio_rza2m_port_get_output(port_dev);

	gpio_rza2m_port_write(port_dev, (base_value & ~pins));

	return 0;
}

static void gpio_rza2m_pin_configure_as_gpio(const struct device *port_dev, uint8_t pin,
					     uint8_t dir)
{
	uint16_t mask16;
	uint16_t reg16;
	uint8_t reg8;
	const struct gpio_rza2m_port_config *config = port_dev->config;
	const struct device *int_dev = config->int_dev;
	uint8_t port = config->port;

	/* Set pin direction */
	reg16 = sys_read16(RZA2M_PDR(int_dev, port));
	mask16 = RZA2M_PDR_MASK << (pin * 2);
	reg16 &= ~mask16;
	reg16 |= dir << (pin * 2);
	sys_write16(reg16, RZA2M_PDR(int_dev, port));

	/* Select general I/O pin function */
	reg8 = sys_read8(RZA2M_PMR(int_dev, port));
	reg8 &= ~BIT(pin);
	sys_write8(reg8, RZA2M_PMR(int_dev, port));
}

static void gpio_rza2m_set_pin_mux_protection(const struct device *port_dev, bool protect)
{
	const struct gpio_rza2m_port_config *config = port_dev->config;
	const struct device *int_dev = config->int_dev;
	uint8_t reg_value = sys_read8(RZA2M_PWPR(int_dev));

	if (protect) {
		reg_value &= ~RZA2M_PWPR_PFSWE_MASK;
		sys_write8(reg_value, RZA2M_PWPR(int_dev));

		reg_value |= RZA2M_PWPR_B0WI_MASK;
		sys_write8(reg_value, RZA2M_PWPR(int_dev));
	} else {
		reg_value &= ~RZA2M_PWPR_B0WI_MASK;
		sys_write8(reg_value, RZA2M_PWPR(int_dev));

		reg_value |= RZA2M_PWPR_PFSWE_MASK;
		sys_write8(reg_value, RZA2M_PWPR(int_dev));
	}
}

static void gpio_rza2m_set_pin_int(const struct device *port_dev, uint8_t pin, bool int_en)
{
	uint8_t value;
	const struct gpio_rza2m_port_config *config = port_dev->config;
	const struct device *int_dev = config->int_dev;
	uint8_t port = config->port;

	/* PFS Register Write Protect : OFF */
	gpio_rza2m_set_pin_mux_protection(port_dev, false);

	value = sys_read8(RZA2M_PFS(int_dev, port, pin));
	if (int_en) {
		value |= RZA2M_PFS_ISEL_MASK;
	} else {
		value &= ~RZA2M_PFS_ISEL_MASK;
	}
	sys_write8(value, RZA2M_PFS(int_dev, port, pin));

	/* PFS Register Write Protect : ON */
	gpio_rza2m_set_pin_mux_protection(port_dev, true);
}

static void rza2m_configure_interrupt_line(int tint_num, enum gpio_rza2m_tint_sense sense)
{
	mm_reg_t reg;
	uint32_t mask;
	uint32_t reg_val;

	if (tint_num >= TINT16) {
		reg = RZA2M_GICD_ICFGR31;
	} else {
		reg = RZA2M_GICD_ICFGR30;
	}

	mask = (1u << (((tint_num % 16u) * 2u) + 1u));

	reg_val = sys_read32(reg);
	reg_val &= ~mask;
	if (sense == RZA2M_GPIO_TINT_SENSE_RISING_EDGE) {
		reg_val |= mask;
	}
	sys_write32(reg_val, reg);
	sys_read32(reg);

	irq_enable(tint_num);
}

/*
 * GPIO HIGH is only possible for
 * PG_2, PG_3, PG_4, PG_5, PG_6, PG_7, PJ_0, PJ_1, PJ_2, PJ_3, PJ_4, PJ_5, and PJ_6
 * see 51.3.5 section of HW Manual
 */
static const gpio_rza2m_high_allowed_pin_t allowed_gpio_high_pins[] = {
	{PORTG, 0xFC}, /* 0xFC = 0b11111100 for bits 2-7 */
	{PORTJ, 0x7F}, /* 0x7F = 0b01111111 for bits 0-6 */
};

static bool is_gpio_high_allowed(uint8_t port, uint8_t pin)
{
	int num_allowed_pins = ARRAY_SIZE(allowed_gpio_high_pins);
	int i;

	for (i = 0; i < num_allowed_pins; i++) {
		if (allowed_gpio_high_pins[i].port == port &&
		    (allowed_gpio_high_pins[i].mask & BIT(pin))) {
			return true;
		}
	}

	return false;
}

static int gpio_rza2m_pin_drive_set(const struct device *port_dev, uint8_t pin, gpio_flags_t flags)
{
	uint16_t reg16;
	uint16_t mask16;
	const struct gpio_rza2m_port_config *config = port_dev->config;
	const struct device *int_dev = config->int_dev;
	uint8_t port = config->port;
	uint8_t drive_strength;

	if (flags & RZA2M_GPIO_DRIVE_HIGH) {
		if (!is_gpio_high_allowed(port, pin)) {
			return -ENOTSUP;
		}
		drive_strength = RZA2M_GPIO_DRIVE_STRENGTH_HIGH;
	} else {
		drive_strength = RZA2M_GPIO_DRIVE_STRENGTH_NORMAL;
	}

	reg16 = sys_read16(RZA2M_DSCR(int_dev, port));
	mask16 = RZA2M_DSCR_MASK << (pin * 2);
	reg16 &= ~mask16;
	reg16 |= drive_strength << (pin * 2);

	sys_write16(reg16, RZA2M_DSCR(int_dev, port));

	return 0;
}

static int gpio_rza2m_pin_interrupt_configure(const struct device *port_dev, gpio_pin_t pin,
					      enum gpio_int_mode mode, enum gpio_int_trig trig)
{
	const struct gpio_rza2m_port_config *config = port_dev->config;
	struct gpio_rza2m_port_data *data = port_dev->data;
	enum gpio_rza2m_tint_sense sense;
	int tint_num;

	/* Validate pin */
	if (pin >= config->ngpios) {
		return -EINVAL;
	}

	/* Map mode and trigger to sense */
	switch (mode) {
	case GPIO_INT_MODE_EDGE:
		if (trig != GPIO_INT_TRIG_HIGH) {
			return -ENOTSUP;
		}
		sense = RZA2M_GPIO_TINT_SENSE_RISING_EDGE;
		break;

	case GPIO_INT_MODE_LEVEL:
		if (trig != GPIO_INT_TRIG_HIGH) {
			return -ENOTSUP;
		}
		sense = RZA2M_GPIO_TINT_SENSE_HIGH_LEVEL;
		break;

	case GPIO_INT_MODE_DISABLED:
		data->mask_irq_en &= ~BIT(pin);
		gpio_rza2m_set_pin_int(port_dev, pin, false);
		return 0;

	default:
		return -EINVAL;
	}

	/* Enable interrupt */
	data->mask_irq_en |= BIT(pin);

	tint_num = gpio_rza2m_get_pin_interrupt_line(config->port, pin);
	if (tint_num < 0) {
		return tint_num;
	}

	rza2m_configure_interrupt_line(tint_num, sense);
	gpio_rza2m_set_pin_int(port_dev, pin, true);

	return 0;
}

static int gpio_rza2m_pin_configure(const struct device *port_dev, gpio_pin_t pin,
				    gpio_flags_t flags)
{
	int ret = 0;
	const struct gpio_rza2m_port_config *config = port_dev->config;

	if (pin >= config->ngpios) {
		LOG_ERR("provided pin %d > %d (ngpios)", pin, config->ngpios);
		return -EINVAL;
	}

	if ((flags & GPIO_PULL_UP) || (flags & GPIO_PULL_DOWN)) {
		return -ENOTSUP;
	}

	if (!flags) {
		/* Disconnected mode */
		gpio_rza2m_pin_configure_as_gpio(port_dev, pin, RZA2M_PDR_HIZ);
	} else if (!(flags & GPIO_OPEN_DRAIN)) {
		/* Configure pin direction */
		if (flags & GPIO_OUTPUT) {
			gpio_rza2m_pin_configure_as_gpio(port_dev, pin, RZA2M_PDR_OUTPUT);
		} else if (flags & GPIO_INPUT) {
			gpio_rza2m_pin_configure_as_gpio(port_dev, pin, RZA2M_PDR_INPUT);
		}

		/* Configure pin drive strength */
		ret = gpio_rza2m_pin_drive_set(port_dev, pin, flags);
		if (ret) {
			LOG_ERR("unable to set gpio drive level");
			return ret;
		}

		/* Configure pin initial value */
		if (flags & GPIO_OUTPUT_INIT_HIGH) {
			ret = gpio_rza2m_port_set_bits_raw(port_dev, BIT(pin));
		} else if (flags & GPIO_OUTPUT_INIT_LOW) {
			ret = gpio_rza2m_port_clear_bits_raw(port_dev, BIT(pin));
		}
	}

	return ret;
}

#ifdef CONFIG_GPIO_GET_CONFIG
static int gpio_rza2m_pin_get_config(const struct device *port_dev, gpio_pin_t pin,
				     gpio_flags_t *flags)
{
	const struct gpio_rza2m_port_config *config = port_dev->config;
	const struct device *int_dev = config->int_dev;
	uint8_t port = config->port;
	uint16_t mask16;
	uint16_t reg16;
	uint8_t reg8;

	/* Get pin direction */
	reg16 = sys_read16(RZA2M_PDR(int_dev, port));
	mask16 = RZA2M_PDR_MASK << (pin * 2);
	reg16 &= mask16;
	if ((reg16 >> (pin * 2)) == RZA2M_PDR_INPUT) {
		*flags |= GPIO_INPUT;
	} else if ((reg16 >> (pin * 2)) == RZA2M_PDR_OUTPUT) {
		*flags |= GPIO_OUTPUT;
	}

	/* Get pin initial value */
	reg8 = sys_read8(RZA2M_PODR(int_dev, port));
	if (reg8 & BIT(pin)) {
		*flags |= GPIO_OUTPUT_INIT_HIGH;
	} else {
		*flags |= GPIO_OUTPUT_INIT_LOW;
	}

	/* Get pin drive strength */
	reg16 = sys_read16(RZA2M_DSCR(int_dev, port));
	mask16 = RZA2M_DSCR_MASK << (pin * 2);
	reg16 &= mask16;
	if ((reg16 >> (pin * 2)) == RZA2M_GPIO_DRIVE_STRENGTH_HIGH) {
		*flags |= RZA2M_GPIO_DRIVE_HIGH;
	}

	return 0;
}
#endif

static int gpio_rza2m_port_get_raw(const struct device *port_dev, gpio_port_value_t *value)
{
	const struct gpio_rza2m_port_config *config = port_dev->config;
	const struct device *int_dev = config->int_dev;
	uint8_t port = config->port;

	*value = sys_read8(RZA2M_PIDR(int_dev, port));

	return 0;
}

static int gpio_rza2m_port_set_masked_raw(const struct device *port_dev, gpio_port_pins_t mask,
					  gpio_port_value_t value)
{
	uint8_t base_value = gpio_rza2m_port_get_output(port_dev);

	gpio_rza2m_port_write(port_dev, (base_value & ~mask) | (value & mask));

	return 0;
}

static int gpio_rza2m_port_toggle_bits(const struct device *port_dev, gpio_port_pins_t pins)
{
	uint8_t base_value = gpio_rza2m_port_get_output(port_dev);

	gpio_rza2m_port_write(port_dev, (base_value ^ pins));

	return 0;
}

static int gpio_rza2m_manage_callback(const struct device *port_dev, struct gpio_callback *callback,
				      bool set)
{
	struct gpio_rza2m_port_data *data = port_dev->data;

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

	return 0;
}

static DEVICE_API(gpio, gpio_rza2m_driver_api) = {
	.pin_configure = gpio_rza2m_pin_configure,
#ifdef CONFIG_GPIO_GET_CONFIG
	.pin_get_config = gpio_rza2m_pin_get_config,
#endif
	.port_get_raw = gpio_rza2m_port_get_raw,
	.port_set_masked_raw = gpio_rza2m_port_set_masked_raw,
	.port_set_bits_raw = gpio_rza2m_port_set_bits_raw,
	.port_clear_bits_raw = gpio_rza2m_port_clear_bits_raw,
	.port_toggle_bits = gpio_rza2m_port_toggle_bits,
	.pin_interrupt_configure = gpio_rza2m_pin_interrupt_configure,
	.manage_callback = gpio_rza2m_manage_callback,
};

static void gpio_rza2m_isr_common(uint16_t idx)
{
	gpio_port_value_t value;

	for (int j = 0; j < RZA2M_MAX_PORTS_PER_TINT; j++) {
		uint32_t port = gpio_rza2m_port_tint_map[idx].ports[j];
		uint16_t mask = gpio_rza2m_port_tint_map[idx].masks[j];

		if (port == UNUSED_PORT) {
			continue;
		}

		for (int i = 0; i < ARRAY_SIZE(gpio_port_regs); i++) {
			if (gpio_port_regs[i] == port) {
				const struct device *port_dev = gpio_port_devs[i];
				struct gpio_rza2m_port_data *data = port_dev->data;
				int pin;

				gpio_rza2m_port_get_raw(port_dev, &value);
				pin = find_lsb_set(data->mask_irq_en & (value & mask)) - 1;

				if (pin >= 0) {
					gpio_fire_callbacks(&data->callbacks, port_dev, BIT(pin));
				}
				break;
			}
		}
	}
}

static int gpio_rza2m_int_init(const struct device *dev)
{
	const struct gpio_rza2m_tint_config *config = dev->config;

	DEVICE_MMIO_MAP(dev, K_MEM_CACHE_NONE);
	config->gpio_int_init();

	return 0;
}

static int gpio_rza2m_port_init(const struct device *port_dev)
{
	return 0;
}

#define GPIO_RZA2M_IRQ_DECLARE_ISR(irq_idx, node_id)                                               \
	static void gpio_rza2m_##irq_idx##_isr(void *param)                                        \
	{                                                                                          \
		int idx = (ARRAY_SIZE(gpio_rza2m_port_tint_map) + TINT0) -                         \
			  (DT_IRQ_BY_IDX(node_id, irq_idx, irq) - GIC_SPI_INT_BASE) - 1;           \
                                                                                                   \
		gpio_rza2m_isr_common(idx);                                                        \
	}

#define GPIO_RZA2M_DECLARE_ALL_IRQS(node_id)                                                       \
	LISTIFY(DT_NUM_IRQS(node_id), GPIO_RZA2M_IRQ_DECLARE_ISR, (;), node_id)

#define GPIO_RZA2M_TINT_CONNECT(irq_idx, node_id)                                                  \
	IRQ_CONNECT(DT_IRQ_BY_IDX(node_id, irq_idx, irq) - GIC_SPI_INT_BASE,                       \
		    DT_IRQ_BY_IDX(node_id, irq_idx, priority), gpio_rza2m_##irq_idx##_isr, NULL,   \
		    DT_IRQ_BY_IDX(node_id, irq_idx, flags));

#define GPIO_RZA2M_TINT_CONNECT_FUNC(node_id)                                                      \
	static void gpio_rza2m_tint_connect_func##node_id(void)                                    \
	{                                                                                          \
		LISTIFY(DT_NUM_IRQS(node_id),                                   \
			GPIO_RZA2M_TINT_CONNECT, (;), node_id)                  \
	}

#define GPIO_RZA2M_INT_INIT(node_id)                                                               \
	GPIO_RZA2M_DECLARE_ALL_IRQS(node_id)                                                       \
	GPIO_RZA2M_TINT_CONNECT_FUNC(node_id)                                                      \
	static const struct gpio_rza2m_tint_config gpio_rza2m_tint_cfg_##node_id = {               \
		DEVICE_MMIO_ROM_INIT(DT_PARENT(DT_INST(0, renesas_rza2m_gpio_int))),               \
		.gpio_int_init = gpio_rza2m_tint_connect_func##node_id,                            \
	};                                                                                         \
	static struct gpio_rza2m_tint_data gpio_rza2m_tint_data_##node_id;                         \
	DEVICE_DT_DEFINE(node_id, gpio_rza2m_int_init, NULL, &gpio_rza2m_tint_data_##node_id,      \
			 &gpio_rza2m_tint_cfg_##node_id, POST_KERNEL,                              \
			 UTIL_DEC(CONFIG_GPIO_INIT_PRIORITY), NULL);

#define GPIO_RZA2M_PORT_INIT(inst)                                                                 \
	static const struct gpio_rza2m_port_config gpio_rza2m_cfg_##inst = {                       \
		.common =                                                                          \
			{                                                                          \
				.port_pin_mask = GPIO_PORT_PIN_MASK_FROM_DT_INST(inst),            \
			},                                                                         \
		.port = DT_INST_REG_ADDR(inst),                                                    \
		.ngpios = DT_INST_PROP(inst, ngpios),                                              \
		.int_dev = DEVICE_DT_GET_OR_NULL(DT_INST(0, renesas_rza2m_gpio_int)),              \
	};                                                                                         \
	static struct gpio_rza2m_port_data gpio_rza2m_data_##inst = {.mask_irq_en = 0};            \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(inst, gpio_rza2m_port_init, NULL, &gpio_rza2m_data_##inst,           \
			      &gpio_rza2m_cfg_##inst, POST_KERNEL, CONFIG_GPIO_INIT_PRIORITY,      \
			      &gpio_rza2m_driver_api);

DT_INST_FOREACH_STATUS_OKAY(GPIO_RZA2M_PORT_INIT)
DT_FOREACH_STATUS_OKAY(renesas_rza2m_gpio_int, GPIO_RZA2M_INT_INIT)
