// SPDX-License-Identifier: GPL-2.0+
/*
 * (C) Copyright 2017
 * Mario Six, Guntermann & Drunck GmbH, mario.six@gdsys.cc
 *
 * based on the gdsys osd driver, which is
 *
 * (C) Copyright 2010
 * Dirk Eibach, Guntermann & Drunck GmbH, dirk.eibach@gdsys.de
 */

#include <common.h>
#include <display.h>
#include <dm.h>
#include <log.h>
#include <regmap.h>
#include <video_osd.h>
#include <asm/gpio.h>

static const uint MAX_X_CHARS = 53;
static const uint MAX_Y_CHARS = 26;
static const uint MAX_VIDEOMEM_WIDTH = 64;
static const uint MAX_VIDEOMEM_HEIGHT = 32;
static const uint CHAR_WIDTH = 12;
static const uint CHAR_HEIGHT = 18;

static const u16 BASE_WIDTH_MASK = 0x3f00;
static const uint BASE_WIDTH_SHIFT = 8;
static const u16 BASE_HEIGTH_MASK = 0x001f;
static const uint BASE_HEIGTH_SHIFT;

struct ihs_video_out_regs {
	/* Device version register */
	u16 versions;
	/* Device feature register */
	u16 features;
	/* Device control register */
	u16 control;
	/* Register controlling screen size */
	u16 xy_size;
	/* Register controlling screen scaling */
	u16 xy_scale;
	/* Register controlling screen x position */
	u16 x_pos;
	/* Register controlling screen y position */
	u16 y_pos;
};

#define ihs_video_out_set(map, member, val) \
	regmap_range_set(map, 1, struct ihs_video_out_regs, member, val)

#define ihs_video_out_get(map, member, valp) \
	regmap_range_get(map, 1, struct ihs_video_out_regs, member, valp)

enum {
	CONTROL_FILTER_BLACK = (0 << 0),
	CONTROL_FILTER_ORIGINAL = (1 << 0),
	CONTROL_FILTER_DARKER = (2 << 0),
	CONTROL_FILTER_GRAY = (3 << 0),

	CONTROL_MODE_PASSTHROUGH = (0 << 3),
	CONTROL_MODE_OSD = (1 << 3),
	CONTROL_MODE_AUTO = (2 << 3),
	CONTROL_MODE_OFF = (3 << 3),

	CONTROL_ENABLE_OFF = (0 << 6),
	CONTROL_ENABLE_ON = (1 << 6),
};

struct ihs_video_out_priv {
	/* Register map for OSD device */
	struct regmap *map;
	/* Pointer to video memory */
	u16 *vidmem;
	/* Display width in text columns */
	uint base_width;
	/* Display height in text rows */
	uint base_height;
	/* x-resolution of the display in pixels */
	uint res_x;
	/* y-resolution of the display in pixels */
	uint res_y;
	/* OSD's sync mode (resolution + frequency) */
	int sync_src;
	/* The display port output for this OSD */
	struct udevice *video_tx;
	/* The pixel clock generator for the display */
	struct udevice *clk_gen;
};

static const struct udevice_id ihs_video_out_ids[] = {
	{ .compatible = "gdsys,ihs_video_out" },
	{ }
};

/**
 * set_control() - Set the control register to a given value
 *
 * The current value of sync_src is preserved by the function automatically.
 *
 * @dev: the OSD device whose control register to set
 * @value: the 16-bit value to write to the control register
 * Return: 0
 */
static int set_control(struct udevice *dev, u16 value)
{
	struct ihs_video_out_priv *priv = dev_get_priv(dev);

	if (priv->sync_src)
		value |= ((priv->sync_src & 0x7) << 8);

	ihs_video_out_set(priv->map, control, value);

	return 0;
}

int ihs_video_out_get_info(struct udevice *dev, struct video_osd_info *info)
{
	struct ihs_video_out_priv *priv = dev_get_priv(dev);
	u16 versions;

	ihs_video_out_get(priv->map, versions, &versions);

	info->width = priv->base_width;
	info->height = priv->base_height;
	info->major_version = versions / 100;
	info->minor_version = versions % 100;

	return 0;
}

int ihs_video_out_set_mem(struct udevice *dev, uint col, uint row, u8 *buf,
			  size_t buflen, uint count)
{
	struct ihs_video_out_priv *priv = dev_get_priv(dev);
	int res;
	uint offset;
	uint k, rep;
	u16 data;

	/* Repetitions (controlled via count parmeter) */
	for (rep = 0; rep < count; ++rep) {
		offset = row * priv->base_width + col + rep * (buflen / 2);

		/* Write a single buffer copy */
		for (k = 0; k < buflen / 2; ++k) {
			uint max_size = priv->base_width * priv->base_height;

			if (offset + k >= max_size) {
				debug("%s: Write would be out of OSD bounds\n",
				      dev->name);
				return -E2BIG;
			}

			data = buf[2 * k + 1] + 256 * buf[2 * k];
			out_le16(priv->vidmem + offset + k, data);
		}
	}

	res = set_control(dev, CONTROL_FILTER_ORIGINAL |
			       CONTROL_MODE_OSD |
			       CONTROL_ENABLE_ON);
	if (res) {
		debug("%s: Could not set control register\n", dev->name);
		return res;
	}

	return 0;
}

/**
 * div2_u16() - Approximately divide a 16-bit number by 2
 *
 * @val: The 16-bit value to divide by two
 * Return: The approximate division of val by two
 */
static inline u16 div2_u16(u16 val)
{
	return (32767 * val) / 65535;
}

int ihs_video_out_set_size(struct udevice *dev, uint col, uint row)
{
	struct ihs_video_out_priv *priv = dev_get_priv(dev);

	if (!col || col > MAX_VIDEOMEM_WIDTH || col > MAX_X_CHARS ||
	    !row || row > MAX_VIDEOMEM_HEIGHT || row > MAX_Y_CHARS) {
		debug("%s: Desired OSD size invalid\n", dev->name);
		return -EINVAL;
	}

	ihs_video_out_set(priv->map, xy_size, ((col - 1) << 8) | (row - 1));
	/* Center OSD on screen */
	ihs_video_out_set(priv->map, x_pos,
			  div2_u16(priv->res_x - CHAR_WIDTH * col));
	ihs_video_out_set(priv->map, y_pos,
			  div2_u16(priv->res_y - CHAR_HEIGHT * row));

	return 0;
}

int ihs_video_out_print(struct udevice *dev, uint col, uint row, ulong color,
			char *text)
{
	int res;
	u8 buffer[2 * MAX_VIDEOMEM_WIDTH];
	uint k;
	uint charcount = strlen(text);
	uint len = min(charcount, 2 * MAX_VIDEOMEM_WIDTH);

	for (k = 0; k < len; ++k) {
		buffer[2 * k] = text[k];
		buffer[2 * k + 1] = color;
	}

	res = ihs_video_out_set_mem(dev, col, row, buffer, 2 * len, 1);
	if (res < 0) {
		debug("%s: Could not write to video memory\n", dev->name);
		return res;
	}

	return 0;
}

static const struct video_osd_ops ihs_video_out_ops = {
	.get_info = ihs_video_out_get_info,
	.set_mem = ihs_video_out_set_mem,
	.set_size = ihs_video_out_set_size,
	.print = ihs_video_out_print,
};

int ihs_video_out_probe(struct udevice *dev)
{
	struct ihs_video_out_priv *priv = dev_get_priv(dev);
	struct ofnode_phandle_args phandle_args;
	const char *mode;
	u16 features;
	struct display_timing timing;
	int res;

	res = regmap_init_mem(dev_ofnode(dev), &priv->map);
	if (res) {
		debug("%s: Could not initialize regmap (err = %d)\n", dev->name,
		      res);
		return res;
	}

	/* Range with index 2 is video memory */
	priv->vidmem = regmap_get_range(priv->map, 2);

	mode = dev_read_string(dev, "mode");
	if (!mode) {
		debug("%s: Could not read mode property\n", dev->name);
		return -EINVAL;
	}

	if (!strcmp(mode, "1024_768_60")) {
		priv->sync_src = 2;
		priv->res_x = 1024;
		priv->res_y = 768;
		timing.hactive.typ = 1024;
		timing.vactive.typ = 768;
	} else if (!strcmp(mode, "720_400_70")) {
		priv->sync_src = 1;
		priv->res_x = 720;
		priv->res_y = 400;
		timing.hactive.typ = 720;
		timing.vactive.typ = 400;
	} else {
		priv->sync_src = 0;
		priv->res_x = 640;
		priv->res_y = 480;
		timing.hactive.typ = 640;
		timing.vactive.typ = 480;
	}

	ihs_video_out_get(priv->map, features, &features);

	res = set_control(dev, CONTROL_FILTER_ORIGINAL |
			       CONTROL_MODE_OSD |
			       CONTROL_ENABLE_OFF);
	if (res) {
		debug("%s: Could not set control register (err = %d)\n",
		      dev->name, res);
		return res;
	}

	priv->base_width = ((features & BASE_WIDTH_MASK)
			    >> BASE_WIDTH_SHIFT) + 1;
	priv->base_height = ((features & BASE_HEIGTH_MASK)
			     >> BASE_HEIGTH_SHIFT) + 1;

	res = dev_read_phandle_with_args(dev, "clk_gen", NULL, 0, 0,
					 &phandle_args);
	if (res) {
		debug("%s: Could not get clk_gen node (err = %d)\n",
		      dev->name, res);
		return -EINVAL;
	}

	res = uclass_get_device_by_ofnode(UCLASS_CLK, phandle_args.node,
					  &priv->clk_gen);
	if (res) {
		debug("%s: Could not get clk_gen dev (err = %d)\n",
		      dev->name, res);
		return -EINVAL;
	}

	res = dev_read_phandle_with_args(dev, "video_tx", NULL, 0, 0,
					 &phandle_args);
	if (res) {
		debug("%s: Could not get video_tx (err = %d)\n",
		      dev->name, res);
		return -EINVAL;
	}

	res = uclass_get_device_by_ofnode(UCLASS_DISPLAY, phandle_args.node,
					  &priv->video_tx);
	if (res) {
		debug("%s: Could not get video_tx dev (err = %d)\n",
		      dev->name, res);
		return -EINVAL;
	}

	res = display_enable(priv->video_tx, 8, &timing);
	if (res && res != -EIO) { /* Ignore missing DP sink error */
		debug("%s: Could not enable the display (err = %d)\n",
		      dev->name, res);
		return res;
	}

	return 0;
}

U_BOOT_DRIVER(ihs_video_out_drv) = {
	.name           = "ihs_video_out_drv",
	.id             = UCLASS_VIDEO_OSD,
	.ops		= &ihs_video_out_ops,
	.of_match       = ihs_video_out_ids,
	.probe          = ihs_video_out_probe,
	.priv_auto	= sizeof(struct ihs_video_out_priv),
};
