/* Copyright (c) 2011-2013, 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/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/spinlock.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/sched.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <mach/msm_iomap.h>

#include <linux/fsm_dfe_hh.h>

/*
 * DFE of FSM9XXX
 */

#define HH_ADDR_MASK			0x000ffffc
#define HH_OFFSET_VALID(offset)		(((offset) & ~HH_ADDR_MASK) == 0)
#define HH_REG_IOADDR(offset)		((uint8_t *) MSM_HH_BASE + (offset))
#define HH_MAKE_OFFSET(blk, adr)	(((blk)&0x1F)<<15|((adr)&0x1FFF)<<2)

#define HH_REG_SCPN_IREQ_MASK		HH_REG_IOADDR(HH_MAKE_OFFSET(5, 0x12))
#define HH_REG_SCPN_IREQ_FLAG		HH_REG_IOADDR(HH_MAKE_OFFSET(5, 0x13))

/*
 * Device private information per device node
 */

#define HH_IRQ_FIFO_SIZE		64
#define HH_IRQ_FIFO_EMPTY(pdev)		((pdev)->irq_fifo_head == \
					(pdev)->irq_fifo_tail)
#define HH_IRQ_FIFO_FULL(pdev)		((((pdev)->irq_fifo_tail + 1) % \
					HH_IRQ_FIFO_SIZE) == \
					(pdev)->irq_fifo_head)

#define UINT32_MAX  (0xFFFFFFFFU)

static struct hh_dev_node_info {
	spinlock_t hh_lock;
	char irq_fifo[HH_IRQ_FIFO_SIZE];
	unsigned int irq_fifo_head, irq_fifo_tail;
	wait_queue_head_t wq;
} hh_dev_info;

/*
 * Device private information per file
 */

struct hh_dev_file_info {
	/* Buffer */
	unsigned int *parray;
	unsigned int array_num;

	struct dfe_command_entry *pcmd;
	unsigned int cmd_num;
};

/*
 * File interface
 */

static int hh_open(struct inode *inode, struct file *file)
{
	struct hh_dev_file_info *pdfi;

	/* private data allocation */
	pdfi = kmalloc(sizeof(*pdfi), GFP_KERNEL);
	if (pdfi == NULL)
		return -ENOMEM;
	file->private_data = pdfi;

	/* buffer initialization */
	pdfi->parray = NULL;
	pdfi->array_num = 0;
	pdfi->pcmd = NULL;
	pdfi->cmd_num = 0;

	return 0;
}

static int hh_release(struct inode *inode, struct file *file)
{
	struct hh_dev_file_info *pdfi;

	pdfi = (struct hh_dev_file_info *) file->private_data;

	kfree(pdfi->parray);
	pdfi->parray = NULL;
	pdfi->array_num = 0;

	kfree(pdfi->pcmd);
	pdfi->pcmd = NULL;
	pdfi->cmd_num = 0;

	kfree(file->private_data);
	file->private_data = NULL;

	return 0;
}

static ssize_t hh_read(struct file *filp, char __user *buf, size_t count,
	loff_t *f_pos)
{
	signed char irq = -1;
	unsigned long irq_flags;

	do {
		spin_lock_irqsave(&hh_dev_info.hh_lock, irq_flags);
		if (!HH_IRQ_FIFO_EMPTY(&hh_dev_info)) {
			irq = hh_dev_info.irq_fifo[hh_dev_info.irq_fifo_head];
			if (++hh_dev_info.irq_fifo_head == HH_IRQ_FIFO_SIZE)
				hh_dev_info.irq_fifo_head = 0;
		}
		spin_unlock_irqrestore(&hh_dev_info.hh_lock, irq_flags);

		if (irq < 0)
			if (wait_event_interruptible(hh_dev_info.wq,
				!HH_IRQ_FIFO_EMPTY(&hh_dev_info)) < 0)
				break;
	} while (irq < 0);

	if (irq < 0) {
		/* No pending interrupt */
		return 0;
	} else {
		put_user(irq, buf);
		return 1;
	}

	return 0;
}

static ssize_t hh_write(struct file *file, const char __user *buffer,
	size_t count, loff_t *ppos)
{
	return 0;
}

static long hh_ioctl(struct file *file,
	unsigned int cmd, unsigned long arg)
{
	unsigned int __user *argp = (unsigned int __user *) arg;
	struct hh_dev_file_info *pdfi =
		(struct hh_dev_file_info *) file->private_data;

	switch (cmd) {
	case DFE_IOCTL_READ_REGISTER:
		{
			unsigned int offset, value;

			if (get_user(offset, argp))
				return -EFAULT;
			if (!HH_OFFSET_VALID(offset))
				return -EINVAL;
			value = __raw_readl(HH_REG_IOADDR(offset));
			if (put_user(value, argp))
				return -EFAULT;
		}
		break;

	case DFE_IOCTL_WRITE_REGISTER:
		{
			struct dfe_write_register_param param;

			if (copy_from_user(&param, argp, sizeof param))
				return -EFAULT;
			if (!HH_OFFSET_VALID(param.offset))
				return -EINVAL;
			__raw_writel(param.value,
				HH_REG_IOADDR(param.offset));
		}
		break;

	case DFE_IOCTL_WRITE_REGISTER_WITH_MASK:
		{
			struct dfe_write_register_mask_param param;
			unsigned int value;
			unsigned long irq_flags;

			if (copy_from_user(&param, argp, sizeof param))
				return -EFAULT;
			if (!HH_OFFSET_VALID(param.offset))
				return -EINVAL;
			spin_lock_irqsave(&hh_dev_info.hh_lock,
				irq_flags);
			value = __raw_readl(HH_REG_IOADDR(param.offset));
			value &= ~param.mask;
			value |= param.value & param.mask;
			__raw_writel(value, HH_REG_IOADDR(param.offset));
			spin_unlock_irqrestore(&hh_dev_info.hh_lock,
				irq_flags);
		}
		break;

	case DFE_IOCTL_READ_REGISTER_ARRAY:
	case DFE_IOCTL_WRITE_REGISTER_ARRAY:
		{
			struct dfe_read_write_array_param param;
			unsigned int req_sz;
			unsigned long irq_flags;
			unsigned int i;
			void *addr;

			if (copy_from_user(&param, argp, sizeof param))
				return -EFAULT;
			if (!HH_OFFSET_VALID(param.offset))
				return -EINVAL;
			if ((param.num == 0) ||
			(param.num >= (UINT32_MAX / sizeof(unsigned int))))
				return -EINVAL;
			req_sz = sizeof(unsigned int) * param.num;

			if (pdfi->array_num < param.num) {
				void *pmem;

				pmem = kmalloc(req_sz, GFP_KERNEL);
				if (pmem == NULL)
					return -ENOMEM;
				pdfi->parray = (unsigned int *) pmem;
				pdfi->array_num = param.num;
			}

			if (cmd == DFE_IOCTL_WRITE_REGISTER_ARRAY)
				if (copy_from_user(pdfi->parray,
					param.pArray, req_sz))
					return -EFAULT;

			addr = HH_REG_IOADDR(param.offset);

			spin_lock_irqsave(&hh_dev_info.hh_lock,
				irq_flags);
			for (i = 0; i < param.num; ++i, addr += 4) {
				if (cmd == DFE_IOCTL_READ_REGISTER_ARRAY)
					pdfi->parray[i] = __raw_readl(addr);
				else
					__raw_writel(pdfi->parray[i], addr);
			}
			spin_unlock_irqrestore(&hh_dev_info.hh_lock,
				irq_flags);

			if (cmd == DFE_IOCTL_READ_REGISTER_ARRAY)
				if (copy_to_user(pdfi->parray,
					param.pArray, req_sz))
					return -EFAULT;
		}
		break;

	case DFE_IOCTL_COMMAND:
		{
			struct dfe_command_param param;
			unsigned int req_sz;
			unsigned long irq_flags;
			unsigned int i, value;
			struct dfe_command_entry *pcmd;
			void *addr;

			if (copy_from_user(&param, argp, sizeof param))
				return -EFAULT;
			if ((param.num == 0) ||
			(param.num >= (UINT32_MAX  /
			sizeof(struct dfe_command_entry))))
				return -EINVAL;
			req_sz = sizeof(struct dfe_command_entry) * param.num;

			if (pdfi->cmd_num < param.num) {
				void *pmem;

				pmem = kmalloc(req_sz, GFP_KERNEL);
				if (pmem == NULL)
					return -ENOMEM;
				pdfi->pcmd = (struct dfe_command_entry *) pmem;
				pdfi->cmd_num = param.num;
			}

			if (copy_from_user(pdfi->pcmd, param.pEntry, req_sz))
				return -EFAULT;

			pcmd = pdfi->pcmd;

			spin_lock_irqsave(&hh_dev_info.hh_lock,
				irq_flags);
			for (i = 0; i < param.num; ++i, ++pcmd) {
				if (!HH_OFFSET_VALID(pcmd->offset))
					return -EINVAL;
				addr = HH_REG_IOADDR(pcmd->offset);

				switch (pcmd->code) {
				case DFE_IOCTL_COMMAND_CODE_WRITE:
					__raw_writel(pcmd->value, addr);
					break;
				case DFE_IOCTL_COMMAND_CODE_WRITE_WITH_MASK:
					value = __raw_readl(addr);
					value &= ~pcmd->mask;
					value |= pcmd->value & pcmd->mask;
					__raw_writel(value, addr);
					break;
				}
			}
			spin_unlock_irqrestore(&hh_dev_info.hh_lock,
				irq_flags);
		}
		break;

	default:
		return -EINVAL;
	}

	return 0;
}

static unsigned int hh_poll(struct file *filp,
	struct poll_table_struct *wait)
{
	unsigned mask = 0;

	if (!HH_IRQ_FIFO_EMPTY(&hh_dev_info))
		mask |= POLLIN;

	if (mask == 0) {
		poll_wait(filp, &hh_dev_info.wq, wait);
		if (!HH_IRQ_FIFO_EMPTY(&hh_dev_info))
			mask |= POLLIN;
	}

	return mask;
}

static const struct file_operations hh_fops = {
	.owner = THIS_MODULE,
	.open = hh_open,
	.release = hh_release,
	.read = hh_read,
	.write = hh_write,
	.unlocked_ioctl = hh_ioctl,
	.poll = hh_poll,
};

/*
 * Interrupt handling
 */

static irqreturn_t hh_irq_handler(int irq, void *data)
{
	unsigned int irq_enable, irq_flag, irq_mask;
	int i;

	irq_enable = __raw_readl(HH_REG_SCPN_IREQ_MASK);
	irq_flag = __raw_readl(HH_REG_SCPN_IREQ_FLAG);
	irq_flag &= irq_enable;

	/* Disables interrupts */
	irq_enable &= ~irq_flag;
	__raw_writel(irq_enable, HH_REG_SCPN_IREQ_MASK);

	/* Adds the pending interrupts to irq_fifo */
	spin_lock(&hh_dev_info.hh_lock);
	for (i = 0, irq_mask = 1; i < 32; ++i, irq_mask <<= 1) {
		if (HH_IRQ_FIFO_FULL(&hh_dev_info))
			break;
		if (irq_flag & irq_mask) {
			hh_dev_info.irq_fifo[hh_dev_info.irq_fifo_tail] = \
				(char) i;
			if (++hh_dev_info.irq_fifo_tail == HH_IRQ_FIFO_SIZE)
				hh_dev_info.irq_fifo_tail = 0;
		}
	}
	spin_unlock(&hh_dev_info.hh_lock);

	/* Wakes up pending processes */
	wake_up_interruptible(&hh_dev_info.wq);

	return IRQ_HANDLED;
}

/*
 * Driver initialization & cleanup
 */

static struct miscdevice hh_misc_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DFE_HH_DEVICE_NAME,
	.fops = &hh_fops,
};

static int __init hh_init(void)
{
	int ret;

	/* lock initialization */
	spin_lock_init(&hh_dev_info.hh_lock);

	/* interrupt handler */
	hh_dev_info.irq_fifo_head = 0;
	hh_dev_info.irq_fifo_tail = 0;
	ret = request_irq(INT_HH_SUPSS_IRQ, hh_irq_handler,
		IRQF_TRIGGER_RISING, "hh_dev", 0);
	if (ret < 0) {
		pr_err("Cannot register HH interrupt handler.\n");
		return ret;
	}

	/* wait queue */
	init_waitqueue_head(&hh_dev_info.wq);

	return misc_register(&hh_misc_dev);
}

static void __exit hh_exit(void)
{
	misc_deregister(&hh_misc_dev);
	free_irq(INT_HH_SUPSS_IRQ, 0);
}

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Rohit Vaswani <rvaswani@codeaurora.org>");
MODULE_DESCRIPTION("Qualcomm Hammerhead Digital Front End driver");
MODULE_VERSION("1.0");

module_init(hh_init);
module_exit(hh_exit);

