#include <qutex.h>
#include <lockerAndInvokerBase.h>

namespace smo {

bool Qutex::tryAcquire(
	const LockerAndInvokerBase &tryingLockvoker, int nRequiredLocks
	)
{
	lock.acquire();

	const int qNItems = static_cast<int>(queue.size());

	// If queue is empty, this should never happen since we register before trying to acquire
	if (qNItems < 1)
	{
		lock.release();

		throw std::runtime_error(
			std::string(__func__) + 
			": tryAcquire called on empty queue - this should never happen");
	}

	// If lock is already owned, fail
	if (isOwned)
	{
		lock.release();
		return false;
	}

	/**	EXPLANATION:
	 * Calculate how many items from the rear we need to scan
	 *
	 * For nRequiredLocks=1: must be at front (nRearItemsToScan = qNItems, scan all)
	 * For nRequiredLocks=2: must be in top 50% (nRearItemsToScan = qNItems/2)
	 * For nRequiredLocks=3: must be in top 66% (nRearItemsToScan = qNItems/3)
	 * etc.
	 */
	const int nRearItemsToScan = qNItems / nRequiredLocks;

	// If we're the only item in queue, or if the fraction calculation
	// results in 0 rear items to scan, we automatically succeed
	if (qNItems == 1 || nRearItemsToScan < 1)
	{
		isOwned = true;
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
		// Use the stored iterator from the LockSet
		auto it = tryingLockvoker.getLockvokerIteratorForQutex(*this);
		currOwner = *it;
#endif
		lock.release();
		return true;
	}

	// For single-lock requests, they must be at the front of the queue
	if (nRequiredLocks == 1)
	{
		bool ret = false;

		if ((*queue.front()) == tryingLockvoker)
		{
			isOwned = true;
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
			currOwner = queue.front();
#endif
			ret = true;
		}
		else {
			ret = false;
		}

		lock.release();
		return ret;
	}

	// For multi-lock requests, check if the lockvoker is in the rear portion
	// If it's NOT in the rear portion, then it's in the top X% and should succeed
	auto rIt = queue.rbegin();
	auto rEndIt = queue.rend();
	bool foundInRear = false;

	for (int i = 0; i < nRearItemsToScan && rIt != rEndIt; ++rIt, ++i)
	{
		if ((**rIt) == tryingLockvoker)
		{
			foundInRear = true;
			break;
		}
	}

	if (foundInRear)
	{
		// Found in rear portion - not in top X%, so fail
		lock.release();
		return false;
	}

	// Not found in rear portion - must be in top X%, so succeed
	isOwned = true;
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
	// Use the stored iterator from the LockSet
	auto it = tryingLockvoker.getLockvokerIteratorForQutex(*this);
	currOwner = *it;
#endif
	lock.release();
	return true;
}

void Qutex::backoff(
	const LockerAndInvokerBase &failedAcquirer, int nRequiredLocks
	)
{
	lock.acquire();

	const int nQItems = static_cast<int>(queue.size());
	
	if (nQItems < 1)
	{
		lock.release();

		throw std::runtime_error(
			std::string(__func__) +
			": backoff called on empty queue - this should never happen");
	}

	/* Check if failedAcquirer is at the front of the queue with
	* nRequiredLocks == 1. This should never happen because an
	* acquirer at the front of the queue with nRequiredLocks == 1
	* should always succeed.
	*/
	const LockerAndInvokerBase& oldFront = *queue.front();
	if (oldFront == failedAcquirer && nRequiredLocks == 1)
	{
		lock.release();

		throw std::runtime_error(
			std::string(__func__) +
			": Failed acquirer is at front of queue with nRequiredLocks==1 - "
			"acquirer at front of queue with nRequiredLocks==1 should always "
			"succeed.");
	}

	// Rotate queue members if failedAcquirer is at front of queue
	if (oldFront == failedAcquirer && nQItems > 1)
	{
		/**	EXPLANATION:
		 * Rotate the top LockSet.size() items in the queue by moving
		 * the failedAcquirer to the last position in the top
		 * LockSet.size() items within the queue.
		 *
		 * I.e: if queue.size()==20, and lockSet.size()==5, then move
		 * failedAcquirer from the front to the 5th position in the queue,
		 * which should push the other 4 items forward.
		 * If queue.size()==3 and LockSet.size()==5, then just
		 * push_back(failedAcquirer).
		 *
		 * It is impossible for a Qutex queue to have only one
		 * item in it, yet for that Lockvoker item to have failed to
		 * acquire the Qutex. Being the only item in the ticketQ
		 * means that you must succeed at acquiring the Qutex.
		 */
		int indexOfItemToInsertCurrFrontBefore;
		if (nQItems > nRequiredLocks) {
			indexOfItemToInsertCurrFrontBefore = nRequiredLocks;
		} else
		{
			// -1 means insert at back -- i.e, use list::end() as insertPos.
			indexOfItemToInsertCurrFrontBefore = -1;
		}

		/*	EXPLANATION:
		 * Rotate them here.
		 *
		 * The reason why we do this rotation is to avoid a particular kind
		 * of deadlock wherein a grid of async requests is perfectly
		 * configured so as to guarantee that none of them can make any
		 * forward progress unless they get reordered.
		 *
		 * Consider 2 different locks with 2 different items in them
		 * each, both of which come from 2 particular requests:
		 *	Qutex1: Lockvoker1, Lv2
		 *	Qutex2: Lv2, Lv1
		 *
		 * Moreover, both of these lockvokers have requiredLocks.size()==2,
		 * and the particular 2 locks that each one requires are indeed
		 * Qutex1 and Qutex2.
		 *
		 * This particular setup basically means that in TL1's queue, Lv1
		 * will wakeup since it's at the front of TL1. It'll successfully
		 * acquire TL1 (since it's at the front), and then it'll try to
		 * acquire TL2. But since Lv1 isn't in the top 50% of items in TL2's
		 * queue, Lv1 will fail to acquire TL2.
		 *
		 * Then similarly, in TL2's queue, Lv2 will wakeup since it's at
		 * the front. Again, it'll successfully acquire TL2 since it's at
		 * the front of TL2's queue. But then it'll try to acquire TL1.
		 * Since it's not in the top 50% of TL1's enqueued items, it'll fail
		 * to acquire TL1.
		 *
		 * N.B: This type of perfectly ordered deadlock can occur in any
		 * kind of NxN situation where ticketQ.size()==requiredLocks.size().
		 * That could be 4x4, 5x5, 6x6, etc. It doesn't happen in 1x1
		 * because a Lockvoker that only requires one lock will always just
		 * succeed if it's at the front of its queue.
		 *
		 * This state of affairs is stable and will persist unless these
		 * queues are reordered in some way. Hence: that's why we rotate the
		 * items in a QutexQ after backing off of it. Backing off means
		 * Not necessarily that the calling LockVoker failed to acquire
		 * THIS PARTICULAR Qutex, but rather than it failed to acquire
		 * ALL of its required locks.
		 *
		 * Hence, if we are backing out, we should also rotate the items
		 * in the queue if the current front item is the failed acquirer.
		 * So that's why we do this rotation here.
		 */

		// Find the iterator for the failed acquirer (which is at the front)
		auto frontIt = queue.begin();

		// Find the position to insert before using indexOfItemToInsertCurrFrontBefore
		auto insertPos = queue.begin();
		if (indexOfItemToInsertCurrFrontBefore == -1)
		{
			// -1 means insert at the back (before end())
			insertPos = queue.end();
		}
		else
		{
			// Move to the specified position (0-based index)
			for (
				int i = 0;
				i < indexOfItemToInsertCurrFrontBefore
					&& insertPos != queue.end(); ++i)
			{
				++insertPos;
			}
		}

		/**	NOTE:
		 * According to https://en.cppreference.com/w/cpp/container/list/splice:
		 *	"No iterators or references become invalidated. If *this and other
		 *	refer to different objects, the iterators to the transferred elements
		 *	now refer into *this, not into other."
		 *
		 * So our stored iterator inside of LockSet will still be valid after
		 * the splice, and we can use it to unregister the lockvoker later on.
		 */
		queue.splice(insertPos, queue, frontIt);
	}

	isOwned = false;
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
	currOwner = nullptr;
#endif
	LockerAndInvokerBase &newFront = *queue.front();

	lock.release();

	/**	EXPLANATION:
	 * Why should this never happen? Well, if we were at the front of the queue
	 * and we failed to acquire the lock, we should have been rotated away from
	 * the front. On the other hand, if we were not at the front of the queue
	 * and we failed to acquire the lock, then we weren't at the front of the
	 * queue to begin with.
	 * The exception is if the queue has only one item in it.
	 *
	 * Hence there ought to be no way for the failedAcquirer to be at the front
	 * of the queue at this point UNLESS the queue has only one item in it.
	 */
	if (newFront == failedAcquirer && nQItems > 1)
	{
		throw std::runtime_error(
			std::string(__func__) +
			": Failed acquirer is at the front of the queue at the end of "
			"backoff, yet nQItems > 1 - this should never happen");
	}

	/**	EXPLANATION:
	 * We should always awaken whoever is at the front of the queue, even if
	 * we didn't rotate. Why? Consider this scenario:
	 *
	 *	Lv1 has LockSet.size==1. Lv2 has LockSet.size==3.
	 *	Lv1's required lock overlaps with Lv2's set of 3 required locks.
	 *	Lv1 registers itself in its 1 qutex's queue.
	 *	Lv2 registers itself in all 3 of its qutexes' queues.
	 *	Lv2 acquires the lock that it needs in common with Lv1.
	 *		(Assume that Lv2 was not at the front of the common qutex's
	 *		internal queue -- it only needed to be in the top 66%.)
	 *	Lv1 tries to acquire the common lock and fails. It gets taken off of
	 *		its io_service. It's now asleep until it gets
	 *		re-added into an io_service.
	 *	Lv2 fails to acquire the other 2 locks it needs and backoff()s from
	 *		the common lock it shares with Lv1.
	 *
	 *	If Lv2 does NOT awaken the item at the front of the common lock's
	 *	queue (aka: Lv1), then Lv1 is doomed to never wake up again.
	 *
	 * Hence: backout() callers should always wake up the lockvoker at the
	 * front of their queue before leaving.
	 *
	 * The exception is if the item at the front is the backout() caller
	 * itself. This can happen if, for example a multi-locking lockvoker
	 * is backing off of a qutex within which it's the only waiter.
	 */
	if (nQItems > 1) {
		newFront.awaken();
	}
}

void Qutex::release()
{
	lock.acquire();

	/**	EXPLAINATION:
	 * A qutex must not have its release() called when it's not owned. The
	 * plumbing required to permit that is a bit excessive, and we have
	 * instrumentation to track early qutex release()ing in
	 * SerializedAsynchronousContinuation.
	 */
	if (!isOwned
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
		|| currOwner == nullptr
#endif
	)
	{
		throw std::runtime_error(
			std::string(__func__) +
			": release() called on unowned qutex - this should never happen");
	}

	isOwned = false;
#ifdef CONFIG_ENABLE_DEBUG_LOCKS
	currOwner = nullptr;
#endif

	// It's possible for there to be 0 items left in queue after unregistering.
	if (queue.empty())
	{
		lock.release();
		return;
	}

	/** EXPLANATION:
	 * It would be nice to be able to optimize by only awakening if the
	 * release()ing lockvoker was at the front of the qutexQ, but if we
	 * don't unconditionally wakeup() the front item, we could get lost
	 * wakeups. Consider:
	 *
	 *	Lv1 only has 1 requiredLock.
	 *	Lv2 has 3 requiredLocks. One of its requiredLocks overlaps with
	 *		Lv1's single requiredLock. So they both share a common lock.
	 *	Lv3's currently owns Lv1 & Lv2's common requiredLock.
	 *	Lv3 release()s that common lock.
	 *	Lv1 happens to be next in queue after Lv3 unregisters itself.
	 *	Lv3 wakes up Lv1.
	 *	Just before Lv1 can acquire the common lock, Lv2 acquires it now,
	 *		because it only needs to be in the top 66% to succeed.
	 *	Lv1 checks the currOwner and sees that it's owned. Lv1 is now
	 *		dequeued from its io_service. It won't be awakened until someone
	 *		awakens it.
	 *	Lv2 finishes its critical section and releas()es the common lock.
	 *	Lv2 was not at the front of the qutexQ, so it does NOT awaken the
	 *		current item at the front.
	 *
	 * Thus, Lv1 never gets awakened again. The end.
	 * This also means that no LockSet.size()==1 lockvoker will ever be able
	 * to run again since they can only run if they are at the front of the
	 * qutexQ.
	 *
	 * Therefore we must always awaken the front item when releas()ing.
	 */
	LockerAndInvokerBase &front = *queue.front();

	lock.release();

	front.awaken();
}

} // namespace smo
