/*******************************************************************************
    AudioScience HPI driver
    Common Linux HPI ioctl and module probe/remove functions

    Copyright (C) 1997-2017  AudioScience Inc. <support@audioscience.com>

    This program is free software; you can redistribute it and/or modify
    it under the terms of version 2 of the GNU General Public License 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.

*******************************************************************************/
#define SOURCEFILE_NAME "hpioctl.c"

#include "hpi_internal.h"
#include "hpi_version.h"
#include "hpimsginit.h"
#include "hpidebug.h"
#include "hpimsgx.h"
#include "hpioctl.h"
#include "hpicmn.h"

#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/moduleparam.h>
#include <linux/uaccess.h>
#include <linux/pci.h>
#include <linux/stringify.h>
#include <linux/module.h>
#include <linux/vmalloc.h>

#ifdef MODULE_FIRMWARE

#ifndef HPI_VER_MAJMIN_STRING
#error "HPI_VER_MAJMIN_STRING must be defined."
#endif

MODULE_FIRMWARE("asihpi/" HPI_VER_MAJMIN_STRING "/dsp5000.bin");
MODULE_FIRMWARE("asihpi/" HPI_VER_MAJMIN_STRING "/dsp6200.bin");
MODULE_FIRMWARE("asihpi/" HPI_VER_MAJMIN_STRING "/dsp6205.bin");
MODULE_FIRMWARE("asihpi/" HPI_VER_MAJMIN_STRING "/dsp6400.bin");
MODULE_FIRMWARE("asihpi/" HPI_VER_MAJMIN_STRING "/dsp6600.bin");
MODULE_FIRMWARE("asihpi/" HPI_VER_MAJMIN_STRING "/dsp6700.bin");
MODULE_FIRMWARE("asihpi/" HPI_VER_MAJMIN_STRING "/dsp8700.bin");
MODULE_FIRMWARE("asihpi/" HPI_VER_MAJMIN_STRING "/dsp8900.bin");
#endif

static int prealloc_stream_buf;
module_param(prealloc_stream_buf, int, S_IRUGO);
MODULE_PARM_DESC(prealloc_stream_buf,
	"Preallocate size for per-adapter stream buffer");

static int per_stream_dma_buf_max_size;
module_param(per_stream_dma_buf_max_size, int, S_IRUGO);
MODULE_PARM_DESC(per_stream_dma_buf_max_size,
	"Max size for per-stream DMA buffer");

/* Allow the debug level to be changed after module load.
 E.g.   echo 2 > /sys/module/asihpi/parameters/hpiDebugLevel
*/
module_param(hpiDebugLevel, int, S_IRUGO|S_IWUSR);
MODULE_PARM_DESC(hpiDebugLevel, "Debug verbosity 0..5");

/* Wrapper function to HPI_Message to enable dumping of the
   message and response types.
*/
static void HPI_Message_f(
		struct hpi_message *phm,
		struct hpi_response *phr,
		struct file *file)
{
	if ((phm->wAdapterIndex >= HPI_MAX_ADAPTERS) &&
	    (phm->wObject != HPI_OBJ_SUBSYSTEM))
		phr->wError = HPI_ERROR_INVALID_OBJ_INDEX;
	else
		HPI_MessageEx(phm, phr, file);
}

/* This is called from hpifunc.c functions, called by ALSA
 * (or other kernel process) In this case there is no file descriptor
 * available for the message cache code
 */
void HPI_Message(struct hpi_message* phm, struct hpi_response *phr)
{
	HPI_Message_f(phm, phr, HOWNER_KERNEL);
}
EXPORT_SYMBOL(HPI_Message);
/* for radio-asihpi */

int asihpi_hpi_release(struct file *file)
{
	struct hpi_message hm;
	struct hpi_response hr;

/* HPI_DEBUG_LOG(INFO,"hpi_release file %p, pid %d\n", file, current->pid); */
	/* close the subsystem just in case the application forgot to.*/
	HPI_InitMessageResponse(&hm, &hr,  HPI_OBJ_SUBSYSTEM, HPI_SUBSYS_CLOSE);
	HPI_MessageEx(&hm, &hr, file);
	return 0;
}

long asihpi_hpi_ioctl(struct file *file,
	  unsigned int cmd, unsigned long arg)
{
	struct hpi_ioctl_linux __user *phpi_ioctl_data;
	void __user *puhm;
	void __user *puhr;
	union hpi_message_buffer_v1 *hm;
	union hpi_response_buffer_v1 *hr;
	u16 msg_size;
	u16 res_max_size;
	u32 uncopied_bytes;
	int err = 0;
	ktime_t before = ktime_get();
	ktime_t after;
	u64 elapsed_ns;
	int microseconds;

	if (cmd != HPI_IOCTL_LINUX)
		return -EINVAL;


	hm = kmalloc(sizeof(*hm), GFP_KERNEL);
	hr = kzalloc(sizeof(*hr), GFP_KERNEL);
	if (!hm || !hr) {
		err = -ENOMEM;
		goto out;
	}

	phpi_ioctl_data = (struct hpi_ioctl_linux __user *) arg;

	/* Read the message and response pointers from user space.  */
	if (get_user(puhm, &phpi_ioctl_data->phm) ||
	     get_user(puhr, &phpi_ioctl_data->phr)) {
		err = -EFAULT;
		goto out;
	}

	/* Now read the message size and data from user space.  */
	if (get_user(msg_size, (u16 __user *)puhm)) {
		err = -EFAULT;
		goto out;
	}
	if (msg_size > sizeof(*hm)) {
		err = -ENOMEM;
		goto out;
	}

	/* printk(KERN_INFO "message size %d\n", hm->h.wSize); */

	uncopied_bytes = copy_from_user(hm, puhm, msg_size);
	if (uncopied_bytes) {
		HPI_DEBUG_LOG(ERROR, "uncopied bytes %d\n", uncopied_bytes);
		err = -EFAULT;
		goto out;
	}

	/* Override h.size in case it is changed between two userspace fetches */
	hm->h.wSize = msg_size;

	if (get_user(res_max_size, (u16 __user *)puhr)) {
		err = -EFAULT;
		goto out;
	}
	/* printk(KERN_INFO "user response size %d\n", res_max_size); */
	if (res_max_size < sizeof(struct hpi_response_header)) {
		HPI_DEBUG_LOG(WARNING, "Small res size %d\n", res_max_size);
		err = -EFAULT;
		goto out;
	}

	res_max_size = min_t(size_t, res_max_size, sizeof(*hr));

	switch (hm->h.wFunction) {
	case HPI_SUBSYS_CREATE_ADAPTER:
	case HPI_ADAPTER_DELETE:
		/* Application must not use these functions! */
		hr->h.wSize = sizeof(hr->h);
		hr->h.wError = HPI_ERROR_INVALID_OPERATION;
		hr->h.wFunction = hm->h.wFunction;
		uncopied_bytes = copy_to_user(puhr, hr, hr->h.wSize);
		if (uncopied_bytes)
			err = -EFAULT;
		else
			err = 0;
		goto out;
	}

	hr->h.wSize = res_max_size;
 	if (hm->h.wObject == HPI_OBJ_SUBSYSTEM) {
		HPI_Message_f(&hm->m0, &hr->r0, file);
	} else  {
		u16 __user *ptr = NULL;
		u32 size = 0;
		struct hpi_adapter_obj *pa;

		pa = HpiGetAdapter(hm->h.wAdapterIndex);

		if (!pa || !pa->type) {
			HPI_InitResponse(&hr->r0, hm->h.wObject, hm->h.wFunction,
						HPI_ERROR_BAD_ADAPTER_NUMBER);

			uncopied_bytes = copy_to_user(puhr, hr, sizeof(hr->h));
			if (uncopied_bytes)
				err = -EFAULT;
			else
				err = 0;
			goto out;
		}

		if (mutex_lock_interruptible(&pa->os.mutex)) {
			err = -EINTR;
			goto out;
		}

		/* Dig out any pointers embedded in the message.  */
		switch (hm->h.wFunction) {
		case HPI_OSTREAM_WRITE:
		case HPI_ISTREAM_READ: {
			/* Yes, sparse, this is correct. */
			ptr = (u16 __user *) hm->m0.u.d.u.data.pbData;
			size = hm->m0.u.d.u.data.dwDataSize;

			/* Allocate buffer according to application request.
			   ?Is it better to alloc/free for the duration
			   of the transaction?
			*/
			if (pa->os.buffer_size < size) {
				HPI_DEBUG_LOG(DEBUG,
					"Realloc adapter %d stream "
					"buffer from %zd to %d\n",
					hm->h.wAdapterIndex ,
					pa->os.buffer_size, size);

				pa->os.buffer_size = 0;
				vfree(pa->os.p_buffer);
				pa->os.p_buffer = vmalloc(size);
				if (pa->os.p_buffer)
					pa->os.buffer_size = size;
				else {
					HPI_DEBUG_LOG(ERROR,
						"HPI could not allocate "
						"stream buffer size %d\n",
						size);

					mutex_unlock(&pa->os.mutex);
					err = -EINVAL;
					goto out;
				}
			}

			hm->m0.u.d.u.data.pbData = pa->os.p_buffer;
			break;
		}

		default:
			size = 0;
			break;
		}

		if (size && (hm->h.wFunction == HPI_OSTREAM_WRITE))  {
			uncopied_bytes =
				copy_from_user(pa->os.p_buffer, ptr, size);
			if (uncopied_bytes)
				HPI_DEBUG_LOG(WARNING,
					"Missed %d of %d "
					"bytes from user\n",
					uncopied_bytes, size);
		}

		HPI_Message_f(&hm->m0, &hr->r0, file);

		if (size && (hm->h.wFunction == HPI_ISTREAM_READ)) {
			uncopied_bytes =
				copy_to_user(ptr, pa->os.p_buffer, size);
			if (uncopied_bytes)
				HPI_DEBUG_LOG(WARNING,
					"Missed %d of %d "
					"bytes to user\n",
					uncopied_bytes, size);
		}

		mutex_unlock(&pa->os.mutex);
	}

	/* on return response size must be set */
	//printk(KERN_INFO "response size %d\n", hr->h.wSize);

	if (!hr->h.wSize) {
		HPI_DEBUG_LOG(ERROR, "response zero size\n");
		err = -EFAULT;
		goto out;
	}

	if (hr->h.wSize > res_max_size) {
		HPI_DEBUG_LOG(ERROR, "response too big %d %d\n", hr->h.wSize, res_max_size);
		hr->h.wError = HPI_ERROR_RESPONSE_BUFFER_TOO_SMALL;
		hr->h.wSpecificError = hr->h.wSize;
		hr->h.wSize = sizeof(hr->h);
	}

	uncopied_bytes = copy_to_user(puhr, hr, hr->h.wSize);

	if (uncopied_bytes) {
		HPI_DEBUG_LOG(ERROR, "uncopied bytes %d\n", uncopied_bytes);
		err = -EFAULT;
		goto out;
	}

out:
	kfree(hm);
	kfree(hr);

	after = ktime_get();
	elapsed_ns = ktime_to_ns(ktime_sub(after, before));
	microseconds = do_div(elapsed_ns, 1000);
	HPI_DEBUG_LOG1(VERBOSE, "HPI transaction took %d microseconds.\n", microseconds);

	return err;
}

int asihpi_adapter_probe(struct pci_dev *pci_dev,
			const struct pci_device_id *pci_id)
{
	struct device *dev = &pci_dev->dev;
	struct hpi_adapter_obj *pa;
	adapter_initialize_func *adapter_init;

	dev_info(dev, "Probe %04x:%04x,%04x:%04x,%04x\n",
		pci_dev->vendor, pci_dev->device, pci_dev->subsystem_vendor,
		pci_dev->subsystem_device, pci_dev->devfn);

	if (pcim_enable_device(pci_dev) < 0) {
		dev_err(dev, "pci_enable_device failed, disabling device\n");
		return -EIO;
	}

	pci_set_master(pci_dev); /* also sets latency timer if < 16 */

	pa = devm_kzalloc(dev, sizeof(*pa), GFP_KERNEL);
	if (!pa)
		return -ENOMEM;

	pa->os.pci_dev = pci_dev;
	pci_set_drvdata(pci_dev, pa);

	if (per_stream_dma_buf_max_size) {
		pa->os.per_stream_dma_buf_max_size = per_stream_dma_buf_max_size;
	}

	adapter_init = (adapter_initialize_func *)pci_id->driver_data;
	if (!adapter_init || adapter_init(pa))
		goto err;

	if (HPIMSGX_AdapterPrepare(pa) || HpiAddAdapter(pa)) {
		pa->ep.shutdown(pa);
		goto err;
	}
	/* After HpiAddAdapter, can use HPI_MessageEx() */

	if (prealloc_stream_buf) {
		pa->os.p_buffer = vmalloc(prealloc_stream_buf);
		if (!pa->os.p_buffer) {
			dev_err(dev,
				"HPI could not allocate kernel buffer size %d\n",
				prealloc_stream_buf);
			goto err;
		}
		pa->os.buffer_size = prealloc_stream_buf;
	}

	mutex_init(&pa->os.mutex);

	dev_info(dev, "Probe succeeded for ASI%04X HPI index %d\n",
		pa->type, pa->index);

	return 0;

err:
	vfree(pa->os.p_buffer);
	dev_err(dev, "adapter_probe failed\n");
	return -ENODEV;
}

void asihpi_adapter_remove(struct pci_dev *pci_dev)
{
	struct hpi_message hm;
	struct hpi_response hr;
	struct hpi_adapter_obj *pa;

	pa = pci_get_drvdata(pci_dev);

	/* Disable IRQ generation on DSP side */
	HPI_InitMessageResponse(&hm, &hr, HPI_OBJ_ADAPTER,
		HPI_ADAPTER_SET_PROPERTY);
	hm.wAdapterIndex = pa->index;
	hm.u.ax.property_set.wProperty = HPI_ADAPTER_PROPERTY_IRQ_RATE;
	hm.u.ax.property_set.wParameter1 = 0;
	hm.u.ax.property_set.wParameter2 = 0;
	HPI_MessageEx(&hm, &hr, HOWNER_KERNEL);


	HPI_InitMessageResponse(&hm, &hr,  HPI_OBJ_ADAPTER, HPI_ADAPTER_DELETE);
	hm.wAdapterIndex = pa->index;
	HPI_MessageEx(&hm, &hr, HOWNER_KERNEL);

	HpiRemoveAdapter(pa);

	vfree(pa->os.p_buffer);
	mutex_destroy(&pa->os.mutex);

	if (1)
		dev_info(&pci_dev->dev,
			"remove %04x:%04x,%04x:%04x,%04x, HPI index %d.\n",
			pci_dev->vendor, pci_dev->device,
			pci_dev->subsystem_vendor, pci_dev->subsystem_device,
			pci_dev->devfn, pa->index);
}

void __init asihpi_init(void)
{
	struct hpi_message hm;
	struct hpi_response hr;

	printk(KERN_INFO "ASIHPI driver " HPI_VER_STRING "\n");

	HPI_InitMessageResponse(&hm, &hr,  HPI_OBJ_SUBSYSTEM, HPI_SUBSYS_DRIVER_LOAD);
	HPI_MessageEx(&hm, &hr, HOWNER_KERNEL);
}

void asihpi_exit(void) {
	struct hpi_message hm;
	struct hpi_response hr;

	HPI_InitMessageResponse(&hm, &hr, HPI_OBJ_SUBSYSTEM,
				HPI_SUBSYS_DRIVER_UNLOAD);
	HPI_MessageEx(&hm, &hr, HOWNER_KERNEL);
}
