#include <linux/module.h>
#include <linux/init.h>
#include <linux/usb.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/errno.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/version.h>
#include <linux/jiffies.h>
#include <linux/timer.h>

#define VENDOR_ID 0x13c0

#define USB_MINOR_BASE 1

#define USB_INFO KERN_INFO "_SL_USBIO_: "

#define SL_READ_MONITOR_INTERVAL 1000000

#define SL_INTERRUPT_TIMEOUT 500000

#if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 15, 0))
#define SLX_USE_TIMER_SETUP /* 4.15.0 required */
#endif

static DEFINE_MUTEX(disconnect_mutex);

struct usb_sl
{
	int flag;
	unsigned short idProduct;
	struct usb_endpoint_descriptor *int_in_endpoint;
	struct usb_endpoint_descriptor *int_out_endpoint;
	struct urb *int_in_urb;
	struct urb *int_out_urb;
	struct usb_device *usbdev;
	unsigned char *int_in_buffer;
	unsigned char *int_out_buffer;
	unsigned char *user_in_buffer;
	unsigned char *user_out_buffer;
	int in_transfer_type;
	int out_transfer_type;
	size_t len_in_buffer;
	size_t len_out_buffer;
	size_t len_user_buffer;
	bool ongoing_read;
	bool ongoing_write;
	wait_queue_head_t int_in_wait;
	wait_queue_head_t int_out_wait;
	bool is_busy;
	bool is_polling;
	unsigned char idle_cnt;
	struct usb_interface *interface;

	struct timer_list monitor_timer;
	spinlock_t state_lock;
};

static void free_usb_sl(struct usb_sl *dev)
{
	printk(USB_INFO "free_usb_sl()\n");
	usb_free_urb(dev->int_in_urb);
	usb_free_urb(dev->int_out_urb);
	usb_put_dev(dev->usbdev);
	kfree(dev->int_in_buffer);
	kfree(dev->int_out_buffer);
	kfree(dev->user_in_buffer);
	kfree(dev->user_out_buffer);
	kfree(dev);
}

static struct usb_driver sl_driver;

static int sl_open(struct inode *inode, struct file *filp)
{
	struct usb_sl *dev = NULL;
	int retval = 0;
	int subminor;
	struct usb_interface *interface;
	// printk(USB_INFO "sl_open()\n");

	subminor = iminor(inode);

	interface = usb_find_interface(&sl_driver, subminor);

	if (!interface)
	{
		pr_err("_SL_%s - error, can't find device for minor %d\n", __func__, subminor);
		retval = -ENODEV;
		goto exit;
	}

	dev = usb_get_intfdata(interface);
	if (!dev)
	{
		retval = -ENODEV;
		goto exit;
	}
	filp->private_data = dev;

	dev->flag = 0;
	dev->len_in_buffer = 0;
	dev->len_out_buffer = 0;
	dev->ongoing_read = false;
	dev->ongoing_write = false;

	// printk(USB_INFO "data pointer recorded in the open file\n");
exit:
	return retval;
}

static int sl_release(struct inode *inode, struct file *file)
{
	// printk(USB_INFO "sl_release()\n");
	return 0;
}

static void sl_read_int_callback(struct urb *urb)
{
	struct usb_sl *dev;
	// printk(USB_INFO "slr_read_int_callback()\n");

	dev = urb->context;

	if (urb->status)
	{
		if (!(urb->status == -ENOENT ||
			  urb->status == -ECONNRESET ||
			  urb->status == -ESHUTDOWN))
			pr_err(USB_INFO "nonzero write interuption status received: %d\n", urb->status);
	}
	dev->len_in_buffer = urb->actual_length;
	// printk(USB_INFO "actual len:%d\n",(int) dev->len_in_buffer);
	dev->ongoing_read = false;

	if (dev->flag == 1)
		dev->flag = 2;

	wake_up_interruptible(&dev->int_in_wait);
}

static ssize_t sl_read(struct file *filp, char *buffer, size_t count, loff_t *ppos)
{
	ssize_t rs = 0;
	struct usb_sl *dev = NULL;
	// printk(USB_INFO "sl_read()\n");

	dev = filp->private_data;

	dev->idle_cnt = 0;

retry:
	if (dev->ongoing_read)
	{

		// printk(USB_INFO "\t dev->ongoing_read = true, waiting...\n");

		rs = wait_event_interruptible_timeout(dev->int_in_wait, (!dev->ongoing_read), usecs_to_jiffies(SL_INTERRUPT_TIMEOUT));

		// printk(USB_INFO "\t wake up!!!\n");
		if (rs <= 0)
		{
			printk(USB_INFO "\t rs=%d, error while waiting\n", (int)rs);
			dev->ongoing_read = false;
			dev->len_in_buffer = 0;
			dev->is_busy = false;
			dev->flag = 0;
			return rs;
		}

		if (dev->flag == 2)
		{
			dev->len_in_buffer = 0;
			dev->flag = 3;
		}
	}

	if (dev->len_in_buffer > 0)
	{
		if (dev->idProduct == 0x0070)
		{
			dev->user_in_buffer[0] = dev->int_in_buffer[29];
			dev->user_in_buffer[1] = dev->int_in_buffer[30];
			dev->user_in_buffer[4] = dev->int_in_buffer[41];

			rs = copy_to_user(buffer, dev->user_in_buffer, dev->len_user_buffer);

			// printk(USB_INFO "return of the reading:%d\n", (unsigned int)rs);
			*ppos += dev->len_user_buffer - rs;

			rs = dev->len_user_buffer - rs;
			// printk(USB_INFO "\t dev->len_user_buffer = %d\n", (int)dev->len_user_buffer);
		}
		else if (dev->idProduct == 0x0080)
		{
			dev->user_in_buffer[0] = dev->int_in_buffer[29];
			dev->user_in_buffer[1] = dev->int_in_buffer[30];
			dev->user_in_buffer[2] = dev->int_in_buffer[31];
			dev->user_in_buffer[4] = dev->int_in_buffer[41];
			//TODO: add inputs

			rs = copy_to_user(buffer, dev->user_in_buffer, dev->len_user_buffer);

			// printk(USB_INFO "return of the reading:%d\n", (unsigned int)rs);
			*ppos += dev->len_user_buffer - rs;

			rs = dev->len_user_buffer - rs;
			// printk(USB_INFO "\t dev->len_user_buffer = %d\n", (int)dev->len_user_buffer);
		}
		else if (dev->idProduct == 0x01c0)
		{
			dev->user_in_buffer[0] = dev->int_in_buffer[36];
			dev->user_in_buffer[4] = dev->int_in_buffer[52];

			rs = copy_to_user(buffer, dev->user_in_buffer, dev->len_user_buffer);

			// printk(USB_INFO "return of the reading:%d\n", (unsigned int)rs);
			*ppos += dev->len_user_buffer - rs;

			rs = dev->len_user_buffer - rs;
			// printk(USB_INFO "\t dev->len_user_buffer = %d\n", (int)dev->len_user_buffer);
		}
		else if (dev->idProduct == 0xc001)
		{
			dev->user_in_buffer[0] = dev->int_in_buffer[29];
			dev->user_in_buffer[1] = dev->int_in_buffer[30];
			dev->user_in_buffer[4] = dev->int_in_buffer[41];

			rs = copy_to_user(buffer, dev->user_in_buffer, dev->len_user_buffer);

			// printk(USB_INFO "return of the reading:%d\n", (unsigned int)rs);
			*ppos += dev->len_user_buffer - rs;

			rs = dev->len_user_buffer - rs;
			// printk(USB_INFO "\t dev->len_user_buffer = %d\n", (int)dev->len_user_buffer);
		}
		else
		{
			rs = copy_to_user(buffer, dev->int_in_buffer, dev->len_in_buffer);

			// printk(USB_INFO "return of the reading:%d\n", (unsigned int)rs);
			*ppos += dev->len_in_buffer - rs;

			rs = dev->len_in_buffer - rs;
			// printk(USB_INFO "\t dev->len_in_buffer = %d\n", (int)dev->len_in_buffer);
		}

		dev->len_in_buffer = 0;
		dev->is_busy = false;
		dev->flag = 0;
		return rs;
	}

	if (dev->flag == 0 || dev->flag == 3)
	{
		dev->is_busy = true;

		// printk(USB_INFO "\t preparing the urb\n");
		if (usb_endpoint_is_int_in(dev->int_in_endpoint))
			usb_fill_int_urb(
				dev->int_in_urb,
				dev->usbdev,
				usb_rcvintpipe(dev->usbdev,
							dev->int_in_endpoint->bEndpointAddress),
				dev->int_in_buffer,
				dev->int_in_endpoint->wMaxPacketSize,
				sl_read_int_callback,
				dev,
				dev->int_in_endpoint->bInterval);

		if (usb_endpoint_is_bulk_in(dev->int_in_endpoint))
			usb_fill_bulk_urb(
				dev->int_in_urb,
				dev->usbdev,
				usb_rcvbulkpipe(dev->usbdev,
								dev->int_in_endpoint->bEndpointAddress),
				dev->int_in_buffer,
				dev->int_in_endpoint->wMaxPacketSize,
				sl_read_int_callback,
				dev);

		// printk(USB_INFO "\t sending the urb\n");
		rs = usb_submit_urb(dev->int_in_urb, GFP_KERNEL);
		if (rs < 0)
		{
			pr_err(USB_INFO "failed submitting read urb, error %d\n", (int)rs);
			dev->is_busy = false;
			dev->flag = 0;
			return rs;
		}
		dev->ongoing_read = true;

		if (dev->flag == 0)
			dev->flag = 1;

		if (dev->flag == 3)
			dev->flag = 4;

		// printk(USB_INFO "\t going to retry\n");
		goto retry;
	}

	dev->flag = 0;
	return rs;
}

static void sl_write_int_callback(struct urb *urb)
{
	struct usb_sl *dev;
	// printk(USB_INFO "sl_write_int_callback()\n");

	dev = urb->context;

	if (urb->status)
	{
		if (!(urb->status == -ENOENT ||
			  urb->status == -ECONNRESET ||
			  urb->status == -ESHUTDOWN))
			pr_err(USB_INFO "nonzero write interuption status received: %d\n", urb->status);
	}
	dev->len_out_buffer = urb->actual_length;
	// printk(USB_INFO "actual len:%d\n",(int) dev->len_out_buffer);
	dev->ongoing_write = false;

	wake_up_interruptible(&dev->int_out_wait);
}

static ssize_t sl_write(struct file *filp, const char *buffer, size_t count, loff_t *ppos)
{
	ssize_t rs = 0, res = 0;
	struct usb_sl *dev = NULL;
	// printk(USB_INFO "sl_write()\n");

	dev = filp->private_data;

	dev->is_busy = true;

	// printk(USB_INFO "\t count = %d\n", (int)count);
	if (count > 0 && count <= 64)
	{
		if (dev->idProduct == 0x0070)
		{
			rs = copy_from_user(dev->user_out_buffer, buffer, count);

			dev->int_out_buffer[29] = dev->user_out_buffer[0];
			dev->int_out_buffer[30] = dev->user_out_buffer[1];
			dev->int_out_buffer[41] = dev->user_out_buffer[4];
		}
		else if (dev->idProduct == 0x0080)
		{
			rs = copy_from_user(dev->user_out_buffer, buffer, count);
			dev->int_out_buffer[29] = dev->user_out_buffer[0];
			dev->int_out_buffer[30] = dev->user_out_buffer[1];
			dev->int_out_buffer[31] = dev->user_out_buffer[2];
			dev->int_out_buffer[41] = dev->user_out_buffer[4];
		}
		else if (dev->idProduct == 0x01c0)
		{
			rs = copy_from_user(dev->user_out_buffer, buffer, count);

			dev->int_out_buffer[36] = dev->user_out_buffer[0];
			dev->int_out_buffer[52] = dev->user_out_buffer[4];
		}
		else if (dev->idProduct == 0xc001)
		{
			rs = copy_from_user(dev->user_out_buffer, buffer, count);

			dev->int_out_buffer[29] = dev->user_out_buffer[0];
			dev->int_out_buffer[30] = dev->user_out_buffer[1];
			dev->int_out_buffer[41] = dev->user_out_buffer[4];
		}
		else
			rs = copy_from_user(dev->int_out_buffer, buffer, count);

		// printk(USB_INFO "return of the writing:%d\n", (unsigned int)rs);
		*ppos += count - rs;

		rs = count - rs;

		// printk(USB_INFO "\t preparing the urb\n");
		if (usb_endpoint_is_int_out(dev->int_out_endpoint))
			usb_fill_int_urb(
				dev->int_out_urb,
				dev->usbdev,
				usb_sndintpipe(dev->usbdev,
							dev->int_out_endpoint->bEndpointAddress),
				dev->int_out_buffer,
				dev->int_out_endpoint->wMaxPacketSize,
				sl_write_int_callback,
				dev,
				dev->int_out_endpoint->bInterval);

		if (usb_endpoint_is_bulk_out(dev->int_out_endpoint))
			usb_fill_bulk_urb(
				dev->int_out_urb,
				dev->usbdev,
				usb_sndbulkpipe(dev->usbdev,
								dev->int_out_endpoint->bEndpointAddress),
				dev->int_out_buffer,
				dev->int_out_endpoint->wMaxPacketSize,
				sl_write_int_callback,
				dev);

		// printk(USB_INFO "\t sending the urb\n");
		res = usb_submit_urb(dev->int_out_urb, GFP_KERNEL);
		if (res < 0)
		{
			pr_err(USB_INFO "failed submitting write urb, error %d\n", (int)res);
			dev->is_busy = false;
			return res;
		}
		dev->ongoing_write = true;
	}

	if (dev->ongoing_write)
	{
		// printk(USB_INFO "\t dev->ongoing_write = true, waiting...\n");

		res = wait_event_interruptible_timeout(dev->int_out_wait, (!dev->ongoing_write), usecs_to_jiffies(SL_INTERRUPT_TIMEOUT));

		// printk(USB_INFO "\t wake up!!!\n");
		if (res <= 0)
		{
			printk(USB_INFO "\t res=%d, error while waiting\n", (int)res);
			dev->is_busy = false;
			return res;
		}
	}

	dev->is_busy = false;

	return rs;
}

static const struct file_operations sl_fops = {
	.owner = THIS_MODULE,
	.open = sl_open,
	.release = sl_release,
	.read = sl_read,
	.write = sl_write,
};

static struct usb_class_driver sl_class = {
	.name = "sl_usb_io%d",
	.fops = &sl_fops,
	.minor_base = USB_MINOR_BASE,
};

static struct usb_device_id id_table[] = {
	{USB_DEVICE(VENDOR_ID, 0x0011)},
	{USB_DEVICE(VENDOR_ID, 0x0040)},
	{USB_DEVICE(VENDOR_ID, 0x0050)},
	{USB_DEVICE(VENDOR_ID, 0x0060)},
	{USB_DEVICE(VENDOR_ID, 0x0070)},
	{USB_DEVICE(VENDOR_ID, 0x0080)},
	{USB_DEVICE(VENDOR_ID, 0x0090)},
	{USB_DEVICE(VENDOR_ID, 0x00A0)},
	{USB_DEVICE(VENDOR_ID, 0x00B0)},
	{USB_DEVICE(VENDOR_ID, 0x00C0)},
	{USB_DEVICE(VENDOR_ID, 0x01C0)},
	{USB_DEVICE(VENDOR_ID, 0xC001)},
	{},
};

MODULE_DEVICE_TABLE(usb, id_table);

static void sl_read_monitor_callback(struct urb *urb)
{
	struct usb_sl *dev;

	dev = urb->context;

	dev->ongoing_read = false;
}

#ifdef SLX_USE_TIMER_SETUP
static void sl_read_monitor(struct timer_list *timer)
{
	struct usb_sl *dev = container_of(timer, struct usb_sl, monitor_timer);
#else
static void slx_input_monitor(unsigned long data)
{
	struct usb_sl *dev = (struct usb_sl *)data;
#endif

	ssize_t rs = 0;

	if (!dev->is_busy && dev->idle_cnt == 5)
	{
		dev->is_polling = true;

		// printk(USB_INFO "%d-%s sl_read_task idProduct = %04X\n", dev->usbdev->bus->busnum, dev->usbdev->devpath, dev->idProduct);

		dev->ongoing_read = true;

		if (usb_endpoint_is_int_in(dev->int_in_endpoint))
			usb_fill_int_urb(
				dev->int_in_urb,
				dev->usbdev,
				usb_rcvintpipe(dev->usbdev,
							dev->int_in_endpoint->bEndpointAddress),
				dev->int_in_buffer,
				dev->int_in_endpoint->wMaxPacketSize,
				sl_read_monitor_callback,
				dev,
				dev->int_in_endpoint->bInterval);

		if (usb_endpoint_is_bulk_in(dev->int_in_endpoint))
			usb_fill_bulk_urb(
				dev->int_in_urb,
				dev->usbdev,
				usb_rcvbulkpipe(dev->usbdev,
								dev->int_in_endpoint->bEndpointAddress),
				dev->int_in_buffer,
				dev->int_in_endpoint->wMaxPacketSize,
				sl_read_monitor_callback,
				dev);		

		rs = usb_submit_urb(dev->int_in_urb, GFP_KERNEL);

		// rs = usb_submit_urb(dev->int_in_urb, GFP_ATOMIC);
		if (rs < 0)
		{
			// pr_err(USB_INFO "===failed submitting read urb, error %d\n", (int)rs);
		}
	}

	dev->idle_cnt = (dev->idle_cnt + 1) % 6;

	/* restart timer */
	mod_timer(&dev->monitor_timer, jiffies +
									   usecs_to_jiffies(SL_READ_MONITOR_INTERVAL));

	dev->is_polling = false;
}

static int sl_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
	int retval = -ENODEV;
	struct usb_sl *dev = NULL;
	struct usb_endpoint_descriptor *endpoint;
	struct usb_host_interface *iface_desc;
	int i;

	dev = kzalloc(sizeof(*dev), GFP_KERNEL);

	if (!dev)
	{
		pr_err(USB_INFO "Out of memory\n");
		goto error;
	}

	dev->usbdev = usb_get_dev(interface_to_usbdev(interface));

	init_waitqueue_head(&dev->int_in_wait);
	init_waitqueue_head(&dev->int_out_wait);

	iface_desc = interface->cur_altsetting;

	dev->idProduct = id->idProduct;

	dev->len_user_buffer = 8;

	for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i)
	{
		endpoint = &iface_desc->endpoint[i].desc;

		// if (!dev->int_in_endpoint && usb_endpoint_is_int_in(endpoint))
		if (!dev->int_in_endpoint && usb_endpoint_dir_in(endpoint))
		{
			dev->int_in_endpoint = endpoint;
			dev->int_in_buffer = kmalloc(endpoint->wMaxPacketSize,
										 GFP_KERNEL);
			dev->user_in_buffer = kmalloc(dev->len_user_buffer, GFP_KERNEL);

			if (!dev->int_in_buffer)
			{
				pr_err(USB_INFO "Could not allocate int_in_buffer\n");
				goto error;
			}
			if (!dev->user_in_buffer)
			{
				pr_err(USB_INFO "Could not allocate user_in_buffer\n");
				goto error;
			}
			dev->int_in_endpoint->bInterval = 50;
			printk(USB_INFO "IN endpoint found at 0x%x\n", dev->int_in_endpoint->bEndpointAddress);
		}

		// if (!dev->int_out_endpoint && usb_endpoint_is_int_out(endpoint))
		if (!dev->int_out_endpoint && usb_endpoint_dir_out(endpoint))
		{
			dev->int_out_endpoint = endpoint;
			dev->int_out_buffer = kmalloc(endpoint->wMaxPacketSize,
										  GFP_KERNEL);
			dev->user_out_buffer = kmalloc(dev->len_user_buffer, GFP_KERNEL);

			if (!dev->int_out_buffer)
			{
				pr_err(USB_INFO "Could not allocate int_out_buffer\n");
				goto error;
			}
			if (!dev->user_out_buffer)
			{
				pr_err(USB_INFO "Could not allocate user_out_buffer\n");
				goto error;
			}
			dev->int_out_endpoint->bInterval = 50;
			printk(USB_INFO "OUT endpoint found at 0x%x\n", dev->int_out_endpoint->bEndpointAddress);
		}
	}
	if (!dev->int_in_endpoint)
	{
		pr_err(USB_INFO "could not find interrupt IN endpoint\n");
		goto error;
	}
	if (!dev->int_out_endpoint)
	{
		pr_err(USB_INFO "could not find interrupt OUT endpoint\n");
		goto error;
	}

	dev->in_transfer_type = usb_endpoint_type(dev->int_in_endpoint);
	dev->out_transfer_type = usb_endpoint_type(dev->int_out_endpoint);

	dev->int_in_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!dev->int_in_urb)
	{
		pr_err(USB_INFO "Could not allocate int_in_urb\n");
		goto error;
	}

	dev->int_out_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (!dev->int_out_urb)
	{
		pr_err(USB_INFO "Could not allocate int_out_urb\n");
		goto error;
	}

	dev->is_busy = false;
	dev->is_polling = false;
	dev->idle_cnt = 0;

	dev->interface = interface;

	usb_set_intfdata(interface, dev);

	retval = usb_register_dev(interface, &sl_class);
	if (retval)
	{
		pr_err(USB_INFO "Not able to get a minor for this device.\n");
		usb_set_intfdata(interface, NULL);
		goto error;
	}

/* hardware monitor */
#ifdef SLX_USE_TIMER_SETUP
	timer_setup(&dev->monitor_timer,
				sl_read_monitor,
				0);
#else
	setup_timer(&dev->monitor_timer,
				sl_read_monitor,
				(unsigned long)dev);
#endif

	/* start the device monitor */
	mod_timer(&dev->monitor_timer, jiffies +
									   usecs_to_jiffies(SL_READ_MONITOR_INTERVAL));

	return 0;

error:

	if (dev)
	{
		free_usb_sl(dev);
	}
	return retval;
}

static void sl_disconnect(struct usb_interface *interface)
{
	struct usb_sl *dev;
	printk(USB_INFO "sl_disconnect()\n");

	mutex_lock(&disconnect_mutex);
	dev = usb_get_intfdata(interface);

	dev->idle_cnt = 0;

	/* stop the device monitor */
    //del_timer_sync(&dev->monitor_timer);
    timer_delete_sync(&dev->monitor_timer);

	usb_set_intfdata(interface, NULL);
	if (dev)
	{
		free_usb_sl(dev);
	}
	usb_deregister_dev(interface, &sl_class);

	mutex_unlock(&disconnect_mutex);
	// printk(USB_INFO "all cleaned!\n");
}

static struct usb_driver sl_driver = {
	.name = "sl_usb_io_driver",
	.id_table = id_table,
	.probe = sl_probe,
	.disconnect = sl_disconnect,
};

static int __init usb_sl_init(void)
{
	int retval = -1;
	// printk(USB_INFO "Constructor of driver - usb_sl_init() -\n");
	printk(USB_INFO "Registering kernel with driver\n");
	retval = usb_register(&sl_driver);
	if (retval)
		pr_err(USB_INFO "usb_register failed. Error number %d\n", retval);
	else
		printk(USB_INFO "Registration is complete\n");
	return retval;
}

static void __exit usb_sl_exit(void)
{
	// printk(USB_INFO "Destructor of the driver - usb_sl_exit() -\n");
	usb_deregister(&sl_driver);
	printk(USB_INFO "Unregistration complete\n");
}

module_init(usb_sl_init);
module_exit(usb_sl_exit);

MODULE_AUTHOR("StreamLabs");
MODULE_LICENSE("GPL v2");
