/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright (c) 2024 Realtek Semiconductor Corporation, SIBG-SD7
 * Author: Lin Yu-Cheng <lin_yu_cheng@realtek.com>
 */

#define DT_DRV_COMPAT realtek_rts5912_gpio

#include <errno.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/irq.h>
#include "zephyr/drivers/gpio/gpio_utils.h"
#include <zephyr/logging/log.h>
#include <zephyr/dt-bindings/gpio/realtek-gpio.h>

#include <reg/reg_gpio.h>

LOG_MODULE_REGISTER(gpio_rts5912, CONFIG_GPIO_LOG_LEVEL);

#define RTS5912_GPIOA_REG_BASE ((GPIO_Type *)(DT_REG_ADDR(DT_NODELABEL(gpioa))))

struct gpio_rts5912_config {
	struct gpio_driver_config common;
	volatile uint32_t *reg_base;
	uint8_t num_pins;
};

struct gpio_rts5912_data {
	struct gpio_driver_data common;
	sys_slist_t callbacks;
};

static int pin_is_valid(const struct gpio_rts5912_config *config, gpio_pin_t pin)
{
	if (pin >= config->num_pins) {
		return -EINVAL;
	}

	return 0;
}

static int pin_output_high(const struct device *port, gpio_pin_t pin)
{
	const struct gpio_rts5912_config *config = port->config;
	volatile uint32_t *gcr = &config->reg_base[pin];

	int err = pin_is_valid(config, pin);

	if (err) {
		return err;
	}

	if (*gcr & GPIO_GCR_OUTMD_Msk) {
		/* Switch I/O mode to input mode when configuration is open-drain with output high
		 */
		*gcr = (*gcr & ~GPIO_GCR_DIR_Msk) | GPIO_GCR_OUTCTRL_Msk;
	} else {
		*gcr |= GPIO_GCR_OUTCTRL_Msk | GPIO_GCR_DIR_Msk;
	}

	return 0;
}

static int pin_output_low(const struct device *port, gpio_pin_t pin)
{
	const struct gpio_rts5912_config *config = port->config;
	volatile uint32_t *gcr = &config->reg_base[pin];

	int err = pin_is_valid(config, pin);

	if (err) {
		return err;
	}

	*gcr = (*gcr & ~GPIO_GCR_OUTCTRL_Msk) | GPIO_GCR_DIR_Msk;

	return 0;
}

static int gpio_rts5912_configuration(const struct device *port, gpio_pin_t pin, gpio_flags_t flags)
{
	const struct gpio_rts5912_config *config = port->config;
	volatile uint32_t *gcr = &config->reg_base[pin];
	uint32_t cfg_val = *gcr;

	int err = pin_is_valid(config, pin);

	if (err) {
		return err;
	}

	if (flags & GPIO_INPUT) {
		cfg_val &= ~GPIO_GCR_DIR_Msk;
		cfg_val &= ~GPIO_GCR_OUTCTRL_Msk;
		/* enable input detect */
		cfg_val |= GPIO_GCR_INDETEN_Msk;
	}

	if (flags & GPIO_DISCONNECTED) {
		cfg_val &= ~GPIO_GCR_DIR_Msk;
		cfg_val &= ~GPIO_GCR_OUTCTRL_Msk;
		/* disable input detect */
		cfg_val &= ~GPIO_GCR_INDETEN_Msk;
	}

	if (flags & GPIO_OPEN_DRAIN) {
		cfg_val |= GPIO_GCR_OUTMD_Msk;
	} else {
		cfg_val &= ~GPIO_GCR_OUTMD_Msk;
	}

	switch (flags & (GPIO_PULL_UP | GPIO_PULL_DOWN)) {
	case GPIO_PULL_UP:
		cfg_val &= ~GPIO_GCR_PULLDWEN_Msk;
		cfg_val |= GPIO_GCR_PULLUPEN_Msk;
		break;
	case GPIO_PULL_DOWN:
		cfg_val &= ~GPIO_GCR_PULLUPEN_Msk;
		cfg_val |= GPIO_GCR_PULLDWEN_Msk;
		break;
	default:
		break;
	}

	switch (flags & RTS5912_GPIO_VOLTAGE_MASK) {
	case RTS5912_GPIO_VOLTAGE_1V8:
		cfg_val |= GPIO_GCR_INVOLMD_Msk;
		break;
	case RTS5912_GPIO_VOLTAGE_DEFAULT:
	case RTS5912_GPIO_VOLTAGE_3V3:
		cfg_val &= ~GPIO_GCR_INVOLMD_Msk;
		break;
	case RTS5912_GPIO_VOLTAGE_5V0:
		return -ENOTSUP;
	default:
		break;
	}

	if (flags & RTS5912_GPIO_OUTDRV) {
		cfg_val |= GPIO_GCR_OUTDRV_Msk;
	} else {
		cfg_val &= ~GPIO_GCR_OUTDRV_Msk;
	}

	if (flags & RTS5912_GPIO_SLEWRATE) {
		cfg_val |= GPIO_GCR_SLEWRATE_Msk;
	} else {
		cfg_val &= ~GPIO_GCR_SLEWRATE_Msk;
	}

	if (flags & RTS5912_GPIO_SCHEN) {
		cfg_val |= GPIO_GCR_SCHEN_Msk;
	} else {
		cfg_val &= ~GPIO_GCR_SCHEN_Msk;
	}

	cfg_val &= ~GPIO_GCR_MFCTRL_Msk;
	switch (flags & RTS5912_GPIO_MFCTRL_MASK) {
	case RTS5912_GPIO_MFCTRL_0:
		cfg_val |= (0U << GPIO_GCR_MFCTRL_Pos);
		break;
	case RTS5912_GPIO_MFCTRL_1:
		cfg_val |= (1U << GPIO_GCR_MFCTRL_Pos);
		break;
	case RTS5912_GPIO_MFCTRL_2:
		cfg_val |= (2U << GPIO_GCR_MFCTRL_Pos);
		break;
	case RTS5912_GPIO_MFCTRL_3:
		cfg_val |= (3U << GPIO_GCR_MFCTRL_Pos);
		break;
	default:
		return -EINVAL;
	}

	*gcr = cfg_val;

	if (flags & GPIO_OUTPUT) {
		if (flags & GPIO_OUTPUT_INIT_HIGH) {
			pin_output_high(port, pin);
		} else if (flags & GPIO_OUTPUT_INIT_LOW) {
			pin_output_low(port, pin);
		}
	}

	return 0;
}

#ifdef CONFIG_GPIO_GET_CONFIG
static int gpio_rts5912_get_configuration(const struct device *port, gpio_pin_t pin,
					  gpio_flags_t *flags)
{
	const struct gpio_rts5912_config *config = port->config;
	volatile uint32_t *gcr = &config->reg_base[pin];
	gpio_flags_t cfg_flag = 0x0UL;

	int err = pin_is_valid(config, pin);

	if (err) {
		return err;
	}

	if (*gcr & GPIO_GCR_OUTCTRL_Msk) {
		cfg_flag |= GPIO_OUTPUT | GPIO_OUTPUT_INIT_HIGH;
	} else {
		if (*gcr & GPIO_GCR_DIR_Msk) {
			cfg_flag |= GPIO_OUTPUT | GPIO_OUTPUT_INIT_LOW;
		} else {
			cfg_flag |= GPIO_INPUT;
			if (*gcr & GPIO_GCR_INVOLMD_Msk) {
				cfg_flag |= RTS5912_GPIO_VOLTAGE_1V8;
			} else {
				cfg_flag |= RTS5912_GPIO_VOLTAGE_3V3;
			}
		}
	}

	if (*gcr & GPIO_GCR_OUTMD_Msk) {
		cfg_flag |= GPIO_OPEN_DRAIN;
	}

	if (*gcr & GPIO_GCR_PULLUPEN_Msk) {
		cfg_flag |= GPIO_PULL_UP;
	} else if (*gcr & GPIO_GCR_PULLDWEN_Msk) {
		cfg_flag |= GPIO_PULL_DOWN;
	}

	if (*gcr & GPIO_GCR_INDETEN_Msk) {
		cfg_flag |= RTS5912_GPIO_INDETEN;
	} else {
		cfg_flag &= ~RTS5912_GPIO_INDETEN;
	}

	if (*gcr & GPIO_GCR_OUTDRV_Msk) {
		cfg_flag |= RTS5912_GPIO_OUTDRV;
	} else {
		cfg_flag &= ~RTS5912_GPIO_OUTDRV;
	}

	if (*gcr & GPIO_GCR_SLEWRATE_Msk) {
		cfg_flag |= RTS5912_GPIO_SLEWRATE;
	} else {
		cfg_flag &= ~RTS5912_GPIO_SLEWRATE;
	}

	if (*gcr & GPIO_GCR_SCHEN_Msk) {
		cfg_flag |= RTS5912_GPIO_SCHEN;
	} else {
		cfg_flag &= ~RTS5912_GPIO_SCHEN;
	}

	switch ((*gcr & GPIO_GCR_MFCTRL_Msk) >> GPIO_GCR_MFCTRL_Pos) {
	case 0:
		cfg_flag |= RTS5912_GPIO_MFCTRL_0;
		break;
	case 1:
		cfg_flag |= RTS5912_GPIO_MFCTRL_1;
		break;
	case 2:
		cfg_flag |= RTS5912_GPIO_MFCTRL_2;
		break;
	case 3:
		cfg_flag |= RTS5912_GPIO_MFCTRL_3;
		break;
	default:
		cfg_flag |= RTS5912_GPIO_MFCTRL_0;
		break;
	}

	if (*gcr & GPIO_GCR_INTEN_Msk) {
		switch (*gcr & GPIO_GCR_INTCTRL_Msk) {
		case GPIO_GCR_INTCTRL_TRIG_EDGE_HIGH:
			cfg_flag |= GPIO_INT_EDGE_RISING;
			break;
		case GPIO_GCR_INTCTRL_TRIG_EDGE_LOW:
			cfg_flag |= GPIO_INT_EDGE_FALLING;
			break;
		case GPIO_GCR_INTCTRL_TRIG_EDGE_BOTH:
			cfg_flag |= GPIO_INT_EDGE_BOTH;
			break;
		case GPIO_GCR_INTCTRL_TRIG_LEVEL_LOW:
			cfg_flag |= GPIO_INT_LEVEL_LOW;
			break;
		case GPIO_GCR_INTCTRL_TRIG_LEVEL_HIGH:
			cfg_flag |= GPIO_INT_LEVEL_HIGH;
			break;
		default:
			cfg_flag |= GPIO_INT_LEVEL_LOW;
			break;
		}
	} else {
		cfg_flag |= GPIO_INT_DISABLE;
	}

	*flags = cfg_flag;

	return 0;
}
#endif

static int gpio_rts5912_port_get_raw(const struct device *port, gpio_port_value_t *value)
{
	const struct gpio_rts5912_config *config = port->config;
	gpio_port_value_t ret_val = 0;
	uint16_t mask = 0x1U;

	for (gpio_pin_t i = 0; i < config->num_pins; i++) {
		if (config->reg_base[i] & GPIO_GCR_PINSTS_Msk) {
			ret_val |= (gpio_port_value_t)mask;
		}
		mask <<= 1;
	}

	*value = ret_val;

	return 0;
}

static int gpio_rts5912_port_set_masked_raw(const struct device *port, gpio_port_pins_t mask,
					    gpio_port_value_t value)
{
	const struct gpio_rts5912_config *config = port->config;
	uint32_t pin;

	mask &= 0x0000FFFF;
	for (; mask; mask &= ~BIT(pin)) {
		pin = find_lsb_set(mask) - 1;
		if (pin >= config->num_pins) {
			break;
		}

		if (value & BIT(pin)) {
			pin_output_high(port, pin);
		} else {
			pin_output_low(port, pin);
		}
	}

	return 0;
}

static int gpio_rts5912_port_set_bits_raw(const struct device *port, gpio_port_pins_t pins)
{
	const struct gpio_rts5912_config *config = port->config;
	volatile uint32_t *gcr = config->reg_base;
	uint32_t pin = 0;

	pins &= 0x0000FFFF;
	gpio_port_pins_t sel_pin = 1;

	for (; pins;) {
		if (pins & sel_pin) {
			pin_output_high(port, pin);
		}
		pins &= ~sel_pin;
		sel_pin <<= 1;
		gcr++;
		pin++;
	}

	return 0;
}

static int gpio_rts5912_port_clear_bits_raw(const struct device *port, gpio_port_pins_t pins)
{
	const struct gpio_rts5912_config *config = port->config;
	volatile uint32_t *gcr = config->reg_base;
	uint32_t pin = 0;

	pins &= 0x0000FFFF;
	gpio_port_pins_t sel_pin = 1;

	for (; pins;) {
		if (pins & sel_pin) {
			pin_output_low(port, pin);
		}
		pins &= ~sel_pin;
		sel_pin <<= 1;
		gcr++;
		pin++;
	}

	return 0;
}

static int gpio_rts5912_port_toggle_bits(const struct device *port, gpio_port_pins_t pins)
{
	const struct gpio_rts5912_config *config = port->config;
	volatile uint32_t *gcr = config->reg_base;
	uint32_t pin = 0;

	pins &= 0x0000FFFF;
	gpio_port_pins_t sel_pin = 0x1UL;

	for (; pins;) {
		if (pins & sel_pin) {
			if (*gcr & GPIO_GCR_OUTCTRL_Msk) {
				pin_output_low(port, pin);
			} else {
				pin_output_high(port, pin);
			}
		}

		pins &= ~sel_pin;
		sel_pin <<= 1;
		gcr++;
		pin++;
	}

	return 0;
}

static gpio_pin_t gpio_rts5912_get_intr_pin(volatile uint32_t *reg_base)
{
	gpio_pin_t pin = 0U;

	for (; pin < 16; pin++) {
		if (reg_base[pin] & GPIO_GCR_INTSTS_Msk) {
			break;
		}
	}

	return pin;
}

static void gpio_rts5912_isr(const void *arg)
{
	const struct device *port = arg;
	const struct gpio_rts5912_config *config = port->config;
	struct gpio_rts5912_data *data = port->data;
	volatile uint32_t *gcr = config->reg_base;
	unsigned int key = irq_lock();
	gpio_pin_t pin = gpio_rts5912_get_intr_pin(gcr);

	if (gcr[pin] & GPIO_GCR_INTSTS_Msk) {
		gcr[pin] |= GPIO_GCR_INTSTS_Msk;

		gpio_fire_callbacks(&data->callbacks, port, BIT(pin));
	}
	irq_unlock(key);
}

static int gpio_rts5912_intr_config(const struct device *port, gpio_pin_t pin,
				    enum gpio_int_mode mode, enum gpio_int_trig trig)
{
	const struct gpio_rts5912_config *config = port->config;
	volatile uint32_t *gcr = &config->reg_base[pin];
	uint32_t cfg_val = *gcr;
	uint32_t pin_index =
		DT_IRQ_BY_IDX(DT_NODELABEL(gpioa), 0, irq) +
		((uint32_t)(&config->reg_base[pin]) - (uint32_t)(RTS5912_GPIOA_REG_BASE)) / 4;

	int err = pin_is_valid(config, pin);

	if (err) {
		return err;
	}

	switch (mode) {
	case GPIO_INT_MODE_DISABLED:
		cfg_val &= ~GPIO_GCR_INTEN_Msk;
		irq_disable(pin_index);
		*gcr = cfg_val;
		return 0;
	case GPIO_INT_MODE_LEVEL:
		switch (trig) {
		case GPIO_INT_TRIG_LOW:
			cfg_val &= ~GPIO_GCR_INTCTRL_Msk;
			cfg_val |= GPIO_GCR_INTCTRL_TRIG_LEVEL_LOW;
			break;
		case GPIO_INT_TRIG_HIGH:
			cfg_val &= ~GPIO_GCR_INTCTRL_Msk;
			cfg_val |= GPIO_GCR_INTCTRL_TRIG_LEVEL_HIGH;
			break;
		default:
			return -EINVAL;
		}
		break;
	case GPIO_INT_MODE_EDGE:
		switch (trig) {
		case GPIO_INT_TRIG_LOW:
			cfg_val &= ~GPIO_GCR_INTCTRL_Msk;
			cfg_val |= GPIO_GCR_INTCTRL_TRIG_EDGE_LOW;
			break;
		case GPIO_INT_TRIG_HIGH:
			cfg_val &= ~GPIO_GCR_INTCTRL_Msk;
			cfg_val |= GPIO_GCR_INTCTRL_TRIG_EDGE_HIGH;
			break;
		case GPIO_INT_TRIG_BOTH:
			cfg_val &= ~GPIO_GCR_INTCTRL_Msk;
			cfg_val |= GPIO_GCR_INTCTRL_TRIG_EDGE_BOTH;
			break;
		default:
			return -EINVAL;
		}
		break;
	default:
		return -EINVAL;
	}

	/* enable interrupt */
	cfg_val |= GPIO_GCR_INTEN_Msk;
	/* set value to GPIO register */
	*gcr = cfg_val;

	irq_enable(pin_index);

	return 0;
}

static int gpio_rts5912_manage_cb(const struct device *port, struct gpio_callback *cb, bool set)
{
	struct gpio_rts5912_data *data = port->data;

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

static DEVICE_API(gpio, gpio_rts5912_driver_api) = {
	.pin_configure = gpio_rts5912_configuration,
#ifdef CONFIG_GPIO_GET_CONFIG
	.pin_get_config = gpio_rts5912_get_configuration,
#endif
	.port_get_raw = gpio_rts5912_port_get_raw,
	.port_set_masked_raw = gpio_rts5912_port_set_masked_raw,
	.port_set_bits_raw = gpio_rts5912_port_set_bits_raw,
	.port_clear_bits_raw = gpio_rts5912_port_clear_bits_raw,
	.port_toggle_bits = gpio_rts5912_port_toggle_bits,
	.pin_interrupt_configure = gpio_rts5912_intr_config,
	.manage_callback = gpio_rts5912_manage_cb,
};

#ifdef CONFIG_GEN_ISR_TABLES
#define RTS5912_GPIO_DTNAMIC_IRQ(id)                                                               \
	for (int i = 0; i < 16 && (DT_INST_IRQ_BY_IDX(id, 0, irq) + i) < 132; i++) {               \
		irq_connect_dynamic((DT_INST_IRQ_BY_IDX(id, 0, irq) + i),                          \
				    DT_INST_IRQ(id, priority), gpio_rts5912_isr,                   \
				    DEVICE_DT_INST_GET(id), 0U);                                   \
	}
#else
#define RTS5912_GPIO_DTNAMIC_IRQ(id)                                                               \
	IRQ_CONNECT(DT_INST_IRQN(id), DT_INST_IRQ(id, priority), gpio_rts5912_isr,                 \
		    DEVICE_DT_INST_GET(id), 0U);
#endif

#define GPIO_RTS5912_INIT(id)                                                                      \
	static int gpio_rts5912_init_##id(const struct device *dev)                                \
	{                                                                                          \
		if (!(DT_INST_IRQ_HAS_CELL(id, irq))) {                                            \
			return 0;                                                                  \
		}                                                                                  \
                                                                                                   \
		RTS5912_GPIO_DTNAMIC_IRQ(id)                                                       \
                                                                                                   \
		return 0;                                                                          \
	}                                                                                          \
                                                                                                   \
	static struct gpio_rts5912_data gpio_rts5912_data_##id;                                    \
                                                                                                   \
	static const struct gpio_rts5912_config gpio_rts5912_config_##id = {                       \
		.common = {.port_pin_mask = GPIO_PORT_PIN_MASK_FROM_DT_INST(id)},                  \
		.reg_base = (volatile uint32_t *)DT_INST_REG_ADDR(id),                             \
		.num_pins = DT_INST_PROP(id, ngpios),                                              \
	};                                                                                         \
	DEVICE_DT_INST_DEFINE(id, gpio_rts5912_init_##id, NULL, &gpio_rts5912_data_##id,           \
			      &gpio_rts5912_config_##id, POST_KERNEL, CONFIG_GPIO_INIT_PRIORITY,   \
			      &gpio_rts5912_driver_api);

DT_INST_FOREACH_STATUS_OKAY(GPIO_RTS5912_INIT)
