/*
 * 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/>.
 */

#include "keyframeview.h"

#include <QMouseEvent>
#include <QToolTip>
#include <QVBoxLayout>

#include "common/qtutils.h"
#include "dialog/keyframeproperties/keyframeproperties.h"
#include "keyframeviewundo.h"
#include "node/group/group.h"
#include "node/node.h"
#include "node/nodeundo.h"
#include "node/project/serializer/serializer.h"
#include "widget/menu/menu.h"
#include "widget/menu/menushared.h"

namespace olive
{

#define super TimeBasedView

KeyframeView::KeyframeView(QWidget *parent)
	: super(parent)
	, selection_manager_(this)
	, autoselect_siblings_(true)
	, max_scroll_(0)
	, first_chance_mouse_event_(false)
{
	setAlignment(Qt::AlignLeft | Qt::AlignTop);
	SetDefaultDragMode(RubberBandDrag);
	setContextMenuPolicy(Qt::CustomContextMenu);

	connect(this, &KeyframeView::customContextMenuRequested, this,
			&KeyframeView::ShowContextMenu);
}

void KeyframeView::DeleteSelected()
{
	if (!selection_manager_.IsDragging()) {
		MultiUndoCommand *command = new MultiUndoCommand();

		foreach (NodeKeyframe *key, GetSelectedKeyframes()) {
			command->add_child(new NodeParamRemoveKeyframeCommand(key));
		}

		Core::instance()->undo_stack()->push(
			command,
			tr("Deleted %1 Keyframe(s)").arg(GetSelectedKeyframes().size()));
	}
}

KeyframeView::NodeConnections KeyframeView::AddKeyframesOfNode(Node *n)
{
	NodeConnections map;

	foreach (const QString &i, n->inputs()) {
		map.insert(i, AddKeyframesOfInput(n, i));
	}

	return map;
}

KeyframeView::InputConnections
KeyframeView::AddKeyframesOfInput(Node *on, const QString &oinput)
{
	InputConnections vec;

	NodeInput resolved = NodeGroup::ResolveInput(NodeInput(on, oinput));
	Node *n = resolved.node();
	const QString &input = resolved.input();

	if (n->IsInputKeyframable(input)) {
		int arr_sz = n->InputArraySize(input);
		vec.resize(arr_sz + 1);
		for (int i = -1; i < arr_sz; i++) {
			vec[i + 1] = AddKeyframesOfElement(NodeInput(n, input, i));
		}
	}

	return vec;
}

KeyframeView::ElementConnections
KeyframeView::AddKeyframesOfElement(const NodeInput &input)
{
	const QVector<NodeKeyframeTrack> &tracks =
		input.node()->GetKeyframeTracks(input);
	ElementConnections vec(tracks.size());

	for (int i = 0; i < tracks.size(); i++) {
		vec[i] = AddKeyframesOfTrack(NodeKeyframeTrackReference(input, i));
	}

	return vec;
}

KeyframeViewInputConnection *
KeyframeView::AddKeyframesOfTrack(const NodeKeyframeTrackReference &ref)
{
	KeyframeViewInputConnection *track =
		new KeyframeViewInputConnection(ref, this);
	connect(track, &KeyframeViewInputConnection::RequireUpdate, this,
			&KeyframeView::Redraw);
	tracks_.append(track);
	Redraw();
	return track;
}

void KeyframeView::RemoveKeyframesOfTrack(
	KeyframeViewInputConnection *connection)
{
	if (tracks_.removeOne(connection)) {
		foreach (NodeKeyframe *key, connection->GetKeyframes()) {
			selection_manager_.Deselect(key);
		}
		delete connection;
		Redraw();
		emit SelectionChanged();
	}
}

void KeyframeView::SelectAll()
{
	foreach (KeyframeViewInputConnection *track, tracks_) {
		foreach (NodeKeyframe *key, track->GetKeyframes()) {
			SelectKeyframe(key);
		}
	}
}

void KeyframeView::DeselectAll()
{
	selection_manager_.ClearSelection();

	Redraw();
}

void KeyframeView::Clear()
{
	if (!tracks_.isEmpty()) {
		qDeleteAll(tracks_);
		tracks_.clear();
		Redraw();
	}

	selection_manager_.ClearSelection();
}

void KeyframeView::SelectionManagerSelectEvent(void *obj)
{
	if (autoselect_siblings_) {
		NodeKeyframe *key = static_cast<NodeKeyframe *>(obj);
		QVector<NodeKeyframe *> keys = key->parent()->GetKeyframesAtTime(
			key->input(), key->time(), key->element());
		foreach (NodeKeyframe *k, keys) {
			if (k != key) {
				SelectKeyframe(k);
			}
		}
	}

	emit SelectionChanged();
}

void KeyframeView::SelectionManagerDeselectEvent(void *obj)
{
	if (autoselect_siblings_) {
		NodeKeyframe *key = static_cast<NodeKeyframe *>(obj);
		QVector<NodeKeyframe *> keys = key->parent()->GetKeyframesAtTime(
			key->input(), key->time(), key->element());
		foreach (NodeKeyframe *k, keys) {
			if (k != key) {
				DeselectKeyframe(k);
			}
		}
	}

	emit SelectionChanged();
}

bool KeyframeView::CopySelected(bool cut)
{
	if (!selection_manager_.GetSelectedObjects().empty()) {
		ProjectSerializer::SaveData sdata(ProjectSerializer::kOnlyKeyframes);
		sdata.SetOnlySerializeKeyframes(
			selection_manager_.GetSelectedObjects());

		ProjectSerializer::Copy(sdata);

		if (cut) {
			DeleteSelected();
		}

		return true;
	}

	return false;
}

bool KeyframeView::Paste(
	std::function<Node *(const QString &)> find_node_function)
{
	if (!GetViewerNode()) {
		return false;
	}

	ProjectSerializer::Result res =
		ProjectSerializer::Paste(ProjectSerializer::kOnlyKeyframes);
	if (res == ProjectSerializer::kSuccess) {
		const ProjectSerializer::SerializedKeyframes &keys =
			res.GetLoadData().keyframes;

		MultiUndoCommand *command = new MultiUndoCommand();

		rational min = RATIONAL_MAX;
		for (auto it = keys.cbegin(); it != keys.cend(); it++) {
			for (NodeKeyframe *key : it.value()) {
				min = std::min(min, key->time());
			}
		}
		min -= GetViewerNode()->GetPlayhead();

		for (auto it = keys.cbegin(); it != keys.cend(); it++) {
			const QString &paste_id = it.key();

			// Find a node with this ID
			Node *node_with_id = find_node_function(paste_id);

			if (node_with_id) {
				for (NodeKeyframe *key : it.value()) {
					// Adjust sequence time to node's time
					rational t = key->time() - min;
					t = GetAdjustedTime(GetTimeTarget(), node_with_id, t,
										Node::kTransformTowardsInput);
					key->set_time(t);

					if (NodeKeyframe *existing =
							node_with_id->GetKeyframeAtTimeOnTrack(
								key->input(), key->time(), key->track(),
								key->element())) {
						command->add_child(
							new NodeParamRemoveKeyframeCommand(existing));
					}

					command->add_child(
						new NodeParamInsertKeyframeCommand(node_with_id, key));
				}
			} else {
				qDeleteAll(it.value());
			}
		}

		Core::instance()->undo_stack()->push(
			command, tr("Pasted %1 Keyframe(s)").arg(keys.size()));
		return true;
	}

	return false;
}

void KeyframeView::CatchUpScrollEvent()
{
	super::CatchUpScrollEvent();

	this->selection_manager_.ForceDragUpdate();
}

void KeyframeView::mousePressEvent(QMouseEvent *event)
{
	NodeKeyframe *key_under_cursor =
		selection_manager_.GetObjectAtPoint(event->pos());

	if (HandPress(event) || (!key_under_cursor && PlayheadPress(event))) {
		return;
	}

	// Do mouse press things
	if (FirstChanceMousePress(event)) {
		first_chance_mouse_event_ = true;
	} else if (NodeKeyframe *initial_key =
				   selection_manager_.MousePress(event)) {
		selection_manager_.DragStart(initial_key, event, this);
		KeyframeDragStart(event);
	} else {
		selection_manager_.RubberBandStart(event);
	}

	// Update view
	Redraw();
}

void KeyframeView::mouseMoveEvent(QMouseEvent *event)
{
	if (HandMove(event) || PlayheadMove(event)) {
		return;
	}

	if (first_chance_mouse_event_) {
		FirstChanceMouseMove(event);
	} else if (selection_manager_.IsDragging()) {
		QString tip;
		KeyframeDragMove(event, tip);
		selection_manager_.DragMove(event->pos(), tip);
	} else if (selection_manager_.IsRubberBanding()) {
		selection_manager_.RubberBandMove(event->pos());
		Redraw();
	}

	if (event->buttons()) {
		// Signal cursor pos in case we should scroll to catch up to it
		emit Dragged(event->pos().x(), event->pos().y());
	}
}

void KeyframeView::mouseReleaseEvent(QMouseEvent *event)
{
	if (HandRelease(event) || PlayheadRelease(event)) {
		return;
	}

	if (first_chance_mouse_event_) {
		FirstChanceMouseRelease(event);
		first_chance_mouse_event_ = false;
	} else if (selection_manager_.IsDragging()) {
		MultiUndoCommand *command = new MultiUndoCommand();
		selection_manager_.DragStop(command);
		KeyframeDragRelease(event, command);
		Core::instance()->undo_stack()->push(
			command, tr("Moved %1 Keyframe(s)")
						 .arg(selection_manager_.GetSelectedObjects().size()));
	} else if (selection_manager_.IsRubberBanding()) {
		selection_manager_.RubberBandStop();
		Redraw();
		emit SelectionChanged();
	}

	emit Released();
}

int BinarySearchFirstKeyframeAfterOrAt(const QVector<NodeKeyframe *> &keys,
									   const rational &time)
{
	int low = 0;
	int high = keys.size() - 1;

	while (low <= high) {
		int mid = low + (high - low) / 2;
		NodeKeyframe *test_key = keys.at(mid);

		if (test_key->time() == time ||
			(test_key->time() > time &&
			 (mid == 0 || keys.at(mid - 1)->time() < time))) {
			return mid;
		} else if (test_key->time() < time) {
			low = mid + 1;
		} else {
			high = mid - 1;
		}
	}

	return keys.size();
}

void KeyframeView::drawForeground(QPainter *painter, const QRectF &rect)
{
	int key_sz = QtUtils::QFontMetricsWidth(fontMetrics(), "Oi");
	int key_rad = key_sz / 2;

	selection_manager_.ClearDrawnObjects();

	painter->setRenderHint(QPainter::Antialiasing);

	foreach (KeyframeViewInputConnection *track, tracks_) {
		const QVector<NodeKeyframe *> &keys = track->GetKeyframes();

		if (keys.isEmpty()) {
			continue;
		}

		if (!IsYAxisEnabled()) {
			// Filter out if the keyframes are offscreen Y
			qreal y = GetKeyframeSceneY(track, keys.first());
			if (y + key_rad < rect.top() || y - key_rad >= rect.bottom()) {
				continue;
			}
		}

		// Find first keyframe to show with binary search
		rational left_time = GetUnadjustedKeyframeTime(
			keys.first(), SceneToTime(rect.left() - key_sz));
		int using_index = BinarySearchFirstKeyframeAfterOrAt(keys, left_time);

		rational next_key = RATIONAL_MIN;
		NodeKeyframe::Type last_type = NodeKeyframe::kInvalid;
		for (int i = using_index; i < keys.size(); i++) {
			NodeKeyframe *key = keys.at(i);

			if (key->time() < next_key && key->type() == last_type) {
				// This key will be drawn at exactly the same location as the last one and therefore
				// doesn't need to be drawn. See if the next one will be drawn.
				i++;
				if (i == keys.size()) {
					break;
				}

				key = keys.at(i);

				if (key->time() < next_key) {
					// Next key still won't be drawn, so we'll switch to a binary search
					i = BinarySearchFirstKeyframeAfterOrAt(keys, next_key);

					if (i == keys.size()) {
						break;
					}

					key = keys.at(i);
				}
			}

			QRectF key_rect(-key_rad, -key_rad, key_sz, key_sz);
			qreal key_x = GetKeyframeSceneX(key);
			key_rect.translate(key_x, GetKeyframeSceneY(track, key));

			if (key_rect.left() >= rect.right()) {
				// Break after last keyframe
				break;
			}

			DrawKeyframe(painter, key, track, key_rect);

			next_key = GetUnadjustedKeyframeTime(key, SceneToTime(key_x + 1));
			last_type = key->type();
		}
	}

	super::drawForeground(painter, rect);
}

void KeyframeView::DrawKeyframe(QPainter *painter, NodeKeyframe *key,
								KeyframeViewInputConnection *track,
								const QRectF &key_rect)
{
	painter->setPen(Qt::black);

	if (IsKeyframeSelected(key)) {
		painter->setBrush(palette().highlight());
	} else {
		painter->setBrush(track->GetBrush());
	}

	selection_manager_.DeclareDrawnObject(key, key_rect);

	switch (key->type()) {
	case NodeKeyframe::kInvalid:
		break;
	case NodeKeyframe::kLinear: {
		QPointF points[] = { QPointF(key_rect.center().x(), key_rect.top()),
							 QPointF(key_rect.right(), key_rect.center().y()),
							 QPointF(key_rect.center().x(), key_rect.bottom()),
							 QPointF(key_rect.left(), key_rect.center().y()) };

		painter->drawPolygon(points, 4);
		break;
	}
	case NodeKeyframe::kBezier:
		painter->drawEllipse(key_rect);
		break;
	case NodeKeyframe::kHold:
		painter->drawRect(key_rect);
		break;
	}
}

void KeyframeView::ScaleChangedEvent(const double &scale)
{
	super::ScaleChangedEvent(scale);

	Redraw();
}

void KeyframeView::TimeTargetChangedEvent(ViewerOutput *v)
{
	Redraw();
}

void KeyframeView::TimebaseChangedEvent(const rational &timebase)
{
	super::TimebaseChangedEvent(timebase);

	selection_manager_.SetTimebase(timebase);
}

void KeyframeView::ContextMenuEvent(Menu &m)
{
	Q_UNUSED(m)
}

void KeyframeView::SelectKeyframe(NodeKeyframe *key)
{
	if (selection_manager_.Select(key)) {
		Redraw();

		emit SelectionChanged();
	}
}

void KeyframeView::DeselectKeyframe(NodeKeyframe *key)
{
	if (selection_manager_.Deselect(key)) {
		Redraw();

		emit SelectionChanged();
	}
}

rational KeyframeView::GetUnadjustedKeyframeTime(NodeKeyframe *key,
												 const rational &time)
{
	return GetAdjustedTime(GetTimeTarget(), key->parent(), time,
						   Node::kTransformTowardsInput);
}

rational KeyframeView::GetAdjustedKeyframeTime(NodeKeyframe *key)
{
	return GetAdjustedTime(key->parent(), GetTimeTarget(), key->time(),
						   Node::kTransformTowardsOutput);
}

double KeyframeView::GetKeyframeSceneX(NodeKeyframe *key)
{
	return TimeToScene(GetAdjustedKeyframeTime(key));
}

qreal KeyframeView::GetKeyframeSceneY(KeyframeViewInputConnection *track,
									  NodeKeyframe *key)
{
	return mapFromGlobal(QPoint(0, track->GetKeyframeY())).y();
}

void KeyframeView::SceneRectUpdateEvent(QRectF &rect)
{
	rect.setY(0);
	rect.setHeight(max_scroll_);
}

rational KeyframeView::CalculateNewTimeFromScreen(const rational &old_time,
												  double cursor_diff)
{
	return rational::fromDouble(old_time.toDouble() + cursor_diff);
}

void KeyframeView::ShowContextMenu()
{
	Menu m;

	MenuShared::instance()->AddItemsForEditMenu(&m, false);

	QAction *linear_key_action = nullptr;
	QAction *bezier_key_action = nullptr;
	QAction *hold_key_action = nullptr;

	if (!GetSelectedKeyframes().empty()) {
		bool all_keys_are_same_type = true;
		NodeKeyframe::Type type = GetSelectedKeyframes().front()->type();

		for (size_t i = 1; i < GetSelectedKeyframes().size(); i++) {
			NodeKeyframe *key_item = GetSelectedKeyframes().at(i);
			NodeKeyframe *prev_item = GetSelectedKeyframes().at(i - 1);

			if (key_item->type() != prev_item->type()) {
				all_keys_are_same_type = false;
				break;
			}
		}

		m.addSeparator();

		linear_key_action = m.addAction(tr("Linear"));
		bezier_key_action = m.addAction(tr("Bezier"));
		hold_key_action = m.addAction(tr("Hold"));

		if (all_keys_are_same_type) {
			switch (type) {
			case NodeKeyframe::kInvalid:
				break;
			case NodeKeyframe::kLinear:
				linear_key_action->setChecked(true);
				break;
			case NodeKeyframe::kBezier:
				bezier_key_action->setChecked(true);
				break;
			case NodeKeyframe::kHold:
				hold_key_action->setChecked(true);
				break;
			}
		}
	}

	m.addSeparator();

	ContextMenuEvent(m);

	if (!GetSelectedKeyframes().empty()) {
		m.addSeparator();

		QAction *properties_action = m.addAction(tr("P&roperties"));
		connect(properties_action, &QAction::triggered, this,
				&KeyframeView::ShowKeyframePropertiesDialog);
	}

	QAction *selected = m.exec(QCursor::pos());

	// Process keyframe type changes
	if (selected) {
		if (selected == linear_key_action || selected == bezier_key_action ||
			selected == hold_key_action) {
			NodeKeyframe::Type new_type;

			if (selected == hold_key_action) {
				new_type = NodeKeyframe::kHold;
			} else if (selected == bezier_key_action) {
				new_type = NodeKeyframe::kBezier;
			} else {
				new_type = NodeKeyframe::kLinear;
			}

			MultiUndoCommand *command = new MultiUndoCommand();
			foreach (NodeKeyframe *item, GetSelectedKeyframes()) {
				command->add_child(new KeyframeSetTypeCommand(item, new_type));
			}
			Core::instance()->undo_stack()->push(
				command, tr("Set Type of %1 Keyframe(s)")
							 .arg(GetSelectedKeyframes().size()));
		}
	}
}

void KeyframeView::ShowKeyframePropertiesDialog()
{
	if (!GetSelectedKeyframes().empty()) {
		KeyframePropertiesDialog kd(GetSelectedKeyframes(), timebase(), this);
		kd.exec();
	}
}

void KeyframeView::UpdateRubberBandForScroll()
{
	this->selection_manager_.ForceDragUpdate();
}

void KeyframeView::Redraw()
{
	viewport()->update();
}

}
