/*
 * 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 "GesturePinch.hpp"

namespace OpenVulkano::Input
{
	namespace
	{
		constexpr float MIN_DISTANCE = 2;
		constexpr float MIN_DISTANCE2 = MIN_DISTANCE * MIN_DISTANCE;
		constexpr float START_DISTANCE = 400;
		constexpr uint32_t REQUIRED_TOUCHES = 2;
	}

	void GesturePinch::TryStart()
	{
		uint32_t dragCount = 0, existingActive = 0;

		for (const auto& t : m_pendingTouches)
		{
			if (t.isDrag) dragCount++;
			if (t.active) existingActive++;
		}

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

			uint32_t activeCount = existingActive;

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

			m_paused = false;

			if (!IsActive())
			{
				SetActive(true);
				float distance = CalculateDistance();
				if (distance < START_DISTANCE)
				{
					SetActive(false);
					return;
				}
					
				m_initialDistance = distance;

				m_pinchInfo.scale = 0;
				m_pinchInfo.position = CalculateCenter();

				OnPinchStarted.NotifyAll(this, m_pinchInfo);

				m_lastDistance = m_initialDistance;
			}
			else
			{
				OnPinchMoved.NotifyAll(this, m_pinchInfo);
			}
		}
	}

	float GesturePinch::LinearLength(const std::vector<Math::Vector2f>& points)
	{
		if (points.size() < 2) return 0;
		auto start = points.begin();
		auto finish = start + 1;
		float sum = 0;

		while (finish != points.end())
		{
			Math::Vector2f displacement = *start - *finish;
			sum += Math::Utils::length(displacement);
			start = finish++;
		}

		return sum;
	}

	float GesturePinch::CalculateDistance() const
	{
		if (m_pendingTouches.size() >= REQUIRED_TOUCHES && IsActive() && !m_paused)
		{
			std::vector<Math::Vector2f> positions;
			for (const auto& t : m_pendingTouches)
			{
				if (t.active) positions.push_back(t.currentPosition);
			}
			return LinearLength(positions);
		}
		return 0;
	}

	Math::Vector2f GesturePinch::CalculateCenter() const
	{
		Math::Vector2f center(0);
		if (m_pendingTouches.size() >= REQUIRED_TOUCHES && IsActive() && !m_paused)
		{
			int count = 0;
			for (const auto& t : m_pendingTouches)
			{
				if (t.active)
				{
					center += t.currentPosition;
					count++;
				}
			}
			if (count)
			{
				center /= count;
			}
		}
		return center;
	}

	void GesturePinch::Cancel()
	{
		for (auto& t : m_pendingTouches)
		{
			t.active = false;
		}
		SetActive(false);
		m_paused = false;
		OnPinchEnded.NotifyAll(this, m_pinchInfo);
		m_pinchInfo.position = {};
		m_pinchInfo.scale = 0.0f;
	}

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

		m_pendingTouches.push_back(ti);
	}

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

					Math::Vector2f displacement = t.currentPosition - t.initialPosition;

					if (Math::Utils::length2(displacement) > MIN_DISTANCE2)
					{
						t.isDrag = true;
						oneIsDrag = true;
					}

					if (t.active) isActive = true;

					isPending = true;
				}
			}


			if (oneIsDrag)
			{
				// Just to activate both cursors
				for (auto& t : m_pendingTouches)
				{
					t.isDrag = true;
				}
			}

			if (isPending)
			{
				if (!IsActive() || m_paused)
				{
					TryStart();
				}
				else if (isActive)
				{
					float newDistance = CalculateDistance();

					if (m_lastDistance == 0) m_lastDistance = newDistance;

					m_pinchInfo.scale = (newDistance - m_initialDistance);
					m_pinchInfo.position = CalculateCenter();

					OnPinchMoved.NotifyAll(this, m_pinchInfo);

					m_lastDistance = newDistance;
				}
			}
		}
	}

	void GesturePinch::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();
		}
	}
}
