//
//  LottieAnimationView.swift
//  lottie-swift
//
//  Created by Brandon Withrow on 1/23/19.
//

import QuartzCore

// MARK: - LottieBackgroundBehavior

/// Describes the behavior of an AnimationView when the app is moved to the background.
public enum LottieBackgroundBehavior {
  /// Stop the animation and reset it to the beginning of its current play time. The completion block is called.
  case stop

  /// Pause the animation in its current state. The completion block is called.
  case pause

  /// Pause the animation and restart it when the application moves to the foreground.
  /// The completion block is stored and called when the animation completes.
  ///  - This is the default when using the Main Thread rendering engine.
  case pauseAndRestore

  /// Stops the animation and sets it to the end of its current play time. The completion block is called.
  case forceFinish

  /// The animation continues playing in the background.
  ///  - This is the default when using the Core Animation rendering engine.
  ///    Playing an animation using the Core Animation engine doesn't come with any CPU overhead,
  ///    so using `.continuePlaying` avoids the need to stop and then resume the animation
  ///    (which does come with some CPU overhead).
  ///  - This mode should not be used with the Main Thread rendering engine.
  case continuePlaying

  // MARK: Public

  /// The default background behavior, based on the rendering engine being used to play the animation.
  ///  - Playing an animation using the Main Thread rendering engine comes with CPU overhead,
  ///    so the animation should be paused or stopped when the `LottieAnimationView` is not visible.
  ///  - Playing an animation using the Core Animation rendering engine does not come with any
  ///    CPU overhead, so these animations do not need to be paused in the background.
  public static func `default`(for renderingEngine: RenderingEngine) -> LottieBackgroundBehavior {
    switch renderingEngine {
    case .mainThread:
      .pauseAndRestore
    case .coreAnimation:
      .continuePlaying
    }
  }
}

// MARK: - LottieLoopMode

/// Defines animation loop behavior
public enum LottieLoopMode: Hashable {
  /// Animation is played once then stops.
  case playOnce
  /// Animation will loop from beginning to end until stopped.
  case loop
  /// Animation will play forward, then backwards and loop until stopped.
  case autoReverse
  /// Animation will loop from beginning to end up to defined amount of times.
  case `repeat`(Float)
  /// Animation will play forward, then backwards a defined amount of times.
  case repeatBackwards(Float)
}

// MARK: Equatable

extension LottieLoopMode: Equatable {
  public static func ==(lhs: LottieLoopMode, rhs: LottieLoopMode) -> Bool {
    switch (lhs, rhs) {
    case (.repeat(let lhsAmount), .repeat(let rhsAmount)),
         (.repeatBackwards(let lhsAmount), .repeatBackwards(let rhsAmount)):
      lhsAmount == rhsAmount
    case (.playOnce, .playOnce),
         (.loop, .loop),
         (.autoReverse, .autoReverse):
      true
    default:
      false
    }
  }
}

// MARK: - LottieAnimationView

/// A UIView subclass for rendering Lottie animations.
///  - Also available as a SwiftUI view (`LottieView`) and a CALayer subclass (`LottieAnimationLayer`)
@IBDesignable
open class LottieAnimationView: LottieAnimationViewBase {

  // MARK: Lifecycle

  /// Initializes an AnimationView with an animation.
  public init(
    animation: LottieAnimation?,
    imageProvider: AnimationImageProvider? = nil,
    textProvider: AnimationKeypathTextProvider = DefaultTextProvider(),
    fontProvider: AnimationFontProvider = DefaultFontProvider(),
    configuration: LottieConfiguration = .shared,
    logger: LottieLogger = .shared
  ) {
    lottieAnimationLayer = LottieAnimationLayer(
      animation: animation,
      imageProvider: imageProvider,
      textProvider: textProvider,
      fontProvider: fontProvider,
      configuration: configuration,
      logger: logger
    )
    self.logger = logger
    super.init(frame: .zero)
    commonInit()
    if let animation {
      frame = animation.bounds
    }
  }

  /// Initializes an AnimationView with a .lottie file.
  public init(
    dotLottie: DotLottieFile?,
    animationId: String? = nil,
    textProvider: AnimationKeypathTextProvider = DefaultTextProvider(),
    fontProvider: AnimationFontProvider = DefaultFontProvider(),
    configuration: LottieConfiguration = .shared,
    logger: LottieLogger = .shared
  ) {
    lottieAnimationLayer = LottieAnimationLayer(
      dotLottie: dotLottie,
      animationId: animationId,
      textProvider: textProvider,
      fontProvider: fontProvider,
      configuration: configuration,
      logger: logger
    )
    self.logger = logger
    super.init(frame: .zero)
    commonInit()
    if let animation {
      frame = animation.bounds
    }
  }

  public init(
    configuration: LottieConfiguration = .shared,
    logger: LottieLogger = .shared
  ) {
    lottieAnimationLayer = LottieAnimationLayer(configuration: configuration, logger: logger)
    self.logger = logger
    super.init(frame: .zero)
    commonInit()
  }

  public override init(frame: CGRect) {
    lottieAnimationLayer = LottieAnimationLayer(
      animation: nil,
      imageProvider: BundleImageProvider(bundle: Bundle.main, searchPath: nil),
      textProvider: DefaultTextProvider(),
      fontProvider: DefaultFontProvider(),
      configuration: .shared,
      logger: .shared
    )
    logger = .shared
    super.init(frame: frame)
    commonInit()
  }

  required public init?(coder aDecoder: NSCoder) {
    lottieAnimationLayer = LottieAnimationLayer(
      animation: nil,
      imageProvider: BundleImageProvider(bundle: Bundle.main, searchPath: nil),
      textProvider: DefaultTextProvider(),
      fontProvider: DefaultFontProvider(),
      configuration: .shared,
      logger: .shared
    )
    logger = .shared
    super.init(coder: aDecoder)
    commonInit()
  }

  convenience init(
    animationSource: LottieAnimationSource?,
    imageProvider: AnimationImageProvider? = nil,
    textProvider: AnimationKeypathTextProvider = DefaultTextProvider(),
    fontProvider: AnimationFontProvider = DefaultFontProvider(),
    configuration: LottieConfiguration = .shared,
    logger: LottieLogger = .shared
  ) {
    switch animationSource {
    case .lottieAnimation(let animation):
      self.init(
        animation: animation,
        imageProvider: imageProvider,
        textProvider: textProvider,
        fontProvider: fontProvider,
        configuration: configuration,
        logger: logger
      )

    case .dotLottieFile(let dotLottieFile):
      self.init(
        dotLottie: dotLottieFile,
        textProvider: textProvider,
        fontProvider: fontProvider,
        configuration: configuration,
        logger: logger
      )

    case nil:
      self.init(
        animation: nil,
        imageProvider: imageProvider,
        textProvider: textProvider,
        fontProvider: fontProvider,
        configuration: configuration,
        logger: logger
      )
    }
  }

  // MARK: Open

  /// Applies the given `LottiePlaybackMode` to this layer.
  /// - Parameter completion: A closure that is called after
  ///   an animation triggered by this method completes.
  open func play(_ mode: LottiePlaybackMode.PlaybackMode, completion: LottieCompletionBlock? = nil) {
    lottieAnimationLayer.play(mode, completion: completion)
  }

  /// Plays the animation from its current state to the end.
  ///
  /// - Parameter completion: An optional completion closure to be called when the animation completes playing.
  open func play(completion: LottieCompletionBlock? = nil) {
    lottieAnimationLayer.play(completion: completion)
  }

  /// Plays the animation from a progress (0-1) to a progress (0-1).
  ///
  /// - Parameter fromProgress: The start progress of the animation. If `nil` the animation will start at the current progress.
  /// - Parameter toProgress: The end progress of the animation.
  /// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
  /// - Parameter completion: An optional completion closure to be called when the animation stops.
  open func play(
    fromProgress: AnimationProgressTime? = nil,
    toProgress: AnimationProgressTime,
    loopMode: LottieLoopMode? = nil,
    completion: LottieCompletionBlock? = nil
  ) {
    lottieAnimationLayer.play(fromProgress: fromProgress, toProgress: toProgress, loopMode: loopMode, completion: completion)
  }

  /// Plays the animation from a start frame to an end frame in the animation's framerate.
  ///
  /// - Parameter fromFrame: The start frame of the animation. If `nil` the animation will start at the current frame.
  /// - Parameter toFrame: The end frame of the animation.
  /// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
  /// - Parameter completion: An optional completion closure to be called when the animation stops.
  open func play(
    fromFrame: AnimationFrameTime? = nil,
    toFrame: AnimationFrameTime,
    loopMode: LottieLoopMode? = nil,
    completion: LottieCompletionBlock? = nil
  ) {
    lottieAnimationLayer.play(fromFrame: fromFrame, toFrame: toFrame, loopMode: loopMode, completion: completion)
  }

  /// Plays the animation from a named marker to another marker.
  ///
  /// Markers are point in time that are encoded into the Animation data and assigned
  /// a name.
  ///
  /// NOTE: If markers are not found the play command will exit.
  ///
  /// - Parameter fromMarker: The start marker for the animation playback. If `nil` the
  /// animation will start at the current progress.
  /// - Parameter toMarker: The end marker for the animation playback.
  /// - Parameter playEndMarkerFrame: A flag to determine whether or not to play the frame of the end marker. If the
  /// end marker represents the end of the section to play, it should be to true. If the provided end marker
  /// represents the beginning of the next section, it should be false.
  /// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
  /// - Parameter completion: An optional completion closure to be called when the animation stops.
  open func play(
    fromMarker: String? = nil,
    toMarker: String,
    playEndMarkerFrame: Bool = true,
    loopMode: LottieLoopMode? = nil,
    completion: LottieCompletionBlock? = nil
  ) {
    lottieAnimationLayer.play(
      fromMarker: fromMarker,
      toMarker: toMarker,
      playEndMarkerFrame: playEndMarkerFrame,
      loopMode: loopMode,
      completion: completion
    )
  }

  /// Plays the animation from a named marker to the end of the marker's duration.
  ///
  /// A marker is a point in time with an associated duration that is encoded into the
  /// animation data and assigned a name.
  ///
  /// NOTE: If marker is not found the play command will exit.
  ///
  /// - Parameter marker: The start marker for the animation playback.
  /// - Parameter loopMode: The loop behavior of the animation. If `nil` the view's `loopMode` property will be used.
  /// - Parameter completion: An optional completion closure to be called when the animation stops.
  open func play(
    marker: String,
    loopMode: LottieLoopMode? = nil,
    completion: LottieCompletionBlock? = nil
  ) {
    lottieAnimationLayer.play(marker: marker, loopMode: loopMode, completion: completion)
  }

  /// Plays the given markers sequentially in order.
  ///
  /// A marker is a point in time with an associated duration that is encoded into the
  /// animation data and assigned a name. Multiple markers can be played sequentially
  /// to create programmable animations.
  ///
  /// If a marker is not found, it will be skipped.
  ///
  /// If a marker doesn't have a duration value, it will play with a duration of 0
  /// (effectively being skipped).
  ///
  /// If another animation is played (by calling any `play` method) while this
  /// marker sequence is playing, the marker sequence will be cancelled.
  ///
  /// - Parameter markers: The list of markers to play sequentially.
  /// - Parameter completion: An optional completion closure to be called when the animation stops.
  open func play(
    markers: [String],
    completion: LottieCompletionBlock? = nil
  ) {
    lottieAnimationLayer.play(markers: markers, completion: completion)
  }

  /// Stops the animation and resets the view to its start frame.
  ///
  /// The completion closure will be called with `false`
  open func stop() {
    lottieAnimationLayer.stop()
  }

  /// Pauses the animation in its current state.
  ///
  /// The completion closure will be called with `false`
  open func pause() {
    lottieAnimationLayer.pause()
  }

  @available(*, deprecated, renamed: "setPlaybackMode(_:completion:)", message: "Will be removed in a future major release.")
  open func play(
    _ playbackMode: LottiePlaybackMode,
    animationCompletionHandler: LottieCompletionBlock? = nil
  ) {
    lottieAnimationLayer.setPlaybackMode(playbackMode, completion: animationCompletionHandler)
  }

  /// Applies the given `LottiePlaybackMode` to this layer.
  /// - Parameter completion: A closure that is called after
  ///   an animation triggered by this method completes.
  open func setPlaybackMode(
    _ playbackMode: LottiePlaybackMode,
    completion: LottieCompletionBlock? = nil
  ) {
    lottieAnimationLayer.setPlaybackMode(playbackMode, completion: completion)
  }

  // MARK: Public

  /// Whether or not transform and position changes of the view should animate alongside
  /// any existing animation context.
  ///  - Defaults to `true` which will grab the current animation context and animate position and
  ///    transform changes matching the current context's curve and duration.
  ///    `false` will cause transform and position changes to happen unanimated
  public var animateLayoutChangesWithCurrentCoreAnimationContext = true

  /// The configuration that this `LottieAnimationView` uses when playing its animation
  public var configuration: LottieConfiguration {
    get { lottieAnimationLayer.configuration }
    set { lottieAnimationLayer.configuration = newValue }
  }

  /// Value Providers that have been registered using `setValueProvider(_:keypath:)`
  public var valueProviders: [AnimationKeypath: AnyValueProvider] {
    lottieAnimationLayer.valueProviders
  }

  /// Describes the behavior of an AnimationView when the app is moved to the background.
  ///
  /// The default for the Main Thread animation engine is `pause`,
  /// which pauses the animation when the application moves to
  /// the background. This prevents the animation from consuming CPU
  /// resources when not on-screen. The completion block is called with
  /// `false` for completed.
  ///
  /// The default for the Core Animation engine is `continuePlaying`,
  /// since the Core Animation engine does not have any CPU overhead.
  public var backgroundBehavior: LottieBackgroundBehavior {
    get { lottieAnimationLayer.backgroundBehavior }
    set { lottieAnimationLayer.backgroundBehavior = newValue }
  }

  /// Sets the animation backing the animation view. Setting this will clear the
  /// view's contents, completion blocks and current state. The new animation will
  /// be loaded up and set to the beginning of its timeline.
  public var animation: LottieAnimation? {
    get { lottieAnimationLayer.animation }
    set { lottieAnimationLayer.animation = newValue }
  }

  /// A closure that is called when `self.animation` is loaded. When setting this closure,
  /// it is called immediately if `self.animation` is non-nil.
  ///
  /// When initializing a `LottieAnimationView`, the animation will either be loaded
  /// synchronously (when loading a `LottieAnimation` from a .json file on disk)
  /// or asynchronously (when loading a `DotLottieFile` from disk, or downloading
  /// an animation from a URL). This closure is called in both cases once the
  /// animation is loaded and applied, so can be a useful way to configure this
  /// `LottieAnimationView` regardless of which initializer was used. For example:
  ///
  /// ```
  /// let animationView: LottieAnimationView
  ///
  /// if loadDotLottieFile {
  ///   // Loads the .lottie file asynchronously
  ///   animationView = LottieAnimationView(dotLottieName: "animation")
  /// } else {
  ///   // Loads the .json file synchronously
  ///   animationView = LottieAnimationView(name: "animation")
  /// }
  ///
  /// animationView.animationLoaded = { animationView, animation in
  ///   // If using a .lottie file, this is called once the file finishes loading.
  ///   // If using a .json file, this is called immediately (since the animation is loaded synchronously).
  ///   animationView.play()
  /// }
  /// ```
  public var animationLoaded: ((_ animationView: LottieAnimationView, _ animation: LottieAnimation) -> Void)? {
    didSet {
      if let animation {
        animationLoaded?(self, animation)
      }
    }
  }

  /// Sets the image provider for the animation view. An image provider provides the
  /// animation with its required image data.
  ///
  /// Setting this will cause the animation to reload its image contents.
  public var imageProvider: AnimationImageProvider {
    get { lottieAnimationLayer.imageProvider }
    set { lottieAnimationLayer.imageProvider = newValue }
  }

  /// Sets the text provider for animation view. A text provider provides the
  /// animation with values for text layers
  public var textProvider: AnimationKeypathTextProvider {
    get { lottieAnimationLayer.textProvider }
    set { lottieAnimationLayer.textProvider = newValue }
  }

  /// Sets the text provider for animation view. A text provider provides the
  /// animation with values for text layers
  public var fontProvider: AnimationFontProvider {
    get { lottieAnimationLayer.fontProvider }
    set { lottieAnimationLayer.fontProvider = newValue }
  }

  /// Whether or not the animation is masked to the bounds. Defaults to true.
  public var maskAnimationToBounds: Bool {
    get { lottieAnimationLayer.maskAnimationToBounds }
    set { lottieAnimationLayer.maskAnimationToBounds = newValue }
  }

  /// Returns `true` if the animation is currently playing.
  public var isAnimationPlaying: Bool {
    lottieAnimationLayer.isAnimationPlaying
  }

  /// Returns `true` if the animation will start playing when this view is added to a window.
  public var isAnimationQueued: Bool {
    lottieAnimationLayer.hasAnimationContext && waitingToPlayAnimation
  }

  /// Sets the loop behavior for `play` calls. Defaults to `playOnce`
  public var loopMode: LottieLoopMode {
    get { lottieAnimationLayer.loopMode }
    set { lottieAnimationLayer.loopMode = newValue }
  }

  /// When `true` the animation view will rasterize its contents when not animating.
  /// Rasterizing will improve performance of static animations.
  ///
  /// Note: this will not produce crisp results at resolutions above the animations natural resolution.
  ///
  /// Defaults to `false`
  public var shouldRasterizeWhenIdle: Bool {
    get { lottieAnimationLayer.shouldRasterizeWhenIdle }
    set { lottieAnimationLayer.shouldRasterizeWhenIdle = newValue }
  }

  /// Sets the current animation time with a Progress Time
  ///
  /// Note: Setting this will stop the current animation, if any.
  /// Note 2: If `animation` is nil, setting this will fallback to 0
  public var currentProgress: AnimationProgressTime {
    get { lottieAnimationLayer.currentProgress }
    set { lottieAnimationLayer.currentProgress = newValue }
  }

  /// Sets the current animation time with a time in seconds.
  ///
  /// Note: Setting this will stop the current animation, if any.
  /// Note 2: If `animation` is nil, setting this will fallback to 0
  public var currentTime: TimeInterval {
    get { lottieAnimationLayer.currentTime }
    set { lottieAnimationLayer.currentTime = newValue }
  }

  /// Sets the current animation time with a frame in the animations framerate.
  ///
  /// Note: Setting this will stop the current animation, if any.
  public var currentFrame: AnimationFrameTime {
    get { lottieAnimationLayer.currentFrame }
    set { lottieAnimationLayer.currentFrame = newValue }
  }

  /// Returns the current animation frame while an animation is playing.
  public var realtimeAnimationFrame: AnimationFrameTime {
    lottieAnimationLayer.realtimeAnimationFrame
  }

  /// Returns the current animation frame while an animation is playing.
  public var realtimeAnimationProgress: AnimationProgressTime {
    lottieAnimationLayer.realtimeAnimationProgress
  }

  /// Sets the speed of the animation playback. Defaults to 1
  public var animationSpeed: CGFloat {
    get { lottieAnimationLayer.animationSpeed }
    set { lottieAnimationLayer.animationSpeed = newValue }
  }

  /// When `true` the animation will play back at the framerate encoded in the
  /// `LottieAnimation` model. When `false` the animation will play at the framerate
  /// of the device.
  ///
  /// Defaults to false
  public var respectAnimationFrameRate: Bool {
    get { lottieAnimationLayer.respectAnimationFrameRate }
    set { lottieAnimationLayer.respectAnimationFrameRate = newValue }
  }

  /// Controls the cropping of an Animation. Setting this property will crop the animation
  /// to the current views bounds by the viewport frame. The coordinate space is specified
  /// in the animation's coordinate space.
  ///
  /// Animatable.
  public var viewportFrame: CGRect? {
    didSet {
      // This is really ugly, but is needed to trigger a layout pass within an animation block.
      // Typically this happens automatically, when layout objects are UIView based.
      // The animation layer is a CALayer which will not implicitly grab the animation
      // duration of a UIView animation block.
      //
      // By setting bounds and then resetting bounds the UIView animation block's
      // duration and curve are captured and added to the layer. This is used in the
      // layout block to animate the animationLayer's position and size.
      let rect = bounds
      bounds = CGRect.zero
      bounds = rect
      setNeedsLayout()
    }
  }

  override public var intrinsicContentSize: CGSize {
    if let animation = lottieAnimationLayer.animation {
      return animation.bounds.size
    }
    return .zero
  }

  /// The rendering engine currently being used by this view.
  ///  - This will only be `nil` in cases where the configuration is `automatic`
  ///    but a `RootAnimationLayer` hasn't been constructed yet
  public var currentRenderingEngine: RenderingEngine? {
    lottieAnimationLayer.currentRenderingEngine
  }

  /// The current `LottiePlaybackMode` that is being used
  public var currentPlaybackMode: LottiePlaybackMode? {
    lottieAnimationLayer.currentPlaybackMode
  }

  /// Whether or not the Main Thread rendering engine should use `forceDisplayUpdate()`
  /// when rendering each individual frame.
  ///  - The main thread rendering engine implements optimizations to decrease the amount
  ///    of properties that have to be re-rendered on each frame. There are some cases
  ///    where this can result in bugs / incorrect behavior, so we allow it to be disabled.
  ///  - Forcing a full render on every frame will decrease performance, and is not recommended
  ///    except as a workaround to a bug in the main thread rendering engine.
  ///  - Has no effect when using the Core Animation rendering engine.
  public var mainThreadRenderingEngineShouldForceDisplayUpdateOnEachFrame: Bool {
    get { lottieAnimationLayer.mainThreadRenderingEngineShouldForceDisplayUpdateOnEachFrame }
    set { lottieAnimationLayer.mainThreadRenderingEngineShouldForceDisplayUpdateOnEachFrame = newValue }
  }

  /// Sets the lottie file backing the animation view. Setting this will clear the
  /// view's contents, completion blocks and current state. The new animation will
  /// be loaded up and set to the beginning of its timeline.
  /// The loopMode, animationSpeed and imageProvider will be set according
  /// to lottie file settings
  /// - Parameters:
  ///   - animationId: Internal animation id to play. Optional
  ///   Defaults to play first animation in file.
  ///   - dotLottieFile: Lottie file to play
  public func loadAnimation(
    _ animationId: String? = nil,
    from dotLottieFile: DotLottieFile
  ) {
    lottieAnimationLayer.loadAnimation(animationId, from: dotLottieFile)
  }

  /// Sets the lottie file backing the animation view. Setting this will clear the
  /// view's contents, completion blocks and current state. The new animation will
  /// be loaded up and set to the beginning of its timeline.
  /// The loopMode, animationSpeed and imageProvider will be set according
  /// to lottie file settings
  /// - Parameters:
  ///   - atIndex: Internal animation index to play. Optional
  ///   Defaults to play first animation in file.
  ///   - dotLottieFile: Lottie file to play
  public func loadAnimation(
    atIndex index: Int,
    from dotLottieFile: DotLottieFile
  ) {
    lottieAnimationLayer.loadAnimation(atIndex: index, from: dotLottieFile)
  }

  /// Reloads the images supplied to the animation from the `imageProvider`
  public func reloadImages() {
    lottieAnimationLayer.reloadImages()
  }

  /// Forces the LottieAnimationView to redraw its contents.
  public func forceDisplayUpdate() {
    lottieAnimationLayer.forceDisplayUpdate()
  }

  /// Sets a ValueProvider for the specified keypath. The value provider will be set
  /// on all properties that match the keypath.
  ///
  /// Nearly all properties of a Lottie animation can be changed at runtime using a
  /// combination of `Animation Keypaths` and `Value Providers`.
  /// Setting a ValueProvider on a keypath will cause the animation to update its
  /// contents and read the new Value Provider.
  ///
  /// A value provider provides a typed value on a frame by frame basis.
  ///
  /// - Parameter valueProvider: The new value provider for the properties.
  /// - Parameter keypath: The keypath used to search for properties.
  ///
  /// Example:
  /// ```
  /// /// A keypath that finds the color value for all `Fill 1` nodes.
  /// let fillKeypath = AnimationKeypath(keypath: "**.Fill 1.Color")
  /// /// A Color Value provider that returns a reddish color.
  /// let redValueProvider = ColorValueProvider(Color(r: 1, g: 0.2, b: 0.3, a: 1))
  /// /// Set the provider on the animationView.
  /// animationView.setValueProvider(redValueProvider, keypath: fillKeypath)
  /// ```
  public func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) {
    lottieAnimationLayer.setValueProvider(valueProvider, keypath: keypath)
  }

  /// Sets a ValueProvider for the specified keypath. The value provider will be removed
  /// on all properties that match the keypath.
  public func removeValueProvider(for keypath: AnimationKeypath) {
    lottieAnimationLayer.removeValueProvider(for: keypath)
  }

  /// Reads the value of a property specified by the Keypath.
  /// Returns nil if no property is found.
  ///
  /// Note: This method isn't supported by the Core Animation rendering engine and will always return `nil` if used.
  /// It is still supported by the Main Thread rendering engine.
  ///
  /// - Parameter for: The keypath used to search for the property.
  /// - Parameter atFrame: The Frame Time of the value to query. If nil then the current frame is used.
  public func getValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? {
    lottieAnimationLayer.getValue(for: keypath, atFrame: atFrame)
  }

  /// Reads the original value of a property specified by the Keypath.
  /// This will ignore any value providers and can be useful when implementing a value providers that makes change to the original value from the animation.
  /// Returns nil if no property is found.
  ///
  /// - Parameter for: The keypath used to search for the property.
  /// - Parameter atFrame: The Frame Time of the value to query. If nil then the current frame is used.
  public func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? {
    lottieAnimationLayer.getOriginalValue(for: keypath, atFrame: atFrame)
  }

  /// Logs all child keypaths.
  /// Logs the result of `allHierarchyKeypaths()` to the `LottieLogger`.
  public func logHierarchyKeypaths() {
    lottieAnimationLayer.logHierarchyKeypaths()
  }

  /// Computes and returns a list of all child keypaths in the current animation.
  /// The returned list is the same as the log output of `logHierarchyKeypaths()`
  public func allHierarchyKeypaths() -> [String] {
    lottieAnimationLayer.allHierarchyKeypaths()
  }

  /// Searches for the nearest child layer to the first Keypath and adds the subview
  /// to that layer. The subview will move and animate with the child layer.
  /// Furthermore the subview will be in the child layers coordinate space.
  ///
  /// Note: if no layer is found for the keypath, then nothing happens.
  ///
  /// - Parameter subview: The subview to add to the found animation layer.
  /// - Parameter keypath: The keypath used to find the animation layer.
  ///
  /// Example:
  /// ```
  /// /// A keypath that finds `Layer 1`
  /// let layerKeypath = AnimationKeypath(keypath: "Layer 1")
  ///
  /// /// Wrap the custom view in an `AnimationSubview`
  /// let subview = AnimationSubview()
  /// subview.addSubview(customView)
  ///
  /// /// Set the provider on the animationView.
  /// animationView.addSubview(subview, forLayerAt: layerKeypath)
  /// ```
  public func addSubview(_ subview: AnimationSubview, forLayerAt keypath: AnimationKeypath) {
    guard let sublayer = lottieAnimationLayer.rootAnimationLayer?.layer(for: keypath) else {
      return
    }
    setNeedsLayout()
    layoutIfNeeded()
    lottieAnimationLayer.forceDisplayUpdate()
    addSubview(subview)
    if let subViewLayer = subview.viewLayer {
      sublayer.addSublayer(subViewLayer)
    }
  }

  /// Converts a CGRect from the LottieAnimationView's coordinate space into the
  /// coordinate space of the layer found at Keypath.
  ///
  /// If no layer is found, nil is returned
  ///
  /// - Parameter rect: The CGRect to convert.
  /// - Parameter toLayerAt: The keypath used to find the layer.
  public func convert(_ rect: CGRect, toLayerAt keypath: AnimationKeypath?) -> CGRect? {
    let convertedRect = lottieAnimationLayer.convert(rect, toLayerAt: keypath)
    setNeedsLayout()
    layoutIfNeeded()
    return convertedRect
  }

  /// Converts a CGPoint from the LottieAnimationView's coordinate space into the
  /// coordinate space of the layer found at Keypath.
  ///
  /// If no layer is found, nil is returned
  ///
  /// - Parameter point: The CGPoint to convert.
  /// - Parameter toLayerAt: The keypath used to find the layer.
  public func convert(_ point: CGPoint, toLayerAt keypath: AnimationKeypath?) -> CGPoint? {
    let convertedRect = lottieAnimationLayer.convert(point, toLayerAt: keypath)
    setNeedsLayout()
    layoutIfNeeded()
    return convertedRect
  }

  /// Sets the enabled state of all animator nodes found with the keypath search.
  /// This can be used to interactively enable / disable parts of the animation.
  ///
  /// - Parameter isEnabled: When true the animator nodes affect the rendering tree. When false the node is removed from the tree.
  /// - Parameter keypath: The keypath used to find the node(s).
  public func setNodeIsEnabled(isEnabled: Bool, keypath: AnimationKeypath) {
    lottieAnimationLayer.setNodeIsEnabled(isEnabled: isEnabled, keypath: keypath)
  }

  /// Markers are a way to describe a point in time by a key name.
  ///
  /// Markers are encoded into animation JSON. By using markers a designer can mark
  /// playback points for a developer to use without having to worry about keeping
  /// track of animation frames. If the animation file is updated, the developer
  /// does not need to update playback code.
  ///
  /// Returns the Progress Time for the marker named. Returns nil if no marker found.
  public func progressTime(forMarker named: String) -> AnimationProgressTime? {
    lottieAnimationLayer.progressTime(forMarker: named)
  }

  /// Markers are a way to describe a point in time by a key name.
  ///
  /// Markers are encoded into animation JSON. By using markers a designer can mark
  /// playback points for a developer to use without having to worry about keeping
  /// track of animation frames. If the animation file is updated, the developer
  /// does not need to update playback code.
  ///
  /// Returns the Frame Time for the marker named. Returns nil if no marker found.
  public func frameTime(forMarker named: String) -> AnimationFrameTime? {
    lottieAnimationLayer.frameTime(forMarker: named)
  }

  /// Markers are a way to describe a point in time and a duration by a key name.
  ///
  /// Markers are encoded into animation JSON. By using markers a designer can mark
  /// playback points for a developer to use without having to worry about keeping
  /// track of animation frames. If the animation file is updated, the developer
  /// does not need to update playback code.
  ///
  /// - Returns: The duration frame time for the marker, or `nil` if no marker found.
  public func durationFrameTime(forMarker named: String) -> AnimationFrameTime? {
    lottieAnimationLayer.durationFrameTime(forMarker: named)
  }

  // MARK: Internal

  /// The backing CALayer for this animation view.
  let lottieAnimationLayer: LottieAnimationLayer

  var animationLayer: RootAnimationLayer? {
    lottieAnimationLayer.rootAnimationLayer
  }

  /// Set animation name from Interface Builder
  @IBInspectable var animationName: String? {
    didSet {
      lottieAnimationLayer.animation = animationName.flatMap { LottieAnimation.named($0, animationCache: nil)
      }
    }
  }

  override func commonInit() {
    super.commonInit()
    lottieAnimationLayer.screenScale = screenScale
    viewLayer?.addSublayer(lottieAnimationLayer)

    lottieAnimationLayer.animationLoaded = { [weak self] _, animation in
      guard let self else { return }
      animationLoaded?(self, animation)
      invalidateIntrinsicContentSize()
      setNeedsLayout()
    }

    lottieAnimationLayer.animationLayerDidLoad = { [weak self] _, _ in
      guard let self else { return }
      invalidateIntrinsicContentSize()
      setNeedsLayout()
    }
  }

  override func layoutAnimation() {
    guard let animation = lottieAnimationLayer.animation, let animationLayer = lottieAnimationLayer.animationLayer else { return }

    var position = animation.bounds.center
    let xform: CATransform3D
    var shouldForceUpdates = false

    if let viewportFrame {
      shouldForceUpdates = contentMode == .redraw

      let compAspect = viewportFrame.size.width / viewportFrame.size.height
      let viewAspect = bounds.size.width / bounds.size.height
      let dominantDimension = compAspect > viewAspect ? bounds.size.width : bounds.size.height
      let compDimension = compAspect > viewAspect ? viewportFrame.size.width : viewportFrame.size.height
      let scale = dominantDimension / compDimension

      let viewportOffset = animation.bounds.center - viewportFrame.center
      xform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1), viewportOffset.x, viewportOffset.y, 0)
      position = bounds.center
    } else {
      switch contentMode {
      case .scaleToFill:
        position = bounds.center
        xform = CATransform3DMakeScale(
          bounds.size.width / animation.size.width,
          bounds.size.height / animation.size.height,
          1
        )
      case .scaleAspectFit:
        position = bounds.center
        let compAspect = animation.size.width / animation.size.height
        let viewAspect = bounds.size.width / bounds.size.height
        let dominantDimension = compAspect > viewAspect ? bounds.size.width : bounds.size.height
        let compDimension = compAspect > viewAspect ? animation.size.width : animation.size.height
        let scale = dominantDimension / compDimension
        xform = CATransform3DMakeScale(scale, scale, 1)
      case .scaleAspectFill:
        position = bounds.center
        let compAspect = animation.size.width / animation.size.height
        let viewAspect = bounds.size.width / bounds.size.height
        let scaleWidth = compAspect < viewAspect
        let dominantDimension = scaleWidth ? bounds.size.width : bounds.size.height
        let compDimension = scaleWidth ? animation.size.width : animation.size.height
        let scale = dominantDimension / compDimension
        xform = CATransform3DMakeScale(scale, scale, 1)
      case .redraw:
        shouldForceUpdates = true
        xform = CATransform3DIdentity
      case .center:
        position = bounds.center
        xform = CATransform3DIdentity
      case .top:
        position.x = bounds.center.x
        xform = CATransform3DIdentity
      case .bottom:
        position.x = bounds.center.x
        position.y = bounds.maxY - animation.bounds.midY
        xform = CATransform3DIdentity
      case .left:
        position.y = bounds.center.y
        xform = CATransform3DIdentity
      case .right:
        position.y = bounds.center.y
        position.x = bounds.maxX - animation.bounds.midX
        xform = CATransform3DIdentity
      case .topLeft:
        xform = CATransform3DIdentity
      case .topRight:
        position.x = bounds.maxX - animation.bounds.midX
        xform = CATransform3DIdentity
      case .bottomLeft:
        position.y = bounds.maxY - animation.bounds.midY
        xform = CATransform3DIdentity
      case .bottomRight:
        position.x = bounds.maxX - animation.bounds.midX
        position.y = bounds.maxY - animation.bounds.midY
        xform = CATransform3DIdentity

      #if canImport(UIKit)
      @unknown default:
        logger.assertionFailure("unsupported contentMode: \(contentMode.rawValue)")
        xform = CATransform3DIdentity
      #endif
      }
    }

    // UIView Animation does not implicitly set CAAnimation time or timing functions.
    // If layout is changed in an animation we must get the current animation duration
    // and timing function and then manually create a CAAnimation to match the UIView animation.
    // If layout is changed without animation, explicitly set animation duration to 0.0
    // inside CATransaction to avoid unwanted artifacts.
    /// Check if any animation exist on the view's layer, and match it.
    if
      let key = lottieAnimationLayer.animationKeys()?.first,
      let animation = lottieAnimationLayer.animation(forKey: key),
      animateLayoutChangesWithCurrentCoreAnimationContext
    {
      // The layout is happening within an animation block. Grab the animation data.

      let positionKey = "LayoutPositionAnimation"
      let transformKey = "LayoutTransformAnimation"
      animationLayer.removeAnimation(forKey: positionKey)
      animationLayer.removeAnimation(forKey: transformKey)

      let positionAnimation = animation.copy() as? CABasicAnimation ?? CABasicAnimation(keyPath: "position")
      positionAnimation.keyPath = "position"
      positionAnimation.isAdditive = false
      positionAnimation.fromValue = (animationLayer.presentation() ?? animationLayer).position
      positionAnimation.toValue = position
      positionAnimation.isRemovedOnCompletion = true

      let xformAnimation = animation.copy() as? CABasicAnimation ?? CABasicAnimation(keyPath: "transform")
      xformAnimation.keyPath = "transform"
      xformAnimation.isAdditive = false
      xformAnimation.fromValue = (animationLayer.presentation() ?? animationLayer).transform
      xformAnimation.toValue = xform
      xformAnimation.isRemovedOnCompletion = true

      animationLayer.position = position
      animationLayer.transform = xform
      animationLayer.anchorPoint = lottieAnimationLayer.anchorPoint
      animationLayer.add(positionAnimation, forKey: positionKey)
      animationLayer.add(xformAnimation, forKey: transformKey)
    } else {
      // In performance tests, we have to wrap the animation view setup
      // in a `CATransaction` in order for the layers to be deallocated at
      // the correct time. The `CATransaction`s in this method interfere
      // with the ones managed by the performance test, and aren't actually
      // necessary in a headless environment, so we disable them.
      if TestHelpers.performanceTestsAreRunning {
        animationLayer.position = position
        animationLayer.transform = xform
      } else {
        CATransaction.begin()
        CATransaction.setAnimationDuration(0.0)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .linear))
        animationLayer.position = position
        animationLayer.transform = xform
        CATransaction.commit()
      }
    }

    if shouldForceUpdates {
      lottieAnimationLayer.forceDisplayUpdate()
    }
  }

  func updateRasterizationState() {
    lottieAnimationLayer.updateRasterizationState()
  }

  /// Updates the animation frame. Does not affect any current animations
  func updateAnimationFrame(_ newFrame: CGFloat) {
    lottieAnimationLayer.updateAnimationFrame(newFrame)
  }

  @objc
  override func animationWillMoveToBackground() {
    updateAnimationForBackgroundState()
  }

  @objc
  override func animationWillEnterForeground() {
    updateAnimationForForegroundState()
  }

  override func animationMovedToWindow() {
    /// Don't update any state if the `superview`  is `nil`
    /// When A viewA owns superViewB, it removes the superViewB from the window. At this point, viewA still owns superViewB and triggers the viewA method: -didmovetowindow
    guard superview != nil else { return }

    if window != nil {
      updateAnimationForForegroundState()
    } else {
      updateAnimationForBackgroundState()
    }
  }

  func updateInFlightAnimation() {
    lottieAnimationLayer.updateInFlightAnimation()
  }

  func loadAnimation(_ animationSource: LottieAnimationSource?) {
    lottieAnimationLayer.loadAnimation(animationSource)
  }

  // MARK: Fileprivate

  fileprivate var waitingToPlayAnimation = false

  fileprivate func updateAnimationForBackgroundState() {
    lottieAnimationLayer.updateAnimationForBackgroundState()
  }

  fileprivate func updateAnimationForForegroundState() {
    let wasWaitingToPlayAnimation = waitingToPlayAnimation
    if waitingToPlayAnimation {
      waitingToPlayAnimation = false
    }
    lottieAnimationLayer.updateAnimationForForegroundState(wasWaitingToPlayAnimation: wasWaitingToPlayAnimation)
  }

  // MARK: Private

  private let logger: LottieLogger
}
