/*
 * Oak Video Editor - Non-Linear Video Editor
 * Copyright (C) 2025 Olive CE Team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef TIMEBASEDVIEWSELECTIONMANAGER_H
#define TIMEBASEDVIEWSELECTIONMANAGER_H

#include <QGraphicsView>
#include <QMouseEvent>
#include <QRubberBand>
#include <QToolTip>

#include "common/qtutils.h"
#include "timebasedview.h"
#include "timebasedwidget.h"
#include "widget/timetarget/timetarget.h"

namespace olive
{

template <typename T> class TimeBasedViewSelectionManager {
public:
	TimeBasedViewSelectionManager(TimeBasedView *view)
		: view_(view)
		, rubberband_(nullptr)
		, snap_mask_(TimeBasedWidget::kSnapAll)
	{
	}

	void SetSnapMask(TimeBasedWidget::SnapMask e)
	{
		snap_mask_ = e;
	}

	void ClearDrawnObjects()
	{
		drawn_objects_.clear();
	}

	void DeclareDrawnObject(T *object, const QRectF &rect)
	{
		QRectF r(view_->UnscalePoint(rect.topLeft()),
				 view_->UnscalePoint(rect.bottomRight()));
		drawn_objects_.push_back({ object, r });
	}

	bool Select(T *key)
	{
		Q_ASSERT(key);

		if (!IsSelected(key)) {
			selected_.push_back(key);
			return true;
		}

		return false;
	}

	bool Deselect(T *key)
	{
		Q_ASSERT(key);

		auto it = std::find(selected_.cbegin(), selected_.cend(), key);
		if (it == selected_.cend()) {
			return false;
		} else {
			selected_.erase(it);
			return true;
		}
	}

	void ClearSelection()
	{
		selected_.clear();
	}

	bool IsSelected(T *key) const
	{
		return std::find(selected_.cbegin(), selected_.cend(), key) !=
			   selected_.cend();
	}

	const std::vector<T *> &GetSelectedObjects() const
	{
		return selected_;
	}

	void SetTimebase(const rational &tb)
	{
		timebase_ = tb;
	}

	T *GetObjectAtPoint(const QPointF &scene_pt)
	{
		// Iterate in reverse order because the objects drawn later will appear on top to the user
		QPointF unscaled = view_->UnscalePoint(scene_pt);
		for (auto it = drawn_objects_.crbegin(); it != drawn_objects_.crend();
			 it++) {
			const DrawnObject &kp = *it;
			if (kp.second.contains(unscaled)) {
				return kp.first;
			}
		}

		return nullptr;
	}

	T *GetObjectAtPoint(const QPoint &pt)
	{
		return GetObjectAtPoint(view_->mapToScene(pt));
	}

	T *MousePress(QMouseEvent *event)
	{
		T *key_under_cursor = nullptr;

		if (event->button() == Qt::LeftButton ||
			event->button() == Qt::RightButton) {
			// See if there's a keyframe in this position
			key_under_cursor = GetObjectAtPoint(event->pos());

			bool holding_shift = event->modifiers() & Qt::ShiftModifier;

			if (!key_under_cursor || !IsSelected(key_under_cursor)) {
				if (!holding_shift) {
					// If not already selecting and not holding shift, clear the current selection
					ClearSelection();
				}

				// Add item to selection, either nothing if shift wasn't held, or the existing selection
				if (key_under_cursor) {
					Select(key_under_cursor);
					view_->SelectionManagerSelectEvent(key_under_cursor);
				}
			} else if (holding_shift) {
				// If selected and holding shift, de-select this item but do nothing else
				Deselect(key_under_cursor);
				view_->SelectionManagerDeselectEvent(key_under_cursor);
				key_under_cursor = nullptr;
			}
		}

		return key_under_cursor;
	}

	bool IsDragging() const
	{
		return !dragging_.empty();
	}

	void DragStart(T *initial_item, QMouseEvent *event,
				   TimeTargetObject *target = nullptr)
	{
		if (event->button() != Qt::LeftButton) {
			return;
		}

		time_target_ = target;

		initial_drag_item_ = initial_item;

		dragging_.resize(selected_.size());

		if constexpr (std::is_same_v<T, TimelineMarker>) {
			snap_points_.resize(selected_.size() * 2);
		} else {
			snap_points_.resize(selected_.size());
		}

		if (target) {
			time_targets_.resize(snap_points_.size());
			memset(time_targets_.data(), 0,
				   time_targets_.size() * sizeof(Node *));
		} else {
			time_targets_.clear();
		}

		for (size_t i = 0; i < selected_.size(); i++) {
			T *obj = selected_.at(i);

			if constexpr (std::is_same_v<T, TimelineMarker>) {
				dragging_[i] = obj->time().in();
				snap_points_[i] = obj->time().in();
				snap_points_[i + selected_.size()] = obj->time().out();

				if (target) {
					time_targets_[i] = time_targets_[i + selected_.size()] =
						QtUtils::GetParentOfType<Node>(obj);
				}
			} else {
				dragging_[i] = obj->time();
				snap_points_[i] = obj->time();

				if (target) {
					time_targets_[i] = QtUtils::GetParentOfType<Node>(obj);
				}
			}
		}

		drag_mouse_start_ =
			view_->UnscalePoint(view_->mapToScene(event->pos()));
	}

	void SnapPoints(rational *movement)
	{
		std::vector<rational> copy = snap_points_;

		if (time_target_) {
			for (size_t i = 0; i < copy.size(); i++) {
				if (Node *parent = time_targets_[i]) {
					copy[i] = time_target_->GetAdjustedTime(
						parent, time_target_->GetTimeTarget(), copy[i],
						Node::kTransformTowardsOutput);
				}
			}
		}

		if (Core::instance()->snapping() && view_->GetSnapService()) {
			view_->GetSnapService()->SnapPoint(copy, movement, snap_mask_);
		}
	}

	void Unsnap()
	{
		if (view_->GetSnapService()) {
			view_->GetSnapService()->HideSnaps();
		}
	}

	void DragMove(const QPoint &local_pos,
				  const QString &tip_format = QString())
	{
		rational time_diff =
			view_->SceneToTimeNoGrid(view_->mapToScene(local_pos).x() -
									 view_->ScalePoint(drag_mouse_start_).x());

		// Snap points
		rational presnap_time_diff = time_diff;
		SnapPoints(&time_diff);

		// Validate snapping
		if (Core::instance()->snapping() && view_->GetSnapService()) {
			for (size_t i = 0; i < selected_.size(); i++) {
				rational proposed_time = dragging_.at(i) + time_diff;
				T *sel = selected_.at(i);

				if (sel->has_sibling_at_time(proposed_time)) {
					// Unsnap
					time_diff = presnap_time_diff;
					if (view_->GetSnapService()) {
						view_->GetSnapService()->HideSnaps();
					}
					break;
				}
			}
		}

		// Validate movement
		for (size_t i = 0; i < selected_.size(); i++) {
			rational proposed_time = dragging_.at(i) + time_diff;
			T *sel = selected_.at(i);

			// Magic number: use interval of 1ms to avoid collisions
			rational adj(1, 1000);
			if (dragging_.at(i) < proposed_time) {
				// Negate adjustment value if origin is less than proposed time
				adj = -adj;
			}

			bool loop;
			do {
				loop = false;
				while (sel->has_sibling_at_time(proposed_time)) {
					proposed_time += adj;
					Unsnap();
				}

				if (proposed_time < 0) {
					// Prevent any object from going below zero
					proposed_time = 0;
					Unsnap();

					// Setting our proposed time to zero may (re)introduce a conflict that we just avoided
					// with the sibling check above, so we request it to happen again. To avoid a negative
					// adj bringing us back below zero, we force adj to positive so it'll only nudge higher
					adj = qAbs(adj);

					loop = true;
				}
			} while (loop);

			time_diff = proposed_time - dragging_.at(i);
		}

		// Apply movement
		for (size_t i = 0; i < selected_.size(); i++) {
			selected_.at(i)->set_time(dragging_.at(i) + time_diff);
		}

		// Show information about this keyframe
		rational display_time;

		if constexpr (std::is_same_v<T, TimelineMarker>) {
			display_time = initial_drag_item_->time().in();
		} else {
			display_time = initial_drag_item_->time();
		}

		QString tip = QString::fromStdString(Timecode::time_to_timecode(
			display_time, timebase_, Core::instance()->GetTimecodeDisplay(),
			false));

		last_used_tip_format_ = tip_format;
		if (!tip_format.isEmpty()) {
			tip = tip_format.arg(tip);
		}

		QToolTip::hideText();
		QToolTip::showText(QCursor::pos(), tip);
	}

	void DragStop(MultiUndoCommand *command)
	{
		QToolTip::hideText();

		for (size_t i = 0; i < selected_.size(); i++) {
			rational current;
			if constexpr (std::is_same_v<T, TimelineMarker>) {
				current = selected_.at(i)->time().in();
			} else {
				current = selected_.at(i)->time();
			}
			command->add_child(
				new SetTimeCommand(selected_.at(i), current, dragging_.at(i)));
		}

		dragging_.clear();
		Unsnap();
	}

	void RubberBandStart(QMouseEvent *event)
	{
		if (event->button() == Qt::LeftButton ||
			event->button() == Qt::RightButton) {
			rubberband_scene_start_ =
				view_->UnscalePoint(view_->mapToScene(event->pos()));

			rubberband_ = new QRubberBand(QRubberBand::Rectangle, view_);
			rubberband_->setGeometry(
				QRect(event->pos().x(), event->pos().y(), 0, 0));
			rubberband_->show();

			rubberband_preselected_ = selected_;
		}
	}

	void RubberBandMove(const QPoint &pos)
	{
		if (IsRubberBanding()) {
			QRectF band_rect = QRectF(view_->mapFromScene(view_->ScalePoint(
										  rubberband_scene_start_)),
									  pos)
								   .normalized();
			rubberband_->setGeometry(band_rect.toRect());

			QPointF current = view_->UnscalePoint(view_->mapToScene(pos));
			QRectF scene_rect =
				QRectF(rubberband_scene_start_, current).normalized();

			selected_ = rubberband_preselected_;
			foreach (const DrawnObject &kp, drawn_objects_) {
				if (scene_rect.intersects(kp.second)) {
					Select(kp.first);
				}
			}
		}
	}

	void RubberBandStop()
	{
		if (IsRubberBanding()) {
			delete rubberband_;
			rubberband_ = nullptr;
		}
	}

	bool IsRubberBanding() const
	{
		return rubberband_;
	}

	void ForceDragUpdate()
	{
		if (IsRubberBanding() || IsDragging()) {
			QPoint local_pos = view_->viewport()->mapFromGlobal(QCursor::pos());
			if (IsRubberBanding()) {
				RubberBandMove(local_pos);
			} else {
				DragMove(local_pos, last_used_tip_format_);
			}
		}
	}

private:
	class SetTimeCommand : public UndoCommand {
	public:
		SetTimeCommand(T *key, const rational &time)
		{
			key_ = key;
			new_time_ = time;
			old_time_ = key_->time();
		}

		SetTimeCommand(T *key, const rational &new_time,
					   const rational &old_time)
		{
			key_ = key;
			new_time_ = new_time;
			old_time_ = old_time;
		}

		virtual Project *GetRelevantProject() const override
		{
			return Project::GetProjectFromObject(key_);
		}

	protected:
		virtual void redo() override
		{
			key_->set_time(new_time_);
		}

		virtual void undo() override
		{
			key_->set_time(old_time_);
		}

	private:
		T *key_;

		rational old_time_;
		rational new_time_;
	};

	TimeBasedView *view_;

	using DrawnObject = QPair<T *, QRectF>;
	std::vector<DrawnObject> drawn_objects_;

	std::vector<T *> selected_;

	std::vector<rational> dragging_;
	std::vector<rational> snap_points_;
	std::vector<Node *> time_targets_;

	T *initial_drag_item_;

	QPointF drag_mouse_start_;

	rational timebase_;

	QRubberBand *rubberband_;
	QPointF rubberband_scene_start_;
	std::vector<T *> rubberband_preselected_;

	TimeBasedWidget::SnapMask snap_mask_;

	TimeTargetObject *time_target_;

	QString last_used_tip_format_;
};

}

#endif // TIMEBASEDVIEWSELECTIONMANAGER_H
