/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2022 Olive Team
  Modifications Copyright (C) 2025 mikesolar

  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 "projectcopier.h"

#include "node/group/group.h"
#include "node/project/footage/footage.h"

namespace olive
{

ProjectCopier::ProjectCopier(QObject *parent)
	: QObject(parent)
{
	original_ = nullptr;
	copy_ = new Project();
	copy_->setParent(this);
}

void ProjectCopier::SetProject(Project *project)
{
	if (original_) {
		// Clear current project
		qDeleteAll(created_nodes_);
		created_nodes_.clear();
		copy_map_.clear();
		graph_update_queue_.clear();

		disconnect(original_, &Project::NodeAdded, this,
				   &ProjectCopier::QueueNodeAdd);
		disconnect(original_, &Project::NodeRemoved, this,
				   &ProjectCopier::QueueNodeRemove);
		disconnect(original_, &Project::InputConnected, this,
				   &ProjectCopier::QueueEdgeAdd);
		disconnect(original_, &Project::InputDisconnected, this,
				   &ProjectCopier::QueueEdgeRemove);
		disconnect(original_, &Project::ValueChanged, this,
				   &ProjectCopier::QueueValueChange);
		disconnect(original_, &Project::InputValueHintChanged, this,
				   &ProjectCopier::QueueValueHintChange);
		disconnect(original_, &Project::SettingChanged, this,
				   &ProjectCopier::QueueProjectSettingChange);
	}

	original_ = project;

	if (original_) {
		// The copied project is only used as an in-memory render proxy. Mark it so
		// downstream code (e.g. RenderWorkerPool) knows it is safe to reset its
		// modified flag after serializing a snapshot.
		copy_->setProperty("_oak_render_proxy", true);

		// Add all nodes
		for (int i = 0; i < copy_->nodes().size(); i++) {
			InsertIntoCopyMap(original_->nodes().at(i), copy_->nodes().at(i));
		}

		for (int i = copy_->nodes().size(); i < original_->nodes().size();
			 i++) {
			DoNodeAdd(original_->nodes().at(i));
		}

		// Add all connections
		foreach (Node *node, original_->nodes()) {
			for (auto it = node->input_connections().cbegin();
				 it != node->input_connections().cend(); it++) {
				DoEdgeAdd(it->second, it->first);
			}
		}

		// Copy project settings
		Project::CopySettings(original_, copy_);

		// Ensure graph change value is just before the sync value
		UpdateGraphChangeValue();
		UpdateLastSyncedValue();

		// Connect signals for future node additions/deletions
		connect(original_, &Project::NodeAdded, this,
				&ProjectCopier::QueueNodeAdd, Qt::DirectConnection);
		connect(original_, &Project::NodeRemoved, this,
				&ProjectCopier::QueueNodeRemove, Qt::DirectConnection);
		connect(original_, &Project::InputConnected, this,
				&ProjectCopier::QueueEdgeAdd, Qt::DirectConnection);
		connect(original_, &Project::InputDisconnected, this,
				&ProjectCopier::QueueEdgeRemove, Qt::DirectConnection);
		connect(original_, &Project::ValueChanged, this,
				&ProjectCopier::QueueValueChange, Qt::DirectConnection);
		connect(original_, &Project::InputValueHintChanged, this,
				&ProjectCopier::QueueValueHintChange, Qt::DirectConnection);
		connect(original_, &Project::SettingChanged, this,
				&ProjectCopier::QueueProjectSettingChange,
				Qt::DirectConnection);
	}
}

void ProjectCopier::ProcessUpdateQueue()
{
	bool copy_changed = false;

	// Iterate everything that happened to the graph and do the same thing on our end
	while (!graph_update_queue_.empty()) {
		QueuedJob job = graph_update_queue_.front();
		graph_update_queue_.pop_front();
		copy_changed = true;

		switch (job.type) {
		case QueuedJob::kNodeAdded:
			DoNodeAdd(job.node);
			break;
		case QueuedJob::kNodeRemoved:
			DoNodeRemove(job.node);
			break;
		case QueuedJob::kEdgeAdded:
			DoEdgeAdd(job.output, job.input);
			break;
		case QueuedJob::kEdgeRemoved:
			DoEdgeRemove(job.output, job.input);
			break;
		case QueuedJob::kValueChanged:
			DoValueChange(job.input);
			break;
		case QueuedJob::kValueHintChanged:
			DoValueHintChange(job.input);
			break;
		case QueuedJob::kProjectSettingChanged:
			DoProjectSettingChange(job.key, job.value);
			break;
		}
	}

	// The copied project is not saved, so its modified flag is only used by the
	// render worker pool to decide whether the serialized graph snapshot is stale.
	// Mark it modified whenever the copy has actually changed.
	if (copy_changed) {
		copy_->set_modified(true);
	}

	// Indicate that we have synchronized to this point, which is compared with the graph change
	// time to see if our copied graph is up to date
	UpdateLastSyncedValue();
}

void ProjectCopier::DoNodeAdd(Node *node)
{
	if (dynamic_cast<NodeGroup *>(node)) {
		// Group nodes are just dummy nodes, no need to copy them
		return;
	}

	// Copy node
	Node *copy = node->copy();

	// Add to project
	copy->setParent(copy_);

	// Disable caches for copy
	copy->SetCachesEnabled(false);

	// Copy cache UUIDs
	copy->CopyCacheUuidsFrom(node);

	// Insert into map
	InsertIntoCopyMap(node, copy);

	// Keep track of our nodes
	created_nodes_.append(copy);
}

void ProjectCopier::DoNodeRemove(Node *node)
{
	// Find our copy and remove it
	Node *copy = copy_map_.take(node);

	// Disconnect from node's caches
	emit RemovedNode(node);

	// Remove from created list
	created_nodes_.removeOne(copy);

	// Delete it
	delete copy;
}

void ProjectCopier::DoEdgeAdd(Node *output, const NodeInput &input)
{
	// Create same connection with our copied graph
	Node *our_output = copy_map_.value(output);
	Node *our_input = copy_map_.value(input.node());

	Node::ConnectEdge(our_output,
					  NodeInput(our_input, input.input(), input.element()));
}

void ProjectCopier::DoEdgeRemove(Node *output, const NodeInput &input)
{
	// Remove same connection with our copied graph
	Node *our_output = copy_map_.value(output);
	Node *our_input = copy_map_.value(input.node());

	Node::DisconnectEdge(our_output,
						 NodeInput(our_input, input.input(), input.element()));
}

void ProjectCopier::DoValueChange(const NodeInput &input)
{
	if (dynamic_cast<NodeGroup *>(input.node())) {
		// Group nodes are just dummy nodes, no need to copy them
		return;
	}

	// Copy all values to our graph
	Node *our_input = copy_map_.value(input.node());
	Node::CopyValuesOfElement(input.node(), our_input, input.input(),
							  input.element());
}

void ProjectCopier::DoValueHintChange(const NodeInput &input)
{
	if (dynamic_cast<NodeGroup *>(input.node())) {
		// Group nodes are just dummy nodes, no need to copy them
		return;
	}

	// Copy value hint to our graph
	Node *our_input = copy_map_.value(input.node());
	Node::ValueHint hint =
		input.node()->GetValueHintForInput(input.input(), input.element());
	our_input->SetValueHintForInput(input.input(), hint, input.element());
}

void ProjectCopier::DoProjectSettingChange(const QString &key,
										   const QString &value)
{
	copy_->SetSetting(key, value);
}

void ProjectCopier::InsertIntoCopyMap(Node *node, Node *copy)
{
	// Insert into map
	copy_map_.insert(node, copy);

	// Copy parameters
	Node::CopyInputs(node, copy, false);

	// Sync Footage proxy state (which is not stored as a Node input)
	if (Footage *src_footage = dynamic_cast<Footage *>(node)) {
		if (dynamic_cast<Footage *>(copy)) {
			connect(src_footage, &Footage::ProxySettingsChanged, this,
					[this, src_footage]() { SyncFootageProxySettings(src_footage); });
			SyncFootageProxySettings(src_footage);
		}
	}

	// Connect to node's cache
	emit AddedNode(node);
}

void ProjectCopier::SyncFootageProxySettings(Footage *source)
{
	Footage *copy = GetCopy(source);
	if (!copy) {
		qWarning() << "ProjectCopier::SyncFootageProxySettings: no copy for"
				  << source->filename();
		return;
	}

	qDebug() << "ProjectCopier::SyncFootageProxySettings:" << source->filename()
			 << "enabled=" << source->proxy_enabled() << "->"
			 << copy->proxy_enabled() << "state="
			 << ProxyManager::ProxyStateToString(source->proxy_state());

	copy->SetProxy(source->proxy_path(), source->proxy_state(),
				   source->proxy_video_stream_index(),
				   source->proxy_preset_version(), source->proxy_enabled());

	if (Project *cp = copy->project()) {
		cp->set_modified(true);
	}
}

void ProjectCopier::QueueNodeAdd(Node *node)
{
	graph_update_queue_.push_back({ QueuedJob::kNodeAdded, node, NodeInput(),
									nullptr, QString(), QString() });
	UpdateGraphChangeValue();
}

void ProjectCopier::QueueNodeRemove(Node *node)
{
	graph_update_queue_.push_back({ QueuedJob::kNodeRemoved, node, NodeInput(),
									nullptr, QString(), QString() });
	UpdateGraphChangeValue();
}

void ProjectCopier::QueueEdgeAdd(Node *output, const NodeInput &input)
{
	graph_update_queue_.push_back({ QueuedJob::kEdgeAdded, nullptr, input,
									output, QString(), QString() });
	UpdateGraphChangeValue();
}

void ProjectCopier::QueueEdgeRemove(Node *output, const NodeInput &input)
{
	graph_update_queue_.push_back({ QueuedJob::kEdgeRemoved, nullptr, input,
									output, QString(), QString() });
	UpdateGraphChangeValue();
}

void ProjectCopier::QueueValueChange(const NodeInput &input)
{
	/*for (auto it = graph_update_queue_.begin(); it != graph_update_queue_.end(); ) {
    if (it->type == QueuedJob::kValueChanged && it->input == input) {
      it = graph_update_queue_.erase(it);
    } else {
      it++;
    }
  }*/

	graph_update_queue_.push_back({ QueuedJob::kValueChanged, nullptr, input,
									nullptr, QString(), QString() });
	UpdateGraphChangeValue();
}

void ProjectCopier::QueueValueHintChange(const NodeInput &input)
{
	graph_update_queue_.push_back({ QueuedJob::kValueHintChanged, nullptr,
									input, nullptr, QString(), QString() });
	UpdateGraphChangeValue();
}

void ProjectCopier::QueueProjectSettingChange(const QString &key,
											  const QString &value)
{
	graph_update_queue_.push_back({ QueuedJob::kProjectSettingChanged, nullptr,
									NodeInput(), nullptr, key, value });
	UpdateGraphChangeValue();
}

void ProjectCopier::UpdateGraphChangeValue()
{
	graph_changed_time_.Acquire();
}

void ProjectCopier::UpdateLastSyncedValue()
{
	last_update_time_.Acquire();
}

}
