#include <linux/clk.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/io.h>

#include <mach/clock.h>
#include <mach/hardware.h>
#include <mach/common.h>

#include <asm/clkdev.h>
#include <asm/bug.h>
#include <asm/div64.h>

#include "crm_regs.h"

#define CRM_SMALL_DIVIDER(base, name) \
	crm_small_divider(base, \
			  base ## _ ## name ## _OFFSET, \
			  base ## _ ## name ## _MASK)
#define CRM_1DIVIDER(base, name) \
	crm_divider(base, \
		    base ## _ ## name ## _OFFSET, \
		    base ## _ ## name ## _MASK, 1)
#define CRM_16DIVIDER(base, name) \
	crm_divider(base, \
		    base ## _ ## name ## _OFFSET, \
		    base ## _ ## name ## _MASK, 16)

static u32 crm_small_divider(void __iomem *reg, u8 offset, u32 mask)
{
	static const u32 crm_small_dividers[] = {
		2, 3, 4, 5, 6, 8, 10, 12
	};
	u8 idx;

	idx = (__raw_readl(reg) & mask) >> offset;
	if (idx > 7)
		return 1;

	return crm_small_dividers[idx];
}

static u32 crm_divider(void __iomem *reg, u8 offset, u32 mask, u32 z)
{
	u32 div;
	div = (__raw_readl(reg) & mask) >> offset;
	return div ? div : z;
}

static int _clk_1bit_enable(struct clk *clk)
{
	u32 reg;

	reg = __raw_readl(clk->enable_reg);
	reg |= 1 << clk->enable_shift;
	__raw_writel(reg, clk->enable_reg);

	return 0;
}

static void _clk_1bit_disable(struct clk *clk)
{
	u32 reg;

	reg = __raw_readl(clk->enable_reg);
	reg &= ~(1 << clk->enable_shift);
	__raw_writel(reg, clk->enable_reg);
}

static int _clk_3bit_enable(struct clk *clk)
{
	u32 reg;

	reg = __raw_readl(clk->enable_reg);
	reg |= 0x7 << clk->enable_shift;
	__raw_writel(reg, clk->enable_reg);

	return 0;
}

static void _clk_3bit_disable(struct clk *clk)
{
	u32 reg;

	reg = __raw_readl(clk->enable_reg);
	reg &= ~(0x7 << clk->enable_shift);
	__raw_writel(reg, clk->enable_reg);
}

static unsigned long ckih_rate;

static unsigned long clk_ckih_get_rate(struct clk *clk)
{
	return ckih_rate;
}

static struct clk ckih_clk = {
	.get_rate = clk_ckih_get_rate,
};

static unsigned long clk_ckih_x2_get_rate(struct clk *clk)
{
	return 2 * clk_get_rate(clk->parent);
}

static struct clk ckih_x2_clk = {
	.parent = &ckih_clk,
	.get_rate = clk_ckih_x2_get_rate,
};

static unsigned long clk_ckil_get_rate(struct clk *clk)
{
	return CKIL_CLK_FREQ;
}

static struct clk ckil_clk = {
	.get_rate = clk_ckil_get_rate,
};

/* plls stuff */
static struct clk mcu_pll_clk;
static struct clk dsp_pll_clk;
static struct clk usb_pll_clk;

static struct clk *pll_clk(u8 sel)
{
	switch (sel) {
	case 0:
		return &mcu_pll_clk;
	case 1:
		return &dsp_pll_clk;
	case 2:
		return &usb_pll_clk;
	}
	BUG();
}

static void __iomem *pll_base(struct clk *clk)
{
	if (clk == &mcu_pll_clk)
		return MXC_PLL0_BASE;
	else if (clk == &dsp_pll_clk)
		return MXC_PLL1_BASE;
	else if (clk == &usb_pll_clk)
		return MXC_PLL2_BASE;
	BUG();
}

static unsigned long clk_pll_get_rate(struct clk *clk)
{
	const void __iomem *pllbase;
	unsigned long dp_op, dp_mfd, dp_mfn, pll_hfsm, ref_clk, mfi;
	long mfn, mfn_abs, mfd, pdf;
	s64 temp;
	pllbase = pll_base(clk);

	pll_hfsm = __raw_readl(pllbase + MXC_PLL_DP_CTL) & MXC_PLL_DP_CTL_HFSM;
	if (pll_hfsm == 0) {
		dp_op = __raw_readl(pllbase + MXC_PLL_DP_OP);
		dp_mfd = __raw_readl(pllbase + MXC_PLL_DP_MFD);
		dp_mfn = __raw_readl(pllbase + MXC_PLL_DP_MFN);
	} else {
		dp_op = __raw_readl(pllbase + MXC_PLL_DP_HFS_OP);
		dp_mfd = __raw_readl(pllbase + MXC_PLL_DP_HFS_MFD);
		dp_mfn = __raw_readl(pllbase + MXC_PLL_DP_HFS_MFN);
	}

	pdf = dp_op & MXC_PLL_DP_OP_PDF_MASK;
	mfi = (dp_op >> MXC_PLL_DP_OP_MFI_OFFSET) & MXC_PLL_DP_OP_PDF_MASK;
	mfi = (mfi <= 5) ? 5 : mfi;
	mfd = dp_mfd & MXC_PLL_DP_MFD_MASK;
	mfn = dp_mfn & MXC_PLL_DP_MFN_MASK;
	mfn = (mfn <= 0x4000000) ? mfn : (mfn - 0x10000000);

	if (mfn < 0)
		mfn_abs = -mfn;
	else
		mfn_abs = mfn;

/* XXX: actually this asumes that ckih is fed to pll, but spec says
 * that ckih_x2 is also possible. need to check this out.
 */
	ref_clk = clk_get_rate(&ckih_clk);

	ref_clk *= 2;
	ref_clk /= pdf + 1;

	temp = (u64) ref_clk * mfn_abs;
	do_div(temp, mfd);
	if (mfn < 0)
		temp = -temp;
	temp += ref_clk * mfi;

	return temp;
}

static int clk_pll_enable(struct clk *clk)
{
	void __iomem *ctl;
	u32 reg;

	ctl = pll_base(clk);
	reg = __raw_readl(ctl);
	reg |= (MXC_PLL_DP_CTL_RST | MXC_PLL_DP_CTL_UPEN);
	__raw_writel(reg, ctl);
	do {
		reg = __raw_readl(ctl);
	} while ((reg & MXC_PLL_DP_CTL_LRF) != MXC_PLL_DP_CTL_LRF);
	return 0;
}

static void clk_pll_disable(struct clk *clk)
{
	void __iomem *ctl;
	u32 reg;

	ctl = pll_base(clk);
	reg = __raw_readl(ctl);
	reg &= ~(MXC_PLL_DP_CTL_RST | MXC_PLL_DP_CTL_UPEN);
	__raw_writel(reg, ctl);
}

static struct clk mcu_pll_clk = {
	.parent = &ckih_clk,
	.get_rate = clk_pll_get_rate,
	.enable = clk_pll_enable,
	.disable = clk_pll_disable,
};

static struct clk dsp_pll_clk = {
	.parent = &ckih_clk,
	.get_rate = clk_pll_get_rate,
	.enable = clk_pll_enable,
	.disable = clk_pll_disable,
};

static struct clk usb_pll_clk = {
	.parent = &ckih_clk,
	.get_rate = clk_pll_get_rate,
	.enable = clk_pll_enable,
	.disable = clk_pll_disable,
};
/* plls stuff end */

/* ap_ref_clk stuff */
static struct clk ap_ref_clk;

static unsigned long clk_ap_ref_get_rate(struct clk *clk)
{
	u32 ascsr, acsr;
	u8 ap_pat_ref_div_2, ap_isel, acs, ads;

	ascsr = __raw_readl(MXC_CRMAP_ASCSR);
	acsr = __raw_readl(MXC_CRMAP_ACSR);

	/* 0 for ckih, 1 for ckih*2 */
	ap_isel = ascsr & MXC_CRMAP_ASCSR_APISEL;
	/* reg divider */
	ap_pat_ref_div_2 = (ascsr >> MXC_CRMAP_ASCSR_AP_PATDIV2_OFFSET) & 0x1;
	/* undocumented, 1 for disabling divider */
	ads = (acsr >> MXC_CRMAP_ACSR_ADS_OFFSET) & 0x1;
	/* 0 for pat_ref, 1 for divider out */
	acs = acsr & MXC_CRMAP_ACSR_ACS;

	if (acs & !ads)
		/* use divided clock */
		return clk_get_rate(clk->parent) / (ap_pat_ref_div_2 ? 2 : 1);

	return clk_get_rate(clk->parent) * (ap_isel ? 2 : 1);
}

static struct clk ap_ref_clk = {
	.parent = &ckih_clk,
	.get_rate = clk_ap_ref_get_rate,
};
/* ap_ref_clk stuff end */

/* ap_pre_dfs_clk stuff */
static struct clk ap_pre_dfs_clk;

static unsigned long clk_ap_pre_dfs_get_rate(struct clk *clk)
{
	u32 acsr, ascsr;

	acsr = __raw_readl(MXC_CRMAP_ACSR);
	ascsr = __raw_readl(MXC_CRMAP_ASCSR);

	if (acsr & MXC_CRMAP_ACSR_ACS) {
		u8 sel;
		sel = (ascsr & MXC_CRMAP_ASCSR_APSEL_MASK) >>
			MXC_CRMAP_ASCSR_APSEL_OFFSET;
		return clk_get_rate(pll_clk(sel)) /
			CRM_SMALL_DIVIDER(MXC_CRMAP_ACDR, ARMDIV);
	}
	return clk_get_rate(&ap_ref_clk);
}

static struct clk ap_pre_dfs_clk = {
	.get_rate = clk_ap_pre_dfs_get_rate,
};
/* ap_pre_dfs_clk stuff end */

/* usb_clk stuff */
static struct clk usb_clk;

static struct clk *clk_usb_parent(struct clk *clk)
{
	u32 acsr, ascsr;

	acsr = __raw_readl(MXC_CRMAP_ACSR);
	ascsr = __raw_readl(MXC_CRMAP_ASCSR);

	if (acsr & MXC_CRMAP_ACSR_ACS) {
		u8 sel;
		sel = (ascsr & MXC_CRMAP_ASCSR_USBSEL_MASK) >>
			MXC_CRMAP_ASCSR_USBSEL_OFFSET;
		return pll_clk(sel);
	}
	return &ap_ref_clk;
}

static unsigned long clk_usb_get_rate(struct clk *clk)
{
	return clk_get_rate(clk->parent) /
		CRM_SMALL_DIVIDER(MXC_CRMAP_ACDER2, USBDIV);
}

static struct clk usb_clk = {
	.enable_reg = MXC_CRMAP_ACDER2,
	.enable_shift = MXC_CRMAP_ACDER2_USBEN_OFFSET,
	.get_rate = clk_usb_get_rate,
	.enable = _clk_1bit_enable,
	.disable = _clk_1bit_disable,
};
/* usb_clk stuff end */

static unsigned long clk_ipg_get_rate(struct clk *clk)
{
	return clk_get_rate(clk->parent) / CRM_16DIVIDER(MXC_CRMAP_ACDR, IPDIV);
}

static unsigned long clk_ahb_get_rate(struct clk *clk)
{
	return clk_get_rate(clk->parent) /
		CRM_16DIVIDER(MXC_CRMAP_ACDR, AHBDIV);
}

static struct clk ipg_clk = {
	.parent = &ap_pre_dfs_clk,
	.get_rate = clk_ipg_get_rate,
};

static struct clk ahb_clk = {
	.parent = &ap_pre_dfs_clk,
	.get_rate = clk_ahb_get_rate,
};

/* perclk_clk stuff */
static struct clk perclk_clk;

static unsigned long clk_perclk_get_rate(struct clk *clk)
{
	u32 acder2;

	acder2 = __raw_readl(MXC_CRMAP_ACDER2);
	if (acder2 & MXC_CRMAP_ACDER2_BAUD_ISEL_MASK)
		return 2 * clk_get_rate(clk->parent);

	return clk_get_rate(clk->parent);
}

static struct clk perclk_clk = {
	.parent = &ckih_clk,
	.get_rate = clk_perclk_get_rate,
};
/* perclk_clk stuff end */

/* uart_clk stuff */
static struct clk uart_clk[];

static unsigned long clk_uart_get_rate(struct clk *clk)
{
	u32 div;

	switch (clk->id) {
	case 0:
	case 1:
		div = CRM_SMALL_DIVIDER(MXC_CRMAP_ACDER2, BAUDDIV);
		break;
	case 2:
		div = CRM_SMALL_DIVIDER(MXC_CRMAP_APRA, UART3DIV);
		break;
	default:
		BUG();
	}
	return clk_get_rate(clk->parent) / div;
}

static struct clk uart_clk[] = {
	{
		.id = 0,
		.parent = &perclk_clk,
		.enable_reg = MXC_CRMAP_APRA,
		.enable_shift = MXC_CRMAP_APRA_UART1EN_OFFSET,
		.get_rate = clk_uart_get_rate,
		.enable = _clk_1bit_enable,
		.disable = _clk_1bit_disable,
	}, {
		.id = 1,
		.parent = &perclk_clk,
		.enable_reg = MXC_CRMAP_APRA,
		.enable_shift = MXC_CRMAP_APRA_UART2EN_OFFSET,
		.get_rate = clk_uart_get_rate,
		.enable = _clk_1bit_enable,
		.disable = _clk_1bit_disable,
	}, {
		.id = 2,
		.parent = &perclk_clk,
		.enable_reg = MXC_CRMAP_APRA,
		.enable_shift = MXC_CRMAP_APRA_UART3EN_OFFSET,
		.get_rate = clk_uart_get_rate,
		.enable = _clk_1bit_enable,
		.disable = _clk_1bit_disable,
	},
};
/* uart_clk stuff end */

/* sdhc_clk stuff */
static struct clk nfc_clk;

static unsigned long clk_nfc_get_rate(struct clk *clk)
{
	return clk_get_rate(clk->parent) /
		CRM_1DIVIDER(MXC_CRMAP_ACDER2, NFCDIV);
}

static struct clk nfc_clk = {
	.parent = &ahb_clk,
	.enable_reg = MXC_CRMAP_ACDER2,
	.enable_shift = MXC_CRMAP_ACDER2_NFCEN_OFFSET,
	.get_rate = clk_nfc_get_rate,
	.enable = _clk_1bit_enable,
	.disable = _clk_1bit_disable,
};
/* sdhc_clk stuff end */

/* sdhc_clk stuff */
static struct clk sdhc_clk[];

static struct clk *clk_sdhc_parent(struct clk *clk)
{
	u32 aprb;
	u8 sel;
	u32 mask;
	int offset;

	aprb = __raw_readl(MXC_CRMAP_APRB);

	switch (clk->id) {
	case 0:
		mask = MXC_CRMAP_APRB_SDHC1_ISEL_MASK;
		offset = MXC_CRMAP_APRB_SDHC1_ISEL_OFFSET;
		break;
	case 1:
		mask = MXC_CRMAP_APRB_SDHC2_ISEL_MASK;
		offset = MXC_CRMAP_APRB_SDHC2_ISEL_OFFSET;
		break;
	default:
		BUG();
	}
	sel = (aprb & mask) >> offset;

	switch (sel) {
	case 0:
		return &ckih_clk;
	case 1:
		return &ckih_x2_clk;
	}
	return &usb_clk;
}

static unsigned long clk_sdhc_get_rate(struct clk *clk)
{
	u32 div;

	switch (clk->id) {
	case 0:
		div = CRM_SMALL_DIVIDER(MXC_CRMAP_APRB, SDHC1_DIV);
		break;
	case 1:
		div = CRM_SMALL_DIVIDER(MXC_CRMAP_APRB, SDHC2_DIV);
		break;
	default:
		BUG();
	}

	return clk_get_rate(clk->parent) / div;
}

static int clk_sdhc_enable(struct clk *clk)
{
	u32 amlpmre1, aprb;

	amlpmre1 = __raw_readl(MXC_CRMAP_AMLPMRE1);
	aprb = __raw_readl(MXC_CRMAP_APRB);
	switch (clk->id) {
	case 0:
		amlpmre1 |= (0x7 << MXC_CRMAP_AMLPMRE1_MLPME4_OFFSET);
		aprb |= (0x1 << MXC_CRMAP_APRB_SDHC1EN_OFFSET);
		break;
	case 1:
		amlpmre1 |= (0x7 << MXC_CRMAP_AMLPMRE1_MLPME5_OFFSET);
		aprb |= (0x1 << MXC_CRMAP_APRB_SDHC2EN_OFFSET);
		break;
	}
	__raw_writel(amlpmre1, MXC_CRMAP_AMLPMRE1);
	__raw_writel(aprb, MXC_CRMAP_APRB);
	return 0;
}

static void clk_sdhc_disable(struct clk *clk)
{
	u32 amlpmre1, aprb;

	amlpmre1 = __raw_readl(MXC_CRMAP_AMLPMRE1);
	aprb = __raw_readl(MXC_CRMAP_APRB);
	switch (clk->id) {
	case 0:
		amlpmre1 &= ~(0x7 << MXC_CRMAP_AMLPMRE1_MLPME4_OFFSET);
		aprb &= ~(0x1 << MXC_CRMAP_APRB_SDHC1EN_OFFSET);
		break;
	case 1:
		amlpmre1 &= ~(0x7 << MXC_CRMAP_AMLPMRE1_MLPME5_OFFSET);
		aprb &= ~(0x1 << MXC_CRMAP_APRB_SDHC2EN_OFFSET);
		break;
	}
	__raw_writel(amlpmre1, MXC_CRMAP_AMLPMRE1);
	__raw_writel(aprb, MXC_CRMAP_APRB);
}

static struct clk sdhc_clk[] = {
	{
		.id = 0,
		.get_rate = clk_sdhc_get_rate,
		.enable = clk_sdhc_enable,
		.disable = clk_sdhc_disable,
	}, {
		.id = 1,
		.get_rate = clk_sdhc_get_rate,
		.enable = clk_sdhc_enable,
		.disable = clk_sdhc_disable,
	},
};
/* sdhc_clk stuff end */

/* wdog_clk stuff */
static struct clk wdog_clk[] = {
	{
		.id = 0,
		.parent = &ipg_clk,
		.enable_reg = MXC_CRMAP_AMLPMRD,
		.enable_shift = MXC_CRMAP_AMLPMRD_MLPMD7_OFFSET,
		.enable = _clk_3bit_enable,
		.disable = _clk_3bit_disable,
	}, {
		.id = 1,
		.parent = &ipg_clk,
		.enable_reg = MXC_CRMAP_AMLPMRD,
		.enable_shift = MXC_CRMAP_AMLPMRD_MLPMD3_OFFSET,
		.enable = _clk_3bit_enable,
		.disable = _clk_3bit_disable,
	},
};
/* wdog_clk stuff end */

/* gpt_clk stuff */
static struct clk gpt_clk = {
	.parent = &ipg_clk,
	.enable_reg = MXC_CRMAP_AMLPMRC,
	.enable_shift = MXC_CRMAP_AMLPMRC_MLPMC4_OFFSET,
	.enable = _clk_3bit_enable,
	.disable = _clk_3bit_disable,
};
/* gpt_clk stuff end */

/* cspi_clk stuff */
static struct clk cspi_clk[] = {
	{
		.id = 0,
		.parent = &ipg_clk,
		.enable_reg = MXC_CRMAP_AMLPMRE2,
		.enable_shift = MXC_CRMAP_AMLPMRE2_MLPME0_OFFSET,
		.enable = _clk_3bit_enable,
		.disable = _clk_3bit_disable,
	}, {
		.id = 1,
		.parent = &ipg_clk,
		.enable_reg = MXC_CRMAP_AMLPMRE1,
		.enable_shift = MXC_CRMAP_AMLPMRE1_MLPME6_OFFSET,
		.enable = _clk_3bit_enable,
		.disable = _clk_3bit_disable,
	},
};
/* cspi_clk stuff end */

#define _REGISTER_CLOCK(d, n, c) \
	{ \
		.dev_id = d, \
		.con_id = n, \
		.clk = &c, \
	},

static struct clk_lookup lookups[] = {
	_REGISTER_CLOCK("imx-uart.0", NULL, uart_clk[0])
	_REGISTER_CLOCK("imx-uart.1", NULL, uart_clk[1])
	_REGISTER_CLOCK("imx-uart.2", NULL, uart_clk[2])
	_REGISTER_CLOCK("mxc-mmc.0", NULL, sdhc_clk[0])
	_REGISTER_CLOCK("mxc-mmc.1", NULL, sdhc_clk[1])
	_REGISTER_CLOCK("mxc-wdt.0", NULL, wdog_clk[0])
	_REGISTER_CLOCK("spi_imx.0", NULL, cspi_clk[0])
	_REGISTER_CLOCK("spi_imx.1", NULL, cspi_clk[1])
};

int __init mxc91231_clocks_init(unsigned long fref)
{
	void __iomem *gpt_base;
	int i;

	ckih_rate = fref;

	usb_clk.parent = clk_usb_parent(&usb_clk);
	sdhc_clk[0].parent = clk_sdhc_parent(&sdhc_clk[0]);
	sdhc_clk[1].parent = clk_sdhc_parent(&sdhc_clk[1]);

	for (i = 0; i < ARRAY_SIZE(lookups); i++)
		clkdev_add(&lookups[i]);

	gpt_base = MXC91231_IO_ADDRESS(MXC91231_GPT1_BASE_ADDR);
	mxc_timer_init(&gpt_clk, gpt_base, MXC91231_INT_GPT);

	return 0;
}
