/*
 * Copyright (c) 2022 Nordic Semiconductor ASA
 * SPDX-License-Identifier: Apache-2.0
 */

#define DT_DRV_COMPAT nordic_npm6001_gpio

#include <errno.h>

#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/gpio/gpio_utils.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/dt-bindings/gpio/nordic-npm6001-gpio.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util_macro.h>
#include <zephyr/toolchain.h>

/* nPM6001 GPIO related registers */
#define NPM6001_GPIOOUTSET 0x69U
#define NPM6001_GPIOOUTCLR 0x6AU
#define NPM6001_GPIOIN	   0x6BU
#define NPM6001_GPIO0CONF  0x6CU
#define NPM6001_GPIO1CONF  0x6DU
#define NPM6001_GPIO2CONF  0x6EU

/* GPIO(0|1|2)CONF fields */
#define NPM6001_CONF_DIRECTION_OUTPUT BIT(0)
#define NPM6001_CONF_INPUT_ENABLED    BIT(1)
#define NPM6001_CONF_PULLDOWN_ENABLED BIT(2)
#define NPM6001_CONF_DRIVE_HIGH	      BIT(5)
#define NPM6001_CONF_SENSE_CMOS	      BIT(6)

#define NPM6001_PIN_MAX 2U
#define NPM6001_PIN_MSK 0x7U

struct gpio_npm6001_config {
	struct gpio_driver_config common;
	struct i2c_dt_spec bus;
};

struct gpio_npm6001_data {
	struct gpio_driver_data common;
};

static int gpio_npm6001_port_get_raw(const struct device *dev, uint32_t *value)
{
	const struct gpio_npm6001_config *config = dev->config;
	uint8_t reg = NPM6001_GPIOIN;
	uint8_t val;
	int ret;

	if (k_is_in_isr()) {
		return -EWOULDBLOCK;
	}

	ret = i2c_write_read_dt(&config->bus, &reg, sizeof(reg), &val,
				sizeof(val));
	if (ret < 0) {
		return ret;
	}

	*value = val;

	return 0;
}

static int gpio_npm6001_port_set_bits_raw(const struct device *dev,
					  gpio_port_pins_t pins)
{
	const struct gpio_npm6001_config *config = dev->config;
	uint8_t buf[2] = {NPM6001_GPIOOUTSET, (uint8_t)pins & NPM6001_PIN_MSK};

	if (k_is_in_isr()) {
		return -EWOULDBLOCK;
	}

	return i2c_write_dt(&config->bus, buf, sizeof(buf));
}

static int gpio_npm6001_port_clear_bits_raw(const struct device *dev,
					    gpio_port_pins_t pins)
{
	const struct gpio_npm6001_config *config = dev->config;
	uint8_t buf[2] = {NPM6001_GPIOOUTCLR, (uint8_t)pins & NPM6001_PIN_MSK};

	if (k_is_in_isr()) {
		return -EWOULDBLOCK;
	}

	return i2c_write_dt(&config->bus, buf, sizeof(buf));
}

static inline int gpio_npm6001_configure(const struct device *dev,
					 gpio_pin_t pin, gpio_flags_t flags)
{
	const struct gpio_npm6001_config *config = dev->config;
	uint8_t buf[2] = {NPM6001_GPIO0CONF, 0U};

	if (k_is_in_isr()) {
		return -EWOULDBLOCK;
	}

	if (pin > NPM6001_PIN_MAX) {
		return -EINVAL;
	}

	/* select GPIO0CONF/GPIO1CONF/GPIO2CONF */
	buf[0] += pin;

	if ((flags & GPIO_OUTPUT) != 0U) {
		/* input buffer enabled to allow reading output level */
		buf[1] |= NPM6001_CONF_DIRECTION_OUTPUT |
			  NPM6001_CONF_INPUT_ENABLED;

		/* open-drain/open-source not supported */
		if ((flags & GPIO_SINGLE_ENDED) != 0U) {
			return -ENOTSUP;
		}

		/* drive strength (defaults to normal) */
		if ((flags & NPM6001_GPIO_DRIVE_MSK) ==
		    NPM6001_GPIO_DRIVE_HIGH) {
			buf[1] |= NPM6001_CONF_DRIVE_HIGH;
		}

		if ((flags & GPIO_OUTPUT_INIT_HIGH) != 0U) {
			int ret;

			ret = gpio_npm6001_port_set_bits_raw(
				dev, (gpio_port_pins_t)BIT(pin));
			if (ret < 0) {
				return ret;
			}
		} else if ((flags & GPIO_OUTPUT_INIT_LOW) != 0U) {
			int ret;

			ret = gpio_npm6001_port_clear_bits_raw(
				dev, (gpio_port_pins_t)BIT(pin));
			if (ret < 0) {
				return ret;
			}
		}
	} else if ((flags & GPIO_INPUT) != 0U) {
		buf[1] |= NPM6001_CONF_INPUT_ENABLED;

		/* pull resistor */
		if ((flags & GPIO_PULL_DOWN) != 0U) {
			buf[1] |= NPM6001_CONF_PULLDOWN_ENABLED;
		} else if ((flags & GPIO_PULL_UP) != 0U) {
			return -ENOTSUP;
		}

		/* input type (defaults to schmitt trigger) */
		if ((flags & NPM6001_GPIO_SENSE_MSK) ==
		    NPM6001_GPIO_SENSE_CMOS) {
			buf[1] |= NPM6001_CONF_SENSE_CMOS;
		}
	} else {
		return -ENOTSUP;
	}

	return i2c_write_dt(&config->bus, buf, sizeof(buf));
}

static int gpio_npm6001_port_set_masked_raw(const struct device *dev,
					    gpio_port_pins_t mask,
					    gpio_port_value_t value)
{
	int ret;

	ret = gpio_npm6001_port_set_bits_raw(dev, mask & value);
	if (ret < 0) {
		return ret;
	}

	return gpio_npm6001_port_clear_bits_raw(
		dev, mask & (~value & NPM6001_PIN_MSK));
}

static int gpio_npm6001_port_toggle_bits(const struct device *dev,
					 gpio_port_pins_t pins)
{
	uint32_t val;
	int ret;

	ret = gpio_npm6001_port_get_raw(dev, &val);
	if (ret < 0) {
		return ret;
	}

	return gpio_npm6001_port_set_masked_raw(dev, pins,
						~val & NPM6001_PIN_MSK);
}

static DEVICE_API(gpio, gpio_npm6001_api) = {
	.pin_configure = gpio_npm6001_configure,
	.port_get_raw = gpio_npm6001_port_get_raw,
	.port_set_masked_raw = gpio_npm6001_port_set_masked_raw,
	.port_set_bits_raw = gpio_npm6001_port_set_bits_raw,
	.port_clear_bits_raw = gpio_npm6001_port_clear_bits_raw,
	.port_toggle_bits = gpio_npm6001_port_toggle_bits,
};

static int gpio_npm6001_init(const struct device *dev)
{
	const struct gpio_npm6001_config *config = dev->config;

	if (!device_is_ready(config->bus.bus)) {
		return -ENODEV;
	}

	return 0;
}

#define GPIO_NPM6001_DEFINE(n)                                                 \
	static const struct gpio_npm6001_config gpio_npm6001_config##n = {     \
		.common =                                                      \
			{                                                      \
				.port_pin_mask =                               \
					GPIO_PORT_PIN_MASK_FROM_DT_INST(n),    \
			},                                                     \
		.bus = I2C_DT_SPEC_GET(DT_INST_PARENT(n))};                    \
                                                                               \
	static struct gpio_npm6001_data gpio_npm6001_data##n;                  \
                                                                               \
	DEVICE_DT_INST_DEFINE(n, gpio_npm6001_init, NULL,                      \
			      &gpio_npm6001_data##n, &gpio_npm6001_config##n,  \
			      POST_KERNEL, CONFIG_GPIO_NPM6001_INIT_PRIORITY,  \
			      &gpio_npm6001_api);

DT_INST_FOREACH_STATUS_OKAY(GPIO_NPM6001_DEFINE)
