# MIT LICENSE
#
# Copyright 2022 Anatol Bogun
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
# associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


class_name Smoother extends Node

## Smoother Node
## Version: 1.0.4
##
## A node type that smoothes scene nodes' properties by interpolating _physics_process steps.
##
## For documentation please visit https://github.com/anatolbogun/godot-smoother-node .

## Node properties that are interpolated.
## Defaults to ["position"], even if not displayed in the inspector.
@export var properties:Array[String] = ["position"]

## Apply interpolation to this node's parent.
@export var smooth_parent: = true :
	set (value):
		if value == false:
			# remove parent from _properties in case this gets toggled on and off during runtime
			_properties.erase(get_parent())

		smooth_parent = value

## Apply interpolation to the recursive children of this node's parent.
@export var recursive: = true

## Explicitly include node paths in addition to the nodes that are included by other Smoother
## settings.
@export var includes:Array[NodePath] = []

## Explicitly exclude node paths.
## This will exclude nodes that would otherwise be included by other settings.
@export var excludes:Array[NodePath] = []

# get an array of all currently smoothed nodes; mainly for debugging performance optimisations
var smoothed_nodes:Array[Node] :
	get:
		var parent: = get_parent()
		return _get_physics_process_nodes(parent, !smooth_parent) if parent != null else [] as Array[Node]

var _properties: = {}
var _physics_process_nodes:Array[Node]
var _physics_process_just_updated: = false


## Reset all smoothed nodes.
func reset() -> void:
	_properties.clear()


## Reset a specific node. You may want to call this when a node gets teleported.
func reset_node(node:Node) -> void:
	_properties.erase(node)


## Reset a specific Node by NodePath. You may want to call this when a Node gets teleported.
func reset_node_path(path:NodePath) -> void:
	var node: = get_node_or_null(path)

	if node != null:
		reset_node(node)


## Add a Node to the includes Array[NodePath].
func add_include_node(node:Node) -> Array[NodePath]:
	return add_include_path(get_path_to(node))


## Add a NodePath to the includes Array[NodePath].
func add_include_path(path:NodePath) -> Array[NodePath]:
	return _add_unique_to_array(includes, path) as Array[NodePath]


## Remove a Node from the includes Array[NodePath].
func remove_include_node(node:Node) -> Array[NodePath]:
	return remove_include_path(get_path_to(node))


## Remove a NodePath from the includes Array[NodePath].
func remove_include_path(path:NodePath) -> Array[NodePath]:
	return _remove_all_from_array(includes, path) as Array[NodePath]


## Add a Node to the excludes Array[NodePath].
func add_exclude_node(node:Node) -> Array[NodePath]:
	return add_exclude_path(get_path_to(node))


## Add a NodePath to the excludes Array[NodePath].
func add_exclude_path(path:NodePath) -> Array[NodePath]:
	return _add_unique_to_array(excludes, path) as Array[NodePath]


## Remove a Node from the excludes Array[NodePath].
func remove_exclude_node(node:Node) -> Array[NodePath]:
	return remove_exclude_path(get_path_to(node))


## Remove a NodePath from the excludes Array[NodePath].
func remove_exclude_path(path:NodePath) -> Array[NodePath]:
	return _remove_all_from_array(excludes, path) as Array[NodePath]


## Add an item to an array unless the array already contains that item.
func _add_unique_to_array(array:Array, item:Variant) -> Array:
	if !array.has(item):
		array.push_back(item)

	return array


## Remove all array items that match item.
func _remove_all_from_array(array:Array, item:Variant) -> Array:
	while array.has(item):
		array.erase(item)

	return array


## Apply interpolation to all smoothed_nodes supported properties.
func _process(_delta: float) -> void:
	for node in _physics_process_nodes:
		if !_properties.has(node): continue

		for property in _properties[node]:
			var values = _properties[node][property]

			if values.size() == 2:
				if _physics_process_just_updated:
					values[1] = node[property]

				node[property] = lerp(values[0], values[1], Engine.get_physics_interpolation_fraction())

	_physics_process_just_updated = false


## Store all smoothed_nodes' relevant properties of the previous (origin) and this (target)
## _physics_process frames for interpolation in the upcoming _process frames and apply the origin
## values.
func _physics_process(_delta: float) -> void:
	var parent: = get_parent()
	if parent == null: return

	# move this node to the top of the parent tree (typically a scene's root node) so that it is
	# called before all other _physics_processes
	parent.move_child(self, 0)

	if smooth_parent:
		process_priority = parent.process_priority - 1

	# update the relevant nodes once per _physics_process
	_physics_process_nodes = _get_physics_process_nodes(parent, !smooth_parent)

	# clean up _properties
	for key in _properties.keys():
		if !_physics_process_nodes.has(key):
			_properties.erase(key)

	for node in _physics_process_nodes:
		if !_properties.has(node):
			# called on the first frame after a node was added to _properties
			_properties[node] = {}

			# clean up _properties when a node exited the tree
			node.tree_exited.connect(func (): _properties.erase(node))

		for property in properties:
			if ! property in node: continue

			if !_properties[node].has(property):
				# called on the first frame after a node was added to _properties
				_properties[node][property] = [node[property]]
			elif _properties[node][property].size() < 2:
				# called on the second frame after a node was added to _properties
				_properties[node][property].push_front(_properties[node][property][0])
				_properties[node][property][1] = node[property]
			else:
				_properties[node][property][0] = _properties[node][property][1]
				node[property] = _properties[node][property][0]

	_physics_process_just_updated = true


## Get the relevant nodes to be smoothed based on this node's tree position and properties.
func _get_physics_process_nodes(node: Node, ignore_node: = false, with_includes: = true) -> Array[Node]:
	var nodes:Array[Node] = []

	nodes.assign(includes.map(
		get_node_or_null
	).filter(
		func (_node:Node) -> bool: return _node != null && !excludes.has(get_path_to(_node))
	) if with_includes else [])

	if (
		!ignore_node
		&& node != self
		&& !node is RigidBody2D
		&& !node is RigidBody3D
		&& !nodes.has(node)
		&& !excludes.has(get_path_to(node))
		&& node.has_method("_physics_process")
	):
		nodes.push_back(node)

	if recursive:
		for child in node.get_children():
			for nested_node in _get_physics_process_nodes(child, false, false):
				_add_unique_to_array(nodes, nested_node)

	return nodes
