// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright 2015 Maxime Ripard
 *
 * Maxime Ripard <maxime.ripard@free-electrons.com>
 */

#include <linux/clk-provider.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/spinlock.h>

#define TCON_CH1_SCLK2_PARENTS		4

#define TCON_CH1_SCLK2_GATE_BIT		BIT(31)
#define TCON_CH1_SCLK2_MUX_MASK		3
#define TCON_CH1_SCLK2_MUX_SHIFT	24
#define TCON_CH1_SCLK2_DIV_MASK		0xf
#define TCON_CH1_SCLK2_DIV_SHIFT	0

#define TCON_CH1_SCLK1_GATE_BIT		BIT(15)
#define TCON_CH1_SCLK1_HALF_BIT		BIT(11)

struct tcon_ch1_clk {
	struct clk_hw	hw;
	spinlock_t	lock;
	void __iomem	*reg;
};

#define hw_to_tclk(hw)	container_of(hw, struct tcon_ch1_clk, hw)

static void tcon_ch1_disable(struct clk_hw *hw)
{
	struct tcon_ch1_clk *tclk = hw_to_tclk(hw);
	unsigned long flags;
	u32 reg;

	spin_lock_irqsave(&tclk->lock, flags);
	reg = readl(tclk->reg);
	reg &= ~(TCON_CH1_SCLK2_GATE_BIT | TCON_CH1_SCLK1_GATE_BIT);
	writel(reg, tclk->reg);
	spin_unlock_irqrestore(&tclk->lock, flags);
}

static int tcon_ch1_enable(struct clk_hw *hw)
{
	struct tcon_ch1_clk *tclk = hw_to_tclk(hw);
	unsigned long flags;
	u32 reg;

	spin_lock_irqsave(&tclk->lock, flags);
	reg = readl(tclk->reg);
	reg |= TCON_CH1_SCLK2_GATE_BIT | TCON_CH1_SCLK1_GATE_BIT;
	writel(reg, tclk->reg);
	spin_unlock_irqrestore(&tclk->lock, flags);

	return 0;
}

static int tcon_ch1_is_enabled(struct clk_hw *hw)
{
	struct tcon_ch1_clk *tclk = hw_to_tclk(hw);
	u32 reg;

	reg = readl(tclk->reg);
	return reg & (TCON_CH1_SCLK2_GATE_BIT | TCON_CH1_SCLK1_GATE_BIT);
}

static u8 tcon_ch1_get_parent(struct clk_hw *hw)
{
	struct tcon_ch1_clk *tclk = hw_to_tclk(hw);
	u32 reg;

	reg = readl(tclk->reg) >> TCON_CH1_SCLK2_MUX_SHIFT;
	reg &= reg >> TCON_CH1_SCLK2_MUX_MASK;

	return reg;
}

static int tcon_ch1_set_parent(struct clk_hw *hw, u8 index)
{
	struct tcon_ch1_clk *tclk = hw_to_tclk(hw);
	unsigned long flags;
	u32 reg;

	spin_lock_irqsave(&tclk->lock, flags);
	reg = readl(tclk->reg);
	reg &= ~(TCON_CH1_SCLK2_MUX_MASK << TCON_CH1_SCLK2_MUX_SHIFT);
	reg |= index << TCON_CH1_SCLK2_MUX_SHIFT;
	writel(reg, tclk->reg);
	spin_unlock_irqrestore(&tclk->lock, flags);

	return 0;
};

static unsigned long tcon_ch1_calc_divider(unsigned long rate,
					   unsigned long parent_rate,
					   u8 *div,
					   bool *half)
{
	unsigned long best_rate = 0;
	u8 best_m = 0, m;
	bool is_double;

	for (m = 1; m < 16; m++) {
		u8 d;

		for (d = 1; d < 3; d++) {
			unsigned long tmp_rate;

			tmp_rate = parent_rate / m / d;

			if (tmp_rate > rate)
				continue;

			if (!best_rate ||
			    (rate - tmp_rate) < (rate - best_rate)) {
				best_rate = tmp_rate;
				best_m = m;
				is_double = d;
			}
		}
	}

	if (div && half) {
		*div = best_m;
		*half = is_double;
	}

	return best_rate;
}

static int tcon_ch1_determine_rate(struct clk_hw *hw,
				   struct clk_rate_request *req)
{
	long best_rate = -EINVAL;
	int i;

	for (i = 0; i < clk_hw_get_num_parents(hw); i++) {
		unsigned long parent_rate;
		unsigned long tmp_rate;
		struct clk_hw *parent;

		parent = clk_hw_get_parent_by_index(hw, i);
		if (!parent)
			continue;

		parent_rate = clk_hw_get_rate(parent);

		tmp_rate = tcon_ch1_calc_divider(req->rate, parent_rate,
						 NULL, NULL);

		if (best_rate < 0 ||
		    (req->rate - tmp_rate) < (req->rate - best_rate)) {
			best_rate = tmp_rate;
			req->best_parent_rate = parent_rate;
			req->best_parent_hw = parent;
		}
	}

	if (best_rate < 0)
		return best_rate;

	req->rate = best_rate;
	return 0;
}

static unsigned long tcon_ch1_recalc_rate(struct clk_hw *hw,
					  unsigned long parent_rate)
{
	struct tcon_ch1_clk *tclk = hw_to_tclk(hw);
	u32 reg;

	reg = readl(tclk->reg);

	parent_rate /= (reg & TCON_CH1_SCLK2_DIV_MASK) + 1;

	if (reg & TCON_CH1_SCLK1_HALF_BIT)
		parent_rate /= 2;

	return parent_rate;
}

static int tcon_ch1_set_rate(struct clk_hw *hw, unsigned long rate,
			     unsigned long parent_rate)
{
	struct tcon_ch1_clk *tclk = hw_to_tclk(hw);
	unsigned long flags;
	bool half;
	u8 div_m;
	u32 reg;

	tcon_ch1_calc_divider(rate, parent_rate, &div_m, &half);

	spin_lock_irqsave(&tclk->lock, flags);
	reg = readl(tclk->reg);
	reg &= ~(TCON_CH1_SCLK2_DIV_MASK | TCON_CH1_SCLK1_HALF_BIT);
	reg |= (div_m - 1) & TCON_CH1_SCLK2_DIV_MASK;

	if (half)
		reg |= TCON_CH1_SCLK1_HALF_BIT;

	writel(reg, tclk->reg);
	spin_unlock_irqrestore(&tclk->lock, flags);

	return 0;
}

static const struct clk_ops tcon_ch1_ops = {
	.disable	= tcon_ch1_disable,
	.enable		= tcon_ch1_enable,
	.is_enabled	= tcon_ch1_is_enabled,

	.get_parent	= tcon_ch1_get_parent,
	.set_parent	= tcon_ch1_set_parent,

	.determine_rate	= tcon_ch1_determine_rate,
	.recalc_rate	= tcon_ch1_recalc_rate,
	.set_rate	= tcon_ch1_set_rate,
};

static void __init tcon_ch1_setup(struct device_node *node)
{
	const char *parents[TCON_CH1_SCLK2_PARENTS];
	const char *clk_name = node->name;
	struct clk_init_data init;
	struct tcon_ch1_clk *tclk;
	struct resource res;
	struct clk *clk;
	void __iomem *reg;
	int ret;

	of_property_read_string(node, "clock-output-names", &clk_name);

	reg = of_io_request_and_map(node, 0, of_node_full_name(node));
	if (IS_ERR(reg)) {
		pr_err("%s: Could not map the clock registers\n", clk_name);
		return;
	}

	ret = of_clk_parent_fill(node, parents, TCON_CH1_SCLK2_PARENTS);
	if (ret != TCON_CH1_SCLK2_PARENTS) {
		pr_err("%s Could not retrieve the parents\n", clk_name);
		goto err_unmap;
	}

	tclk = kzalloc(sizeof(*tclk), GFP_KERNEL);
	if (!tclk)
		goto err_unmap;

	init.name = clk_name;
	init.ops = &tcon_ch1_ops;
	init.parent_names = parents;
	init.num_parents = TCON_CH1_SCLK2_PARENTS;
	init.flags = CLK_SET_RATE_PARENT;

	tclk->reg = reg;
	tclk->hw.init = &init;
	spin_lock_init(&tclk->lock);

	clk = clk_register(NULL, &tclk->hw);
	if (IS_ERR(clk)) {
		pr_err("%s: Couldn't register the clock\n", clk_name);
		goto err_free_data;
	}

	ret = of_clk_add_provider(node, of_clk_src_simple_get, clk);
	if (ret) {
		pr_err("%s: Couldn't register our clock provider\n", clk_name);
		goto err_unregister_clk;
	}

	return;

err_unregister_clk:
	clk_unregister(clk);
err_free_data:
	kfree(tclk);
err_unmap:
	iounmap(reg);
	of_address_to_resource(node, 0, &res);
	release_mem_region(res.start, resource_size(&res));
}

CLK_OF_DECLARE(tcon_ch1, "allwinner,sun4i-a10-tcon-ch1-clk",
	       tcon_ch1_setup);