/*
 * Copyright (C) 2014 Lucas Stach <l.stach@pengutronix.de>
 *
 * Partly based on code
 * Copyright (C) 2014, NVIDIA CORPORATION.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <common.h>
#include <clock.h>
#include <init.h>
#include <io.h>
#include <malloc.h>
#include <pinctrl.h>
#include <linux/err.h>
#include <linux/reset.h>
#include <linux/phy/phy.h>

#include <dt-bindings/pinctrl/pinctrl-tegra-xusb.h>

#define XUSB_PADCTL_ELPG_PROGRAM 0x01c
#define XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_VCORE_DOWN (1 << 26)
#define XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_CLAMP_EN_EARLY (1 << 25)
#define XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_CLAMP_EN (1 << 24)

#define XUSB_PADCTL_IOPHY_PLL_P0_CTL1 0x040
#define XUSB_PADCTL_IOPHY_PLL_P0_CTL1_PLL0_LOCKDET (1 << 19)
#define XUSB_PADCTL_IOPHY_PLL_P0_CTL1_REFCLK_SEL_MASK (0xf << 12)
#define XUSB_PADCTL_IOPHY_PLL_P0_CTL1_PLL_RST (1 << 1)

#define XUSB_PADCTL_IOPHY_PLL_P0_CTL2 0x044
#define XUSB_PADCTL_IOPHY_PLL_P0_CTL2_REFCLKBUF_EN (1 << 6)
#define XUSB_PADCTL_IOPHY_PLL_P0_CTL2_TXCLKREF_EN (1 << 5)
#define XUSB_PADCTL_IOPHY_PLL_P0_CTL2_TXCLKREF_SEL (1 << 4)

#define XUSB_PADCTL_IOPHY_PLL_S0_CTL1 0x138
#define XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL1_LOCKDET (1 << 27)
#define XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL1_MODE (1 << 24)
#define XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_PWR_OVRD (1 << 3)
#define XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_RST (1 << 1)
#define XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_IDDQ (1 << 0)

#define XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1 0x148
#define XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1_IDDQ_OVRD (1 << 1)
#define XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1_IDDQ (1 << 0)

struct tegra_xusb_padctl_soc {
	const struct tegra_xusb_padctl_lane *lanes;
	unsigned int num_lanes;
};

struct tegra_xusb_padctl_lane {
	const char *name;

	unsigned int offset;
	unsigned int shift;
	unsigned int mask;
	unsigned int iddq;

	const char **funcs;
	unsigned int num_funcs;
};

struct tegra_xusb_padctl {
	struct device_d *dev;
	void __iomem *regs;
	struct reset_control *rst;

	const struct tegra_xusb_padctl_soc *soc;
	struct pinctrl_device pinctrl;

	struct phy_provider *provider;
	struct phy *phys[2];

	unsigned int enable;
};

static inline void padctl_writel(struct tegra_xusb_padctl *padctl, u32 value,
				 unsigned long offset)
{
	writel(value, padctl->regs + offset);
}

static inline u32 padctl_readl(struct tegra_xusb_padctl *padctl,
			       unsigned long offset)
{
	return readl(padctl->regs + offset);
}

static int tegra_xusb_padctl_enable(struct tegra_xusb_padctl *padctl)
{
	u32 value;

	if (padctl->enable++ > 0)
		return 0;

	value = padctl_readl(padctl, XUSB_PADCTL_ELPG_PROGRAM);
	value &= ~XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_CLAMP_EN;
	padctl_writel(padctl, value, XUSB_PADCTL_ELPG_PROGRAM);

	udelay(100);

	value = padctl_readl(padctl, XUSB_PADCTL_ELPG_PROGRAM);
	value &= ~XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_CLAMP_EN_EARLY;
	padctl_writel(padctl, value, XUSB_PADCTL_ELPG_PROGRAM);

	udelay(100);

	value = padctl_readl(padctl, XUSB_PADCTL_ELPG_PROGRAM);
	value &= ~XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_VCORE_DOWN;
	padctl_writel(padctl, value, XUSB_PADCTL_ELPG_PROGRAM);

	return 0;
}

static int tegra_xusb_padctl_disable(struct tegra_xusb_padctl *padctl)
{
	u32 value;

	if (WARN_ON(padctl->enable == 0))
		return 0;

	if (--padctl->enable > 0)
		return 0;

	value = padctl_readl(padctl, XUSB_PADCTL_ELPG_PROGRAM);
	value |= XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_VCORE_DOWN;
	padctl_writel(padctl, value, XUSB_PADCTL_ELPG_PROGRAM);

	udelay(100);

	value = padctl_readl(padctl, XUSB_PADCTL_ELPG_PROGRAM);
	value |= XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_CLAMP_EN_EARLY;
	padctl_writel(padctl, value, XUSB_PADCTL_ELPG_PROGRAM);

	udelay(100);

	value = padctl_readl(padctl, XUSB_PADCTL_ELPG_PROGRAM);
	value |= XUSB_PADCTL_ELPG_PROGRAM_AUX_MUX_LP0_CLAMP_EN;
	padctl_writel(padctl, value, XUSB_PADCTL_ELPG_PROGRAM);

	return 0;
}

static int tegra_xusb_phy_init(struct phy *phy)
{
	struct tegra_xusb_padctl *padctl = phy_get_drvdata(phy);

	return tegra_xusb_padctl_enable(padctl);
}

static int tegra_xusb_phy_exit(struct phy *phy)
{
	struct tegra_xusb_padctl *padctl = phy_get_drvdata(phy);

	return tegra_xusb_padctl_disable(padctl);
}

static int pcie_phy_power_on(struct phy *phy)
{
	struct tegra_xusb_padctl *padctl = phy_get_drvdata(phy);
	int err;
	u32 value;

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_P0_CTL1);
	value &= ~XUSB_PADCTL_IOPHY_PLL_P0_CTL1_REFCLK_SEL_MASK;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_P0_CTL1);

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_P0_CTL2);
	value |= XUSB_PADCTL_IOPHY_PLL_P0_CTL2_REFCLKBUF_EN |
		 XUSB_PADCTL_IOPHY_PLL_P0_CTL2_TXCLKREF_EN |
		 XUSB_PADCTL_IOPHY_PLL_P0_CTL2_TXCLKREF_SEL;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_P0_CTL2);

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_P0_CTL1);
	value |= XUSB_PADCTL_IOPHY_PLL_P0_CTL1_PLL_RST;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_P0_CTL1);

	err = wait_on_timeout(50 * MSECOND,
			padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_P0_CTL1) &
			XUSB_PADCTL_IOPHY_PLL_P0_CTL1_PLL0_LOCKDET);

	return err;
}

static int pcie_phy_power_off(struct phy *phy)
{
	struct tegra_xusb_padctl *padctl = phy_get_drvdata(phy);
	u32 value;

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_P0_CTL1);
	value &= ~XUSB_PADCTL_IOPHY_PLL_P0_CTL1_PLL_RST;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_P0_CTL1);

	return 0;
}

static const struct phy_ops pcie_phy_ops = {

	.init = tegra_xusb_phy_init,
	.exit = tegra_xusb_phy_exit,
	.power_on = pcie_phy_power_on,
	.power_off = pcie_phy_power_off,
};

static int sata_phy_power_on(struct phy *phy)
{
	struct tegra_xusb_padctl *padctl = phy_get_drvdata(phy);
	int err;
	u32 value;

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1);
	value &= ~XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1_IDDQ_OVRD;
	value &= ~XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1_IDDQ;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1);

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);
	value &= ~XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_PWR_OVRD;
	value &= ~XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_IDDQ;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);
	value |= XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL1_MODE;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);
	value |= XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_RST;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);

	err = wait_on_timeout(50 * MSECOND,
			padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_S0_CTL1) &
			XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL1_LOCKDET);

	return err;
}

static int sata_phy_power_off(struct phy *phy)
{
	struct tegra_xusb_padctl *padctl = phy_get_drvdata(phy);
	u32 value;

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);
	value &= ~XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_RST;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);
	value &= ~XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL1_MODE;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);
	value |= XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_PWR_OVRD;
	value |= XUSB_PADCTL_IOPHY_PLL_S0_CTL1_PLL_IDDQ;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_PLL_S0_CTL1);

	value = padctl_readl(padctl, XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1);
	value |= ~XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1_IDDQ_OVRD;
	value |= ~XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1_IDDQ;
	padctl_writel(padctl, value, XUSB_PADCTL_IOPHY_MISC_PAD_S0_CTL1);

	return 0;
}

static const struct phy_ops sata_phy_ops = {

	.init = tegra_xusb_phy_init,
	.exit = tegra_xusb_phy_exit,
	.power_on = sata_phy_power_on,
	.power_off = sata_phy_power_off,
};

static struct phy *tegra_xusb_padctl_xlate(struct device_d *dev,
					   struct of_phandle_args *args)
{
	struct tegra_xusb_padctl *padctl = dev->priv;
	unsigned int index = args->args[0];

	if (args->args_count <= 0)
		return ERR_PTR(-EINVAL);

	if (index >= ARRAY_SIZE(padctl->phys))
		return ERR_PTR(-EINVAL);

	return padctl->phys[index];
}

static int pinctrl_tegra_xusb_set_state(struct pinctrl_device *pdev,
					struct device_node *np)
{
	struct tegra_xusb_padctl *padctl =
			container_of(pdev, struct tegra_xusb_padctl, pinctrl);
	struct device_node *childnode;
	int iddq = -1, i, j, k;
	const char *lanes, *func = NULL;
	const struct tegra_xusb_padctl_lane *lane = NULL;
	u32 val;

	/*
	 * At first look if the node we are pointed at has children,
	 * which we may want to visit.
	 */
	list_for_each_entry(childnode, &np->children, parent_list)
		pinctrl_tegra_xusb_set_state(pdev, childnode);

	/* read relevant state from devicetree */
	of_property_read_string(np, "nvidia,function", &func);
	of_property_read_u32_array(np, "nvidia,iddq", &iddq, 1);

	/* iterate over all lanes referenced in the dt node */
	for (i = 0; ; i++) {
		if (of_property_read_string_index(np, "nvidia,lanes", i, &lanes))
			break;

		for (j = 0; j < padctl->soc->num_lanes; j++) {
			if (!strcmp(lanes, padctl->soc->lanes[j].name)) {
				lane = &padctl->soc->lanes[j];
				break;
			}
		}
		/* if no matching lane is found */
		if (j == padctl->soc->num_lanes) {
			/* nothing matching found, warn and bail out */
			dev_warn(padctl->pinctrl.dev,
				 "invalid lane %s referenced in node %s\n",
				 lanes, np->name);
			continue;
		}

		if (func) {
			for (k = 0; k < lane->num_funcs; k++) {
				if (!strcmp(func, lane->funcs[k]))
					break;
			}
			if (k < lane->num_funcs) {
				val = padctl_readl(padctl, lane->offset);
				val &= ~(lane->mask << lane->shift);
				val |= k << lane->shift;
				padctl_writel(padctl, val, lane->offset);
			} else {
				dev_warn(padctl->pinctrl.dev,
					 "invalid function %s for lane %s in node %s\n",
					 func, lane->name, np->name);
			}
		}

		if (iddq >= 0) {
			if (lane->iddq) {
				val = padctl_readl(padctl, lane->offset);
				if (iddq)
					val &= ~BIT(lane->iddq);
				else
					val |= BIT(lane->iddq);
				padctl_writel(padctl, val, lane->offset);
			} else {
				dev_warn(padctl->pinctrl.dev,
					 "invalid iddq setting for lane %s in node %s\n",
					 lane->name, np->name);
			}
		}
	}

	return 0;
}

static struct pinctrl_ops pinctrl_tegra_xusb_ops = {
	.set_state = pinctrl_tegra_xusb_set_state,
};

static int pinctrl_tegra_xusb_probe(struct device_d *dev)
{
	struct tegra_xusb_padctl *padctl;
	struct phy *phy;
	int err;

	padctl = xzalloc(sizeof(*padctl));

	dev->priv = padctl;
	padctl->dev = dev;

	dev_get_drvdata(dev, (const void **)&padctl->soc);

	padctl->regs = dev_request_mem_region(dev, 0);
	if (IS_ERR(padctl->regs)) {
		dev_err(dev, "Could not get iomem region\n");
		return PTR_ERR(padctl->regs);
	}

	padctl->rst = reset_control_get(dev, NULL);
	if (IS_ERR(padctl->rst))
		return PTR_ERR(padctl->rst);

	err = reset_control_deassert(padctl->rst);
	if (err < 0)
		return err;

	padctl->pinctrl.dev = dev;
	padctl->pinctrl.ops = &pinctrl_tegra_xusb_ops;

	err = pinctrl_register(&padctl->pinctrl);
	if (err) {
		dev_err(dev, "failed to register pincontrol\n");
		err = -ENODEV;
		goto reset;
	}

	phy = phy_create(dev, NULL, &pcie_phy_ops, NULL);
	if (IS_ERR(phy)) {
		err = PTR_ERR(phy);
		goto unregister;
	}

	padctl->phys[TEGRA_XUSB_PADCTL_PCIE] = phy;
	phy_set_drvdata(phy, padctl);

	phy = phy_create(dev, NULL, &sata_phy_ops, NULL);
	if (IS_ERR(phy)) {
		err = PTR_ERR(phy);
		goto unregister;
	}

	padctl->phys[TEGRA_XUSB_PADCTL_SATA] = phy;
	phy_set_drvdata(phy, padctl);

	padctl->provider = of_phy_provider_register(dev, tegra_xusb_padctl_xlate);
	if (IS_ERR(padctl->provider)) {
		err = PTR_ERR(padctl->provider);
		dev_err(dev, "failed to register PHYs: %d\n", err);
		goto unregister;
	}

	return 0;

unregister:
	pinctrl_unregister(&padctl->pinctrl);
reset:
	reset_control_assert(padctl->rst);
	return err;
}

static const char *tegra124_otg_functions[] = {
	"snps",
	"xusb",
	"uart",
	"rsvd",
};

static const char *tegra124_usb_functions[] = {
	"snps",
	"xusb",
};

static const char *tegra124_pci_functions[] = {
	"pcie",
	"usb3",
	"sata",
	"rsvd",
};

#define TEGRA124_LANE(_name, _offs, _shift, _mask, _iddq, _funcs, _num)	\
	{								\
		.name = _name,						\
		.offset = _offs,					\
		.shift = _shift,					\
		.mask = _mask,						\
		.iddq = _iddq,						\
		.num_funcs = _num,					\
		.funcs = tegra124_##_funcs##_functions,			\
	}

static const struct tegra_xusb_padctl_lane tegra124_lanes[] = {
	TEGRA124_LANE("otg-0",  0x004,  0, 0x3, 0, otg, 4),
	TEGRA124_LANE("otg-1",  0x004,  2, 0x3, 0, otg, 4),
	TEGRA124_LANE("otg-2",  0x004,  4, 0x3, 0, otg, 4),
	TEGRA124_LANE("ulpi-0", 0x004, 12, 0x1, 0, usb, 2),
	TEGRA124_LANE("hsic-0", 0x004, 14, 0x1, 0, usb, 2),
	TEGRA124_LANE("hsic-1", 0x004, 15, 0x1, 0, usb, 2),
	TEGRA124_LANE("pcie-0", 0x134, 16, 0x3, 1, pci, 4),
	TEGRA124_LANE("pcie-1", 0x134, 18, 0x3, 2, pci, 4),
	TEGRA124_LANE("pcie-2", 0x134, 20, 0x3, 3, pci, 4),
	TEGRA124_LANE("pcie-3", 0x134, 22, 0x3, 4, pci, 4),
	TEGRA124_LANE("pcie-4", 0x134, 24, 0x3, 5, pci, 4),
	TEGRA124_LANE("sata-0", 0x134, 26, 0x3, 6, pci, 4),
};

static const struct tegra_xusb_padctl_soc tegra124_soc = {
	.num_lanes = ARRAY_SIZE(tegra124_lanes),
	.lanes = tegra124_lanes,
};

static __maybe_unused struct of_device_id pinctrl_tegra_xusb_dt_ids[] = {
	{
		.compatible = "nvidia,tegra124-xusb-padctl",
		.data = &tegra124_soc,
	}, {
		/* sentinel */
	}
};

static struct driver_d pinctrl_tegra_xusb_driver = {
	.name		= "pinctrl-tegra-xusb",
	.probe		= pinctrl_tegra_xusb_probe,
	.of_compatible	= DRV_OF_COMPAT(pinctrl_tegra_xusb_dt_ids),
};

static int pinctrl_tegra_xusb_init(void)
{
	return platform_driver_register(&pinctrl_tegra_xusb_driver);
}
postcore_initcall(pinctrl_tegra_xusb_init);
