/*
 * 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 "nodeviewcontext.h"

#include <QBrush>
#include <QCoreApplication>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPen>
#include <QStyleOptionGraphicsItem>

#include "core.h"
#include "node/block/block.h"
#include "node/group/group.h"
#include "node/output/track/track.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
#include "nodeviewitem.h"
#include "ui/colorcoding.h"

namespace olive
{

#define super QGraphicsRectItem

NodeViewContext::NodeViewContext(Node *context, QGraphicsItem *item)
	: super(item)
	, context_(context)
{
	Block *block = dynamic_cast<Block *>(context_);
	if (block && block->track() && block->track()->sequence()) {
		rational timebase = block->track()
								->sequence()
								->GetVideoParams()
								.frame_rate_as_time_base();
		lbl_ =
			QCoreApplication::translate("NodeViewContext", "%1 [%2] :: %3 - %4")
				.arg(block->GetLabelAndName(),
					 Track::Reference::TypeToTranslatedString(
						 block->track()->type()),
					 QString::fromStdString(Timecode::time_to_timecode(
						 block->in(), timebase,
						 Core::instance()->GetTimecodeDisplay())),
					 QString::fromStdString(Timecode::time_to_timecode(
						 block->out(), timebase,
						 Core::instance()->GetTimecodeDisplay())));
	} else {
		lbl_ = context_->GetLabelAndName();
	}

	const Node::PositionMap &map = context_->GetContextPositions();
	for (auto it = map.cbegin(); it != map.cend(); it++) {
		AddChild(it.key());
	}

	connect(context_, &Node::NodeAddedToContext, this,
			&NodeViewContext::AddChild, Qt::DirectConnection);
	connect(context_, &Node::NodePositionInContextChanged, this,
			&NodeViewContext::SetChildPosition, Qt::DirectConnection);
	connect(context_, &Node::NodeRemovedFromContext, this,
			&NodeViewContext::RemoveChild, Qt::DirectConnection);
}

NodeViewContext::~NodeViewContext()
{
	// Delete edges before items, because the edge constructor references the items
	qDeleteAll(edges_);
	edges_.clear();
}

void NodeViewContext::AddChild(Node *node)
{
	if (!context_) {
		return;
	}

	NodeViewItem *item = new NodeViewItem(node, context_, this);
	item->SetFlowDirection(flow_dir_);

	AddNodeInternal(node, item);

	if (NodeGroup *group = dynamic_cast<NodeGroup *>(node)) {
		for (auto it = group->GetContextPositions().cbegin();
			 it != group->GetContextPositions().cend(); it++) {
			// Use this item as the representative for all of these nodes too
			AddNodeInternal(it.key(), item);
		}

		connect(group, &NodeGroup::NodeAddedToContext, this,
				&NodeViewContext::GroupAddedNode);
		connect(group, &NodeGroup::NodeRemovedFromContext, this,
				&NodeViewContext::GroupRemovedNode);
	}

	UpdateRect();
}

void NodeViewContext::SetChildPosition(Node *node, const QPointF &pos)
{
	item_map_.value(node)->SetNodePosition(pos);
}

void NodeViewContext::RemoveChild(Node *node)
{
	disconnect(node, &Node::InputConnected, this,
			   &NodeViewContext::ChildInputConnected);
	disconnect(node, &Node::InputDisconnected, this,
			   &NodeViewContext::ChildInputDisconnected);

	if (NodeGroup *group = dynamic_cast<NodeGroup *>(node)) {
		disconnect(group, &NodeGroup::NodeAddedToContext, this,
				   &NodeViewContext::GroupAddedNode);
		disconnect(group, &NodeGroup::NodeRemovedFromContext, this,
				   &NodeViewContext::GroupRemovedNode);
	}

	NodeViewItem *item = item_map_.take(node);

	// Remove from scene before emitting signal so that any drag functions that might be happening
	// now can be handled before the item is destroyed
	scene()->removeItem(item);

	emit ItemAboutToBeDeleted(item);

	// Delete edges first because the edge destructor will try to reference item (maybe that should
	// be changed...)
	QVector<NodeViewEdge *> edges_to_remove = item->GetAllEdgesRecursively();
	foreach (NodeViewEdge *edge, edges_to_remove) {
		if (node == item->GetNode() || edge->output() == node ||
			edge->input().node() == node) {
			ChildInputDisconnected(edge->output(), edge->input());
		}
	}

	// Check if this item is specifically for this node and the node is a group. If so, remove it for
	// all other entries in the map.
	if (item->GetNode() == node) {
		if (dynamic_cast<NodeGroup *>(item->GetNode())) {
			for (auto it = item_map_.begin(); it != item_map_.end();) {
				if (it.value() == item) {
					it = item_map_.erase(it);
				} else {
					it++;
				}
			}
		}

		delete item;
	}

	UpdateRect();
}

void NodeViewContext::ChildInputConnected(Node *output, const NodeInput &input)
{
	// Add edge
	if (!input.IsHidden()) {
		if (NodeViewItem *output_item = item_map_.value(output)) {
			AddEdgeInternal(
				output, input, output_item,
				item_map_.value(input.node())->GetItemForInput(input));
		}
	}
}

bool NodeViewContext::ChildInputDisconnected(Node *output,
											 const NodeInput &input)
{
	// Remove edge
	for (int i = 0; i < edges_.size(); i++) {
		NodeViewEdge *e = edges_.at(i);
		if (e->output() == output && e->input() == input) {
			delete e;
			edges_.removeAt(i);
			return true;
		}
	}

	return false;
}

qreal GetTextOffset(const QFontMetricsF &fm)
{
	return fm.height() / 2;
}

void NodeViewContext::UpdateRect()
{
	QFont f;
	QFontMetricsF fm(f);
	qreal lbl_offset = GetTextOffset(fm);

	QRectF cbr = childrenBoundingRect();
	QRectF rect = cbr;
	int pad = NodeViewItem::DefaultItemHeight();
	rect.adjust(-pad, -lbl_offset * 2 - fm.height() - pad, pad, pad);
	setRect(rect);

	last_titlebar_height_ = rect.y() + (cbr.y() - rect.y()) - pad;
}

void NodeViewContext::SetFlowDirection(NodeViewCommon::FlowDirection dir)
{
	flow_dir_ = dir;

	foreach (NodeViewItem *item, item_map_) {
		item->SetFlowDirection(dir);
	}
}

void NodeViewContext::SetCurvedEdges(bool e)
{
	curved_edges_ = e;

	foreach (NodeViewEdge *edge, edges_) {
		edge->SetCurved(e);
	}
}

int NodeViewContext::DeleteSelected(NodeViewDeleteCommand *command)
{
	int count = 0;

	// Delete any selected edges
	foreach (NodeViewEdge *edge, edges_) {
		if (edge->isSelected()) {
			command->AddEdge(edge->output(), edge->input());
		}
	}

	// Delete any selected nodes
	foreach (NodeViewItem *node, item_map_) {
		if (node->isSelected()) {
			command->AddNode(node->GetNode(), context_);
			count++;
		}
	}

	return count;
}

void NodeViewContext::Select(const QVector<Node *> &nodes)
{
	foreach (Node *n, nodes) {
		if (NodeViewItem *item = item_map_.value(n)) {
			item->setSelected(true);
		}
	}
}

QVector<NodeViewItem *> NodeViewContext::GetSelectedItems() const
{
	QVector<NodeViewItem *> items;

	for (auto it = item_map_.cbegin(); it != item_map_.cend(); it++) {
		if (it.value()->isSelected()) {
			if (!items.contains(it.value())) {
				items.append(it.value());
			}
		}
	}

	return items;
}

QPointF NodeViewContext::MapScenePosToNodePosInContext(const QPointF &pos) const
{
	for (auto it = item_map_.cbegin(); it != item_map_.cend(); it++) {
		QPointF pos_inside_parent =
			it.value()->mapToParent(it.value()->mapFromScene(pos));
		return NodeViewItem::ScreenToNodePoint(pos_inside_parent, flow_dir_);
	}
	return QPointF(0, 0);
}

void NodeViewContext::paint(QPainter *painter,
							const QStyleOptionGraphicsItem *option,
							QWidget *widget)
{
	// Set pen and brush
	Color color = context_->color();
	QColor c = QtUtils::toQColor(color);
	QPen pen(c, 2);
	if (option->state & QStyle::State_Selected) {
		pen.setStyle(Qt::DotLine);
	}
	painter->setPen(pen);

	QColor bg = c;
	bg.setAlpha(128);
	painter->setBrush(bg);

	// Draw semi-transparent rect for whole item
	int rounded = painter->fontMetrics().height();
	painter->drawRoundedRect(rect(), rounded, rounded);

	// Draw solid background for titlebar
	QRectF titlebar_rect = rect();
	titlebar_rect.setHeight(last_titlebar_height_ - rect().top());
	painter->setClipRect(titlebar_rect);
	painter->setBrush(c);
	painter->drawRoundedRect(rect(), rounded, rounded);
	painter->setClipping(false);

	// Draw titlebar text
	painter->setPen(ColorCoding::GetUISelectorColor(color));

	int offset = GetTextOffset(painter->fontMetrics());

	QRectF text_rect = rect();
	text_rect.adjust(offset, offset, -offset, -offset);
	painter->drawText(text_rect, lbl_);
}

QVariant NodeViewContext::itemChange(GraphicsItemChange change,
									 const QVariant &value)
{
	return super::itemChange(change, value);
}

void NodeViewContext::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
	bool clicked_inside_titlebar = (event->pos().y() < last_titlebar_height_);

	setFlag(ItemIsMovable, clicked_inside_titlebar);
	setFlag(ItemIsSelectable, clicked_inside_titlebar);

	super::mousePressEvent(event);
}

void NodeViewContext::AddNodeInternal(Node *node, NodeViewItem *item)
{
	connect(node, &Node::InputConnected, this,
			&NodeViewContext::ChildInputConnected);
	connect(node, &Node::InputDisconnected, this,
			&NodeViewContext::ChildInputDisconnected);

	item_map_.insert(node, item);

	if (node == context_) {
		item->SetLabelAsOutput(true);
	}

	for (auto it = node->output_connections().cbegin();
		 it != node->output_connections().cend(); it++) {
		if (!it->second.IsHidden()) {
			if (NodeViewItem *other_item = item_map_.value(it->second.node())) {
				AddEdgeInternal(node, it->second, item,
								other_item->GetItemForInput(it->second));
			}
		}
	}

	for (auto it = node->input_connections().cbegin();
		 it != node->input_connections().cend(); it++) {
		if (!it->first.IsHidden()) {
			if (NodeViewItem *other_item = item_map_.value(it->second)) {
				AddEdgeInternal(it->second, it->first, other_item,
								item->GetItemForInput(it->first));
			}
		}
	}
}

void NodeViewContext::AddEdgeInternal(Node *output, const NodeInput &input,
									  NodeViewItem *from, NodeViewItem *to)
{
	if (from == to) {
		return;
	}

	NodeViewEdge *edge_ui = new NodeViewEdge(output, input, from, to, this);

	edge_ui->Adjust();
	edge_ui->SetCurved(curved_edges_);

	edges_.append(edge_ui);
}

void NodeViewContext::GroupAddedNode(Node *node)
{
	NodeGroup *group = static_cast<NodeGroup *>(sender());

	AddNodeInternal(node, item_map_.value(group));
}

void NodeViewContext::GroupRemovedNode(Node *node)
{
	NodeGroup *group = static_cast<NodeGroup *>(sender());

	if (item_map_.value(node) == item_map_.value(group)) {
		item_map_.remove(node);
	}
}

}
