/*
 * Intel MIC Platform Software Stack (MPSS)
 *
 * Copyright(c) 2013 Intel Corporation.
 *
 * This program 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 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.
 *
 * The full GNU General Public License is included in this distribution in
 * the file called "COPYING".
 *
 * Intel MIC Host driver.
 *
 */
#include <linux/poll.h>
#include <linux/pci.h>

#include <linux/mic_common.h>
#include "../common/mic_dev.h"
#include "mic_device.h"
#include "mic_fops.h"
#include "mic_virtio.h"

int mic_open(struct inode *inode, struct file *f)
{
	struct mic_vdev *mvdev;
	struct mic_device *mdev = container_of(inode->i_cdev,
		struct mic_device, cdev);

	mvdev = kzalloc(sizeof(*mvdev), GFP_KERNEL);
	if (!mvdev)
		return -ENOMEM;

	init_waitqueue_head(&mvdev->waitq);
	INIT_LIST_HEAD(&mvdev->list);
	mvdev->mdev = mdev;
	mvdev->virtio_id = -1;

	f->private_data = mvdev;
	return 0;
}

int mic_release(struct inode *inode, struct file *f)
{
	struct mic_vdev *mvdev = (struct mic_vdev *)f->private_data;

	if (-1 != mvdev->virtio_id)
		mic_virtio_del_device(mvdev);
	f->private_data = NULL;
	kfree(mvdev);
	return 0;
}

long mic_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
	struct mic_vdev *mvdev = (struct mic_vdev *)f->private_data;
	void __user *argp = (void __user *)arg;
	int ret;

	switch (cmd) {
	case MIC_VIRTIO_ADD_DEVICE:
	{
		ret = mic_virtio_add_device(mvdev, argp);
		if (ret < 0) {
			dev_err(mic_dev(mvdev),
				"%s %d errno ret %d\n",
				__func__, __LINE__, ret);
			return ret;
		}
		break;
	}
	case MIC_VIRTIO_COPY_DESC:
	{
		struct mic_copy_desc copy;

		ret = mic_vdev_inited(mvdev);
		if (ret)
			return ret;

		if (copy_from_user(&copy, argp, sizeof(copy)))
			return -EFAULT;

		dev_dbg(mic_dev(mvdev),
			"%s %d === iovcnt 0x%x vr_idx 0x%x update_used %d\n",
			__func__, __LINE__, copy.iovcnt, copy.vr_idx,
			copy.update_used);

		ret = mic_virtio_copy_desc(mvdev, &copy);
		if (ret < 0) {
			dev_err(mic_dev(mvdev),
				"%s %d errno ret %d\n",
				__func__, __LINE__, ret);
			return ret;
		}
		if (copy_to_user(
			&((struct mic_copy_desc __user *)argp)->out_len,
			&copy.out_len, sizeof(copy.out_len))) {
			dev_err(mic_dev(mvdev), "%s %d errno ret %d\n",
				__func__, __LINE__, -EFAULT);
			return -EFAULT;
		}
		break;
	}
	case MIC_VIRTIO_CONFIG_CHANGE:
	{
		ret = mic_vdev_inited(mvdev);
		if (ret)
			return ret;

		ret = mic_virtio_config_change(mvdev, argp);
		if (ret < 0) {
			dev_err(mic_dev(mvdev),
				"%s %d errno ret %d\n",
				__func__, __LINE__, ret);
			return ret;
		}
		break;
	}
	default:
		return -ENOIOCTLCMD;
	};
	return 0;
}

/*
 * We return POLLIN | POLLOUT from poll when new buffers are enqueued, and
 * not when previously enqueued buffers may be available. This means that
 * in the card->host (TX) path, when userspace is unblocked by poll it
 * must drain all available descriptors or it can stall.
 */
unsigned int mic_poll(struct file *f, poll_table *wait)
{
	struct mic_vdev *mvdev = (struct mic_vdev *)f->private_data;
	int mask = 0;

	poll_wait(f, &mvdev->waitq, wait);

	if (mic_vdev_inited(mvdev)) {
		mask = POLLERR;
	} else if (mvdev->poll_wake) {
		mvdev->poll_wake = 0;
		mask = POLLIN | POLLOUT;
	}

	return mask;
}

static inline int
mic_query_offset(struct mic_vdev *mvdev, unsigned long offset,
		 unsigned long *size, unsigned long *pa)
{
	struct mic_device *mdev = mvdev->mdev;
	unsigned long start = MIC_DP_SIZE;
	int i;

	/*
	 * MMAP interface is as follows:
	 * offset				region
	 * 0x0					virtio device_page
	 * 0x1000				first vring
	 * 0x1000 + size of 1st vring		second vring
	 * ....
	 */
	if (!offset) {
		*pa = virt_to_phys(mdev->dp);
		*size = MIC_DP_SIZE;
		return 0;
	}

	for (i = 0; i < mvdev->dd->num_vq; i++) {
		struct mic_vringh *mvr = &mvdev->mvr[i];
		if (offset == start) {
			*pa = virt_to_phys(mvr->vring.va);
			*size = mvr->vring.len;
			return 0;
		}
		start += mvr->vring.len;
	}
	return -1;
}

/*
 * Maps the device page and virtio rings to user space for readonly access.
 */
int
mic_mmap(struct file *f, struct vm_area_struct *vma)
{
	struct mic_vdev *mvdev = (struct mic_vdev *)f->private_data;
	unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
	unsigned long pa, size = vma->vm_end - vma->vm_start, size_rem = size;
	int i, err;

	err = mic_vdev_inited(mvdev);
	if (err)
		return err;

	if (vma->vm_flags & VM_WRITE)
		return -EACCES;

	while (size_rem) {
		i = mic_query_offset(mvdev, offset, &size, &pa);
		if (i < 0)
			return -EINVAL;
		err = remap_pfn_range(vma, vma->vm_start + offset,
			pa >> PAGE_SHIFT, size, vma->vm_page_prot);
		if (err)
			return err;
		dev_dbg(mic_dev(mvdev),
			"%s %d type %d size 0x%lx off 0x%lx pa 0x%lx vma 0x%lx\n",
			__func__, __LINE__, mvdev->virtio_id, size, offset,
			pa, vma->vm_start + offset);
		size_rem -= size;
		offset += size;
	}
	return 0;
}