/*
 * Copyright (c) 2024 Texas Instruments Incorporated
 * Copyright (c) 2024 BayLibre, SAS
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT ti_cc23x0_gpio

#include <zephyr/types.h>
#include <zephyr/device.h>
#include <zephyr/irq.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/gpio/gpio_utils.h>
#include <zephyr/pm/device.h>

#include <driverlib/clkctl.h>
#include <driverlib/gpio.h>
#include <inc/hw_ioc.h>

#define IOC_ADDR(index)       (IOC_BASE + IOC_O_IOC0 + (sizeof(uint32_t) * (index)))

struct gpio_cc23x0_config {
	/* gpio_driver_config needs to be first */
	struct gpio_driver_config common;
};

struct gpio_cc23x0_data {
	/* gpio_driver_data needs to be first */
	struct gpio_driver_data common;
	sys_slist_t callbacks;
};

static void set_pin_mask_non_atomic(uint8_t index, uint32_t registerBaseAddress)
{
	GPIOSetConfigDio(GPIO_BASE + registerBaseAddress, BIT(index));
}

static int gpio_cc23x0_config(const struct device *port, gpio_pin_t pin, gpio_flags_t flags)
{
	uint32_t config = 0;
	uint32_t iocfg_reg = IOC_ADDR(pin);
	gpio_flags_t direction = flags & GPIO_DIR_MASK;

	if (flags & GPIO_PULL_UP) {
		config |= IOC_IOC0_PULLCTL_PULL_UP;
	} else if (flags & GPIO_PULL_DOWN) {
		config |= IOC_IOC0_PULLCTL_PULL_DOWN;
	} else {
		config |= IOC_IOC0_PULLCTL_PULL_DIS;
	}

	if (flags & GPIO_INT_WAKEUP) {
		config |= IOC_IOC0_WUENSB;
	}

	if (!(flags & GPIO_SINGLE_ENDED)) {
		config |= IOC_IOC0_IOMODE_NORMAL;
	} else {
		if (flags & GPIO_LINE_OPEN_DRAIN) {
			config |= IOC_IOC0_IOMODE_OPEND;
		} else {
			config |= IOC_IOC0_IOMODE_OPENS;
		}
	}
	if (direction & GPIO_INPUT) {
		config |= IOC_IOC0_INPEN_EN | IOC_IOC0_HYSTEN_EN;
	}

	GPIOSetConfigDio(iocfg_reg, config);

	if (flags & GPIO_OUTPUT) {
		if (flags & GPIO_OUTPUT_INIT_HIGH) {
			GPIOSetDio(pin);
		} else if (flags & GPIO_OUTPUT_INIT_LOW) {
			GPIOClearDio(pin);
		}
		GPIOSetOutputEnableDio(pin, GPIO_OUTPUT_ENABLE);
	} else {
		GPIOSetOutputEnableDio(pin, GPIO_OUTPUT_DISABLE);
	}
	return 0;
}

#ifdef CONFIG_GPIO_GET_CONFIG
static int gpio_cc23x0_get_config(const struct device *port, gpio_pin_t pin, gpio_flags_t *flags)
{
	uint32_t out_flag = 0;
	uint32_t iocfg_reg = IOC_ADDR(pin);
	uint32_t config = GPIOGetConfigDio(iocfg_reg);

	/* GPIO input/output configuration flags */
	if (config & IOC_IOC0_INPEN_EN) {
		out_flag |= GPIO_INPUT;
	}

	if (GPIOGetOutputEnableDio(pin)) {
		out_flag |= GPIO_OUTPUT;

		if (GPIOReadDio(pin)) {
			out_flag |= GPIO_OUTPUT_INIT_HIGH;
		} else {
			/* This is the default value. If not explicitly set,
			 * the returned config will not be symmetric
			 */
			out_flag |= GPIO_OUTPUT_INIT_LOW;
		}
	}

	/* GPIO interrupt configuration flags */
	if ((config & IOC_IOC0_EDGEDET_M) != IOC_IOC0_EDGEDET_EDGE_DIS) {
		if (config & IOC_IOC0_EDGEDET_EDGE_POS) {
			out_flag |= GPIO_INT_EDGE_RISING;
		}

		if (config & IOC_IOC0_EDGEDET_EDGE_NEG) {
			out_flag |= GPIO_INT_EDGE_FALLING;
		}
	} else {
		/* This is the default value. If not explicitly set,
		 * the returned config will not be symmetric
		 */
		out_flag |= GPIO_INT_DISABLE;
	}

	/* GPIO pin drive flags */
	if (config & IOC_IOC0_IOMODE_OPENS) {
		out_flag |= GPIO_OPEN_SOURCE;
	}

	if (config & IOC_IOC0_IOMODE_OPEND) {
		out_flag |= IOC_IOC0_IOMODE_OPEND;
	}

	if (config & IOC_IOC0_PULLCTL_PULL_UP) {
		out_flag |= GPIO_PULL_UP;
	}

	if (config & IOC_IOC0_PULLCTL_PULL_DOWN) {
		out_flag |= GPIO_PULL_DOWN;
	}

	*flags = out_flag;

	return 0;
}
#endif

static int gpio_cc23x0_port_get_raw(const struct device *port, uint32_t *value)
{
	*value = GPIOReadMultiDio(GPIO_DIO_ALL_MASK);

	return 0;
}

static int gpio_cc23x0_port_set_masked_raw(const struct device *port, uint32_t mask, uint32_t value)
{
	GPIOWriteMultiDio(mask, value);

	return 0;
}

static int gpio_cc23x0_port_set_bits_raw(const struct device *port, uint32_t mask)
{
	GPIOSetMultiDio(mask);

	return 0;
}

static int gpio_cc23x0_port_clear_bits_raw(const struct device *port, uint32_t mask)
{
	GPIOClearMultiDio(mask);

	return 0;
}

static int gpio_cc23x0_port_toggle_bits(const struct device *port, uint32_t mask)
{
	GPIOToggleMultiDio(mask);

	return 0;
}

static int gpio_cc23x0xx_pin_interrupt_configure(const struct device *port, gpio_pin_t pin,
						 enum gpio_int_mode mode, enum gpio_int_trig trig)
{
	if (mode == GPIO_INT_MODE_LEVEL) {
		return -ENOTSUP;
	}

	uint32_t config = GPIOGetConfigDio(IOC_ADDR(pin)) & ~IOC_IOC0_EDGEDET_M;

	if (mode == GPIO_INT_MODE_DISABLED) {
		config |= IOC_IOC1_EDGEDET_EDGE_DIS;

		GPIOSetConfigDio(IOC_ADDR(pin), config);

		/* Disable interrupt mask */
		set_pin_mask_non_atomic(pin, GPIO_O_IMCLR);

	} else if (mode == GPIO_INT_MODE_EDGE) {
		switch (trig) {
		case GPIO_INT_TRIG_LOW:
			config |= IOC_IOC1_EDGEDET_EDGE_NEG;
			break;
		case GPIO_INT_TRIG_HIGH:
			config |= IOC_IOC1_EDGEDET_EDGE_POS;
			break;
		case GPIO_INT_TRIG_BOTH:
			config |= IOC_IOC1_EDGEDET_EDGE_BOTH;
			break;
		default:
			return -ENOTSUP;
		}

		GPIOSetConfigDio(IOC_ADDR(pin), config);

		/* Enable interrupt mask */
		set_pin_mask_non_atomic(pin, GPIO_O_ICLR);
		set_pin_mask_non_atomic(pin, GPIO_O_IMSET);
	}

	return 0;
}

static int gpio_cc23x0_manage_callback(const struct device *port, struct gpio_callback *callback,
				       bool set)
{
	struct gpio_cc23x0_data *data = port->data;

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

static uint32_t gpio_cc23x0_get_pending_int(const struct device *dev)
{
	return GPIOGetEventMultiDio(GPIO_DIO_ALL_MASK);
}

static void gpio_cc23x0_isr(const struct device *dev)
{
	struct gpio_cc23x0_data *data = dev->data;

	uint32_t status = GPIOGetEventMultiDio(GPIO_DIO_ALL_MASK);

	GPIOClearEventMultiDio(status);

	gpio_fire_callbacks(&data->callbacks, dev, status);
}

#ifdef CONFIG_PM_DEVICE
static int gpio_cc23x0_pm_action(const struct device *dev, enum pm_device_action action)
{
	switch (action) {
	case PM_DEVICE_ACTION_RESUME:
		CLKCTLEnable(CLKCTL_BASE, CLKCTL_GPIO);
		break;
	case PM_DEVICE_ACTION_SUSPEND:
		CLKCTLDisable(CLKCTL_BASE, CLKCTL_GPIO);
		break;
	default:
		return -ENOTSUP;
	}

	return 0;
}
#endif /* CONFIG_PM_DEVICE */

static int gpio_cc23x0_init(const struct device *dev)
{
	/* Enable GPIO domain clock */
	CLKCTLEnable(CLKCTL_BASE, CLKCTL_GPIO);

	/* Enable IRQ */
	IRQ_CONNECT(DT_INST_IRQN(0), DT_INST_IRQ(0, priority), gpio_cc23x0_isr,
		    DEVICE_DT_INST_GET(0), 0);

	irq_enable(DT_INST_IRQN(0));

	return 0;
}

static DEVICE_API(gpio, gpio_cc23x0_driver_api) = {
	.pin_configure = gpio_cc23x0_config,
#ifdef CONFIG_GPIO_GET_CONFIG
	.pin_get_config = gpio_cc23x0_get_config,
#endif
	.port_get_raw = gpio_cc23x0_port_get_raw,
	.port_set_masked_raw = gpio_cc23x0_port_set_masked_raw,
	.port_set_bits_raw = gpio_cc23x0_port_set_bits_raw,
	.port_clear_bits_raw = gpio_cc23x0_port_clear_bits_raw,
	.port_toggle_bits = gpio_cc23x0_port_toggle_bits,
	.pin_interrupt_configure = gpio_cc23x0xx_pin_interrupt_configure,
	.manage_callback = gpio_cc23x0_manage_callback,
	.get_pending_int = gpio_cc23x0_get_pending_int,
};

static const struct gpio_cc23x0_config gpio_cc23x0_config_0 = {
	.common = {/* Read ngpios from DT */
		   .port_pin_mask = GPIO_PORT_PIN_MASK_FROM_DT_INST(0)}};

static struct gpio_cc23x0_data gpio_cc23x0_data_0;

PM_DEVICE_DT_DEFINE(0, gpio_cc23x0_pm_action);

DEVICE_DT_INST_DEFINE(0, gpio_cc23x0_init, PM_DEVICE_DT_GET(0), &gpio_cc23x0_data_0,
		      &gpio_cc23x0_config_0, PRE_KERNEL_1,
		      CONFIG_GPIO_INIT_PRIORITY, &gpio_cc23x0_driver_api);
