/*
 * hsi-char.c
 *
 * HSI character device driver, implements the character device
 * interface.
 *
 * Copyright (C) 2009 Nokia Corporation. All rights reserved.
 * Copyright (C) 2011 Texas Instruments, Inc.
 *
 * Author: Andras Domokos <andras.domokos@nokia.com>
 * Author: Sebastien JAN <s-jan@ti.com>
 * Author: Djamil ELAIDI <d-elaidi@ti.com>
 *
 * This package is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

#include <linux/errno.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/miscdevice.h>
#include <linux/file.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <asm/mach-types.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/hsi_driver_if.h>
#include <linux/hsi_char.h>

#include <plat/omap_hsi.h>

#include "hsi-char.h"

#define DRIVER_VERSION  "0.2.4"
#define HSI_CHAR_DEVICE_NAME  "hsi_char"

static unsigned int port = 1;
module_param(port, uint, S_IRUGO);
MODULE_PARM_DESC(port, "HSI port to be probed");

static unsigned int num_channels = 1;
static unsigned int channels_map[HSI_MAX_CHAR_DEVS] = { 1 };
module_param_array(channels_map, uint, &num_channels, S_IRUGO);
MODULE_PARM_DESC(channels_map, "HSI channels to be probed");

static int hsi_char_major;	     /* HSI char major number */
static struct class *hsi_char_class; /* HSI char class during class_create */
static dev_t hsi_char_dev;	     /* HSI char dev with first minor number */

struct char_queue {
	struct list_head list;
	u32 *data;
	unsigned int count;
};

struct hsi_char {
	unsigned int opened;
	int poll_event;
	struct list_head rx_queue;
	struct list_head tx_queue;
	spinlock_t lock;	/* Serialize access to driver data and API */
	struct fasync_struct *async_queue;
	wait_queue_head_t rx_wait;
	wait_queue_head_t tx_wait;
	wait_queue_head_t poll_wait;
};

static struct hsi_char hsi_char_data[HSI_MAX_CHAR_DEVS];

void if_hsi_notify(int ch, struct hsi_event *ev)
{
	struct char_queue *entry;

	pr_debug("%s, ev = {0x%x, 0x%p, %u}\n", __func__, ev->event, ev->data,
		 ev->count);

	spin_lock(&hsi_char_data[ch].lock);

	if (!hsi_char_data[ch].opened) {
		pr_debug("%s, device not opened\n!", __func__);
		spin_unlock(&hsi_char_data[ch].lock);
		return;
	}

	switch (HSI_EV_TYPE(ev->event)) {
	case HSI_EV_IN:
		entry = kmalloc(sizeof(*entry), GFP_ATOMIC);
		if (!entry) {
			pr_err("HSI-CHAR: entry allocation failed.\n");
			spin_unlock(&hsi_char_data[ch].lock);
			return;
		}
		entry->data = ev->data;
		entry->count = ev->count;
		list_add_tail(&entry->list, &hsi_char_data[ch].rx_queue);
		spin_unlock(&hsi_char_data[ch].lock);
		pr_debug("%s, HSI_EV_IN\n", __func__);
		wake_up_interruptible(&hsi_char_data[ch].rx_wait);
		break;
	case HSI_EV_OUT:
		entry = kmalloc(sizeof(*entry), GFP_ATOMIC);
		if (!entry) {
			pr_err("HSI-CHAR: entry allocation failed.\n");
			spin_unlock(&hsi_char_data[ch].lock);
			return;
		}
		entry->data = ev->data;
		entry->count = ev->count;
		hsi_char_data[ch].poll_event |= (POLLOUT | POLLWRNORM);
		list_add_tail(&entry->list, &hsi_char_data[ch].tx_queue);
		spin_unlock(&hsi_char_data[ch].lock);
		pr_debug("%s, HSI_EV_OUT\n", __func__);
		wake_up_interruptible(&hsi_char_data[ch].tx_wait);
		break;
	case HSI_EV_EXCEP:
		hsi_char_data[ch].poll_event |= POLLPRI;
		spin_unlock(&hsi_char_data[ch].lock);
		pr_debug("%s, HSI_EV_EXCEP\n", __func__);
		wake_up_interruptible(&hsi_char_data[ch].poll_wait);
		break;
	case HSI_EV_AVAIL:
		hsi_char_data[ch].poll_event |= (POLLIN | POLLRDNORM);
		spin_unlock(&hsi_char_data[ch].lock);
		pr_debug("%s, HSI_EV_AVAIL\n", __func__);
		wake_up_interruptible(&hsi_char_data[ch].poll_wait);
		break;
	default:
		spin_unlock(&hsi_char_data[ch].lock);
		break;
	}
}

static int hsi_char_fasync(int fd, struct file *file, int on)
{
	int ch = (int)file->private_data;
	if (fasync_helper(fd, file, on, &hsi_char_data[ch].async_queue) >= 0)
		return 0;
	else
		return -EIO;
}

static unsigned int hsi_char_poll(struct file *file, poll_table * wait)
{
	int ch = (int)file->private_data;
	unsigned int ret = 0;

	/*printk(KERN_DEBUG "%s\n", __func__); */

	poll_wait(file, &hsi_char_data[ch].poll_wait, wait);
	poll_wait(file, &hsi_char_data[ch].tx_wait, wait);
	spin_lock_bh(&hsi_char_data[ch].lock);
	ret = hsi_char_data[ch].poll_event;
	spin_unlock_bh(&hsi_char_data[ch].lock);

	pr_debug("%s, ret = 0x%x\n", __func__, ret);
	return ret;
}

static ssize_t hsi_char_read(struct file *file, char __user *buf,
			     size_t count, loff_t *ppos)
{
	int ch = (int)file->private_data;
	DECLARE_WAITQUEUE(wait, current);
	u32 *data;
	unsigned int data_len;
	struct char_queue *entry;
	ssize_t ret;

	/*printk(KERN_DEBUG "%s, count = %d\n", __func__, count); */

	/* only 32bit data is supported for now */
	if ((count < 4) || (count & 3))
		return -EINVAL;

	data = kmalloc(count, GFP_ATOMIC);

	ret = if_hsi_read(ch, data, count);
	if (ret < 0) {
		kfree(data);
		goto out2;
	}

	spin_lock_bh(&hsi_char_data[ch].lock);
	add_wait_queue(&hsi_char_data[ch].rx_wait, &wait);
	spin_unlock_bh(&hsi_char_data[ch].lock);

	for (;;) {
		data = NULL;
		data_len = 0;

		set_current_state(TASK_INTERRUPTIBLE);

		spin_lock_bh(&hsi_char_data[ch].lock);
		if (!list_empty(&hsi_char_data[ch].rx_queue)) {
			entry = list_entry(hsi_char_data[ch].rx_queue.next,
					   struct char_queue, list);
			data = entry->data;
			data_len = entry->count;
			list_del(&entry->list);
			kfree(entry);
		}
		spin_unlock_bh(&hsi_char_data[ch].lock);

		pr_debug("%s, data = 0x%p, data_len = %d\n",
			 __func__, data, data_len);

		if (data_len) {
			pr_debug("%s, RX finished\n", __func__);
			spin_lock_bh(&hsi_char_data[ch].lock);
			hsi_char_data[ch].poll_event &= ~(POLLIN | POLLRDNORM);
			spin_unlock_bh(&hsi_char_data[ch].lock);
			if_hsi_poll(ch);
			break;
		} else if (file->f_flags & O_NONBLOCK) {
			pr_debug("%s, O_NONBLOCK\n", __func__);
			ret = -EAGAIN;
			goto out;
		} else if (signal_pending(current)) {
			pr_debug("%s, ERESTARTSYS\n", __func__);
			ret = -EAGAIN;
			if_hsi_cancel_read(ch);
			/* goto out; */
			break;
		}

		/*printk(KERN_DEBUG "%s, going to sleep...\n", __func__); */
		schedule();
		/*printk(KERN_DEBUG "%s, woke up\n", __func__); */
	}

	if (data_len) {
		ret = copy_to_user((void __user *)buf, data, data_len);
		if (!ret)
			ret = data_len;
	}

	kfree(data);

out:
	__set_current_state(TASK_RUNNING);
	remove_wait_queue(&hsi_char_data[ch].rx_wait, &wait);

out2:
	/*printk(KERN_DEBUG "%s, ret = %d\n", __func__, ret); */
	return ret;
}

static ssize_t hsi_char_write(struct file *file, const char __user *buf,
			      size_t count, loff_t *ppos)
{
	int ch = (int)file->private_data;
	DECLARE_WAITQUEUE(wait, current);
	u32 *data;
	unsigned int data_len = 0;
	struct char_queue *entry;
	ssize_t ret;

	/*printk(KERN_DEBUG "%s, count = %d\n", __func__, count); */

	/* only 32bit data is supported for now */
	if ((count < 4) || (count & 3))
		return -EINVAL;

	data = kmalloc(count, GFP_ATOMIC);
	if (!data) {
		WARN_ON(1);
		return -ENOMEM;
	}
	if (copy_from_user(data, (void __user *)buf, count)) {
		ret = -EFAULT;
		kfree(data);
		goto out2;
	} else {
		ret = count;
	}

	/* Write callback can occur very fast in response to hsi_write()    */
	/* and may cause a race condition between hsi_char_write() and      */
	/* if_hsi_notify(). This can cause the poll_event flag to be set by */
	/* if_hsi_notify() then cleared by hsi_char_write(), remaining to 0 */
	/* forever. Clear the flag ahead of hsi_write to avoid this.        */
	spin_lock_bh(&hsi_char_data[ch].lock);
	hsi_char_data[ch].poll_event &= ~(POLLOUT | POLLWRNORM);
	add_wait_queue(&hsi_char_data[ch].tx_wait, &wait);
	spin_unlock_bh(&hsi_char_data[ch].lock);

	ret = if_hsi_write(ch, data, count);
	if (ret < 0) {
		kfree(data);
		spin_lock_bh(&hsi_char_data[ch].lock);
		hsi_char_data[ch].poll_event |= (POLLOUT | POLLWRNORM);
		spin_unlock_bh(&hsi_char_data[ch].lock);
		remove_wait_queue(&hsi_char_data[ch].tx_wait, &wait);
		goto out2;
	}

	for (;;) {
		data = NULL;
		data_len = 0;

		set_current_state(TASK_INTERRUPTIBLE);

		spin_lock_bh(&hsi_char_data[ch].lock);
		if (!list_empty(&hsi_char_data[ch].tx_queue)) {
			entry = list_entry(hsi_char_data[ch].tx_queue.next,
					   struct char_queue, list);
			data = entry->data;
			data_len = entry->count;
			list_del(&entry->list);
			kfree(entry);
		}
		spin_unlock_bh(&hsi_char_data[ch].lock);

		if (data_len) {
			pr_debug("%s, TX finished\n", __func__);
			ret = data_len;
			break;
		} else if (file->f_flags & O_NONBLOCK) {
			pr_debug("%s, O_NONBLOCK\n", __func__);
			ret = -EAGAIN;
			goto out;
		} else if (signal_pending(current)) {
			pr_debug("%s, ERESTARTSYS\n", __func__);
			ret = -ERESTARTSYS;
			goto out;
		}

		/*printk(KERN_DEBUG "%s, going to sleep...\n", __func__); */
		schedule();
		/*printk(KERN_DEBUG "%s, woke up\n", __func__); */
	}

	kfree(data);

out:
	__set_current_state(TASK_RUNNING);
	remove_wait_queue(&hsi_char_data[ch].tx_wait, &wait);

out2:
	/*printk(KERN_DEBUG "%s, ret = %d\n", __func__, ret); */
	return ret;
}

static long  hsi_char_ioctl(struct file *file,
			  unsigned int cmd, unsigned long arg)
{
	int ch = (int)file->private_data;
	unsigned int state;
	size_t size;
	struct hsi_rx_config rx_cfg;
	struct hsi_tx_config tx_cfg;
	int ret = 0;

	pr_debug("%s, ch = %d, cmd = 0x%08x\n", __func__, ch, cmd);

	switch (cmd) {
	case CS_SEND_BREAK:
		if_hsi_send_break(ch);
		break;
	case CS_FLUSH_RX:
		if_hsi_flush_rx(ch, &size);
		if (copy_to_user((void __user *)arg, &size, sizeof(size)))
			ret = -EFAULT;
		break;
	case CS_FLUSH_TX:
		if_hsi_flush_tx(ch, &size);
		if (copy_to_user((void __user *)arg, &size, sizeof(size)))
			ret = -EFAULT;
		break;
	case CS_SET_ACWAKELINE:
		if (copy_from_user(&state, (void __user *)arg, sizeof(state)))
			ret = -EFAULT;
		else
			if_hsi_set_acwakeline(ch, state);
		break;
	case CS_GET_ACWAKELINE:
		if_hsi_get_acwakeline(ch, &state);
		if (copy_to_user((void __user *)arg, &state, sizeof(state)))
			ret = -EFAULT;
		break;
	case CS_SET_WAKE_RX_3WIRES_MODE:
		if (copy_from_user(&state, (void __user *)arg, sizeof(state)))
			ret = -EFAULT;
		else
			if_hsi_set_wake_rx_3wires_mode(ch, state);
		break;
	case CS_GET_CAWAKELINE:
		if_hsi_get_cawakeline(ch, &state);
		if (copy_to_user((void __user *)arg, &state, sizeof(state)))
			ret = -EFAULT;
		break;
	case CS_SET_RX:
		if (copy_from_user(&rx_cfg, (void __user *)arg, sizeof(rx_cfg)))
			ret = -EFAULT;
		else
			ret = if_hsi_set_rx(ch, &rx_cfg);
		break;
	case CS_GET_RX:
		if_hsi_get_rx(ch, &rx_cfg);
		if (copy_to_user((void __user *)arg, &rx_cfg, sizeof(rx_cfg)))
			ret = -EFAULT;
		break;
	case CS_SET_TX:
		if (copy_from_user(&tx_cfg, (void __user *)arg, sizeof(tx_cfg)))
			ret = -EFAULT;
		else
			ret = if_hsi_set_tx(ch, &tx_cfg);
		break;
	case CS_GET_TX:
		if_hsi_get_tx(ch, &tx_cfg);
		if (copy_to_user((void __user *)arg, &tx_cfg, sizeof(tx_cfg)))
			ret = -EFAULT;
		break;
	case CS_SW_RESET:
		if_hsi_sw_reset(ch);
		break;
	case CS_GET_FIFO_OCCUPANCY:
		if_hsi_get_fifo_occupancy(ch, &size);
		if (copy_to_user((void __user *)arg, &size, sizeof(size)))
			ret = -EFAULT;
		break;
	default:
		ret = -ENOIOCTLCMD;
		break;
	}

	return ret;
}

static int hsi_char_open(struct inode *inode, struct file *file)
{
	int ret = 0, ch = iminor(inode);
	int i;

	for (i = 0; i < HSI_MAX_CHAR_DEVS; i++)
		if ((channels_map[i] - 1) == ch)
			break;

	if (i == HSI_MAX_CHAR_DEVS) {
		pr_err("HSI char open: Channel %d not found\n", ch);
		return -ENODEV;
	}

	pr_debug("HSI char open: opening channel %d\n", ch);

	spin_lock_bh(&hsi_char_data[ch].lock);

	if (hsi_char_data[ch].opened) {
		spin_unlock_bh(&hsi_char_data[ch].lock);
		pr_err("HSI char open: Channel %d already opened\n", ch);
		return -EBUSY;
	}

	file->private_data = (void *)ch;
	hsi_char_data[ch].opened++;
	hsi_char_data[ch].poll_event = (POLLOUT | POLLWRNORM);
	spin_unlock_bh(&hsi_char_data[ch].lock);

	ret = if_hsi_start(ch);

	return ret;
}

static int hsi_char_release(struct inode *inode, struct file *file)
{
	int ch = (int)file->private_data;
	struct char_queue *entry;
	struct list_head *cursor, *next;

	pr_debug("%s, ch = %d\n", __func__, ch);

	if_hsi_stop(ch);
	spin_lock_bh(&hsi_char_data[ch].lock);
	hsi_char_data[ch].opened--;

	if (!list_empty(&hsi_char_data[ch].rx_queue)) {
		list_for_each_safe(cursor, next, &hsi_char_data[ch].rx_queue) {
			entry = list_entry(cursor, struct char_queue, list);
			list_del(&entry->list);
			kfree(entry);
		}
	}

	if (!list_empty(&hsi_char_data[ch].tx_queue)) {
		list_for_each_safe(cursor, next, &hsi_char_data[ch].tx_queue) {
			entry = list_entry(cursor, struct char_queue, list);
			list_del(&entry->list);
			kfree(entry);
		}
	}

	spin_unlock_bh(&hsi_char_data[ch].lock);

	return 0;
}

static const struct file_operations hsi_char_fops = {
	.owner = THIS_MODULE,
	.read = hsi_char_read,
	.write = hsi_char_write,
	.poll = hsi_char_poll,
	.unlocked_ioctl = hsi_char_ioctl,
	.open = hsi_char_open,
	.release = hsi_char_release,
	.fasync = hsi_char_fasync,
};

static struct cdev hsi_char_cdev;

static int __init hsi_char_init(void)
{
	int ret, i, i_dev_create = 0;

	pr_info("HSI character device version " DRIVER_VERSION "\n");
	pr_info(HSI_CHAR_DEVICE_NAME ": %d channels mapped\n", num_channels);

	for (i = 0; i < HSI_MAX_CHAR_DEVS; i++) {
		init_waitqueue_head(&hsi_char_data[i].rx_wait);
		init_waitqueue_head(&hsi_char_data[i].tx_wait);
		init_waitqueue_head(&hsi_char_data[i].poll_wait);
		spin_lock_init(&hsi_char_data[i].lock);
		hsi_char_data[i].opened = 0;
		INIT_LIST_HEAD(&hsi_char_data[i].rx_queue);
		INIT_LIST_HEAD(&hsi_char_data[i].tx_queue);
	}

	ret = if_hsi_init(port, channels_map, num_channels);
	if (ret < 0) {
		pr_err(HSI_CHAR_DEVICE_NAME ": Failed to init HSI driver\n");
		return ret;
	}

	/* Allocate the range of minor numbers */
	ret = alloc_chrdev_region(&hsi_char_dev, 0, num_channels,
				  HSI_CHAR_DEVICE_NAME);
	if (ret < 0) {
		pr_err(HSI_CHAR_DEVICE_NAME": Failed to register char device "
					   "numbers\n");
		goto out_hsi_exit;
	}
	pr_debug(HSI_CHAR_DEVICE_NAME ": Allocated major %d, minor 0 to %d\n",
		 MAJOR(hsi_char_dev), num_channels);

	cdev_init(&hsi_char_cdev, &hsi_char_fops);
	ret = cdev_add(&hsi_char_cdev, hsi_char_dev, HSI_MAX_CHAR_DEVS);
	if (ret < 0) {
		pr_err(HSI_CHAR_DEVICE_NAME ": Failed to add char devices\n");
		goto out_unregister_chrdev;
	}

	/* Expose the hsi_char device to user space*/
	hsi_char_major = MAJOR(hsi_char_dev);
	hsi_char_class = class_create(THIS_MODULE, HSI_CHAR_DEVICE_NAME);
	if (IS_ERR(hsi_char_class)) {
		pr_err(HSI_CHAR_DEVICE_NAME ": Failed to create class\n");
		goto out_cdev_del;
	}

	for (i = 0; (i < num_channels) && channels_map[i]; i++) {
		struct device *dev;
		dev = device_create(hsi_char_class, NULL,
				MKDEV(hsi_char_major, channels_map[i] - 1),
				NULL, "hsi%d", channels_map[i] - 1);
		if (IS_ERR(dev)) {
			pr_err("Error in device_create hsi%d\n",
				channels_map[i] - 1);
			i_dev_create = i;
			goto out_device_destroy;
		}
		pr_info("HSI device_created /dev/hsi%d\n", channels_map[i] - 1);
	}

	return 0;

out_device_destroy:
	for (i = 0; (i < i_dev_create) && channels_map[i]; i++)
		device_destroy(hsi_char_class,
			       MKDEV(hsi_char_major, channels_map[i] - 1));
	class_destroy(hsi_char_class);
out_cdev_del:
	cdev_del(&hsi_char_cdev);
out_unregister_chrdev:
	unregister_chrdev_region(hsi_char_dev, num_channels);
out_hsi_exit:
	if_hsi_exit();

	return ret;
}

static void __exit hsi_char_exit(void)
{
	int i;

	cdev_del(&hsi_char_cdev);
	for (i = 0; (i < num_channels) && channels_map[i]; i++)
		device_destroy(hsi_char_class,
			       MKDEV(hsi_char_major, channels_map[i] - 1));

	class_destroy(hsi_char_class);
	unregister_chrdev_region(hsi_char_dev, num_channels);
	if_hsi_exit();
}

MODULE_AUTHOR("Andras Domokos <andras.domokos@nokia.com>");
MODULE_AUTHOR("Sebatien Jan <s-jan@ti.com> / Texas Instruments");
MODULE_AUTHOR("Djamil Elaidi <d-elaidi@ti.com> / Texas Instruments");
MODULE_DESCRIPTION("HSI character device");
MODULE_LICENSE("GPL");
MODULE_VERSION(DRIVER_VERSION);

module_init(hsi_char_init);
module_exit(hsi_char_exit);