/*
  Copyright 2004, 2005 Jean-Baptiste Note
  Copyright (C) 2003 Conexant Americas Inc.

  This file is part of prism54usb.

  prism54usb is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  prism54usb 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 prism54usb; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

*/

#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/smp_lock.h>
#include <linux/delay.h>

/* for register / unregister device */
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
/* for class_simple usage */
#include <linux/device.h>

#include <linux/ioctl.h>
#include <asm/uaccess.h>

#include "isl_38xx.h"
#include "isl_sm.h"
#include "islsm_uart.h"
#include "islsm_smioctl.h"
#include "islsm_log.h"
/* will allow to get rid of some ioctls easily */
#include "islsm_sysfs.h"

MODULE_DESCRIPTION("Prism54 UART-over-softmac driver");
MODULE_AUTHOR("Jean-Baptiste Note <jean-baptiste.note@m4x.org>");
MODULE_LICENSE("GPL");

/*******
 *   UART over SoftMac Instance Character driver
 *******/

/* The name for our device, as it will appear in /proc/devices */
#define DEVICE_NAME        "islsm"
#define DRV_NAME           "isluart"
#define ISLSM_OF_FILE(f)    ((struct islsm *)(f)->private_data)
#define UART_OF_FILE(f)     (&((struct islsm *)(f)->private_data)->uart)

/* module state */
static unsigned major;
static struct class *islsm_class;

static void uart_prot_reinit(struct islsm *device);

static int uart_init(uart_instance_t *uart_ref);
static void uart_cleanup(uart_instance_t *uart_ref);
static int uart_initq(uart_instance_t *, struct queue_s *queue, int size);
static void uart_cleanupq(struct queue_s *queue);

/* interface with userspace */
static int uart_open(struct inode *inode, struct file *filp);
static int uart_release(struct inode *inode, struct file *filp);
static ssize_t uart_read(struct file *filp, char *buffer,
			 size_t length, loff_t *offset);
static ssize_t uart_write(struct file *filp, const char *buffer,
			  size_t length, loff_t *offset);

void
uart_prot_reinit(struct islsm *device)
{
	uart_instance_t *uart = &device->uart;

	if (!uart->is_open)
		return;

	device->uart_prot_init(device);
}

static int
uart_initq(uart_instance_t *uart, struct queue_s *queue, int size)
{
	unsigned long flags;

	queue->q = kmalloc(size, GFP_ATOMIC);
	if (!queue->q) {
		printk(KERN_ERR "%s: Could not allocate queue\n", DRV_NAME);
		return -ENOMEM;
	}

	spin_lock_irqsave(&uart->lock, flags);
	queue->p = queue->c = 0;
	queue->s = size;
	spin_unlock_irqrestore(&uart->lock, flags);

	return 0;
}

static void
uart_cleanupq(struct queue_s *queue)
{
	kfree(queue->q);
}

static int
uart_open(struct inode *inode, struct file *filp)
{
	struct islsm *device;
	uart_instance_t *uart;
	struct cdev *dev = inode->i_cdev;

	islog(L_FUNC, "uart_open(%p,%p)\n", inode, filp);

	/* fetch the associated islsm device */
	uart = container_of(dev, uart_instance_t, dev);
	device = container_of(uart, struct islsm, uart);

	islog(L_DEBUG, "uart device is %p\n", device);

	filp->private_data = device;

	if (!uart->is_open)
		/* Initialize the UART over PCI protocol on first open */
		device->uart_prot_init(device);

	uart->is_open++;
	return 0;
}

static int
uart_release(struct inode *inode, struct file *filp)
{
	struct islsm *device = ISLSM_OF_FILE(filp);
	uart_instance_t *uart = UART_OF_FILE(filp);

	islog(L_FUNC, "uart_release(%p,%p)\n", inode, filp);

	uart->is_open--;

	/* last open restores the state */
	if (!uart->is_open)
		device->uart_prot_exit(device);

	return 0;
}

static unsigned int
uart_poll(struct file *filp, poll_table * wait)
{
	uart_instance_t *uart = UART_OF_FILE(filp);

	poll_wait(filp, &uart->wait, wait);

	if (uart_inq(uart, &uart->rxq) > 0)
		return POLLIN | POLLRDNORM;
	return 0;
}

static ssize_t
uart_read(struct file *filp, char *buffer, size_t length, loff_t *offset)
{
	uart_instance_t *uart = UART_OF_FILE(filp);
	unsigned long flags;
	int bytes_read, uart_size, err;
	DECLARE_WAITQUEUE(wait, current);

	islog(L_FUNC, "uart_read(%p,%p, %zd)\n", filp, buffer, length);

	err = 0;

	add_wait_queue(&uart->wait, &wait);
	set_current_state(TASK_INTERRUPTIBLE);

	spin_lock_irqsave(&uart->lock, flags);
	uart_size = uart_inq(uart, &uart->rxq);
	spin_unlock_irqrestore(&uart->lock, flags);

	while (uart_size <= 0) {
		if (filp->f_flags & O_NONBLOCK) {
			err = -EAGAIN;
			break;
		}
		schedule();
		if (signal_pending(current)) {
			err = -ERESTARTSYS;
			break;
		}

		spin_lock_irqsave(&uart->lock, flags);
		uart_size = uart_inq(uart, &uart->rxq);
		spin_unlock_irqrestore(&uart->lock, flags);
	}

	set_current_state(TASK_RUNNING);
	remove_wait_queue(&uart->wait, &wait);

	if (err)
		return err;

	bytes_read = 0;

	/* Note that we may well be returning without any read bytes,
	   in case some other process has stolen all available from us.
	 */
	while (bytes_read < length) {
		spin_lock_irqsave(&uart->lock, flags);
		if (uart_inq(uart, &uart->rxq) > 0) {
			int ret;
			uint8_t data;
			data = uart_qconsume(uart, &uart->rxq);
			spin_unlock_irqrestore(&uart->lock, flags);
			ret = put_user(data, buffer);
			/* we loose a character in this case */
			if (ret < 0)
				return ret;
			buffer++;
			bytes_read++;
		} else {
			spin_unlock_irqrestore(&uart->lock, flags);
			break;
		}
	}

	return bytes_read;
}

static ssize_t
uart_write(struct file *filp, const char *buffer, size_t length, loff_t *offset)
{
	struct islsm *device = ISLSM_OF_FILE(filp);
	uart_instance_t *uart = UART_OF_FILE(filp);
	unsigned long flags;

	int written = 0;
	char c;
	int tries = 0;

	islog(L_FUNC, "uart_write(%p,%p, %zd)\n", filp, buffer, length);

	/* while there are still bytes to be written */
	/* the idea there is that the data TX will happen
	   asynchronously in the interrupt handler. The current code has
	   a problem in case we miss an interrupt. */
	while (written < length && tries < 10) {
		/* fill it with one more character */
		if (get_user(c, buffer))
			return -EFAULT;

		/* if queue is not full */
		spin_lock_irqsave(&uart->lock, flags);
		if (uart_inq(uart, &uart->txq) < UARTPCI_TXQSIZE) {
			uart_qproduce(uart, &uart->txq, c);
			written++;
			buffer++;
			spin_unlock_irqrestore(&uart->lock, flags);
		} else {
			/* better yet, go to sleep, with timeout, until
			   buffer is somewhat emptied */
			/* else (queue is full),
			   see if we're not hogging the cpu */
			spin_unlock_irqrestore(&uart->lock, flags);
			schedule();
			tries++;
		}

		/* cts is raised when device gave us CTS without us
		   having more data to transmit. In this case, we need
		   to call uart_cts ourselves (the irq handler never
		   will do it) */
		if (uart->cts) {
			uart->cts = 0;
			device->uart_cts(device);
		}
	}

	if (tries >= 10)
		islog(L_DEBUG, "UPCI: Write Q full, giving up\n");

	return written;
}

/*
 * ioctl implementation
 */

static int
check_ioc_args(unsigned int cmd, unsigned long arg)
{
	int err = 0;
	/* do the checks */
	if (_IOC_TYPE(cmd) != ISLSM_IOC_MAGIC)
		return -ENOTTY;

	if (_IOC_DIR(cmd) & _IOC_READ)
		err = !access_ok(VERIFY_WRITE, (void *) arg, _IOC_SIZE(cmd));
	else if (_IOC_DIR(cmd) & _IOC_WRITE)
		err = !access_ok(VERIFY_READ, (void *) arg, _IOC_SIZE(cmd));

	if (err)
		return -EFAULT;
	return 0;
}

static int
uart_ioctl(struct inode *inode, struct file *filp,
	   unsigned int cmd, unsigned long arg)
{
	/* if we can issue ioctls without opening the device, then this
	   will segfault */
	int err;
	struct islsm *device;
	islsm_reg_t reg_arg;

	device = ISLSM_OF_FILE(filp);
	err = check_ioc_args(cmd, arg);

	if (err)
		return err;

	if (_IOC_NR(cmd) & ISLSM_IOC_ARGTYPE_REG) {
		/* argument is a reg */
		err = copy_from_user(&reg_arg, (void *) arg, sizeof (reg_arg));
		if (err)
			return -EFAULT;
	}

	switch (cmd) {
	case ISLSM_IOCGREG:
		{
			/* check that we are not out to lunch wrt pci addressing
			   space */
			if (!device->isl_read_pcireg)
				return -ENOTTY;
			if (reg_arg.addr >= ISL38XX_PCI_MEM_SIZE)
				return -EFAULT;
			reg_arg.val =
			    device->isl_read_pcireg(device, reg_arg.addr);
			break;
		}
	case ISLSM_IOCSREG:
		{
			/* check that we are not out to lunch wrt pci addressing
			   space */
			if (!device->isl_write_pcireg)
				return -ENOTTY;
			if (reg_arg.addr >= ISL38XX_PCI_MEM_SIZE)
				return -EFAULT;
			device->isl_write_pcireg(device, reg_arg.val,
						 reg_arg.addr);
			return 0;
		}
	case ISLSM_IOCGMEM:
		if (!device->isl_read_devmem)
			return -ENOTTY;
		reg_arg.val = device->isl_read_devmem(device, reg_arg.addr);
		break;
	case ISLSM_IOCSMEM:
		if (!device->isl_write_devmem)
			return -ENOTTY;
		device->isl_write_devmem(device, reg_arg.val, reg_arg.addr);
		return 0;
	case ISLSM_IOCBOOTROM:
		{
			/* only OK for PCI and PCI+USB */
			/* lacks implementation for 3887 */
			if (!device->isl_romboot)
				return -ENOTTY;
			(void) device->isl_romboot(device);
			uart_prot_reinit(device);
			return 0;
		}
	case ISLSM_IOCLOADFW:
		{
			int err = 0;
			/* load the firmware via the device-preferred
			   method. This is one of
			   - rom loading (3887, possibly others)
			   - dma loading (PCI+USB, possibly others)
			   - write in io space loading (PCI)
			 */
			/* should restart the uart protocol.
			   Don't do this yet, as this allows freemac to work
			   alright */
			if (!device->isl_boot)
				return -ENOTTY;
			err = device->isl_boot(device);
			if (!err)
				uart_prot_reinit(device);
			return err;
		}
	case ISLSM_IOCSENDPING:
	{
		islsm_ping_device(device, 0x30);
		/* should wait for the response */
		return 0;
	}
		/* should add an ioctl to reset the protocol, so that i don't
		   need to do it in the loadfw and bootrom case */
	default:
		return -ENOTTY;
	}

	if (_IOC_NR(cmd) & ISLSM_IOC_ARGTYPE_REG) {
		/* argument is a reg */
		err = copy_to_user((void *) arg, &reg_arg, sizeof (reg_arg));
		if (err)
			return err;
	}

	return 0;
}

static struct file_operations uart_fops = {
	.owner = THIS_MODULE,
	.read = uart_read,
	.write = uart_write,
	.open = uart_open,
	.release = uart_release,
	.poll = uart_poll,
	.ioctl = uart_ioctl,
};

/*****
 * Allocation functions
 *****/

static int
uart_init(uart_instance_t *uart)
{

	memset(uart, 0, sizeof (uart_instance_t));

	init_waitqueue_head(&uart->wait);
	spin_lock_init(&uart->lock);

	if (uart_initq(uart, &uart->txq, UARTPCI_TXQSIZE))
		goto failed;

	if (uart_initq(uart, &uart->rxq, UARTPCI_RXQSIZE))
		goto failed_txq;

	return 0;

      failed_txq:
	uart_cleanupq(&uart->txq);
      failed:
	return -ENOMEM;
}

int
uart_init_dev(struct islsm *islsm)
{
	uart_instance_t *uart = &islsm->uart;
	unsigned minor;
	dev_t dev;
	struct class_device *c;
	int err;

	FN_ENTER;
	err = uart_init(uart);

	if (err)
		goto failed;

	minor = islsm->minor;
	dev = MKDEV(major, minor);

	cdev_init(&uart->dev, &uart_fops);
	err = cdev_add(&uart->dev, dev, 1);
	if (err)
		goto failed_uart;

	c = class_device_create(islsm_class,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,15)
				NULL, dev, NULL,
#else
				dev, NULL,
#endif
				"islsm%d", minor);

	if (IS_ERR(c)) {
		/* Not fatal */
		printk(KERN_WARNING
		       "%s: Unable to add device to class for islsm %d, error %ld\n",
		       DRV_NAME, minor, PTR_ERR(c));
		goto failed;
	}

	/* create the sysfs entries */
	class_set_devdata(c, islsm);
	err = islsm_sysfs_create_files(c);
	if (err)
		goto failed_uart;

	FN_EXIT1(0);
	return 0;

 failed_uart:
	uart_cleanup(uart);
 failed:
	FN_EXIT1(err);
	return err;
}

EXPORT_SYMBOL(uart_init_dev);

static void
uart_cleanup(uart_instance_t *uart)
{
	unsigned long flags;
	/* maybe make sure no-one is waiting on the wait queue */
	spin_lock_irqsave(&uart->lock, flags);
	uart_cleanupq(&uart->txq);
	uart_cleanupq(&uart->rxq);
	spin_unlock_irqrestore(&uart->lock, flags);
}

void
uart_release_dev(struct islsm *islsm)
{
	FN_ENTER;
	class_device_destroy(islsm_class,MKDEV(major, islsm->minor));
	cdev_del(&islsm->uart.dev);
	uart_cleanup(&islsm->uart);
	FN_EXIT0;
}

EXPORT_SYMBOL(uart_release_dev);

/* Initialize the module - Register the character device */
#define DEVICE_RANGE 32
int
uart_init_module(void)
{
	int err = 0;
	dev_t dev;

	islsm_class = class_create(THIS_MODULE, "islsm");
	if (IS_ERR(islsm_class)) {
		printk(KERN_ERR "%s: error creating islsm class", DRV_NAME);
		err = -EINVAL;
		goto out;
	}

	/* register our range of character device */
	err = alloc_chrdev_region(&dev, 0, DEVICE_RANGE, DEVICE_NAME);
	if (err) {
		printk(KERN_ERR "%s: cannot allocate dev region", DRV_NAME);
		goto out_free_class;
	}

	major = MAJOR(dev);
	islog(L_DEBUG, "islsm_uart: registered device major %d\n", major);

	goto out;

 out_free_class:
	class_destroy(islsm_class);
 out:
	return err;
}

/* Cleanup - unregister the appropriate file from /proc */
void
uart_cleanup_module(void)
{
	FN_ENTER;
	unregister_chrdev_region(MKDEV(major, 0), DEVICE_RANGE);
	class_destroy(islsm_class);
	FN_EXIT0;
}

module_init(uart_init_module);
module_exit(uart_cleanup_module);
