/***

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

***/

#ifndef NODE_H
#define NODE_H

#include "ofxhImageEffectAPI.h"

#include <map>
#include <QMutex>
#include <QObject>
#include <QPainter>
#include <QPointF>
#include <QXmlStreamWriter>

#include "codec/frame.h"
#include "common/xmlutils.h"
#include "node/gizmo/draggable.h"
#include "node/globals.h"
#include "node/keyframe.h"
#include "node/inputimmediate.h"
#include "node/param.h"
#include "render/audioplaybackcache.h"
#include "render/audiowaveformcache.h"
#include "render/framehashcache.h"
#include "render/job/generatejob.h"
#include "render/job/samplejob.h"
#include "render/job/shaderjob.h"
#include "render/shadercode.h"
#include "splitvalue.h"

namespace olive
{

#define NODE_DEFAULT_FUNCTIONS(x) \
	NODE_DEFAULT_DESTRUCTOR(x)    \
	NODE_COPY_FUNCTION(x)

#define NODE_DEFAULT_DESTRUCTOR(x) \
	virtual ~x() override          \
	{                              \
		DisconnectAll();           \
	}

#define NODE_COPY_FUNCTION(x)           \
	virtual Node *copy() const override \
	{                                   \
		return new x();                 \
	}

class Folder;
class Project;
struct SerializedData;

/**
 * @brief A single processing unit that can be connected with others to create intricate processing systems
 *
 * A cornerstone of "visual programming", a node is a single "function" that takes input and returns an output that can
 * be connected to other nodes. Inputs can be either user-set or retrieved from the output of another node. By joining
 * several nodes together, intricate, highly customizable, and infinitely extensible systems can be made for processing
 * data. It can also all be exposed to the user without forcing them to write code or compile anything.
 *
 * A major example in Olive is the entire rendering workflow. To render a frame, Olive will work through a node graph
 * that can be infinitely customized by the user to create images.
 *
 * This is a simple base class designed to contain all the functionality for this kind of processing connective unit.
 * It is an abstract class intended to be subclassed to create nodes with actual functionality.
 */
class Node : public QObject {
	Q_OBJECT
public:
	enum CategoryID {
		kCategoryUnknown = -1,

		kCategoryOutput,
		kCategoryGenerator,
		kCategoryMath,
		kCategoryKeying,
		kCategoryFilter,
		kCategoryColor,
		kCategoryTime,
		kCategoryTimeline,
		kCategoryTransition,
		kCategoryDistort,
		kCategoryProject,
		kCategoryOpenFX,

		kCategoryCount
	};

	enum Flag {
		kNone = 0,
		kDontShowInParamView = 0x1,
		kVideoEffect = 0x2,
		kAudioEffect = 0x4,
		kDontShowInCreateMenu = 0x8,
		kIsItem = 0x10
	};

	struct ContextPair {
		Node *node;
		Node *context;
	};

	Node();

	virtual ~Node() override;

	/**
   * @brief Creates a clone of the Node
   *
   * By default, the clone will NOT have the values and connections of the original node. The caller is responsible for
   * copying that data with functions like CopyInputs() as copies may be done for different reasons.
   */
	virtual Node *copy() const = 0;

	/**
   * @brief Convenience function - assumes parent is a NodeGraph
   */
	Project *parent() const;

	Project *project() const;

	const uint64_t &GetFlags() const
	{
		return flags_;
	}

	/**
   * @brief Return the name of the node
   *
   * This is the node's name shown to the user. This must be overridden by subclasses, and preferably run through the
   * translator.
   */
	virtual QString Name() const = 0;

	/**
   * @brief Returns a shortened name of this node if applicable
   *
   * Defaults to returning Name() but can be overridden.
   */
	virtual QString ShortName() const;

	/**
   * @brief Return the unique identifier of the node
   *
   * This is used in save files and any other times a specific node must be picked out at runtime. This must be an ID
   * completely unique to this node, and preferably in bundle identifier format (e.g. "org.company.Name"). This string
   * should NOT be translated.
   */
	virtual QString id() const = 0;

	/**
   * @brief Return the category this node is in (optional for subclassing, but recommended)
   *
   * In any organized node menus, show the node in this category. If this node should be in a subfolder of a subfolder,
   * use a "/" to separate categories (e.g. "Distort/Noise"). The string should not start with a "/" as this will be
   * interpreted as an empty string category. This value should be run through a translator as its largely user
   * oriented.
   */
	virtual QVector<CategoryID> Category() const = 0;

	/**
	 * @brief Return a sub-category string for secondary grouping
	 *        within the primary category (e.g. "Filter" under "OpenFX").
	 */
	virtual QString SubCategory() const { return QString(); }

	/**
   * @brief Return a description of this node's purpose (optional for subclassing, but recommended)
   *
   * A short (1-2 sentence) description of what this node should do to help the user understand its purpose. This should
   * be run through a translator.
   */
	virtual QString Description() const;

	Folder *folder() const
	{
		return folder_;
	}

	bool IsItem() const
	{
		return flags_ & kIsItem;
	}

	/**
   * @brief Function called to retranslate parameter names (should be overridden in derivatives)
   */
	virtual void Retranslate();

	enum DataType {
		ICON,
		DURATION,
		CREATED_TIME,
		MODIFIED_TIME,
		FREQUENCY_RATE,
		TOOLTIP
	};

	virtual QVariant data(const DataType &d) const;

	const QVector<QString> &inputs() const
	{
		return input_ids_;
	}

	virtual QVector<QString> IgnoreInputsForRendering() const
	{
		return QVector<QString>();
	}

	class ActiveElements {
	public:
		enum Mode { kAllElements, kSpecified, kNoElements };

		ActiveElements(Mode m = kAllElements)
		{
			mode_ = m;
		}

		Mode mode() const
		{
			return mode_;
		}
		std::list<int> elements() const
		{
			return elements_;
		}

		void add(int e)
		{
			elements_.push_back(e);
			mode_ = kSpecified;
		}

	private:
		Mode mode_;
		std::list<int> elements_;
	};

	virtual ActiveElements GetActiveElementsAtTime(const QString &input,
												   const TimeRange &r) const
	{
		return ActiveElements::kAllElements;
	}

	bool HasInputWithID(const QString &id) const
	{
		return input_ids_.contains(id);
	}

	bool HasParamWithID(const QString &id) const
	{
		return HasInputWithID(id);
	}

	FrameHashCache *video_frame_cache() const
	{
		return video_cache_;
	}

	ThumbnailCache *thumbnail_cache() const
	{
		return thumbnail_cache_;
	}

	AudioPlaybackCache *audio_playback_cache() const
	{
		return audio_cache_;
	}

	AudioWaveformCache *waveform_cache() const
	{
		return waveform_cache_;
	}

	virtual TimeRange GetVideoCacheRange() const
	{
		return TimeRange();
	}
	virtual TimeRange GetAudioCacheRange() const
	{
		return TimeRange();
	}

	struct Position {
		Position(const QPointF &p = QPointF(0, 0), bool e = false)
		{
			position = p;
			expanded = e;
		}

		bool load(QXmlStreamReader *reader);
		void save(QXmlStreamWriter *writer) const;

		QPointF position;
		bool expanded;

		inline Position &operator+=(const Position &p)
		{
			position += p.position;
			return *this;
		}

		inline Position &operator-=(const Position &p)
		{
			position -= p.position;
			return *this;
		}

		friend inline const Position operator+(Position a, const Position &b)
		{
			a += b;
			return a;
		}

		friend inline const Position operator-(Position a, const Position &b)
		{
			a -= b;
			return a;
		}
	};

	using PositionMap = QHash<Node *, Position>;
	const PositionMap &GetContextPositions() const
	{
		return context_positions_;
	}

	bool IsNodeExpandedInContext(Node *node) const
	{
		return context_positions_.value(node).expanded;
	}

	bool ContextContainsNode(Node *node) const
	{
		return context_positions_.contains(node);
	}

	Position GetNodePositionDataInContext(Node *node)
	{
		return context_positions_.value(node);
	}

	QPointF GetNodePositionInContext(Node *node)
	{
		return GetNodePositionDataInContext(node).position;
	}

	bool SetNodePositionInContext(Node *node, const QPointF &pos);

	bool SetNodePositionInContext(Node *node, const Position &pos);

	void SetNodeExpandedInContext(Node *node, bool e)
	{
		context_positions_[node].expanded = e;
	}

	bool RemoveNodeFromContext(Node *node);

	/**
   * @brief Retrieve the color of this node
   */
	Color color() const;

	/**
   * @brief Same as color() but return a pretty gradient version
   */
	QLinearGradient gradient_color(qreal top, qreal bottom) const;

	/**
   * @brief Uses config and returns either color() for flat shading or gradient for gradient
   */
	QBrush brush(qreal top, qreal bottom) const;

	int GetOverrideColor() const
	{
		return override_color_;
	}

	/**
   * @brief Sets the override color. Set to -1 for no override color.
   */
	void SetOverrideColor(int index)
	{
		if (override_color_ != index) {
			override_color_ = index;
			emit ColorChanged();
		}
	}

	static void ConnectEdge(Node *output, const NodeInput &input);

	static void DisconnectEdge(Node *output, const NodeInput &input);

	void CopyCacheUuidsFrom(Node *n);

	bool AreCachesEnabled() const
	{
		return caches_enabled_;
	}
	void SetCachesEnabled(bool e)
	{
		caches_enabled_ = e;
	}

	virtual QString GetInputName(const QString &id) const;

	void SetInputName(const QString &id, const QString &name);

	bool IsInputHidden(const QString &input) const;
	bool IsInputConnectable(const QString &input) const;
	bool IsInputKeyframable(const QString &input) const;

	bool IsInputKeyframing(const QString &input, int element = -1) const;
	bool IsInputKeyframing(const NodeInput &input) const
	{
		return IsInputKeyframing(input.input(), input.element());
	}

	void SetInputIsKeyframing(const QString &input, bool e, int element = -1);
	void SetInputIsKeyframing(const NodeInput &input, bool e)
	{
		SetInputIsKeyframing(input.input(), e, input.element());
	}

	bool IsInputConnected(const QString &input, int element = -1) const;
	bool IsInputConnected(const NodeInput &input) const
	{
		return IsInputConnected(input.input(), input.element());
	}

	virtual bool IsInputConnectedForRender(const QString &input,
										   int element = -1) const
	{
		return IsInputConnected(input, element);
	}
	bool IsInputConnectedForRender(const NodeInput &input) const
	{
		return IsInputConnectedForRender(input.input(), input.element());
	}

	bool IsInputStatic(const QString &input, int element = -1) const
	{
		return !IsInputConnected(input, element) &&
			   !IsInputKeyframing(input, element);
	}

	bool IsInputStatic(const NodeInput &input) const
	{
		return IsInputStatic(input.input(), input.element());
	}

	Node *GetConnectedOutput(const QString &input, int element = -1) const;

	Node *GetConnectedOutput(const NodeInput &input) const
	{
		return GetConnectedOutput(input.input(), input.element());
	}

	virtual Node *GetConnectedRenderOutput(const QString &input,
										   int element = -1) const
	{
		return GetConnectedOutput(input, element);
	}

	Node *GetConnectedRenderOutput(const NodeInput &input) const
	{
		return GetConnectedRenderOutput(input.input(), input.element());
	}

	bool IsUsingStandardValue(const QString &input, int track,
							  int element = -1) const;

	NodeValue::Type GetInputDataType(const QString &id) const;
	void SetInputDataType(const QString &id, const NodeValue::Type &type);

	bool HasInputProperty(const QString &id, const QString &name) const;
	QHash<QString, QVariant> GetInputProperties(const QString &id) const;
	QVariant GetInputProperty(const QString &id, const QString &name) const;
	void SetInputProperty(const QString &id, const QString &name,
						  const QVariant &value);

	QVariant GetValueAtTime(const QString &input, const rational &time,
							int element = -1) const
	{
		NodeValue::Type type = GetInputDataType(input);

		return NodeValue::combine_track_values_into_normal_value(
			type, GetSplitValueAtTime(input, time, element));
	}

	QVariant GetValueAtTime(const NodeInput &input, const rational &time)
	{
		return GetValueAtTime(input.input(), time, input.element());
	}

	SplitValue GetSplitValueAtTime(const QString &input, const rational &time,
								   int element = -1) const;

	SplitValue GetSplitValueAtTime(const NodeInput &input, const rational &time)
	{
		return GetSplitValueAtTime(input.input(), time, input.element());
	}

	QVariant GetSplitValueAtTimeOnTrack(const QString &input,
										const rational &time, int track,
										int element = -1) const;
	QVariant GetSplitValueAtTimeOnTrack(const NodeInput &input,
										const rational &time, int track) const
	{
		return GetSplitValueAtTimeOnTrack(input.input(), time, track,
										  input.element());
	}

	QVariant GetSplitValueAtTimeOnTrack(const NodeKeyframeTrackReference &input,
										const rational &time) const
	{
		return GetSplitValueAtTimeOnTrack(input.input(), time, input.track());
	}

	QVariant GetDefaultValue(const QString &input) const;
	SplitValue GetSplitDefaultValue(const QString &input) const;
	QVariant GetSplitDefaultValueOnTrack(const QString &input, int track) const;

	void SetDefaultValue(const QString &input, const QVariant &val);
	void SetSplitDefaultValue(const QString &input, const SplitValue &val);
	void SetSplitDefaultValueOnTrack(const QString &input, const QVariant &val,
									 int track);

	const QVector<NodeKeyframeTrack> &GetKeyframeTracks(const QString &input,
														int element) const;
	const QVector<NodeKeyframeTrack> &
	GetKeyframeTracks(const NodeInput &input) const
	{
		return GetKeyframeTracks(input.input(), input.element());
	}

	QVector<NodeKeyframe *> GetKeyframesAtTime(const QString &input,
											   const rational &time,
											   int element = -1) const;
	QVector<NodeKeyframe *> GetKeyframesAtTime(const NodeInput &input,
											   const rational &time) const
	{
		return GetKeyframesAtTime(input.input(), time, input.element());
	}

	NodeKeyframe *GetKeyframeAtTimeOnTrack(const QString &input,
										   const rational &time, int track,
										   int element = -1) const;
	NodeKeyframe *GetKeyframeAtTimeOnTrack(const NodeInput &input,
										   const rational &time,
										   int track) const
	{
		return GetKeyframeAtTimeOnTrack(input.input(), time, track,
										input.element());
	}

	NodeKeyframe *
	GetKeyframeAtTimeOnTrack(const NodeKeyframeTrackReference &input,
							 const rational &time) const
	{
		return GetKeyframeAtTimeOnTrack(input.input(), time, input.track());
	}

	NodeKeyframe::Type
	GetBestKeyframeTypeForTimeOnTrack(const QString &input,
									  const rational &time, int track,
									  int element = -1) const;

	NodeKeyframe::Type GetBestKeyframeTypeForTimeOnTrack(const NodeInput &input,
														 const rational &time,
														 int track) const
	{
		return GetBestKeyframeTypeForTimeOnTrack(input.input(), time, track,
												 input.element());
	}

	NodeKeyframe::Type
	GetBestKeyframeTypeForTimeOnTrack(const NodeKeyframeTrackReference &input,
									  const rational &time) const
	{
		return GetBestKeyframeTypeForTimeOnTrack(input.input(), time,
												 input.track());
	}

	int GetNumberOfKeyframeTracks(const QString &id) const;
	int GetNumberOfKeyframeTracks(const NodeInput &id) const
	{
		return GetNumberOfKeyframeTracks(id.input());
	}

	NodeKeyframe *GetEarliestKeyframe(const QString &id,
									  int element = -1) const;
	NodeKeyframe *GetEarliestKeyframe(const NodeInput &id) const
	{
		return GetEarliestKeyframe(id.input(), id.element());
	}

	NodeKeyframe *GetLatestKeyframe(const QString &id, int element = -1) const;
	NodeKeyframe *GetLatestKeyframe(const NodeInput &id) const
	{
		return GetLatestKeyframe(id.input(), id.element());
	}

	NodeKeyframe *GetClosestKeyframeBeforeTime(const QString &id,
											   const rational &time,
											   int element = -1) const;
	NodeKeyframe *GetClosestKeyframeBeforeTime(const NodeInput &id,
											   const rational &time) const
	{
		return GetClosestKeyframeBeforeTime(id.input(), time, id.element());
	}

	NodeKeyframe *GetClosestKeyframeAfterTime(const QString &id,
											  const rational &time,
											  int element = -1) const;
	NodeKeyframe *GetClosestKeyframeAfterTime(const NodeInput &id,
											  const rational &time) const
	{
		return GetClosestKeyframeAfterTime(id.input(), time, id.element());
	}

	bool HasKeyframeAtTime(const QString &id, const rational &time,
						   int element = -1) const;
	bool HasKeyframeAtTime(const NodeInput &id, const rational &time) const
	{
		return HasKeyframeAtTime(id.input(), time, id.element());
	}

	QStringList GetComboBoxStrings(const QString &id) const;

	QVariant GetStandardValue(const QString &id, int element = -1) const;
	QVariant GetStandardValue(const NodeInput &id) const
	{
		return GetStandardValue(id.input(), id.element());
	}

	SplitValue GetSplitStandardValue(const QString &id, int element = -1) const;
	SplitValue GetSplitStandardValue(const NodeInput &id) const
	{
		return GetSplitStandardValue(id.input(), id.element());
	}

	QVariant GetSplitStandardValueOnTrack(const QString &input, int track,
										  int element = -1) const;
	QVariant
	GetSplitStandardValueOnTrack(const NodeKeyframeTrackReference &id) const
	{
		return GetSplitStandardValueOnTrack(id.input().input(), id.track(),
											id.input().element());
	}

	void SetStandardValue(const QString &id, const QVariant &value,
						  int element = -1);
	void SetStandardValue(const NodeInput &id, const QVariant &value)
	{
		SetStandardValue(id.input(), value, id.element());
	}

	void SetSplitStandardValue(const QString &id, const SplitValue &value,
							   int element = -1);
	void SetSplitStandardValue(const NodeInput &id, const SplitValue &value)
	{
		SetSplitStandardValue(id.input(), value, id.element());
	}

	void SetSplitStandardValueOnTrack(const QString &id, int track,
									  const QVariant &value, int element = -1);
	void SetSplitStandardValueOnTrack(const NodeKeyframeTrackReference &id,
									  const QVariant &value)
	{
		SetSplitStandardValueOnTrack(id.input().input(), id.track(), value,
									 id.input().element());
	}

	bool InputIsArray(const QString &id) const;

	void InputArrayInsert(const QString &id, int index);
	void InputArrayResize(const QString &id, int size);
	void InputArrayRemove(const QString &id, int index);

	void InputArrayAppend(const QString &id)
	{
		InputArrayResize(id, InputArraySize(id) + 1);
	}

	void InputArrayPrepend(const QString &id)
	{
		InputArrayInsert(id, 0);
	}

	void InputArrayRemoveLast(const QString &id)
	{
		InputArrayResize(id, InputArraySize(id) - 1);
	}

	int InputArraySize(const QString &id) const;

	NodeInputImmediate *GetImmediate(const QString &input, int element) const;

	NodeInput GetEffectInput()
	{
		return effect_input_.isEmpty() ? NodeInput() :
										 NodeInput(this, effect_input_);
	}

	const QString &GetEffectInputID() const
	{
		return effect_input_;
	}

	class ValueHint {
	public:
		explicit ValueHint(
			const QVector<NodeValue::Type> &types = QVector<NodeValue::Type>(),
			int index = -1, const QString &tag = QString())
			: type_(types)
			, index_(index)
			, tag_(tag)
		{
		}

		explicit ValueHint(const QVector<NodeValue::Type> &types,
						   const QString &tag)
			: type_(types)
			, index_(-1)
			, tag_(tag)
		{
		}

		explicit ValueHint(int index)
			: index_(index)
		{
		}

		explicit ValueHint(const QString &tag)
			: index_(-1)
			, tag_(tag)
		{
		}

		const QVector<NodeValue::Type> &types() const
		{
			return type_;
		}
		const int &index() const
		{
			return index_;
		}
		const QString &tag() const
		{
			return tag_;
		}

		void set_type(const QVector<NodeValue::Type> &type)
		{
			type_ = type;
		}
		void set_index(const int &index)
		{
			index_ = index;
		}
		void set_tag(const QString &tag)
		{
			tag_ = tag;
		}

		bool load(QXmlStreamReader *reader);
		void save(QXmlStreamWriter *writer) const;

	private:
		QVector<NodeValue::Type> type_;
		int index_;
		QString tag_;
	};

	const QMap<InputElementPair, ValueHint> &GetValueHints() const
	{
		return value_hints_;
	}

	ValueHint GetValueHintForInput(const QString &input, int element = -1) const
	{
		return value_hints_.value({ input, element });
	}

	void SetValueHintForInput(const QString &input, const ValueHint &hint,
							  int element = -1);

	const NodeKeyframeTrack &GetTrackFromKeyframe(NodeKeyframe *key) const;

	using InputConnections = std::map<NodeInput, Node *>;

	/**
   * @brief Return map of input connections
   *
   * Inputs can only have one connection, so the key is the input connected and the value is the
   * output that it's connected to.
   */
	const InputConnections &input_connections() const
	{
		return input_connections_;
	}

	using OutputConnection = std::pair<Node *, NodeInput>;
	using OutputConnections = std::vector<OutputConnection>;

	/**
   * @brief Return list of output connections
   *
   * An output can connect an infinite amount of inputs, so in this map, the key is the output and
   * the value is a vector of inputs.
   */
	const OutputConnections &output_connections() const
	{
		return output_connections_;
	}

	/**
   * @brief Return a list of all Nodes that this Node's inputs are connected to (does not include this Node)
   */
	QVector<Node *> GetDependencies() const;

	/**
   * @brief Returns a list of Nodes that this Node is dependent on, provided no other Nodes are dependent on them
   * outside of this hierarchy.
   *
   * Similar to GetDependencies(), but excludes any Nodes that are used outside the dependency graph of this Node.
   */
	QVector<Node *> GetExclusiveDependencies() const;

	/**
   * @brief Retrieve immediate dependencies (only nodes that are directly connected to the inputs of this one)
   */
	QVector<Node *> GetImmediateDependencies() const;

	struct ShaderRequest {
		ShaderRequest(const QString &shader_id)
		{
			id = shader_id;
		}

		ShaderRequest(const QString &shader_id, const QString &shader_stub)
		{
			id = shader_id;
			stub = shader_stub;
		}

		QString id;
		QString stub;
	};

	/**
   * @brief Generate hardware accelerated code for this Node
   */
	virtual ShaderCode GetShaderCode(const ShaderRequest &request) const;

	/**
   * @brief If Value() pushes a ShaderJob, this is the function that will process them.
   */
	virtual void ProcessSamples(const NodeValueRow &values,
								const SampleBuffer &input, SampleBuffer &output,
								int index) const;

	/**
   * @brief If Value() pushes a GenerateJob, override this function for the image to create
   *
   * @param frame
   *
   * The destination buffer. It will already be allocated and ready for writing to.
   */
	virtual void GenerateFrame(FramePtr frame, const GenerateJob &job) const;

	/**
   * @brief Returns whether this node ever receives an input from a particular node instance
   */
	bool InputsFrom(Node *n, bool recursively) const;

	/**
   * @brief Returns whether this node ever receives an input from a node with a particular ID
   */
	bool InputsFrom(const QString &id, bool recursively) const;

	/**
   * @brief Find inputs that `output` outputs to in order to arrive at this node
   *
   * Traverse this node's inputs recursively looking for `output`, and return a list of
   * edges that `output` uses to get to `this` node.
   */
	QVector<NodeInput> FindWaysNodeArrivesHere(const Node *output) const;

	/**
   * @brief Severs all input and output connections
   */
	void DisconnectAll();

	/**
   * @brief Get the human-readable name for any category
   */
	static QString GetCategoryName(const CategoryID &c);

	enum TransformTimeDirection {
		kTransformTowardsInput,
		kTransformTowardsOutput
	};

	/**
   * @brief Transforms time from this node through the connections it takes to get to the specified node
   */
	TimeRange TransformTimeTo(TimeRange time, Node *target,
							  TransformTimeDirection dir, int path_index);

	/**
   * @brief Find nodes of a certain type that this Node takes inputs from
   */
	template <class T> QVector<T *> FindInputNodes(int maximum = 0) const;

	/**
   * @brief Find nodes of a certain type that this Node takes inputs from
   */
	template <class T>
	static QVector<T *> FindInputNodesConnectedToInput(const NodeInput &input,
													   int maximum = 0);

	using InvalidateCacheOptions = QHash<QString, QVariant>;

	/**
   * @brief Signal all dependent Nodes that anything cached between start_range and end_range is now invalid and
   *        requires re-rendering
   *
   * Override this if your Node subclass keeps a cache, but call this base function at the end of the subclass function.
   * Default behavior is to relay this signal to all connected outputs, which will need to be done as to not break
   * the DAG. Even if the time needs to be transformed somehow (e.g. converting media time to sequence time), you can
   * call this function with transformed time and relay the signal that way.
   */
	virtual void
	InvalidateCache(const TimeRange &range, const QString &from,
					int element = -1,
					InvalidateCacheOptions options = InvalidateCacheOptions());

	void InvalidateCache(
		const TimeRange &range, const NodeInput &from,
		const InvalidateCacheOptions &options = InvalidateCacheOptions())
	{
		InvalidateCache(range, from.input(), from.element(), options);
	}

	/**
   * @brief Adjusts time that should be sent to nodes connected to certain inputs.
   *
   * If this node modifies the `time` (i.e. a clip converting sequence time to media time), this function should be
   * overridden to do so. Also make sure to override OutputTimeAdjustment() to provide the inverse function.
   */
	virtual TimeRange InputTimeAdjustment(const QString &input, int element,
										  const TimeRange &input_time,
										  bool clamp) const;

	/**
   * @brief The inverse of InputTimeAdjustment()
   */
	virtual TimeRange OutputTimeAdjustment(const QString &input, int element,
										   const TimeRange &input_time) const;

	/**
   * @brief Copies inputs from from Node to another including connections
   *
   * Nodes must be of the same types (i.e. have the same ID)
   */
	static void CopyInputs(const Node *source, Node *destination,
						   bool include_connections = true,
						   MultiUndoCommand *command = nullptr);

	static void CopyInput(const Node *src, Node *dst, const QString &input,
						  bool include_connections, bool traverse_arrays,
						  MultiUndoCommand *command);

	static void CopyValuesOfElement(const Node *src, Node *dst,
									const QString &input, int src_element,
									int dst_element,
									MultiUndoCommand *command = nullptr);
	static void CopyValuesOfElement(const Node *src, Node *dst,
									const QString &input, int element,
									MultiUndoCommand *command = nullptr)
	{
		return CopyValuesOfElement(src, dst, input, element, element, command);
	}

	/**
   * @brief Clones a set of nodes and connects the new ones the way the old ones were
   */
	static QVector<Node *> CopyDependencyGraph(const QVector<Node *> &nodes,
											   MultiUndoCommand *command);
	static void CopyDependencyGraph(const QVector<Node *> &src,
									const QVector<Node *> &dst,
									MultiUndoCommand *command);

	static Node *
	CopyNodeAndDependencyGraphMinusItems(Node *node, MultiUndoCommand *command);

	static Node *CopyNodeInGraph(Node *node, MultiUndoCommand *command);

	/**
   * @brief The main processing function
   *
   * The node's main purpose is to take values from inputs to set values in outputs. For whatever subclass node you
   * create, this is where the code for that goes.
   *
   * Note that as a video editor, the node graph has to work across time. Depending on the purpose of your node, it may
   * output different values depending on the time, and even if not, it will likely be receiving different input
   * depending on the time. Most of the difficult work here is handled by NodeInput::get_value() which you should pass
   * the `time` parameter to. It will return its value (at that time, if it's keyframed), or pass the time to a
   * corresponding output if it's connected to one. If your node doesn't directly deal with time, the default behavior
   * of the NodeParam objects will handle everything related to it automatically.
   */
	virtual void Value(const NodeValueRow &value, const NodeGlobals &globals,
					   NodeValueTable *table) const;

	bool HasGizmos() const
	{
		return !gizmos_.isEmpty();
	}

	const QVector<NodeGizmo *> &GetGizmos() const
	{
		return gizmos_;
	}

	virtual QTransform GizmoTransformation(const NodeValueRow &row,
										   const NodeGlobals &globals) const
	{
		return QTransform();
	}

	virtual void UpdateGizmoPositions(const NodeValueRow &row,
									  const NodeGlobals &globals)
	{
	}

	const QString &GetLabel() const;
	void SetLabel(const QString &s);

	QString GetLabelAndName() const;
	QString GetLabelOrName() const;

	void InvalidateAll(const QString &input, int element = -1);

	bool HasLinks() const
	{
		return !links_.isEmpty();
	}

	const QVector<Node *> &links() const
	{
		return links_;
	}

	static bool Link(Node *a, Node *b);
	static bool Unlink(Node *a, Node *b);
	static bool AreLinked(Node *a, Node *b);

	bool Load(QXmlStreamReader *reader, SerializedData *data);
	void Save(QXmlStreamWriter *writer) const;

	virtual bool LoadCustom(QXmlStreamReader *reader, SerializedData *data);
	virtual void SaveCustom(QXmlStreamWriter *writer) const
	{
	}
	virtual void PostLoadEvent(SerializedData *data);

	bool LoadInput(QXmlStreamReader *reader, SerializedData *data);
	void SaveInput(QXmlStreamWriter *writer, const QString &id) const;

	bool LoadImmediate(QXmlStreamReader *reader, const QString &input,
					   int element, SerializedData *data);
	void SaveImmediate(QXmlStreamWriter *writer, const QString &input,
					   int element) const;

	void SetFolder(Folder *folder)
	{
		folder_ = folder;
	}

	InputFlags GetInputFlags(const QString &input) const;
	void SetInputFlag(const QString &input, InputFlag f, bool on = true);

	virtual void LoadFinishedEvent()
	{
	}
	virtual void ConnectedToPreviewEvent()
	{
	}

	static void SetValueAtTime(const NodeInput &input, const rational &time,
							   const QVariant &value, int track,
							   MultiUndoCommand *command,
							   bool insert_on_all_tracks_if_no_key);

	/**
   * @brief Find path starting at `from` that outputs to arrive at `to`
   */
	static std::list<NodeInput> FindPath(Node *from, Node *to, int path_index);

	void ArrayResizeInternal(const QString &id, int size);

	virtual void AddedToGraphEvent(Project *p)
	{
	}
	virtual void RemovedFromGraphEvent(Project *p)
	{
	}

	static QString GetConnectCommandString(Node *output,
										   const NodeInput &input);
	static QString GetDisconnectCommandString(Node *output,
											  const NodeInput &input);

	static const QString kEnabledInput;

	OFX::Host::ImageEffect::Instance* getPluginInstance() const
	{
		return plugin_instance_;
	}
	OFX::Host::ImageEffect::ImageEffectPlugin* getPlugin() const
	{
		return plugin_instance_ ? plugin_instance_->getPlugin() : nullptr;
	}
protected:

	// If set, this node owns a plugin instance.
	OFX::Host::ImageEffect::Instance* plugin_instance_ = nullptr;

	void setPluginInstance(OFX::Host::ImageEffect::Instance* instance)
	{
		plugin_instance_ = instance;
	}

	void InsertInput(const QString &id, NodeValue::Type type,
					 const QVariant &default_value, InputFlags flags,
					 int index);

	void PrependInput(const QString &id, NodeValue::Type type,
					  const QVariant &default_value,
					  InputFlags flags = InputFlags(kInputFlagNormal))
	{
		InsertInput(id, type, default_value, flags, 0);
	}

	void PrependInput(const QString &id, NodeValue::Type type,
					  InputFlags flags = InputFlags(kInputFlagNormal))
	{
		PrependInput(id, type, QVariant(), flags);
	}

	void AddInput(const QString &id, NodeValue::Type type,
				  const QVariant &default_value,
				  InputFlags flags = InputFlags(kInputFlagNormal))
	{
		InsertInput(id, type, default_value, flags, input_ids_.size());
	}

	void AddInput(const QString &id, NodeValue::Type type,
				  InputFlags flags = InputFlags(kInputFlagNormal))
	{
		AddInput(id, type, QVariant(), flags);
	}

	void RemoveInput(const QString &id);

	void SetComboBoxStrings(const QString &id, const QStringList &strings)
	{
		SetInputProperty(id, QStringLiteral("combo_str"), strings);
	}

	void SendInvalidateCache(const TimeRange &range,
							 const InvalidateCacheOptions &options);

	enum GizmoScaleHandles {
		kGizmoScaleTopLeft,
		kGizmoScaleTopCenter,
		kGizmoScaleTopRight,
		kGizmoScaleBottomLeft,
		kGizmoScaleBottomCenter,
		kGizmoScaleBottomRight,
		kGizmoScaleCenterLeft,
		kGizmoScaleCenterRight,
		kGizmoScaleCount,
	};

	virtual void LinkChangeEvent()
	{
	}

	virtual void InputValueChangedEvent(const QString &input, int element);

	virtual void InputConnectedEvent(const QString &input, int element,
									 Node *output);

	virtual void InputDisconnectedEvent(const QString &input, int element,
										Node *output);

	virtual void OutputConnectedEvent(const NodeInput &input);

	virtual void OutputDisconnectedEvent(const NodeInput &input);

	virtual void childEvent(QChildEvent *event) override;

	void SetEffectInput(const QString &input)
	{
		effect_input_ = input;
	}

	void SetFlag(Flag f, bool on = true)
	{
		if (on) {
			flags_ |= f;
		} else {
			flags_ &= ~f;
		}
	}

	template <typename T>
	T *AddDraggableGizmo(const QVector<NodeKeyframeTrackReference> &inputs =
							 QVector<NodeKeyframeTrackReference>(),
						 DraggableGizmo::DragValueBehavior behavior =
							 DraggableGizmo::kDeltaFromStart)
	{
		T *gizmo = new T(this);
		gizmo->SetDragValueBehavior(behavior);
		foreach (const NodeKeyframeTrackReference &input, inputs) {
			gizmo->AddInput(input);
		}
		connect(gizmo, &DraggableGizmo::HandleStart, this,
				&Node::GizmoDragStart);
		connect(gizmo, &DraggableGizmo::HandleMovement, this,
				&Node::GizmoDragMove);
		return gizmo;
	}

	template <typename T>
	T *AddDraggableGizmo(const QStringList &inputs,
						 DraggableGizmo::DragValueBehavior behavior =
							 DraggableGizmo::kDeltaFromStart)
	{
		QVector<NodeKeyframeTrackReference> refs(inputs.size());
		for (int i = 0; i < refs.size(); i++) {
			refs[i] = NodeInput(this, inputs[i]);
		}
		return AddDraggableGizmo<T>(refs, behavior);
	}

protected slots:
	virtual void GizmoDragStart(const olive::NodeValueRow &row, double x,
								double y, const olive::core::rational &time)
	{
	}

	virtual void GizmoDragMove(double x, double y,
							   const Qt::KeyboardModifiers &modifiers)
	{
	}

signals:
	/**
   * @brief Signal emitted when SetLabel() is called
   */
	void LabelChanged(const QString &s);

	void ColorChanged();

	void ValueChanged(const NodeInput &input, const TimeRange &range);

	void InputConnected(Node *output, const NodeInput &input);

	void InputDisconnected(Node *output, const NodeInput &input);

	void OutputConnected(Node *output, const NodeInput &input);

	void OutputDisconnected(Node *output, const NodeInput &input);

	void InputValueHintChanged(const NodeInput &input);

	void InputPropertyChanged(const QString &input, const QString &key,
							  const QVariant &value);

	void LinksChanged();

	void InputArraySizeChanged(const QString &input, int old_size,
							   int new_size);

	void KeyframeAdded(NodeKeyframe *key);

	void KeyframeRemoved(NodeKeyframe *key);

	void KeyframeTimeChanged(NodeKeyframe *key);

	void MessageCountChanged();

	void KeyframeTypeChanged(NodeKeyframe *key);

	void KeyframeValueChanged(NodeKeyframe *key);

	void KeyframeEnableChanged(const NodeInput &input, bool enabled);

	void InputAdded(const QString &id);

	void InputRemoved(const QString &id);

	void InputNameChanged(const QString &id, const QString &name);

	void InputDataTypeChanged(const QString &id, NodeValue::Type type);

	void AddedToGraph(Project *graph);

	void RemovedFromGraph(Project *graph);

	void NodeAddedToContext(Node *node);

	void NodePositionInContextChanged(Node *node, const QPointF &pos);

	void NodeRemovedFromContext(Node *node);

	void InputFlagsChanged(const QString &input, const InputFlags &flags);

private:
	struct Input {
		NodeValue::Type type;
		InputFlags flags;
		SplitValue default_value;
		QHash<QString, QVariant> properties;
		QString human_name;
		int array_size;
	};

	NodeInputImmediate *CreateImmediate(const QString &input);

	int GetInternalInputIndex(const QString &input) const
	{
		return input_ids_.indexOf(input);
	}

	Input *GetInternalInputData(const QString &input)
	{
		int i = GetInternalInputIndex(input);

		if (i == -1) {
			return nullptr;
		} else {
			return &input_data_[i];
		}
	}

	const Input *GetInternalInputData(const QString &input) const
	{
		int i = GetInternalInputIndex(input);

		if (i == -1) {
			return nullptr;
		} else {
			return &input_data_.at(i);
		}
	}

	void ReportInvalidInput(const char *attempted_action, const QString &id,
							int element) const;

	static Node *CopyNodeAndDependencyGraphMinusItemsInternal(
		QMap<Node *, Node *> &created, Node *node, MultiUndoCommand *command);

	/**
   * @brief Immediates aren't deleted, so the actual array size may be larger than ArraySize()
   */
	int GetInternalInputArraySize(const QString &input);

	/**
   * @brief Find nodes of a certain type that this Node takes inputs from
   */
	template <class T>
	static void FindInputNodesConnectedToInputInternal(const NodeInput &input,
													   QVector<T *> &list,
													   int maximum);

	template <class T>
	static void FindInputNodeInternal(const Node *n, QVector<T *> &list,
									  int maximum);

	QVector<Node *> GetDependenciesInternal(bool traverse,
											bool exclusive_only) const;

	void ParameterValueChanged(const QString &input, int element,
							   const olive::core::TimeRange &range);
	void ParameterValueChanged(const NodeInput &input,
							   const olive::core::TimeRange &range)
	{
		ParameterValueChanged(input.input(), input.element(), range);
	}

	/**
   * @brief Intelligently determine how what time range is affected by a keyframe
   */
	TimeRange GetRangeAffectedByKeyframe(NodeKeyframe *key) const;

	/**
   * @brief Gets a time range between the previous and next keyframes of index
   */
	TimeRange GetRangeAroundIndex(const QString &input, int index, int track,
								  int element) const;

	void ClearElement(const QString &input, int index);

	/**
   * @brief Custom user label for node
   */
	QString label_;

	/**
   * @brief -1 if the color should be based on the category, >=0 if the user has set a custom color
   */
	int override_color_;

	/**
   * @brief Nodes that are linked with this one
   */
	QVector<Node *> links_;

	QVector<QString> input_ids_;
	QVector<Input> input_data_;

	QMap<QString, NodeInputImmediate *> standard_immediates_;

	QMap<QString, QVector<NodeInputImmediate *>> array_immediates_;

	InputConnections input_connections_;

	OutputConnections output_connections_;

	Folder *folder_;

	QMap<InputElementPair, ValueHint> value_hints_;

	PositionMap context_positions_;

	uint64_t flags_;

	QVector<NodeGizmo *> gizmos_;

	QString effect_input_;

	FrameHashCache *video_cache_;
	ThumbnailCache *thumbnail_cache_;

	AudioPlaybackCache *audio_cache_;
	AudioWaveformCache *waveform_cache_;

	bool caches_enabled_;

private slots:
	/**
   * @brief Slot when a keyframe's time changes to keep the keyframes correctly sorted by time
   */
	void InvalidateFromKeyframeTimeChange();

	/**
   * @brief Slot when a keyframe's value changes to signal that the cache needs updating
   */
	void InvalidateFromKeyframeValueChange();

	/**
   * @brief Slot when a keyframe's type changes to signal that the cache needs updating
   */
	void InvalidateFromKeyframeTypeChanged();

	/**
   * @brief Slot when a keyframe's bezier in value changes to signal that the cache needs updating
   */
	void InvalidateFromKeyframeBezierInChange();

	/**
   * @brief Slot when a keyframe's bezier out value changes to signal that the cache needs updating
   */
	void InvalidateFromKeyframeBezierOutChange();
};

template <class T>
void Node::FindInputNodesConnectedToInputInternal(const NodeInput &input,
												  QVector<T *> &list,
												  int maximum)
{
	Node *edge = input.GetConnectedOutput();
	if (!edge) {
		return;
	}

	T *cast_test = dynamic_cast<T *>(edge);

	if (cast_test) {
		list.append(cast_test);
		if (maximum != 0 && list.size() == maximum) {
			return;
		}
	}

	FindInputNodeInternal<T>(edge, list, maximum);
}

template <class T>
QVector<T *> Node::FindInputNodesConnectedToInput(const NodeInput &input,
												  int maximum)
{
	QVector<T *> list;

	FindInputNodesConnectedToInputInternal<T>(input, list, maximum);

	return list;
}

template <class T>
void Node::FindInputNodeInternal(const Node *n, QVector<T *> &list, int maximum)
{
	for (auto it = n->input_connections_.cbegin();
		 it != n->input_connections_.cend(); it++) {
		FindInputNodesConnectedToInputInternal(it->first, list, maximum);
	}
}

template <class T> QVector<T *> Node::FindInputNodes(int maximum) const
{
	QVector<T *> list;

	FindInputNodeInternal<T>(this, list, maximum);

	return list;
}

}

Q_DECLARE_METATYPE(olive::Node::ValueHint)

#endif // NODE_H
