/* Copyright (c) 2010, 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.
 *
 */

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/errno.h>
#include <linux/mfd/pmic8058.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/bitops.h>
#include <linux/workqueue.h>
#include <linux/debugfs.h>
#include <linux/slab.h>

#include <mach/msm_xo.h>
#include <mach/msm_hsusb.h>

/* Config Regs  and their bits*/
#define PM8058_CHG_TEST			0x75
#define IGNORE_LL                       2

#define PM8058_CHG_TEST_2		0xEA
#define PM8058_CHG_TEST_3		0xEB
#define PM8058_OVP_TEST_REG		0xF6
#define FORCE_OVP_OFF			3

#define PM8058_CHG_CNTRL		0x1E
#define CHG_TRICKLE_EN			7
#define CHG_USB_SUSPEND			6
#define CHG_IMON_CAL			5
#define CHG_IMON_GAIN			4
#define CHG_VBUS_FROM_BOOST_OVRD	2
#define CHG_CHARGE_DIS			1
#define CHG_VCP_EN			0

#define PM8058_CHG_CNTRL_2		0xD8
#define ATC_DIS				7	/* coincell backed */
#define CHARGE_AUTO_DIS			6
#define DUMB_CHG_OVRD			5	/* coincell backed */
#define ENUM_DONE			4
#define CHG_TEMP_MODE			3
#define CHG_BATT_TEMP_DIS		1	/* coincell backed */
#define CHG_FAILED_CLEAR		0

#define PM8058_CHG_VMAX_SEL		0x21
#define PM8058_CHG_VBAT_DET		0xD9
#define PM8058_CHG_IMAX			0x1F
#define PM8058_CHG_TRICKLE		0xDB
#define PM8058_CHG_ITERM		0xDC
#define PM8058_CHG_TTRKL_MAX		0xE1
#define PM8058_CHG_TCHG_MAX		0xE4
#define PM8058_CHG_TEMP_THRESH		0xE2
#define PM8058_CHG_TEMP_REG		0xE3
#define PM8058_CHG_PULSE		0x22

/* IRQ STATUS and CLEAR */
#define PM8058_CHG_STATUS_CLEAR_IRQ_1	0x31
#define PM8058_CHG_STATUS_CLEAR_IRQ_3	0x33
#define PM8058_CHG_STATUS_CLEAR_IRQ_10	0xB3
#define PM8058_CHG_STATUS_CLEAR_IRQ_11	0xB4

/* IRQ MASKS */
#define PM8058_CHG_MASK_IRQ_1		0x38

#define PM8058_CHG_MASK_IRQ_3		0x3A
#define PM8058_CHG_MASK_IRQ_10		0xBA
#define PM8058_CHG_MASK_IRQ_11		0xBB

/* IRQ Real time status regs */
#define PM8058_CHG_STATUS_RT_1		0x3F
#define STATUS_RTCHGVAL			7
#define STATUS_RTCHGINVAL		6
#define STATUS_RTBATT_REPLACE		5
#define STATUS_RTVBATDET_LOW		4
#define STATUS_RTCHGILIM		3
#define STATUS_RTPCTDONE		1
#define STATUS_RTVCP			0
#define PM8058_CHG_STATUS_RT_3		0x41
#define PM8058_CHG_STATUS_RT_10		0xC1
#define PM8058_CHG_STATUS_RT_11		0xC2

/* VTRIM */
#define PM8058_CHG_VTRIM		0x1D
#define PM8058_CHG_VBATDET_TRIM		0x1E
#define PM8058_CHG_ITRIM		0x1F
#define PM8058_CHG_TTRIM		0x20

#define AUTO_CHARGING_VMAXSEL				4200
#define AUTO_CHARGING_FAST_TIME_MAX_MINUTES		512
#define AUTO_CHARGING_TRICKLE_TIME_MINUTES		30
#define AUTO_CHARGING_VEOC_ITERM			100
#define AUTO_CHARGING_IEOC_ITERM			160

#define AUTO_CHARGING_VBATDET				4150
#define AUTO_CHARGING_VEOC_VBATDET			4100
#define AUTO_CHARGING_VEOC_TCHG				16
#define AUTO_CHARGING_VEOC_TCHG_FINAL_CYCLE		32
#define AUTO_CHARGING_VEOC_BEGIN_TIME_MS		5400000

#define AUTO_CHARGING_VEOC_VBAT_LOW_CHECK_TIME_MS	60000
#define AUTO_CHARGING_RESUME_CHARGE_DETECTION_COUNTER	5

#define PM8058_CHG_I_STEP_MA 50
#define PM8058_CHG_I_MIN_MA 50
#define PM8058_CHG_T_TCHG_SHIFT 2
#define PM8058_CHG_I_TERM_STEP_MA 10
#define PM8058_CHG_V_STEP_MV 25
#define PM8058_CHG_V_MIN_MV  2400
/*
 * enum pmic_chg_interrupts: pmic interrupts
 * @CHGVAL_IRQ: charger V between 3.3 and 7.9
 * @CHGINVAL_IRQ: charger V outside 3.3 and 7.9
 * @VBATDET_LOW_IRQ: VBAT < VBATDET
 * @VCP_IRQ: VDD went below VBAT: BAT_FET is turned on
 * @CHGILIM_IRQ: mA consumed>IMAXSEL: chgloop draws less mA
 * @ATC_DONE_IRQ: Auto Trickle done
 * @ATCFAIL_IRQ: Auto Trickle fail
 * @AUTO_CHGDONE_IRQ: Auto chg done
 * @AUTO_CHGFAIL_IRQ: time exceeded w/o reaching term current
 * @CHGSTATE_IRQ: something happend causing a state change
 * @FASTCHG_IRQ: trkl charging completed: moving to fastchg
 * @CHG_END_IRQ: mA has dropped to termination current
 * @BATTTEMP_IRQ: batt temp is out of range
 * @CHGHOT_IRQ: the pass device is too hot
 * @CHGTLIMIT_IRQ: unused
 * @CHG_GONE_IRQ: charger was removed
 * @VCPMAJOR_IRQ: vcp major
 * @VBATDET_IRQ: VBAT >= VBATDET
 * @BATFET_IRQ: BATFET closed
 * @BATT_REPLACE_IRQ:
 * @BATTCONNECT_IRQ:
 */
enum pmic_chg_interrupts {
	CHGVAL_IRQ,
	CHGINVAL_IRQ,
	VBATDET_LOW_IRQ,
	VCP_IRQ,
	CHGILIM_IRQ,
	ATC_DONE_IRQ,
	ATCFAIL_IRQ,
	AUTO_CHGDONE_IRQ,
	AUTO_CHGFAIL_IRQ,
	CHGSTATE_IRQ,
	FASTCHG_IRQ,
	CHG_END_IRQ,
	BATTTEMP_IRQ,
	CHGHOT_IRQ,
	CHGTLIMIT_IRQ,
	CHG_GONE_IRQ,
	VCPMAJOR_IRQ,
	VBATDET_IRQ,
	BATFET_IRQ,
	BATT_REPLACE_IRQ,
	BATTCONNECT_IRQ,
	PMIC_CHG_MAX_INTS
};

struct pm8058_charger {
	struct pmic_charger_pdata *pdata;
	struct pm8058_chip *pm_chip;
	struct device *dev;

	int pmic_chg_irq[PMIC_CHG_MAX_INTS];
	DECLARE_BITMAP(enabled_irqs, PMIC_CHG_MAX_INTS);

	struct delayed_work check_vbat_low_work;
	struct delayed_work veoc_begin_work;
	int waiting_for_topoff;
	int waiting_for_veoc;
	int current_charger_current;

	struct msm_xo_voter *voter;
	struct dentry *dent;
};

static struct pm8058_charger pm8058_chg;

static int pm_chg_get_rt_status(int irq)
{
	int count = 3;
	int ret;

	while ((ret =
		pm8058_irq_get_rt_status(pm8058_chg.pm_chip, irq)) == -EAGAIN
	       && count--) {
		dev_info(pm8058_chg.dev, "%s trycount=%d\n", __func__, count);
		cpu_relax();
	}
	if (ret == -EAGAIN)
		return 0;
	else
		return ret;
}

static int is_chg_plugged_in(void)
{
	return pm_chg_get_rt_status(pm8058_chg.pmic_chg_irq[CHGVAL_IRQ]);
}

static irqreturn_t pm8058_chg_chgval_handler(int irq, void *dev_id)
{
	u8 old, temp;
	int ret;

	if (!is_chg_plugged_in()) {	/*this debounces it */
		ret = pm8058_read(pm8058_chg.pm_chip, PM8058_OVP_TEST_REG,
					&old, 1);
		temp = old | BIT(FORCE_OVP_OFF);
		ret = pm8058_write(pm8058_chg.pm_chip, PM8058_OVP_TEST_REG,
					&temp, 1);
		temp = 0xFC;
		ret = pm8058_write(pm8058_chg.pm_chip, PM8058_CHG_TEST,
					&temp, 1);
		pr_debug("%s forced wrote 0xFC to test ret=%d\n",
							__func__, ret);
		/* 20 ms sleep is for the VCHG to discharge */
		msleep(20);
		temp = 0xF0;
		ret = pm8058_write(pm8058_chg.pm_chip, PM8058_CHG_TEST,
					&temp, 1);
		ret = pm8058_write(pm8058_chg.pm_chip, PM8058_OVP_TEST_REG,
					&old, 1);
	}

	return IRQ_HANDLED;
}

static void free_irqs(void)
{
	int i;

	for (i = 0; i < PMIC_CHG_MAX_INTS; i++)
		if (pm8058_chg.pmic_chg_irq[i]) {
			free_irq(pm8058_chg.pmic_chg_irq[i], NULL);
			pm8058_chg.pmic_chg_irq[i] = 0;
		}
}

static int __devinit request_irqs(struct platform_device *pdev)
{
	struct resource *res;
	int ret;

	ret = 0;
	bitmap_fill(pm8058_chg.enabled_irqs, PMIC_CHG_MAX_INTS);

	res = platform_get_resource_byname(pdev, IORESOURCE_IRQ, "CHGVAL");
	if (res == NULL) {
		dev_err(pm8058_chg.dev,
			"%s:couldnt find resource CHGVAL\n", __func__);
		goto err_out;
	} else {
		ret = request_any_context_irq(res->start,
				  pm8058_chg_chgval_handler,
				  IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
				  res->name, NULL);
		if (ret < 0) {
			dev_err(pm8058_chg.dev, "%s:couldnt request %d %d\n",
				__func__, res->start, ret);
			goto err_out;
		} else {
			pm8058_chg.pmic_chg_irq[CHGVAL_IRQ] = res->start;
		}
	}

	return 0;

err_out:
	free_irqs();
	return -EINVAL;
}

static int pm8058_usb_voltage_lower_limit(void)
{
	u8 temp, old;
	int ret = 0;

	temp = 0x10;
	ret |= pm8058_write(pm8058_chg.pm_chip, PM8058_CHG_TEST, &temp, 1);
	ret |= pm8058_read(pm8058_chg.pm_chip, PM8058_CHG_TEST, &old, 1);
	old = old & ~BIT(IGNORE_LL);
	temp = 0x90  | (0xF & old);
	pr_debug("%s writing 0x%x to test\n", __func__, temp);
	ret |= pm8058_write(pm8058_chg.pm_chip, PM8058_CHG_TEST, &temp, 1);

	return ret;
}

static int __devinit pm8058_charger_probe(struct platform_device *pdev)
{
	struct pm8058_chip *pm_chip;

	pm_chip = dev_get_drvdata(pdev->dev.parent);
	if (pm_chip == NULL) {
		pr_err("%s:no parent data passed in.\n", __func__);
		return -EFAULT;
	}

	pm8058_chg.pm_chip = pm_chip;
	pm8058_chg.pdata = pdev->dev.platform_data;
	pm8058_chg.dev = &pdev->dev;

	if (request_irqs(pdev)) {
		pr_err("%s: couldnt register interrupts\n", __func__);
		return -EINVAL;
	}

	if (pm8058_usb_voltage_lower_limit()) {
		pr_err("%s: couldnt write to IGNORE_LL\n", __func__);
		return -EINVAL;
	}

	return 0;
}

static int __devexit pm8058_charger_remove(struct platform_device *pdev)
{
	free_irqs();
	return 0;
}

static struct platform_driver pm8058_charger_driver = {
	.probe = pm8058_charger_probe,
	.remove = __devexit_p(pm8058_charger_remove),
	.driver = {
		   .name = "pm-usb-fix",
		   .owner = THIS_MODULE,
	},
};

static int __init pm8058_charger_init(void)
{
	return platform_driver_register(&pm8058_charger_driver);
}

static void __exit pm8058_charger_exit(void)
{
	platform_driver_unregister(&pm8058_charger_driver);
}

late_initcall(pm8058_charger_init);
module_exit(pm8058_charger_exit);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("PMIC8058 BATTERY driver");
MODULE_VERSION("1.0");
MODULE_ALIAS("platform:pm8058_charger");