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

import Foundation
import QuartzCore

// MARK: - NodeOutput

/**
 Defines the basic outputs of an animator node.

 */
protocol NodeOutput {

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

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

  var outputPath: CGPath? { get }

  var isEnabled: Bool { get set }
}

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

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

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

  // MARK: Optional

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

}
