/* Copyright (c) 2011-2012, 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/sched.h>
#include <linux/slab.h>
#include <linux/mutex.h>
#include <linux/miscdevice.h>
#include <linux/poll.h>
#include <linux/uaccess.h>
#include <linux/idle_stats_device.h>
#include <linux/module.h>

DEFINE_MUTEX(device_list_lock);
LIST_HEAD(device_list);

static ktime_t us_to_ktime(__u32 us)
{
	return ns_to_ktime((u64)us * NSEC_PER_USEC);
}

static struct msm_idle_stats_device *_device_from_minor(unsigned int minor)
{
	struct msm_idle_stats_device *device, *ret = NULL;


	mutex_lock(&device_list_lock);
	list_for_each_entry(device, &device_list, list) {
		if (minor == device->miscdev.minor) {
			ret = device;
			break;
		}
	}
	mutex_unlock(&device_list_lock);
	return ret;
}

void msm_idle_stats_update_event(struct msm_idle_stats_device *device,
	__u32 event)
{
	__u32 wake_up = !device->stats->event;

	device->stats->event |= event;
	if (wake_up)
		wake_up_interruptible(&device->wait);
}
EXPORT_SYMBOL(msm_idle_stats_update_event);

static enum hrtimer_restart msm_idle_stats_busy_timer(struct hrtimer *timer)
{
	struct msm_idle_stats_device *device =
		container_of(timer, struct msm_idle_stats_device, busy_timer);


	/* This is the only case that the event is modified without a device
	 * lock. However, since the timer is cancelled in the other cases we are
	 * assured that we have exclusive access to the event at this time.
	 */
	hrtimer_set_expires(&device->busy_timer, us_to_ktime(0));
	msm_idle_stats_update_event(device,
		MSM_IDLE_STATS_EVENT_BUSY_TIMER_EXPIRED);
	return HRTIMER_NORESTART;
}

static void start_busy_timer(struct msm_idle_stats_device *device,
						     ktime_t relative_time)
{
	hrtimer_cancel(&device->busy_timer);
	hrtimer_set_expires(&device->busy_timer, us_to_ktime(0));
	if (!((device->stats->event &
		   MSM_IDLE_STATS_EVENT_BUSY_TIMER_EXPIRED) ||
	      (device->stats->event & MSM_IDLE_STATS_EVENT_COLLECTION_FULL))) {
		if (ktime_to_us(relative_time) > 0) {
			hrtimer_start(&device->busy_timer,
						  relative_time,
						  HRTIMER_MODE_REL);
		}
	}
}

static unsigned int msm_idle_stats_device_poll(struct file *file,
						poll_table *wait)
{
	struct msm_idle_stats_device *device = file->private_data;
	unsigned int mask = 0;

	poll_wait(file, &device->wait, wait);
	if (device->stats->event)
		mask = POLLIN | POLLRDNORM;
	return mask;
}

static void msm_idle_stats_add_sample(struct msm_idle_stats_device *device,
		struct msm_idle_pulse *pulse)
{
	hrtimer_cancel(&device->busy_timer);
	hrtimer_set_expires(&device->busy_timer, us_to_ktime(0));
	if (device->stats->nr_collected >= MSM_IDLE_STATS_NR_MAX_INTERVALS) {
		pr_warning("idle_stats_device: Overwriting samples\n");
		device->stats->nr_collected = 0;
	}
	device->stats->pulse_chain[device->stats->nr_collected] = *pulse;
	device->stats->nr_collected++;

	if (device->stats->nr_collected == device->max_samples) {
		msm_idle_stats_update_event(device,
			MSM_IDLE_STATS_EVENT_COLLECTION_FULL);
	} else if (device->stats->nr_collected ==
				((device->max_samples * 3) / 4)) {
		msm_idle_stats_update_event(device,
			MSM_IDLE_STATS_EVENT_COLLECTION_NEARLY_FULL);
	}
}

static long ioctl_read_stats(struct msm_idle_stats_device *device,
		unsigned long arg)
{
	int remaining;
	int requested;
	struct msm_idle_pulse pulse;
	struct msm_idle_read_stats *stats;
	__s64 remaining_time =
		ktime_to_us(hrtimer_get_remaining(&device->busy_timer));

	device->get_sample(device, &pulse);
	spin_lock(&device->lock);
	hrtimer_cancel(&device->busy_timer);
	stats = device->stats;
	if (stats == &device->stats_vector[0])
		device->stats = &device->stats_vector[1];
	else
		device->stats = &device->stats_vector[0];
	device->stats->event = 0;
	device->stats->nr_collected = 0;
	spin_unlock(&device->lock);
	if (stats->nr_collected >= device->max_samples) {
		stats->nr_collected = device->max_samples;
	} else {
	    stats->pulse_chain[stats->nr_collected] = pulse;
	    stats->nr_collected++;
	    if (stats->nr_collected == device->max_samples)
			stats->event |= MSM_IDLE_STATS_EVENT_COLLECTION_FULL;
	    else if (stats->nr_collected ==
				 ((device->max_samples * 3) / 4))
			stats->event |=
				MSM_IDLE_STATS_EVENT_COLLECTION_NEARLY_FULL;
	}
	if (remaining_time < 0) {
		stats->busy_timer_remaining = 0;
	} else {
	    stats->busy_timer_remaining = remaining_time;
		if ((__s64)stats->busy_timer_remaining != remaining_time)
			stats->busy_timer_remaining = -1;
	}
	stats->return_timestamp = ktime_to_us(ktime_get());
	requested =
		((sizeof(*stats) - sizeof(stats->pulse_chain)) +
		 (sizeof(stats->pulse_chain[0]) * stats->nr_collected));
	remaining = copy_to_user((void __user *)arg, stats, requested);
	if (remaining > 0)
		return -EFAULT;

	return 0;
}

static long ioctl_write_stats(struct msm_idle_stats_device *device,
		unsigned long arg)
{
	struct msm_idle_write_stats stats;
	int remaining;
	int ret = 0;

	remaining = copy_from_user(&stats,  (void __user *) arg, sizeof(stats));
	if (remaining > 0) {
		ret = -EFAULT;
	} else {
	    spin_lock(&device->lock);
	    device->busy_timer_interval = us_to_ktime(stats.next_busy_timer);
	    if (ktime_to_us(device->idle_start) == 0)
			start_busy_timer(device, us_to_ktime(stats.busy_timer));
		if ((stats.max_samples > 0) &&
			(stats.max_samples <= MSM_IDLE_STATS_NR_MAX_INTERVALS))
			device->max_samples = stats.max_samples;
	    spin_unlock(&device->lock);
	}
	return ret;
}

void msm_idle_stats_prepare_idle_start(struct msm_idle_stats_device *device)
{
	spin_lock(&device->lock);
	hrtimer_cancel(&device->busy_timer);
	spin_unlock(&device->lock);
}
EXPORT_SYMBOL(msm_idle_stats_prepare_idle_start);

void msm_idle_stats_abort_idle_start(struct msm_idle_stats_device *device)
{
	spin_lock(&device->lock);
	if (ktime_to_us(hrtimer_get_expires(&device->busy_timer)) > 0)
		hrtimer_restart(&device->busy_timer);
	spin_unlock(&device->lock);
}
EXPORT_SYMBOL(msm_idle_stats_abort_idle_start);

void msm_idle_stats_idle_start(struct msm_idle_stats_device *device)
{
	spin_lock(&device->lock);
	hrtimer_cancel(&device->busy_timer);
	device->idle_start = ktime_get();
	if (ktime_to_us(hrtimer_get_expires(&device->busy_timer)) > 0) {
		device->remaining_time =
				hrtimer_get_remaining(&device->busy_timer);
		if (ktime_to_us(device->remaining_time) <= 0)
			device->remaining_time = us_to_ktime(0);
	} else {
		device->remaining_time = us_to_ktime(0);
	}
	spin_unlock(&device->lock);
}
EXPORT_SYMBOL(msm_idle_stats_idle_start);

void msm_idle_stats_idle_end(struct msm_idle_stats_device *device,
				struct msm_idle_pulse *pulse)
{
	int tmp;
	u32 idle_time = 0;
	spin_lock(&device->lock);
	if (ktime_to_us(device->idle_start) != 0) {
		idle_time = ktime_to_us(ktime_get())
			- ktime_to_us(device->idle_start);
		device->idle_start = us_to_ktime(0);
	    msm_idle_stats_add_sample(device, pulse);
		if (device->stats->event &
			MSM_IDLE_STATS_EVENT_BUSY_TIMER_EXPIRED) {
			device->stats->event &=
				~MSM_IDLE_STATS_EVENT_BUSY_TIMER_EXPIRED;
			msm_idle_stats_update_event(device,
				MSM_IDLE_STATS_EVENT_BUSY_TIMER_EXPIRED_RESET);
		} else if (ktime_to_us(device->busy_timer_interval) > 0) {
			ktime_t busy_timer = device->busy_timer_interval;
			/* if it is serialized, it would be full busy,
			 * checking 80%
			 */
			if ((pulse->wait_interval*5 >= idle_time*4) &&
				(ktime_to_us(device->remaining_time) > 0) &&
				(ktime_to_us(device->remaining_time) <
				 ktime_to_us(busy_timer)))
				busy_timer = device->remaining_time;
		    start_busy_timer(device, busy_timer);
		    /* If previous busy interval exceeds the current submit,
		     * raise a busy timer expired event intentionally.
		     */
		    tmp = device->stats->nr_collected - 1;
		    if (tmp > 0) {
			if ((device->stats->pulse_chain[tmp - 1].busy_start_time
			+ device->stats->pulse_chain[tmp - 1].busy_interval) >
			  device->stats->pulse_chain[tmp].busy_start_time)
				msm_idle_stats_update_event(device,
				   MSM_IDLE_STATS_EVENT_BUSY_TIMER_EXPIRED);
		    }
		}
	}
	spin_unlock(&device->lock);
}
EXPORT_SYMBOL(msm_idle_stats_idle_end);

static long msm_idle_stats_device_ioctl(struct file *file, unsigned int cmd,
		unsigned long arg)
{
	struct msm_idle_stats_device *device = file->private_data;
	int ret;

	switch (cmd) {
	case MSM_IDLE_STATS_IOC_READ_STATS:
		ret = ioctl_read_stats(device, arg);
		break;
	case MSM_IDLE_STATS_IOC_WRITE_STATS:
		ret = ioctl_write_stats(device, arg);
		break;
	default:
		ret = -EINVAL;
	}

	return ret;
}

static int msm_idle_stats_device_release
			  (struct inode *inode, struct file *filep)
{
	return 0;
}

static int msm_idle_stats_device_open(struct inode *inode, struct file *filep)
{
	struct msm_idle_stats_device *device;


	device = _device_from_minor(iminor(inode));

	if (device == NULL)
		return -EPERM;

	filep->private_data = device;
	return 0;
}

static const struct file_operations msm_idle_stats_fops = {
	.open = msm_idle_stats_device_open,
	.release = msm_idle_stats_device_release,
	.unlocked_ioctl = msm_idle_stats_device_ioctl,
	.poll = msm_idle_stats_device_poll,
};

int msm_idle_stats_register_device(struct msm_idle_stats_device *device)
{
	int ret = -ENOMEM;

	spin_lock_init(&device->lock);
	init_waitqueue_head(&device->wait);
	hrtimer_init(&device->busy_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
	device->busy_timer.function = msm_idle_stats_busy_timer;

	device->stats_vector[0].event         = 0;
	device->stats_vector[0].nr_collected  = 0;
	device->stats_vector[1].event         = 0;
	device->stats_vector[1].nr_collected  = 0;
	device->stats = &device->stats_vector[0];
	device->busy_timer_interval = us_to_ktime(0);
	device->max_samples = MSM_IDLE_STATS_NR_MAX_INTERVALS;

	mutex_lock(&device_list_lock);
	list_add(&device->list, &device_list);
	mutex_unlock(&device_list_lock);

	device->miscdev.minor = MISC_DYNAMIC_MINOR;
	device->miscdev.name = device->name;
	device->miscdev.fops = &msm_idle_stats_fops;

	ret = misc_register(&device->miscdev);

	if (ret)
		goto err_list;

	return ret;

err_list:
	mutex_lock(&device_list_lock);
	list_del(&device->list);
	mutex_unlock(&device_list_lock);
	return ret;
}
EXPORT_SYMBOL(msm_idle_stats_register_device);

int msm_idle_stats_deregister_device(struct msm_idle_stats_device *device)
{
	if (device == NULL)
		return 0;

	mutex_lock(&device_list_lock);
	spin_lock(&device->lock);
	hrtimer_cancel(&device->busy_timer);
	list_del(&device->list);
	spin_unlock(&device->lock);
	mutex_unlock(&device_list_lock);

	return misc_deregister(&device->miscdev);
}
EXPORT_SYMBOL(msm_idle_stats_deregister_device);
