//
//  AnimationPublic.swift
//  lottie-swift
//
//  Created by Brandon Withrow on 2/5/19.
//

import CoreGraphics
import Foundation

extension LottieAnimation {

  /// A closure for an Animation download. The closure is passed `nil` if there was an error.
  public typealias DownloadClosure = (LottieAnimation?) -> Void

  /// The duration in seconds of the animation.
  public var duration: TimeInterval {
    Double(endFrame - startFrame) / framerate
  }

  /// The natural bounds in points of the animation.
  public var bounds: CGRect {
    CGRect(x: 0, y: 0, width: width, height: height)
  }

  /// The natural size in points of the animation.
  public var size: CGSize {
    CGSize(width: width, height: height)
  }

  /// Loads an animation model from a bundle by its name. Returns `nil` if an animation is not found.
  ///
  /// - Parameter name: The name of the json file without the json extension. EG "StarAnimation"
  /// - Parameter bundle: The bundle in which the animation is located. Defaults to `Bundle.main`
  /// - Parameter subdirectory: A subdirectory in the bundle in which the animation is located. Optional.
  /// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LottieAnimationCache.shared`. Optional.
  ///
  /// - Returns: Deserialized `LottieAnimation`. Optional.
  public static func named(
    _ name: String,
    bundle: Bundle = Bundle.main,
    subdirectory: String? = nil,
    animationCache: AnimationCacheProvider? = LottieAnimationCache.shared
  ) -> LottieAnimation? {
    /// Create a cache key for the animation.
    let cacheKey = bundle.bundlePath + (subdirectory ?? "") + "/" + name

    /// Check cache for animation
    if
      let animationCache,
      let animation = animationCache.animation(forKey: cacheKey)
    {
      /// If found, return the animation.
      return animation
    }

    do {
      /// Decode animation.
      let json = try bundle.getAnimationData(name, subdirectory: subdirectory)
      let animation = try LottieAnimation.from(data: json)
      animationCache?.setAnimation(animation, forKey: cacheKey)
      return animation
    } catch {
      /// Decoding error.
      LottieLogger.shared.warn("Error when decoding animation \"\(name)\": \(error)")
      return nil
    }
  }

  /// Loads an animation from a specific filepath.
  /// - Parameter filepath: The absolute filepath of the animation to load. EG "/User/Me/starAnimation.json"
  /// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LottieAnimationCache.shared`. Optional.
  ///
  /// - Returns: Deserialized `LottieAnimation`. Optional.
  public static func filepath(
    _ filepath: String,
    animationCache: AnimationCacheProvider? = LottieAnimationCache.shared
  ) -> LottieAnimation? {
    /// Check cache for animation
    if
      let animationCache,
      let animation = animationCache.animation(forKey: filepath)
    {
      return animation
    }

    do {
      /// Decode the animation.
      let json = try Data(contentsOf: URL(fileURLWithPath: filepath))
      let animation = try LottieAnimation.from(data: json)
      animationCache?.setAnimation(animation, forKey: filepath)
      return animation
    } catch {
      LottieLogger.shared.warn("""
        Failed to load animation from filepath \(filepath)
        with underlying error: \(error.localizedDescription)
        """)
      return nil
    }
  }

  ///    Loads an animation model from the asset catalog by its name. Returns `nil` if an animation is not found.
  ///    - Parameter name: The name of the json file in the asset catalog. EG "StarAnimation"
  ///    - Parameter bundle: The bundle in which the animation is located. Defaults to `Bundle.main`
  ///    - Parameter animationCache: A cache for holding loaded animations. Defaults to `LottieAnimationCache.shared` Optional.
  ///    - Returns: Deserialized `LottieAnimation`. Optional.
  public static func asset(
    _ name: String,
    bundle: Bundle = Bundle.main,
    animationCache: AnimationCacheProvider? = LottieAnimationCache.shared
  ) -> LottieAnimation? {
    /// Create a cache key for the animation.
    let cacheKey = bundle.bundlePath + "/" + name

    /// Check cache for animation
    if
      let animationCache,
      let animation = animationCache.animation(forKey: cacheKey)
    {
      /// If found, return the animation.
      return animation
    }

    do {
      /// Load jsonData from Asset
      let json = try Data(assetName: name, in: bundle)
      /// Decode animation.
      let animation = try LottieAnimation.from(data: json)
      animationCache?.setAnimation(animation, forKey: cacheKey)
      return animation
    } catch {
      LottieLogger.shared.warn("""
        Failed to load animation with asset name \(name)
        in \(bundle.bundlePath)
        with underlying error: \(error.localizedDescription)
        """)
      return nil
    }
  }

  /// Loads a Lottie animation from a `Data` object containing a JSON animation.
  ///
  /// - Parameter data: The object to load the animation from.
  /// - Parameter strategy: How the data should be decoded. Defaults to using the strategy set in `LottieConfiguration.shared`.
  /// - Returns: Deserialized `LottieAnimation`. Optional.
  ///
  public static func from(
    data: Data,
    strategy: DecodingStrategy = LottieConfiguration.shared.decodingStrategy
  ) throws -> LottieAnimation {
    switch strategy {
    case .legacyCodable:
      return try JSONDecoder().decode(LottieAnimation.self, from: data)
    case .dictionaryBased:
      let json = try JSONSerialization.jsonObject(with: data)
      guard let dict = json as? [String: Any] else {
        throw InitializableError.invalidInput()
      }
      return try LottieAnimation(dictionary: dict)
    }
  }

  /// Loads a Lottie animation asynchronously from the URL.
  ///
  /// - Parameter url: The url to load the animation from.
  /// - Parameter session: The `LottieURLSession` used to load the animation. Defaults to `LottieConfiguration.defaultURLSession`.
  /// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LottieAnimationCache.shared`. Optional.
  ///
  /// - Returns: Loaded Lottie animation, or `nil` if loading failed.
  public static func loadedFrom(
    url: URL,
    session: LottieURLSession = LottieConfiguration.defaultURLSession,
    animationCache: AnimationCacheProvider? = LottieAnimationCache.shared
  ) async -> LottieAnimation? {
    var dataTask: URLSessionDataTask?
    let cancelTask = { dataTask?.cancel() }
    return await withTaskCancellationHandler {
      await withCheckedContinuation { continuation in
        dataTask = LottieAnimation.loadedFrom(
          url: url,
          session: session,
          closure: { result in
            continuation.resume(returning: result)
          },
          animationCache: animationCache
        )
      }
    } onCancel: {
      cancelTask()
    }
  }

  /// Loads a Lottie animation asynchronously from the URL.
  ///
  /// - Parameter url: The url to load the animation from.
  /// - Parameter session: The `LottieURLSession` used to load the animation. Defaults to `LottieConfiguration.defaultURLSession`.
  /// - Parameter closure: A closure to be called when the animation has loaded.
  /// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LottieAnimationCache.shared`. Optional.
  ///
  /// - Returns: `URLSessionDataTask` that can be used to cancel the request, or `nil` if the animation was loaded from cache.
  @discardableResult
  public static func loadedFrom(
    url: URL,
    session: LottieURLSession = LottieConfiguration.defaultURLSession,
    closure: @escaping LottieAnimation.DownloadClosure,
    animationCache: AnimationCacheProvider? = LottieAnimationCache.shared
  ) -> URLSessionDataTask? {
    if let animationCache, let animation = animationCache.animation(forKey: url.absoluteString) {
      closure(animation)
      return nil
    } else {
      let task = session.lottieDataTask(with: url) { data, _, error in
        guard error == nil, let jsonData = data else {
          DispatchQueue.main.async {
            closure(nil)
          }
          return
        }
        do {
          let animation = try LottieAnimation.from(data: jsonData)
          DispatchQueue.main.async {
            animationCache?.setAnimation(animation, forKey: url.absoluteString)
            closure(animation)
          }
        } catch {
          DispatchQueue.main.async {
            closure(nil)
          }
        }
      }
      task?.resume()
      return task
    }
  }

  /// 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? {
    guard let markers = markerMap, let marker = markers[named] else {
      return nil
    }
    return progressTime(forFrame: marker.frameTime)
  }

  /// 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? {
    guard let markers = markerMap, let marker = markers[named] else {
      return nil
    }
    return marker.frameTime
  }

  /// 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? {
    guard let marker = markerMap?[named] else {
      return nil
    }
    return marker.durationFrameTime
  }

  /// Converts Frame Time (Seconds * Framerate) into Progress Time
  /// (optionally clamped to between 0 and 1).
  public func progressTime(
    forFrame frameTime: AnimationFrameTime,
    clamped: Bool = true
  ) -> AnimationProgressTime {
    let progressTime: AnimationFrameTime
    let frameDuration = endFrame - startFrame

    if frameDuration == 0.0 {
      progressTime = 0.0
    } else {
      progressTime = ((frameTime - startFrame) / frameDuration)
    }

    if clamped {
      return progressTime.clamp(0, 1)
    } else {
      return progressTime
    }
  }

  /// Converts Progress Time (0 to 1) into Frame Time (Seconds * Framerate)
  public func frameTime(forProgress progressTime: AnimationProgressTime) -> AnimationFrameTime {
    ((endFrame - startFrame) * progressTime) + startFrame
  }

  /// Converts Frame Time (Seconds * Framerate) into Time (Seconds)
  public func time(forFrame frameTime: AnimationFrameTime) -> TimeInterval {
    Double(frameTime - startFrame) / framerate
  }

  /// Converts Time (Seconds) into Frame Time (Seconds * Framerate)
  public func frameTime(forTime time: TimeInterval) -> AnimationFrameTime {
    CGFloat(time * framerate) + startFrame
  }
}
