// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (c) 2018-2019, 2021 The Linux Foundation. All rights reserved.
 * Copyright (c) 2022-2023, Qualcomm Innovation Center, Inc. All rights reserved.
 */

#define pr_fmt(fmt) "%s:%s " fmt, KBUILD_MODNAME, __func__

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/thermal.h>
#include <linux/err.h>
#include <linux/slab.h>
#include <linux/io.h>

#define CXIP_LM_CDEV_DRIVER "cx-ipeak-cooling-device"
#define CXIP_LM_CDEV_MAX_STATE 1

#define CXIP_LM_VOTE_STATUS       0x0
#define CXIP_LM_BYPASS            0x4
#define CXIP_LM_VOTE_CLEAR        0x8
#define CXIP_LM_VOTE_SET          0xc
#define CXIP_LM_FEATURE_EN        0x10
#define CXIP_LM_BYPASS_VAL        0xff20
#define CXIP_LM_THERM_VOTE_VAL    0x80
#define CXIP_LM_FEATURE_EN_VAL    0x1

struct cxip_lm_cooling_device {
	struct thermal_cooling_device	*cool_dev;
	char				cdev_name[THERMAL_NAME_LENGTH];
	void				*cx_ip_reg_base;
	unsigned int			therm_clnt;
	unsigned int			*bypass_clnts;
	unsigned int			bypass_clnt_cnt;
	bool				state;
};

static void cxip_lm_therm_vote_apply(struct cxip_lm_cooling_device *cxip_dev,
					bool vote)
{
	int vote_offset = 0, val = 0, sts_offset = 0;

	if (!cxip_dev->therm_clnt) {
		vote_offset = vote ? CXIP_LM_VOTE_SET : CXIP_LM_VOTE_CLEAR;
		val = CXIP_LM_THERM_VOTE_VAL;
		sts_offset = CXIP_LM_VOTE_STATUS;
	} else {
		vote_offset = cxip_dev->therm_clnt;
		val = vote ? 0x1 : 0x0;
		sts_offset = vote_offset;
	}

	writel_relaxed(val, cxip_dev->cx_ip_reg_base + vote_offset);
	pr_debug("%s vote for cxip_lm. vote:0x%x\n",
		vote ? "Applied" : "Cleared",
		readl_relaxed(cxip_dev->cx_ip_reg_base + sts_offset));
}

static void cxip_lm_initialize_cxip_hw(struct cxip_lm_cooling_device *cxip_dev)
{
	int i = 0;

	/* Set CXIP LM proxy vote for clients who are not participating */
	if (cxip_dev->bypass_clnt_cnt)
		for (i = 0; i < cxip_dev->bypass_clnt_cnt; i++)
			writel_relaxed(0x1, cxip_dev->cx_ip_reg_base +
					cxip_dev->bypass_clnts[i]);
	else if (!cxip_dev->therm_clnt)
		writel_relaxed(CXIP_LM_BYPASS_VAL,
			cxip_dev->cx_ip_reg_base + CXIP_LM_BYPASS);

	/* Enable CXIP LM HW */
	writel_relaxed(CXIP_LM_FEATURE_EN_VAL, cxip_dev->cx_ip_reg_base +
			CXIP_LM_FEATURE_EN);
}

static int cxip_lm_get_max_state(struct thermal_cooling_device *cdev,
				 unsigned long *state)
{
	*state = CXIP_LM_CDEV_MAX_STATE;

	return 0;
}

static int cxip_lm_set_cur_state(struct thermal_cooling_device *cdev,
				 unsigned long state)
{
	struct cxip_lm_cooling_device *cxip_dev = cdev->devdata;
	int ret = 0;

	if (state > CXIP_LM_CDEV_MAX_STATE)
		return -EINVAL;

	if (cxip_dev->state == state)
		return 0;

	cxip_lm_therm_vote_apply(cxip_dev, state);
	cxip_dev->state = state;

	return ret;
}

static int cxip_lm_get_cur_state(struct thermal_cooling_device *cdev,
				 unsigned long *state)
{
	struct cxip_lm_cooling_device *cxip_dev = cdev->devdata;

	*state = cxip_dev->state;

	return 0;
}

static struct thermal_cooling_device_ops cxip_lm_device_ops = {
	.get_max_state = cxip_lm_get_max_state,
	.get_cur_state = cxip_lm_get_cur_state,
	.set_cur_state = cxip_lm_set_cur_state,
};

static int cxip_lm_cdev_remove(struct platform_device *pdev)
{
	struct cxip_lm_cooling_device *cxip_dev =
		(struct cxip_lm_cooling_device *)dev_get_drvdata(&pdev->dev);

	if (cxip_dev) {
		if (cxip_dev->cool_dev) {
			thermal_cooling_device_unregister(cxip_dev->cool_dev);
			cxip_dev->cool_dev = NULL;
		}

		if (cxip_dev->cx_ip_reg_base)
			cxip_lm_therm_vote_apply(cxip_dev->cx_ip_reg_base,
							false);
	}

	return 0;
}

static int cxip_lm_get_devicetree_data(struct platform_device *pdev,
					struct cxip_lm_cooling_device *cxip_dev,
					struct device_node *np)
{
	int ret = 0;

	ret = of_property_read_u32(np, "qcom,thermal-client-offset",
			&cxip_dev->therm_clnt);
	if (ret) {
		dev_dbg(&pdev->dev,
			"error for qcom,thermal-client-offset. ret:%d\n",
			ret);
		cxip_dev->therm_clnt = 0;
		ret = 0;
		return ret;
	}

	ret = of_property_count_u32_elems(np, "qcom,bypass-client-list");
	if (ret <= 0) {
		dev_dbg(&pdev->dev, "Invalid number of clients err:%d\n", ret);
		ret = 0;
		return ret;
	}
	cxip_dev->bypass_clnt_cnt = ret;

	cxip_dev->bypass_clnts = devm_kcalloc(&pdev->dev,
				cxip_dev->bypass_clnt_cnt,
				sizeof(*cxip_dev->bypass_clnts), GFP_KERNEL);
	if (!cxip_dev->bypass_clnts)
		return -ENOMEM;

	ret = of_property_read_u32_array(np, "qcom,bypass-client-list",
		cxip_dev->bypass_clnts, cxip_dev->bypass_clnt_cnt);
	if (ret) {
		dev_dbg(&pdev->dev, "bypass client list err:%d, cnt:%d\n",
			ret, cxip_dev->bypass_clnt_cnt);
		cxip_dev->bypass_clnt_cnt = 0;
		ret = 0;
	}

	return ret;
}

static int cxip_lm_cdev_probe(struct platform_device *pdev)
{
	struct cxip_lm_cooling_device *cxip_dev = NULL;
	int ret = 0;
	struct device_node *np;
	struct resource *res = NULL;

	np = dev_of_node(&pdev->dev);
	if (!np) {
		dev_err(&pdev->dev,
			"of node not available for cxip_lm cdev\n");
		return -EINVAL;
	}

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

	ret = cxip_lm_get_devicetree_data(pdev, cxip_dev, np);
	if (ret)
		return ret;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		dev_err(&pdev->dev,
			"cxip_lm platform get resource failed\n");
		return -ENODEV;
	}

	cxip_dev->cx_ip_reg_base = devm_ioremap(&pdev->dev, res->start,
						resource_size(res));
	if (!cxip_dev->cx_ip_reg_base) {
		dev_err(&pdev->dev, "cxip_lm reg remap failed\n");
		return -ENOMEM;
	}

	cxip_lm_initialize_cxip_hw(cxip_dev);

	/* Set thermal vote till we get first vote from TF */
	cxip_dev->state = true;
	cxip_lm_therm_vote_apply(cxip_dev, cxip_dev->state);

	strscpy(cxip_dev->cdev_name, np->name, THERMAL_NAME_LENGTH);
	cxip_dev->cool_dev = thermal_of_cooling_device_register(
					np, cxip_dev->cdev_name, cxip_dev,
					&cxip_lm_device_ops);
	if (IS_ERR(cxip_dev->cool_dev)) {
		ret = PTR_ERR(cxip_dev->cool_dev);
		dev_err(&pdev->dev, "cxip_lm cdev register err:%d\n",
				ret);
		cxip_dev->cool_dev = NULL;
		cxip_lm_therm_vote_apply(cxip_dev->cx_ip_reg_base,
						false);
		return ret;
	}

	dev_set_drvdata(&pdev->dev, cxip_dev);

	return ret;
}

static const struct of_device_id cxip_lm_cdev_of_match[] = {
	{.compatible = "qcom,cxip-lm-cooling-device", },
	{}
};

static struct platform_driver cxip_lm_cdev_driver = {
	.driver = {
		.name = CXIP_LM_CDEV_DRIVER,
		.of_match_table = cxip_lm_cdev_of_match,
	},
	.probe = cxip_lm_cdev_probe,
	.remove = cxip_lm_cdev_remove,
};
module_platform_driver(cxip_lm_cdev_driver);
MODULE_DESCRIPTION("CX IPEAK cooling device driver");
MODULE_LICENSE("GPL");