/*
 * This file is part of OMAP DSP driver (DSP Gateway version 3.3.1)
 *
 * Copyright (C) 2002-2006 Nokia Corporation. All rights reserved.
 *
 * Contact: Toshihiro Kobayashi <toshihiro.kobayashi@nokia.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * 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., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/sched.h>
#include <linux/mutex.h>
#include <linux/err.h>
#include <linux/clk.h>
#include <asm/delay.h>
#include <asm/arch/mailbox.h>
#include <asm/arch/dsp_common.h>
#include "dsp_mbcmd.h"
#include "dsp.h"
#include "ipbuf.h"
#include "dsp_common.h"

MODULE_AUTHOR("Toshihiro Kobayashi <toshihiro.kobayashi@nokia.com>");
MODULE_DESCRIPTION("OMAP DSP driver module");
MODULE_LICENSE("GPL");

struct omap_dsp *omap_dsp;
static struct sync_seq *mbseq;
static u16 mbseq_expect_tmp;
static u16 *mbseq_expect = &mbseq_expect_tmp;

extern void dsp_mem_late_init(void);

/*
 * mailbox commands
 */
extern void mbox_wdsnd(struct mbcmd *mb);
extern void mbox_wdreq(struct mbcmd *mb);
extern void mbox_bksnd(struct mbcmd *mb);
extern void mbox_bkreq(struct mbcmd *mb);
extern void mbox_bkyld(struct mbcmd *mb);
extern void mbox_bksndp(struct mbcmd *mb);
extern void mbox_bkreqp(struct mbcmd *mb);
extern void mbox_tctl(struct mbcmd *mb);
extern void mbox_poll(struct mbcmd *mb);
#ifdef OLD_BINARY_SUPPORT
/* v3.3 obsolete */
extern void mbox_wdt(struct mbcmd *mb);
#endif
extern void mbox_suspend(struct mbcmd *mb);
static void mbox_kfunc(struct mbcmd *mb);
extern void mbox_tcfg(struct mbcmd *mb);
extern void mbox_tadd(struct mbcmd *mb);
extern void mbox_tdel(struct mbcmd *mb);
extern void mbox_dspcfg(struct mbcmd *mb);
extern void mbox_regrw(struct mbcmd *mb);
extern void mbox_getvar(struct mbcmd *mb);
extern void mbox_err(struct mbcmd *mb);
extern void mbox_dbg(struct mbcmd *mb);

static const struct cmdinfo
	cif_wdsnd    = { "WDSND",    CMD_L_TYPE_TID,    mbox_wdsnd   },
	cif_wdreq    = { "WDREQ",    CMD_L_TYPE_TID,    mbox_wdreq   },
	cif_bksnd    = { "BKSND",    CMD_L_TYPE_TID,    mbox_bksnd   },
	cif_bkreq    = { "BKREQ",    CMD_L_TYPE_TID,    mbox_bkreq   },
	cif_bkyld    = { "BKYLD",    CMD_L_TYPE_NULL,   mbox_bkyld   },
	cif_bksndp   = { "BKSNDP",   CMD_L_TYPE_TID,    mbox_bksndp  },
	cif_bkreqp   = { "BKREQP",   CMD_L_TYPE_TID,    mbox_bkreqp  },
	cif_tctl     = { "TCTL",     CMD_L_TYPE_TID,    mbox_tctl    },
	cif_poll     = { "POLL",     CMD_L_TYPE_NULL,   mbox_poll    },
#ifdef OLD_BINARY_SUPPORT
	/* v3.3 obsolete */
	cif_wdt      = { "WDT",      CMD_L_TYPE_NULL,   mbox_wdt     },
#endif
	cif_runlevel = { "RUNLEVEL", CMD_L_TYPE_SUBCMD, NULL        },
	cif_pm       = { "PM",       CMD_L_TYPE_SUBCMD, NULL        },
	cif_suspend  = { "SUSPEND",  CMD_L_TYPE_NULL,   mbox_suspend },
	cif_kfunc    = { "KFUNC",    CMD_L_TYPE_SUBCMD, mbox_kfunc   },
	cif_tcfg     = { "TCFG",     CMD_L_TYPE_TID,    mbox_tcfg    },
	cif_tadd     = { "TADD",     CMD_L_TYPE_TID,    mbox_tadd    },
	cif_tdel     = { "TDEL",     CMD_L_TYPE_TID,    mbox_tdel    },
	cif_tstop    = { "TSTOP",    CMD_L_TYPE_TID,    NULL        },
	cif_dspcfg   = { "DSPCFG",   CMD_L_TYPE_SUBCMD, mbox_dspcfg  },
	cif_regrw    = { "REGRW",    CMD_L_TYPE_SUBCMD, mbox_regrw   },
	cif_getvar   = { "GETVAR",   CMD_L_TYPE_SUBCMD, mbox_getvar  },
	cif_setvar   = { "SETVAR",   CMD_L_TYPE_SUBCMD, NULL        },
	cif_err      = { "ERR",      CMD_L_TYPE_SUBCMD, mbox_err     },
	cif_dbg      = { "DBG",      CMD_L_TYPE_NULL,   mbox_dbg     };

#define MBOX_CMD_MAX	0x80
const struct cmdinfo *cmdinfo[MBOX_CMD_MAX] = {
	[MBOX_CMD_DSP_WDSND]    = &cif_wdsnd,
	[MBOX_CMD_DSP_WDREQ]    = &cif_wdreq,
	[MBOX_CMD_DSP_BKSND]    = &cif_bksnd,
	[MBOX_CMD_DSP_BKREQ]    = &cif_bkreq,
	[MBOX_CMD_DSP_BKYLD]    = &cif_bkyld,
	[MBOX_CMD_DSP_BKSNDP]   = &cif_bksndp,
	[MBOX_CMD_DSP_BKREQP]   = &cif_bkreqp,
	[MBOX_CMD_DSP_TCTL]     = &cif_tctl,
	[MBOX_CMD_DSP_POLL]     = &cif_poll,
#ifdef OLD_BINARY_SUPPORT
	[MBOX_CMD_DSP_WDT]      = &cif_wdt, /* v3.3 obsolete */
#endif
	[MBOX_CMD_DSP_RUNLEVEL] = &cif_runlevel,
	[MBOX_CMD_DSP_PM]       = &cif_pm,
	[MBOX_CMD_DSP_SUSPEND]  = &cif_suspend,
	[MBOX_CMD_DSP_KFUNC]    = &cif_kfunc,
	[MBOX_CMD_DSP_TCFG]     = &cif_tcfg,
	[MBOX_CMD_DSP_TADD]     = &cif_tadd,
	[MBOX_CMD_DSP_TDEL]     = &cif_tdel,
	[MBOX_CMD_DSP_TSTOP]    = &cif_tstop,
	[MBOX_CMD_DSP_DSPCFG]   = &cif_dspcfg,
	[MBOX_CMD_DSP_REGRW]    = &cif_regrw,
	[MBOX_CMD_DSP_GETVAR]   = &cif_getvar,
	[MBOX_CMD_DSP_SETVAR]   = &cif_setvar,
	[MBOX_CMD_DSP_ERR]      = &cif_err,
	[MBOX_CMD_DSP_DBG]      = &cif_dbg,
};

static int dsp_kfunc_probe_devices(struct omap_dsp *dsp)
{
	struct dsp_kfunc_device *p;
	int ret, fail = 0;

	mutex_lock(&dsp->lock);
	list_for_each_entry(p, dsp->kdev_list, entry) {
		if (p->probe == NULL)
			continue;
		ret = p->probe(p);
		if (ret) {
			printk(KERN_ERR
			       "probing %s failed\n", p->name);
			fail++;
		}
	}
	mutex_unlock(&dsp->lock);

	pr_debug("%s() fail:%d\n", __FUNCTION__, fail);

	return fail;
}

static int dsp_kfunc_remove_devices(struct omap_dsp *dsp)
{
	struct dsp_kfunc_device *p;
	int ret, fail = 0;

	mutex_lock(&dsp->lock);
	list_for_each_entry_reverse(p, dsp->kdev_list, entry) {
		if (p->remove == NULL)
			continue;
		ret = p->remove(p);
		if (ret) {
			printk(KERN_ERR
			       "removing %s failed\n", p->name);
			fail++;
		}
	}
	mutex_unlock(&dsp->lock);

	pr_debug("%s() fail:%d\n", __FUNCTION__, fail);

	return fail;
}

static int dsp_kfunc_enable_devices(struct omap_dsp *dsp, int type, int stage)
{
	struct dsp_kfunc_device *p;
	int ret, fail = 0;

	mutex_lock(&dsp->lock);
	list_for_each_entry(p, dsp->kdev_list, entry) {
		if ((p->type != type) || (p->enable == NULL))
			continue;
		ret = p->enable(p, stage);
		if (ret) {
			printk(KERN_ERR
			       "enabling %s failed\n", p->name);
			fail++;
		}
	}
	mutex_unlock(&dsp->lock);

	pr_debug("%s(%d) fail:%d\n", __FUNCTION__, type, fail);

	return fail;
}

static int dsp_kfunc_disable_devices(struct omap_dsp *dsp, int type, int stage)
{
	struct dsp_kfunc_device *p;
	int ret, fail = 0;

	mutex_lock(&dsp->lock);
	list_for_each_entry_reverse(p, omap_dsp->kdev_list, entry) {
		if ((p->type != type) || (p->disable == NULL))
			continue;
		ret = p->disable(p, stage);
		if (ret) {
			printk(KERN_ERR
			       "disabling %s failed\n", p->name);
			fail++;
		}
	}
	mutex_unlock(&dsp->lock);

	pr_debug("%s(%d) fail:%d\n", __FUNCTION__, type, fail);

	return fail;
}

int sync_with_dsp(u16 *adr, u16 val, int try_cnt)
{
	int try;

	if (*(volatile u16 *)adr == val)
		return 0;

	for (try = 0; try < try_cnt; try++) {
		udelay(1);
		if (*(volatile u16 *)adr == val) {
			/* success! */
			pr_info("omapdsp: sync_with_dsp(): try = %d\n", try);
			return 0;
		}
	}

	/* fail! */
	return -1;
}

static int mbcmd_sender_prepare(void *data)
{
	struct mb_exarg *arg = data;
	int i, ret = 0;
	/*
	 * even if ipbuf_sys_ad is in DSP internal memory,
	 * dsp_mem_enable() never cause to call PM mailbox command
	 * because in that case DSP memory should be always enabled.
	 * (see ipbuf_sys_hold_mem_active in ipbuf.c)
	 *
	 * Therefore, we can call this function here safely.
	 */
	if (sync_with_dsp(&ipbuf_sys_ad->s, TID_FREE, 10) < 0) {
		printk(KERN_ERR "omapdsp: ipbuf_sys_ad is busy.\n");
		ret = -EBUSY;
		goto out;
	}

	for (i = 0; i < arg->argc; i++) {
		ipbuf_sys_ad->d[i] = arg->argv[i];
	}
	ipbuf_sys_ad->s = arg->tid;
 out:
	return ret;
}

/*
 * __dsp_mbcmd_send_exarg(): mailbox dispatcher
 */
int __dsp_mbcmd_send_exarg(struct mbcmd *mb, struct mb_exarg *arg,
			   int recovery_flag)
{
	int ret = 0;

	if (unlikely(omap_dsp->enabled == 0)) {
		ret = dsp_kfunc_enable_devices(omap_dsp,
					       DSP_KFUNC_DEV_TYPE_COMMON, 0);
		if (ret == 0)
			omap_dsp->enabled = 1;
	}

	/*
	 * while MMU fault is set,
	 * only recovery command can be executed
	 */
	if (dsp_err_isset(ERRCODE_MMU) && !recovery_flag) {
		printk(KERN_ERR
		       "mbox: mmu interrupt is set. %s is aborting.\n",
		       cmd_name(*mb));
		goto out;
	}

	if (arg)
		dsp_mem_enable(ipbuf_sys_ad);

	ret = omap_mbox_msg_send(omap_dsp->mbox,
				 *(mbox_msg_t *)mb, (void*)arg);
	if (ret)
		goto out;

	if (mbseq)
		mbseq->ad_arm++;

	mblog_add(mb, DIR_A2D);
 out:
	if (arg)
		dsp_mem_disable(ipbuf_sys_ad);

	return ret;
}

int dsp_mbcmd_send_and_wait_exarg(struct mbcmd *mb, struct mb_exarg *arg,
				  wait_queue_head_t *q)
{
	long current_state;
	DECLARE_WAITQUEUE(wait, current);

	add_wait_queue(q, &wait);
	current_state = current->state;
	set_current_state(TASK_INTERRUPTIBLE);
	if (dsp_mbcmd_send_exarg(mb, arg) < 0) {
		set_current_state(current_state);
		remove_wait_queue(q, &wait);
		return -1;
	}
	schedule_timeout(DSP_TIMEOUT);
	set_current_state(current_state);
	remove_wait_queue(q, &wait);

	return 0;
}

/*
 * mbcmd receiver
 */
static void mbcmd_receiver(mbox_msg_t msg)
{
	struct mbcmd *mb = (struct mbcmd *)&msg;

	if (cmdinfo[mb->cmd_h] == NULL) {
		printk(KERN_ERR
		       "invalid message (%08x) for mbcmd_receiver().\n", msg);
		return;
	}

	(*mbseq_expect)++;

	mblog_add(mb, DIR_D2A);

	/* call handler for the command */
	if (cmdinfo[mb->cmd_h]->handler)
		cmdinfo[mb->cmd_h]->handler(mb);
	else
		printk(KERN_ERR "mbox: %s is not allowed from DSP.\n",
		       cmd_name(*mb));
}

static int mbsync_hold_mem_active;

void dsp_mbox_start(void)
{
	omap_mbox_init_seq(omap_dsp->mbox);
	mbseq_expect_tmp = 0;
}

void dsp_mbox_stop(void)
{
	mbseq = NULL;
	mbseq_expect = &mbseq_expect_tmp;
}

int dsp_mbox_config(void *p)
{
	unsigned long flags;

	if (dsp_address_validate(p, sizeof(struct sync_seq), "mbseq") < 0)
		return -1;
	if (dsp_mem_type(p, sizeof(struct sync_seq)) != MEM_TYPE_EXTERN) {
		printk(KERN_WARNING
		       "omapdsp: mbseq is placed in DSP internal memory.\n"
		       "         It will prevent DSP from idling.\n");
		mbsync_hold_mem_active = 1;
		/*
		 * dsp_mem_enable() never fails because
		 * it has been already enabled in dspcfg process and
		 * this will just increment the usecount.
		 */
		dsp_mem_enable((void *)daram_base);
	}

	local_irq_save(flags);
	mbseq = p;
	mbseq->da_arm = mbseq_expect_tmp;
	mbseq_expect = &mbseq->da_arm;
	local_irq_restore(flags);

	return 0;
}

static int __init dsp_mbox_init(void)
{
	omap_dsp->mbox = omap_mbox_get("dsp");
	if (IS_ERR(omap_dsp->mbox)) {
		printk(KERN_ERR "failed to get mailbox handler for DSP.\n");
		return -ENODEV;
	}

	omap_dsp->mbox->msg_receive_cb = mbcmd_receiver;
	omap_dsp->mbox->msg_sender_cb = mbcmd_sender_prepare;

	return 0;
}

static void dsp_mbox_exit(void)
{
	omap_dsp->mbox->msg_sender_cb = NULL;
	omap_dsp->mbox->msg_receive_cb = NULL;

	if (mbsync_hold_mem_active) {
		dsp_mem_disable((void *)daram_base);
		mbsync_hold_mem_active = 0;
	}
}

/*
 * kernel function dispatcher
 */
extern void mbox_fbctl_upd(void);
extern void mbox_fbctl_disable(struct mbcmd *mb);

static void mbox_kfunc_fbctl(struct mbcmd *mb)
{
	switch (mb->data) {
	case FBCTL_UPD:
		mbox_fbctl_upd();
		break;
	case FBCTL_DISABLE:
		mbox_fbctl_disable(mb);
		break;
	default:
		printk(KERN_ERR
		       "mbox: Unknown FBCTL from DSP: 0x%04x\n", mb->data);
	}
}

/*
 * dspgw: KFUNC message handler
 */
static void mbox_kfunc_power(unsigned short data)
{
	int ret = -1;

	switch (data) {
	case DVFS_START: /* ACK from DSP */
		/* TBD */
		break;
	case AUDIO_PWR_UP:
		ret = dsp_kfunc_enable_devices(omap_dsp,
					       DSP_KFUNC_DEV_TYPE_AUDIO, 0);
		if (ret == 0)
			ret++;
		break;
	case AUDIO_PWR_DOWN: /* == AUDIO_PWR_DOWN1 */
		ret = dsp_kfunc_disable_devices(omap_dsp,
						DSP_KFUNC_DEV_TYPE_AUDIO, 1);
		break;
	case AUDIO_PWR_DOWN2:
		ret = dsp_kfunc_disable_devices(omap_dsp,
						DSP_KFUNC_DEV_TYPE_AUDIO, 2);
		break;
	case DSP_PWR_DOWN:
		ret = dsp_kfunc_disable_devices(omap_dsp,
						DSP_KFUNC_DEV_TYPE_COMMON, 0);
		if (ret == 0)
			omap_dsp->enabled = 0;
		break;
	default:
		printk(KERN_ERR
		       "mailbox: Unknown PWR from DSP: 0x%04x\n", data);
		break;
	}

	if (unlikely(ret < 0)) {
		printk(KERN_ERR "mailbox: PWR(0x%04x) failed\n", data);
		return;
	}

	if (likely(ret == 0))
		return;

	mbcompose_send(KFUNC, KFUNC_POWER, data);
}

static void mbox_kfunc(struct mbcmd *mb)
{
	switch (mb->cmd_l) {
	case KFUNC_FBCTL:
		mbox_kfunc_fbctl(mb);
		break;
	case KFUNC_POWER:
		mbox_kfunc_power(mb->data);
		break;
	default:
		printk(KERN_ERR
		       "mbox: Unknown KFUNC from DSP: 0x%02x\n", mb->cmd_l);
	}
}

int dsp_late_init(void)
{
	int ret;

	dsp_clk_autoidle();

#ifdef CONFIG_ARCH_OMAP2
	clk_enable(dsp_fck_handle);
	clk_enable(dsp_ick_handle);
	__dsp_per_enable();
#endif
	dsp_mem_late_init();

#ifdef CONFIG_ARCH_OMAP1
	dsp_set_idle_boot_base(IDLEPG_BASE, IDLEPG_SIZE);
#endif
	ret = dsp_kfunc_enable_devices(omap_dsp,
				       DSP_KFUNC_DEV_TYPE_COMMON, 0);
	if (ret == 0)
		omap_dsp->enabled = 0;

	return 0;
}

extern int  dsp_ctl_core_init(void);
extern void dsp_ctl_core_exit(void);
extern void dsp_ctl_init(void);
extern void dsp_ctl_exit(void);
extern int  dsp_mem_init(void);
extern void dsp_mem_exit(void);
extern void mblog_init(void);
extern void mblog_exit(void);
extern int  dsp_taskmod_init(void);
extern void dsp_taskmod_exit(void);

/*
 * driver functions
 */
static int __init dsp_drv_probe(struct platform_device *pdev)
{
	int ret;
	struct omap_dsp *info;
	struct dsp_platform_data *pdata = pdev->dev.platform_data;

	dev_info(&pdev->dev, "OMAP DSP driver initialization\n");

	info = kzalloc(sizeof(struct omap_dsp), GFP_KERNEL);
	if (unlikely(info == NULL)) {
		dev_dbg(&pdev->dev, "no memory for info\n");
		return -ENOMEM;
	}
	platform_set_drvdata(pdev, info);
	omap_dsp = info;

	mutex_init(&info->lock);
	info->dev = &pdev->dev;
	info->kdev_list = &pdata->kdev_list;

	ret = dsp_kfunc_probe_devices(info);
	if (ret) {
		ret = -ENXIO;
		goto fail0;
	}

	info->mmu_irq = platform_get_irq_byname(pdev, "dsp_mmu");
	if (unlikely(info->mmu_irq) < 0) {
		ret = -ENXIO;
		goto fail1;
	}

	if ((ret = dsp_ctl_core_init()) < 0)
		goto fail2;
	if ((ret = dsp_mem_init()) < 0)
		goto fail3;
	dsp_ctl_init();
	mblog_init();
	if ((ret = dsp_taskmod_init()) < 0)
		goto fail4;
	if ((ret = dsp_mbox_init()) < 0)
		goto fail5;

	return 0;

 fail5:
	dsp_taskmod_exit();
 fail4:
	mblog_exit();
	dsp_ctl_exit();
	dsp_mem_exit();
 fail3:
	dsp_ctl_core_exit();
 fail2:
 fail1:
	dsp_kfunc_remove_devices(info);
 fail0:
	kfree(info);

	return ret;
}

static int dsp_drv_remove(struct platform_device *pdev)
{
	struct omap_dsp *info = platform_get_drvdata(pdev);

	dsp_cpustat_request(CPUSTAT_RESET);

	dsp_cfgstat_request(CFGSTAT_CLEAN);
	dsp_mbox_exit();
	dsp_taskmod_exit();
	mblog_exit();
	dsp_ctl_exit();
	dsp_mem_exit();

	dsp_ctl_core_exit();

#ifdef CONFIG_ARCH_OMAP2
	__dsp_per_disable();
	clk_disable(dsp_ick_handle);
	clk_disable(dsp_fck_handle);
#endif
	dsp_kfunc_remove_devices(info);
	kfree(info);

	return 0;
}

#ifdef CONFIG_PM
static int dsp_drv_suspend(struct platform_device *pdev, pm_message_t state)
{
	dsp_cfgstat_request(CFGSTAT_SUSPEND);

	return 0;
}

static int dsp_drv_resume(struct platform_device *pdev)
{
	dsp_cfgstat_request(CFGSTAT_RESUME);

	return 0;
}
#else
#define dsp_drv_suspend		NULL
#define dsp_drv_resume		NULL
#endif /* CONFIG_PM */

static struct platform_driver dsp_driver = {
	.probe		= dsp_drv_probe,
	.remove		= dsp_drv_remove,
	.suspend	= dsp_drv_suspend,
	.resume		= dsp_drv_resume,
	.driver		= {
		.name	= "dsp",
	},
};

static int __init omap_dsp_mod_init(void)
{
	return platform_driver_register(&dsp_driver);
}

static void __exit omap_dsp_mod_exit(void)
{
	platform_driver_unregister(&dsp_driver);
}

module_init(omap_dsp_mod_init);
module_exit(omap_dsp_mod_exit);
