/*
 * Copyright (c) 2021 Teslabs Engineering S.L.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/init.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/gd32.h>
#include <zephyr/drivers/pinctrl.h>

#include <gd32_gpio.h>

/** AFIO DT node */
#define AFIO_NODE DT_NODELABEL(afio)

/** GPIO mode: input floating (CTL bits) */
#define GPIO_MODE_INP_FLOAT 0x4U
/** GPIO mode: input with pull-up/down (CTL bits) */
#define GPIO_MODE_INP_PUPD 0x8U
/** GPIO mode: output push-pull (CTL bits) */
#define GPIO_MODE_ALT_PP 0x8U
/** GPIO mode: output open-drain (CTL bits) */
#define GPIO_MODE_ALT_OD 0xCU

/** Utility macro that expands to the GPIO port address if it exists */
#define GD32_PORT_ADDR_OR_NONE(nodelabel)				       \
	COND_CODE_1(DT_NODE_EXISTS(DT_NODELABEL(nodelabel)),		       \
		   (DT_REG_ADDR(DT_NODELABEL(nodelabel)),), ())

/** Utility macro that expands to the GPIO clock id if it exists */
#define GD32_PORT_CLOCK_ID_OR_NONE(nodelabel)				       \
	COND_CODE_1(DT_NODE_EXISTS(DT_NODELABEL(nodelabel)),		       \
		   (DT_CLOCKS_CELL(DT_NODELABEL(nodelabel), id),), ())

/** GD32 port addresses */
static const uint32_t gd32_port_addrs[] = {
	GD32_PORT_ADDR_OR_NONE(gpioa)
	GD32_PORT_ADDR_OR_NONE(gpiob)
	GD32_PORT_ADDR_OR_NONE(gpioc)
	GD32_PORT_ADDR_OR_NONE(gpiod)
	GD32_PORT_ADDR_OR_NONE(gpioe)
	GD32_PORT_ADDR_OR_NONE(gpiof)
	GD32_PORT_ADDR_OR_NONE(gpiog)
};

/** GD32 port clock identifiers */
static const uint16_t gd32_port_clkids[] = {
	GD32_PORT_CLOCK_ID_OR_NONE(gpioa)
	GD32_PORT_CLOCK_ID_OR_NONE(gpiob)
	GD32_PORT_CLOCK_ID_OR_NONE(gpioc)
	GD32_PORT_CLOCK_ID_OR_NONE(gpiod)
	GD32_PORT_CLOCK_ID_OR_NONE(gpioe)
	GD32_PORT_CLOCK_ID_OR_NONE(gpiof)
	GD32_PORT_CLOCK_ID_OR_NONE(gpiog)
};

/**
 * @brief Initialize AFIO
 *
 * This function enables AFIO clock and configures the I/O compensation if
 * available and enabled in Devicetree.
 *
 * @retval 0 Always
 */
static int afio_init(void)
{
	uint16_t clkid = DT_CLOCKS_CELL(AFIO_NODE, id);


	(void)clock_control_on(GD32_CLOCK_CONTROLLER,
			       (clock_control_subsys_t)&clkid);

#ifdef AFIO_CPSCTL
	if (DT_PROP(AFIO_NODE, enable_cps)) {
		gpio_compensation_config(GPIO_COMPENSATION_ENABLE);
		while (gpio_compensation_flag_get() == RESET)
			;
	}
#endif /* AFIO_CPSCTL */

	return 0;
}

SYS_INIT(afio_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);

/**
 * @brief Helper function to configure the SPD register if available.
 *
 * @param port GPIO port address.
 * @param pin_bit GPIO pin, set as bit position.
 * @param speed GPIO speed.
 *
 * @return Value of the mode register (speed) that should be set later.
 */
static inline uint8_t configure_spd(uint32_t port, uint32_t pin_bit,
				    uint8_t speed)
{
	switch (speed) {
	case GD32_OSPEED_MAX:
#ifdef GPIOx_SPD
		GPIOx_SPD(port) |= pin_bit;
#endif /* GPIOx_SPD */
		return speed;
	default:
#ifdef GPIOx_SPD
		GPIOx_SPD(port) &= ~pin_bit;
#endif /* GPIOx_SPD */
		return speed + 1U;
	}
}

/**
 * @brief Configure a pin.
 *
 * @param pin The pin to configure.
 */
static void configure_pin(pinctrl_soc_pin_t pin)
{
	uint8_t port_idx, mode, pin_num;
	uint32_t port, pin_bit, reg_val;
	volatile uint32_t *reg;
	uint16_t clkid;

	port_idx = GD32_PORT_GET(pin);
	__ASSERT_NO_MSG(port_idx < ARRAY_SIZE(gd32_port_addrs));

	clkid = gd32_port_clkids[port_idx];
	port = gd32_port_addrs[port_idx];
	pin_num = GD32_PIN_GET(pin);
	pin_bit = BIT(pin_num);
	mode = GD32_MODE_GET(pin);

	if (pin_num < 8U) {
		reg = &GPIO_CTL0(port);
	} else {
		reg = &GPIO_CTL1(port);
		pin_num -= 8U;
	}

	(void)clock_control_on(GD32_CLOCK_CONTROLLER,
			       (clock_control_subsys_t)&clkid);

	reg_val = *reg;
	reg_val &= ~GPIO_MODE_MASK(pin_num);

	if (mode == GD32_MODE_ALTERNATE) {
		uint8_t new_mode;

		new_mode = configure_spd(port, pin_bit, GD32_OSPEED_GET(pin));

		if (GD32_OTYPE_GET(pin) == GD32_OTYPE_PP) {
			new_mode |= GPIO_MODE_ALT_PP;
		} else {
			new_mode |= GPIO_MODE_ALT_OD;
		}

		reg_val |= GPIO_MODE_SET(pin_num, new_mode);
	} else if (mode == GD32_MODE_GPIO_IN) {
		uint8_t pupd = GD32_PUPD_GET(pin);

		if (pupd == GD32_PUPD_NONE) {
			reg_val |= GPIO_MODE_SET(pin_num, GPIO_MODE_INP_FLOAT);
		} else {
			reg_val |= GPIO_MODE_SET(pin_num, GPIO_MODE_INP_PUPD);

			if (pupd == GD32_PUPD_PULLDOWN) {
				GPIO_BC(port) = pin_bit;
			} else if (pupd == GD32_PUPD_PULLUP) {
				GPIO_BOP(port) = pin_bit;
			}
		}
	}

	*reg = reg_val;
}

/**
 * @brief Configure remap.
 *
 * @param remap Remap bit field as encoded by #GD32_REMAP.
 */
static void configure_remap(uint16_t remap)
{
	uint8_t pos;
	uint32_t reg_val;
	volatile uint32_t *reg;

	/* not remappable */
	if (remap == GD32_NORMP) {
		return;
	}

	if (GD32_REMAP_REG_GET(remap) == 0U) {
		reg = &AFIO_PCF0;
	} else {
		reg = &AFIO_PCF1;
	}

	pos = GD32_REMAP_POS_GET(remap);

	reg_val = *reg;
	reg_val &= ~(GD32_REMAP_MSK_GET(remap) << pos);
	reg_val |= GD32_REMAP_VAL_GET(remap) << pos;
	*reg = reg_val;
}

int pinctrl_configure_pins(const pinctrl_soc_pin_t *pins, uint8_t pin_cnt,
			   uintptr_t reg)
{
	ARG_UNUSED(reg);

	if (pin_cnt == 0U) {
		return -EINVAL;
	}

	/* same remap is encoded in all pins, so just pick the first */
	configure_remap(GD32_REMAP_GET(pins[0]));

	/* configure all pins */
	for (uint8_t i = 0U; i < pin_cnt; i++) {
		configure_pin(pins[i]);
	}

	return 0;
}
