/* Copyright (c) 2008-2009, Code Aurora Forum. 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.
 *
 * 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 Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 */
/*
 * SMD NMEA Driver -- Provides GPS NMEA device to SMD port interface.
 *
 */

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/miscdevice.h>
#include <linux/workqueue.h>
#include <linux/uaccess.h>

#include <mach/msm_smd.h>

#define MAX_BUF_SIZE 200

static DEFINE_MUTEX(nmea_ch_lock);
static DEFINE_MUTEX(nmea_rx_buf_lock);

static DECLARE_WAIT_QUEUE_HEAD(nmea_wait_queue);

struct nmea_device_t {
	struct miscdevice misc;

	struct smd_channel *ch;

	unsigned char rx_buf[MAX_BUF_SIZE];
	unsigned int bytes_read;
};

struct nmea_device_t *nmea_devp;

static void nmea_work_func(struct work_struct *ws)
{
	int sz;

	for (;;) {
		sz = smd_cur_packet_size(nmea_devp->ch);
		if (sz == 0)
			break;
		if (sz > smd_read_avail(nmea_devp->ch))
			break;
		if (sz > MAX_BUF_SIZE) {
			smd_read(nmea_devp->ch, 0, sz);
			continue;
		}

		mutex_lock(&nmea_rx_buf_lock);
		if (smd_read(nmea_devp->ch, nmea_devp->rx_buf, sz) != sz) {
			mutex_unlock(&nmea_rx_buf_lock);
			printk(KERN_ERR "nmea: not enough data?!\n");
			continue;
		}
		nmea_devp->bytes_read = sz;
		mutex_unlock(&nmea_rx_buf_lock);
		wake_up_interruptible(&nmea_wait_queue);
	}
}

struct workqueue_struct *nmea_wq;
static DECLARE_WORK(nmea_work, nmea_work_func);

static void nmea_notify(void *priv, unsigned event)
{
	switch (event) {
	case SMD_EVENT_DATA: {
		int sz;
		sz = smd_cur_packet_size(nmea_devp->ch);
		if ((sz > 0) && (sz <= smd_read_avail(nmea_devp->ch)))
			queue_work(nmea_wq, &nmea_work);
		break;
	}
	case SMD_EVENT_OPEN:
		printk(KERN_INFO "nmea: smd opened\n");
		break;
	case SMD_EVENT_CLOSE:
		printk(KERN_INFO "nmea: smd closed\n");
		break;
	}
}

static ssize_t nmea_read(struct file *fp, char __user *buf,
			 size_t count, loff_t *pos)
{
	int r;
	int bytes_read;

	r = wait_event_interruptible(nmea_wait_queue,
				nmea_devp->bytes_read);
	if (r < 0) {
		/* qualify error message */
		if (r != -ERESTARTSYS) {
			/* we get this anytime a signal comes in */
			printk(KERN_ERR "ERROR:%s:%i:%s: "
				"wait_event_interruptible ret %i\n",
				__FILE__,
				__LINE__,
				__func__,
				r
				);
		}
		return r;
	}

	mutex_lock(&nmea_rx_buf_lock);
	bytes_read = nmea_devp->bytes_read;
	nmea_devp->bytes_read = 0;
	r = copy_to_user(buf, nmea_devp->rx_buf, bytes_read);
	mutex_unlock(&nmea_rx_buf_lock);

	if (r > 0) {
		printk(KERN_ERR "ERROR:%s:%i:%s: "
			"copy_to_user could not copy %i bytes.\n",
			__FILE__,
			__LINE__,
			__func__,
			r);
		return r;
	}

	return bytes_read;
}

static int nmea_open(struct inode *ip, struct file *fp)
{
	int r = 0;

	mutex_lock(&nmea_ch_lock);
	if (nmea_devp->ch == 0)
		r = smd_open("GPSNMEA", &nmea_devp->ch, nmea_devp, nmea_notify);
	mutex_unlock(&nmea_ch_lock);

	return r;
}

static int nmea_release(struct inode *ip, struct file *fp)
{
	int r = 0;

	mutex_lock(&nmea_ch_lock);
	if (nmea_devp->ch != 0) {
		r = smd_close(nmea_devp->ch);
		nmea_devp->ch = 0;
	}
	mutex_unlock(&nmea_ch_lock);

	return r;
}

static const struct file_operations nmea_fops = {
	.owner = THIS_MODULE,
	.read = nmea_read,
	.open = nmea_open,
	.release = nmea_release,
};

static struct nmea_device_t nmea_device = {
	.misc = {
		.minor = MISC_DYNAMIC_MINOR,
		.name = "nmea",
		.fops = &nmea_fops,
	}
};

static void __exit nmea_exit(void)
{
	destroy_workqueue(nmea_wq);
	misc_deregister(&nmea_device.misc);
}

static int __init nmea_init(void)
{
	int ret;

	nmea_device.bytes_read = 0;
	nmea_devp = &nmea_device;

	nmea_wq = create_singlethread_workqueue("nmea");
	if (nmea_wq == 0)
		return -ENOMEM;

	ret = misc_register(&nmea_device.misc);
	return ret;
}

module_init(nmea_init);
module_exit(nmea_exit);

MODULE_DESCRIPTION("MSM Shared Memory NMEA Driver");
MODULE_LICENSE("GPL v2");
