/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.util

import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.HorizontalScrollView
import android.widget.ScrollView
import android.widget.Spinner
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat

/**
 * This class provides utility methods for determining certain accessibility properties of [View]s
 * and [AccessibilityNodeInfoCompat]s. It is porting some of the checks from
 * [com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils], but has stripped many features which
 * are unnecessary here.
 */
object AccessibilityUtil {
  /**
   * Returns whether the specified node has text or a content description.
   *
   * @param node The node to check.
   * @return `true` if the node has text.
   */
  fun hasText(node: AccessibilityNodeInfoCompat?): Boolean {
    return if (node == null) {
      false
    } else !TextUtils.isEmpty(node.text) || !TextUtils.isEmpty(node.contentDescription)
  }

  /**
   * Returns whether the supplied [View] and [AccessibilityNodeInfoCompat] would produce spoken
   * feedback if it were accessibility focused. NOTE: not all speaking nodes are focusable.
   *
   * @param view The [View] to evaluate
   * @param node The [AccessibilityNodeInfoCompat] to evaluate
   * @return `true` if it meets the criterion for producing spoken feedback
   */
  private fun isSpeakingNode(node: AccessibilityNodeInfoCompat?, view: View?): Boolean {
    if (node == null || view == null) {
      return false
    }
    if (!node.isVisibleToUser) {
      return false
    }
    val important = ViewCompat.getImportantForAccessibility(view)
    return if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS ||
        important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO && node.childCount <= 0) {
      false
    } else node.isCheckable || hasText(node) || hasNonActionableSpeakingDescendants(node, view)
  }

  /**
   * Determines if the supplied [View] and [AccessibilityNodeInfoCompat] has any children which are
   * not independently accessibility focusable and also have a spoken description.
   *
   * NOTE: Accessibility services will include these children's descriptions in the closest
   * focusable ancestor.
   *
   * @param view The [View] to evaluate
   * @param node The [AccessibilityNodeInfoCompat] to evaluate
   * @return `true` if it has any non-actionable speaking descendants within its subtree
   */
  private fun hasNonActionableSpeakingDescendants(
      node: AccessibilityNodeInfoCompat?,
      view: View?
  ): Boolean {
    if (node == null || view == null || view !is ViewGroup) {
      return false
    }
    val viewGroup = view as ViewGroup
    val count = viewGroup.childCount - 1
    for (i in 0..count) {
      val childView = viewGroup.getChildAt(i) ?: continue
      val childNode = AccessibilityNodeInfoCompat.obtain()
      try {
        ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode)
        if (isAccessibilityFocusable(childNode, childView)) {
          continue
        }
        if (isSpeakingNode(childNode, childView)) {
          return true
        }
      } finally {
        childNode.recycle()
      }
    }
    return false
  }

  /**
   * Determines if the provided [View] and [AccessibilityNodeInfoCompat] meet the criteria for
   * gaining accessibility focus.
   *
   * @param view The [View] to evaluate
   * @param node The [AccessibilityNodeInfoCompat] to evaluate
   * @return `true` if it is possible to gain accessibility focus
   */
  private fun isAccessibilityFocusable(node: AccessibilityNodeInfoCompat?, view: View?): Boolean {
    if (node == null || view == null) {
      return false
    }

    // Never focus invisible nodes.
    if (!node.isVisibleToUser) {
      return false
    }

    // Always focus "actionable" nodes.
    return if (isActionableForAccessibility(node)) {
      true
    } else isTopLevelScrollItem(node, view) && isSpeakingNode(node, view)
    // only focus top-level list items with non-actionable speaking children.
  }

  /**
   * Determines whether the provided [View] and [AccessibilityNodeInfoCompat] is a top-level item in
   * a scrollable container.
   *
   * @param view The [View] to evaluate
   * @param node The [AccessibilityNodeInfoCompat] to evaluate
   * @return `true` if it is a top-level item in a scrollable container.
   */
  fun isTopLevelScrollItem(node: AccessibilityNodeInfoCompat?, view: View?): Boolean {
    if (node == null || view == null) {
      return false
    }
    val parent = ViewCompat.getParentForAccessibility(view) as View? ?: return false
    if (node.isScrollable) {
      return true
    }
    val actionList: List<*> = node.actionList
    if (actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) ||
        actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)) {
      return true
    }

    // AdapterView, ScrollView, and HorizontalScrollView are focusable
    // containers, but Spinner is a special case.
    return if (parent is Spinner) {
      false
    } else parent is AdapterView<*> || parent is ScrollView || parent is HorizontalScrollView
  }

  /**
   * Returns whether a node is actionable. That is, the node supports one of
   * [AccessibilityNodeInfoCompat.isClickable], [AccessibilityNodeInfoCompat.isFocusable], or
   * [AccessibilityNodeInfoCompat.isLongClickable].
   *
   * @param node The [AccessibilityNodeInfoCompat] to evaluate
   * @return `true` if node is actionable.
   */
  fun isActionableForAccessibility(node: AccessibilityNodeInfoCompat?): Boolean {
    if (node == null) {
      return false
    }
    if (node.isClickable || node.isLongClickable || node.isFocusable) {
      return true
    }
    val actionList: List<*> = node.actionList
    return actionList.contains(AccessibilityNodeInfoCompat.ACTION_CLICK) ||
        actionList.contains(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK) ||
        actionList.contains(AccessibilityNodeInfoCompat.ACTION_FOCUS)
  }

  /**
   * Determines if any of the provided [View]'s and [AccessibilityNodeInfoCompat]'s ancestors can
   * receive accessibility focus
   *
   * @param view The [View] to evaluate
   * @param node The [AccessibilityNodeInfoCompat] to evaluate
   * @return `true` if an ancestor of may receive accessibility focus
   */
  fun hasFocusableAncestor(node: AccessibilityNodeInfoCompat?, view: View?): Boolean {
    if (node == null || view == null) {
      return false
    }
    val parentView = ViewCompat.getParentForAccessibility(view)
    if (parentView !is View) {
      return false
    }
    val parentNode = AccessibilityNodeInfoCompat.obtain()
    try {
      ViewCompat.onInitializeAccessibilityNodeInfo((parentView as View), parentNode)
      if (parentNode == null) {
        return false
      }
      if (isAccessibilityFocusable(parentNode, parentView as View)) {
        return true
      }
      if (hasFocusableAncestor(parentNode, parentView as View)) {
        return true
      }
    } finally {
      parentNode!!.recycle()
    }
    return false
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.util

import android.content.res.Resources
import com.facebook.flipper.plugins.uidebugger.model.Bounds

object DisplayMetrics {
  fun getDisplayBounds(): Bounds {
    val displayMetrics = Resources.getSystem().displayMetrics
    return Bounds(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.util

/**
 * This abstracts over a value which could be available immediately or calculated later. The use
 * case is for shifting computation to a background thread.
 */
abstract class MaybeDeferred<T> {
  abstract fun value(): T

  /** similar to map on an Option or Functor in a functional language. */
  abstract fun <U> map(fn: (T) -> U): MaybeDeferred<U>
}

class Immediate<T>(private val value: T) : MaybeDeferred<T>() {
  override fun value(): T = value

  override fun <U> map(fn: (T) -> U): MaybeDeferred<U> = Immediate(fn(value))
}

class Deferred<T>(private val lazyLoader: () -> T) : MaybeDeferred<T>() {
  override fun value(): T = lazyLoader()

  override fun <U> map(fn: (T) -> U): MaybeDeferred<U> {
    return Deferred { fn(lazyLoader()) }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.util

fun Any.objectIdentity(): Int {
  return System.identityHashCode(this)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.util

import android.content.res.Resources
import android.content.res.Resources.NotFoundException
import javax.annotation.Nonnull

object ResourcesUtil {
  @Nonnull
  fun getIdStringQuietly(idContext: Any, r: Resources?, resourceId: Int): String {
    return try {
      getIdString(r, resourceId)
    } catch (e: NotFoundException) {
      getFallbackIdString(resourceId)
    }
  }

  @Throws(NotFoundException::class)
  fun getIdString(r: Resources?, resourceId: Int): String {
    if (r == null) {
      return getFallbackIdString(resourceId)
    }
    val prefix: String
    val prefixSeparator: String
    when (getResourcePackageId(resourceId)) {
      0x7f -> {
        prefix = ""
        prefixSeparator = ""
      }
      else -> {
        prefix = r.getResourcePackageName(resourceId)
        prefixSeparator = ":"
      }
    }
    val typeName = r.getResourceTypeName(resourceId)
    val entryName = r.getResourceEntryName(resourceId)
    val sb =
        StringBuilder(
            1 + prefix.length + prefixSeparator.length + typeName.length + 1 + entryName.length)
    sb.append("@")
    sb.append(prefix)
    sb.append(prefixSeparator)
    sb.append(typeName)
    sb.append("/")
    sb.append(entryName)
    return sb.toString()
  }

  private fun getFallbackIdString(resourceId: Int): String {
    return "#" + Integer.toHexString(resourceId)
  }

  private fun getResourcePackageId(id: Int): Int {
    return (id ushr 24) and 0xff
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger

import android.view.View
import com.facebook.flipper.plugins.uidebugger.common.EnumMapping
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class EnumMappingTest {

  val visibility: EnumMapping<Int> =
      object :
          EnumMapping<Int>(
              mapOf(
                  "VISIBLE" to View.VISIBLE, "INVISIBLE" to View.INVISIBLE, "GONE" to View.GONE)) {}

  @Test
  fun testTurnsEnumToString() {
    assertThat(visibility.getEnumValue("VISIBLE"), equalTo(View.VISIBLE))
  }

  @Test
  fun testTurnsStringToEnum() {
    assertThat(visibility.getStringRepresentation(View.VISIBLE), equalTo("VISIBLE"))
  }

  @Test
  fun testTurnsIntoEnumInspectable() {
    assertThat(visibility.toInspectable(View.GONE), equalTo(InspectableValue.Enum("GONE")))
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger

import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
import com.facebook.flipper.plugins.uidebugger.core.UIDContext
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment

@RunWith(RobolectricTestRunner::class)
class UIDebuggerFlipperPluginTest {

  val app = Mockito.spy(RuntimeEnvironment.application)
  private var appRef: ApplicationRef = Mockito.spy(ApplicationRef(app))

  @Before
  open fun setup() {
    Mockito.`when`(app.applicationContext).thenReturn(app)
    Mockito.`when`(app.packageName).thenReturn("com.facebook.flipper")
  }

  @Throws(Exception::class)
  @Test
  fun emptyTest() {
    var plugin = UIDebuggerFlipperPlugin(UIDContext.create(app))
    Assert.assertNotNull(plugin)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.facebook.flipper.sample.tutorial.ui.RootComponent
import com.facebook.litho.LithoView
import com.facebook.litho.sections.SectionContext

class MainActivity : AppCompatActivity() {

  private val sectionContext: SectionContext by lazy { SectionContext(this) }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(LithoView.create(this, RootComponent.create(sectionContext).build()))
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial

import android.net.Uri

data class MarineMammal(val title: String, val picture_url: Uri)


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial

import androidx.core.net.toUri

object MarineMammals {
  val list =
      listOf(
          MarineMammal(
              "Polar Bear",
              "https://upload.wikimedia.org/wikipedia/commons/thumb/8/84/Ursus_maritimus_4_1996-08-04.jpg/190px-Ursus_maritimus_4_1996-08-04.jpg"
                  .toUri()),
          MarineMammal(
              "Sea Otter",
              "https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Sea_otter_cropped.jpg/220px-Sea_otter_cropped.jpg"
                  .toUri()),
          MarineMammal(
              "West Indian Manatee",
              "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/FL_fig04.jpg/230px-FL_fig04.jpg"
                  .toUri()),
          MarineMammal(
              "Bottlenose Dolphin",
              "https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Tursiops_truncatus_01.jpg/220px-Tursiops_truncatus_01.jpg"
                  .toUri()))
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial

import android.app.Application
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.sample.tutorial.plugin.SeaMammalFlipperPlugin
import com.facebook.fresco.vito.init.FrescoVito
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.editor.flipper.LithoFlipperDescriptors
import com.facebook.soloader.SoLoader

class TutorialApplication : Application() {
  override fun onCreate() {
    super.onCreate()

    SoLoader.init(this, false)
    Fresco.initialize(this)
    FrescoVito.initialize()

    // Normally, you would want to make these dependent on BuildConfig.DEBUG.
    ComponentsConfiguration.isDebugModeEnabled = true

    val flipperClient = AndroidFlipperClient.getInstance(this)
    val descriptorMapping = DescriptorMapping.withDefaults()
    LithoFlipperDescriptors.addWithSections(descriptorMapping)

    flipperClient.addPlugin(InspectorFlipperPlugin(this, descriptorMapping))
    flipperClient.addPlugin(FrescoFlipperPlugin())
    flipperClient.addPlugin(SeaMammalFlipperPlugin())
    flipperClient.start()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial.plugin

import com.facebook.flipper.core.FlipperConnection
import com.facebook.flipper.core.FlipperObject
import com.facebook.flipper.core.FlipperPlugin
import com.facebook.flipper.sample.tutorial.MarineMammals

class SeaMammalFlipperPlugin : FlipperPlugin {
  private var connection: FlipperConnection? = null

  override fun getId(): String = "sea-mammals"

  override fun onConnect(connection: FlipperConnection?) {
    this.connection = connection

    MarineMammals.list
        .mapIndexed { index, (title, picture_url) ->
          FlipperObject.Builder()
              .put("id", index)
              .put("title", title)
              .put("url", picture_url)
              .build()
        }
        .forEach(this::newRow)
  }

  override fun onDisconnect() {
    connection = null
  }

  override fun runInBackground(): Boolean = true

  private fun newRow(row: FlipperObject) {
    connection?.send("newRow", row)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial.ui

import com.facebook.flipper.sample.tutorial.MarineMammal
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.widget.Card
import com.facebook.yoga.YogaEdge.HORIZONTAL
import com.facebook.yoga.YogaEdge.VERTICAL

@LayoutSpec
object FeedItemCardSpec {

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop mammal: MarineMammal): Component =
      Column.create(c)
          .paddingDip(VERTICAL, 8f)
          .paddingDip(HORIZONTAL, 16f)
          .child(Card.create(c).content(MarineMammelComponent.create(c).mammal(mammal)))
          .build()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial.ui

import com.facebook.flipper.sample.tutorial.MarineMammal
import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.Children
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.annotations.GroupSectionSpec
import com.facebook.litho.sections.annotations.OnCreateChildren
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.RenderEvent
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.RenderInfo

@GroupSectionSpec
object FeedSectionSpec {
  @OnCreateChildren
  fun onCreateChildren(c: SectionContext, @Prop data: List<MarineMammal>): Children =
      Children.create()
          .child(
              DataDiffSection.create<MarineMammal>(c)
                  .data(data)
                  .renderEventHandler(FeedSection.render(c)))
          .build()

  @OnEvent(RenderEvent::class)
  fun render(c: SectionContext, @FromEvent model: MarineMammal): RenderInfo =
      ComponentRenderInfo.create().component(FeedItemCard.create(c).mammal(model).build()).build()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial.ui

import android.graphics.Typeface
import com.facebook.flipper.sample.tutorial.MarineMammal
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaEdge.BOTTOM
import com.facebook.yoga.YogaEdge.HORIZONTAL
import com.facebook.yoga.YogaEdge.LEFT
import com.facebook.yoga.YogaPositionType.ABSOLUTE

@LayoutSpec
object MarineMammelComponentSpec {
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop mammal: MarineMammal): Component =
      Column.create(c)
          .child(SingleImageComponent.create(c).image(mammal.picture_url).build())
          .child(
              Text.create(c)
                  .text(mammal.title)
                  .textStyle(Typeface.BOLD)
                  .textSizeDip(24f)
                  .backgroundColor(0xDDFFFFFF.toInt())
                  .positionType(ABSOLUTE)
                  .positionDip(BOTTOM, 4f)
                  .positionDip(LEFT, 4f)
                  .paddingDip(HORIZONTAL, 6f))
          .build()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial.ui

import com.facebook.flipper.sample.tutorial.MarineMammals
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.yoga.YogaEdge

@LayoutSpec
object RootComponentSpec {
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext): Component =
      RecyclerCollectionComponent.create(c)
          .disablePTR(true)
          .section(FeedSection.create(SectionContext(c)).data(MarineMammals.list).build())
          .paddingDip(YogaEdge.TOP, 8f)
          .build()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial.ui

import android.net.Uri
import com.facebook.fresco.vito.litho.FrescoVitoImage2
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.PropDefault

@LayoutSpec
object SingleImageComponentSpec {

  @get:PropDefault val imageAspectRatio = 1f

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop image: Uri,
      @Prop(optional = true) imageAspectRatio: Float
  ): Component = FrescoVitoImage2.create(c).uri(image).imageAspectRatio(imageAspectRatio).build()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.sample.tutorial

import org.junit.Assert.*
import org.junit.Test

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {
  @Test
  fun addition_isCorrect() {
    assertEquals(4, 2 + 2)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.cache

import android.graphics.Bitmap
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType
import com.facebook.fresco.animation.bitmap.BitmapFrameCache
import com.facebook.fresco.animation.bitmap.BitmapFrameCache.FrameCacheListener
import com.facebook.imagepipeline.animated.base.AnimatedImageResult
import com.facebook.imagepipeline.cache.AnimatedCache
import com.facebook.imagepipeline.cache.AnimationFrames

/** Bitmap frame cache used for animated drawables */
class FrescoFpsCache(
    animatedImageResult: AnimatedImageResult,
    private val maxFps: Int,
    private val animatedDrawableCache: AnimatedCache
) : BitmapFrameCache {

  /** Unique reference for this animation asset */
  private val cacheKey: String =
      animatedImageResult.source ?: animatedImageResult.image.hashCode().toString()

  /** Duration in ms of the animation */
  private val assetDurationMs = animatedImageResult.image.duration.toLong()

  /** Reference to the loaded animation */
  private var animationFrames: CloseableReference<AnimationFrames>? =
      animatedDrawableCache.findAnimation(cacheKey)

  override fun getCachedFrame(frameNumber: Int): CloseableReference<Bitmap>? {
    return safeAnimationFrames()?.getFrame(frameNumber)
  }

  override fun getFallbackFrame(frameNumber: Int): CloseableReference<Bitmap>? {
    return null
  }

  override fun getBitmapToReuseForFrame(
      frameNumber: Int,
      width: Int,
      height: Int
  ): CloseableReference<Bitmap>? {
    return null
  }

  override fun contains(frameNumber: Int): Boolean {
    return getCachedFrame(frameNumber) != null
  }

  override val sizeInBytes: Int
    get() = safeAnimationFrames()?.sizeBytes ?: 0

  override fun clear() {
    releaseCache()
  }

  private fun releaseCache() {
    animationFrames?.close()
    animationFrames = null
  }

  override fun isAnimationReady(): Boolean = safeAnimationFrames()?.frames.orEmpty().size > 1

  override fun onFrameRendered(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>,
      @FrameType frameType: Int
  ) = Unit

  override fun onFramePrepared(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>,
      @FrameType frameType: Int
  ) = Unit

  override fun onAnimationPrepared(frameBitmaps: Map<Int, CloseableReference<Bitmap>>): Boolean {
    val loadedFramesCount = safeAnimationFrames()?.frames.orEmpty().size

    // If saved quality is higher than new, then skip
    if (frameBitmaps.size < loadedFramesCount) {
      return true
    }

    val cacheRef = compressAnimation(frameBitmaps)
    if (cacheRef != null) {
      releaseCache()
      this.animationFrames = cacheRef
    }

    return cacheRef != null
  }

  /**
   * Creates a AnimationFrame reference allocated in AnimatedCache. This method compress the
   * animation based on the maximum fps allowed. The animation will reduce the number of frames
   * until it can be allocated in memory
   *
   * @param frameBitmaps has the bitmaps of the animation. FrameNumber -> Bitmap
   * @return AnimationFrame reference if the animation was saved in memory. Returns null if
   *   animation couldn't be allocated
   */
  private fun compressAnimation(
      frameBitmaps: Map<Int, CloseableReference<Bitmap>>
  ): CloseableReference<AnimationFrames>? {
    var fps = maxFps
    var animationFrames: CloseableReference<AnimationFrames>? = null

    while (animationFrames == null && fps > 1) {
      val realToCompressIndex = calculateReducedIndexes(frameBitmaps.size, fps)
      val compressedAnimation = releaseCompressedBitmaps(frameBitmaps, realToCompressIndex)
      val animation = AnimationFrames(compressedAnimation, realToCompressIndex)

      animationFrames = animatedDrawableCache.saveAnimation(cacheKey, animation)
      fps -= FPS_COMPRESSION_STEP
    }

    return animationFrames
  }

  /**
   * Create a Map<AssetBitmapIndex, CompressBitmapIndex> calculated based on the maximum FPS allowed
   *
   * @param animationFramesCount Number of frames extracted from the animation asset
   * @param fpsTarget Maximum fps
   * @return Map of equivalences between the original frame number and compress frame number
   */
  private fun calculateReducedIndexes(animationFramesCount: Int, fpsTarget: Int): Map<Int, Int> {
    val maxAllowedFrames = fpsTarget.times(assetDurationMs.div(1000f)).coerceAtLeast(0f)

    return if (maxAllowedFrames >= animationFramesCount) {
      (0 until animationFramesCount).associateWith { it }
    } else {
      val offset = maxAllowedFrames.div(animationFramesCount.toFloat())
      (0 until animationFramesCount).associateWith { it.times(offset).toInt() }
    }
  }

  /**
   * Compress animation releasing those bitmaps which wont be in the final animation
   *
   * @param frameBitmaps relation frameNumber->bitmap of the original animation
   * @param realToReducedIndex map of equivalences between originalFrameNumber->compressFrameNumber
   * @return map associating compressFrameNumber->bitmap
   */
  private fun releaseCompressedBitmaps(
      frameBitmaps: Map<Int, CloseableReference<Bitmap>>,
      realToReducedIndex: Map<Int, Int>
  ): Map<Int, CloseableReference<Bitmap>> {
    val compressedAnim = mutableMapOf<Int, CloseableReference<Bitmap>>()
    frameBitmaps.forEach { (i, bitmapRef) ->
      val reducedIndex = realToReducedIndex[i] ?: return@forEach
      if (compressedAnim.contains(reducedIndex)) {
        // Release this bitmap because it wont be used
        CloseableReference.closeSafely(bitmapRef)
      } else {
        compressedAnim[reducedIndex] = bitmapRef
      }
    }
    return compressedAnim
  }

  /**
   * Return animation frames based on the cacheKey. Check if current animationFrames exists,
   * otherwise fetch into AnimationCache in case the animation was loaded previously and they are
   * valid
   *
   * @return animation frames which contains the bitmaps of the animation
   */
  @Synchronized
  private fun safeAnimationFrames(): AnimationFrames? {
    val animatedCache =
        animationFrames ?: animatedDrawableCache.findAnimation(cacheKey) ?: return null

    // animatedCache instance is shared between this class and AnimatedCache class. Then we need to
    // specify that this instance cannot be modified when we perform .get()
    return synchronized(animatedCache) { if (animatedCache.isValid) animatedCache.get() else null }
  }

  override fun setFrameCacheListener(frameCacheListener: FrameCacheListener?) = Unit

  companion object {
    private const val FPS_COMPRESSION_STEP = 4
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap

import android.graphics.Bitmap
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.graphics.drawable.Drawable
import androidx.annotation.IntDef
import androidx.annotation.IntRange
import com.facebook.common.logging.FLog
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.backend.AnimationBackend
import com.facebook.fresco.animation.backend.AnimationBackendDelegateWithInactivityCheck.InactivityListener
import com.facebook.fresco.animation.backend.AnimationInformation
import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparationStrategy
import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparer
import com.facebook.fresco.vito.options.RoundingOptions
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory

/**
 * Bitmap animation backend that renders bitmap frames.
 *
 * The given [BitmapFrameCache] is used to cache frames and create new bitmaps.
 * [AnimationInformation] defines the main animation parameters, like frame and loop count.
 * [BitmapFrameRenderer] is used to render frames to the bitmaps aquired from the [BitmapFrameCache]
 * .
 */
class BitmapAnimationBackend(
    private val platformBitmapFactory: PlatformBitmapFactory,
    private val bitmapFrameCache: BitmapFrameCache,
    private val animationInformation: AnimationInformation,
    private val bitmapFrameRenderer: BitmapFrameRenderer,
    private val isNewRenderImplementation: Boolean,
    private val bitmapFramePreparationStrategy: BitmapFramePreparationStrategy?,
    private val bitmapFramePreparer: BitmapFramePreparer?,
    roundingOptions: RoundingOptions? = null,
) : AnimationBackend, InactivityListener {

  val cornerRadii: FloatArray? =
      roundingOptions?.let { roundingOptions ->
        if (roundingOptions.cornerRadius != RoundingOptions.CORNER_RADIUS_UNSET) {
          val corners = FloatArray(8)
          corners.fill(roundingOptions.cornerRadius)
          corners
        } else {
          roundingOptions.cornerRadii
        }
      }

  interface FrameListener {
    /**
     * Called when the backend started drawing the given frame.
     *
     * @param backend the backend
     * @param frameNumber the frame number to be drawn
     */
    fun onDrawFrameStart(backend: BitmapAnimationBackend, frameNumber: Int)

    /**
     * Called when the given frame has been drawn.
     *
     * @param backend the backend
     * @param frameNumber the frame number that has been drawn
     * @param frameType the [FrameType] that has been drawn
     */
    fun onFrameDrawn(backend: BitmapAnimationBackend, frameNumber: Int, @FrameType frameType: Int)

    /**
     * Called when no bitmap could be drawn by the backend for the given frame number.
     *
     * @param backend the backend
     * @param frameNumber the frame number that could not be drawn
     */
    fun onFrameDropped(backend: BitmapAnimationBackend, frameNumber: Int)
  }

  /** Frame type that has been drawn. Can be used for logging. */
  @Retention(AnnotationRetention.SOURCE)
  @IntDef(
      FRAME_TYPE_UNKNOWN,
      FRAME_TYPE_CACHED,
      FRAME_TYPE_REUSED,
      FRAME_TYPE_CREATED,
      FRAME_TYPE_FALLBACK)
  annotation class FrameType

  private val bitmapConfig = Bitmap.Config.ARGB_8888

  private val paint: Paint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.DITHER_FLAG)
  private var bounds: Rect? = null
  private var bitmapWidth = 0
  private var bitmapHeight = 0

  private val path: Path = Path()
  private val matrix: Matrix = Matrix()
  private var pathFrameNumber: Int = -1

  private var frameListener: FrameListener? = null
  private var animationListener: AnimationBackend.Listener? = null

  init {
    updateBitmapDimensions()
  }

  fun setFrameListener(frameListener: FrameListener?) {
    this.frameListener = frameListener
  }

  override fun getFrameCount(): Int = animationInformation.frameCount

  override fun getFrameDurationMs(frameNumber: Int): Int =
      animationInformation.getFrameDurationMs(frameNumber)

  override fun width(): Int = animationInformation.width()

  override fun height(): Int = animationInformation.height()

  override fun getLoopDurationMs(): Int = animationInformation.loopDurationMs

  override fun getLoopCount(): Int = animationInformation.loopCount

  override fun drawFrame(parent: Drawable, canvas: Canvas, frameNumber: Int): Boolean {
    frameListener?.onDrawFrameStart(this, frameNumber)
    val drawn = drawFrameOrFallback(canvas, frameNumber, FRAME_TYPE_CACHED)

    // We could not draw anything
    if (!drawn) {
      frameListener?.onFrameDropped(this, frameNumber)
    }

    // Prepare next frames
    if (!isNewRenderImplementation && bitmapFramePreparer != null) {
      bitmapFramePreparationStrategy?.prepareFrames(
          bitmapFramePreparer, bitmapFrameCache, this, frameNumber)
    }
    return drawn
  }

  private fun drawFrameOrFallback(
      canvas: Canvas,
      frameNumber: Int,
      @FrameType frameType: Int
  ): Boolean {
    var bitmapReference: CloseableReference<Bitmap>? = null
    val drawn: Boolean
    var nextFrameType = FRAME_TYPE_UNKNOWN

    if (isNewRenderImplementation) {
      bitmapReference =
          bitmapFramePreparationStrategy?.getBitmapFrame(frameNumber, canvas.width, canvas.height)

      if (bitmapReference != null && bitmapReference.isValid) {
        drawBitmap(frameNumber, bitmapReference.get(), canvas)
        return true
      }

      // If bitmap couldnt be drawn, then fetch frames
      bitmapFramePreparationStrategy?.prepareFrames(canvas.width, canvas.height, null)
      return false
    }

    try {
      when (frameType) {
        FRAME_TYPE_CACHED -> {
          bitmapReference = bitmapFrameCache.getCachedFrame(frameNumber)
          drawn = drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_CACHED)
          nextFrameType = FRAME_TYPE_REUSED
        }
        FRAME_TYPE_REUSED -> {
          bitmapReference =
              bitmapFrameCache.getBitmapToReuseForFrame(frameNumber, bitmapWidth, bitmapHeight)
          // Try to render the frame and draw on the canvas immediately after
          drawn =
              (renderFrameInBitmap(frameNumber, bitmapReference) &&
                  drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_REUSED))
          nextFrameType = FRAME_TYPE_CREATED
        }
        FRAME_TYPE_CREATED -> {
          bitmapReference =
              try {
                platformBitmapFactory.createBitmap(bitmapWidth, bitmapHeight, bitmapConfig)
              } catch (e: RuntimeException) {
                // Failed to create the bitmap for the frame, return and report that we could not
                // draw the frame.
                FLog.w(TAG, "Failed to create frame bitmap", e)
                return false
              }
          // Try to render the frame and draw on the canvas immediately after
          drawn =
              (renderFrameInBitmap(frameNumber, bitmapReference) &&
                  drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_CREATED))
          nextFrameType = FRAME_TYPE_FALLBACK
        }
        FRAME_TYPE_FALLBACK -> {
          bitmapReference = bitmapFrameCache.getFallbackFrame(frameNumber)
          drawn = drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_FALLBACK)
        }
        else -> return false
      }
    } finally {
      CloseableReference.closeSafely(bitmapReference)
    }
    return if (drawn || nextFrameType == FRAME_TYPE_UNKNOWN) {
      drawn
    } else {
      drawFrameOrFallback(canvas, frameNumber, nextFrameType)
    }
  }

  override fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) {
    paint.alpha = alpha
  }

  override fun setColorFilter(colorFilter: ColorFilter?) {
    paint.colorFilter = colorFilter
  }

  override fun setBounds(bounds: Rect?) {
    this.bounds = bounds
    bitmapFrameRenderer.setBounds(bounds)
    updateBitmapDimensions()
  }

  override fun getIntrinsicWidth(): Int = bitmapWidth

  override fun getIntrinsicHeight(): Int = bitmapHeight

  override fun getSizeInBytes(): Int = bitmapFrameCache.sizeInBytes

  override fun clear() {
    if (isNewRenderImplementation) {
      bitmapFramePreparationStrategy?.clearFrames()
    } else {
      bitmapFrameCache.clear()
    }
  }

  override fun preloadAnimation() {
    if (!isNewRenderImplementation && bitmapFramePreparer != null) {
      bitmapFramePreparationStrategy?.prepareFrames(
          bitmapFramePreparer, bitmapFrameCache, this, 0) {
            animationListener?.onAnimationLoaded()
          }
    } else {
      bitmapFramePreparationStrategy?.prepareFrames(
          animationInformation.width(), animationInformation.height()) {
            animationListener?.onAnimationLoaded()
          }
    }
  }

  override fun onInactive() {
    if (isNewRenderImplementation) {
      bitmapFramePreparationStrategy?.onStop()
    } else {
      clear()
    }
  }

  override fun setAnimationListener(listener: AnimationBackend.Listener?) {
    animationListener = listener
  }

  private fun updateBitmapDimensions() {
    // Calculate the correct bitmap dimensions
    bitmapWidth = bitmapFrameRenderer.intrinsicWidth
    if (bitmapWidth == AnimationBackend.INTRINSIC_DIMENSION_UNSET) {
      bitmapWidth = bounds?.width() ?: AnimationBackend.INTRINSIC_DIMENSION_UNSET
    }
    bitmapHeight = bitmapFrameRenderer.intrinsicHeight
    if (bitmapHeight == AnimationBackend.INTRINSIC_DIMENSION_UNSET) {
      bitmapHeight = bounds?.height() ?: AnimationBackend.INTRINSIC_DIMENSION_UNSET
    }
  }

  /**
   * Try to render the frame to the given target bitmap. If the rendering fails, the target bitmap
   * reference will be closed and false is returned. If rendering succeeds, the target bitmap
   * reference can be drawn and has to be manually closed after drawing has been completed.
   *
   * @param frameNumber the frame number to render
   * @param targetBitmap the target bitmap
   * @return true if rendering successful
   */
  private fun renderFrameInBitmap(
      frameNumber: Int,
      targetBitmap: CloseableReference<Bitmap>?
  ): Boolean {
    if (targetBitmap == null || !targetBitmap.isValid) {
      return false
    }
    // Render the image
    val frameRendered = bitmapFrameRenderer.renderFrame(frameNumber, targetBitmap.get())
    if (!frameRendered) {
      CloseableReference.closeSafely(targetBitmap)
    }
    return frameRendered
  }

  private fun updatePath(
      frameNumber: Int,
      bitmap: Bitmap,
      currentBoundsWidth: Float,
      currentBoundsHeight: Float
  ): Boolean {
    if (cornerRadii == null) {
      return false
    }
    if (frameNumber == pathFrameNumber) {
      return true
    }

    val bitmapShader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    val src = RectF(0f, 0f, bitmapWidth.toFloat(), bitmapHeight.toFloat())
    val dst = RectF(0f, 0f, currentBoundsWidth, currentBoundsHeight)
    matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL)
    bitmapShader.setLocalMatrix(matrix)
    paint.shader = bitmapShader
    path.addRoundRect(
        RectF(0f, 0f, currentBoundsWidth, currentBoundsHeight), cornerRadii, Path.Direction.CW)

    pathFrameNumber = frameNumber
    return true
  }

  private fun drawBitmap(frameNumber: Int, bitmap: Bitmap, canvas: Canvas) {
    val currentBounds = bounds

    if (currentBounds == null) {
      canvas.drawBitmap(bitmap, 0f, 0f, paint)
    } else {
      if (updatePath(
          frameNumber, bitmap, currentBounds.width().toFloat(), currentBounds.height().toFloat())) {
        canvas.drawPath(path, paint)
      } else {
        canvas.drawBitmap(bitmap, null, currentBounds, paint)
      }
    }
  }

  /**
   * Helper method that draws the given bitmap on the canvas respecting the bounds (if set).
   *
   * If rendering was successful, it notifies the cache that the frame has been rendered with the
   * given bitmap. In addition, it will notify the [FrameListener] if set.
   *
   * @param frameNumber the current frame number passed to the cache
   * @param bitmapReference the bitmap to draw
   * @param canvas the canvas to draw an
   * @param frameType the [FrameType] to be rendered
   * @return true if the bitmap has been drawn
   */
  private fun drawBitmapAndCache(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>?,
      canvas: Canvas,
      @FrameType frameType: Int
  ): Boolean {
    if (bitmapReference == null || !CloseableReference.isValid(bitmapReference)) {
      return false
    }

    this.drawBitmap(frameNumber, bitmapReference.get(), canvas)

    // Notify the cache that a frame has been rendered.
    // We should not cache fallback frames since they do not represent the actual frame.
    if (frameType != FRAME_TYPE_FALLBACK && !isNewRenderImplementation) {
      bitmapFrameCache.onFrameRendered(frameNumber, bitmapReference, frameType)
    }
    frameListener?.onFrameDrawn(this, frameNumber, frameType)

    return true
  }

  companion object {
    const val FRAME_TYPE_UNKNOWN: Int = -1
    const val FRAME_TYPE_CACHED = 0
    const val FRAME_TYPE_REUSED = 1
    const val FRAME_TYPE_CREATED = 2
    const val FRAME_TYPE_FALLBACK = 3
    private val TAG = BitmapAnimationBackend::class.java
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap

import android.graphics.Bitmap
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType

/** Bitmap frame cache that is used for animated images. */
interface BitmapFrameCache {

  interface FrameCacheListener {
    /**
     * Called when the frame for the given frame number has been put in the frame cache.
     *
     * @param bitmapFrameCache the frame cache that holds the frame
     * @param frameNumber the cached frame number
     */
    fun onFrameCached(bitmapFrameCache: BitmapFrameCache, frameNumber: Int)

    /**
     * Called when the frame for the given frame number has been evicted from the frame cache.
     *
     * @param bitmapFrameCache the frame cache that evicted the frame
     * @param frameNumber the frame number of the evicted frame
     */
    fun onFrameEvicted(bitmapFrameCache: BitmapFrameCache, frameNumber: Int)
  }

  /**
   * Get the cached frame for the given frame number.
   *
   * @param frameNumber the frame number to get the cached frame for
   * @return the cached frame or null if not cached
   */
  fun getCachedFrame(frameNumber: Int): CloseableReference<Bitmap>?

  /**
   * Get a fallback frame for the given frame number. This method is called if all other attempts to
   * draw a frame failed. The bitmap returned could for example be the last drawn frame (if any).
   *
   * @param frameNumber the frame number to get the fallback
   * @return the fallback frame or null if not cached
   */
  fun getFallbackFrame(frameNumber: Int): CloseableReference<Bitmap>?

  /**
   * Return a reusable bitmap that should be used to render the given frame.
   *
   * @param frameNumber the frame number to be rendered
   * @param width the width of the target bitmap
   * @param height the height of the target bitmap
   * @return the reusable bitmap or null if no reusable bitmaps available
   */
  fun getBitmapToReuseForFrame(
      frameNumber: Int,
      width: Int,
      height: Int
  ): CloseableReference<Bitmap>?

  /**
   * Check whether the cache contains a certain frame.
   *
   * @param frameNumber the frame number to check
   * @return true if the frame is cached
   */
  operator fun contains(frameNumber: Int): Boolean

  /** @return the size in bytes of all cached data */
  val sizeInBytes: Int

  /** Send the list of frames when the animation frames are loaded */
  fun onAnimationPrepared(frameBitmaps: Map<Int, CloseableReference<Bitmap>>): Boolean = true

  /** Clear the cache. */
  fun clear()

  /** Indicates if animation is loaded in cache and ready for usage */
  fun isAnimationReady(): Boolean = false

  /**
   * Callback when the given bitmap has been drawn to a canvas. This bitmap can either be a reused
   * bitmap returned by [getBitmapToReuseForFrame(int, int, int)] or a new bitmap.
   *
   * Note: the implementation of this interface must manually clone the given bitmap reference if it
   * wants to hold on to the bitmap. The original reference will be automatically closed after this
   * call.
   *
   * @param frameNumber the frame number that has been rendered
   * @param bitmapReference the bitmap reference that has been rendered
   * @param frameType the frame type that has been rendered
   */
  fun onFrameRendered(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>,
      @FrameType frameType: Int
  )

  /**
   * Callback when a bitmap reference for a given frame has been prepared for future rendering.
   *
   * This method is called ahead of render time (i.e. when future frames have been prepared in the
   * background), whereas [onFrameRendered(int, CloseableReference, int)] is invoked when the actual
   * frame has been drawn on a Canvas.
   *
   * The supplied bitmap reference can either hold a reused bitmap returned by
   * [getBitmapToReuseForFrame(int, int, int)] or a new bitmap as indicated by the frame type
   * parameter.
   *
   * Note: the implementation of this interface must manually clone the given bitmap reference if it
   * wants to hold on to the bitmap. The original reference will be automatically closed after this
   * call.
   *
   * @param frameNumber the frame number of the passed bitmapReference
   * @param bitmapReference the bitmap reference that has been prepared for future rendering
   * @param frameType the frame type of the prepared frame
   * @return true if the frame has been successfully cached
   */
  fun onFramePrepared(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>,
      @FrameType frameType: Int
  )

  /**
   * Set a frame cache listener that gets notified about caching events.
   *
   * @param frameCacheListener the listener to use
   */
  fun setFrameCacheListener(frameCacheListener: FrameCacheListener?)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap

import android.graphics.Bitmap
import android.graphics.Rect

/**
 * Bitmap frame renderer used by [BitmapAnimationBackend] to render animated images (e.g. GIFs or
 * animated WebPs).
 */
interface BitmapFrameRenderer {

  /**
   * Render the frame for the given frame number to the target bitmap.
   *
   * @param frameNumber the frame number to render
   * @param targetBitmap the bitmap to render the frame in
   * @return true if successful
   */
  fun renderFrame(frameNumber: Int, targetBitmap: Bitmap): Boolean

  /**
   * Set the parent drawable bounds to be used for frame rendering.
   *
   * @param bounds the bounds to use
   */
  fun setBounds(bounds: Rect?)

  /**
   * Return the intrinsic width of bitmap frames. Return
   * [AnimationBackend#INTRINSIC_DIMENSION_UNSET] if no specific width is set.
   *
   * @return the intrinsic width
   */
  val intrinsicWidth: Int

  /**
   * Return the intrinsic height of bitmap frames. Return
   * [AnimationBackend#INTRINSIC_DIMENSION_UNSET] if no specific height is set.
   *
   * @return the intrinsic height
   */
  val intrinsicHeight: Int
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.cache

import android.graphics.Bitmap
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType
import com.facebook.fresco.animation.bitmap.BitmapFrameCache
import com.facebook.fresco.animation.bitmap.BitmapFrameCache.FrameCacheListener
import com.facebook.imageutils.BitmapUtil
import javax.annotation.concurrent.GuardedBy

/** Simple bitmap cache that keeps the last frame and reuses it if possible. */
class KeepLastFrameCache : BitmapFrameCache {

  private var lastFrameNumber = FRAME_NUMBER_UNSET
  private var frameCacheListener: FrameCacheListener? = null

  @GuardedBy("this") private var lastBitmapReference: CloseableReference<Bitmap>? = null

  @Synchronized
  override fun getCachedFrame(frameNumber: Int): CloseableReference<Bitmap>? =
      if (lastFrameNumber == frameNumber) {
        CloseableReference.cloneOrNull(lastBitmapReference)
      } else {
        null
      }

  @Synchronized
  override fun getFallbackFrame(frameNumber: Int): CloseableReference<Bitmap>? =
      CloseableReference.cloneOrNull(lastBitmapReference)

  @Synchronized
  override fun getBitmapToReuseForFrame(
      frameNumber: Int,
      width: Int,
      height: Int
  ): CloseableReference<Bitmap>? =
      try {
        CloseableReference.cloneOrNull(lastBitmapReference)
      } finally {
        closeAndResetLastBitmapReference()
      }

  @Synchronized
  override fun contains(frameNumber: Int): Boolean =
      frameNumber == lastFrameNumber && CloseableReference.isValid(lastBitmapReference)

  @get:Synchronized
  override val sizeInBytes: Int
    get() =
        if (lastBitmapReference == null) 0
        else BitmapUtil.getSizeInBytes(lastBitmapReference!!.get())

  @Synchronized
  override fun clear() {
    closeAndResetLastBitmapReference()
  }

  @Synchronized
  override fun onFrameRendered(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>,
      @FrameType frameType: Int
  ) {
    if (lastBitmapReference != null && bitmapReference.get() == lastBitmapReference?.get()) {
      return
    }
    CloseableReference.closeSafely(lastBitmapReference)
    if (lastFrameNumber != FRAME_NUMBER_UNSET) {
      frameCacheListener?.onFrameEvicted(this, lastFrameNumber)
    }
    lastBitmapReference = CloseableReference.cloneOrNull(bitmapReference)
    frameCacheListener?.onFrameCached(this, frameNumber)
    lastFrameNumber = frameNumber
  }

  override fun onFramePrepared(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>,
      @FrameType frameType: Int
  ) = Unit

  override fun setFrameCacheListener(frameCacheListener: FrameCacheListener?) {
    this.frameCacheListener = frameCacheListener
  }

  @Synchronized
  private fun closeAndResetLastBitmapReference() {
    if (lastFrameNumber != FRAME_NUMBER_UNSET) {
      frameCacheListener?.onFrameEvicted(this, lastFrameNumber)
    }
    CloseableReference.closeSafely(lastBitmapReference)
    lastBitmapReference = null
    lastFrameNumber = FRAME_NUMBER_UNSET
  }

  companion object {
    private const val FRAME_NUMBER_UNSET = -1
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.cache

import android.graphics.Bitmap
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType
import com.facebook.fresco.animation.bitmap.BitmapFrameCache
import com.facebook.fresco.animation.bitmap.BitmapFrameCache.FrameCacheListener

/** No-op bitmap cache that doesn't do anything. */
class NoOpCache : BitmapFrameCache {

  override fun getCachedFrame(frameNumber: Int): CloseableReference<Bitmap>? = null

  override fun getFallbackFrame(frameNumber: Int): CloseableReference<Bitmap>? = null

  override fun getBitmapToReuseForFrame(
      frameNumber: Int,
      width: Int,
      height: Int
  ): CloseableReference<Bitmap>? = null

  override fun contains(frameNumber: Int): Boolean = false

  override val sizeInBytes: Int = 0

  override fun clear() {
    // no-op
  }

  override fun onFrameRendered(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>,
      @FrameType frameType: Int
  ) {
    // no-op
  }

  override fun onFramePrepared(
      frameNumber: Int,
      bitmapReference: CloseableReference<Bitmap>,
      @FrameType frameType: Int
  ) {
    // Does not cache anything
  }

  override fun setFrameCacheListener(frameCacheListener: FrameCacheListener?) {
    // Does not cache anything
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation

import android.graphics.Bitmap
import android.os.SystemClock
import androidx.annotation.UiThread
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.backend.AnimationInformation
import com.facebook.fresco.animation.bitmap.BitmapFrameCache
import com.facebook.fresco.animation.bitmap.preparation.loadframe.AnimationLoaderExecutor
import com.facebook.fresco.animation.bitmap.preparation.loadframe.LoadFrameOutput
import com.facebook.fresco.animation.bitmap.preparation.loadframe.LoadFrameTask
import com.facebook.fresco.animation.bitmap.preparation.loadframe.LoadFrameTaskFactory
import java.io.Closeable
import java.util.SortedSet
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.ceil

/**
 * Balanced strategy consist on retrieving the animation frames balancing between RAM and CPU.
 * - RAM: Bitmap cache allocates the bitmaps in memory
 * - CPU: OnDemand threads load in advance the next missing bitmap in cache
 *
 * This strategy will increase the CPU usage in order to reduce the RAM allocation. Depending on the
 * asset FPS, the RAM could decrease 15-30%. [ON_DEMAND_PREPARATION_TIME_MS] indicates how many ms
 * needs the OnDemand thread pool to load the next frame:
 * - greater onDemant preparation => lower CPU usage + greater RAM usage
 *
 * Due to this strategy will be unique per animation, it is highly recommended
 * [ON_DEMAND_PREPARATION_TIME_MS] >= 200ms
 */
class BalancedAnimationStrategy(
    animationInformation: AnimationInformation,
    onDemandPreparationMs: Int,
    private val loadFrameTaskFactory: LoadFrameTaskFactory,
    private val bitmapCache: BitmapFrameCache,
    private val downscaleFrameToDrawableDimensions: Boolean,
) : BitmapFramePreparationStrategy {

  private val fetchingFrames = AtomicBoolean(false)
  private val fetchingOnDemand = AtomicBoolean(false)
  private val framesCached: Boolean
    get() = bitmapCache.isAnimationReady()

  private val onDemandFrames: SortedSet<Int> = sortedSetOf()
  private var nextPrepareFrames = SystemClock.uptimeMillis()

  private val frameCount: Int = animationInformation.frameCount
  private val animationWidth: Int = animationInformation.width()
  private val animationHeight: Int = animationInformation.height()
  private val onDemandRatio: Int
  private var onDemandBitmap: OnDemandFrame? = null

  init {
    val avgFrameDurationMs = animationInformation.loopDurationMs.div(frameCount)
    val optimisticRatio = ceil(onDemandPreparationMs.div(avgFrameDurationMs.toFloat())).toInt()
    onDemandRatio = optimisticRatio.coerceAtLeast(2)
  }

  private fun isFirstFrameReady() = bitmapCache.getCachedFrame(0)?.isValid == true

  @UiThread
  override fun prepareFrames(
      canvasWidth: Int,
      canvasHeight: Int,
      onAnimationLoaded: (() -> Unit)?
  ) {
    // Validate inputs
    if (canvasWidth <= 0 || canvasHeight <= 0 || animationWidth <= 0 || animationHeight <= 0) {
      return
    }

    // Validate status
    if (framesCached || fetchingFrames.get() || SystemClock.uptimeMillis() < nextPrepareFrames) {
      if (framesCached) {
        onAnimationLoaded?.invoke()
      }
      return
    }

    fetchingFrames.set(true)
    val frameSize = calculateFrameSize(canvasWidth, canvasHeight)

    val task =
        if (!isFirstFrameReady()) {
          loadFrameTaskFactory.createFirstFrameTask(
              frameSize.width,
              frameSize.height,
              object : LoadFrameOutput {
                override fun onSuccess(frames: Map<Int, CloseableReference<Bitmap>>) {
                  val cachedSucceed = bitmapCache.onAnimationPrepared(frames)
                  if (!cachedSucceed) {
                    nextPrepareFrames = SystemClock.uptimeMillis() + FETCH_FIRST_CACHE_DELAY_MS
                  }

                  // Once first frame is loaded, then load the rest of frames
                  AnimationLoaderExecutor.execute(loadAllFrames(frameSize, onAnimationLoaded))
                }

                override fun onFail() {
                  bitmapCache.clear()
                  fetchingFrames.set(false)
                }
              })
        } else {
          loadAllFrames(frameSize, onAnimationLoaded)
        }

    AnimationLoaderExecutor.execute(task)
  }

  private fun loadAllFrames(frameSize: Size, notifyOnLoad: (() -> Unit)?): LoadFrameTask {
    return loadFrameTaskFactory.createLoadFullAnimationTask(
        frameSize.width,
        frameSize.height,
        frameCount,
        object : LoadFrameOutput {
          override fun onSuccess(frames: Map<Int, CloseableReference<Bitmap>>) {
            onDemandFrames.clear()
            onDemandFrames.addAll(frames.filter { isOnDemandFrame(it.key) }.map { it.key })

            val memoryFrames = frames.filter { !onDemandFrames.contains(it.key) }

            val cachedSucceed = bitmapCache.onAnimationPrepared(memoryFrames)
            if (!cachedSucceed) {
              nextPrepareFrames = SystemClock.uptimeMillis() + FETCH_FULL_ANIMATION_CACHE_DELAY_MS
            }

            notifyOnLoad?.invoke()
            fetchingFrames.set(false)
          }

          override fun onFail() {
            bitmapCache.clear()
            fetchingFrames.set(false)
          }
        })
  }

  @UiThread
  override fun getBitmapFrame(
      frameNumber: Int,
      canvasWidth: Int,
      canvasHeight: Int
  ): CloseableReference<Bitmap>? {
    // Find the bitmap in cache memory (RAM)
    val cache = bitmapCache.getCachedFrame(frameNumber)
    if (cache?.isValid == true) {
      prepareNextOnDemandFrame(frameNumber)
      return cache
    }

    // Check if bitmap should be cached
    if (!isOnDemandFrame(frameNumber)) {
      prepareFrames(canvasWidth, canvasHeight) {}
    }

    if (onDemandBitmap?.isValidFor(frameNumber) == true) {
      return onDemandBitmap?.bitmap
    }

    return findNearestFrame(frameNumber)
  }

  private fun prepareNextOnDemandFrame(lastFrameRendered: Int) {
    if (fetchingOnDemand.getAndSet(true)) {
      return
    }

    val nextFrame = findNextOnDemandFrame(lastFrameRendered)
    if (nextFrame != null && onDemandBitmap?.isValidFor(nextFrame) != true) {
      val onDemandTask =
          loadFrameTaskFactory.createOnDemandTask(
              nextFrame,
              { bitmapCache.getCachedFrame(it) },
              { bitmap ->
                if (bitmap != null) {
                  onDemandBitmap = OnDemandFrame(nextFrame, bitmap)
                }
                fetchingOnDemand.set(false)
              })
      AnimationLoaderExecutor.execute(onDemandTask)
    } else {
      fetchingOnDemand.set(false)
    }
  }

  /** Calculate if [frameNumber] have to be rendered by onDemand (CPU) */
  private fun isOnDemandFrame(frameNumber: Int): Boolean {
    // If onDemandRatio > frame count, means that we disable onDemand generation
    if (onDemandRatio > frameCount) {
      return false
    }
    // Compare with 1 because we don't want to onDemand the first frame
    return frameNumber % onDemandRatio == 1
  }

  private fun findNearestFrame(fromFrame: Int): CloseableReference<Bitmap>? {
    return (fromFrame downTo 0).asSequence().firstNotNullOfOrNull {
      val frame = bitmapCache.getCachedFrame(it)
      if (frame?.isValid == true) frame else null
    }
  }

  override fun onStop() {
    onDemandBitmap?.close()
    bitmapCache.clear()
  }

  override fun clearFrames() {
    bitmapCache.clear()
  }

  /** Find next frame since [from] index which is missing in cache */
  @UiThread
  private fun findNextOnDemandFrame(from: Int): Int? {
    if (onDemandFrames.isEmpty()) {
      return null
    }

    return onDemandFrames.firstOrNull { it > from } ?: onDemandFrames.first()
  }

  private fun calculateFrameSize(canvasWidth: Int, canvasHeight: Int): Size {
    if (!downscaleFrameToDrawableDimensions) {
      return Size(animationWidth, animationHeight)
    }

    var bitmapWidth: Int = animationWidth
    var bitmapHeight: Int = animationHeight

    // The maximum size for the bitmap is the size of the animation if the canvas is bigger
    if (canvasWidth < animationWidth || canvasHeight < animationHeight) {
      val ratioW = animationWidth.toDouble().div(animationHeight)
      if (canvasHeight > canvasWidth) {
        bitmapHeight = canvasHeight.coerceAtMost(animationHeight)
        bitmapWidth = bitmapHeight.times(ratioW).toInt()
      } else {
        bitmapWidth = canvasWidth.coerceAtMost(animationWidth)
        bitmapHeight = bitmapWidth.div(ratioW).toInt()
      }
    }

    return Size(bitmapWidth, bitmapHeight)
  }

  companion object {
    private val FETCH_FIRST_CACHE_DELAY_MS = 500
    private val FETCH_FULL_ANIMATION_CACHE_DELAY_MS = TimeUnit.SECONDS.toMillis(5)
  }
}

private class Size(val width: Int, val height: Int)

private class OnDemandFrame(val frameNumber: Int, val bitmap: CloseableReference<Bitmap>) :
    Closeable {
  fun isValidFor(frameNumber: Int): Boolean = this.frameNumber == frameNumber && bitmap.isValid

  override fun close() {
    bitmap.close()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation

import android.graphics.Bitmap
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.backend.AnimationBackend
import com.facebook.fresco.animation.bitmap.BitmapFrameCache

/** Frame preparation strategy to prepare next animation frames. */
interface BitmapFramePreparationStrategy {
  /**
   * Decide whether frames should be prepared ahead of time when a frame is drawn.
   *
   * @param bitmapFramePreparer the preparer to be used to create frames
   * @param bitmapFrameCache the cache to pass to the preparer
   * @param animationBackend the animation backend to prepare frames for
   * @param lastDrawnFrameNumber the last drawn frame number
   */
  fun prepareFrames(
      bitmapFramePreparer: BitmapFramePreparer,
      bitmapFrameCache: BitmapFrameCache,
      animationBackend: AnimationBackend,
      lastDrawnFrameNumber: Int,
      onAnimationLoaded: (() -> Unit)? = null
  ) = Unit

  /**
   * Prepare the frames for the animation giving the size of the animation and the size of the
   * canvas
   *
   * @param onAnimationLoaded This callback is invoked every time that bitmaps are loaded in memory
   *   successfully. This can be triggered several times because animation could be dropped from
   *   memory and then loaded again
   */
  fun prepareFrames(canvasWidth: Int, canvasHeight: Int, onAnimationLoaded: (() -> Unit)?) = Unit

  /** Force stop any task running on the strategy */
  fun onStop() = Unit

  /** Clear the frames from cache */
  fun clearFrames() = Unit

  fun getBitmapFrame(
      frameNumber: Int,
      canvasWidth: Int,
      canvasHeight: Int
  ): CloseableReference<Bitmap>? = null
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation

import com.facebook.fresco.animation.backend.AnimationBackend
import com.facebook.fresco.animation.bitmap.BitmapFrameCache

/** Prepare frames for animated images ahead of time. */
interface BitmapFramePreparer {
  /**
   * Prepare the frame with the given frame number and notify the supplied bitmap frame cache once
   * the frame is ready by calling [BitmapFrameCache#onFramePrepared(int, CloseableReference, int)]
   *
   * @param bitmapFrameCache the cache to notify for prepared frames
   * @param animationBackend the backend to prepare frames for
   * @param frameNumber
   * @return
   */
  fun prepareFrame(
      bitmapFrameCache: BitmapFrameCache,
      animationBackend: AnimationBackend,
      frameNumber: Int
  ): Boolean
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation

import android.graphics.Bitmap
import android.util.SparseArray
import com.facebook.common.logging.FLog
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.backend.AnimationBackend
import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend
import com.facebook.fresco.animation.bitmap.BitmapAnimationBackend.FrameType
import com.facebook.fresco.animation.bitmap.BitmapFrameCache
import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory
import java.lang.RuntimeException
import java.util.concurrent.ExecutorService

/**
 * Default bitmap frame preparer that uses the given [ExecutorService] to schedule jobs. An instance
 * of this class can be shared between multiple animated images.
 */
class DefaultBitmapFramePreparer(
    private val platformBitmapFactory: PlatformBitmapFactory,
    private val bitmapFrameRenderer: BitmapFrameRenderer,
    private val bitmapConfig: Bitmap.Config,
    private val executorService: ExecutorService
) : BitmapFramePreparer {

  private val TAG = DefaultBitmapFramePreparer::class.java

  private val pendingFrameDecodeJobs: SparseArray<Runnable> = SparseArray()

  override fun prepareFrame(
      bitmapFrameCache: BitmapFrameCache,
      animationBackend: AnimationBackend,
      frameNumber: Int
  ): Boolean {
    // Create a unique ID to identify the frame for the given backend.
    val frameId = getUniqueId(animationBackend, frameNumber)
    synchronized(pendingFrameDecodeJobs) {

      // Check if already scheduled.
      if (pendingFrameDecodeJobs[frameId] != null) {
        FLog.v(TAG, "Already scheduled decode job for frame %d", frameNumber)
        return true
      }

      // Check if already cached.
      if (bitmapFrameCache.contains(frameNumber)) {
        FLog.v(TAG, "Frame %d is cached already.", frameNumber)
        return true
      }

      val frameDecodeRunnable =
          FrameDecodeRunnable(animationBackend, bitmapFrameCache, frameNumber, frameId)
      pendingFrameDecodeJobs.put(frameId, frameDecodeRunnable)
      executorService.execute(frameDecodeRunnable)
    }
    return true
  }

  private inner class FrameDecodeRunnable(
      private val animationBackend: AnimationBackend,
      private val bitmapFrameCache: BitmapFrameCache,
      private val frameNumber: Int,
      private val frameId: Int
  ) : Runnable {
    override fun run() {
      try {
        // If we have a cached frame already, we don't need to do anything.
        if (bitmapFrameCache.contains(frameNumber)) {
          FLog.v(TAG, "Frame %d is cached already.", frameNumber)
          return
        }

        // Prepare the frame.
        if (prepareFrameAndCache(frameNumber, BitmapAnimationBackend.FRAME_TYPE_REUSED)) {
          FLog.v(TAG, "Prepared frame %d.", frameNumber)
        } else {
          FLog.e(TAG, "Could not prepare frame %d.", frameNumber)
        }
      } finally {
        synchronized(pendingFrameDecodeJobs) { pendingFrameDecodeJobs.remove(frameId) }
      }
    }

    private fun prepareFrameAndCache(frameNumber: Int, @FrameType frameType: Int): Boolean {
      var bitmapReference: CloseableReference<Bitmap>? = null
      val created: Boolean
      val nextFrameType: Int
      try {
        when (frameType) {
          BitmapAnimationBackend.FRAME_TYPE_REUSED -> {
            bitmapReference =
                bitmapFrameCache.getBitmapToReuseForFrame(
                    frameNumber, animationBackend.intrinsicWidth, animationBackend.intrinsicHeight)
            nextFrameType = BitmapAnimationBackend.FRAME_TYPE_CREATED
          }
          BitmapAnimationBackend.FRAME_TYPE_CREATED -> {
            bitmapReference =
                try {
                  platformBitmapFactory.createBitmap(
                      animationBackend.intrinsicWidth,
                      animationBackend.intrinsicHeight,
                      bitmapConfig)
                } catch (e: RuntimeException) {
                  // Failed to create the bitmap for the frame, return and report that we could not
                  // prepare the frame.
                  FLog.w(TAG, "Failed to create frame bitmap", e)
                  return false
                }
            nextFrameType = BitmapAnimationBackend.FRAME_TYPE_UNKNOWN
          }
          else -> return false
        }
        // Try to render and cache the frame
        created = renderFrameAndCache(frameNumber, bitmapReference, frameType)
      } finally {
        CloseableReference.closeSafely(bitmapReference)
      }
      return if (created || nextFrameType == BitmapAnimationBackend.FRAME_TYPE_UNKNOWN) {
        created
      } else {
        prepareFrameAndCache(frameNumber, nextFrameType)
      }
    }

    private fun renderFrameAndCache(
        frameNumber: Int,
        bitmapReference: CloseableReference<Bitmap>?,
        @FrameType frameType: Int
    ): Boolean {
      // Check if the bitmap is valid
      if (!CloseableReference.isValid(bitmapReference)) {
        return false
      }
      // Try to render the frame
      if (bitmapReference == null ||
          !bitmapFrameRenderer.renderFrame(frameNumber, bitmapReference.get())) {
        return false
      }
      FLog.v(TAG, "Frame %d ready.", frameNumber)
      // Cache the frame
      synchronized(pendingFrameDecodeJobs) {
        bitmapFrameCache.onFramePrepared(frameNumber, bitmapReference, frameType)
      }
      return true
    }
  }

  private fun getUniqueId(backend: AnimationBackend, frameNumber: Int): Int =
      31 * backend.hashCode() + frameNumber
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation

import com.facebook.common.logging.FLog
import com.facebook.fresco.animation.backend.AnimationBackend
import com.facebook.fresco.animation.bitmap.BitmapFrameCache

/** Frame preparation strategy to prepare the next n frames */
class FixedNumberBitmapFramePreparationStrategy
@JvmOverloads
constructor(private val framesToPrepare: Int = 3) : BitmapFramePreparationStrategy {

  private val TAG = FixedNumberBitmapFramePreparationStrategy::class.java

  override fun prepareFrames(
      bitmapFramePreparer: BitmapFramePreparer,
      bitmapFrameCache: BitmapFrameCache,
      animationBackend: AnimationBackend,
      lastDrawnFrameNumber: Int,
      onAnimationLoaded: (() -> Unit)?
  ) {
    for (i in 1..framesToPrepare) {
      val nextFrameNumber = (lastDrawnFrameNumber + i) % animationBackend.frameCount
      if (FLog.isLoggable(FLog.VERBOSE)) {
        FLog.v(TAG, "Preparing frame %d, last drawn: %d", nextFrameNumber, lastDrawnFrameNumber)
      }
      if (!bitmapFramePreparer.prepareFrame(bitmapFrameCache, animationBackend, nextFrameNumber)) {
        // We cannot prepare more frames, so we return early
        return
      }
    }

    onAnimationLoaded?.invoke()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation.loadframe

import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit

/**
 * This executor allocated a high priority pool of thread. The priority is high because the task
 * affect directly to the UI render frame. In order to avoid glitches or bad UX, this tasks should
 * be finished asap
 */
object AnimationLoaderExecutor {
  private val THREADS = Runtime.getRuntime().availableProcessors() * 2

  private val uiThreadFactory = ThreadFactory { runnable: Runnable? ->
    val thread = Thread(runnable)
    thread.priority = Thread.MAX_PRIORITY - 1
    thread
  }

  private val executor: ThreadPoolExecutor =
      ThreadPoolExecutor(
          THREADS, THREADS, 0L, TimeUnit.MILLISECONDS, PriorityBlockingQueue(), uiThreadFactory)

  fun execute(task: LoadFramePriorityTask) {
    executor.execute(task)
  }
}

interface LoadFramePriorityTask : Comparable<LoadFramePriorityTask>, Runnable {
  val priority: Int

  override fun compareTo(other: LoadFramePriorityTask): Int {
    return other.priority.compareTo(priority)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation.loadframe

import android.graphics.Bitmap
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory

/**
 * This task render and save in a Map all the bitmap frames from Frame 0 until [untilFrame]. Once
 * the render is finished, the bitmaps are provided via [output]
 */
class LoadFrameTask(
    private val width: Int,
    private val height: Int,
    private val untilFrame: Int,
    override val priority: Int,
    private val output: LoadFrameOutput,
    private val platformBitmapFactory: PlatformBitmapFactory,
    private val bitmapFrameRenderer: BitmapFrameRenderer,
) : LoadFramePriorityTask {

  private val bitmapConfig = Bitmap.Config.ARGB_8888

  override fun run() {
    val frameCollection = mutableMapOf<Int, CloseableReference<Bitmap>>()
    val canvasBitmapFrame = platformBitmapFactory.createBitmap(width, height, bitmapConfig)

    // Animation frames have to be render incrementally
    (0 until untilFrame).forEach { frameNumber ->
      var currentFrame: Bitmap? = null
      var renderSucceed = false

      // Render frame
      if (CloseableReference.isValid(canvasBitmapFrame)) {
        currentFrame = canvasBitmapFrame.get()
        renderSucceed = bitmapFrameRenderer.renderFrame(frameNumber, currentFrame)
      }

      if (currentFrame == null || !renderSucceed) {
        CloseableReference.closeSafely(canvasBitmapFrame)
        frameCollection.values.forEach { CloseableReference.closeSafely(it) }
        output.onFail()
        return@forEach
      }

      // Save frame
      val copyFrame = platformBitmapFactory.createBitmap(currentFrame)
      frameCollection[frameNumber] = copyFrame
    }

    CloseableReference.closeSafely(canvasBitmapFrame)
    output.onSuccess(frameCollection)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation.loadframe

import android.graphics.Bitmap
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory

/** This factory provides task for the animation and set the priority on each task */
class LoadFrameTaskFactory(
    private val platformBitmapFactory: PlatformBitmapFactory,
    private val bitmapFrameRenderer: BitmapFrameRenderer,
) {

  fun createFirstFrameTask(
      width: Int,
      height: Int,
      output: LoadFrameOutput,
  ): LoadFrameTask {
    return LoadFrameTask(
        width = width,
        height = height,
        untilFrame = 1,
        priority = LOAD_FIRST_FRAME_PRIORITY,
        output = output,
        platformBitmapFactory = platformBitmapFactory,
        bitmapFrameRenderer = bitmapFrameRenderer)
  }

  fun createLoadFullAnimationTask(
      width: Int,
      height: Int,
      frameCount: Int,
      output: LoadFrameOutput,
  ): LoadFrameTask {
    return LoadFrameTask(
        width = width,
        height = height,
        untilFrame = frameCount,
        priority = LOAD_FULL_FRAMES_PRIORITY,
        output = output,
        platformBitmapFactory = platformBitmapFactory,
        bitmapFrameRenderer = bitmapFrameRenderer)
  }

  fun createOnDemandTask(
      frameNumber: Int,
      getCachedBitmap: (Int) -> CloseableReference<Bitmap>?,
      output: (CloseableReference<Bitmap>?) -> Unit,
  ): LoadOnDemandFrameTask {
    return LoadOnDemandFrameTask(
        frameNumber = frameNumber,
        getCachedBitmap = getCachedBitmap,
        priority = LOAD_ON_DEMAND_PRIORITY,
        output = output,
        platformBitmapFactory = platformBitmapFactory,
        bitmapFrameRenderer = bitmapFrameRenderer,
    )
  }

  companion object {
    private const val LOAD_FIRST_FRAME_PRIORITY = 10
    private const val LOAD_ON_DEMAND_PRIORITY = 5
    private const val LOAD_FULL_FRAMES_PRIORITY = 1
  }
}

interface LoadFrameOutput {
  fun onSuccess(frames: Map<Int, CloseableReference<Bitmap>>)

  fun onFail()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.bitmap.preparation.loadframe

import android.graphics.Bitmap
import com.facebook.common.references.CloseableReference
import com.facebook.fresco.animation.bitmap.BitmapFrameRenderer
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory

/**
 * Render the frame given [frameNumber]. We have to find the nearest cached bitmap using
 * [getCachedBitmap] to start with, and the apply deltas until to render the [frameNumber] desired.
 * The final bitmap will be sent via [output]
 */
class LoadOnDemandFrameTask(
    private val frameNumber: Int,
    private val getCachedBitmap: (Int) -> CloseableReference<Bitmap>?,
    override val priority: Int,
    private val output: (CloseableReference<Bitmap>?) -> Unit,
    private val platformBitmapFactory: PlatformBitmapFactory,
    private val bitmapFrameRenderer: BitmapFrameRenderer,
) : LoadFramePriorityTask {

  override fun run() {
    val nearestFrame =
        (frameNumber downTo 0)
            .asSequence()
            .mapNotNull {
              val bitmap = getCachedBitmap(it) ?: return@mapNotNull null
              Pair(it, bitmap)
            }
            .firstOrNull()
            ?: return exit(null)

    val canvasBitmap = platformBitmapFactory.createBitmap(nearestFrame.second.get())
    (nearestFrame.first + 1..frameNumber).forEach {
      bitmapFrameRenderer.renderFrame(it, canvasBitmap.get())
    }
    exit(canvasBitmap)
  }

  private fun exit(bitmap: CloseableReference<Bitmap>?) {
    output.invoke(bitmap)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.drawable

import android.os.SystemClock
import com.facebook.fresco.animation.frame.FrameScheduler
import com.facebook.fresco.animation.frame.FrameScheduler.NO_NEXT_TARGET_RENDER_TIME
import kotlin.math.max

const val INVALID_FRAME_TIME = -1L
private const val DEFAULT_FRAME_SCHEDULING_DELAY_MS = 8L
private const val DEFAULT_FRAME_SCHEDULING_OFFSET_MS = 0L

class AnimationFrameScheduler(private val frameScheduler: FrameScheduler) {

  var running: Boolean = false

  // Frame scheduling timer in milliseconds
  var frameSchedulingDelayMs: Long = DEFAULT_FRAME_SCHEDULING_DELAY_MS
  var frameSchedulingOffsetMs: Long = DEFAULT_FRAME_SCHEDULING_OFFSET_MS
  private var pauseTimeMs = 0L
  private var startMs = 0L
  private var expectedRenderTimeMs = 0L
  private var lastFrameAnimationTimeMs = 0L
  private var lastFrameAnimationTimeDifferenceMs = 0L

  // Frame management
  var lastDrawnFrameNumber = -1
  private var pausedLastDrawnFrameNumber = -1

  // Stats
  private var framesDropped = 0

  private fun now() = SystemClock.uptimeMillis()

  fun start() {
    if (!running) {
      val now = now()
      startMs = now - pauseTimeMs
      expectedRenderTimeMs = startMs
      lastFrameAnimationTimeMs = now - lastFrameAnimationTimeDifferenceMs
      lastDrawnFrameNumber = pausedLastDrawnFrameNumber
      running = true
    }
  }

  fun stop() {
    if (running) {
      val now = now()
      pauseTimeMs = now - startMs
      lastFrameAnimationTimeDifferenceMs = now - lastFrameAnimationTimeMs
      startMs = 0L
      expectedRenderTimeMs = 0L
      lastFrameAnimationTimeMs = -1L
      lastDrawnFrameNumber = -1
      running = false
    }
  }

  fun frameToDraw(): Int {
    val renderTimeMillis = now()

    val animationTimeMillis =
        if (running) {
          renderTimeMillis - startMs + frameSchedulingOffsetMs
        } else {
          max(lastFrameAnimationTimeMs, 0)
        }

    // What frame should be drawn?
    val frameNumberToDraw =
        frameScheduler.getFrameNumberToRender(animationTimeMillis, lastFrameAnimationTimeMs)

    lastFrameAnimationTimeMs = animationTimeMillis

    return frameNumberToDraw
  }

  // Returns -1 if it's an valid next render time
  fun nextRenderTime(): Long {
    if (!running) {
      return INVALID_FRAME_TIME
    }

    val actualRenderTimeEnd = now()
    val targetRenderTimeForNextFrameMs =
        frameScheduler.getTargetRenderTimeForNextFrameMs(actualRenderTimeEnd - startMs)
    if (targetRenderTimeForNextFrameMs != NO_NEXT_TARGET_RENDER_TIME.toLong()) {
      val nextFrameTime = targetRenderTimeForNextFrameMs + frameSchedulingDelayMs
      expectedRenderTimeMs = startMs + nextFrameTime
      return nextFrameTime
    }
    running = false

    return INVALID_FRAME_TIME
  }

  fun shouldRepeatAnimation(): Boolean {
    return lastDrawnFrameNumber != -1 && now() >= expectedRenderTimeMs
  }

  fun onFrameDropped() {
    framesDropped++
    // TODO Add log info here
  }

  fun infinite(): Boolean = frameScheduler.isInfiniteAnimation

  fun loopDuration(): Long = frameScheduler.loopDurationMs
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.drawable

import android.graphics.drawable.Drawable

/**
 * Animation listener that can be used to get notified about [AnimatedDrawable2] events. Call
 * [AnimatedDrawable2#setAnimationListener(AnimationListener)] to set a listener.
 */
interface AnimationListener {
  /**
   * Called when the animation is started for the given drawable.
   *
   * @param drawable the affected drawable
   */
  fun onAnimationStart(drawable: Drawable)

  /**
   * Called when the animation is stopped for the given drawable.
   *
   * @param drawable the affected drawable
   */
  fun onAnimationStop(drawable: Drawable)

  /**
   * Called when the animation is reset for the given drawable.
   *
   * @param drawable the affected drawable
   */
  fun onAnimationReset(drawable: Drawable)

  /**
   * Called when the animation is repeated for the given drawable. Animations have a loop count, and
   * frame count, so this is called when the frame count is 0 and the loop count is increased.
   *
   * @param drawable the affected drawable
   */
  fun onAnimationRepeat(drawable: Drawable)

  /**
   * Called when a frame of the animation is about to be rendered.
   *
   * @param drawable the affected drawable
   * @param frameNumber the frame number to be rendered
   */
  fun onAnimationFrame(drawable: Drawable, frameNumber: Int)

  /** Triggered when animation is loaded in memory and ready to play */
  fun onAnimationLoaded() = Unit
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.drawable

import android.graphics.drawable.Drawable

/**
 * Base animation listener. This convenience class can be used to simplify the code if the extending
 * class is not interested in all events. Just override the ones you need.
 *
 * See [AnimationListener] for more information.
 */
open class BaseAnimationListener : AnimationListener {
  override fun onAnimationStart(drawable: Drawable) = Unit

  override fun onAnimationStop(drawable: Drawable) = Unit

  override fun onAnimationReset(drawable: Drawable) = Unit

  override fun onAnimationRepeat(drawable: Drawable) = Unit

  override fun onAnimationFrame(drawable: Drawable, frameNumber: Int) = Unit
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.animation.drawable

import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import com.facebook.drawable.base.DrawableWithCaches
import com.facebook.drawee.drawable.DrawableProperties
import com.facebook.fresco.animation.backend.AnimationBackend
import com.facebook.fresco.animation.frame.DropFramesFrameScheduler
import com.facebook.fresco.animation.frame.FrameScheduler

class KAnimatedDrawable2(private var animationBackend: AnimationBackend) :
    Drawable(), Animatable, DrawableWithCaches {

  private val animatedFrameScheduler =
      AnimationFrameScheduler(DropFramesFrameScheduler(animationBackend))
  private var animationListener: AnimationListener = BaseAnimationListener()
  private var drawListener: DrawListener? = null
  private val drawableProperties = DrawableProperties().apply { applyTo(this@KAnimatedDrawable2) }

  @Volatile private var isRunning = false

  /**
   * Runnable that invalidates the drawable that will be scheduled according to the next target
   * frame.
   */
  private val invalidateRunnable =
      object : Runnable {
        override fun run() {
          // Remove all potential other scheduled runnables
          // (e.g. if the view has been invalidated a lot)
          unscheduleSelf(this)
          // Draw the next frame
          invalidateSelf()
        }
      }

  override fun setAlpha(alpha: Int) {
    drawableProperties.setAlpha(alpha)
    animationBackend.setAlpha(alpha)
  }

  override fun setColorFilter(colorFilter: ColorFilter?) {
    drawableProperties.setColorFilter(colorFilter)
    animationBackend.setColorFilter(colorFilter)
  }

  override fun getOpacity(): Int = PixelFormat.TRANSLUCENT

  /** Start the animation. */
  override fun start() {
    if (animationBackend.frameCount <= 0) {
      return
    }

    animatedFrameScheduler.start()
    animationListener.onAnimationStart(this)
    invalidateSelf()
  }

  /** Stop the animation at the current frame. It can be resumed by calling [start()] again. */
  override fun stop() {
    animatedFrameScheduler.stop()
    animationListener.onAnimationStop(this)
    unscheduleSelf(invalidateRunnable)
  }

  /**
   * Check whether the animation is running.
   *
   * @return true if the animation is currently running
   */
  override fun isRunning(): Boolean = animatedFrameScheduler.running

  override fun dropCaches() {
    animationBackend.clear()
  }

  override fun onBoundsChange(bounds: Rect) {
    animationBackend.setBounds(bounds)
  }

  override fun getIntrinsicWidth(): Int = animationBackend.intrinsicWidth

  override fun getIntrinsicHeight(): Int = animationBackend.intrinsicHeight

  /**
   * Get the animation duration for 1 loop by summing all frame durations.
   *
   * @return the duration of 1 animation loop in ms
   */
  fun loopDurationMs(): Int = animationBackend.loopDurationMs

  /**
   * Get the number of frames for the animation. If no animation backend is set, 0 will be returned.
   *
   * @return the number of frames of the animation
   */
  fun getFrameCount(): Int = animationBackend.frameCount

  /**
   * Get the loop count of the animation. The returned value is either
   * [AnimationInformation#LOOP_COUNT_INFINITE] if the animation is repeated infinitely or a
   * positive integer that corresponds to the number of loops. If no animation backend is set,
   * [AnimationInformation#LOOP_COUNT_INFINITE] will be returned.
   *
   * @return the loop count of the animation or [AnimationInformation#LOOP_COUNT_INFINITE]
   */
  fun loopCount(): Int = animationBackend.loopCount

  /**
   * Frame scheduling delay to shift the target render time for a frame within the frame's visible
   * window. If the value is set to 0, the frame will be scheduled right at the beginning of the
   * frame's visible window.
   *
   * @param delayMs the delay to use in ms
   */
  fun setFrameSchedulingDelayMs(delayMs: Long) {
    animatedFrameScheduler.frameSchedulingDelayMs = delayMs
  }

  /**
   * Frame scheduling offset to shift the animation time by the given offset. This is similar to
   * [frameSchedulingDelayMs] but instead of delaying the invalidation, this offsets the animation
   * time by the given value.
   *
   * @param offsetMs the offset to use in ms
   */
  fun setFrameSchedulingOffsetMs(offsetMs: Long) {
    animatedFrameScheduler.frameSchedulingOffsetMs = offsetMs
  }

  /**
   * Set an animation listener that is notified for various animation events.
   *
   * @param listener the listener to use
   */
  fun setAnimationListener(listener: AnimationListener?) {
    animationListener = listener ?: animationListener
  }

  /**
   * Set a draw listener that is notified for each [draw(Canvas)] call.
   *
   * @param listener the listener to use
   */
  fun setDrawListener(listener: DrawListener?) {
    drawListener = listener
  }

  /**
   * Update the animation backend to be used for the animation. This will also stop the animation.
   * In order to remove the current animation backend, call this method with null.
   *
   * @param animationBackend the animation backend to be used or null
   */
  fun setAnimationBackend(animationBackend: AnimationBackend?) {
    animationBackend ?: return
    stop()
    animationBackend.setBounds(bounds)
    drawableProperties.applyTo(this)
    this.animationBackend = animationBackend
  }

  override fun draw(canvas: Canvas) {
    var frameNumber = animatedFrameScheduler.frameToDraw()
    // Check if the animation is finished and draw last frame if so
    if (frameNumber == FrameScheduler.FRAME_NUMBER_DONE) {
      frameNumber = animationBackend.frameCount - 1
      animatedFrameScheduler.running = false
      animationListener.onAnimationStop(this)
    } else if (frameNumber == 0 && animatedFrameScheduler.shouldRepeatAnimation()) {
      animationListener.onAnimationRepeat(this)
    }

    val frameDrawn = animationBackend.drawFrame(this, canvas, frameNumber)
    if (frameDrawn) {
      // Notify listeners that we draw a new frame and
      // that the animation might be repeated
      animationListener.onAnimationFrame(this, frameNumber)
      animatedFrameScheduler.lastDrawnFrameNumber = frameNumber
    } else {
      animatedFrameScheduler.onFrameDropped()
    }

    val nextFrameTime = animatedFrameScheduler.nextRenderTime()
    if (nextFrameTime != INVALID_FRAME_TIME) {
      scheduleSelf(invalidateRunnable, nextFrameTime)
    } else {
      animationListener.onAnimationStop(this)
      animatedFrameScheduler.running = false
    }
  }

  interface DrawListener {
    fun onDraw(
        animatedDrawable: KAnimatedDrawable2,
        frameScheduler: FrameScheduler,
        frameNumberToDraw: Int,
        frameDrawn: Boolean,
        isAnimationRunning: Boolean,
        animationStartTimeMs: Long,
        animationTimeMs: Long,
        lastFrameAnimationTimeMs: Long,
        actualRenderTimeStartMs: Long,
        actualRenderTimeEndMs: Long,
        startRenderTimeForNextFrameMs: Long,
        scheduledRenderTimeForNextFrameMs: Long
    )
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.fresco.buildsrc

object SampleDeps {

  object AndroidX {
    const val appcompat = "androidx.appcompat:appcompat:1.0.2"
    const val cardview = "androidx.cardview:cardview:1.0.0"
    const val multidex = "androidx.multidex:multidex:2.0.1"
    const val preference = "androidx.preference:preference:1.0.0"
    const val recyclerview = "androidx.recyclerview:recyclerview:1.0.0"
  }

  object Google {
    const val material = "com.google.android.material:material:1.1.0-alpha03"
  }

  object Comparison {
    object Glide {
      private const val version = "4.9.0"
      const val glide = "com.github.bumptech.glide:glide:$version"
      const val compiler = "com.github.bumptech.glide:compiler:$version"
    }

    object Uil {
      private const val version = "1.9.5"
      const val uil = "com.nostra13.universalimageloader:universal-image-loader:$version"
    }

    object Picasso {
      private const val version = "2.71828"
      const val picasso = "com.squareup.picasso:picasso:$version"
      const val okhttpDownloader = "com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2"
    }

    object AndroidQuery {
      private const val version = "0.25.9"
      const val aquery = "com.googlecode.android-query:android-query:$version"
    }
  }

  object Showcase {

    const val caverockSvg = "com.caverock:androidsvg-aar:1.4"
  }

  object Zoomable {
    const val legacyAndroidXSupportCoreUi = "androidx.legacy:legacy-support-core-ui:1.0.0"
  }
}