/*
 * Copyright (C) 2010-2014, 2016 ARM Limited. All rights reserved.
 * 
 * This program is free software and is provided to you under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation, and any use by you of this program is subject to the terms of such GNU licence.
 * 
 * A copy of the licence is included with the program, and can also be obtained from Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

/**
 * @file mali_osk_locks.c
 * Implemenation of the OS abstraction layer for the kernel device driver
 */

#include "mali_osk_locks.h"
#include "mali_kernel_common.h"
#include "mali_osk.h"


#ifdef DEBUG
#ifdef LOCK_ORDER_CHECKING
static DEFINE_SPINLOCK(lock_tracking_lock);
static mali_bool add_lock_to_log_and_check(struct _mali_osk_lock_debug_s *lock, uint32_t tid);
static void remove_lock_from_log(struct _mali_osk_lock_debug_s *lock, uint32_t tid);
static const char *const lock_order_to_string(_mali_osk_lock_order_t order);
#endif /* LOCK_ORDER_CHECKING */

void _mali_osk_locks_debug_init(struct _mali_osk_lock_debug_s *checker, _mali_osk_lock_flags_t flags, _mali_osk_lock_order_t order)
{
	checker->orig_flags = flags;
	checker->owner = 0;

#ifdef LOCK_ORDER_CHECKING
	checker->order = order;
	checker->next = NULL;
#endif
}

void _mali_osk_locks_debug_add(struct _mali_osk_lock_debug_s *checker)
{
	checker->owner = _mali_osk_get_tid();

#ifdef LOCK_ORDER_CHECKING
	if (!(checker->orig_flags & _MALI_OSK_LOCKFLAG_UNORDERED)) {
		if (!add_lock_to_log_and_check(checker, _mali_osk_get_tid())) {
			printk(KERN_ERR "%d: ERROR lock %p taken while holding a lock of a higher order.\n",
			       _mali_osk_get_tid(), checker);
			dump_stack();
		}
	}
#endif
}

void _mali_osk_locks_debug_remove(struct _mali_osk_lock_debug_s *checker)
{

#ifdef LOCK_ORDER_CHECKING
	if (!(checker->orig_flags & _MALI_OSK_LOCKFLAG_UNORDERED)) {
		remove_lock_from_log(checker, _mali_osk_get_tid());
	}
#endif
	checker->owner = 0;
}


#ifdef LOCK_ORDER_CHECKING
/* Lock order checking
 * -------------------
 *
 * To assure that lock ordering scheme defined by _mali_osk_lock_order_t is strictly adhered to, the
 * following function will, together with a linked list and some extra members in _mali_osk_lock_debug_s,
 * make sure that a lock that is taken has a higher order than the current highest-order lock a
 * thread holds.
 *
 * This is done in the following manner:
 * - A linked list keeps track of locks held by a thread.
 * - A `next' pointer is added to each lock. This is used to chain the locks together.
 * - When taking a lock, the `add_lock_to_log_and_check' makes sure that taking
 *   the given lock is legal. It will follow the linked list  to find the last
 *   lock taken by this thread. If the last lock's order was lower than the
 *   lock that is to be taken, it appends the new lock to the list and returns
 *   true, if not, it return false. This return value is assert()'ed on in
 *   _mali_osk_lock_wait().
 */

static struct _mali_osk_lock_debug_s *lock_lookup_list;

static void dump_lock_tracking_list(void)
{
	struct _mali_osk_lock_debug_s *l;
	u32 n = 1;

	/* print list for debugging purposes */
	l = lock_lookup_list;

	while (NULL != l) {
		printk(" [lock: %p, tid_owner: %d, order: %d] ->", l, l->owner, l->order);
		l = l->next;
		MALI_DEBUG_ASSERT(n++ < 100);
	}
	printk(" NULL\n");
}

static int tracking_list_length(void)
{
	struct _mali_osk_lock_debug_s *l;
	u32 n = 0;
	l = lock_lookup_list;

	while (NULL != l) {
		l = l->next;
		n++;
		MALI_DEBUG_ASSERT(n < 100);
	}
	return n;
}

static mali_bool add_lock_to_log_and_check(struct _mali_osk_lock_debug_s *lock, uint32_t tid)
{
	mali_bool ret = MALI_FALSE;
	_mali_osk_lock_order_t highest_order_for_tid = _MALI_OSK_LOCK_ORDER_FIRST;
	struct _mali_osk_lock_debug_s *highest_order_lock = (struct _mali_osk_lock_debug_s *)0xbeefbabe;
	struct _mali_osk_lock_debug_s *l;
	unsigned long local_lock_flag;
	u32 len;

	spin_lock_irqsave(&lock_tracking_lock, local_lock_flag);
	len = tracking_list_length();

	l  = lock_lookup_list;
	if (NULL == l) { /* This is the first lock taken by this thread -- record and return true */
		lock_lookup_list = lock;
		spin_unlock_irqrestore(&lock_tracking_lock, local_lock_flag);
		return MALI_TRUE;
	} else {
		/* Traverse the locks taken and find the lock of the highest order.
		 * Since several threads may hold locks, each lock's owner must be
		 * checked so that locks not owned by this thread can be ignored. */
		for (;;) {
			MALI_DEBUG_ASSERT_POINTER(l);
			if (tid == l->owner && l->order >= highest_order_for_tid) {
				highest_order_for_tid = l->order;
				highest_order_lock = l;
			}

			if (NULL != l->next) {
				l = l->next;
			} else {
				break;
			}
		}

		l->next = lock;
		l->next = NULL;
	}

	/* We have now found the highest order lock currently held by this thread and can see if it is
	 * legal to take the requested lock. */
	ret = highest_order_for_tid < lock->order;

	if (!ret) {
		printk(KERN_ERR "Took lock of order %d (%s) while holding lock of order %d (%s)\n",
		       lock->order, lock_order_to_string(lock->order),
		       highest_order_for_tid, lock_order_to_string(highest_order_for_tid));
		dump_lock_tracking_list();
	}

	if (len + 1 != tracking_list_length()) {
		printk(KERN_ERR "************ lock: %p\n", lock);
		printk(KERN_ERR "************ before: %d *** after: %d ****\n", len, tracking_list_length());
		dump_lock_tracking_list();
		MALI_DEBUG_ASSERT_POINTER(NULL);
	}

	spin_unlock_irqrestore(&lock_tracking_lock, local_lock_flag);
	return ret;
}

static void remove_lock_from_log(struct _mali_osk_lock_debug_s *lock, uint32_t tid)
{
	struct _mali_osk_lock_debug_s *curr;
	struct _mali_osk_lock_debug_s *prev = NULL;
	unsigned long local_lock_flag;
	u32 len;
	u32 n = 0;

	spin_lock_irqsave(&lock_tracking_lock, local_lock_flag);
	len = tracking_list_length();
	curr = lock_lookup_list;

	if (NULL == curr) {
		printk(KERN_ERR "Error: Lock tracking list was empty on call to remove_lock_from_log\n");
		dump_lock_tracking_list();
	}

	MALI_DEBUG_ASSERT_POINTER(curr);


	while (lock != curr) {
		prev = curr;

		MALI_DEBUG_ASSERT_POINTER(curr);
		curr = curr->next;
		MALI_DEBUG_ASSERT(n++ < 100);
	}

	if (NULL == prev) {
		lock_lookup_list = curr->next;
	} else {
		MALI_DEBUG_ASSERT_POINTER(curr);
		MALI_DEBUG_ASSERT_POINTER(prev);
		prev->next = curr->next;
	}

	lock->next = NULL;

	if (len - 1 != tracking_list_length()) {
		printk(KERN_ERR "************ lock: %p\n", lock);
		printk(KERN_ERR "************ before: %d *** after: %d ****\n", len, tracking_list_length());
		dump_lock_tracking_list();
		MALI_DEBUG_ASSERT_POINTER(NULL);
	}

	spin_unlock_irqrestore(&lock_tracking_lock, local_lock_flag);
}

static const char *const lock_order_to_string(_mali_osk_lock_order_t order)
{
	switch (order) {
	case _MALI_OSK_LOCK_ORDER_SESSIONS:
		return "_MALI_OSK_LOCK_ORDER_SESSIONS";
		break;
	case _MALI_OSK_LOCK_ORDER_MEM_SESSION:
		return "_MALI_OSK_LOCK_ORDER_MEM_SESSION";
		break;
	case _MALI_OSK_LOCK_ORDER_MEM_INFO:
		return "_MALI_OSK_LOCK_ORDER_MEM_INFO";
		break;
	case _MALI_OSK_LOCK_ORDER_MEM_PT_CACHE:
		return "_MALI_OSK_LOCK_ORDER_MEM_PT_CACHE";
		break;
	case _MALI_OSK_LOCK_ORDER_DESCRIPTOR_MAP:
		return "_MALI_OSK_LOCK_ORDER_DESCRIPTOR_MAP";
		break;
	case _MALI_OSK_LOCK_ORDER_PM_EXECUTION:
		return "_MALI_OSK_LOCK_ORDER_PM_EXECUTION";
		break;
	case _MALI_OSK_LOCK_ORDER_EXECUTOR:
		return "_MALI_OSK_LOCK_ORDER_EXECUTOR";
		break;
	case _MALI_OSK_LOCK_ORDER_TIMELINE_SYSTEM:
		return "_MALI_OSK_LOCK_ORDER_TIMELINE_SYSTEM";
		break;
	case _MALI_OSK_LOCK_ORDER_SCHEDULER:
		return "_MALI_OSK_LOCK_ORDER_SCHEDULER";
		break;
	case _MALI_OSK_LOCK_ORDER_SCHEDULER_DEFERRED:
		return "_MALI_OSK_LOCK_ORDER_SCHEDULER_DEFERRED";
		break;
	case _MALI_OSK_LOCK_ORDER_DMA_COMMAND:
		return "_MALI_OSK_LOCK_ORDER_DMA_COMMAND";
		break;
	case _MALI_OSK_LOCK_ORDER_PROFILING:
		return "_MALI_OSK_LOCK_ORDER_PROFILING";
		break;
	case _MALI_OSK_LOCK_ORDER_L2:
		return "_MALI_OSK_LOCK_ORDER_L2";
		break;
	case _MALI_OSK_LOCK_ORDER_L2_COMMAND:
		return "_MALI_OSK_LOCK_ORDER_L2_COMMAND";
		break;
	case _MALI_OSK_LOCK_ORDER_UTILIZATION:
		return "_MALI_OSK_LOCK_ORDER_UTILIZATION";
		break;
	case _MALI_OSK_LOCK_ORDER_SESSION_PENDING_JOBS:
		return "_MALI_OSK_LOCK_ORDER_SESSION_PENDING_JOBS";
		break;
	case _MALI_OSK_LOCK_ORDER_PM_STATE:
		return "_MALI_OSK_LOCK_ORDER_PM_STATE";
		break;
	default:
		return "<UNKNOWN_LOCK_ORDER>";
	}
}
#endif /* LOCK_ORDER_CHECKING */
#endif /* DEBUG */
