// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2015-2019, The Linux Foundation. All rights reserved.
 */

#define pr_fmt(fmt)	"[drm:%s:%d] " fmt, __func__, __LINE__

#include <uapi/drm/sde_drm.h>

#include "msm_kms.h"
#include "sde_kms.h"
#include "sde_wb.h"
#include "sde_formats.h"

/* maximum display mode resolution if not available from catalog */
#define SDE_WB_MODE_MAX_WIDTH	4096
#define SDE_WB_MODE_MAX_HEIGHT	4096

/* Serialization lock for sde_wb_list */
static DEFINE_MUTEX(sde_wb_list_lock);

/* List of all writeback devices installed */
static LIST_HEAD(sde_wb_list);

/**
 * sde_wb_is_format_valid - check if given format/modifier is supported
 * @wb_dev:	Pointer to writeback device
 * @pixel_format:	Fourcc pixel format
 * @format_modifier:	Format modifier
 * Returns:		true if valid; false otherwise
 */
static int sde_wb_is_format_valid(struct sde_wb_device *wb_dev,
		u32 pixel_format, u64 format_modifier)
{
	const struct sde_format_extended *fmts = wb_dev->wb_cfg->format_list;
	int i;

	if (!fmts)
		return false;

	for (i = 0; fmts[i].fourcc_format; i++)
		if ((fmts[i].modifier == format_modifier) &&
				(fmts[i].fourcc_format == pixel_format))
			return true;

	return false;
}

enum drm_connector_status
sde_wb_connector_detect(struct drm_connector *connector,
		bool force,
		void *display)
{
	enum drm_connector_status rc = connector_status_unknown;

	SDE_DEBUG("\n");

	if (display)
		rc = ((struct sde_wb_device *)display)->detect_status;

	return rc;
}

int sde_wb_connector_get_modes(struct drm_connector *connector, void *display,
		const struct msm_resource_caps_info *avail_res)
{
	struct sde_wb_device *wb_dev;
	int num_modes = 0;

	if (!connector || !display)
		return 0;

	wb_dev = display;

	SDE_DEBUG("\n");

	mutex_lock(&wb_dev->wb_lock);
	if (wb_dev->count_modes && wb_dev->modes) {
		struct drm_display_mode *mode;
		int i, ret;

		for (i = 0; i < wb_dev->count_modes; i++) {
			mode = drm_mode_create(connector->dev);
			if (!mode) {
				SDE_ERROR("failed to create mode\n");
				break;
			}
			ret = drm_mode_convert_umode(wb_dev->drm_dev, mode,
					&wb_dev->modes[i]);
			if (ret) {
				SDE_ERROR("failed to convert mode %d\n", ret);
				break;
			}

			drm_mode_probed_add(connector, mode);
			num_modes++;
		}
	} else {
		u32 max_width = (wb_dev->wb_cfg && wb_dev->wb_cfg->sblk) ?
				wb_dev->wb_cfg->sblk->maxlinewidth :
				SDE_WB_MODE_MAX_WIDTH;

		num_modes = drm_add_modes_noedid(connector, max_width,
				SDE_WB_MODE_MAX_HEIGHT);
	}
	mutex_unlock(&wb_dev->wb_lock);
	return num_modes;
}

struct drm_framebuffer *
sde_wb_connector_state_get_output_fb(struct drm_connector_state *state)
{
	if (!state || !state->connector ||
		(state->connector->connector_type !=
				DRM_MODE_CONNECTOR_VIRTUAL)) {
		SDE_ERROR("invalid params\n");
		return NULL;
	}

	SDE_DEBUG("\n");

	return sde_connector_get_out_fb(state);
}

int sde_wb_connector_state_get_output_roi(struct drm_connector_state *state,
		struct sde_rect *roi)
{
	if (!state || !roi || !state->connector ||
		(state->connector->connector_type !=
				DRM_MODE_CONNECTOR_VIRTUAL)) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	roi->x = sde_connector_get_property(state, CONNECTOR_PROP_DST_X);
	roi->y = sde_connector_get_property(state, CONNECTOR_PROP_DST_Y);
	roi->w = sde_connector_get_property(state, CONNECTOR_PROP_DST_W);
	roi->h = sde_connector_get_property(state, CONNECTOR_PROP_DST_H);

	return 0;
}

/**
 * sde_wb_connector_set_modes - set writeback modes and connection status
 * @wb_dev:	Pointer to write back device
 * @count_modes:	Count of modes
 * @modes:	Pointer to writeback mode requested
 * @connected:	Connection status requested
 * Returns:	0 if success; error code otherwise
 */
static
int sde_wb_connector_set_modes(struct sde_wb_device *wb_dev,
		u32 count_modes, struct drm_mode_modeinfo __user *modes,
		bool connected)
{
	struct drm_mode_modeinfo *modeinfo = NULL;
	int ret = 0;
	int i;

	if (!wb_dev || !wb_dev->connector ||
			(wb_dev->connector->connector_type !=
			 DRM_MODE_CONNECTOR_VIRTUAL)) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	if (connected) {
		SDE_DEBUG("connect\n");

		if (!count_modes || !modes) {
			SDE_ERROR("invalid count_modes :%u and modes :%d\n",
				count_modes, !modes);
			return -EINVAL;
		}

		modeinfo = kcalloc(count_modes,
				sizeof(struct drm_mode_modeinfo),
				GFP_KERNEL);
		if (!modeinfo) {
			SDE_ERROR("invalid params\n");
			ret = -ENOMEM;
			goto error;
		}

		if (copy_from_user(modeinfo, modes,
				count_modes *
				sizeof(struct drm_mode_modeinfo))) {
			SDE_ERROR("failed to copy modes\n");
			kfree(modeinfo);
			ret = -EFAULT;
			goto error;
		}

		for (i = 0; i < count_modes; i++) {
			struct drm_display_mode dispmode;

			memset(&dispmode, 0, sizeof(dispmode));
			ret = drm_mode_convert_umode(wb_dev->drm_dev,
					&dispmode, &modeinfo[i]);
			if (ret) {
				SDE_ERROR(
					"failed to convert mode %d:\"%s\" %d %d %d %d %d %d %d %d %d %d 0x%x 0x%x status:%d rc:%d\n",
					i,
					modeinfo[i].name,
					modeinfo[i].vrefresh,
					modeinfo[i].clock,
					modeinfo[i].hdisplay,
					modeinfo[i].hsync_start,
					modeinfo[i].hsync_end,
					modeinfo[i].htotal,
					modeinfo[i].vdisplay,
					modeinfo[i].vsync_start,
					modeinfo[i].vsync_end,
					modeinfo[i].vtotal,
					modeinfo[i].type,
					modeinfo[i].flags,
					dispmode.status,
					ret);
				kfree(modeinfo);
				goto error;
			}
		}

		if (wb_dev->modes) {
			wb_dev->count_modes = 0;

			kfree(wb_dev->modes);
			wb_dev->modes = NULL;
		}

		wb_dev->count_modes = count_modes;
		wb_dev->modes = modeinfo;
		wb_dev->detect_status = connector_status_connected;
	} else {
		SDE_DEBUG("disconnect\n");

		if (wb_dev->modes) {
			wb_dev->count_modes = 0;

			kfree(wb_dev->modes);
			wb_dev->modes = NULL;
		}

		wb_dev->detect_status = connector_status_disconnected;
	}

error:
	return ret;
}

int sde_wb_connector_set_property(struct drm_connector *connector,
		struct drm_connector_state *state,
		int property_index,
		uint64_t value,
		void *display)
{
	struct sde_wb_device *wb_dev = display;
	struct drm_framebuffer *out_fb;
	int rc = 0;

	SDE_DEBUG("\n");

	if (state && (property_index == CONNECTOR_PROP_OUT_FB)) {
		const struct sde_format *sde_format;

		out_fb = sde_connector_get_out_fb(state);
		if (!out_fb)
			goto done;

		sde_format = sde_get_sde_format_ext(out_fb->format->format,
				out_fb->modifier);
		if (!sde_format) {
			SDE_ERROR("failed to get sde format\n");
			rc = -EINVAL;
			goto done;
		}

		if (!sde_wb_is_format_valid(wb_dev, out_fb->format->format,
				out_fb->modifier)) {
			SDE_ERROR("unsupported writeback format 0x%x/0x%llx\n",
					out_fb->format->format,
					out_fb->modifier);
			rc = -EINVAL;
			goto done;
		}
	}

done:
	return rc;
}

int sde_wb_get_info(struct drm_connector *connector,
		struct msm_display_info *info, void *display)
{
	struct sde_wb_device *wb_dev = display;

	if (!info || !wb_dev) {
		pr_err("invalid params\n");
		return -EINVAL;
	}

	memset(info, 0, sizeof(struct msm_display_info));
	info->intf_type = DRM_MODE_CONNECTOR_VIRTUAL;
	info->num_of_h_tiles = 1;
	info->h_tile_instance[0] = sde_wb_get_index(display);
	info->is_connected = true;
	info->capabilities = MSM_DISPLAY_CAP_HOT_PLUG | MSM_DISPLAY_CAP_EDID;
	info->max_width = (wb_dev->wb_cfg && wb_dev->wb_cfg->sblk) ?
			wb_dev->wb_cfg->sblk->maxlinewidth :
			SDE_WB_MODE_MAX_WIDTH;
	info->max_height = SDE_WB_MODE_MAX_HEIGHT;
	return 0;
}

int sde_wb_get_mode_info(struct drm_connector *connector,
		const struct drm_display_mode *drm_mode,
		struct msm_mode_info *mode_info,
		void *display, const struct msm_resource_caps_info *avail_res)
{
	const u32 dual_lm = 2;
	const u32 single_lm = 1;
	const u32 single_intf = 1;
	const u32 no_enc = 0;
	struct msm_display_topology *topology;
	struct sde_wb_device *wb_dev = display;
	u16 hdisplay;
	int i;

	if (!drm_mode || !mode_info || !avail_res ||
			!avail_res->max_mixer_width || !display) {
		pr_err("invalid params\n");
		return -EINVAL;
	}

	hdisplay = drm_mode->hdisplay;

	/* find maximum display width to support */
	for (i = 0; i < wb_dev->count_modes; i++)
		hdisplay = max(hdisplay, wb_dev->modes[i].hdisplay);

	topology = &mode_info->topology;
	topology->num_lm = (avail_res->max_mixer_width <= hdisplay) ?
			dual_lm : single_lm;
	topology->num_enc = no_enc;
	topology->num_intf = single_intf;

	mode_info->comp_info.comp_type = MSM_DISPLAY_COMPRESSION_NONE;
	mode_info->wide_bus_en = false;
	mode_info->comp_info.comp_ratio = MSM_DISPLAY_COMPRESSION_RATIO_NONE;

	return 0;
}

int sde_wb_connector_set_info_blob(struct drm_connector *connector,
		void *info, void *display, struct msm_mode_info *mode_info)
{
	struct sde_wb_device *wb_dev = display;
	const struct sde_format_extended *format_list;

	if (!connector || !info || !display || !wb_dev->wb_cfg) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	format_list = wb_dev->wb_cfg->format_list;

	/*
	 * Populate info buffer
	 */
	if (format_list) {
		sde_kms_info_start(info, "pixel_formats");
		while (format_list->fourcc_format) {
			sde_kms_info_append_format(info,
					format_list->fourcc_format,
					format_list->modifier);
			++format_list;
		}
		sde_kms_info_stop(info);
	}

	sde_kms_info_add_keyint(info,
			"wb_intf_index",
			wb_dev->wb_idx - WB_0);

	sde_kms_info_add_keyint(info,
			"maxlinewidth",
			wb_dev->wb_cfg->sblk->maxlinewidth);

	sde_kms_info_start(info, "features");
	if (wb_dev->wb_cfg && (wb_dev->wb_cfg->features & BIT(SDE_WB_UBWC)))
		sde_kms_info_append(info, "wb_ubwc");
	sde_kms_info_stop(info);

	return 0;
}

int sde_wb_connector_post_init(struct drm_connector *connector, void *display)
{
	struct sde_connector *c_conn;
	struct sde_wb_device *wb_dev = display;
	static const struct drm_prop_enum_list e_fb_translation_mode[] = {
		{SDE_DRM_FB_NON_SEC, "non_sec"},
		{SDE_DRM_FB_SEC, "sec"},
	};

	if (!connector || !display || !wb_dev->wb_cfg) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	c_conn = to_sde_connector(connector);
	wb_dev->connector = connector;
	wb_dev->detect_status = connector_status_connected;

	/*
	 * Add extra connector properties
	 */
	msm_property_install_range(&c_conn->property_info, "FB_ID",
			0x0, 0, ~0, 0, CONNECTOR_PROP_OUT_FB);
	msm_property_install_range(&c_conn->property_info, "DST_X",
			0x0, 0, UINT_MAX, 0, CONNECTOR_PROP_DST_X);
	msm_property_install_range(&c_conn->property_info, "DST_Y",
			0x0, 0, UINT_MAX, 0, CONNECTOR_PROP_DST_Y);
	msm_property_install_range(&c_conn->property_info, "DST_W",
			0x0, 0, UINT_MAX, 0, CONNECTOR_PROP_DST_W);
	msm_property_install_range(&c_conn->property_info, "DST_H",
			0x0, 0, UINT_MAX, 0, CONNECTOR_PROP_DST_H);
	msm_property_install_enum(&c_conn->property_info,
			"fb_translation_mode",
			0x0,
			0, e_fb_translation_mode,
			ARRAY_SIZE(e_fb_translation_mode),
			CONNECTOR_PROP_FB_TRANSLATION_MODE);

	return 0;
}

struct drm_framebuffer *sde_wb_get_output_fb(struct sde_wb_device *wb_dev)
{
	struct drm_framebuffer *fb;

	if (!wb_dev || !wb_dev->connector) {
		SDE_ERROR("invalid params\n");
		return NULL;
	}

	SDE_DEBUG("\n");

	mutex_lock(&wb_dev->wb_lock);
	fb = sde_wb_connector_state_get_output_fb(wb_dev->connector->state);
	mutex_unlock(&wb_dev->wb_lock);

	return fb;
}

int sde_wb_get_output_roi(struct sde_wb_device *wb_dev, struct sde_rect *roi)
{
	int rc;

	if (!wb_dev || !wb_dev->connector || !roi) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	mutex_lock(&wb_dev->wb_lock);
	rc = sde_wb_connector_state_get_output_roi(
			wb_dev->connector->state, roi);
	mutex_unlock(&wb_dev->wb_lock);

	return rc;
}

u32 sde_wb_get_num_of_displays(void)
{
	u32 count = 0;
	struct sde_wb_device *wb_dev;

	SDE_DEBUG("\n");

	mutex_lock(&sde_wb_list_lock);
	list_for_each_entry(wb_dev, &sde_wb_list, wb_list) {
		count++;
	}
	mutex_unlock(&sde_wb_list_lock);

	return count;
}

int wb_display_get_displays(void **display_array, u32 max_display_count)
{
	struct sde_wb_device *curr;
	int i = 0;

	SDE_DEBUG("\n");

	if (!display_array || !max_display_count) {
		if (!display_array)
			SDE_ERROR("invalid param\n");
		return 0;
	}

	mutex_lock(&sde_wb_list_lock);
	list_for_each_entry(curr, &sde_wb_list, wb_list) {
		if (i >= max_display_count)
			break;
		display_array[i++] = curr;
	}
	mutex_unlock(&sde_wb_list_lock);

	return i;
}

int sde_wb_config(struct drm_device *drm_dev, void *data,
				struct drm_file *file_priv)
{
	struct sde_drm_wb_cfg *config = data;
	struct msm_drm_private *priv;
	struct sde_wb_device *wb_dev = NULL;
	struct sde_wb_device *curr;
	struct drm_connector *connector;
	uint32_t flags;
	uint32_t connector_id;
	uint32_t count_modes;
	uint64_t modes;
	int rc;

	if (!drm_dev || !data) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	flags = config->flags;
	connector_id = config->connector_id;
	count_modes = config->count_modes;
	modes = config->modes;

	priv = drm_dev->dev_private;

	connector = drm_connector_lookup(drm_dev, file_priv, connector_id);
	if (!connector) {
		SDE_ERROR("failed to find connector\n");
		rc = -ENOENT;
		goto fail;
	}

	mutex_lock(&sde_wb_list_lock);
	list_for_each_entry(curr, &sde_wb_list, wb_list) {
		if (curr->connector == connector) {
			wb_dev = curr;
			break;
		}
	}
	mutex_unlock(&sde_wb_list_lock);

	if (!wb_dev) {
		SDE_ERROR("failed to find wb device\n");
		rc = -ENOENT;
		goto fail;
	}

	mutex_lock(&wb_dev->wb_lock);

	rc = sde_wb_connector_set_modes(wb_dev, count_modes,
		(struct drm_mode_modeinfo __user *) (uintptr_t) modes,
		(flags & SDE_DRM_WB_CFG_FLAGS_CONNECTED) ? true : false);

	mutex_unlock(&wb_dev->wb_lock);
	drm_helper_hpd_irq_event(drm_dev);
fail:
	return rc;
}

/**
 * _sde_wb_dev_init - perform device initialization
 * @wb_dev:	Pointer to writeback device
 */
static int _sde_wb_dev_init(struct sde_wb_device *wb_dev)
{
	int rc = 0;

	if (!wb_dev) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	return rc;
}

/**
 * _sde_wb_dev_deinit - perform device de-initialization
 * @wb_dev:	Pointer to writeback device
 */
static int _sde_wb_dev_deinit(struct sde_wb_device *wb_dev)
{
	int rc = 0;

	if (!wb_dev) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	return rc;
}

/**
 * sde_wb_bind - bind writeback device with controlling device
 * @dev:        Pointer to base of platform device
 * @master:     Pointer to container of drm device
 * @data:       Pointer to private data
 * Returns:     Zero on success
 */
static int sde_wb_bind(struct device *dev, struct device *master, void *data)
{
	struct sde_wb_device *wb_dev;

	if (!dev || !master) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	wb_dev = platform_get_drvdata(to_platform_device(dev));
	if (!wb_dev) {
		SDE_ERROR("invalid wb device\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	mutex_lock(&wb_dev->wb_lock);
	wb_dev->drm_dev = dev_get_drvdata(master);
	mutex_unlock(&wb_dev->wb_lock);

	return 0;
}

/**
 * sde_wb_unbind - unbind writeback from controlling device
 * @dev:        Pointer to base of platform device
 * @master:     Pointer to container of drm device
 * @data:       Pointer to private data
 */
static void sde_wb_unbind(struct device *dev,
		struct device *master, void *data)
{
	struct sde_wb_device *wb_dev;

	if (!dev) {
		SDE_ERROR("invalid params\n");
		return;
	}

	wb_dev = platform_get_drvdata(to_platform_device(dev));
	if (!wb_dev) {
		SDE_ERROR("invalid wb device\n");
		return;
	}

	SDE_DEBUG("\n");

	mutex_lock(&wb_dev->wb_lock);
	wb_dev->drm_dev = NULL;
	mutex_unlock(&wb_dev->wb_lock);
}

static const struct component_ops sde_wb_comp_ops = {
	.bind = sde_wb_bind,
	.unbind = sde_wb_unbind,
};

/**
 * sde_wb_drm_init - perform DRM initialization
 * @wb_dev:	Pointer to writeback device
 * @encoder:	Pointer to associated encoder
 */
int sde_wb_drm_init(struct sde_wb_device *wb_dev, struct drm_encoder *encoder)
{
	int rc = 0;

	if (!wb_dev || !wb_dev->drm_dev || !encoder) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	mutex_lock(&wb_dev->wb_lock);

	if (wb_dev->drm_dev->dev_private) {
		struct msm_drm_private *priv = wb_dev->drm_dev->dev_private;
		struct sde_kms *sde_kms = to_sde_kms(priv->kms);

		if (wb_dev->index < sde_kms->catalog->wb_count) {
			wb_dev->wb_idx = sde_kms->catalog->wb[wb_dev->index].id;
			wb_dev->wb_cfg = &sde_kms->catalog->wb[wb_dev->index];
		}
	}

	wb_dev->drm_dev = encoder->dev;
	wb_dev->encoder = encoder;
	mutex_unlock(&wb_dev->wb_lock);
	return rc;
}

int sde_wb_drm_deinit(struct sde_wb_device *wb_dev)
{
	int rc = 0;

	if (!wb_dev) {
		SDE_ERROR("invalid params\n");
		return -EINVAL;
	}

	SDE_DEBUG("\n");

	return rc;
}

/**
 * sde_wb_probe - load writeback module
 * @pdev:	Pointer to platform device
 */
static int sde_wb_probe(struct platform_device *pdev)
{
	struct sde_wb_device *wb_dev;
	int ret;

	wb_dev = devm_kzalloc(&pdev->dev, sizeof(*wb_dev), GFP_KERNEL);
	if (!wb_dev)
		return -ENOMEM;

	SDE_DEBUG("\n");

	ret = of_property_read_u32(pdev->dev.of_node, "cell-index",
			&wb_dev->index);
	if (ret) {
		SDE_DEBUG("cell index not set, default to 0\n");
		wb_dev->index = 0;
	}

	wb_dev->name = of_get_property(pdev->dev.of_node, "label", NULL);
	if (!wb_dev->name) {
		SDE_DEBUG("label not set, default to unknown\n");
		wb_dev->name = "unknown";
	}

	wb_dev->wb_idx = SDE_NONE;

	mutex_init(&wb_dev->wb_lock);
	platform_set_drvdata(pdev, wb_dev);

	mutex_lock(&sde_wb_list_lock);
	list_add(&wb_dev->wb_list, &sde_wb_list);
	mutex_unlock(&sde_wb_list_lock);

	if (!_sde_wb_dev_init(wb_dev)) {
		ret = component_add(&pdev->dev, &sde_wb_comp_ops);
		if (ret)
			pr_err("component add failed\n");
	}

	return ret;
}

/**
 * sde_wb_remove - unload writeback module
 * @pdev:	Pointer to platform device
 */
static int sde_wb_remove(struct platform_device *pdev)
{
	struct sde_wb_device *wb_dev;
	struct sde_wb_device *curr, *next;

	wb_dev = platform_get_drvdata(pdev);
	if (!wb_dev)
		return 0;

	SDE_DEBUG("\n");

	(void)_sde_wb_dev_deinit(wb_dev);

	mutex_lock(&sde_wb_list_lock);
	list_for_each_entry_safe(curr, next, &sde_wb_list, wb_list) {
		if (curr == wb_dev) {
			list_del(&wb_dev->wb_list);
			break;
		}
	}
	mutex_unlock(&sde_wb_list_lock);

	kfree(wb_dev->modes);
	mutex_destroy(&wb_dev->wb_lock);

	platform_set_drvdata(pdev, NULL);
	devm_kfree(&pdev->dev, wb_dev);

	return 0;
}

static const struct of_device_id dt_match[] = {
	{ .compatible = "qcom,wb-display"},
	{}
};

static struct platform_driver sde_wb_driver = {
	.probe = sde_wb_probe,
	.remove = sde_wb_remove,
	.driver = {
		.name = "sde_wb",
		.of_match_table = dt_match,
		.suppress_bind_attrs = true,
	},
};

static int __init sde_wb_register(void)
{
	return platform_driver_register(&sde_wb_driver);
}

static void __exit sde_wb_unregister(void)
{
	platform_driver_unregister(&sde_wb_driver);
}

module_init(sde_wb_register);
module_exit(sde_wb_unregister);
