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

#include <console/console.h>
#include <device/device.h>
#include <device/i2c_bus.h>
#include <types.h>
#include <bootstate.h>

#include "ptn3460.h"

/**
 * \brief  This function selects one of 7 EDID-tables inside PTN3460
 *         which should be emulated on display port and turn emulation ON
 * @param  *dev		Pointer to the relevant I2C controller
 * @param  edid_num	Number of EDID to emulate (0..6)
 * @return PTN_SUCCESS or error code
 */
static int ptn_select_edid(struct device *dev, uint8_t edid_num)
{
	int status = 0;
	u8 val;

	if (edid_num > PTN_MAX_EDID_NUM)
		return PTN_INVALID_EDID;
	val = (edid_num << 1) | PTN_ENABLE_EMULATION;
	status = i2c_dev_writeb_at(dev, PTN_CONFIG_OFF + 4, val);
	return status ? (PTN_BUS_ERROR | status) : PTN_SUCCESS;
}

/**
 * \brief This function writes one EDID data structure to PTN3460
 * @param  *dev      Pointer to the relevant I2C controller
 * @param  edid_num  Number of EDID that must be written (0..6)
 * @param  *data     Pointer to a buffer where data to write is stored in
 * @return           PTN_SUCCESS on success or error code
 */
static int ptn3460_write_edid(struct device *dev, u8 edid_num, u8 *data)
{
	int status;
	int i;

	if (edid_num > PTN_MAX_EDID_NUM)
		return PTN_INVALID_EDID;

	/* First enable access to the desired EDID table */
	status = i2c_dev_writeb_at(dev, PTN_CONFIG_OFF + 5, edid_num);
	if (status)
		return (PTN_BUS_ERROR | status);

	/* Now we can simply write EDID data to ptn3460 */
	for (i = 0; i < PTN_EDID_LEN; i++) {
		status = i2c_dev_writeb_at(dev, PTN_EDID_OFF + i, data[i]);
		if (status)
			return (PTN_BUS_ERROR | status);
	}
	return PTN_SUCCESS;
}

/**
 * \brief This function sets up the DP2LVDS-converter to be used with the
 *         appropriate EDID data
 * @param  *dev	Pointer to the I2C controller where PTN3460 is attached
 */
static void ptn3460_init(struct device *dev)
{
	struct ptn_3460_config cfg;
	uint8_t edid_data[PTN_EDID_LEN], edid_tab, *ptr = (uint8_t *)&cfg;
	int i, val;

	/* Guard against re-initialization of the device */
	static bool init_done = false;

	if (init_done) {
		printk(BIOS_DEBUG, "Skipping PTN3460 init as it's already initialized\n");
		return;
	}

	/* Mainboard provides EDID data. */
	if (mainboard_ptn3460_get_edid(edid_data) != CB_SUCCESS) {
		printk(BIOS_ERR, "PTN3460 error: Unable to get EDID data from mainboard.\n");
		return;
	}

	/* Mainboard decides which EDID table has to be used. */
	edid_tab = mainboard_ptn3460_select_edid_table();
	if (edid_tab > PTN_MAX_EDID_NUM) {
		printk(BIOS_ERR, "PTN3460 error: invalid EDID table (%d) selected.\n",
		       edid_tab);
		return;
	}
	/* Write EDID data into PTN. */
	val = ptn3460_write_edid(dev, edid_tab, edid_data);
	if (val != PTN_SUCCESS) {
		printk(BIOS_ERR, "PTN3460 error: writing EDID data into device failed.\n");
		return;
	}
	/* Activate the selected EDID block. */
	ptn_select_edid(dev, edid_tab);
	/* Read out PTN configuration data. */
	for (i = 0; i < sizeof(struct ptn_3460_config); i++) {
		val = i2c_dev_readb_at(dev, PTN_CONFIG_OFF + i);
		if (val < 0) {
			printk(BIOS_ERR,
			       "PTN3460 error: Unable to read config data from device.\n");
			return;
		}
		*ptr++ = (uint8_t)val; /* fill config structure via ptr */
	}
	/* Mainboard can modify the configuration data.
	   Write back configuration data to PTN3460 if modified by mainboard */
	if (mainboard_ptn3460_config(&cfg) == CB_SUCCESS) {
		ptr = (uint8_t *)&cfg;
		for (i = 0; i < sizeof(struct ptn_3460_config); i++) {
			val = i2c_dev_writeb_at(dev, PTN_CONFIG_OFF + i, *ptr++);
			if (val < 0) {
				printk(BIOS_ERR,
				       "PTN3460 error: Unable to write config data.\n");
				return;
			}
		}
	}

	init_done = true;
}

__weak enum cb_err mainboard_ptn3460_get_edid(uint8_t edid_data[PTN_EDID_LEN])
{
	return CB_ERR;
}
__weak uint8_t mainboard_ptn3460_select_edid_table(void)
{
	return 0;
}
__weak enum cb_err mainboard_ptn3460_config(struct ptn_3460_config *cfg_ptr)
{
	return CB_ERR;
}

static struct device_operations ptn3460_ops = {
	.read_resources		= noop_read_resources,
	.set_resources		= noop_set_resources,
	.init			= ptn3460_init,
};

static void ptn3460_enable(struct device *dev)
{
	dev->ops = &ptn3460_ops;
}

struct chip_operations drivers_i2c_ptn3460_ops = {
	CHIP_NAME("PTN3460")
	.enable_dev = ptn3460_enable
};

#if CONFIG(PTN3460_EARLY_INIT)

/**
 * \brief This function provides a callback for the boot state machine to initialize the
 *        PTN3460 DP-to-LVDS bridge before graphics initialization in order for the bootsplash
 *        logo to be shown.
 * @param  *unused	Unused argument for the callback.
 */

static void ptn3460_early_init(void *unused)
{
	struct device *ptn_dev;

	printk(BIOS_DEBUG, "Attempting PTN3460 early init.\n");
	ptn_dev = dev_find_slot_on_smbus(0, CONFIG_PTN3460_EARLY_ADDR);
	if (!ptn_dev) {
		printk(BIOS_ERR, "Failed to find the PTN3460 device!\n");
		return;
	}
	/* Initialize the I2C controller before it is used. */
	if (ptn_dev->bus && ptn_dev->bus->dev->ops && ptn_dev->bus->dev->ops->init)
		ptn_dev->bus->dev->ops->init(ptn_dev->bus->dev);
	ptn3460_init(ptn_dev);
}

BOOT_STATE_INIT_ENTRY(BS_DEV_INIT, BS_ON_ENTRY, ptn3460_early_init, NULL);

#endif /* CONFIG(PTN3460_EARLY_INIT) */
