/*
 * Copyright (c) 2014-2015, 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.
 */

#define pr_fmt(fmt) "seemp: %s: " fmt, __func__

#include "seemp_logk.h"
#include "seemp_ringbuf.h"

#ifndef VM_RESERVED
#define VM_RESERVED (VM_DONTEXPAND | VM_DONTDUMP)
#endif

#define MASK_BUFFER_SIZE 256
#define FOUR_MB 4
#define YEAR_BASE 1900

static struct seemp_logk_dev *slogk_dev;

static unsigned int ring_sz = FOUR_MB;

/*
 * default is besteffort, apps do not get blocked
 */
static unsigned int block_apps;


/*
 * When this flag is turned on,
 * kmalloc should be used for ring buf allocation
 * otherwise it is vmalloc.
 * default is to use vmalloc
 * kmalloc has a limit of 4MB
 */
unsigned int kmalloc_flag;

static struct class *cl;

static rwlock_t filter_lock;
static struct seemp_source_mask *pmask;
static unsigned int num_sources;

static long seemp_logk_reserve_rdblks(
		struct seemp_logk_dev *sdev, unsigned long arg);
static long seemp_logk_set_mask(unsigned long arg);
static long seemp_logk_set_mapping(unsigned long arg);
static long seemp_logk_check_filter(unsigned long arg);

void* (*seemp_logk_kernel_begin)(char **buf);

void (*seemp_logk_kernel_end)(void *blck);

/*
 * the last param is the permission bits *
 * kernel logging is done in three steps:
 * (1)  fetch a block, fill everything except payload.
 * (2)  return payload pointer to the caller.
 * (3)  caller fills its data directly into the payload area.
 * (4)  caller invoked finish_record(), to finish writing.
 */
void *seemp_logk_kernel_start_record(char **buf)
{
	struct seemp_logk_blk *blk;
	struct timespec now;
	struct tm ts;
	int idx;
	int ret;

	DEFINE_WAIT(write_wait);
	ret = 0;
	idx = 0;
	now = current_kernel_time();
	blk = ringbuf_fetch_wr_block(slogk_dev);
	if (!blk) {
		/*
		 * there is no blk to write
		 * if block_apps == 0; quietly return
		 */
		if (!block_apps) {
			*buf = NULL;
			return NULL;
		}
		/*else wait for the blks to be available*/
		while (1) {
			mutex_lock(&slogk_dev->lock);
			prepare_to_wait(&slogk_dev->writers_wq,
				&write_wait, TASK_INTERRUPTIBLE);
			ret = (slogk_dev->num_write_avail_blks <= 0);
			if (!ret) {
				/* don't have to wait*/
				break;
			}
			mutex_unlock(&slogk_dev->lock);
			if (signal_pending(current)) {
				ret = -EINTR;
				break;
			}
			schedule();
		}

		finish_wait(&slogk_dev->writers_wq, &write_wait);
		if (ret)
			return NULL;

		idx = slogk_dev->write_idx;
		slogk_dev->write_idx =
			(slogk_dev->write_idx + 1) % slogk_dev->num_tot_blks;
		slogk_dev->num_write_avail_blks--;
		slogk_dev->num_write_in_prog_blks++;
		slogk_dev->num_writers++;

		blk = &slogk_dev->ring[idx];
		/*mark block invalid*/
		blk->status = 0x0;
		mutex_unlock(&slogk_dev->lock);
	}

	blk->version = OBSERVER_VERSION;
	blk->pid = current->tgid;
	blk->tid = current->pid;
	blk->uid = (current_uid()).val;
	blk->sec = now.tv_sec;
	blk->nsec = now.tv_nsec;
	strlcpy(blk->appname, current->comm, TASK_COMM_LEN);
	time_to_tm(now.tv_sec, 0, &ts);
	ts.tm_year += YEAR_BASE;
	ts.tm_mon += 1;

	snprintf(blk->ts, TS_SIZE, "%04ld-%02d-%02d %02d:%02d:%02d",
			ts.tm_year, ts.tm_mon, ts.tm_mday,
			ts.tm_hour, ts.tm_min, ts.tm_sec);

	*buf = blk->payload.msg;

	return blk;
}

void seemp_logk_kernel_end_record(void *blck)
{
	struct seemp_logk_blk *blk = (struct seemp_logk_blk *)blck;

	if (blk) {
		/*update status at the very end*/
		blk->status |= 0x1;
		blk->uid =  (current_uid()).val;

		ringbuf_finish_writer(slogk_dev, blk);
	}
}

static int seemp_logk_usr_record(const char __user *buf, size_t count)
{
	struct seemp_logk_blk *blk;
	struct seemp_logk_blk usr_blk;
	struct seemp_logk_blk *local_blk;
	struct timespec now;
	struct tm ts;
	int idx, ret;

	DEFINE_WAIT(write_wait);
	if (buf) {
		local_blk = (struct seemp_logk_blk *)buf;
		if (copy_from_user(&usr_blk.pid, &local_blk->pid,
					sizeof(usr_blk.pid)) != 0)
			return -EFAULT;
		if (copy_from_user(&usr_blk.tid, &local_blk->tid,
					sizeof(usr_blk.tid)) != 0)
			return -EFAULT;
		if (copy_from_user(&usr_blk.uid, &local_blk->uid,
					sizeof(usr_blk.uid)) != 0)
			return -EFAULT;
		if (copy_from_user(&usr_blk.len, &local_blk->len,
					sizeof(usr_blk.len)) != 0)
			return -EFAULT;
		if (copy_from_user(&usr_blk.payload, &local_blk->payload,
					sizeof(struct blk_payload)) != 0)
			return -EFAULT;
	}
	idx = ret = 0;
	now = current_kernel_time();
	blk = ringbuf_fetch_wr_block(slogk_dev);
	if (!blk) {
		if (!block_apps)
			return 0;
		while (1) {
			mutex_lock(&slogk_dev->lock);
			prepare_to_wait(&slogk_dev->writers_wq,
					&write_wait,
					TASK_INTERRUPTIBLE);
			ret = (slogk_dev->num_write_avail_blks <= 0);
			if (!ret)
				break;
			mutex_unlock(&slogk_dev->lock);
			if (signal_pending(current)) {
				ret = -EINTR;
				break;
			}
			schedule();
		}
		finish_wait(&slogk_dev->writers_wq, &write_wait);
		if (ret)
			return -EINTR;

		idx = slogk_dev->write_idx;
		slogk_dev->write_idx =
			(slogk_dev->write_idx + 1) % slogk_dev->num_tot_blks;
		slogk_dev->num_write_avail_blks--;
		slogk_dev->num_write_in_prog_blks++;
		slogk_dev->num_writers++;
		blk = &slogk_dev->ring[idx];
		/*mark block invalid*/
		blk->status = 0x0;
		mutex_unlock(&slogk_dev->lock);
	}
	if (usr_blk.len > sizeof(struct blk_payload)-1)
		usr_blk.len = sizeof(struct blk_payload)-1;

	memcpy(&blk->payload, &usr_blk.payload, sizeof(struct blk_payload));
	blk->pid = usr_blk.pid;
	blk->uid = usr_blk.uid;
	blk->tid = usr_blk.tid;
	blk->sec = now.tv_sec;
	blk->nsec = now.tv_nsec;
	time_to_tm(now.tv_sec, 0, &ts);
	ts.tm_year += YEAR_BASE;
	ts.tm_mon += 1;
	snprintf(blk->ts, TS_SIZE, "%02ld-%02d-%02d %02d:%02d:%02d",
			ts.tm_year, ts.tm_mon, ts.tm_mday,
			ts.tm_hour, ts.tm_min, ts.tm_sec);
	strlcpy(blk->appname, current->comm, TASK_COMM_LEN);
	blk->status |= 0x1;
	ringbuf_finish_writer(slogk_dev, blk);
	return ret;
}

static void seemp_logk_attach(void)
{
	seemp_logk_kernel_end = seemp_logk_kernel_end_record;
	seemp_logk_kernel_begin = seemp_logk_kernel_start_record;
}

static void seemp_logk_detach(void)
{
	seemp_logk_kernel_begin = NULL;
	seemp_logk_kernel_end = NULL;
}

static ssize_t
seemp_logk_write(struct file *file, const char __user *buf, size_t count,
		loff_t *ppos)
{
	return seemp_logk_usr_record(buf, count);
}

static int
seemp_logk_open(struct inode *inode, struct file *filp)
{
	int ret;

	/*disallow seeks on this file*/
	ret = nonseekable_open(inode, filp);
	if (ret) {
		pr_err("ret= %d\n", ret);
		return ret;
	}

	slogk_dev->minor = iminor(inode);
	filp->private_data = slogk_dev;

	return 0;
}

static bool seemp_logk_get_bit_from_vector(__u8 *pVec, __u32 index)
{
	unsigned int byte_num = index/8;
	unsigned int bit_num = index%8;
	unsigned char byte = pVec[byte_num];

	return !(byte & (1 << bit_num));
}

static long seemp_logk_ioctl(struct file *filp, unsigned int cmd,
		unsigned long arg)
{
	struct seemp_logk_dev *sdev;
	int ret;

	sdev = (struct seemp_logk_dev *) filp->private_data;

	if (cmd == SEEMP_CMD_RESERVE_RDBLKS) {
		return seemp_logk_reserve_rdblks(sdev, arg);
	} else if (cmd == SEEMP_CMD_RELEASE_RDBLKS) {
		mutex_lock(&sdev->lock);
		sdev->read_idx = (sdev->read_idx + sdev->num_read_in_prog_blks)
			% sdev->num_tot_blks;
		sdev->num_write_avail_blks += sdev->num_read_in_prog_blks;
		ret = sdev->num_read_in_prog_blks;
		sdev->num_read_in_prog_blks = 0;
		/*wake up any waiting writers*/
		mutex_unlock(&sdev->lock);
		if (ret && block_apps)
			wake_up_interruptible(&sdev->writers_wq);
	} else if (cmd == SEEMP_CMD_GET_RINGSZ) {
		if (copy_to_user((unsigned int *)arg, &sdev->ring_sz,
				sizeof(unsigned int)))
			return -EFAULT;
	} else if (cmd == SEEMP_CMD_GET_BLKSZ) {
		if (copy_to_user((unsigned int *)arg, &sdev->blk_sz,
				sizeof(unsigned int)))
			return -EFAULT;
	} else if (SEEMP_CMD_SET_MASK == cmd) {
		return seemp_logk_set_mask(arg);
	} else if (SEEMP_CMD_SET_MAPPING == cmd) {
		return seemp_logk_set_mapping(arg);
	} else if (SEEMP_CMD_CHECK_FILTER == cmd) {
		return seemp_logk_check_filter(arg);
	} else {
		pr_err("Invalid Request %X\n", cmd);
		return -ENOIOCTLCMD;
	}
	return 0;
}

static long seemp_logk_reserve_rdblks(
		struct seemp_logk_dev *sdev, unsigned long arg)
{
	int ret;
	struct read_range rrange;

	DEFINE_WAIT(read_wait);
	mutex_lock(&sdev->lock);
	if (sdev->num_writers > 0 || sdev->num_read_avail_blks <= 0) {
		ret = -EPERM;
		pr_debug("(reserve): blocking, cannot read.\n");
		pr_debug("num_writers=%d num_read_avail_blks=%d\n",
		sdev->num_writers,
				sdev->num_read_avail_blks);
		mutex_unlock(&sdev->lock);
		/*
		 * unlock the device
		 * wait on a wait queue
		 * after wait, grab the dev lock again
		 */
		while (1) {
			mutex_lock(&sdev->lock);
			prepare_to_wait(&sdev->readers_wq, &read_wait,
					TASK_INTERRUPTIBLE);
			ret = (sdev->num_writers > 0 ||
					sdev->num_read_avail_blks <= 0);
			if (!ret) {
				/*don't have to wait*/
				break;
			}
			mutex_unlock(&sdev->lock);
			if (signal_pending(current)) {
				ret = -EINTR;
				break;
			}
			schedule();
		}

		finish_wait(&sdev->readers_wq, &read_wait);
		if (ret)
			return -EINTR;
	}

	/*sdev->lock is held at this point*/
	sdev->num_read_in_prog_blks = sdev->num_read_avail_blks;
	sdev->num_read_avail_blks = 0;
	rrange.start_idx = sdev->read_idx;
	rrange.num = sdev->num_read_in_prog_blks;
	mutex_unlock(&sdev->lock);

	if (copy_to_user((unsigned int *)arg, &rrange,
			sizeof(struct read_range)))
		return -EFAULT;

	return 0;
}

static long seemp_logk_set_mask(unsigned long arg)
{
	__u8 buffer[256];
	int i;
	unsigned int num_elements;

	if (copy_from_user(&num_elements,
		(unsigned int __user *) arg, sizeof(unsigned int)))
		return -EFAULT;

	read_lock(&filter_lock);
	if (0 == num_sources) {
		read_unlock(&filter_lock);
		return -EINVAL;
	}

	if (num_elements == 0 ||
		MASK_BUFFER_SIZE < DIV_ROUND_UP(num_sources, 8)) {
		read_unlock(&filter_lock);
		return -EINVAL;
	}

	if (copy_from_user(buffer,
			(__u8 *)arg, DIV_ROUND_UP(num_sources, 8))) {
		read_unlock(&filter_lock);
		return -EFAULT;
	}

	read_unlock(&filter_lock);
	write_lock(&filter_lock);
	if (num_elements != num_sources) {
		write_unlock(&filter_lock);
		return -EPERM;
	}

	for (i = 0; i < num_sources; i++) {
		pmask[i].isOn =
				seemp_logk_get_bit_from_vector(
					(__u8 *)buffer, i);
	}
	write_unlock(&filter_lock);
	return 0;
}

static long seemp_logk_set_mapping(unsigned long arg)
{
	__u32 num_elements;
	__u32 *pbuffer;
	int i;
	struct seemp_source_mask *pnewmask;

	if (copy_from_user(&num_elements,
					(__u32 __user *)arg, sizeof(__u32)))
		return -EFAULT;

	if ((0 == num_elements) || (num_elements >
		(UINT_MAX / sizeof(struct seemp_source_mask))))
		return -EFAULT;

	write_lock(&filter_lock);
	if (NULL != pmask) {
		/*
		 * Mask is getting set again.
		 * seemp_core was probably restarted.
		 */
		struct seemp_source_mask *ptempmask;

		num_sources = 0;
		ptempmask = pmask;
		pmask = NULL;
		kfree(ptempmask);
	}
	write_unlock(&filter_lock);
	pbuffer = kmalloc(sizeof(struct seemp_source_mask)
				* num_elements, GFP_KERNEL);
	if (NULL == pbuffer)
		return -ENOMEM;

	/*
	 * Use our new table as scratch space for now.
	 * We copy an ordered list of hash values into our buffer.
	 */
	if (copy_from_user(pbuffer, &((__u32 __user *)arg)[1],
					num_elements*sizeof(unsigned int))) {
		kfree(pbuffer);
		return -EFAULT;
	}
	/*
	 * We arrange the user data into a more usable form.
	 * This is done in-place.
	 */
	pnewmask = (struct seemp_source_mask *) pbuffer;
	for (i = num_elements - 1; i >= 0; i--) {
		pnewmask[i].hash = pbuffer[i];
		/* Observer is off by default*/
		pnewmask[i].isOn = 0;
	}
	write_lock(&filter_lock);
	pmask = pnewmask;
	num_sources = num_elements;
	write_unlock(&filter_lock);
	return 0;
}

static long seemp_logk_check_filter(unsigned long arg)
{
	int i;
	unsigned int hash = (unsigned int) arg;

	/*
	 * This lock may be a bit long.
	 * If it is a problem, it can be fixed.
	 */
	read_lock(&filter_lock);
	for (i = 0; i < num_sources; i++) {
		if (hash == pmask[i].hash) {
			int result = pmask[i].isOn;

			read_unlock(&filter_lock);
			return result;
		}
	}
	read_unlock(&filter_lock);
	return 0;
}

static int seemp_logk_mmap(struct file *filp,
		struct vm_area_struct *vma)
{
	int ret;
	char *vptr;
	unsigned long length, pfn;
	unsigned long start = vma->vm_start;

	length = vma->vm_end - vma->vm_start;

	if (length > (unsigned long) slogk_dev->ring_sz) {
		pr_err("len check failed\n");
		return -EIO;
	}

	vma->vm_flags |= VM_RESERVED | VM_SHARED;
	vptr = (char *) slogk_dev->ring;
	ret = 0;

	if (kmalloc_flag) {
		ret = remap_pfn_range(vma,
				start,
				virt_to_phys((void *)
				((unsigned long)slogk_dev->ring)) >> PAGE_SHIFT,
				length,
				vma->vm_page_prot);
		if (ret != 0) {
			pr_err("remap_pfn_range() fails with ret = %d\n",
				ret);
			return -EAGAIN;
		}
	} else {
		while (length > 0) {
			pfn = vmalloc_to_pfn(vptr);

			ret = remap_pfn_range(vma, start, pfn, PAGE_SIZE,
					vma->vm_page_prot);
			if (ret < 0) {
				pr_err("remap_pfn_range() fails with ret = %d\n",
					ret);
				return ret;
			}
			start += PAGE_SIZE;
			vptr += PAGE_SIZE;
			length -= PAGE_SIZE;
		}
	}

	return 0;
}

static const struct file_operations seemp_logk_fops = {
	.write = seemp_logk_write,
	.open = seemp_logk_open,
	.unlocked_ioctl = seemp_logk_ioctl,
	.compat_ioctl = seemp_logk_ioctl,
	.mmap = seemp_logk_mmap,
};

__init int seemp_logk_init(void)
{
	int ret;
	int devno = 0;

	num_sources = 0;
	kmalloc_flag = 0;
	block_apps = 0;
	pmask = NULL;

	if (kmalloc_flag && ring_sz > FOUR_MB) {
		pr_err("kmalloc cannot allocate > 4MB\n");
		return -ENOMEM;
	}

	ring_sz = ring_sz * SZ_1M;
	if (ring_sz <= 0) {
		pr_err("Too small a ring_sz=%d\n", ring_sz);
		return -EINVAL;
	}

	slogk_dev = kmalloc(sizeof(*slogk_dev), GFP_KERNEL);
	if (slogk_dev == NULL)
		return -ENOMEM;

	slogk_dev->ring_sz = ring_sz;
	slogk_dev->blk_sz = sizeof(struct seemp_logk_blk);
	/*initialize ping-pong buffers*/
	ret = ringbuf_init(slogk_dev);
	if (ret < 0) {
		pr_err("Init Failed, ret = %d\n", ret);
		goto pingpong_fail;
	}

	ret = alloc_chrdev_region(&devno, 0, seemp_LOGK_NUM_DEVS,
			seemp_LOGK_DEV_NAME);
	if (ret < 0) {
		pr_err("alloc_chrdev_region failed with ret = %d\n",
				ret);
		goto register_fail;
	}

	slogk_dev->major = MAJOR(devno);

	pr_debug("logk: major# = %d\n", slogk_dev->major);

	cl = class_create(THIS_MODULE, seemp_LOGK_DEV_NAME);
	if (cl == NULL) {
		pr_err("class create failed");
		goto cdev_fail;
	}
	if (device_create(cl, NULL, devno, NULL,
			seemp_LOGK_DEV_NAME) == NULL) {
		pr_err("device create failed");
		goto class_destroy_fail;
	}
	cdev_init(&(slogk_dev->cdev), &seemp_logk_fops);

	slogk_dev->cdev.owner = THIS_MODULE;
	ret = cdev_add(&(slogk_dev->cdev), MKDEV(slogk_dev->major, 0), 1);
	if (ret) {
		pr_err("cdev_add failed with ret = %d", ret);
		goto class_destroy_fail;
	}

	seemp_logk_attach();
	mutex_init(&slogk_dev->lock);
	init_waitqueue_head(&slogk_dev->readers_wq);
	init_waitqueue_head(&slogk_dev->writers_wq);
	rwlock_init(&filter_lock);
	return 0;
class_destroy_fail:
	class_destroy(cl);
cdev_fail:
	unregister_chrdev_region(devno, seemp_LOGK_NUM_DEVS);
register_fail:
	ringbuf_cleanup(slogk_dev);
pingpong_fail:
	kfree(slogk_dev);
	return -EPERM;
}

__exit void seemp_logk_cleanup(void)
{
	dev_t devno = MKDEV(slogk_dev->major, slogk_dev->minor);

	seemp_logk_detach();

	cdev_del(&slogk_dev->cdev);

	unregister_chrdev_region(devno, seemp_LOGK_NUM_DEVS);
	ringbuf_cleanup(slogk_dev);
	kfree(slogk_dev);

	if (NULL != pmask) {
		kfree(pmask);
		pmask = NULL;
	}
}

module_init(seemp_logk_init);
module_exit(seemp_logk_cleanup);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("seemp Observer");

