/*
 * i.MX drm driver - parallel display implementation
 *
 * Copyright (C) 2012 Sascha Hauer, Pengutronix
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include <common.h>
#include <fb.h>
#include <io.h>
#include <of_graph.h>
#include <driver.h>
#include <malloc.h>
#include <errno.h>
#include <init.h>
#include <video/media-bus-format.h>
#include <video/vpl.h>
#include <mfd/imx6q-iomuxc-gpr.h>
#include <linux/clk.h>
#include <linux/err.h>
#include <linux/math64.h>
#include <linux/clk.h>
#include <mach/imx6-regs.h>
#include <mach/imx53-regs.h>

#include "imx-ipu-v3.h"
#include "ipuv3-plane.h"

#define LDB_CH0_MODE_EN_TO_DI0		(1 << 0)
#define LDB_CH0_MODE_EN_TO_DI1		(3 << 0)
#define LDB_CH0_MODE_EN_MASK		(3 << 0)
#define LDB_CH1_MODE_EN_TO_DI0		(1 << 2)
#define LDB_CH1_MODE_EN_TO_DI1		(3 << 2)
#define LDB_CH1_MODE_EN_MASK		(3 << 2)
#define LDB_SPLIT_MODE_EN		(1 << 4)
#define LDB_DATA_WIDTH_CH0_24		(1 << 5)
#define LDB_BIT_MAP_CH0_JEIDA		(1 << 6)
#define LDB_DATA_WIDTH_CH1_24		(1 << 7)
#define LDB_BIT_MAP_CH1_JEIDA		(1 << 8)
#define LDB_DI0_VS_POL_ACT_LOW		(1 << 9)
#define LDB_DI1_VS_POL_ACT_LOW		(1 << 10)
#define LDB_BGREF_RMODE_INT		(1 << 15)

struct imx_ldb;

struct imx_ldb_channel {
	struct imx_ldb *ldb;
	int chno;
	int mode_valid;
	struct display_timings *modes;
	struct device_node *remote;
	struct vpl vpl;
	int output_port;
	int datawidth;
};

struct imx_ldb_data {
	void __iomem *base;
	int (*prepare)(struct imx_ldb_channel *imx_ldb_ch, int di, unsigned long clkrate);
	unsigned ipu_mask;
	int have_mux;
};

struct imx_ldb {
	struct device_d *dev;
	u32 bus_format;
	int mode_valid;
	struct imx_ldb_channel channel[2];
	u32 ldb_ctrl;
	void __iomem *base;
	const struct imx_ldb_data *soc_data;
};

enum {
	LVDS_BIT_MAP_SPWG,
	LVDS_BIT_MAP_JEIDA
};

static const char * const imx_ldb_bit_mappings[] = {
	[LVDS_BIT_MAP_SPWG]  = "spwg",
	[LVDS_BIT_MAP_JEIDA] = "jeida",
};

static const int of_get_data_mapping(struct device_node *np)
{
	const char *bm;
	int ret, i;

	ret = of_property_read_string(np, "fsl,data-mapping", &bm);
	if (ret < 0)
		return ret;

	for (i = 0; i < ARRAY_SIZE(imx_ldb_bit_mappings); i++)
		if (!strcasecmp(bm, imx_ldb_bit_mappings[i]))
			return i;

	return -EINVAL;
}

static int imx_ldb_prepare(struct imx_ldb_channel *imx_ldb_ch, struct fb_videomode *mode,
		int di)
{
	struct imx_ldb *ldb = imx_ldb_ch->ldb;
	int dual = ldb->ldb_ctrl & LDB_SPLIT_MODE_EN;

	ldb->soc_data->prepare(imx_ldb_ch, di, PICOS2KHZ(mode->pixclock) * 1000UL);

	/* FIXME - assumes straight connections DI0 --> CH0, DI1 --> CH1 */
	if (imx_ldb_ch == &ldb->channel[0]) {
		if (mode->sync & FB_SYNC_VERT_HIGH_ACT)
			ldb->ldb_ctrl |= LDB_DI0_VS_POL_ACT_LOW;
		else
			ldb->ldb_ctrl &= ~LDB_DI0_VS_POL_ACT_LOW;
	}

	if (imx_ldb_ch == &ldb->channel[1]) {
		if (mode->sync & FB_SYNC_VERT_HIGH_ACT)
			ldb->ldb_ctrl |= LDB_DI1_VS_POL_ACT_LOW;
		else
			ldb->ldb_ctrl &= ~LDB_DI1_VS_POL_ACT_LOW;
	}

	if (imx_ldb_ch == &ldb->channel[0]) {
		ldb->ldb_ctrl &= ~LDB_CH0_MODE_EN_MASK;
		ldb->ldb_ctrl |= LDB_CH0_MODE_EN_TO_DI0;
		if (dual) {
			ldb->ldb_ctrl &= ~LDB_CH1_MODE_EN_MASK;
			ldb->ldb_ctrl |= LDB_CH1_MODE_EN_TO_DI0;
		}
	}

	if (imx_ldb_ch == &ldb->channel[1]) {
		ldb->ldb_ctrl &= ~LDB_CH1_MODE_EN_MASK;
		ldb->ldb_ctrl |= LDB_CH1_MODE_EN_TO_DI1;
	}

	writel(ldb->ldb_ctrl, ldb->base);

	return 0;
}

static int imx6q_set_clock(struct imx_ldb *ldb, int ipuno, int dino, int chno, unsigned long pixclk)
{
	struct clk *diclk, *ldbclk;
	char *clkname;
	int ret;

	clkname = basprintf("ipu%d_di%d_sel", ipuno, dino);
	diclk = clk_lookup(clkname);
	free(clkname);
	if (IS_ERR(diclk)) {
		dev_err(ldb->dev, "failed to get di clk: %pe\n", diclk);
		return PTR_ERR(diclk);
	}

	clkname = basprintf("ldb_di%d_podf", chno);
	ldbclk = clk_lookup(clkname);
	free(clkname);
	if (IS_ERR(ldbclk)) {
		dev_err(ldb->dev, "failed to get ldb clk: %pe\n", ldbclk);
		return PTR_ERR(ldbclk);
	}

	ret = clk_set_parent(diclk, ldbclk);
	if (ret) {
		dev_err(ldb->dev, "failed to set display clock parent: %s\n", strerror(-ret));
		return ret;
	}

	clk_set_rate(clk_get_parent(ldbclk), pixclk);
	clk_set_rate(ldbclk, pixclk);

	return 0;
}
static int imx6q_ldb_prepare(struct imx_ldb_channel *imx_ldb_ch, int di,
			     unsigned long pixclk)
{
	struct imx_ldb *ldb = imx_ldb_ch->ldb;
	int ipuno, dino;

	void __iomem *gpr3 = (void *)MX6_IOMUXC_BASE_ADDR + 0xc;
	uint32_t val;
	int shift;
	int dual = ldb->ldb_ctrl & LDB_SPLIT_MODE_EN;

	ipuno = ((di >> 1) & 1) + 1;
	dino = di & 0x1;

	if (!dual)
		pixclk *= 2;

	if (dual) {
		imx6q_set_clock(ldb, ipuno, dino, 0, pixclk);
		imx6q_set_clock(ldb, ipuno, dino, 1, pixclk);
	} else {
		imx6q_set_clock(ldb, ipuno, dino, imx_ldb_ch->chno, pixclk);
	}

	val = readl(gpr3);
	shift = (imx_ldb_ch->chno == 0) ? 6 : 8;
	val &= ~(3 << shift);
	val |= di << shift;
	writel(val, gpr3);

	return 0;
}

static int imx53_ldb_prepare(struct imx_ldb_channel *imx_ldb_ch, int di,
			     unsigned long pixclk)
{
	struct clk *diclk, *ldbclk;
	struct imx_ldb *ldb = imx_ldb_ch->ldb;
	int ret, dino;
	char *clkname;
	int dual = ldb->ldb_ctrl & LDB_SPLIT_MODE_EN;

	dino = di & 0x1;

	clkname = basprintf("ipu_di%d_sel", dino);
	diclk = clk_lookup(clkname);
	free(clkname);
	if (IS_ERR(diclk)) {
		dev_err(ldb->dev, "failed to get di clk: %pe\n", diclk);
		return PTR_ERR(diclk);
	}

	clkname = basprintf("ldb_di%d_div", imx_ldb_ch->chno);
	ldbclk = clk_lookup(clkname);
	free(clkname);
	if (IS_ERR(ldbclk)) {
		dev_err(ldb->dev, "failed to get ldb clk: %pe\n", ldbclk);
		return PTR_ERR(ldbclk);
	}

	ret = clk_set_parent(diclk, ldbclk);
	if (ret) {
		dev_err(ldb->dev, "failed to set display clock parent: %s\n", strerror(-ret));
		return ret;
	}

	if (!dual)
		pixclk *= 2;

	clk_set_rate(clk_get_parent(ldbclk), pixclk);

	return 0;
}

static struct imx_ldb_data imx_ldb_data_imx6q = {
	.base = (void *)MX6_IOMUXC_BASE_ADDR + 0x8,
	.prepare = imx6q_ldb_prepare,
	.ipu_mask = 0xf,
	.have_mux = 1,
};

static struct imx_ldb_data imx_ldb_data_imx53 = {
	.base = (void *)MX53_IOMUXC_BASE_ADDR + 0x8,
	.prepare = imx53_ldb_prepare,
	.ipu_mask = 0x3,
};

static int imx_ldb_ioctl(struct vpl *vpl, unsigned int port,
		unsigned int cmd, void *data)
{
	struct imx_ldb_channel *imx_ldb_ch = container_of(vpl,
			struct imx_ldb_channel, vpl);
	int ret;
	struct ipu_di_mode *mode;

	switch (cmd) {
	case VPL_ENABLE:
		break;
	case VPL_DISABLE:
		break;
	case VPL_PREPARE:
		ret = imx_ldb_prepare(imx_ldb_ch, data, port);
		if (ret)
			return ret;
		break;
	case IMX_IPU_VPL_DI_MODE:
		mode = data;

		mode->di_clkflags = IPU_DI_CLKMODE_EXT | IPU_DI_CLKMODE_SYNC;
		mode->bus_format = (imx_ldb_ch->datawidth == 24) ?
			MEDIA_BUS_FMT_RGB888_1X24 : MEDIA_BUS_FMT_RGB666_1X18;

		return 0;
	case VPL_GET_VIDEOMODES:
		if (imx_ldb_ch->modes) {
			struct display_timings *timings = data;
			timings->num_modes = imx_ldb_ch->modes->num_modes;
			timings->modes = imx_ldb_ch->modes->modes;
			dev_dbg(imx_ldb_ch->ldb->dev, "Using ldb provided timings\n");
			return 0;
		}

		break;
	}

	if (imx_ldb_ch->output_port > 0)
		return vpl_ioctl(vpl, imx_ldb_ch->output_port, cmd, data);

	return 0;
}

static int imx_ldb_probe(struct device_d *dev)
{
	struct device_node *np = dev->device_node;
	struct device_node *child;
	struct imx_ldb *imx_ldb;
	int ret, i;
	int dual = 0;
	int mapping;
	const struct imx_ldb_data *devtype;

	ret = dev_get_drvdata(dev, (const void **)&devtype);
	if (ret)
		return ret;

	imx_ldb = xzalloc(sizeof(*imx_ldb));
	imx_ldb->base = devtype->base;
	imx_ldb->soc_data = devtype;

	dual = of_property_read_bool(np, "fsl,dual-channel");
	if (dual)
		imx_ldb->ldb_ctrl |= LDB_SPLIT_MODE_EN;

	for_each_child_of_node(np, child) {
		struct imx_ldb_channel *channel;
		struct device_node *port;

		ret = of_property_read_u32(child, "reg", &i);
		if (ret || i < 0 || i > 1)
			return -EINVAL;

		if (dual && i > 0) {
			dev_info(dev, "dual-channel mode, ignoring second output\n");
			continue;
		}

		if (!of_device_is_available(child))
			continue;

		channel = &imx_ldb->channel[i];
		channel->ldb = imx_ldb;
		channel->chno = i;
		channel->output_port = imx_ldb->soc_data->have_mux ? 4 : 1;

		channel->modes = of_get_display_timings(child);

		/* The output port is port@4 with mux or port@1 without mux */
		port = of_graph_get_port_by_id(child, channel->output_port);
		if (!port)
			channel->output_port = -1;

		if (!channel->modes && !port) {
			dev_err(dev, "Neither display timings in ldb node nor remote panel found\n");
			continue;
		}

		channel->vpl.node = child;
		channel->vpl.ioctl = &imx_ldb_ioctl;
		ret = vpl_register(&channel->vpl);
		if (ret)
			return ret;

		ret = of_property_read_u32(child, "fsl,data-width", &channel->datawidth);
		if (ret)
			channel->datawidth = 0;
		else if (channel->datawidth != 18 && channel->datawidth != 24)
			return -EINVAL;

		mapping = of_get_data_mapping(child);
		switch (mapping) {
		case LVDS_BIT_MAP_SPWG:
			if (channel->datawidth == 24) {
				if (i == 0 || dual)
					imx_ldb->ldb_ctrl |= LDB_DATA_WIDTH_CH0_24;
				if (i == 1 || dual)
					imx_ldb->ldb_ctrl |= LDB_DATA_WIDTH_CH1_24;
			}
			break;
		case LVDS_BIT_MAP_JEIDA:
			if (channel->datawidth == 18) {
				dev_err(dev, "JEIDA standard only supported in 24 bit\n");
				return -EINVAL;
			}
			if (i == 0 || dual)
				imx_ldb->ldb_ctrl |= LDB_DATA_WIDTH_CH0_24 | LDB_BIT_MAP_CH0_JEIDA;
			if (i == 1 || dual)
				imx_ldb->ldb_ctrl |= LDB_DATA_WIDTH_CH1_24 | LDB_BIT_MAP_CH1_JEIDA;
			break;
		default:
			dev_err(dev, "data mapping not specified or invalid\n");
			return -EINVAL;
		}
	}

	return 0;
}

static struct of_device_id imx_ldb_dt_ids[] = {
	{ .compatible = "fsl,imx6q-ldb", &imx_ldb_data_imx6q},
	{ .compatible = "fsl,imx53-ldb", &imx_ldb_data_imx53},
	{ /* sentinel */ }
};

static struct driver_d imx_ldb_driver = {
	.probe		= imx_ldb_probe,
	.of_compatible	= imx_ldb_dt_ids,
	.name		= "imx-ldb",
};
device_platform_driver(imx_ldb_driver);

MODULE_DESCRIPTION("i.MX LVDS display driver");
MODULE_AUTHOR("Sascha Hauer, Pengutronix");
MODULE_LICENSE("GPL");
