//
//  TextCompositionLayer.swift
//  lottie-swift
//
//  Created by Brandon Withrow on 1/25/19.
//

/// Needed for NSMutableParagraphStyle...
#if os(OSX)
import AppKit
#else
import UIKit
#endif

extension TextJustification {
  var textAlignment: NSTextAlignment {
    switch self {
    case .left:
      .left
    case .right:
      .right
    case .center:
      .center
    case .justifyLastLineLeft, .justifyLastLineRight, .justifyLastLineCenter, .justifyLastLineFull:
      .justified
    }
  }

  var caTextAlignement: CATextLayerAlignmentMode {
    switch self {
    case .left:
      .left
    case .right:
      .right
    case .center:
      .center
    case .justifyLastLineLeft, .justifyLastLineRight, .justifyLastLineCenter, .justifyLastLineFull:
      .justified
    }
  }
}

// MARK: - TextCompositionLayer

final class TextCompositionLayer: CompositionLayer {

  // MARK: Lifecycle

  init(
    textLayer: TextLayerModel,
    textProvider: AnimationKeypathTextProvider,
    fontProvider: AnimationFontProvider,
    rootAnimationLayer: MainThreadAnimationLayer?
  ) {
    var rootNode: TextAnimatorNode?
    for animator in textLayer.animators {
      rootNode = TextAnimatorNode(parentNode: rootNode, textAnimator: animator)
    }
    self.rootNode = rootNode
    textDocument = KeyframeInterpolator(keyframes: textLayer.text.keyframes)

    let transparent = LottieColor(r: 0, g: 0, b: 0, a: 0)
    let black = LottieColor(r: 0, g: 0, b: 0, a: 1)

    let fillKeyframes = textLayer.text.keyframes.map { keyframe in
      keyframe.withValue(keyframe.value.fillColorData ?? black)
    }
    fillColorNode = NodeProperty(provider: KeyframeInterpolator(keyframes: ContiguousArray(fillKeyframes)))

    let strokeKeyframes = textLayer.text.keyframes.map { keyframe in
      keyframe.withValue(keyframe.value.strokeColorData ?? transparent)
    }
    strokeColorNode = NodeProperty(provider: KeyframeInterpolator(keyframes: ContiguousArray(strokeKeyframes)))

    self.textProvider = textProvider
    self.fontProvider = fontProvider
    self.rootAnimationLayer = rootAnimationLayer

    super.init(layer: textLayer, size: .zero)
    contentsLayer.addSublayer(self.textLayer)
    self.textLayer.masksToBounds = false
    self.textLayer.isGeometryFlipped = true

    if let rootNode {
      childKeypaths.append(rootNode)
    }
  }

  required init?(coder _: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override init(layer: Any) {
    /// Used for creating shadow model layers. Read More here: https://developer.apple.com/documentation/quartzcore/calayer/1410842-init
    guard let layer = layer as? TextCompositionLayer else {
      fatalError("init(layer:) Wrong Layer Class")
    }
    rootNode = nil
    textDocument = nil

    fillColorNode = layer.fillColorNode
    strokeColorNode = layer.strokeColorNode

    textProvider = DefaultTextProvider()
    fontProvider = DefaultFontProvider()

    super.init(layer: layer)
  }

  // MARK: Internal

  let rootNode: TextAnimatorNode?
  let textDocument: KeyframeInterpolator<TextDocument>?

  let fillColorNode: NodeProperty<LottieColor>
  let strokeColorNode: NodeProperty<LottieColor>

  let textLayer = CoreTextRenderLayer()
  var textProvider: AnimationKeypathTextProvider
  var fontProvider: AnimationFontProvider
  weak var rootAnimationLayer: MainThreadAnimationLayer?

  lazy var fullAnimationKeypath: AnimationKeypath = // Individual layers don't know their full keypaths, so we have to delegate
    // to the `MainThreadAnimationLayer` to search the layer hierarchy and find
    // the full keypath (which includes this layer's parent layers)
    rootAnimationLayer?.keypath(for: self)
    // If that failed for some reason, just use the last path component (which we do have here)
    ?? AnimationKeypath(keypath: keypathName)

  override var keypathProperties: [String: AnyNodeProperty] {
    guard rootNode != nil else {
      return [
        PropertyName.color.rawValue: fillColorNode,
        PropertyName.strokeColor.rawValue: strokeColorNode,
      ]
    }
    var properties = super.keypathProperties
    properties[PropertyName.color.rawValue] = fillColorNode
    properties[PropertyName.strokeColor.rawValue] = strokeColorNode
    return properties
  }

  override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
    guard let textDocument else { return }

    textLayer.contentsScale = renderScale

    fillColorNode.update(frame: frame)
    strokeColorNode.update(frame: frame)

    let documentUpdate = textDocument.hasUpdate(frame: frame)
    let animatorUpdate = rootNode?.updateContents(frame, forceLocalUpdate: forceUpdates) ?? false
    guard
      documentUpdate == true ||
      animatorUpdate == true ||
      fillColorNode.needsUpdate(frame: frame) ||
      strokeColorNode.needsUpdate(frame: frame)
    else { return }

    rootNode?.rebuildOutputs(frame: frame)

    // Get Text Attributes
    let text = textDocument.value(frame: frame) as! TextDocument

    // Prior to Lottie 4.3.0 the Main Thread rendering engine always just used `LegacyAnimationTextProvider`
    // and called it with the `keypathName` (only the last path component of the full keypath).
    // Starting in Lottie 4.3.0 we use `AnimationKeypathTextProvider` instead if implemented.
    let textString: String =
      if let keypathTextValue = textProvider.text(for: fullAnimationKeypath, sourceText: text.text) {
        keypathTextValue
      } else if let legacyTextProvider = textProvider as? LegacyAnimationTextProvider {
        legacyTextProvider.textFor(keypathName: keypathName, sourceText: text.text)
      } else {
        text.text
      }

    let isStrokeOverridden =
      (strokeColorNode.valueProvider as AnyObject) !== (strokeColorNode.originalValueProvider as AnyObject)

    let strokeColor: CGColor? =
      if let animatorStroke = rootNode?.textOutputNode.strokeColor {
        animatorStroke
      } else if isStrokeOverridden {
        strokeColorNode.value.cgColorValue
      } else {
        text.strokeColorData?.cgColorValue
      }

    let strokeWidth = rootNode?.textOutputNode.strokeWidth ?? CGFloat(text.strokeWidth ?? 0)
    let tracking = (CGFloat(text.fontSize) * (rootNode?.textOutputNode.tracking ?? CGFloat(text.tracking))) / 1000.0
    let matrix = rootNode?.textOutputNode.xform ?? CATransform3DIdentity
    let ctFont = fontProvider.fontFor(family: text.fontFamily, size: CGFloat(text.fontSize))
    let start = rootNode?.textOutputNode.start.flatMap { Int($0) }
    let end = rootNode?.textOutputNode.end.flatMap { Int($0) }
    let selectedRangeOpacity = rootNode?.textOutputNode.selectedRangeOpacity
    let textRangeUnit = rootNode?.textAnimatorProperties.textRangeUnit

    // Set all of the text layer options
    textLayer.text = textString
    textLayer.font = ctFont
    textLayer.alignment = text.justification.textAlignment
    textLayer.lineHeight = CGFloat(text.lineHeight)
    textLayer.tracking = tracking

    // Configure the text animators
    textLayer.start = start
    textLayer.end = end
    textLayer.textRangeUnit = textRangeUnit
    textLayer.selectedRangeOpacity = selectedRangeOpacity

    // Check for an explicit override. If not overridden, we default to `TextDocument`
    // to support transparency (avoiding the default black color of the value provider).
    // `fillColorNode` is non-optional and defaults to black if the color was not overridden.
    // If the color has been overridden, we use the new color: `fillColorNode.value.cgColorValue`.
    // Otherwise, we use the default color from the text: `text.fillColorData?.cgColorValue`.
    //
    // We cannot remove the cast since `AnyValueProvider` is not a class-bound protocol,
    // so we must cast to `AnyObject` to perform an identity comparison.
    let isFillOverridden =
      (fillColorNode.valueProvider as AnyObject) !== (fillColorNode.originalValueProvider as AnyObject)

    textLayer.fillColor =
      if let fillColor = rootNode?.textOutputNode.fillColor {
        fillColor
      } else if isFillOverridden {
        fillColorNode.value.cgColorValue
      } else {
        text.fillColorData?.cgColorValue
      }

    textLayer.preferredSize = text.textFrameSize?.sizeValue
    textLayer.strokeOnTop = text.strokeOverFill ?? false
    textLayer.strokeWidth = strokeWidth
    textLayer.strokeColor = strokeColor
    textLayer.sizeToFit()

    textLayer.opacity = Float(rootNode?.textOutputNode.opacity ?? 1)
    textLayer.transform = CATransform3DIdentity
    textLayer.position = text.textFramePosition?.pointValue ?? CGPoint.zero
    textLayer.transform = matrix
  }

  override func updateRenderScale() {
    super.updateRenderScale()
    textLayer.contentsScale = renderScale
  }
}
