/* SPDX-License-Identifier: GPL-2.0-only */

#define __SIMPLE_DEVICE__
#include <console/console.h>
#include <arch/io.h>
#include <device/pnp_ops.h>
#include <device/device.h>
#include <device/pnp.h>
#include <delay.h>
#include "dock.h"
#include <superio/nsc/pc87382/pc87382.h>

#include <southbridge/intel/i82801ix/i82801ix.h>
#include <ec/lenovo/h8/h8.h>
#include <ec/acpi/ec.h>

struct pin_config {
	u8 port;
	u8 mode;
};

static int poll_clk_stable(pnp_devfn_t dev, int timeout)
{
	/* Enable 14.318MHz CLK on CLKIN */
	pnp_write_config(dev, 0x29, 0xa0);
	while (!(pnp_read_config(dev, 0x29) & 0x10) && timeout--)
		udelay(1000);
	if (!timeout)
		return 1;

	return 0;
}

static int gpio_init(pnp_devfn_t gpio, u16 gpio_base,
	const struct pin_config pincfg[], int num_cfgs)
{
	int i;

	/* Enable GPIO LDN. */
	pnp_set_logical_device(gpio);
	pnp_set_iobase(gpio, PNP_IDX_IO0, gpio_base);
	pnp_set_enable(gpio, 1);

	for (i = 0; i < num_cfgs; i++) {
		pnp_write_config(gpio, 0xf0, pincfg[i].port);
		pnp_write_config(gpio, 0xf1, pincfg[i].mode);
		pnp_write_config(gpio, 0xf2, 0x0);
	}
	return 0;
}

static const pnp_devfn_t l_dlpc = PNP_DEV(0x164e, PC87382_DOCK);
static const pnp_devfn_t l_gpio = PNP_DEV(0x164e, PC87382_GPIO);

static int pc87382_init(pnp_devfn_t dlpc, u16 dlpc_base)
{
	/* Maximum 3300 LCLKs at 14.318MHz */
	int timeout = 230;

	/* Enable LPC bridge LDN. */
	pnp_set_logical_device(dlpc);
	pnp_set_iobase(dlpc, PNP_IDX_IO0, dlpc_base);
	pnp_set_enable(dlpc, 1);

	/* Reset docking state */
	outb(0x00, dlpc_base);
	outb(0x07, dlpc_base);
	while (!(inb(dlpc_base) & 8) && timeout--)
		udelay(1);
	if (!timeout)
		return 1;

	return 0;
}

static void pc87382_close(pnp_devfn_t dlpc)
{
	pnp_set_logical_device(dlpc);

	/* Disconnect LPC bus */
	u16 dlpc_base = pnp_read_iobase(dlpc, PNP_IDX_IO0);
	if (dlpc_base) {
		outb(0x00, dlpc_base);
		pnp_set_enable(dlpc, 0);
	}
}

static const struct pin_config local_gpio[] = {
	{0x00, 3},	{0x01, 3},	{0x02, 0},	{0x03, 3},
	{0x04, 4},	{0x20, 4},	{0x21, 4},	{0x23, 4},
};

/* Enable internal clock and configure GPIO LDN */
int pc87382_early(void)
{
	/* Wake-up time is 33 msec (maximum). */
	if (poll_clk_stable(l_gpio, 33) != 0)
		return 1;

	/* Set up GPIOs */
	if (gpio_init(l_gpio, DLPC_GPIO_BASE,
		local_gpio, ARRAY_SIZE(local_gpio)) != 0) {
		return 1;
	}

	return 0;
}

static int pc87382_connect(void)
{
	u8 reg;

	reg = inb(DLPC_GPDO0);
	reg |= D_PLTRST | D_LPCPD;
	/* Deassert D_PLTRST# and D_LPCPD# */
	outb(reg, DLPC_GPDO0);

	if (pc87382_init(l_dlpc, DLPC_CONTROL) != 0)
		return 1;

	/* Assert D_PLTRST# */
	reg &= ~D_PLTRST;
	outb(reg, DLPC_GPDO0);
	udelay(1000);

	/* Deassert D_PLTRST# */
	reg |= D_PLTRST;
	outb(reg, DLPC_GPDO0);
	mdelay(10);

	return 0;
}

static void pc87382_disconnect(void)
{
	pc87382_close(l_dlpc);

	/* Assert D_PLTRST# and D_LPCPD# */
	u8 reg = inb(DLPC_GPDO0);
	reg &= ~(D_PLTRST | D_LPCPD);
	outb(reg, DLPC_GPDO0);
}

/* Returns 3bit dock id */
static u8 dock_identify(void)
{
	u8 id;

	/* Make sure GPIO LDN is configured first ! */
	id = (inb(DLPC_GPDI0) >> 4) & 1;
	id |= (inb(DLPC_GPDI2) & 3) << 1;

	return id;
}

/* Docking station side. */

#include <superio/nsc/pc87384/pc87384.h>

static const pnp_devfn_t r_gpio = PNP_DEV(SUPERIO_DEV, PC87384_GPIO);
static const pnp_devfn_t r_serial = PNP_DEV(SUPERIO_DEV, PC87384_SP1);

static const struct pin_config remote_gpio[] = {
	{0x00, PC87384_GPIO_PIN_DEBOUNCE | PC87384_GPIO_PIN_PULLUP},
	{0x01, PC87384_GPIO_PIN_TYPE_PUSH_PULL | PC87384_GPIO_PIN_OE},
	{0x02, PC87384_GPIO_PIN_TYPE_PUSH_PULL | PC87384_GPIO_PIN_OE},
	{0x03, PC87384_GPIO_PIN_DEBOUNCE | PC87384_GPIO_PIN_PULLUP},
	{0x04, PC87384_GPIO_PIN_DEBOUNCE | PC87384_GPIO_PIN_PULLUP},
	{0x05, PC87384_GPIO_PIN_DEBOUNCE | PC87384_GPIO_PIN_PULLUP},
	{0x06, PC87384_GPIO_PIN_DEBOUNCE | PC87384_GPIO_PIN_PULLUP},
	{0x07, PC87384_GPIO_PIN_DEBOUNCE | PC87384_GPIO_PIN_PULLUP},
};

static int pc87384_init(void)
{
	if (poll_clk_stable(r_gpio, 1000) != 0)
		return 1;

	/* set GPIO pins to Serial/Parallel Port
	 * functions
	 */
	pnp_write_config(r_gpio, 0x22, 0xa9);

	/* enable serial port */

	if (CONFIG_TTYS0_BASE > 0) {
		pnp_set_logical_device(r_serial);
		pnp_set_iobase(r_serial, PNP_IDX_IO0, CONFIG_TTYS0_BASE);
		pnp_set_enable(r_serial, 1);
	}

	if (gpio_init(r_gpio, DOCK_GPIO_BASE,
		remote_gpio, ARRAY_SIZE(remote_gpio)) != 0)
		return 1;

	/* no GPIO events enabled for PORT0 */
	outb(0x00, DOCK_GPIO_BASE + 0x02);
	/* clear GPIO events on PORT0 */
	outb(0xff, DOCK_GPIO_BASE + 0x03);
	outb(0xff, DOCK_GPIO_BASE + 0x04);

	/* no GPIO events enabled for PORT1 */
	outb(0x00, DOCK_GPIO_BASE + 0x06);
	/* clear GPIO events on PORT1*/
	outb(0xff, DOCK_GPIO_BASE + 0x07);
	outb(0x1f, DOCK_GPIO_BASE + 0x08);

	outb(0xfd, DOCK_GPIO_BASE + 0x00);

	return 0;
}

/* Mainboard */

void dock_connect(void)
{
	const u8 id = dock_identify();

	/* Dock type 2505 doesn't have serial, LPT port or LEDs */
	if (id == DOCK_TYPE_NONE || id == DOCK_TYPE_2505)
		return;

	if (pc87382_connect() != 0 || pc87384_init() != 0) {
		pc87382_disconnect();
		return;
	}

	ec_write(H8_LED_CONTROL,
		 H8_LED_CONTROL_OFF | H8_LED_CONTROL_DOCK_LED1);
	ec_write(H8_LED_CONTROL,
		 H8_LED_CONTROL_ON  | H8_LED_CONTROL_DOCK_LED2);
}

void dock_disconnect(void)
{
	pc87382_disconnect();

	ec_write(H8_LED_CONTROL,
		 H8_LED_CONTROL_OFF | H8_LED_CONTROL_DOCK_LED1);
	ec_write(H8_LED_CONTROL,
		 H8_LED_CONTROL_OFF | H8_LED_CONTROL_DOCK_LED2);
}

void dock_info(void)
{
	const u8 id = dock_identify();

	if (id != DOCK_TYPE_NONE)
		printk(BIOS_DEBUG, "DOCK: is present: id=%d\n", id);
	else
		printk(BIOS_DEBUG, "DOCK: not connected\n");
}