/*-
 * Copyright (c) 2016 Michal Meloun <mmel@FreeBSD.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/gpio.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/sx.h>

#include <machine/bus.h>

#include <dev/fdt/fdt_common.h>
#include <dev/gpio/gpiobusvar.h>

#include "as3722.h"

MALLOC_DEFINE(M_AS3722_GPIO, "AS3722 gpio", "AS3722 GPIO");

/* AS3722_GPIOx_CONTROL	 MODE and IOSF definition. */
#define	AS3722_IOSF_GPIO				0x00
#define	AS3722_IOSF_INTERRUPT_OUT			0x01
#define	AS3722_IOSF_VSUP_VBAT_LOW_UNDEBOUNCE_OUT	0x02
#define	AS3722_IOSF_GPIO_IN_INTERRUPT			0x03
#define	AS3722_IOSF_PWM_IN				0x04
#define	AS3722_IOSF_VOLTAGE_IN_STANDBY			0x05
#define	AS3722_IOSF_OC_PG_SD0				0x06
#define	AS3722_IOSF_POWERGOOD_OUT			0x07
#define	AS3722_IOSF_CLK32K_OUT				0x08
#define	AS3722_IOSF_WATCHDOG_IN				0x09
#define	AS3722_IOSF_SOFT_RESET_IN			0x0b
#define	AS3722_IOSF_PWM_OUT				0x0c
#define	AS3722_IOSF_VSUP_VBAT_LOW_DEBOUNCE_OUT		0x0d
#define	AS3722_IOSF_OC_PG_SD6				0x0e

#define	AS3722_MODE_INPUT				0
#define	AS3722_MODE_PUSH_PULL				1
#define	AS3722_MODE_OPEN_DRAIN				2
#define	AS3722_MODE_TRISTATE				3
#define	AS3722_MODE_INPUT_PULL_UP_LV			4
#define	AS3722_MODE_INPUT_PULL_DOWN			5
#define	AS3722_MODE_OPEN_DRAIN_LV			6
#define	AS3722_MODE_PUSH_PULL_LV			7

#define	NGPIO		8

#define	GPIO_LOCK(_sc)	sx_slock(&(_sc)->gpio_lock)
#define	GPIO_UNLOCK(_sc)	sx_unlock(&(_sc)->gpio_lock)
#define	GPIO_ASSERT(_sc)	sx_assert(&(_sc)->gpio_lock, SA_LOCKED)

#define	AS3722_CFG_BIAS_DISABLE		0x0001
#define	AS3722_CFG_BIAS_PULL_UP		0x0002
#define	AS3722_CFG_BIAS_PULL_DOWN	0x0004
#define	AS3722_CFG_BIAS_HIGH_IMPEDANCE	0x0008
#define	AS3722_CFG_OPEN_DRAIN		0x0010

static const struct {
	const char	*name;
	int  		config;		/* AS3722_CFG_  */
} as3722_cfg_names[] = {
	{"bias-disable",	AS3722_CFG_BIAS_DISABLE},
	{"bias-pull-up",	AS3722_CFG_BIAS_PULL_UP},
	{"bias-pull-down",	AS3722_CFG_BIAS_PULL_DOWN},
	{"bias-high-impedance",	AS3722_CFG_BIAS_HIGH_IMPEDANCE},
	{"drive-open-drain",	AS3722_CFG_OPEN_DRAIN},
};

static struct {
	const char *name;
	int fnc_val;
} as3722_fnc_table[] = {
	{"gpio",			AS3722_IOSF_GPIO},
	{"interrupt-out",		AS3722_IOSF_INTERRUPT_OUT},
	{"vsup-vbat-low-undebounce-out", AS3722_IOSF_VSUP_VBAT_LOW_UNDEBOUNCE_OUT},
	{"gpio-in-interrupt",		AS3722_IOSF_GPIO_IN_INTERRUPT},
	{"pwm-in",			AS3722_IOSF_PWM_IN},
	{"voltage-in-standby",		AS3722_IOSF_VOLTAGE_IN_STANDBY},
	{"oc-pg-sd0",			AS3722_IOSF_OC_PG_SD0},
	{"powergood-out",		AS3722_IOSF_POWERGOOD_OUT},
	{"clk32k-out",			AS3722_IOSF_CLK32K_OUT},
	{"watchdog-in",			AS3722_IOSF_WATCHDOG_IN},
	{"soft-reset-in",		AS3722_IOSF_SOFT_RESET_IN},
	{"pwm-out",			AS3722_IOSF_PWM_OUT},
	{"vsup-vbat-low-debounce-out",	AS3722_IOSF_VSUP_VBAT_LOW_DEBOUNCE_OUT},
	{"oc-pg-sd6",			AS3722_IOSF_OC_PG_SD6},
};

struct as3722_pincfg {
	char	*function;
	int	flags;
};

struct as3722_gpio_pin {
	int	pin_caps;
	uint8_t	pin_ctrl_reg;
	char	pin_name[GPIOMAXNAME];
	int	pin_cfg_flags;
};

/* --------------------------------------------------------------------------
 *
 *  Pinmux functions.
 */
static int
as3722_pinmux_get_function(struct as3722_softc *sc, char *name)
{
	int i;

	for (i = 0; i < nitems(as3722_fnc_table); i++) {
		if (strcmp(as3722_fnc_table[i].name, name) == 0)
			 return (as3722_fnc_table[i].fnc_val);
	}
	return (-1);
}

static int
as3722_pinmux_config_node(struct as3722_softc *sc, char *pin_name,
    struct as3722_pincfg *cfg)
{
	uint8_t ctrl;
	int rv, fnc, pin;

	for (pin = 0; pin < sc->gpio_npins; pin++) {
		if (strcmp(sc->gpio_pins[pin]->pin_name, pin_name) == 0)
			 break;
	}
	if (pin >= sc->gpio_npins) {
		device_printf(sc->dev, "Unknown pin: %s\n", pin_name);
		return (ENXIO);
	}

	ctrl = sc->gpio_pins[pin]->pin_ctrl_reg;
	sc->gpio_pins[pin]->pin_cfg_flags = cfg->flags;
	if (cfg->function != NULL) {
		fnc = as3722_pinmux_get_function(sc, cfg->function);
		if (fnc == -1) {
			device_printf(sc->dev,
			    "Unknown function %s for pin %s\n", cfg->function,
			    sc->gpio_pins[pin]->pin_name);
			return (ENXIO);
		}
		switch (fnc) {
		case AS3722_IOSF_INTERRUPT_OUT:
		case AS3722_IOSF_VSUP_VBAT_LOW_UNDEBOUNCE_OUT:
		case AS3722_IOSF_OC_PG_SD0:
		case AS3722_IOSF_POWERGOOD_OUT:
		case AS3722_IOSF_CLK32K_OUT:
		case AS3722_IOSF_PWM_OUT:
		case AS3722_IOSF_OC_PG_SD6:
			ctrl &= ~(AS3722_GPIO_MODE_MASK <<
			    AS3722_GPIO_MODE_SHIFT);
			ctrl |= AS3722_MODE_PUSH_PULL << AS3722_GPIO_MODE_SHIFT;
			/* XXX Handle flags (OC + pullup) */
			break;
		case AS3722_IOSF_GPIO_IN_INTERRUPT:
		case AS3722_IOSF_PWM_IN:
		case AS3722_IOSF_VOLTAGE_IN_STANDBY:
		case AS3722_IOSF_WATCHDOG_IN:
		case AS3722_IOSF_SOFT_RESET_IN:
			ctrl &= ~(AS3722_GPIO_MODE_MASK <<
			    AS3722_GPIO_MODE_SHIFT);
			ctrl |= AS3722_MODE_INPUT << AS3722_GPIO_MODE_SHIFT;
			/* XXX Handle flags (pulldown + pullup) */

		default:
			break;
		}
		ctrl &= ~(AS3722_GPIO_IOSF_MASK << AS3722_GPIO_IOSF_SHIFT);
		ctrl |= fnc << AS3722_GPIO_IOSF_SHIFT;
	}
	rv = 0;
	if (ctrl != sc->gpio_pins[pin]->pin_ctrl_reg) {
		rv = WR1(sc, AS3722_GPIO0_CONTROL + pin, ctrl);
		sc->gpio_pins[pin]->pin_ctrl_reg = ctrl;
	}
	return (rv);
}

static int
as3722_pinmux_read_node(struct as3722_softc *sc, phandle_t node,
     struct as3722_pincfg *cfg, char **pins, int *lpins)
{
	int rv, i;

	*lpins = OF_getprop_alloc(node, "pins", (void **)pins);
	if (*lpins <= 0)
		return (ENOENT);

	/* Read function (mux) settings. */
	rv = OF_getprop_alloc(node, "function", (void **)&cfg->function);
	if (rv <= 0)
		cfg->function = NULL;

	/* Read boolean properties. */
	for (i = 0; i < nitems(as3722_cfg_names); i++) {
		if (OF_hasprop(node, as3722_cfg_names[i].name))
			cfg->flags |= as3722_cfg_names[i].config;
	}
	return (0);
}

static int
as3722_pinmux_process_node(struct as3722_softc *sc, phandle_t node)
{
	struct as3722_pincfg cfg;
	char *pins, *pname;
	int i, len, lpins, rv;

	rv = as3722_pinmux_read_node(sc, node, &cfg, &pins, &lpins);
	if (rv != 0)
		return (rv);

	len = 0;
	pname = pins;
	do {
		i = strlen(pname) + 1;
		rv = as3722_pinmux_config_node(sc, pname, &cfg);
		if (rv != 0) {
			device_printf(sc->dev,
			    "Cannot configure pin: %s: %d\n", pname, rv);
		}
		len += i;
		pname += i;
	} while (len < lpins);

	if (pins != NULL)
		OF_prop_free(pins);
	if (cfg.function != NULL)
		OF_prop_free(cfg.function);

	return (rv);
}

int as3722_pinmux_configure(device_t dev, phandle_t cfgxref)
{
	struct as3722_softc *sc;
	phandle_t node, cfgnode;
	int rv;

	sc = device_get_softc(dev);
	cfgnode = OF_node_from_xref(cfgxref);

	for (node = OF_child(cfgnode); node != 0; node = OF_peer(node)) {
		if (!ofw_bus_node_status_okay(node))
			continue;
		rv = as3722_pinmux_process_node(sc, node);
		if (rv != 0)
			device_printf(dev, "Failed to process pinmux");
	}
	return (0);
}

/* --------------------------------------------------------------------------
 *
 *  GPIO
 */
device_t
as3722_gpio_get_bus(device_t dev)
{
	struct as3722_softc *sc;

	sc = device_get_softc(dev);
	return (sc->gpio_busdev);
}

int
as3722_gpio_pin_max(device_t dev, int *maxpin)
{

	*maxpin = NGPIO - 1;
	return (0);
}

int
as3722_gpio_pin_getcaps(device_t dev, uint32_t pin, uint32_t *caps)
{
	struct as3722_softc *sc;

	sc = device_get_softc(dev);
	if (pin >= sc->gpio_npins)
		return (EINVAL);
	GPIO_LOCK(sc);
	*caps = sc->gpio_pins[pin]->pin_caps;
	GPIO_UNLOCK(sc);
	return (0);
}

int
as3722_gpio_pin_getname(device_t dev, uint32_t pin, char *name)
{
	struct as3722_softc *sc;

	sc = device_get_softc(dev);
	if (pin >= sc->gpio_npins)
		return (EINVAL);
	GPIO_LOCK(sc);
	memcpy(name, sc->gpio_pins[pin]->pin_name, GPIOMAXNAME);
	GPIO_UNLOCK(sc);
	return (0);
}

int
as3722_gpio_pin_getflags(device_t dev, uint32_t pin, uint32_t *out_flags)
{
	struct as3722_softc *sc;
	uint8_t tmp, mode, iosf;
	uint32_t flags;
	bool inverted;

	sc = device_get_softc(dev);
	if (pin >= sc->gpio_npins)
		return (EINVAL);

	GPIO_LOCK(sc);
	tmp = sc->gpio_pins[pin]->pin_ctrl_reg;
	GPIO_UNLOCK(sc);
	iosf = (tmp >> AS3722_GPIO_IOSF_SHIFT) & AS3722_GPIO_IOSF_MASK;
	mode = (tmp >> AS3722_GPIO_MODE_SHIFT) & AS3722_GPIO_MODE_MASK;
	inverted = (tmp & AS3722_GPIO_INVERT) != 0;
	/* Is pin in GPIO mode ? */
	if (iosf != AS3722_IOSF_GPIO)
		return (ENXIO);

	flags = 0;
	switch (mode) {
	case AS3722_MODE_INPUT:
		flags = GPIO_PIN_INPUT;
		break;
	case AS3722_MODE_PUSH_PULL:
	case AS3722_MODE_PUSH_PULL_LV:
		flags = GPIO_PIN_OUTPUT;
		break;
	case AS3722_MODE_OPEN_DRAIN:
	case AS3722_MODE_OPEN_DRAIN_LV:
		flags = GPIO_PIN_INPUT | GPIO_PIN_OUTPUT | GPIO_PIN_OPENDRAIN;
		break;
	case AS3722_MODE_TRISTATE:
		flags = GPIO_PIN_TRISTATE;
		break;
	case AS3722_MODE_INPUT_PULL_UP_LV:
		flags = GPIO_PIN_INPUT | GPIO_PIN_PULLUP;
		break;

	case AS3722_MODE_INPUT_PULL_DOWN:
		flags = GPIO_PIN_OUTPUT | GPIO_PIN_PULLDOWN;
		break;
	}
	if (inverted)
		flags |= GPIO_PIN_INVIN | GPIO_PIN_INVOUT;
	*out_flags = flags;
	return (0);
}

static int
as3722_gpio_get_mode(struct as3722_softc *sc, uint32_t pin, uint32_t gpio_flags)
{
	uint8_t ctrl;
	int flags;

	ctrl = sc->gpio_pins[pin]->pin_ctrl_reg;
	flags =  sc->gpio_pins[pin]->pin_cfg_flags;

	/* Tristate mode. */
	if (flags & AS3722_CFG_BIAS_HIGH_IMPEDANCE ||
	    gpio_flags & GPIO_PIN_TRISTATE)
		return (AS3722_MODE_TRISTATE);

	/* Open drain modes. */
	if (flags & AS3722_CFG_OPEN_DRAIN || gpio_flags & GPIO_PIN_OPENDRAIN) {
		/* Only pull up have effect */
		if (flags & AS3722_CFG_BIAS_PULL_UP ||
		    gpio_flags & GPIO_PIN_PULLUP)
			return (AS3722_MODE_OPEN_DRAIN_LV);
		return (AS3722_MODE_OPEN_DRAIN);
	}
	/* Input modes. */
	if (gpio_flags & GPIO_PIN_INPUT) {
		/* Accept pull up or pull down. */
		if (flags & AS3722_CFG_BIAS_PULL_UP ||
		    gpio_flags & GPIO_PIN_PULLUP)
			return (AS3722_MODE_INPUT_PULL_UP_LV);

		if (flags & AS3722_CFG_BIAS_PULL_DOWN ||
		    gpio_flags & GPIO_PIN_PULLDOWN)
			return (AS3722_MODE_INPUT_PULL_DOWN);
		return (AS3722_MODE_INPUT);
	}
	/*
	 * Output modes.
	 * Pull down is used as indicator of low voltage output.
	 */
	if (flags & AS3722_CFG_BIAS_PULL_DOWN ||
		    gpio_flags & GPIO_PIN_PULLDOWN)
		return (AS3722_MODE_PUSH_PULL_LV);
	return (AS3722_MODE_PUSH_PULL);
}

int
as3722_gpio_pin_setflags(device_t dev, uint32_t pin, uint32_t flags)
{
	struct as3722_softc *sc;
	uint8_t ctrl, mode, iosf;
	int rv;

	sc = device_get_softc(dev);
	if (pin >= sc->gpio_npins)
		return (EINVAL);

	GPIO_LOCK(sc);
	ctrl = sc->gpio_pins[pin]->pin_ctrl_reg;
	iosf = (ctrl >> AS3722_GPIO_IOSF_SHIFT) & AS3722_GPIO_IOSF_MASK;
	/* Is pin in GPIO mode ? */
	if (iosf != AS3722_IOSF_GPIO) {
		GPIO_UNLOCK(sc);
		return (ENXIO);
	}
	mode = as3722_gpio_get_mode(sc, pin, flags);
	ctrl &= ~(AS3722_GPIO_MODE_MASK << AS3722_GPIO_MODE_SHIFT);
	ctrl |= AS3722_MODE_PUSH_PULL << AS3722_GPIO_MODE_SHIFT;
	rv = 0;
	if (ctrl != sc->gpio_pins[pin]->pin_ctrl_reg) {
		rv = WR1(sc, AS3722_GPIO0_CONTROL + pin, ctrl);
		sc->gpio_pins[pin]->pin_ctrl_reg = ctrl;
	}
	GPIO_UNLOCK(sc);
	return (rv);
}

int
as3722_gpio_pin_set(device_t dev, uint32_t pin, uint32_t val)
{
	struct as3722_softc *sc;
	uint8_t tmp;
	int rv;

	sc = device_get_softc(dev);
	if (pin >= sc->gpio_npins)
		return (EINVAL);

	tmp =  (val != 0) ? 1 : 0;
	if (sc->gpio_pins[pin]->pin_ctrl_reg & AS3722_GPIO_INVERT)
		tmp ^= 1;

	GPIO_LOCK(sc);
	rv = RM1(sc, AS3722_GPIO_SIGNAL_OUT, (1 << pin), (tmp << pin));
	GPIO_UNLOCK(sc);
	return (rv);
}

int
as3722_gpio_pin_get(device_t dev, uint32_t pin, uint32_t *val)
{
	struct as3722_softc *sc;
	uint8_t tmp, mode, ctrl;
	int rv;

	sc = device_get_softc(dev);
	if (pin >= sc->gpio_npins)
		return (EINVAL);

	GPIO_LOCK(sc);
	ctrl = sc->gpio_pins[pin]->pin_ctrl_reg;
	mode = (ctrl >> AS3722_GPIO_MODE_SHIFT) & AS3722_GPIO_MODE_MASK;
	if ((mode == AS3722_MODE_PUSH_PULL) ||
	    (mode == AS3722_MODE_PUSH_PULL_LV))
		rv = RD1(sc, AS3722_GPIO_SIGNAL_OUT, &tmp);
	else
		rv = RD1(sc, AS3722_GPIO_SIGNAL_IN, &tmp);
	GPIO_UNLOCK(sc);
	if (rv != 0)
		return (rv);

	*val = tmp & (1 << pin) ? 1 : 0;
	if (ctrl & AS3722_GPIO_INVERT)
		*val ^= 1;
	return (0);
}

int
as3722_gpio_pin_toggle(device_t dev, uint32_t pin)
{
	struct as3722_softc *sc;
	uint8_t tmp;
	int rv;

	sc = device_get_softc(dev);
	if (pin >= sc->gpio_npins)
		return (EINVAL);

	GPIO_LOCK(sc);
	rv = RD1(sc, AS3722_GPIO_SIGNAL_OUT, &tmp);
	if (rv != 0) {
		GPIO_UNLOCK(sc);
		return (rv);
	}
	tmp ^= (1 <<pin);
	rv = RM1(sc, AS3722_GPIO_SIGNAL_OUT, (1 << pin), tmp);
	GPIO_UNLOCK(sc);
	return (0);
}

int
as3722_gpio_map_gpios(device_t dev, phandle_t pdev, phandle_t gparent,
    int gcells, pcell_t *gpios, uint32_t *pin, uint32_t *flags)
{

	if (gcells != 2)
		return (ERANGE);
	*pin = gpios[0];
	*flags= gpios[1];
	return (0);
}

int
as3722_gpio_attach(struct as3722_softc *sc, phandle_t node)
{
	struct as3722_gpio_pin *pin;
	int i, rv;

	sx_init(&sc->gpio_lock, "AS3722 GPIO lock");
	sc->gpio_npins = NGPIO;
	sc->gpio_pins = malloc(sizeof(struct as3722_gpio_pin *) *
	    sc->gpio_npins, M_AS3722_GPIO, M_WAITOK | M_ZERO);

	sc->gpio_busdev = gpiobus_attach_bus(sc->dev);
	if (sc->gpio_busdev == NULL)
		return (ENXIO);
	for (i = 0; i < sc->gpio_npins; i++) {
		sc->gpio_pins[i] = malloc(sizeof(struct as3722_gpio_pin),
		    M_AS3722_GPIO, M_WAITOK | M_ZERO);
		pin = sc->gpio_pins[i];
		sprintf(pin->pin_name, "gpio%d", i);
		pin->pin_caps = GPIO_PIN_INPUT | GPIO_PIN_OUTPUT  |
		    GPIO_PIN_OPENDRAIN | GPIO_PIN_PUSHPULL | GPIO_PIN_TRISTATE |
		    GPIO_PIN_PULLUP | GPIO_PIN_PULLDOWN | GPIO_PIN_INVIN |
		    GPIO_PIN_INVOUT;
		rv = RD1(sc, AS3722_GPIO0_CONTROL + i, &pin->pin_ctrl_reg);
		if (rv != 0) {
			device_printf(sc->dev,
			    "Cannot read configuration for pin %s\n",
			    sc->gpio_pins[i]->pin_name);
		}
	}
	return (0);
}