/*
 * Copyright (C) 2009 Google, Inc.
 *
 * This software is licensed under the terms of the GNU General Public
 * License version 2, as published by the Free Software Foundation, and
 * may be copied, distributed, and modified under those terms.
 *
 * 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/hrtimer.h>
#include <linux/irq.h>
#include <linux/serial_core.h>
#include <mach/bcm_bt_lpm.h>
#include <asm/gpio.h>

/*
 * Manage WAKE and HOST_WAKE low power mode signals for Broadcom
 * Bluetooth chipsets.
 *
 * This driver needs to be tightly coupled with a uart driver that supports
 * request_clock_off_locked() and request_clock_on_locked(), to clock off and
 * on the uart indepdently of Linux suspend/resume.
 *
 * The uart driver needs to call bcm_bt_lpm_exit_lpm_locked() every time it
 * begins TX, to ensure this driver keeps WAKE asserted during TX.
 *
 * The callbacks and hijacking of the uart_port struct are not a clean API,
 * but the Linux tty and serial core layers do not have a better alternative
 * right now: there is no good way to plumb uart clock control through these
 * layers. See http://lkml.org/lkml/2008/12/19/213 for more background.
 */

struct bcm_bt_lpm {
	unsigned int gpio_wake;
	unsigned int gpio_host_wake;

	int wake;
	int host_wake;

	struct hrtimer enter_lpm_timer;
	ktime_t enter_lpm_delay;

	struct uart_port *uport;

	void (*request_clock_off_locked)(struct uart_port *uport);
	void (*request_clock_on_locked)(struct uart_port *uport);
} bt_lpm;

static void set_wake_locked(int wake)
{
	if (wake == bt_lpm.wake)
		return;
	bt_lpm.wake = wake;

	if (wake || bt_lpm.host_wake)
		bt_lpm.request_clock_on_locked(bt_lpm.uport);
	else
		bt_lpm.request_clock_off_locked(bt_lpm.uport);

	gpio_set_value(bt_lpm.gpio_wake, wake);
}

static enum hrtimer_restart enter_lpm(struct hrtimer *timer) {
	unsigned long flags;

	spin_lock_irqsave(&bt_lpm.uport->lock, flags);
	set_wake_locked(0);
	spin_unlock_irqrestore(&bt_lpm.uport->lock, flags);

	return HRTIMER_NORESTART;
}

void bcm_bt_lpm_exit_lpm_locked(struct uart_port *uport) {
	bt_lpm.uport = uport;

	hrtimer_try_to_cancel(&bt_lpm.enter_lpm_timer);

	set_wake_locked(1);

	hrtimer_start(&bt_lpm.enter_lpm_timer, bt_lpm.enter_lpm_delay,
			HRTIMER_MODE_REL);
}
EXPORT_SYMBOL(bcm_bt_lpm_exit_lpm_locked);

static void update_host_wake_locked(int host_wake)
{
	if (host_wake == bt_lpm.host_wake)
		return;
	bt_lpm.host_wake = host_wake;

	if (bt_lpm.wake || host_wake)
		bt_lpm.request_clock_on_locked(bt_lpm.uport);
	else
		bt_lpm.request_clock_off_locked(bt_lpm.uport);
}

static irqreturn_t host_wake_isr(int irq, void *dev)
{
	int host_wake;
	unsigned long flags;

	host_wake = gpio_get_value(bt_lpm.gpio_host_wake);
	set_irq_type(irq, host_wake ? IRQF_TRIGGER_LOW : IRQF_TRIGGER_HIGH);

	if (!bt_lpm.uport) {
		bt_lpm.host_wake = host_wake;
		return IRQ_HANDLED;
	}

	spin_lock_irqsave(&bt_lpm.uport->lock, flags);

	update_host_wake_locked(host_wake);

	spin_unlock_irqrestore(&bt_lpm.uport->lock, flags);

	return IRQ_HANDLED;
}

static int bcm_bt_lpm_probe(struct platform_device *pdev)
{
	int irq;
	int ret;
	struct bcm_bt_lpm_platform_data *pdata = pdev->dev.platform_data;

	if (bt_lpm.request_clock_off_locked != NULL) {
		printk(KERN_ERR "Cannot register two bcm_bt_lpm drivers\n");
		return -EINVAL;
	}

	bt_lpm.gpio_wake = pdata->gpio_wake;
	bt_lpm.gpio_host_wake = pdata->gpio_host_wake;
	bt_lpm.request_clock_off_locked = pdata->request_clock_off_locked;
	bt_lpm.request_clock_on_locked = pdata->request_clock_on_locked;

	hrtimer_init(&bt_lpm.enter_lpm_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
	bt_lpm.enter_lpm_delay = ktime_set(1, 0);  /* 1 sec */
	bt_lpm.enter_lpm_timer.function = enter_lpm;

	gpio_set_value(bt_lpm.gpio_wake, 0);
	bt_lpm.host_wake = 0;

	irq = gpio_to_irq(bt_lpm.gpio_host_wake);
	ret = request_irq(irq, host_wake_isr, IRQF_TRIGGER_HIGH,
			"bt host_wake", NULL);
	if (ret)
		return ret;
	ret = set_irq_wake(irq, 1);
	if (ret)
		return ret;

	return 0;
}

static struct platform_driver bcm_bt_lpm_driver = {
	.probe = bcm_bt_lpm_probe,
	.driver = {
		.name = "bcm_bt_lpm",
		.owner = THIS_MODULE,
	},
};

static int __init bcm_bt_lpm_init(void)
{
	return platform_driver_register(&bcm_bt_lpm_driver);
}

module_init(bcm_bt_lpm_init);
MODULE_DESCRIPTION("Broadcom Bluetooth low power mode driver");
MODULE_AUTHOR("Nick Pelly <npelly@google.com>");
MODULE_LICENSE("GPL");