/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "GesturePan.hpp"

namespace OpenVulkano::Input
{
	namespace
	{
		constexpr float MIN_DISTANCE = 20;
		constexpr float MIN_DISTANCE2 = MIN_DISTANCE * MIN_DISTANCE;
	}

	void GesturePan::TryStart(const Touch& touch)
	{
		uint32_t dragCount = 0, existingActive = 0;
		for (const auto& t : m_pendingTouches)
		{
			if (t.isDrag) dragCount++;
			if (t.active) existingActive++;
		}

		if (dragCount >= m_requiredTouchInputs)
		{
			if (!IsActive() && !ResolveConflicts()) return;

			uint32_t activeCount = existingActive;
			for (auto& t : m_pendingTouches)
			{
				if (t.isDrag && !t.active && activeCount < m_requiredTouchInputs)
				{
					t.active = true;
					activeCount++;
				}
			}

			m_paused = false;

			if (IsActive())
			{
				m_panInfo.position = CalculatePosition();

				OnPanMoved(this, m_panInfo);
			}
			else
			{
				SetActive(true);
				m_panInfo.position = CalculatePosition();
				m_panInfo.distance = Math::Vector2f();
				m_panInfo.delta = Math::Vector2f();

				OnPanStarted.NotifyAll(this, m_panInfo);
			}
		}
	}

	Math::Vector2f GesturePan::CalculatePosition() const
	{
		Math::Vector2f gesturePos = Math::Vector2f();
		if (m_pendingTouches.size() >= m_requiredTouchInputs && IsActive() && !m_paused)
		{
			int activePointers = 0;
			for (const auto& t : m_pendingTouches)
			{
				gesturePos += t.currentPosition;
				activePointers++;
			}
			gesturePos /= activePointers > 0 ? activePointers : 1;
		}
		return gesturePos;
	}

	void GesturePan::Cancel()
	{
		for (auto& pointer : m_pendingTouches)
		{
			pointer.active = false;
		}

		SetActive(false);
		m_paused = false;

		m_panInfo.delta = Math::Vector2f();
		OnPanEnded(this, m_panInfo);

		m_panInfo.distance = Math::Vector2f();
		m_panInfo.position = Math::Vector2f();
	}

	void GesturePan::TouchDown(const Touch& touch)
	{
		TouchInfo ti;
		ti.touchId = touch.GetId();
		ti.currentPosition = touch.GetPosition();
		ti.initialPosition = touch.GetPosition();

		m_pendingTouches.push_back(ti);
		if (IsActive())
		{
			m_panInfo.position = CalculatePosition();
		}
	}

	void GesturePan::TouchUp(const Touch& touch)
	{
		if (!m_pendingTouches.empty())
		{
			for (auto touchIter = m_pendingTouches.begin(); touchIter != m_pendingTouches.end(); touchIter++)
			{
				if (touch.GetId() == touchIter->touchId)
				{
					if (touchIter->active) m_paused = true;

					m_pendingTouches.erase(touchIter);
					break;
				}
			}

			if (m_pendingTouches.empty() && IsActive())
			{
				Cancel();
			}
			else if (IsActive())
			{
				m_panInfo.position = CalculatePosition();
			}
		}
	}

	void GesturePan::TouchMoved(const Touch& touch)
	{
		if (!m_pendingTouches.empty())
		{
			bool isPending = false;
			bool isActive = false;
			for (auto& t : m_pendingTouches)
			{
				if (touch.GetId() == t.touchId)
				{
					t.currentPosition = touch.GetPosition();

					Math::Vector2f displacement = t.currentPosition - t.initialPosition;
					if (Math::Utils::length2(displacement) > MIN_DISTANCE2)
					{
						t.isDrag = true;
					}

					if (t.active) isActive = true;

					isPending = true;
				}
			}

			if (isPending)
			{
				if (!IsActive() || m_paused)
				{
					TryStart(touch);
				}
				else if (isActive)
				{
					Math::Vector2f newPosition = CalculatePosition();
					m_panInfo.delta = newPosition - m_panInfo.position;
					m_panInfo.distance += m_panInfo.delta;
					m_panInfo.position = newPosition;
					OnPanMoved.NotifyAll(this, m_panInfo);
				}
			}
		}
	}
}