/* Qualcomm TrustZone communicator driver
 *
 * Copyright (c) 2011, Code Aurora Forum. 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 KMSG_COMPONENT "TZCOM"
#define pr_fmt(fmt) KMSG_COMPONENT ": " fmt

#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/debugfs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/sched.h>
#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/android_pmem.h>
#include <linux/io.h>
#include <mach/scm.h>
#include <mach/peripheral-loader.h>
#include <linux/tzcom.h>
#include "tzcomi.h"

#define TZCOM_DEV "tzcom"

#define TZSCHEDULER_CMD_ID 1 /* CMD id of the trustzone scheduler */

#undef PDEBUG
#define PDEBUG(fmt, args...) pr_debug("%s(%i, %s): " fmt "\n", \
		__func__, current->pid, current->comm, ## args)

#undef PERR
#define PERR(fmt, args...) pr_err("%s(%i, %s): " fmt "\n", \
		__func__, current->pid, current->comm, ## args)

#undef PWARN
#define PWARN(fmt, args...) pr_warning("%s(%i, %s): " fmt "\n", \
		__func__, current->pid, current->comm, ## args)


static struct class *driver_class;
static dev_t tzcom_device_no;
static struct cdev tzcom_cdev;

static u8 *sb_in_virt;
static s32 sb_in_phys;
static size_t sb_in_length = 20 * SZ_1K;
static u8 *sb_out_virt;
static s32 sb_out_phys;
static size_t sb_out_length = 20 * SZ_1K;

static void *pil;

static atomic_t svc_instance_ctr = ATOMIC_INIT(0);
static DEFINE_MUTEX(sb_in_lock);
static DEFINE_MUTEX(sb_out_lock);
static DEFINE_MUTEX(send_cmd_lock);

struct tzcom_callback_list {
	struct list_head      list;
	struct tzcom_callback callback;
};

struct tzcom_registered_svc_list {
	struct list_head                 list;
	struct tzcom_register_svc_op_req svc;
	wait_queue_head_t                next_cmd_wq;
	int                              next_cmd_flag;
};

struct tzcom_data_t {
	struct list_head  callback_list_head;
	struct mutex      callback_list_lock;
	struct list_head  registered_svc_list_head;
	spinlock_t        registered_svc_list_lock;
	wait_queue_head_t cont_cmd_wq;
	int               cont_cmd_flag;
	u32               handled_cmd_svc_instance_id;
	int               abort;
	wait_queue_head_t abort_wq;
	atomic_t          ioctl_count;
};

static int tzcom_scm_call(const void *cmd_buf, size_t cmd_len,
		void *resp_buf, size_t resp_len)
{
	return scm_call(SCM_SVC_TZSCHEDULER, TZSCHEDULER_CMD_ID,
			cmd_buf, cmd_len, resp_buf, resp_len);
}

static s32 tzcom_virt_to_phys(u8 *virt)
{
	if (virt >= sb_in_virt &&
			virt < (sb_in_virt + sb_in_length)) {
		return sb_in_phys + (virt - sb_in_virt);
	} else if (virt >= sb_out_virt &&
			virt < (sb_out_virt + sb_out_length)) {
		return sb_out_phys + (virt - sb_out_virt);
	} else {
		return virt_to_phys(virt);
	}
}

static u8 *tzcom_phys_to_virt(s32 phys)
{
	if (phys >= sb_in_phys &&
			phys < (sb_in_phys + sb_in_length)) {
		return sb_in_virt + (phys - sb_in_phys);
	} else if (phys >= sb_out_phys &&
			phys < (sb_out_phys + sb_out_length)) {
		return sb_out_virt + (phys - sb_out_phys);
	} else {
		return phys_to_virt(phys);
	}
}

static int __tzcom_is_svc_unique(struct tzcom_data_t *data,
		struct tzcom_register_svc_op_req svc)
{
	struct tzcom_registered_svc_list *ptr;
	int unique = 1;
	unsigned long flags;

	spin_lock_irqsave(&data->registered_svc_list_lock, flags);
	list_for_each_entry(ptr, &data->registered_svc_list_head, list) {
		if (ptr->svc.svc_id == svc.svc_id) {
			PERR("Service id: %u is already registered",
					ptr->svc.svc_id);
			unique = 0;
			break;
		} else if (svc.cmd_id_low >= ptr->svc.cmd_id_low &&
				svc.cmd_id_low <= ptr->svc.cmd_id_high) {
			PERR("Cmd id low falls in the range of another"
					"registered service");
			unique = 0;
			break;
		} else if (svc.cmd_id_high >= ptr->svc.cmd_id_low &&
				svc.cmd_id_high <= ptr->svc.cmd_id_high) {
			PERR("Cmd id high falls in the range of another"
					"registered service");
			unique = 0;
			break;
		}
	}
	spin_unlock_irqrestore(&data->registered_svc_list_lock, flags);
	return unique;
}

static int tzcom_register_service(struct tzcom_data_t *data, void __user *argp)
{
	int ret;
	unsigned long flags;
	struct tzcom_register_svc_op_req rcvd_svc;
	struct tzcom_registered_svc_list *new_entry;

	ret = copy_from_user(&rcvd_svc, argp, sizeof(rcvd_svc));

	if (ret) {
		PERR("copy_from_user failed");
		return ret;
	}

	PDEBUG("svc_id: %u, cmd_id_low: %u, cmd_id_high: %u",
			rcvd_svc.svc_id, rcvd_svc.cmd_id_low,
			rcvd_svc.cmd_id_high);
	if (!__tzcom_is_svc_unique(data, rcvd_svc)) {
		PERR("Provided service is not unique");
		return -EINVAL;
	}

	rcvd_svc.instance_id = atomic_inc_return(&svc_instance_ctr);

	ret = copy_to_user(argp, &rcvd_svc, sizeof(rcvd_svc));
	if (ret) {
		PERR("copy_to_user failed");
		return ret;
	}

	new_entry = kmalloc(sizeof(*new_entry), GFP_KERNEL);
	if (!new_entry) {
		PERR("kmalloc failed");
		return -ENOMEM;
	}
	memcpy(&new_entry->svc, &rcvd_svc, sizeof(rcvd_svc));
	new_entry->next_cmd_flag = 0;
	init_waitqueue_head(&new_entry->next_cmd_wq);

	spin_lock_irqsave(&data->registered_svc_list_lock, flags);
	list_add_tail(&new_entry->list, &data->registered_svc_list_head);
	spin_unlock_irqrestore(&data->registered_svc_list_lock, flags);


	return ret;
}

static int tzcom_unregister_service(struct tzcom_data_t *data,
		void __user *argp)
{
	int ret = 0;
	unsigned long flags;
	struct tzcom_unregister_svc_op_req req;
	struct tzcom_registered_svc_list *ptr, *next;
	ret = copy_from_user(&req, argp, sizeof(req));
	if (ret) {
		PERR("copy_from_user failed");
		return ret;
	}

	spin_lock_irqsave(&data->registered_svc_list_lock, flags);
	list_for_each_entry_safe(ptr, next, &data->registered_svc_list_head,
			list) {
		if (req.svc_id == ptr->svc.svc_id &&
				req.instance_id == ptr->svc.instance_id) {
			wake_up_all(&ptr->next_cmd_wq);
			list_del(&ptr->list);
			kfree(ptr);
			spin_unlock_irqrestore(&data->registered_svc_list_lock,
					flags);
			return 0;
		}
	}
	spin_unlock_irqrestore(&data->registered_svc_list_lock, flags);

	return -EINVAL;
}

static int __tzcom_is_cont_cmd(struct tzcom_data_t *data)
{
	int ret;
	ret = (data->cont_cmd_flag != 0);
	return ret || data->abort;
}

/**
 *   +---------+                              +-----+       +-----------------+
 *   |  TZCOM  |                              | SCM |       | TZCOM_SCHEDULER |
 *   +----+----+                              +--+--+       +--------+--------+
 *        |                                      |                   |
 *        |        scm_call                      |                   |
 *        |------------------------------------->|                   |
 *        |  cmd_buf = struct tzcom_command {    |                   |
 *        |              cmd_type,               |------------------>|
 * +------+------------- sb_in_cmd_addr,         |                   |
 * |      |              sb_in_cmd_len           |                   |
 * |      |            }                         |                   |
 * |      |   resp_buf = struct tzcom_response { |                   |
 * |                         cmd_status,         |                   |
 * |             +---------- sb_in_rsp_addr,     |                   |
 * |             |           sb_in_rsp_len       |<------------------|
 * |             |         }
 * |             |                            struct tzcom_callback {---------+
 * |             |                                uint32_t cmd_id;            |
 * |             |                                uint32_t sb_out_cb_data_len;|
 * |             +---------------+                uint32_t sb_out_cb_data_off;|
 * |                             |            }                               |
 * |    _________________________|_______________________________             |
 * |    +-----------------------+| +----------------------+                   |
 * +--->+ copy from req.cmd_buf |+>| copy to req.resp_buf |                   |
 *      +-----------------------+  +----------------------+                   |
 *      _________________________________________________________             |
 *                               INPUT SHARED BUFFER                          |
 *   +------------------------------------------------------------------------+
 *   |  _________________________________________________________
 *   |  +---------------------------------------------+
 *   +->| cmd_id | data_len | data_off |   data...    |
 *      +---------------------------------------------+
 *                                     |<------------>|copy to next_cmd.req_buf
 *      _________________________________________________________
 *                              OUTPUT SHARED BUFFER
 */
static int tzcom_send_cmd(struct tzcom_data_t *data, void __user *argp)
{
	int ret = 0;
	unsigned long flags;
	u32 reqd_len_sb_in = 0;
	u32 reqd_len_sb_out = 0;
	struct tzcom_send_cmd_op_req req;
	struct tzcom_command cmd;
	struct tzcom_response resp;
	struct tzcom_callback *next_callback;
	void *cb_data = NULL;
	struct tzcom_callback_list *new_entry;
	struct tzcom_callback *cb;
	size_t new_entry_len = 0;
	struct tzcom_registered_svc_list *ptr_svc;

	ret = copy_from_user(&req, argp, sizeof(req));
	if (ret) {
		PERR("copy_from_user failed");
		return ret;
	}

	if (req.cmd_buf == NULL || req.resp_buf == NULL) {
		PERR("cmd buffer or response buffer is null");
		return -EINVAL;
	}

	if (req.cmd_len <= 0 || req.resp_len <= 0 ||
		req.cmd_len > sb_in_length || req.resp_len > sb_in_length) {
		PERR("cmd buffer length or "
				"response buffer length not valid");
		return -EINVAL;
	}
	PDEBUG("received cmd_req.req: 0x%p",
				req.cmd_buf);
	PDEBUG("received cmd_req.rsp size: %u, ptr: 0x%p",
			req.resp_len,
			req.resp_buf);

	reqd_len_sb_in = req.cmd_len + req.resp_len;
	if (reqd_len_sb_in > sb_in_length) {
		PDEBUG("Not enough memory to fit cmd_buf and "
				"resp_buf. Required: %u, Available: %u",
				reqd_len_sb_in, sb_in_length);
		return -ENOMEM;
	}

	/* Copy req.cmd_buf to SB in and set req.resp_buf to SB in + cmd_len */
	mutex_lock(&sb_in_lock);
	PDEBUG("Before memcpy on sb_in");
	memcpy(sb_in_virt, req.cmd_buf, req.cmd_len);
	PDEBUG("After memcpy on sb_in");

	/* cmd_type will always be a new here */
	cmd.cmd_type = TZ_SCHED_CMD_NEW;
	cmd.sb_in_cmd_addr = (u8 *) tzcom_virt_to_phys(sb_in_virt);
	cmd.sb_in_cmd_len = req.cmd_len;

	resp.cmd_status = TZ_SCHED_STATUS_INCOMPLETE;
	resp.sb_in_rsp_addr = (u8 *) tzcom_virt_to_phys(sb_in_virt +
			req.cmd_len);
	resp.sb_in_rsp_len = req.resp_len;

	PDEBUG("before call tzcom_scm_call, cmd_id = : %u", req.cmd_id);
	PDEBUG("before call tzcom_scm_call, sizeof(cmd) = : %u", sizeof(cmd));

	ret = tzcom_scm_call((const void *) &cmd, sizeof(cmd),
			&resp, sizeof(resp));
	mutex_unlock(&sb_in_lock);

	if (ret) {
		PERR("tzcom_scm_call failed with err: %d", ret);
		return ret;
	}

	while (resp.cmd_status != TZ_SCHED_STATUS_COMPLETE) {
		/*
		 * If cmd is incomplete, get the callback cmd out from SB out
		 * and put it on the list
		 */
		PDEBUG("cmd_status is incomplete.");
		next_callback = (struct tzcom_callback *)sb_out_virt;

		mutex_lock(&sb_out_lock);
		reqd_len_sb_out = sizeof(*next_callback)
					+ next_callback->sb_out_cb_data_len;
		if (reqd_len_sb_out > sb_out_length ||
			reqd_len_sb_out < sizeof(*next_callback) ||
			next_callback->sb_out_cb_data_len > sb_out_length) {
			PERR("Incorrect callback data length"
					" Required: %u, Available: %u, Min: %u",
					reqd_len_sb_out, sb_out_length,
					sizeof(*next_callback));
			mutex_unlock(&sb_out_lock);
			return -ENOMEM;
		}

		/* Assumption is cb_data_off is sizeof(tzcom_callback) */
		new_entry_len = sizeof(*new_entry)
					+ next_callback->sb_out_cb_data_len;
		new_entry = kmalloc(new_entry_len, GFP_KERNEL);
		if (!new_entry) {
			PERR("kmalloc failed");
			mutex_unlock(&sb_out_lock);
			return -ENOMEM;
		}

		cb = &new_entry->callback;
		cb->cmd_id = next_callback->cmd_id;
		cb->sb_out_cb_data_len = next_callback->sb_out_cb_data_len;
		cb->sb_out_cb_data_off = sizeof(*cb);

		cb_data = (u8 *)next_callback
				+ next_callback->sb_out_cb_data_off;
		memcpy((u8 *)cb + cb->sb_out_cb_data_off, cb_data,
				next_callback->sb_out_cb_data_len);
		mutex_unlock(&sb_out_lock);

		mutex_lock(&data->callback_list_lock);
		list_add_tail(&new_entry->list, &data->callback_list_head);
		mutex_unlock(&data->callback_list_lock);

		/*
		 * We don't know which service can handle the command. so we
		 * wake up all blocking services and let them figure out if
		 * they can handle the given command.
		 */
		spin_lock_irqsave(&data->registered_svc_list_lock, flags);
		list_for_each_entry(ptr_svc,
				&data->registered_svc_list_head, list) {
			ptr_svc->next_cmd_flag = 1;
			wake_up_interruptible(&ptr_svc->next_cmd_wq);
		}
		spin_unlock_irqrestore(&data->registered_svc_list_lock,
				flags);

		PDEBUG("waking up next_cmd_wq and "
				"waiting for cont_cmd_wq");
		if (wait_event_interruptible(data->cont_cmd_wq,
				__tzcom_is_cont_cmd(data))) {
			PWARN("Interrupted: exiting send_cmd loop");
			return -ERESTARTSYS;
		}

		if (data->abort) {
			PERR("Aborting driver");
			return -ENODEV;
		}
		data->cont_cmd_flag = 0;
		cmd.cmd_type = TZ_SCHED_CMD_PENDING;
		mutex_lock(&sb_in_lock);
		ret = tzcom_scm_call((const void *) &cmd, sizeof(cmd), &resp,
				sizeof(resp));
		mutex_unlock(&sb_in_lock);
		if (ret) {
			PERR("tzcom_scm_call failed with err: %d", ret);
			return ret;
		}
	}

	mutex_lock(&sb_in_lock);
	resp.sb_in_rsp_addr = sb_in_virt + cmd.sb_in_cmd_len;
	resp.sb_in_rsp_len = req.resp_len;
	memcpy(req.resp_buf, resp.sb_in_rsp_addr, resp.sb_in_rsp_len);
	/* Zero out memory for security purpose */
	memset(sb_in_virt, 0, reqd_len_sb_in);
	mutex_unlock(&sb_in_lock);

	PDEBUG("sending cmd_req.rsp "
			"size: %u, ptr: 0x%p", req.resp_len,
			req.resp_buf);
	ret = copy_to_user(argp, &req, sizeof(req));
	if (ret) {
		PDEBUG("copy_to_user failed");
		return ret;
	}

	return ret;
}

static struct tzcom_registered_svc_list *__tzcom_find_svc(
		struct tzcom_data_t *data,
		uint32_t instance_id)
{
	struct tzcom_registered_svc_list *entry;
	unsigned long flags;

	spin_lock_irqsave(&data->registered_svc_list_lock, flags);
	list_for_each_entry(entry,
			&data->registered_svc_list_head, list) {
		if (entry->svc.instance_id == instance_id)
			break;
	}
	spin_unlock_irqrestore(&data->registered_svc_list_lock, flags);

	return entry;
}

static int __tzcom_copy_cmd(struct tzcom_data_t *data,
		struct tzcom_next_cmd_op_req *req,
		struct tzcom_registered_svc_list *ptr_svc)
{
	int found = 0;
	int ret = -EAGAIN;
	struct tzcom_callback_list *entry, *next;
	struct tzcom_callback *cb;

	PDEBUG("In here");
	mutex_lock(&data->callback_list_lock);
	PDEBUG("Before looping through cmd and svc lists.");
	list_for_each_entry_safe(entry, next, &data->callback_list_head, list) {
		cb = &entry->callback;
		if (req->svc_id == ptr_svc->svc.svc_id &&
			req->instance_id == ptr_svc->svc.instance_id &&
			cb->cmd_id >= ptr_svc->svc.cmd_id_low &&
			cb->cmd_id <= ptr_svc->svc.cmd_id_high) {
			PDEBUG("Found matching entry");
			found = 1;
			if (cb->sb_out_cb_data_len <= req->req_len) {
				PDEBUG("copying cmd buffer %p to req "
					"buffer %p, length: %u",
					(u8 *)cb + cb->sb_out_cb_data_off,
					req->req_buf, cb->sb_out_cb_data_len);
				req->cmd_id = cb->cmd_id;
				ret = copy_to_user(req->req_buf,
					(u8 *)cb + cb->sb_out_cb_data_off,
					cb->sb_out_cb_data_len);
				if (ret) {
					PERR("copy_to_user failed");
					break;
				}
				list_del(&entry->list);
				kfree(entry);
				ret = 0;
			} else {
				PERR("callback data buffer is "
					"larger than provided buffer."
					"Required: %u, Provided: %u",
					cb->sb_out_cb_data_len,
					req->req_len);
				ret = -ENOMEM;
			}
			break;
		}
	}
	PDEBUG("After looping through cmd and svc lists.");
	mutex_unlock(&data->callback_list_lock);
	return ret;
}

static int __tzcom_is_next_cmd(struct tzcom_data_t *data,
		struct tzcom_registered_svc_list *svc)
{
	int ret;
	ret = (svc->next_cmd_flag != 0);
	return ret || data->abort;
}

static int tzcom_read_next_cmd(struct tzcom_data_t *data, void __user *argp)
{
	int ret = 0;
	struct tzcom_next_cmd_op_req req;
	struct tzcom_registered_svc_list *this_svc;

	ret = copy_from_user(&req, argp, sizeof(req));
	if (ret) {
		PERR("copy_from_user failed");
		return ret;
	}

	if (req.instance_id > atomic_read(&svc_instance_ctr)) {
		PERR("Invalid instance_id for the request");
		return -EINVAL;
	}

	if (!req.req_buf || req.req_len == 0) {
		PERR("Invalid request buffer or buffer length");
		return -EINVAL;
	}

	PDEBUG("Before next_cmd loop");
	this_svc = __tzcom_find_svc(data, req.instance_id);

	while (1) {
		PDEBUG("Before wait_event next_cmd.");
		if (wait_event_interruptible(this_svc->next_cmd_wq,
				__tzcom_is_next_cmd(data, this_svc))) {
			PWARN("Interrupted: exiting wait_next_cmd loop");
			/* woken up for different reason */
			return -ERESTARTSYS;
		}

		if (data->abort) {
			PERR("Aborting driver");
			return -ENODEV;
		}
		PDEBUG("After wait_event next_cmd.");
		this_svc->next_cmd_flag = 0;

		ret = __tzcom_copy_cmd(data, &req, this_svc);
		if (ret == 0) {
			PDEBUG("Successfully found svc for cmd");
			data->handled_cmd_svc_instance_id = req.instance_id;
			break;
		} else if (ret == -ENOMEM) {
			PERR("Not enough memory");
			return ret;
		}
	}
	ret = copy_to_user(argp, &req, sizeof(req));
	if (ret) {
		PERR("copy_to_user failed");
		return ret;
	}
	PDEBUG("copy_to_user is done.");
	return ret;
}

static int tzcom_cont_cmd(struct tzcom_data_t *data, void __user *argp)
{
	int ret = 0;
	struct tzcom_cont_cmd_op_req req;
	ret = copy_from_user(&req, argp, sizeof(req));
	if (ret) {
		PERR("copy_from_user failed");
		return ret;
	}

	/*
	 * Only the svc instance that handled the cmd (in read_next_cmd method)
	 * can call continue cmd
	 */
	if (data->handled_cmd_svc_instance_id != req.instance_id) {
		PWARN("Only the service instance that handled the last "
				"callback can continue cmd. "
				"Expected: %u, Received: %u",
				data->handled_cmd_svc_instance_id,
				req.instance_id);
		return -EINVAL;
	}

	if (req.resp_buf) {
		mutex_lock(&sb_out_lock);
		memcpy(sb_out_virt, req.resp_buf, req.resp_len);
		mutex_unlock(&sb_out_lock);
	}

	data->cont_cmd_flag = 1;
	wake_up_interruptible(&data->cont_cmd_wq);
	return ret;
}

static int tzcom_abort(struct tzcom_data_t *data)
{
	int ret = 0;
	unsigned long flags;
	struct tzcom_registered_svc_list *lsvc, *nsvc;
	if (data->abort) {
		PERR("Already aborting");
		return -EINVAL;
	}

	data->abort = 1;

	PDEBUG("Waking up cont_cmd_wq");
	wake_up_all(&data->cont_cmd_wq);

	spin_lock_irqsave(&data->registered_svc_list_lock, flags);
	PDEBUG("Before waking up service wait queues");
	list_for_each_entry_safe(lsvc, nsvc,
			&data->registered_svc_list_head, list) {
		wake_up_all(&lsvc->next_cmd_wq);
	}
	spin_unlock_irqrestore(&data->registered_svc_list_lock, flags);

	PDEBUG("ioctl_count before loop: %d", atomic_read(&data->ioctl_count));
	while (atomic_read(&data->ioctl_count) > 0) {
		if (wait_event_interruptible(data->abort_wq,
				atomic_read(&data->ioctl_count) <= 0)) {
			PERR("Interrupted from abort");
			ret = -ERESTARTSYS;
			break;
		}
	}
	return ret;
}

static long tzcom_ioctl(struct file *file, unsigned cmd,
		unsigned long arg)
{
	int ret = 0;
	struct tzcom_data_t *tzcom_data = file->private_data;
	void __user *argp = (void __user *) arg;
	PDEBUG("enter tzcom_ioctl()");
	if (tzcom_data->abort) {
		PERR("Aborting tzcom driver");
		return -ENODEV;
	}

	switch (cmd) {
	case TZCOM_IOCTL_REGISTER_SERVICE_REQ: {
		PDEBUG("ioctl register_service_req()");
		atomic_inc(&tzcom_data->ioctl_count);
		ret = tzcom_register_service(tzcom_data, argp);
		atomic_dec(&tzcom_data->ioctl_count);
		wake_up_interruptible(&tzcom_data->abort_wq);
		if (ret)
			PERR("failed tzcom_register_service: %d", ret);
		break;
	}
	case TZCOM_IOCTL_UNREGISTER_SERVICE_REQ: {
		PDEBUG("ioctl unregister_service_req()");
		atomic_inc(&tzcom_data->ioctl_count);
		ret = tzcom_unregister_service(tzcom_data, argp);
		atomic_dec(&tzcom_data->ioctl_count);
		wake_up_interruptible(&tzcom_data->abort_wq);
		if (ret)
			PERR("failed tzcom_unregister_service: %d", ret);
		break;
	}
	case TZCOM_IOCTL_SEND_CMD_REQ: {
		PDEBUG("ioctl send_cmd_req()");
		/* Only one client allowed here at a time */
		mutex_lock(&send_cmd_lock);
		atomic_inc(&tzcom_data->ioctl_count);
		ret = tzcom_send_cmd(tzcom_data, argp);
		atomic_dec(&tzcom_data->ioctl_count);
		wake_up_interruptible(&tzcom_data->abort_wq);
		mutex_unlock(&send_cmd_lock);
		if (ret)
			PERR("failed tzcom_send_cmd: %d", ret);
		break;
	}
	case TZCOM_IOCTL_READ_NEXT_CMD_REQ: {
		PDEBUG("ioctl read_next_cmd_req()");
		atomic_inc(&tzcom_data->ioctl_count);
		ret = tzcom_read_next_cmd(tzcom_data, argp);
		atomic_dec(&tzcom_data->ioctl_count);
		wake_up_interruptible(&tzcom_data->abort_wq);
		if (ret)
			PERR("failed tzcom_read_next: %d", ret);
		break;
	}
	case TZCOM_IOCTL_CONTINUE_CMD_REQ: {
		PDEBUG("ioctl continue_cmd_req()");
		atomic_inc(&tzcom_data->ioctl_count);
		ret = tzcom_cont_cmd(tzcom_data, argp);
		atomic_dec(&tzcom_data->ioctl_count);
		wake_up_interruptible(&tzcom_data->abort_wq);
		if (ret)
			PERR("failed tzcom_cont_cmd: %d", ret);
		break;
	}
	case TZCOM_IOCTL_ABORT_REQ: {
		PDEBUG("ioctl abort_req()");
		ret = tzcom_abort(tzcom_data);
		if (ret)
			PERR("failed tzcom_abort: %d", ret);
		break;
	}
	default:
		return -EINVAL;
	}
	return ret;
}

static int tzcom_open(struct inode *inode, struct file *file)
{
	int ret;
	long pil_error;
	struct tz_pr_init_sb_req_s sb_out_init_req;
	struct tz_pr_init_sb_rsp_s sb_out_init_rsp;
	void *rsp_addr_virt;
	struct tzcom_command cmd;
	struct tzcom_response resp;
	struct tzcom_data_t *tzcom_data;

	PDEBUG("In here");
	if (pil == NULL) {
		pil = pil_get("tzapps");
		if (IS_ERR(pil)) {
			PERR("Playready PIL image load failed");
			pil_error = PTR_ERR(pil);
			pil = NULL;
			return pil_error;
		}
		PDEBUG("tzapps image loaded successfully");
	}

	sb_out_init_req.pr_cmd = TZ_SCHED_CMD_ID_INIT_SB_OUT;
	sb_out_init_req.sb_len = sb_out_length;
	sb_out_init_req.sb_ptr = tzcom_virt_to_phys(sb_out_virt);
	PDEBUG("sb_out_init_req { pr_cmd: %d, sb_len: %u, "
			"sb_ptr (phys): 0x%x }",
			sb_out_init_req.pr_cmd,
			sb_out_init_req.sb_len,
			sb_out_init_req.sb_ptr);

	mutex_lock(&sb_in_lock);
	PDEBUG("Before memcpy on sb_in");
	memcpy(sb_in_virt, &sb_out_init_req, sizeof(sb_out_init_req));
	PDEBUG("After memcpy on sb_in");

	/* It will always be a new cmd from this method */
	cmd.cmd_type = TZ_SCHED_CMD_NEW;
	cmd.sb_in_cmd_addr = (u8 *) tzcom_virt_to_phys(sb_in_virt);
	cmd.sb_in_cmd_len = sizeof(sb_out_init_req);
	PDEBUG("tzcom_command { cmd_type: %u, sb_in_cmd_addr: %p, "
			"sb_in_cmd_len: %u }",
			cmd.cmd_type, cmd.sb_in_cmd_addr, cmd.sb_in_cmd_len);

	resp.cmd_status = TZ_SCHED_STATUS_INCOMPLETE;

	PDEBUG("Before scm_call for sb_init");
	ret = tzcom_scm_call(&cmd, sizeof(cmd), &resp, sizeof(resp));
	if (ret) {
		PERR("tzcom_scm_call failed with err: %d", ret);
		return ret;
	}
	PDEBUG("After scm_call for sb_init");

	PDEBUG("tzcom_response after scm cmd_status: %u", resp.cmd_status);
	if (resp.cmd_status == TZ_SCHED_STATUS_COMPLETE) {
		resp.sb_in_rsp_addr = (u8 *)cmd.sb_in_cmd_addr +
				cmd.sb_in_cmd_len;
		resp.sb_in_rsp_len = sizeof(sb_out_init_rsp);
		PDEBUG("tzcom_response sb_in_rsp_addr: %p, sb_in_rsp_len: %u",
				resp.sb_in_rsp_addr, resp.sb_in_rsp_len);
		rsp_addr_virt = tzcom_phys_to_virt((unsigned long)
				resp.sb_in_rsp_addr);
		PDEBUG("Received response phys: %p, virt: %p",
				resp.sb_in_rsp_addr, rsp_addr_virt);
		memcpy(&sb_out_init_rsp, rsp_addr_virt, resp.sb_in_rsp_len);
	} else {
		PERR("Error with SB initialization");
		mutex_unlock(&sb_in_lock);
		return -EPERM;
	}
	mutex_unlock(&sb_in_lock);

	PDEBUG("sb_out_init_rsp { pr_cmd: %d, ret: %d }",
			sb_out_init_rsp.pr_cmd, sb_out_init_rsp.ret);

	if (sb_out_init_rsp.ret) {
		PERR("sb_out_init_req failed: %d", sb_out_init_rsp.ret);
		return -EPERM;
	}

	tzcom_data = kmalloc(sizeof(*tzcom_data), GFP_KERNEL);
	if (!tzcom_data) {
		PERR("kmalloc failed");
		return -ENOMEM;
	}
	file->private_data = tzcom_data;

	INIT_LIST_HEAD(&tzcom_data->callback_list_head);
	mutex_init(&tzcom_data->callback_list_lock);

	INIT_LIST_HEAD(&tzcom_data->registered_svc_list_head);
	spin_lock_init(&tzcom_data->registered_svc_list_lock);

	init_waitqueue_head(&tzcom_data->cont_cmd_wq);
	tzcom_data->cont_cmd_flag = 0;
	tzcom_data->handled_cmd_svc_instance_id = 0;
	tzcom_data->abort = 0;
	init_waitqueue_head(&tzcom_data->abort_wq);
	atomic_set(&tzcom_data->ioctl_count, 0);
	return 0;
}

static int tzcom_release(struct inode *inode, struct file *file)
{
	struct tzcom_data_t *tzcom_data = file->private_data;
	struct tzcom_callback_list *lcb, *ncb;
	struct tzcom_registered_svc_list *lsvc, *nsvc;
	unsigned long flags;
	PDEBUG("In here");

	if (!tzcom_data->abort) {
		PDEBUG("Calling abort");
		tzcom_abort(tzcom_data);
	}

	PDEBUG("Before removing callback list");
	mutex_lock(&tzcom_data->callback_list_lock);
	list_for_each_entry_safe(lcb, ncb,
			&tzcom_data->callback_list_head, list) {
		list_del(&lcb->list);
		kfree(lcb);
	}
	mutex_unlock(&tzcom_data->callback_list_lock);
	PDEBUG("After removing callback list");

	PDEBUG("Before removing svc list");
	spin_lock_irqsave(&tzcom_data->registered_svc_list_lock, flags);
	list_for_each_entry_safe(lsvc, nsvc,
			&tzcom_data->registered_svc_list_head, list) {
		list_del(&lsvc->list);
		kfree(lsvc);
	}
	spin_unlock_irqrestore(&tzcom_data->registered_svc_list_lock, flags);
	PDEBUG("After removing svc list");

	PDEBUG("Freeing tzcom data");
	kfree(tzcom_data);
	return 0;
}

static const struct file_operations tzcom_fops = {
		.owner = THIS_MODULE,
		.unlocked_ioctl = tzcom_ioctl,
		.open = tzcom_open,
		.release = tzcom_release
};

static int __init tzcom_init(void)
{
	int rc;
	struct device *class_dev;

	PDEBUG("Hello tzcom");

	rc = alloc_chrdev_region(&tzcom_device_no, 0, 1, TZCOM_DEV);
	if (rc < 0) {
		PERR("alloc_chrdev_region failed %d", rc);
		return rc;
	}

	driver_class = class_create(THIS_MODULE, TZCOM_DEV);
	if (IS_ERR(driver_class)) {
		rc = -ENOMEM;
		PERR("class_create failed %d", rc);
		goto unregister_chrdev_region;
	}

	class_dev = device_create(driver_class, NULL, tzcom_device_no, NULL,
			TZCOM_DEV);
	if (!class_dev) {
		PERR("class_device_create failed %d", rc);
		rc = -ENOMEM;
		goto class_destroy;
	}

	cdev_init(&tzcom_cdev, &tzcom_fops);
	tzcom_cdev.owner = THIS_MODULE;

	rc = cdev_add(&tzcom_cdev, MKDEV(MAJOR(tzcom_device_no), 0), 1);
	if (rc < 0) {
		PERR("cdev_add failed %d", rc);
		goto class_device_destroy;
	}

	sb_in_phys = pmem_kalloc(sb_in_length, PMEM_MEMTYPE_EBI1 |
			PMEM_ALIGNMENT_4K);
	if (IS_ERR((void *)sb_in_phys)) {
		PERR("could not allocte in kernel pmem buffers for sb_in");
		sb_in_phys = 0;
		rc = -ENOMEM;
		goto class_device_destroy;
	}
	PDEBUG("physical_addr for sb_in: 0x%x", sb_in_phys);

	sb_in_virt = (u8 *) ioremap((unsigned long)sb_in_phys,
			sb_in_length);
	if (!sb_in_virt) {
		PERR("Shared buffer IN allocation failed.");
		rc = -ENOMEM;
		goto class_device_destroy;
	}
	PDEBUG("sb_in virt address: %p, phys address: 0x%x",
			sb_in_virt, tzcom_virt_to_phys(sb_in_virt));

	sb_out_phys = pmem_kalloc(sb_out_length, PMEM_MEMTYPE_EBI1 |
			PMEM_ALIGNMENT_4K);
	if (IS_ERR((void *)sb_out_phys)) {
		PERR("could not allocte in kernel pmem buffers for sb_out");
		sb_out_phys = 0;
		rc = -ENOMEM;
		goto class_device_destroy;
	}
	PDEBUG("physical_addr for sb_out: 0x%x", sb_out_phys);

	sb_out_virt = (u8 *) ioremap((unsigned long)sb_out_phys,
			sb_out_length);
	if (!sb_out_virt) {
		PERR("Shared buffer OUT allocation failed.");
		rc = -ENOMEM;
		goto class_device_destroy;
	}
	PDEBUG("sb_out virt address: %p, phys address: 0x%x",
			sb_out_virt, tzcom_virt_to_phys(sb_out_virt));

	/* Initialized in tzcom_open */
	pil = NULL;

	return 0;

class_device_destroy:
	if (sb_in_virt)
		iounmap(sb_in_virt);
	if (sb_in_phys)
		pmem_kfree(sb_in_phys);
	if (sb_out_virt)
		iounmap(sb_out_virt);
	if (sb_out_phys)
		pmem_kfree(sb_out_phys);
	device_destroy(driver_class, tzcom_device_no);
class_destroy:
	class_destroy(driver_class);
unregister_chrdev_region:
	unregister_chrdev_region(tzcom_device_no, 1);
	return rc;
}

static void __exit tzcom_exit(void)
{
	PDEBUG("Goodbye tzcom");
	if (sb_in_virt)
		iounmap(sb_in_virt);
	if (sb_in_phys)
		pmem_kfree(sb_in_phys);
	if (sb_out_virt)
		iounmap(sb_out_virt);
	if (sb_out_phys)
		pmem_kfree(sb_out_phys);
	if (pil != NULL) {
		pil_put(pil);
		pil = NULL;
	}
	device_destroy(driver_class, tzcom_device_no);
	class_destroy(driver_class);
	unregister_chrdev_region(tzcom_device_no, 1);
}


MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Sachin Shah <sachins@codeaurora.org>");
MODULE_DESCRIPTION("Qualcomm TrustZone Communicator");
MODULE_VERSION("1.00");

module_init(tzcom_init);
module_exit(tzcom_exit);
