/* Copyright (c) 2012, The Linux Foundation. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only 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.
 */

#define pr_fmt(fmt) "%s: " fmt, __func__

#include <linux/module.h>
#include <linux/err.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/stddef.h>
#include <linux/debugfs.h>
#include <linux/mfd/pm8xxx/core.h>
#include <linux/mfd/pm8xxx/spk.h>

#define PM8XXX_SPK_CTL1_REG_OFF		0
#define PM8XXX_SPK_CTL2_REG_OFF		1
#define PM8XXX_SPK_CTL3_REG_OFF		2
#define PM8XXX_SPK_CTL4_REG_OFF		3
#define PM8XXX_SPK_TEST_REG_1_OFF	4
#define PM8XXX_SPK_TEST_REG_2_OFF	5

#define PM8XXX_SPK_BANK_SEL		4
#define PM8XXX_SPK_BANK_WRITE		0x80
#define PM8XXX_SPK_BANK_VAL_MASK	0xF

#define BOOST_6DB_GAIN_EN_MASK		0x8
#define VSEL_LD0_1P1			0x0
#define VSEL_LD0_1P2			0x2
#define VSEL_LD0_1P0			0x4

#define PWM_EN_MASK			0xF
#define PM8XXX_SPK_TEST_REG_1_BANKS	8
#define PM8XXX_SPK_TEST_REG_2_BANKS	2

#define PM8XXX_SPK_GAIN			0x5
#define PM8XXX_ADD_EN			0x1

struct pm8xxx_spk_chip {
	struct list_head                        link;
	struct pm8xxx_spk_platform_data		pdata;
	struct device                           *dev;
	enum pm8xxx_version                     version;
	struct mutex				spk_mutex;
	u16					base;
	u16					end;
};

static struct pm8xxx_spk_chip *the_spk_chip;

static inline bool spk_defined(void)
{
	if (the_spk_chip == NULL || IS_ERR(the_spk_chip))
		return false;
	return true;
}

static int pm8xxx_spk_bank_write(u16 reg, u16 bank, u8 val)
{
	int rc = 0;
	u8 bank_val = PM8XXX_SPK_BANK_WRITE | (bank << PM8XXX_SPK_BANK_SEL);

	bank_val |= (val & PM8XXX_SPK_BANK_VAL_MASK);
	mutex_lock(&the_spk_chip->spk_mutex);
	rc = pm8xxx_writeb(the_spk_chip->dev->parent, reg, bank_val);
	if (rc)
		pr_err("pm8xxx_writeb(): rc=%d\n", rc);
	mutex_unlock(&the_spk_chip->spk_mutex);
	return rc;
}


static int pm8xxx_spk_read(u16 addr)
{
	int rc = 0;
	u8 val = 0;

	mutex_lock(&the_spk_chip->spk_mutex);
	rc = pm8xxx_readb(the_spk_chip->dev->parent,
			the_spk_chip->base + addr, &val);
	if (rc) {
		pr_err("pm8xxx_spk_readb() failed: rc=%d\n", rc);
		val = rc;
	}
	mutex_unlock(&the_spk_chip->spk_mutex);

	return val;
}

static int pm8xxx_spk_write(u16 addr, u8 val)
{
	int rc = 0;

	mutex_lock(&the_spk_chip->spk_mutex);
	rc = pm8xxx_writeb(the_spk_chip->dev->parent,
			the_spk_chip->base + addr, val);
	if (rc)
		pr_err("pm8xxx_writeb() failed: rc=%d\n", rc);
	mutex_unlock(&the_spk_chip->spk_mutex);
	return rc;
}

int pm8xxx_spk_mute(bool mute)
{
	u8 val = 0;
	int ret = 0;
	if (spk_defined() == false) {
		pr_err("Invalid spk handle or no spk_chip\n");
		return -ENODEV;
	}

	val = pm8xxx_spk_read(PM8XXX_SPK_CTL1_REG_OFF);
	if (val < 0)
		return val;
	val |= mute << 2;
	ret = pm8xxx_spk_write(PM8XXX_SPK_CTL1_REG_OFF, val);
	return ret;
}
EXPORT_SYMBOL_GPL(pm8xxx_spk_mute);

int pm8xxx_spk_gain(u8 gain)
{
	u8 val;
	int ret = 0;

	if (spk_defined() == false) {
		pr_err("Invalid spk handle or no spk_chip\n");
		return -ENODEV;
	}

	val = pm8xxx_spk_read(PM8XXX_SPK_CTL1_REG_OFF);
	if (val < 0)
		return val;
	val |= (gain << 4);
	ret = pm8xxx_spk_write(PM8XXX_SPK_CTL1_REG_OFF, val);
	if (!ret) {
		pm8xxx_spk_bank_write(the_spk_chip->base
			+ PM8XXX_SPK_TEST_REG_1_OFF,
			0, BOOST_6DB_GAIN_EN_MASK | VSEL_LD0_1P2);
	}
	return ret;
}
EXPORT_SYMBOL_GPL(pm8xxx_spk_gain);

int pm8xxx_spk_enable(int enable)
{
	int val = 0;
	u16 addr;
	int ret = 0;

	if (spk_defined() == false) {
		pr_err("Invalid spk handle or no spk_chip\n");
		return -ENODEV;
	}

	addr = the_spk_chip->base + PM8XXX_SPK_TEST_REG_1_OFF;
	val = pm8xxx_spk_read(PM8XXX_SPK_CTL1_REG_OFF);
	if (val < 0)
		return val;
	if (enable)
		val |= (1 << 3);
	else
		val &= ~(1 << 3);
	ret = pm8xxx_spk_write(PM8XXX_SPK_CTL1_REG_OFF, val);
	if (!ret)
		ret = pm8xxx_spk_bank_write(addr, 6, PWM_EN_MASK);
	return ret;
}
EXPORT_SYMBOL_GPL(pm8xxx_spk_enable);

static int pm8xxx_spk_config(void)
{
	u16 addr;
	int ret = 0;

	if (spk_defined() == false) {
		pr_err("Invalid spk handle or no spk_chip\n");
		return -ENODEV;
	}

	addr = the_spk_chip->base + PM8XXX_SPK_TEST_REG_1_OFF;
	ret = pm8xxx_spk_bank_write(addr, 6, PWM_EN_MASK & 0);
	if (!ret)
		ret = pm8xxx_spk_gain(PM8XXX_SPK_GAIN);
	return ret;
}

static int __devinit pm8xxx_spk_probe(struct platform_device *pdev)
{
	const struct pm8xxx_spk_platform_data *pdata = pdev->dev.platform_data;
	int ret = 0;
	u8 value = 0;

	if (!pdata) {
		pr_err("missing platform data\n");
		return -EINVAL;
	}

	the_spk_chip = kzalloc(sizeof(struct pm8xxx_spk_chip), GFP_KERNEL);
	if (the_spk_chip == NULL) {
		pr_err("kzalloc() failed.\n");
		return -ENOMEM;
	}

	mutex_init(&the_spk_chip->spk_mutex);

	the_spk_chip->dev = &pdev->dev;
	the_spk_chip->version = pm8xxx_get_version(the_spk_chip->dev->parent);
	switch (pm8xxx_get_version(the_spk_chip->dev->parent)) {
	case PM8XXX_VERSION_8038:
		break;
	default:
		ret = -ENODEV;
		goto err_handle;
	}

	memcpy(&(the_spk_chip->pdata), pdata,
			sizeof(struct pm8xxx_spk_platform_data));

	the_spk_chip->base = pdev->resource[0].start;
	the_spk_chip->end = pdev->resource[0].end;

	if (the_spk_chip->pdata.spk_add_enable) {
		int val;
		val = pm8xxx_spk_read(PM8XXX_SPK_CTL1_REG_OFF);
		if (val < 0) {
			ret = val;
			goto err_handle;
		}
		val |= (the_spk_chip->pdata.spk_add_enable & PM8XXX_ADD_EN);
		ret = pm8xxx_spk_write(PM8XXX_SPK_CTL1_REG_OFF, val);
		if (ret < 0)
			goto err_handle;
	}
	value = ((the_spk_chip->pdata.cd_ng_threshold << 5) |
		the_spk_chip->pdata.cd_nf_preamp_bias << 3);
	pr_debug("Setting SPK_CTL2_REG = %02x\n", value);
	pm8xxx_spk_write(PM8XXX_SPK_CTL2_REG_OFF, value);

	value = ((the_spk_chip->pdata.cd_ng_hold << 5) |
		(the_spk_chip->pdata.cd_ng_max_atten << 1) |
		the_spk_chip->pdata.noise_mute);
	pr_debug("Setting SPK_CTL3_REG = %02x\n", value);
	pm8xxx_spk_write(PM8XXX_SPK_CTL3_REG_OFF, value);

	value = ((the_spk_chip->pdata.cd_ng_decay_rate << 5) |
		(the_spk_chip->pdata.cd_ng_attack_rate << 3) |
		the_spk_chip->pdata.cd_delay << 2);
	pr_debug("Setting SPK_CTL4_REG = %02x\n", value);
	pm8xxx_spk_write(PM8XXX_SPK_CTL4_REG_OFF, value);

	return pm8xxx_spk_config();
err_handle:
	pr_err("pm8xxx_spk_probe failed."
			"Audio unavailable on speaker.\n");
	mutex_destroy(&the_spk_chip->spk_mutex);
	kfree(the_spk_chip);
	return ret;
}

static int __devexit pm8xxx_spk_remove(struct platform_device *pdev)
{
	if (spk_defined() == false) {
		pr_err("Invalid spk handle or no spk_chip\n");
		return -ENODEV;
	}
	mutex_destroy(&the_spk_chip->spk_mutex);
	kfree(the_spk_chip);
	return 0;
}

static struct platform_driver pm8xxx_spk_driver = {
	.probe		= pm8xxx_spk_probe,
	.remove		= __devexit_p(pm8xxx_spk_remove),
	.driver		= {
		.name = PM8XXX_SPK_DEV_NAME,
		.owner = THIS_MODULE,
	},
};

static int __init pm8xxx_spk_init(void)
{
	return platform_driver_register(&pm8xxx_spk_driver);
}
subsys_initcall(pm8xxx_spk_init);

static void __exit pm8xxx_spk_exit(void)
{
	platform_driver_unregister(&pm8xxx_spk_driver);
}
module_exit(pm8xxx_spk_exit);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("PM8XXX SPK driver");
MODULE_ALIAS("platform:" PM8XXX_SPK_DEV_NAME);
