//
//  AnimatorNode.swift
//  lottie-swift
//
//  Created by Brandon Withrow on 1/15/19.
//

import QuartzCore

// MARK: - NodeOutput

/// Defines the basic outputs of an animator node.
///
protocol NodeOutput {

  /// The parent node.
  var parent: NodeOutput? { get }

  var outputPath: CGPath? { get }

  var isEnabled: Bool { get set }

  /// Returns true if there are any updates upstream. OutputPath must be built before returning.
  func hasOutputUpdates(_ forFrame: CGFloat) -> Bool

}

// MARK: - AnimatorNode

/// The Animator Node is the base node in the render system tree.
///
/// It defines a single node that has an output path and option input node.
/// At animation time the root animation node is asked to update its contents for
/// the current frame.
/// The node reaches up its chain of nodes until the first node that does not need
/// updating is found. Then each node updates its contents down the render pipeline.
/// Each node adds its local path to its input path and passes it forward.
///
/// An animator node holds a group of interpolators. These interpolators determine
/// if the node needs an update for the current frame.
///
protocol AnimatorNode: AnyObject, KeypathSearchable {

  /// The available properties of the Node.
  ///
  /// These properties are automatically updated each frame.
  /// These properties are also settable and gettable through the dynamic
  /// property system.
  ///
  var propertyMap: NodePropertyMap & KeypathSearchable { get }

  /// The upstream input node
  var parentNode: AnimatorNode? { get }

  /// The output of the node.
  var outputNode: NodeOutput { get }

  /// Setters for marking current node state.
  var isEnabled: Bool { get set }
  var hasLocalUpdates: Bool { get set }
  var hasUpstreamUpdates: Bool { get set }
  var lastUpdateFrame: CGFloat? { get set }

  /// Update the outputs of the node. Called if local contents were update or if outputsNeedUpdate returns true.
  func rebuildOutputs(frame: CGFloat)

  /// Marks if updates to this node affect nodes downstream.
  func localUpdatesPermeateDownstream() -> Bool
  func forceUpstreamOutputUpdates() -> Bool

  /// Called at the end of this nodes update cycle. Always called. Optional.
  func performAdditionalLocalUpdates(frame: CGFloat, forceLocalUpdate: Bool) -> Bool
  func performAdditionalOutputUpdates(_ frame: CGFloat, forceOutputUpdate: Bool)

  /// The default simply returns `hasLocalUpdates`
  func shouldRebuildOutputs(frame: CGFloat) -> Bool
}

/// Basic Node Logic
extension AnimatorNode {

  func shouldRebuildOutputs(frame _: CGFloat) -> Bool {
    hasLocalUpdates
  }

  func localUpdatesPermeateDownstream() -> Bool {
    /// Optional override
    true
  }

  func forceUpstreamOutputUpdates() -> Bool {
    /// Optional
    false
  }

  func performAdditionalLocalUpdates(frame _: CGFloat, forceLocalUpdate: Bool) -> Bool {
    /// Optional
    forceLocalUpdate
  }

  func performAdditionalOutputUpdates(_: CGFloat, forceOutputUpdate _: Bool) {
    /// Optional
  }

  @discardableResult
  func updateOutputs(_ frame: CGFloat, forceOutputUpdate: Bool) -> Bool {
    guard isEnabled else {
      // Disabled node, pass through.
      lastUpdateFrame = frame
      return parentNode?.updateOutputs(frame, forceOutputUpdate: forceOutputUpdate) ?? false
    }

    if forceOutputUpdate == false && lastUpdateFrame != nil && lastUpdateFrame! == frame {
      /// This node has already updated for this frame. Go ahead and return the results.
      return hasUpstreamUpdates || hasLocalUpdates
    }

    /// Ask if this node should force output updates upstream.
    let forceUpstreamUpdates = forceOutputUpdate || forceUpstreamOutputUpdates()

    /// Perform upstream output updates. Optionally mark upstream updates if any.
    hasUpstreamUpdates = (
      parentNode?
        .updateOutputs(frame, forceOutputUpdate: forceUpstreamUpdates) ?? false || hasUpstreamUpdates
    )

    /// Perform additional local output updates
    performAdditionalOutputUpdates(frame, forceOutputUpdate: forceUpstreamUpdates)

    /// If there are local updates, or if updates have been force, rebuild outputs
    if forceUpstreamUpdates || shouldRebuildOutputs(frame: frame) {
      lastUpdateFrame = frame
      rebuildOutputs(frame: frame)
    }
    return hasUpstreamUpdates || hasLocalUpdates
  }

  /// Rebuilds the content of this node, and upstream nodes if necessary.
  @discardableResult
  func updateContents(_ frame: CGFloat, forceLocalUpdate: Bool) -> Bool {
    guard isEnabled else {
      // Disabled node, pass through.
      return parentNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false
    }

    if forceLocalUpdate == false && lastUpdateFrame != nil && lastUpdateFrame! == frame {
      /// This node has already updated for this frame. Go ahead and return the results.
      return localUpdatesPermeateDownstream() ? hasUpstreamUpdates || hasLocalUpdates : hasUpstreamUpdates
    }

    /// Are there local updates? If so mark the node.
    hasLocalUpdates = forceLocalUpdate ? forceLocalUpdate : propertyMap.needsLocalUpdate(frame: frame)

    /// Were there upstream updates? If so mark the node
    hasUpstreamUpdates = parentNode?.updateContents(frame, forceLocalUpdate: forceLocalUpdate) ?? false

    /// Perform property updates if necessary.
    if hasLocalUpdates {
      /// Rebuild local properties
      propertyMap.updateNodeProperties(frame: frame)
    }

    /// Ask the node to perform any other updates it might have.
    hasUpstreamUpdates = performAdditionalLocalUpdates(frame: frame, forceLocalUpdate: forceLocalUpdate) || hasUpstreamUpdates

    /// If the node can update nodes downstream, notify them, otherwise pass on any upstream updates downstream.
    return localUpdatesPermeateDownstream() ? hasUpstreamUpdates || hasLocalUpdates : hasUpstreamUpdates
  }

  func updateTree(_ frame: CGFloat, forceUpdates: Bool = false) {
    updateContents(frame, forceLocalUpdate: forceUpdates)
    updateOutputs(frame, forceOutputUpdate: forceUpdates)
  }

}

extension AnimatorNode {
  /// Default implementation for Keypath searchable.
  /// Forward all calls to the propertyMap.

  var keypathName: String {
    propertyMap.keypathName
  }

  var keypathProperties: [String: AnyNodeProperty] {
    propertyMap.keypathProperties
  }

  var childKeypaths: [KeypathSearchable] {
    propertyMap.childKeypaths
  }

  var keypathLayer: CALayer? {
    nil
  }

}
