/*
 * (C) Copyright 2012 Samsung Electronics Co. Ltd
 * Copyright 2013 Google Inc.  All rights reserved.
 *
 * See file CREDITS for list of people who contributed to this
 * project.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
 * MA 02111-1307 USA
 */

#include <assert.h>
#include <endian.h>
#include <libpayload.h>
#include <stdint.h>

#include "base/init_funcs.h"
#include "config.h"
#include "arch/cache.h"
#include "drivers/storage/exynos_mshc.h"
#include "drivers/storage/mmc.h"

static void setbits32(uint32_t *data, uint32_t bits)
{
	writel(readl(data) | bits, data);
}

static void clrbits32(uint32_t *data, uint32_t bits)
{
	writel(readl(data) & ~bits, data);
}

static int mshci_setbits(MshciHost *host, unsigned int bits)
{
	setbits32(&host->regs->ctrl, bits);
	if (mmc_busy_wait_io(&host->regs->ctrl, NULL, bits,
			     S5P_MSHC_TIMEOUT_MS)) {
		mmc_error("Set bits failed\n");
		return -1;
	}

	return 0;
}

static int mshci_reset_all(MshciHost *host)
{
	/*
	* Before we reset ciu check the DATA0 line.  If it is low and
	* we resets the ciu then we might see some errors.
	*/
	if (mmc_busy_wait_io(&host->regs->status, NULL, DATA_BUSY,
			     S5P_MSHC_TIMEOUT_MS)) {
		mmc_error("Controller did not release data0 "
			  "before ciu reset\n");
		return -1;
	}

	if (mshci_setbits(host, CTRL_RESET)) {
		mmc_error("Fail to reset card.\n");
		return -1;
	}
	if (mshci_setbits(host, FIFO_RESET)) {
		mmc_error("Fail to reset fifo.\n");
		return -1;
	}
	if (mshci_setbits(host, DMA_RESET)) {
		mmc_error("Fail to reset dma.\n");
		return -1;
	}

	return 0;
}

static void mshci_set_mdma_desc(void *desc_vir, void *desc_phy,
				uint32_t des0, uint32_t des1, uint32_t des2)
{
	MshciIdmac *desc = (MshciIdmac *)desc_vir;

	desc->des0 = des0;
	desc->des1 = des1;
	desc->des2 = des2;
	desc->des3 = (uintptr_t)desc_phy + sizeof(MshciIdmac);
}

static int mshci_prepare_data(MshciHost *host, MmcData *data)
{
	unsigned int i, data_cnt, blksz;
	static MshciIdmac idmac_desc[0x10000];
	/* TODO(alim.akhtar@samsung.com): do we really need this big array? */

	if (mshci_setbits(host, FIFO_RESET)) {
		mmc_error("Fail to reset FIFO\n");
		return -1;
	}

	MshciIdmac *pdesc_dmac = idmac_desc;
	blksz = data->blocksize;
	data_cnt = data->blocks;

	for  (i = 0;; i++) {
		uint32_t des_flag = (MSHCI_IDMAC_OWN | MSHCI_IDMAC_CH);
		des_flag |= (i == 0) ? MSHCI_IDMAC_FS : 0;
		if (data_cnt <= 8) {
			des_flag |= MSHCI_IDMAC_LD;
			mshci_set_mdma_desc(pdesc_dmac, pdesc_dmac,
				des_flag, blksz * data_cnt,
				(uintptr_t)data->dest + i * 0x1000);
			break;
		}
		/* max transfer size is 4KB per descriptor */
		mshci_set_mdma_desc(pdesc_dmac, pdesc_dmac, des_flag,
			blksz * 8, (uintptr_t)data->dest + i * 0x1000);

		data_cnt -= 8;
		pdesc_dmac++;
	}

	void *data_start = idmac_desc;
	size_t data_len = (uint8_t *)pdesc_dmac - (uint8_t *)idmac_desc +
			  DMA_MINALIGN;
	dcache_clean_invalidate_by_mva(data_start, data_len);

	data_start = data->dest;
	data_len = data->blocks * data->blocksize;
	dcache_clean_invalidate_by_mva(data_start, data_len);

	writel((uintptr_t)virt_to_phys(idmac_desc), &host->regs->dbaddr);

	// Enable the Internal DMA Controller.
	setbits32(&host->regs->ctrl, ENABLE_IDMAC | DMA_ENABLE);
	setbits32(&host->regs->bmod, BMOD_IDMAC_ENABLE | BMOD_IDMAC_FB);

	writel(data->blocksize, &host->regs->blksiz);
	writel(data->blocksize * data->blocks, &host->regs->bytcnt);
	return 0;
}

static int mshci_set_transfer_mode(MshciHost *host, MmcData *data)
{
	int mode = CMD_DATA_EXP_BIT;

	if (data->blocks > 1)
		mode |= CMD_SENT_AUTO_STOP_BIT;
	if (data->flags & MMC_DATA_WRITE)
		mode |= CMD_RW_BIT;

	return mode;
}

static int s5p_mshci_send_command(MmcCtrlr *ctrlr, MmcCommand *cmd,
				  MmcData *data)
{
	MshciHost *host = container_of(ctrlr, MshciHost, mmc);

	int flags = 0, i;
	unsigned int mask;

	/*
	 * If auto stop is enabled in the control register, ignore STOP
	 * command, as controller will internally send a STOP command after
	 * every block read/write
	 */
	if ((readl(&host->regs->ctrl) & SEND_AS_CCSD) &&
			(cmd->cmdidx == MMC_CMD_STOP_TRANSMISSION))
		return 0;

	/*
	* We shouldn't wait for data inihibit for stop commands, even
	* though they might use busy signaling
	*/
	if (mmc_busy_wait_io(&host->regs->status, NULL, DATA_BUSY,
			     S5P_MSHC_COMMAND_TIMEOUT)) {
		mmc_error("timeout on data busy\n");
		return -1;
	}

	if ((readl(&host->regs->rintsts) & (INTMSK_CDONE | INTMSK_ACD)) == 0) {
		uint32_t rintsts = readl(&host->regs->rintsts);
		if (rintsts) {
			mmc_debug("there're pending interrupts %#x\n", rintsts);
		}
	}

	// Clear all pending interrupts before sending a command.
	writel(INTMSK_ALL, &host->regs->rintsts);

	if (data) {
		if (mshci_prepare_data(host, data)) {
			mmc_error("fail to prepare data\n");
			return -1;
		}
	}

	writel(cmd->cmdarg, &host->regs->cmdarg);

	if (data)
		flags = mshci_set_transfer_mode(host, data);

	if ((cmd->resp_type & MMC_RSP_136) && (cmd->resp_type & MMC_RSP_BUSY)) {
		// Out of SD spec.
		mmc_error("wrong response type or response busy for cmd %d\n",
			  cmd->cmdidx);
		return -1;
	}

	if (cmd->resp_type & MMC_RSP_PRESENT) {
		flags |= CMD_RESP_EXP_BIT;
		if (cmd->resp_type & MMC_RSP_136)
			flags |= CMD_RESP_LENGTH_BIT;
	}

	if (cmd->resp_type & MMC_RSP_CRC)
		flags |= CMD_CHECK_CRC_BIT;
	flags |= (cmd->cmdidx | CMD_STRT_BIT | CMD_USE_HOLD_REG |
		  CMD_WAIT_PRV_DAT_BIT);

	mask = readl(&host->regs->cmd);
	if (mask & CMD_STRT_BIT) {
		mmc_error("cmd busy, current cmd: %d", cmd->cmdidx);
		return -1;
	}

	writel(flags, &host->regs->cmd);
	/* wait for command complete by busy waiting. */
	for (i = 0; i < S5P_MSHC_COMMAND_TIMEOUT; i++) {
		mask = readl(&host->regs->rintsts);
		if (mask & INTMSK_CDONE) {
			if (!data)
				writel(mask, &host->regs->rintsts);
			break;
		}
	}

	/* timeout for command complete. */
	if (S5P_MSHC_COMMAND_TIMEOUT == i) {
		mmc_error("timeout waiting for status update\n");
		return MMC_TIMEOUT;
	}

	if (mask & INTMSK_RTO)
		return MMC_TIMEOUT;
	else if (mask & INTMSK_RE) {
		mmc_error("response mmc_error: %#x cmd: %d\n",
			  mask, cmd->cmdidx);
		return -1;
	}
	if (cmd->resp_type & MMC_RSP_PRESENT) {
		if (cmd->resp_type & MMC_RSP_136) {
			// CRC is stripped so we need to do some shifting.
			cmd->response[0] = readl(&host->regs->resp3);
			cmd->response[1] = readl(&host->regs->resp2);
			cmd->response[2] = readl(&host->regs->resp1);
			cmd->response[3] = readl(&host->regs->resp0);
		} else {
			cmd->response[0] = readl(&host->regs->resp0);
			mmc_debug("\tcmd->response[0]: %#08x\n",
				  cmd->response[0]);
		}
	}

	if (data) {
		if (mmc_busy_wait_io_until(&host->regs->rintsts,
					   &mask,
					   DATA_ERR | DATA_TOUT | INTMSK_DTO,
					   S5P_MSHC_COMMAND_TIMEOUT)) {
			mmc_debug("timeout on data error\n");
			return -1;
		}
		mmc_debug("%s: writel(mask, rintsts)\n", __func__);
		writel(mask, &host->regs->rintsts);

		if (data->flags & MMC_DATA_READ) {
			size_t data_len = data->blocks * data->blocksize;
			dcache_clean_invalidate_by_mva(data->dest, data_len);
		}
		mmc_debug("%s: clrbits32(ctrl, +DMA, +IDMAC)\n", __func__);
		// Disable IDMAC and IDMAC_Interrupts.
		clrbits32(&host->regs->ctrl, DMA_ENABLE | ENABLE_IDMAC);
		if (mask & (DATA_ERR | DATA_TOUT)) {
			mmc_error("error during transfer: %#x\n", mask);
			// mask all interrupt source of IDMAC.
			writel(0, &host->regs->idinten);
			return -1;
		} else if (mask & INTMSK_DTO) {
			mmc_debug("mshci dma interrupt end\n");
		} else {
			mmc_error("unexpected condition %#x\n", mask);
			return -1;
		}
		clrbits32(&host->regs->ctrl, DMA_ENABLE | ENABLE_IDMAC);
		/* mask all interrupt source of IDMAC */
		writel(0, &host->regs->idinten);
	}

	/* TODO(alim.akhtar@samsung.com): check why we need this delay */
	udelay(100);
	return 0;
}

static int mshci_clock_onoff(MshciHost *host, int val)
{
	if (val)
		writel(CLK_ENABLE, &host->regs->clkena);
	else
		writel(CLK_DISABLE, &host->regs->clkena);

	writel(0, &host->regs->cmd);
	writel(CMD_ONLY_CLK, &host->regs->cmd);

	/* wait till command is taken by CIU, when this bit is set
	 * host should not attempted to write to any command registers.
	 */
	if (mmc_busy_wait_io(&host->regs->cmd, NULL, CMD_STRT_BIT,
			     S5P_MSHC_COMMAND_TIMEOUT)) {
		mmc_error("Clock %s has failed.\n ", val ? "ON" : "OFF");
		return -1;
	}

	return 0;
}

static int mshci_change_clock(MshciHost *host, uint32_t clock)
{
	if (clock == host->clock)
		return 0;

	/* If Input clock is higher than maximum mshc clock */
	if (clock > MAX_MSHCI_CLOCK) {
		mmc_debug("Input clock is too high\n");
		clock = MAX_MSHCI_CLOCK;
	}

	/* disable the clock before changing it */
	if (mshci_clock_onoff(host, CLK_DISABLE)) {
		mmc_error("failed to DISABLE clock\n");
		return -1;
	}

	/* Calculate clock division */
	uint32_t sclk_mshc = host->src_hz /
		(((host->clksel_val >> 24) & 0x7) + 1);
	int div;
	for (div = 1 ; div <= 0xff; div++) {
		if (((sclk_mshc / 4) / (2 * div)) <= clock) {
			writel(div, &host->regs->clkdiv);
			break;
		}
	}

	writel(div, &host->regs->clkdiv);

	writel(0, &host->regs->cmd);
	writel(CMD_ONLY_CLK, &host->regs->cmd);

	/*
	 * wait till command is taken by CIU, when this bit is set
	 * host should not attempted to write to any command registers.
	 */
	if (mmc_busy_wait_io(&host->regs->cmd, NULL, CMD_STRT_BIT,
			     S5P_MSHC_COMMAND_TIMEOUT)) {
		mmc_error("Changing clock has timed out.\n");
		return -1;
	}

	clrbits32(&host->regs->cmd, CMD_SEND_CLK_ONLY);
	if (mshci_clock_onoff(host, CLK_ENABLE)) {
		mmc_error("failed to ENABLE clock\n");
		return -1;
	}

	host->clock = clock;
	return 0;
}

static void s5p_mshci_set_ios(MmcCtrlr *ctrlr)
{
	MshciHost *host = container_of(ctrlr, MshciHost, mmc);

	mmc_debug("bus_width: %x, clock: %d\n",
		  ctrlr->bus_width, ctrlr->bus_hz);
	if (ctrlr->bus_hz > 0 && mshci_change_clock(host, ctrlr->bus_hz))
		mmc_debug("mshci_change_clock failed\n");

	if (ctrlr->bus_width == 8)
		writel(PORT0_CARD_WIDTH8, &host->regs->ctype);
	else if (ctrlr->bus_width == 4)
		writel(PORT0_CARD_WIDTH4, &host->regs->ctype);
	else
		writel(PORT0_CARD_WIDTH1, &host->regs->ctype);

	writel(host->clksel_val, &host->regs->clksel);
}

static void mshci_fifo_init(MshciHost *host)
{
	int fifo_val, fifo_depth, fifo_threshold;

	fifo_val = readl(&host->regs->fifoth);

	fifo_depth = 0x80;
	fifo_threshold = fifo_depth / 2;

	fifo_val &= ~(RX_WMARK | TX_WMARK | MSIZE_MASK);
	fifo_val |= (fifo_threshold | (fifo_threshold << 16) | MSIZE_8);
	writel(fifo_val, &host->regs->fifoth);
}

static int mshci_init(MshciHost *host)
{
	writel(POWER_ENABLE, &host->regs->pwren);

	if (mshci_reset_all(host)) {
		mmc_error("mshci_reset_all() failed\n");
		return -1;
	}

	mshci_fifo_init(host);

	/* clear all pending interrupts */
	writel(INTMSK_ALL, &host->regs->rintsts);

	/* interrupts are not used, disable all */
	writel(0, &host->regs->intmask);
	return 0;
}

static int s5p_mshci_init(BlockDevCtrlrOps *me)
{
	MshciHost *host = container_of(me, MshciHost, mmc.ctrlr.ops);
	unsigned int ier;

	if (mshci_init(host)) {
		mmc_error("mshci_init() failed\n");
		return -1;
	}

	/* enumerate at 400KHz */
	if (mshci_change_clock(host, 400000)) {
		mmc_error("mshci_change_clock failed\n");
		return -1;
	}

	/* set auto stop command */
	ier = readl(&host->regs->ctrl);
	ier |= SEND_AS_CCSD;
	writel(ier, &host->regs->ctrl);

	/* set 1bit card mode */
	writel(PORT0_CARD_WIDTH1, &host->regs->ctype);

	writel(0xfffff, &host->regs->debnce);

	/* set bus mode register for IDMAC */
	writel(BMOD_IDMAC_RESET, &host->regs->bmod);

	writel(0x0, &host->regs->idinten);

	/* set the max timeout for data and response */
	writel(TMOUT_MAX, &host->regs->tmout);

	return 0;
}

static int s5p_mshc_update(BlockDevCtrlrOps *me)
{
	MshciHost *host = container_of(me, MshciHost, mmc.ctrlr.ops);

	if (!host->initialized && s5p_mshci_init(me))
		return -1;

	host->initialized = 1;

	if (host->removable) {
		// cdetect is active low
		int present = !readl(&host->regs->cdetect);

		if (present && !host->mmc.media) {
			// A card is present and not set up yet. Get it ready.
			if (mmc_setup_media(&host->mmc))
				return -1;
			host->mmc.media->dev.name = "removable s5o mmc";
			host->mmc.media->dev.removable = 1;
			host->mmc.media->dev.ops.read = &block_mmc_read;
			host->mmc.media->dev.ops.write = &block_mmc_write;
			host->mmc.media->dev.ops.new_stream =
						&new_simple_stream;
			list_insert_after(&host->mmc.media->dev.list_node,
					  &removable_block_devices);
		} else if (!present && host->mmc.media) {
			// A card was present but isn't any more. Get rid of it.
			list_remove(&host->mmc.media->dev.list_node);
			free(host->mmc.media);
			host->mmc.media = NULL;
		}
	} else {
		if (mmc_setup_media(&host->mmc))
			return -1;
		host->mmc.media->dev.name = "s5p mmc";
		host->mmc.media->dev.removable = 0;
		host->mmc.media->dev.ops.read = &block_mmc_read;
		host->mmc.media->dev.ops.write = &block_mmc_write;
		host->mmc.media->dev.ops.new_stream = &new_simple_stream;
		list_insert_after(&host->mmc.media->dev.list_node,
				  &fixed_block_devices);
		host->mmc.ctrlr.need_update = 0;
	}

	return 0;
}

MshciHost *new_mshci_host(uintptr_t ioaddr, uint32_t src_hz, int bus_width,
			  int removable, uint32_t clksel_val)
{
	MshciHost *ctrlr = xzalloc(sizeof(*ctrlr));

	ctrlr->mmc.ctrlr.ops.update = &s5p_mshc_update;
	ctrlr->mmc.ctrlr.need_update = 1;

	ctrlr->mmc.voltages = MMC_VDD_32_33 | MMC_VDD_33_34;
	ctrlr->mmc.f_min = MIN_MSHCI_CLOCK;
	ctrlr->mmc.f_max = MAX_MSHCI_CLOCK;
	ctrlr->mmc.bus_width = bus_width;
	ctrlr->mmc.bus_hz = ctrlr->mmc.f_min;
	ctrlr->mmc.b_max = 65535; // Some controllers use 16-bit regs.
	if (bus_width == 8) {
		ctrlr->mmc.caps |= MMC_MODE_8BIT;
		ctrlr->mmc.caps &= ~MMC_MODE_4BIT;
	} else {
		ctrlr->mmc.caps |= MMC_MODE_4BIT;
		ctrlr->mmc.caps &= ~MMC_MODE_8BIT;
	}
	ctrlr->mmc.caps |= MMC_MODE_HS_52MHz | MMC_MODE_HS | MMC_MODE_HC;
	ctrlr->mmc.send_cmd = &s5p_mshci_send_command;
	ctrlr->mmc.set_ios = &s5p_mshci_set_ios;

	ctrlr->regs = (void *)ioaddr;
	ctrlr->src_hz = src_hz;
	ctrlr->clksel_val = clksel_val;
	ctrlr->removable = removable;

	return ctrlr;
}
