/* SPDX-License-Identifier: GPL-2.0+ */
/*
 * Copyright (C) 2010-2016 Freescale Semiconductor, Inc. All Rights Reserved.
 * Copyright 2023 NXP
 */
#include <dm.h>
#include <dm/device_compat.h>
#include <common.h>
#include <video.h>
#include <linux/list.h>
#include <linux/err.h>
#include <linux/types.h>
#include <malloc.h>

#include <mxc_epdc_fb.h>
#include <cpu_func.h>
#include <asm/cache.h>
#include <asm/global_data.h>
#include <linux/delay.h>
#include <asm/mach-imx/sys_proto.h>

DECLARE_GLOBAL_DATA_PTR;

void *lcd_base;			/* Start of framebuffer memory	*/
void *lcd_console_address;	/* Start of console buffer	*/

int lcd_color_fg;
int lcd_color_bg;

short console_col;
short console_row;

int rev;

void lcd_initcolregs(void)
{
}

void lcd_setcolreg(ushort regno, ushort red, ushort green, ushort blue)
{
}

#define TEMP_USE_DEFAULT 8

#define UPDATE_MODE_PARTIAL			0x0
#define UPDATE_MODE_FULL			0x1

#define TRUE 1
#define FALSE 0

#define msleep(a)	udelay(a * 1000)


/********************************************************
 * Start Low-Level EPDC Functions
 ********************************************************/

static inline void epdc_set_screen_res(u32 width, u32 height)
{
	u32 val = (height << EPDC_RES_VERTICAL_OFFSET) | width;

	REG_WR(EPDC_BASE, EPDC_RES, val);
}

static inline void epdc_set_update_coord(u32 x, u32 y)
{
	u32 val = (y << EPDC_UPD_CORD_YCORD_OFFSET) | x;

	REG_WR(EPDC_BASE, EPDC_UPD_CORD, val);
}

static inline void epdc_set_update_dimensions(u32 width, u32 height)
{
	u32 val = (height << EPDC_UPD_SIZE_HEIGHT_OFFSET) | width;

	REG_WR(EPDC_BASE, EPDC_UPD_SIZE, val);
}

static void epdc_submit_update(u32 lut_num, u32 waveform_mode, u32 update_mode,
			       int use_test_mode, u32 np_val)
{
	u32 reg_val = 0;

	if (use_test_mode) {
		reg_val |=
			((np_val << EPDC_UPD_FIXED_FIXNP_OFFSET) &
			EPDC_UPD_FIXED_FIXNP_MASK) | EPDC_UPD_FIXED_FIXNP_EN;

		REG_WR(EPDC_BASE, EPDC_UPD_FIXED, reg_val);

		reg_val = EPDC_UPD_CTRL_USE_FIXED;
	} else {
		REG_WR(EPDC_BASE, EPDC_UPD_FIXED, reg_val);
	}

	reg_val |=
		((lut_num << EPDC_UPD_CTRL_LUT_SEL_OFFSET) &
		EPDC_UPD_CTRL_LUT_SEL_MASK) |
		((waveform_mode << EPDC_UPD_CTRL_WAVEFORM_MODE_OFFSET) &
		EPDC_UPD_CTRL_WAVEFORM_MODE_MASK) |
		update_mode;

	REG_WR(EPDC_BASE, EPDC_UPD_CTRL, reg_val);
}

static inline int epdc_is_lut_active(u32 lut_num)
{
	u32 val = REG_RD(EPDC_BASE, EPDC_STATUS_LUTS);
	int is_active = val & (1 << lut_num) ? TRUE : FALSE;

	return is_active;
}

static void epdc_set_horizontal_timing(u32 horiz_start, u32 horiz_end,
				       u32 hsync_width, u32 hsync_line_length)
{
	u32 reg_val =
		((hsync_width << EPDC_TCE_HSCAN1_LINE_SYNC_WIDTH_OFFSET) &
		EPDC_TCE_HSCAN1_LINE_SYNC_WIDTH_MASK)
		| ((hsync_line_length << EPDC_TCE_HSCAN1_LINE_SYNC_OFFSET) &
		EPDC_TCE_HSCAN1_LINE_SYNC_MASK);
	REG_WR(EPDC_BASE, EPDC_TCE_HSCAN1, reg_val);

	reg_val =
		((horiz_start << EPDC_TCE_HSCAN2_LINE_BEGIN_OFFSET) &
		EPDC_TCE_HSCAN2_LINE_BEGIN_MASK)
		| ((horiz_end << EPDC_TCE_HSCAN2_LINE_END_OFFSET) &
		EPDC_TCE_HSCAN2_LINE_END_MASK);
	REG_WR(EPDC_BASE, EPDC_TCE_HSCAN2, reg_val);
}

static void epdc_set_vertical_timing(u32 vert_start, u32 vert_end,
					u32 vsync_width)
{
	u32 reg_val =
		((vert_start << EPDC_TCE_VSCAN_FRAME_BEGIN_OFFSET) &
		EPDC_TCE_VSCAN_FRAME_BEGIN_MASK)
		| ((vert_end << EPDC_TCE_VSCAN_FRAME_END_OFFSET) &
		EPDC_TCE_VSCAN_FRAME_END_MASK)
		| ((vsync_width << EPDC_TCE_VSCAN_FRAME_SYNC_OFFSET) &
		EPDC_TCE_VSCAN_FRAME_SYNC_MASK);
	REG_WR(EPDC_BASE, EPDC_TCE_VSCAN, reg_val);
}

static void epdc_init_settings(void)
{
	u32 reg_val;
	int num_ce;

	/* EPDC_CTRL */
	reg_val = REG_RD(EPDC_BASE, EPDC_CTRL);
	reg_val &= ~EPDC_CTRL_UPD_DATA_SWIZZLE_MASK;
	reg_val |= EPDC_CTRL_UPD_DATA_SWIZZLE_NO_SWAP;
	reg_val &= ~EPDC_CTRL_LUT_DATA_SWIZZLE_MASK;
	reg_val |= EPDC_CTRL_LUT_DATA_SWIZZLE_NO_SWAP;
	REG_SET(EPDC_BASE, EPDC_CTRL, reg_val);

	/* EPDC_FORMAT - 2bit TFT and 4bit Buf pixel format */
	reg_val = EPDC_FORMAT_TFT_PIXEL_FORMAT_2BIT
		| EPDC_FORMAT_BUF_PIXEL_FORMAT_P4N
		| ((0x0 << EPDC_FORMAT_DEFAULT_TFT_PIXEL_OFFSET) &
		EPDC_FORMAT_DEFAULT_TFT_PIXEL_MASK);
	REG_WR(EPDC_BASE, EPDC_FORMAT, reg_val);

	/* EPDC_FIFOCTRL (disabled) */
	reg_val =
		((100 << EPDC_FIFOCTRL_FIFO_INIT_LEVEL_OFFSET) &
		EPDC_FIFOCTRL_FIFO_INIT_LEVEL_MASK)
		| ((200 << EPDC_FIFOCTRL_FIFO_H_LEVEL_OFFSET) &
		EPDC_FIFOCTRL_FIFO_H_LEVEL_MASK)
		| ((100 << EPDC_FIFOCTRL_FIFO_L_LEVEL_OFFSET) &
		EPDC_FIFOCTRL_FIFO_L_LEVEL_MASK);
	REG_WR(EPDC_BASE, EPDC_FIFOCTRL, reg_val);

	/* EPDC_TEMP - Use default temperature */
	REG_WR(EPDC_BASE, EPDC_TEMP, TEMP_USE_DEFAULT);

	/* EPDC_RES */
	epdc_set_screen_res(panel_info.vl_col, panel_info.vl_row);

	/*
	 * EPDC_TCE_CTRL
	 * VSCAN_HOLDOFF = 4
	 * VCOM_MODE = MANUAL
	 * VCOM_VAL = 0
	 * DDR_MODE = DISABLED
	 * LVDS_MODE_CE = DISABLED
	 * LVDS_MODE = DISABLED
	 * DUAL_SCAN = DISABLED
	 * SDDO_WIDTH = 8bit
	 * PIXELS_PER_SDCLK = 4
	 */
	reg_val =
		((panel_info.epdc_data.epdc_timings.vscan_holdoff <<
			EPDC_TCE_CTRL_VSCAN_HOLDOFF_OFFSET) &
			EPDC_TCE_CTRL_VSCAN_HOLDOFF_MASK)
		| EPDC_TCE_CTRL_PIXELS_PER_SDCLK_4;
	REG_WR(EPDC_BASE, EPDC_TCE_CTRL, reg_val);

	/* EPDC_TCE_HSCAN */
	epdc_set_horizontal_timing(panel_info.vl_left_margin,
				panel_info.vl_right_margin,
				panel_info.vl_hsync,
				panel_info.vl_hsync);

	/* EPDC_TCE_VSCAN */
	epdc_set_vertical_timing(panel_info.vl_upper_margin,
				 panel_info.vl_lower_margin,
				 panel_info.vl_vsync);

	/* EPDC_TCE_OE */
	reg_val =
		((panel_info.epdc_data.epdc_timings.sdoed_width <<
			EPDC_TCE_OE_SDOED_WIDTH_OFFSET) &
			EPDC_TCE_OE_SDOED_WIDTH_MASK)
		| ((panel_info.epdc_data.epdc_timings.sdoed_delay <<
			EPDC_TCE_OE_SDOED_DLY_OFFSET) &
			EPDC_TCE_OE_SDOED_DLY_MASK)
		| ((panel_info.epdc_data.epdc_timings.sdoez_width <<
			EPDC_TCE_OE_SDOEZ_WIDTH_OFFSET) &
			EPDC_TCE_OE_SDOEZ_WIDTH_MASK)
		| ((panel_info.epdc_data.epdc_timings.sdoez_delay <<
			EPDC_TCE_OE_SDOEZ_DLY_OFFSET) &
			EPDC_TCE_OE_SDOEZ_DLY_MASK);
	REG_WR(EPDC_BASE, EPDC_TCE_OE, reg_val);

	/* EPDC_TCE_TIMING1 */
	REG_WR(EPDC_BASE, EPDC_TCE_TIMING1, 0x0);

	/* EPDC_TCE_TIMING2 */
	reg_val =
		((panel_info.epdc_data.epdc_timings.gdclk_hp_offs <<
			EPDC_TCE_TIMING2_GDCLK_HP_OFFSET) &
			EPDC_TCE_TIMING2_GDCLK_HP_MASK)
		| ((panel_info.epdc_data.epdc_timings.gdsp_offs <<
			EPDC_TCE_TIMING2_GDSP_OFFSET_OFFSET) &
			EPDC_TCE_TIMING2_GDSP_OFFSET_MASK);
	REG_WR(EPDC_BASE, EPDC_TCE_TIMING2, reg_val);

	/* EPDC_TCE_TIMING3 */
	reg_val =
		((panel_info.epdc_data.epdc_timings.gdoe_offs <<
			EPDC_TCE_TIMING3_GDOE_OFFSET_OFFSET) &
			EPDC_TCE_TIMING3_GDOE_OFFSET_MASK)
		| ((panel_info.epdc_data.epdc_timings.gdclk_offs <<
			EPDC_TCE_TIMING3_GDCLK_OFFSET_OFFSET) &
			EPDC_TCE_TIMING3_GDCLK_OFFSET_MASK);
	REG_WR(EPDC_BASE, EPDC_TCE_TIMING3, reg_val);

	/*
	 * EPDC_TCE_SDCFG
	 * SDCLK_HOLD = 1
	 * SDSHR = 1
	 * NUM_CE = 1
	 * SDDO_REFORMAT = FLIP_PIXELS
	 * SDDO_INVERT = DISABLED
	 * PIXELS_PER_CE = display horizontal resolution
	 */
	num_ce = panel_info.epdc_data.epdc_timings.num_ce;
	if (num_ce == 0)
		num_ce = 1;
	reg_val = EPDC_TCE_SDCFG_SDCLK_HOLD | EPDC_TCE_SDCFG_SDSHR
		| ((num_ce << EPDC_TCE_SDCFG_NUM_CE_OFFSET) & EPDC_TCE_SDCFG_NUM_CE_MASK)
		| EPDC_TCE_SDCFG_SDDO_REFORMAT_FLIP_PIXELS
		| ((panel_info.vl_col << EPDC_TCE_SDCFG_PIXELS_PER_CE_OFFSET) &
		EPDC_TCE_SDCFG_PIXELS_PER_CE_MASK);
	REG_WR(EPDC_BASE, EPDC_TCE_SDCFG, reg_val);

	/*
	 * EPDC_TCE_GDCFG
	 * GDRL = 1
	 * GDOE_MODE = 0;
	 * GDSP_MODE = 0;
	 */
	reg_val = EPDC_TCE_SDCFG_GDRL;
	REG_WR(EPDC_BASE, EPDC_TCE_GDCFG, reg_val);

	/*
	 * EPDC_TCE_POLARITY
	 * SDCE_POL = ACTIVE LOW
	 * SDLE_POL = ACTIVE HIGH
	 * SDOE_POL = ACTIVE HIGH
	 * GDOE_POL = ACTIVE HIGH
	 * GDSP_POL = ACTIVE LOW
	 */
	reg_val = EPDC_TCE_POLARITY_SDLE_POL_ACTIVE_HIGH
		| EPDC_TCE_POLARITY_SDOE_POL_ACTIVE_HIGH
		| EPDC_TCE_POLARITY_GDOE_POL_ACTIVE_HIGH;
	REG_WR(EPDC_BASE, EPDC_TCE_POLARITY, reg_val);

	/* EPDC_IRQ_MASK */
	REG_WR(EPDC_BASE, EPDC_IRQ_MASK,
		EPDC_IRQ_TCE_UNDERRUN_IRQ);

	/*
	 * EPDC_GPIO
	 * PWRCOM = ?
	 * PWRCTRL = ?
	 * BDR = ?
	 */
	reg_val = ((0 << EPDC_GPIO_PWRCTRL_OFFSET) & EPDC_GPIO_PWRCTRL_MASK)
		| ((0 << EPDC_GPIO_BDR_OFFSET) & EPDC_GPIO_BDR_MASK);
	REG_WR(EPDC_BASE, EPDC_GPIO, reg_val);
}

static void draw_mode0(void)
{
	int i;

	/* Program EPDC update to process buffer */
	epdc_set_update_coord(0, 0);
	epdc_set_update_dimensions(panel_info.vl_col, panel_info.vl_row);
	epdc_submit_update(0, panel_info.epdc_data.wv_modes.mode_init,
				UPDATE_MODE_FULL, FALSE, 0);

	debug("Mode0 update - Waiting for LUT to complete...\n");

	/* Will timeout after ~4-5 seconds */

	for (i = 0; i < 40; i++) {
		if (!epdc_is_lut_active(0)) {
			debug("Mode0 init complete\n");
			return;
		}
		msleep(100);
	}

	debug("Mode0 init failed!\n");

}

static void draw_splash_screen(void)
{
	int i;
	int lut_num = 0;

	/* Program EPDC update to process buffer */
	epdc_set_update_coord(0, 0);
	epdc_set_update_dimensions(panel_info.vl_col, panel_info.vl_row);
	epdc_submit_update(lut_num, panel_info.epdc_data.wv_modes.mode_gc16,
		UPDATE_MODE_FULL, FALSE, 0);

	for (i = 0; i < 40; i++) {
		if (!epdc_is_lut_active(lut_num)) {
			debug("Splash screen update complete\n");
			return;
		}
		msleep(100);
	}
	debug("Splash screen update failed!\n");
}

void epdc_enable(void)
{
	if (CONFIG_IS_ENABLED(IMX_MODULE_FUSE)) {
		if (check_module_fused(MODULE_EPDC)) {
			return;
		}
	}

	if (board_setup_logo_file(lcd_base)) {
		debug("Load logo failed!\n");
		return;
	}

	epdc_power_on();

	flush_cache((ulong)lcd_base, panel_info.vl_col * panel_info.vl_row);

	/* Draw data to display */
	draw_mode0();

	draw_splash_screen();
}

void epdc_disable(void)
{
	if (CONFIG_IS_ENABLED(IMX_MODULE_FUSE)) {
		if (check_module_fused(MODULE_EPDC)) {
			return;
		}
	}

	debug("lcd_disable\n");

	/* Disable clocks to EPDC */
	REG_SET(EPDC_BASE, EPDC_CTRL, EPDC_CTRL_CLKGATE);
}

void epdc_panel_disable(void)
{
	epdc_power_off();
}

void epdc_ctrl_init(void *lcdbase)
{
	unsigned int val;

	if (CONFIG_IS_ENABLED(IMX_MODULE_FUSE)) {
		if (check_module_fused(MODULE_EPDC)) {
			printf("EPDC@0x%x is fused, disable it\n", EPDC_BASE_ADDR);
			return;
		}
	}

	/*
	 * We rely on lcdbase being a physical address, i.e., either MMU off,
	 * or 1-to-1 mapping. Might want to add some virt2phys here.
	 */
	if (!lcdbase)
		return;

	panel_info.epdc_data.working_buf_addr = (u_long)memalign(ARCH_DMA_MINALIGN,
		panel_info.vl_col * panel_info.vl_row * 2);

	if (!panel_info.epdc_data.working_buf_addr) {
		printf("EPDC: Error allocating working buffer!\n");
		return;
	}

	panel_info.epdc_data.waveform_buf_addr = (u_long)memalign(ARCH_DMA_MINALIGN,
		CONFIG_WAVEFORM_BUF_SIZE);

	if (!panel_info.epdc_data.waveform_buf_addr) {
		printf("EPDC: Error allocating waveform buffer!\n");
		return;
	}

	lcd_color_fg = 0xFF;
	lcd_color_bg = 0xFF;

	/* Reset */
	REG_SET(EPDC_BASE, EPDC_CTRL, EPDC_CTRL_SFTRST);
	while (!(REG_RD(EPDC_BASE, EPDC_CTRL) & EPDC_CTRL_CLKGATE))
		;
	REG_CLR(EPDC_BASE, EPDC_CTRL, EPDC_CTRL_SFTRST);

	/* Enable clock gating (clear to enable) */
	REG_CLR(EPDC_BASE, EPDC_CTRL, EPDC_CTRL_CLKGATE);
	while (REG_RD(EPDC_BASE, EPDC_CTRL) &
	       (EPDC_CTRL_SFTRST | EPDC_CTRL_CLKGATE))
		;

	debug("resolution %dx%d, bpp %d\n", (int)panel_info.vl_col,
		(int)panel_info.vl_row, 1 << panel_info.vl_bpix);

	/* Get EPDC version */
	val = REG_RD(EPDC_BASE, EPDC_VERSION);
	rev = ((val & EPDC_VERSION_MAJOR_MASK) >>
				EPDC_VERSION_MAJOR_OFFSET) * 10
			+ ((val & EPDC_VERSION_MINOR_MASK) >>
				EPDC_VERSION_MINOR_OFFSET);

	/* Set framebuffer pointer */
	REG_WR(EPDC_BASE, EPDC_UPD_ADDR, (u32)lcdbase);

	/* Set Working Buffer pointer */
	REG_WR(EPDC_BASE, EPDC_WB_ADDR, panel_info.epdc_data.working_buf_addr);
	if (rev > 20)
		REG_WR(EPDC_BASE, EPDC_WB_ADDR_TCE, panel_info.epdc_data.working_buf_addr);

	/* Get waveform data address and offset */
	if (board_setup_waveform_file(panel_info.epdc_data.waveform_buf_addr)) {
		printf("Can't load waveform data!\n");
		return;
	}

	/* Set Waveform Buffer pointer */
	REG_WR(EPDC_BASE, EPDC_WVADDR,
		panel_info.epdc_data.waveform_buf_addr);

	/* Initialize EPDC, passing pointer to EPDC registers */
	epdc_init_settings();

	lcd_base = lcdbase;

	return;
}

ulong calc_fbsize(void)
{
	return panel_info.vl_row * panel_info.vl_col * 2 \
		* (1 << panel_info.vl_bpix) / 8;
}

static int epdc_video_probe(struct udevice *dev)
{
	struct video_uc_plat *plat = dev_get_uclass_plat(dev);
	struct video_priv *uc_priv = dev_get_uclass_priv(dev);
	u32 fb_start, fb_end;

	epdc_ctrl_init((void *)plat->base);

	epdc_enable();

	uc_priv->xsize = panel_info.vl_col;
	uc_priv->ysize = panel_info.vl_row;

	/* Enable dcache for the frame buffer */
	fb_start = plat->base & ~(MMU_SECTION_SIZE - 1);
	fb_end = plat->base + plat->size;
	fb_end = ALIGN(fb_end, 1 << MMU_SECTION_SHIFT);
	mmu_set_region_dcache_behaviour(fb_start, fb_end - fb_start,
					DCACHE_WRITEBACK);
	video_set_flush_dcache(dev, true);
	gd->fb_base = plat->base;

	return 0;
}

static int epdc_video_bind(struct udevice *dev)
{
	struct video_uc_plat *plat = dev_get_uclass_plat(dev);
	plat->size = calc_fbsize();
	plat->hide_logo = true;

	return 0;
}

static int epdc_video_remove(struct udevice *dev)
{
	epdc_disable();

	return 0;
}

static const struct udevice_id mxc_epdc_ids[] = {
	{ .compatible = "fsl,imx6dl-epdc" },
	{ .compatible = "fsl,imx6sl-epdc" },
	{ .compatible = "fsl,imx6sll-epdc" },
	{ .compatible = "fsl,imx7d-epdc" },
	{ /* sentinel */ }
};

U_BOOT_DRIVER(mxc_epdc) = {
	.name	= "mxc_epdc",
	.id	= UCLASS_VIDEO,
	.of_match = mxc_epdc_ids,
	.bind	= epdc_video_bind,
	.probe	= epdc_video_probe,
	.remove = epdc_video_remove,
	.flags	= DM_FLAG_PRE_RELOC | DM_FLAG_OS_PREPARE,
};
