/*
 * Copyright (c) 2020 Nuvoton Technology Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nuvoton_npcx_miwu

/**
 * @file
 * @brief Nuvoton NPCX MIWU driver
 *
 * The device Multi-Input Wake-Up Unit (MIWU) supports the Nuvoton embedded
 * controller (EC) to exit 'Sleep' or 'Deep Sleep' power state which allows chip
 * has better power consumption. Also, it provides signal conditioning such as
 * 'Level' and 'Edge' trigger type and grouping of external interrupt sources
 * of NVIC. The NPCX series has three identical MIWU modules: MIWU0, MIWU1,
 * MIWU2. Together, they support a total of over 140 internal and/or external
 * wake-up input (WUI) sources.
 *
 * This driver uses device tree files to present the relationship bewteen
 * MIWU and the other devices in different npcx series. For npcx7 series,
 * it include:
 *  1. npcxn-miwus-wui-map.dtsi: it presents relationship between wake-up inputs
 *     (WUI) and its source device such as gpio, timer, eSPI VWs and so on.
 *  2. npcx7-miwus-int-map.dtsi: it presents relationship between MIWU group
 *     and NVIC interrupt in npcx7. Please notice it isn't 1-to-1 mapping.
 *     For example, here is the mapping between miwu0's group a & d and IRQ7:
 *
 *     map_miwu0_groups: {
 *         parent = <&miwu0>;
 *         group_ad0: group_ad0_map {
 *             irq        = <7>;
 *             group_mask = <0x09>;
 *         };
 *         ...
 *     };
 *
 *     It will connect IRQ 7 and intc_miwu_isr0() with the argument, group_mask,
 *     by IRQ_CONNECT() during driver initialization function. With group_mask,
 *     0x09, the driver checks the pending bits of group a and group d in ISR.
 *     Then it will execute related callback functions if they have been
 *     registered properly.
 *
 * INCLUDE FILES: soc_miwu.h
 *
 */

#include <assert.h>
#include <device.h>
#include <kernel.h>
#include <soc.h>
#include <irq_nextlevel.h>
#include <drivers/gpio.h>

#include "soc_miwu.h"
#include "soc_gpio.h"

#include <logging/log.h>
LOG_MODULE_REGISTER(intc_miwu, LOG_LEVEL_ERR);

/* MIWU module instances forward declaration */
static const struct device *miwu_devs[];

/* Driver config */
struct intc_miwu_config {
	/* miwu controller base address */
	uintptr_t base;
	/* index of miwu controller */
	uint8_t index;
};

/* Callback functions list for GPIO wake-up inputs */
sys_slist_t cb_list_gpio;

/*
 * Callback functions list for the generic hardware modules  wake-up inputs
 * such as timer, uart, i2c, host interface and so on.
 */
sys_slist_t cb_list_generic;

/* Driver convenience defines */
#define DRV_CONFIG(dev) \
	((const struct intc_miwu_config *)(dev)->config)

BUILD_ASSERT(sizeof(struct miwu_io_callback) == sizeof(struct gpio_callback),
	"Size of struct miwu_io_callback must equal to struct gpio_callback");

BUILD_ASSERT(sizeof(struct miwu_io_params) == sizeof(gpio_port_pins_t),
	"Size of struct miwu_io_params must equal to struct gpio_port_pins_t");

/* MIWU local functions */
static void intc_miwu_dispatch_gpio_isr(uint8_t wui_table,
					uint8_t wui_group, uint8_t wui_bit)
{
	struct miwu_io_callback *cb, *tmp;

	SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&cb_list_gpio, cb, tmp, node) {
		/* Pending bit, group and table match the wui item in list */
		if (cb->params.wui.table == wui_table
			&& cb->params.wui.group == wui_group
			&& cb->params.wui.bit == wui_bit) {
			__ASSERT(cb->handler, "No GPIO callback handler!");
			/*
			 * Execute GPIO callback and the other callback might
			 * match the same wui item.
			 */
			cb->handler(soc_get_gpio_dev(cb->params.gpio_port),
					(struct gpio_callback *)cb,
					cb->params.pin_mask);
		}
	}
}

static void intc_miwu_dispatch_generic_isr(uint8_t wui_table,
					uint8_t wui_group, uint8_t wui_bit)
{
	struct miwu_dev_callback *cb, *tmp;

	SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&cb_list_generic, cb, tmp, node) {
		/* Pending bit, group and table match the wui item in list */
		if (cb->wui.table == wui_table
				&& cb->wui.group == wui_group
				&& cb->wui.bit == wui_bit) {
			__ASSERT(cb->handler, "No Generic callback handler!");
			/*
			 * Execute generic callback and the other callback might
			 * match the same wui item.
			 */
			cb->handler(cb->source, &cb->wui);
		}
	}
}

static void intc_miwu_isr_pri(int wui_table, int wui_group)
{
	int wui_bit;
	uint32_t base = DRV_CONFIG(miwu_devs[wui_table])->base;
	uint8_t mask = NPCX_WKPND(base, wui_group) & NPCX_WKEN(base, wui_group);

	/* Clear pending bits before dispatch ISR */
	if (mask)
		NPCX_WKPCL(base, wui_group) = mask;

	for (wui_bit = 0; wui_bit < 8; wui_bit++) {
		if (mask & BIT(wui_bit)) {
			LOG_DBG("miwu_isr %d %d %d!\n", wui_table,
							wui_group, wui_bit);
			/* Dispatch registed gpio and generic isrs */
			intc_miwu_dispatch_gpio_isr(wui_table,
							wui_group, wui_bit);
			intc_miwu_dispatch_generic_isr(wui_table,
							wui_group, wui_bit);
		}
	}
}

/* Soc specific MIWU functions */
void soc_miwu_irq_enable(const struct npcx_wui *wui)
{
	uint32_t base = DRV_CONFIG(miwu_devs[wui->table])->base;

	NPCX_WKEN(base, wui->group) |= BIT(wui->bit);
}

void soc_miwu_irq_disable(const struct npcx_wui *wui)
{
	uint32_t base = DRV_CONFIG(miwu_devs[wui->table])->base;

	NPCX_WKEN(base, wui->group) &= ~BIT(wui->bit);
}

unsigned int soc_miwu_irq_get_state(const struct npcx_wui *wui)
{
	uint32_t base = DRV_CONFIG(miwu_devs[wui->table])->base;

	if (IS_BIT_SET(NPCX_WKEN(base, wui->group), wui->bit))
		return 1;
	else
		return 0;
}

int soc_miwu_interrupt_configure(const struct npcx_wui *wui,
		enum miwu_int_mode mode, enum miwu_int_trig trig)
{
	uint32_t base = DRV_CONFIG(miwu_devs[wui->table])->base;
	uint8_t pmask = BIT(wui->bit);

	if (mode == NPCX_MIWU_MODE_DISABLED) {
		/* Clear MIWU enable bit */
		NPCX_WKEN(base, wui->group) &= ~pmask;
	} else {
		/* Handle interrupt for level trigger */
		if (mode == NPCX_MIWU_MODE_LEVEL) {
			/* Set detection mode to level */
			NPCX_WKMOD(base, wui->group) |= pmask;
			switch (trig) {
			/* Enable interrupting on level high */
			case NPCX_MIWU_TRIG_HIGH:
				NPCX_WKEDG(base, wui->group) &= ~pmask;
				break;
			/* Enable interrupting on level low */
			case NPCX_MIWU_TRIG_LOW:
				NPCX_WKEDG(base, wui->group) |= pmask;
				break;
			default:
				return -EINVAL;
			}
		/* Handle interrupt for edge trigger */
		} else {
			/* Set detection mode to edge */
			NPCX_WKMOD(base, wui->group) &= ~pmask;
			switch (trig) {
			/* Handle interrupting on falling edge */
			case NPCX_MIWU_TRIG_LOW:
				NPCX_WKAEDG(base, wui->group) &= ~pmask;
				NPCX_WKEDG(base, wui->group) |= pmask;
				break;
			/* Handle interrupting on rising edge */
			case NPCX_MIWU_TRIG_HIGH:
				NPCX_WKAEDG(base, wui->group) &= ~pmask;
				NPCX_WKEDG(base, wui->group) &= ~pmask;
				break;
			/* Handle interrupting on both edges */
			case NPCX_MIWU_TRIG_BOTH:
				/* Enable any edge */
				NPCX_WKAEDG(base, wui->group) |= pmask;
				break;
			default:
				return -EINVAL;
			}
		}

		/* Enable wake-up input sources */
		NPCX_WKINEN(base, wui->group) |= pmask;

		/*
		 * Clear pending bit since it might be set if WKINEN bit is
		 * changed.
		 */
		NPCX_WKPCL(base, wui->group) |= pmask;
	}

	return 0;
}

void soc_miwu_init_gpio_callback(struct miwu_io_callback *callback,
				const struct npcx_wui *io_wui, int port)
{
	/* Initialize WUI and GPIO settings in unused bits field */
	callback->params.wui.table = io_wui->table;
	callback->params.wui.group = io_wui->group;
	callback->params.wui.bit   = io_wui->bit;
	callback->params.gpio_port = port;
}

void soc_miwu_init_dev_callback(struct miwu_dev_callback *callback,
				const struct npcx_wui *dev_wui,
				miwu_dev_callback_handler_t handler,
				const struct device *source)
{
	/* Initialize WUI and input device settings */
	callback->wui.table = dev_wui->table;
	callback->wui.group = dev_wui->group;
	callback->wui.bit   = dev_wui->bit;
	callback->handler = handler;
	callback->source = source;
}

int soc_miwu_manage_gpio_callback(struct miwu_io_callback *cb, bool set)
{
	if (!sys_slist_is_empty(&cb_list_gpio)) {
		if (!sys_slist_find_and_remove(&cb_list_gpio, &cb->node)) {
			if (!set) {
				return -EINVAL;
			}
		}
	}

	if (set) {
		sys_slist_prepend(&cb_list_gpio, &cb->node);
	}

	return 0;
}

int soc_miwu_manage_dev_callback(struct miwu_dev_callback *cb, bool set)
{
	if (!sys_slist_is_empty(&cb_list_generic)) {
		if (!sys_slist_find_and_remove(&cb_list_generic, &cb->node)) {
			if (!set) {
				return -EINVAL;
			}
		}
	}

	if (set) {
		sys_slist_prepend(&cb_list_generic, &cb->node);
	}

	return 0;
}

/* MIWU driver registration */
#define NPCX_MIWU_ISR_FUNC(index) _CONCAT(intc_miwu_isr, index)
#define NPCX_MIWU_INIT_FUNC(inst) _CONCAT(intc_miwu_init, inst)
#define NPCX_MIWU_INIT_FUNC_DECL(inst) \
	static int intc_miwu_init##inst(struct device *dev)

/* MIWU ISR implementation */
#define NPCX_MIWU_ISR_FUNC_IMPL(inst)                                          \
	static void intc_miwu_isr##inst(void *arg)                             \
	{                                                                      \
		uint8_t grp_mask = (uint32_t)arg;                              \
		int group = 0;                                                 \
									       \
		/* Check all MIWU groups belong to the same irq */             \
		do {                                                           \
			if (grp_mask & 0x01)                                   \
				intc_miwu_isr_pri(inst, group);                \
			group++;                                               \
			grp_mask = grp_mask >> 1;                              \
									       \
		} while (grp_mask != 0);                                       \
	}

/* MIWU init function implementation */
#define NPCX_MIWU_INIT_FUNC_IMPL(inst)                                         \
	static int intc_miwu_init##inst(const struct device *dev)                    \
	{                                                                      \
		int i;                                                         \
		uint32_t base = DRV_CONFIG(dev)->base;                         \
									       \
		/* Clear all MIWUs' pending and enable bits of MIWU device */  \
		for (i = 0; i < NPCX_MIWU_GROUP_COUNT; i++) {                  \
			NPCX_WKPCL(base, i) = 0xFF;                            \
			NPCX_WKEN(base, i) = 0;                                \
		}                                                              \
									       \
		/* Config IRQ and MWIU group directly */                       \
		DT_FOREACH_CHILD(DT_NODE_FROM_MIWU_MAP(inst),                  \
			DT_MIWU_IRQ_CONNECT_IMPL_CHILD_FUNC)                   \
		return 0;                                                      \
	}                                                                      \

#define NPCX_MIWU_INIT(inst)                                                   \
	NPCX_MIWU_INIT_FUNC_DECL(inst);                                        \
									       \
	static const struct intc_miwu_config miwu_config_##inst = {	       \
		.base = DT_REG_ADDR(DT_NODELABEL(miwu##inst)),                 \
		.index = DT_PROP(DT_NODELABEL(miwu##inst), index),             \
	};                                                                     \
									       \
	DEVICE_AND_API_INIT(intc_miwu_##inst, DT_INST_LABEL(inst),             \
			    NPCX_MIWU_INIT_FUNC(inst),                         \
			    NULL, &miwu_config_##inst,                         \
			    PRE_KERNEL_1,                                      \
			    CONFIG_KERNEL_INIT_PRIORITY_OBJECTS, NULL);        \
									       \
	NPCX_MIWU_ISR_FUNC_IMPL(inst)                                          \
									       \
	NPCX_MIWU_INIT_FUNC_IMPL(inst)

DT_INST_FOREACH_STATUS_OKAY(NPCX_MIWU_INIT)

/* MIWU module instances */
#define NPCX_MIWU_DEV(inst) DEVICE_GET(intc_miwu_##inst),

static const struct device *miwu_devs[] = {
	DT_INST_FOREACH_STATUS_OKAY(NPCX_MIWU_DEV)
};

BUILD_ASSERT(ARRAY_SIZE(miwu_devs) == NPCX_MIWU_TABLE_COUNT,
		"Size of miwu_devs array must equal to NPCX_MIWU_TABLE_COUNT");
