/*
 * imx-wm8962.c
 *
 * Copyright (C) 2012-2013 Freescale Semiconductor, Inc. All Rights Reserved.
 */

/*
 * The code contained herein is licensed under the GNU General Public
 * License. You may obtain a copy of the GNU General Public License
 * Version 2 or later at the following locations:
 *
 * http://www.opensource.org/licenses/gpl-license.html
 * http://www.gnu.org/copyleft/gpl.html
 */

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/pm.h>
#include <linux/bitops.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/irq.h>
#include <linux/io.h>
#include <linux/fsl_devices.h>
#include <linux/slab.h>
#include <linux/clk.h>
#include <linux/kthread.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>
#include <sound/soc.h>
#include <sound/soc-dapm.h>
#include <sound/initval.h>
#include <sound/jack.h>
#include <mach/dma.h>
#include <mach/clock.h>
#include <mach/audmux.h>
#include <mach/gpio.h>
#include <asm/mach-types.h>

#include "imx-ssi.h"
#include "../codecs/wm8962.h"

struct imx_priv {
	int sysclk;         /*mclk from the outside*/
	int codec_sysclk;
	int dai_hifi;
	int hp_irq;
	int hp_status;
	int amic_irq;
	int amic_status;
	struct platform_device *pdev;
	struct snd_pcm_substream *first_stream;
	struct snd_pcm_substream *second_stream;
};
unsigned int sample_format = SNDRV_PCM_FMTBIT_S16_LE;
static struct imx_priv card_priv;
static struct snd_soc_card snd_soc_card_imx;
static struct snd_soc_codec *gcodec;

static struct snd_soc_jack imx_hp_jack;
static struct snd_soc_jack_pin imx_hp_jack_pins[] = {
	{
		.pin = "Ext Spk",
		.mask = SND_JACK_HEADPHONE,
	},
};
static struct snd_soc_jack_gpio imx_hp_jack_gpio = {
	.name = "headphone detect",
	.report = SND_JACK_HEADPHONE,
	.debounce_time = 150,
	.invert = 0,
};

static struct snd_soc_jack imx_mic_jack;
static struct snd_soc_jack_pin imx_mic_jack_pins[] = {
	{
		.pin = "DMIC",
		.mask = SND_JACK_MICROPHONE,
	},
};
static struct snd_soc_jack_gpio imx_mic_jack_gpio = {
	.name = "micphone detect",
	.report = SND_JACK_MICROPHONE,
	.debounce_time = 150,
	.invert = 0,
};

static int imx_hifi_startup(struct snd_pcm_substream *substream)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_dai *codec_dai = rtd->codec_dai;
	struct imx_priv *priv = &card_priv;
	struct mxc_audio_platform_data *plat = priv->pdev->dev.platform_data;

	if (!codec_dai->active)
		plat->clock_enable(1);

	return 0;
}

static void imx_hifi_shutdown(struct snd_pcm_substream *substream)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_dai *codec_dai = rtd->codec_dai;
	struct imx_priv *priv = &card_priv;
	struct mxc_audio_platform_data *plat = priv->pdev->dev.platform_data;

	if (!codec_dai->active)
		plat->clock_enable(0);

	return;
}

static int check_hw_params(struct snd_pcm_substream *substream,
				struct snd_pcm_hw_params *params)
{
	struct imx_priv *priv = &card_priv;
	unsigned int channels = params_channels(params);
	unsigned int sample_rate = params_rate(params);
	snd_pcm_format_t sample_format = params_format(params);

	substream->runtime->sample_bits =
		snd_pcm_format_physical_width(sample_format);
	substream->runtime->rate = sample_rate;
	substream->runtime->format = sample_format;
	substream->runtime->channels = channels;

	if (!priv->first_stream) {
		priv->first_stream = substream;
	} else {
		priv->second_stream = substream;

		/* Check two sample rates of two streams */
		if (priv->first_stream->runtime->rate !=
				priv->second_stream->runtime->rate) {
			pr_err("\n!KEEP THE SAME SAMPLE RATE: %d!\n",
					priv->first_stream->runtime->rate);
			return -EINVAL;
		}

		/* Check two sample bits of two streams */
		if (priv->first_stream->runtime->sample_bits !=
				priv->second_stream->runtime->sample_bits) {
			snd_pcm_format_t first_format =
				priv->first_stream->runtime->format;

			pr_err("\n!KEEP THE SAME FORMAT: %s!\n",
					snd_pcm_format_name(first_format));
			return -EINVAL;
		}

		/* Check two channel numbers of two streams */
		if (priv->first_stream->runtime->channels !=
				priv->second_stream->runtime->channels) {
			pr_err("\n!KEEP THE SAME CHANNEL NUMBER: %d!\n",
					priv->first_stream->runtime->channels);
			return -EINVAL;
		}
	}

	return 0;
}

static int imx_hifi_hw_params(struct snd_pcm_substream *substream,
				     struct snd_pcm_hw_params *params)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
	struct snd_soc_dai *codec_dai = rtd->codec_dai;
	struct imx_priv *priv = &card_priv;
	unsigned int channels = params_channels(params);
	unsigned int sample_rate = 44100;
	int ret = 0;
	u32 dai_format;
	unsigned int pll_out;

	/*
	 * WM8962 doesn't support two substreams in different parameters
	 * (i.e. different sample rates, audio formats, channel numbers)
	 * So we here check the three parameters above of two substreams
	 * if they are running in the same time.
	 */
	ret = check_hw_params(substream, params);
	if (ret < 0) {
		pr_err("Failed to match hw params: %d\n", ret);
		return ret;
	}

	dai_format = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
		SND_SOC_DAIFMT_CBM_CFM;

	/* set codec DAI configuration */
	ret = snd_soc_dai_set_fmt(codec_dai, dai_format);
	if (ret < 0)
		return ret;

	/* set i.MX active slot mask */
	snd_soc_dai_set_tdm_slot(cpu_dai,
				 channels == 1 ? 0xfffffffe : 0xfffffffc,
				 channels == 1 ? 0xfffffffe : 0xfffffffc,
				 2, 32);

	dai_format = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_IF |
		SND_SOC_DAIFMT_CBM_CFM;

	/* set cpu DAI configuration */
	ret = snd_soc_dai_set_fmt(cpu_dai, dai_format);
	if (ret < 0)
		return ret;

	sample_rate = params_rate(params);
	sample_format = params_format(params);

	if (sample_format == SNDRV_PCM_FORMAT_S24_LE)
		pll_out = sample_rate * 192;
	else
		pll_out = sample_rate * 256;

	ret = snd_soc_dai_set_pll(codec_dai, WM8962_FLL_MCLK,
				  WM8962_FLL_MCLK, priv->sysclk,
				  pll_out);
	if (ret < 0)
		pr_err("Failed to start FLL: %d\n", ret);

	ret = snd_soc_dai_set_sysclk(codec_dai,
					 WM8962_SYSCLK_FLL,
					 pll_out,
					 SND_SOC_CLOCK_IN);
	if (ret < 0) {
		pr_err("Failed to set SYSCLK: %d\n", ret);
		return ret;
	}

	return 0;
}


static int imx_hifi_hw_free(struct snd_pcm_substream *substream)
{
	struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_dai *codec_dai = rtd->codec_dai;
	struct imx_priv *priv = &card_priv;
	int ret;

	if (priv->first_stream == substream)
		priv->first_stream = priv->second_stream;
	priv->second_stream = NULL;

	if (!priv->first_stream) {
		/*
		 * wm8962 doesn't allow us to continuously setting FLL,
		 * So we set MCLK as sysclk once, which'd remove the limitation.
		 */
		ret = snd_soc_dai_set_sysclk(codec_dai, WM8962_SYSCLK_MCLK,
				0, SND_SOC_CLOCK_IN);
		if (ret < 0) {
			pr_err("Failed to set SYSCLK: %d\n", ret);
			return ret;
		}

		/*
		 * Continuously setting FLL would cause playback distortion.
		 * We can fix it just by mute codec after playback.
		 */
		ret = snd_soc_dai_digital_mute(codec_dai, 1);
		if (ret < 0) {
			pr_err("Failed to set MUTE: %d\n", ret);
			return ret;
		}
	}
	return 0;
}

static void imx_resume_event(struct work_struct *wor)
{
	struct imx_priv *priv = &card_priv;
	struct platform_device *pdev = priv->pdev;
	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
	struct snd_soc_jack *jack;
	int enable;
	int report;

	if (plat->hp_gpio != -1) {
		jack = imx_hp_jack_gpio.jack;

		enable = gpio_get_value_cansleep(imx_hp_jack_gpio.gpio);
		if (imx_hp_jack_gpio.invert)
			enable = !enable;

		if (enable)
			report = imx_hp_jack_gpio.report;
		else
			report = 0;

		snd_soc_jack_report(jack, report, imx_hp_jack_gpio.report);
	}

	if (plat->mic_gpio != -1) {
		jack = imx_mic_jack_gpio.jack;

		enable = gpio_get_value_cansleep(imx_mic_jack_gpio.gpio);
		if (imx_mic_jack_gpio.invert)
			enable = !enable;

		if (enable)
			report = imx_mic_jack_gpio.report;
		else
			report = 0;

		snd_soc_jack_report(jack, report, imx_mic_jack_gpio.report);
	}

	return;
}

static int imx_event_hp(struct snd_soc_dapm_widget *w,
				struct snd_kcontrol *kcontrol, int event)
{
	struct imx_priv *priv = &card_priv;
	struct platform_device *pdev = priv->pdev;
	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
	char *envp[3];
	char *buf;

	if (plat->hp_gpio != -1) {
		priv->hp_status = gpio_get_value(plat->hp_gpio);

		buf = kmalloc(32, GFP_ATOMIC);
		if (!buf) {
			pr_err("%s kmalloc failed\n", __func__);
			return -ENOMEM;
		}

		if (priv->hp_status != plat->hp_active_low)
			snprintf(buf, 32, "STATE=%d", 2);
		else
			snprintf(buf, 32, "STATE=%d", 0);

		envp[0] = "NAME=headphone";
		envp[1] = buf;
		envp[2] = NULL;
		kobject_uevent_env(&pdev->dev.kobj, KOBJ_CHANGE, envp);
		kfree(buf);
	}

	return 0;
}

static int imx_event_mic(struct snd_soc_dapm_widget *w,
				struct snd_kcontrol *kcontrol, int event)
{
	struct imx_priv *priv = &card_priv;
	struct platform_device *pdev = priv->pdev;
	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
	char *envp[3];
	char *buf;

	if (plat->mic_gpio != -1) {
		priv->amic_status = gpio_get_value(plat->mic_gpio);

		buf = kmalloc(32, GFP_ATOMIC);
		if (!buf) {
			pr_err("%s kmalloc failed\n", __func__);
			return -ENOMEM;
		}

		if (priv->amic_status == 0)
			snprintf(buf, 32, "STATE=%d", 2);
		else
			snprintf(buf, 32, "STATE=%d", 0);

		envp[0] = "NAME=amic";
		envp[1] = buf;
		envp[2] = NULL;
		kobject_uevent_env(&pdev->dev.kobj, KOBJ_CHANGE, envp);
		kfree(buf);
	}

	return 0;
}


static const struct snd_kcontrol_new controls[] = {
	SOC_DAPM_PIN_SWITCH("Ext Spk"),
};

/* imx card dapm widgets */
static const struct snd_soc_dapm_widget imx_dapm_widgets[] = {
	SND_SOC_DAPM_HP("Headphone Jack", NULL),
	SND_SOC_DAPM_SPK("Ext Spk", imx_event_hp),
	SND_SOC_DAPM_MIC("AMIC", NULL),
	SND_SOC_DAPM_MIC("DMIC", imx_event_mic),
};

/* imx machine connections to the codec pins */
static const struct snd_soc_dapm_route audio_map[] = {
	{ "Headphone Jack", NULL, "HPOUTL" },
	{ "Headphone Jack", NULL, "HPOUTR" },

	{ "Ext Spk", NULL, "SPKOUTL" },
	{ "Ext Spk", NULL, "SPKOUTR" },

	{ "MICBIAS", NULL, "AMIC" },
	{ "IN3R", NULL, "MICBIAS" },

	{ "DMIC", NULL, "MICBIAS" },
	{ "DMICDAT", NULL, "DMIC" },

};

static ssize_t show_headphone(struct device_driver *dev, char *buf)
{
	struct imx_priv *priv = &card_priv;
	struct platform_device *pdev = priv->pdev;
	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;

	/* determine whether hp is plugged in */
	priv->hp_status = gpio_get_value(plat->hp_gpio);

	if (priv->hp_status != plat->hp_active_low)
		strcpy(buf, "headphone\n");
	else
		strcpy(buf, "speaker\n");

	return strlen(buf);
}

static DRIVER_ATTR(headphone, S_IRUGO | S_IWUSR, show_headphone, NULL);

static ssize_t show_amic(struct device_driver *dev, char *buf)
{
	struct imx_priv *priv = &card_priv;
	struct platform_device *pdev = priv->pdev;
	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;

	/* determine whether amic is plugged in */
	priv->amic_status = gpio_get_value(plat->hp_gpio);

	if (priv->amic_status != plat->mic_active_low)
		strcpy(buf, "amic\n");
	else
		strcpy(buf, "dmic\n");

	return strlen(buf);
}

static DRIVER_ATTR(amic, S_IRUGO | S_IWUSR, show_amic, NULL);

static DECLARE_DELAYED_WORK(resume_hp_event, imx_resume_event);

int imx_hifi_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct imx_priv *priv = &card_priv;
	struct platform_device *pdev = priv->pdev;
	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;

	if (SNDRV_PCM_TRIGGER_RESUME == cmd) {
		if ((plat->hp_gpio != -1) || (plat->mic_gpio != -1))
			schedule_delayed_work(&resume_hp_event,
				msecs_to_jiffies(200));
	}

	return 0;
}

static int imx_wm8962_init(struct snd_soc_pcm_runtime *rtd)
{
	struct snd_soc_codec *codec = rtd->codec;
	struct imx_priv *priv = &card_priv;
	struct platform_device *pdev = priv->pdev;
	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
	int ret = 0;

	gcodec = rtd->codec;

	/* Add imx specific widgets */
	snd_soc_dapm_new_controls(&codec->dapm, imx_dapm_widgets,
				  ARRAY_SIZE(imx_dapm_widgets));

	/* Set up imx specific audio path audio_map */
	snd_soc_dapm_add_routes(&codec->dapm, audio_map, ARRAY_SIZE(audio_map));

	snd_soc_dapm_enable_pin(&codec->dapm, "Headphone Jack");
	snd_soc_dapm_enable_pin(&codec->dapm, "AMIC");

	if (plat->hp_gpio != -1) {
		imx_hp_jack_gpio.gpio = plat->hp_gpio;
		snd_soc_jack_new(codec, "Ext Spk", SND_JACK_LINEOUT,
				&imx_hp_jack);
		snd_soc_jack_add_pins(&imx_hp_jack,
					ARRAY_SIZE(imx_hp_jack_pins),
					imx_hp_jack_pins);
		snd_soc_jack_add_gpios(&imx_hp_jack,
					1, &imx_hp_jack_gpio);

		ret = driver_create_file(pdev->dev.driver,
							&driver_attr_headphone);
		if (ret < 0) {
			ret = -EINVAL;
			return ret;
		}
	}

	if (plat->mic_gpio != -1) {
		imx_mic_jack_gpio.gpio = plat->mic_gpio;
		snd_soc_jack_new(codec, "DMIC", SND_JACK_MICROPHONE,
				&imx_mic_jack);
		snd_soc_jack_add_pins(&imx_mic_jack,
					ARRAY_SIZE(imx_mic_jack_pins),
					imx_mic_jack_pins);
		snd_soc_jack_add_gpios(&imx_mic_jack,
					1, &imx_mic_jack_gpio);

		ret = driver_create_file(pdev->dev.driver,
							&driver_attr_amic);
		if (ret < 0) {
			ret = -EINVAL;
			return ret;
		}
	} else {
		snd_soc_dapm_nc_pin(&codec->dapm, "DMIC");
	}

	snd_soc_dapm_sync(&codec->dapm);

	return 0;
}

static struct snd_soc_ops imx_hifi_ops = {
	.startup = imx_hifi_startup,
	.shutdown = imx_hifi_shutdown,
	.hw_params = imx_hifi_hw_params,
	.hw_free = imx_hifi_hw_free,
	.trigger = imx_hifi_trigger,
};

static struct snd_soc_dai_link imx_dai[] = {
	{
		.name = "HiFi",
		.stream_name = "HiFi",
		.codec_dai_name	= "wm8962",
		.codec_name	= "wm8962.0-001a",
		.cpu_dai_name	= "imx-ssi.1",
		.platform_name	= "imx-pcm-audio.1",
		.init		= imx_wm8962_init,
		.ops		= &imx_hifi_ops,
	},
};

static struct snd_soc_card snd_soc_card_imx = {
	.name		= "wm8962-audio",
	.dai_link	= imx_dai,
	.num_links	= ARRAY_SIZE(imx_dai),
};

static int imx_audmux_config(int slave, int master)
{
	unsigned int ptcr, pdcr;
	slave = slave - 1;
	master = master - 1;

	ptcr = MXC_AUDMUX_V2_PTCR_SYN |
		MXC_AUDMUX_V2_PTCR_TFSDIR |
		MXC_AUDMUX_V2_PTCR_TFSEL(master) |
		MXC_AUDMUX_V2_PTCR_TCLKDIR |
		MXC_AUDMUX_V2_PTCR_TCSEL(master);
	pdcr = MXC_AUDMUX_V2_PDCR_RXDSEL(master);
	mxc_audmux_v2_configure_port(slave, ptcr, pdcr);

	ptcr = MXC_AUDMUX_V2_PTCR_SYN;
	pdcr = MXC_AUDMUX_V2_PDCR_RXDSEL(slave);
	mxc_audmux_v2_configure_port(master, ptcr, pdcr);

	return 0;
}

/*
 * This function will register the snd_soc_pcm_link drivers.
 */
static int __devinit imx_wm8962_probe(struct platform_device *pdev)
{

	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
	struct imx_priv *priv = &card_priv;
	int ret = 0;

	priv->pdev = pdev;

	imx_audmux_config(plat->src_port, plat->ext_port);

	if (plat->init && plat->init()) {
		ret = -EINVAL;
		return ret;
	}

	priv->sysclk = plat->sysclk;

	priv->first_stream = NULL;
	priv->second_stream = NULL;

	return ret;
}

static int __devexit imx_wm8962_remove(struct platform_device *pdev)
{
	struct mxc_audio_platform_data *plat = pdev->dev.platform_data;
	struct imx_priv *priv = &card_priv;

	if (plat->finit)
		plat->finit();

	if (priv->hp_irq)
		free_irq(priv->hp_irq, priv);
	if (priv->amic_irq)
		free_irq(priv->amic_irq, priv);

	return 0;
}

static struct platform_driver imx_wm8962_driver = {
	.probe = imx_wm8962_probe,
	.remove = imx_wm8962_remove,
	.driver = {
		   .name = "imx-wm8962",
		   .owner = THIS_MODULE,
		   },
};

static struct platform_device *imx_snd_device;

static int __init imx_asoc_init(void)
{
	int ret;

	ret = platform_driver_register(&imx_wm8962_driver);
	if (ret < 0)
		goto exit;

	if (machine_is_mx6q_sabresd())
		imx_dai[0].codec_name = "wm8962.0-001a";
	else if (machine_is_mx6sl_arm2() | machine_is_mx6sl_evk())
		imx_dai[0].codec_name = "wm8962.1-001a";

	imx_snd_device = platform_device_alloc("soc-audio", 5);
	if (!imx_snd_device)
		goto err_device_alloc;

	platform_set_drvdata(imx_snd_device, &snd_soc_card_imx);

	ret = platform_device_add(imx_snd_device);

	if (0 == ret)
		goto exit;

	platform_device_put(imx_snd_device);

err_device_alloc:
	platform_driver_unregister(&imx_wm8962_driver);
exit:
	return ret;
}

static void __exit imx_asoc_exit(void)
{
	platform_driver_unregister(&imx_wm8962_driver);
	platform_device_unregister(imx_snd_device);
}

module_init(imx_asoc_init);
module_exit(imx_asoc_exit);

/* Module information */
MODULE_DESCRIPTION("ALSA SoC imx wm8962");
MODULE_LICENSE("GPL");