/*
 * 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.vito.core.impl.debug

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.Pair
import android.view.Gravity
import androidx.annotation.ColorInt
import java.util.LinkedHashMap
import kotlin.math.max
import kotlin.math.min

open class DebugOverlayDrawable @JvmOverloads constructor(private val identifier: String = "") :
    Drawable() {

  @ColorInt var backgroundColor: Int = Color.TRANSPARENT
  var textGravity: Int = Gravity.TOP

  // Internal helpers
  private val debugData = LinkedHashMap<String, Pair<String, Int>>()
  private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
  private var maxLineLength = INITIAL_MAX_LINE_LENGTH
  private var startTextXPx = 0
  private var startTextYPx = 0
  private var lineIncrementPx = 0
  private var currentTextXPx = 0
  private var currentTextYPx = 0

  init {
    reset()
  }

  @JvmOverloads
  fun addDebugData(key: String, value: String, color: Int = TEXT_COLOR) {
    debugData[key] = Pair(value, color)
    maxLineLength = max(value.length, maxLineLength)
    prepareDebugTextParameters(bounds)
  }

  fun reset() {
    debugData.clear()
    maxLineLength = INITIAL_MAX_LINE_LENGTH
    invalidateSelf()
  }

  override fun onBoundsChange(bounds: Rect) {
    super.onBoundsChange(bounds)
    prepareDebugTextParameters(bounds)
  }

  override fun draw(canvas: Canvas) {
    // Draw bounding box
    paint.style = Paint.Style.STROKE
    paint.strokeWidth = OUTLINE_STROKE_WIDTH_PX.toFloat()
    paint.color = OUTLINE_COLOR
    canvas.drawRect(
        bounds.left.toFloat(),
        bounds.top.toFloat(),
        bounds.right.toFloat(),
        bounds.bottom.toFloat(),
        paint)

    // Draw overlay
    paint.style = Paint.Style.FILL
    paint.color = backgroundColor
    canvas.drawRect(
        bounds.left.toFloat(),
        bounds.top.toFloat(),
        bounds.right.toFloat(),
        bounds.bottom.toFloat(),
        paint)

    // Draw text
    paint.style = Paint.Style.FILL
    paint.strokeWidth = 0f
    paint.color = TEXT_COLOR

    // Reset the text position
    currentTextXPx = startTextXPx
    currentTextYPx = startTextYPx
    addDebugText(canvas, "Vito", identifier, IDENTIFIER_COLOR)
    for ((key, value) in debugData) {
      addDebugText(canvas, key, value.first, value.second)
    }
  }

  override fun setAlpha(alpha: Int) = Unit

  override fun setColorFilter(cf: ColorFilter?) = Unit

  override fun getOpacity(): Int = PixelFormat.TRANSLUCENT

  private fun prepareDebugTextParameters(bounds: Rect) {
    if (debugData.isEmpty() || maxLineLength <= 0) {
      return
    }
    var textSizePx = min(bounds.width() / maxLineLength, bounds.height() / debugData.size)
    textSizePx = min(MAX_TEXT_SIZE_PX, max(MIN_TEXT_SIZE_PX, textSizePx))
    paint.textSize = textSizePx.toFloat()
    lineIncrementPx = textSizePx + TEXT_LINE_SPACING_PX
    startTextXPx = bounds.left + TEXT_PADDING_PX
    startTextYPx =
        if (textGravity == Gravity.BOTTOM) bounds.bottom - TEXT_PADDING_PX
        else bounds.top + TEXT_PADDING_PX + textSizePx
  }

  protected fun addDebugText(canvas: Canvas, label: String, value: String, color: Int) {
    val labelColon = "$label: "
    val labelColonWidth = paint.measureText(labelColon)
    val valueWidth = paint.measureText(value)
    paint.color = TEXT_BACKGROUND_COLOR
    canvas.drawRect(
        (currentTextXPx - MARGIN).toFloat(),
        (currentTextYPx + TEXT_LINE_SPACING_PX).toFloat(),
        currentTextXPx + labelColonWidth + valueWidth + MARGIN,
        (currentTextYPx - lineIncrementPx + TEXT_LINE_SPACING_PX).toFloat(),
        paint)
    paint.color = TEXT_COLOR
    canvas.drawText(labelColon, currentTextXPx.toFloat(), currentTextYPx.toFloat(), paint)
    paint.color = color
    canvas.drawText(value, currentTextXPx + labelColonWidth, currentTextYPx.toFloat(), paint)
    if (textGravity == Gravity.BOTTOM) {
      currentTextYPx -= lineIncrementPx
    } else {
      currentTextYPx += lineIncrementPx
    }
  }

  protected fun addDebugText(
      canvas: Canvas,
      x: Int,
      y: Int,
      label: String,
      value: String,
      color: Int
  ) {
    val labelColon = "$label: "
    val labelColonWidth = paint.measureText(labelColon)
    val valueWidth = paint.measureText(value)
    paint.color = TEXT_BACKGROUND_COLOR
    canvas.drawRect(
        (x - MARGIN).toFloat(),
        (y + TEXT_LINE_SPACING_PX).toFloat(),
        x + labelColonWidth + valueWidth + MARGIN,
        (y + lineIncrementPx + TEXT_LINE_SPACING_PX).toFloat(),
        paint)
    paint.color = TEXT_COLOR
    canvas.drawText(labelColon, currentTextXPx.toFloat(), currentTextYPx.toFloat(), paint)
    paint.color = color
    canvas.drawText(value, currentTextXPx + labelColonWidth, currentTextYPx.toFloat(), paint)
    currentTextYPx += lineIncrementPx
  }

  companion object {
    private const val OUTLINE_COLOR = 0xFFFF9800.toInt()
    private const val TEXT_BACKGROUND_COLOR = 0x66000000
    private const val TEXT_COLOR = Color.WHITE
    private const val IDENTIFIER_COLOR = 0xFF00FF00.toInt()
    private const val OUTLINE_STROKE_WIDTH_PX = 2
    private const val MAX_TEXT_SIZE_PX = 72
    private const val MIN_TEXT_SIZE_PX = 16
    private const val TEXT_LINE_SPACING_PX = 8
    private const val TEXT_PADDING_PX = 10
    private const val INITIAL_MAX_LINE_LENGTH = 4
    private const val MARGIN = TEXT_LINE_SPACING_PX / 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.vito.core.impl.debug

import android.graphics.Color
import androidx.annotation.ColorInt

object DebugOverlayImageOriginColor {

  private val imageOriginColorMap: Map<String, Int> =
      hashMapOf(
          "unknown" to Color.GRAY,
          "network" to Color.RED,
          "disk" to Color.YELLOW,
          "memory_encoded" to Color.YELLOW,
          "memory_bitmap" to Color.GREEN,
          "local" to Color.GREEN)

  @JvmStatic
  @ColorInt
  fun getImageOriginColor(imageOrigin: String): Int =
      imageOriginColorMap[imageOrigin] ?: Color.WHITE
}


/*
 * 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.vito.core.impl

import android.content.res.Resources
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.renderer.ImageDataModel
import com.facebook.imagepipeline.image.CloseableImage

fun ImageLayerDataModel.setActualImage(
    resources: Resources,
    imageOptions: ImageOptions,
    closeableImage: CloseableImage,
    imageToDataModelMapper: (Resources, CloseableImage, ImageOptions) -> ImageDataModel?
) {
  configure(
      dataModel = imageToDataModelMapper(resources, closeableImage, imageOptions),
      canvasTransformation = imageOptions.createActualImageCanvasTransformation(),
      roundingOptions = imageOptions.roundingOptions,
      borderOptions = imageOptions.borderOptions,
      colorFilter = imageOptions.actualImageColorFilter)
}

fun ImageLayerDataModel.setPlaceholder(resources: Resources, imageOptions: ImageOptions) {
  val model = imageOptions.createPlaceholderModel(resources)
  if (model == null) {
    reset()
    return
  }
  configure(
      dataModel = model,
      canvasTransformation = imageOptions.createPlaceholderCanvasTransformation(),
      roundingOptions =
          if (imageOptions.placeholderApplyRoundingOptions) imageOptions.roundingOptions else null,
      borderOptions =
          if (imageOptions.placeholderApplyRoundingOptions) imageOptions.borderOptions else null)
}

fun ImageLayerDataModel.setOverlay(resources: Resources, imageOptions: ImageOptions) {
  configure(dataModel = imageOptions.createOverlayModel(resources))
}

fun ImageLayerDataModel.setError(resources: Resources, imageOptions: ImageOptions) {
  val model = imageOptions.createErrorModel(resources)
  if (model == null) {
    reset()
    return
  }
  configure(
      dataModel = model, canvasTransformation = imageOptions.createErrorCanvasTransformation())
}


/*
 * 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.vito.core.impl

import android.graphics.Canvas
import android.graphics.Paint
import com.facebook.fresco.vito.options.BorderOptions
import com.facebook.fresco.vito.renderer.Shape
import com.facebook.fresco.vito.renderer.util.ColorUtils

class BorderRenderer {

  companion object {
    fun renderBorder(canvas: Canvas, borderOptions: BorderOptions, shape: Shape, alpha: Int = 255) {
      shape.draw(canvas, createPaint(borderOptions, alpha))
    }

    fun createPaint(borderOptions: BorderOptions, alpha: Int = 255) =
        Paint(Paint.ANTI_ALIAS_FLAG).apply {
          color = ColorUtils.multiplyColorAlpha(borderOptions.color, alpha)
          strokeWidth = borderOptions.width
          style = Paint.Style.STROKE
        }
  }
}


/*
 * 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.vito.core.impl

import android.graphics.Color
import android.graphics.drawable.Drawable
import com.facebook.fresco.ui.common.VitoUtils
import com.facebook.fresco.vito.core.FrescoDrawableInterface
import com.facebook.fresco.vito.core.impl.debug.DebugOverlayImageOriginColor
import java.util.Locale

sealed class DebugDataProvider(
    val shortName: String,
    val longName: String,
    val description: String
)

class StringDebugDataProvider(
    shortName: String,
    longName: String,
    description: String,
    val extractData: (FrescoDrawableInterface) -> String
) : DebugDataProvider(shortName, longName, description)

val imageIDProvider =
    StringDebugDataProvider("id", "image ID", "The ID of the image") {
      VitoUtils.getStringId(it.imageId)
    }

val drawableDimensionsProvider =
    StringDebugDataProvider(
        "D", "Drawable dimensions", "The visible Drawable dimensions on screen") {
          if (it is Drawable) {
            formatDimensions(it.bounds.width(), it.bounds.height())
          } else {
            ""
          }
        }

val imageDimensionsProvider =
    StringDebugDataProvider("I", "Image dimensions", "The dimensions of the decoded image") {
      if (it is KFrescoVitoDrawable) {
        when (val model = it.actualImageLayer.getDataModel()) {
          null -> "unset"
          else -> formatDimensions(model.width, model.height)
        }
      } else {
        ""
      }
    }

fun formatDimensions(width: Int, height: Int): String =
    String.format(Locale.US, "%dx%d", width, height)

class StringAndColorDebugDataProvider(
    shortName: String,
    longName: String,
    description: String,
    val extractDataAndColor: (FrescoDrawableInterface) -> Pair<String, Int>
) : DebugDataProvider(shortName, longName, description)

private fun KFrescoVitoDrawable.extractOriginExtras(): Map<String, Any>? =
    dataSource?.extras
        ?:
        // We did not receive data source extras, so the image did not come from the image
        // pipeline but from the bitmap memory cache shortcut
        obtainExtras().shortcutExtras

val imageOriginProvider =
    StringAndColorDebugDataProvider("o", "Origin", "The source of the image") {
      if (it is KFrescoVitoDrawable) {
        val origin = it.extractOriginExtras()?.get("origin")?.toString() ?: "unknown"
        val color = DebugOverlayImageOriginColor.getImageOriginColor(origin)

        origin to color
      } else {
        "" to Color.WHITE
      }
    }

val imageOriginSubcategoryProvider =
    StringAndColorDebugDataProvider(
        "o_s", "Origin Subcategory", "The subcategory of source of the image") {
          if (it is KFrescoVitoDrawable) {
            val originSubcategory =
                it.extractOriginExtras()?.get("origin_sub")?.toString() ?: "unknown"

            originSubcategory to Color.GRAY
          } else {
            "" to Color.WHITE
          }
        }


/*
 * 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.vito.core.impl

import com.facebook.common.internal.Supplier
import com.facebook.fresco.vito.core.impl.debug.DebugOverlayDrawable
import com.facebook.fresco.vito.renderer.DrawableImageDataModel

class DebugOverlayHandler(
    private val isEnabled: Supplier<Boolean>,
    private val debugDataProviders: List<DebugDataProvider> =
        listOf(
            imageIDProvider,
            drawableDimensionsProvider,
            imageDimensionsProvider,
            imageOriginProvider,
            imageOriginSubcategoryProvider)
) {

  fun update(drawable: KFrescoVitoDrawable) {
    if (!isEnabled.get()) {
      return
    }

    val debugOverlayDrawable = extractDebugOverlayDrawable(drawable)
    debugDataProviders.forEach {
      when (it) {
        is StringDebugDataProvider ->
            debugOverlayDrawable.addDebugData(it.shortName, it.extractData(drawable))
        is StringAndColorDebugDataProvider -> {
          val (data, color) = it.extractDataAndColor(drawable)
          debugOverlayDrawable.addDebugData(it.shortName, data, color)
        }
      }
    }
  }

  private fun extractDebugOverlayDrawable(drawable: KFrescoVitoDrawable): DebugOverlayDrawable {
    val model = drawable.debugOverlayImageLayer?.getDataModel()
    if (model == null ||
        model !is DrawableImageDataModel ||
        model.drawable !is DebugOverlayDrawable) {
      val debugOverlayDrawable = DebugOverlayDrawable("K")
      drawable.debugOverlayImageLayer =
          drawable.createLayer().apply {
            configure(dataModel = DrawableImageDataModel(debugOverlayDrawable))
          }
      return debugOverlayDrawable
    }
    return model.drawable as DebugOverlayDrawable
  }
}


/*
 * 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.vito.core.impl

import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable

fun Drawable?.toggleAnimation(animationEnabled: Boolean) {
  when (this) {
    is Animatable -> if (animationEnabled) start() else stop()
  }
}


/*
 * 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.vito.core.impl

import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.fresco.middleware.MiddlewareUtils
import com.facebook.fresco.ui.common.ControllerListener2
import com.facebook.imagepipeline.image.CloseableImage

private val COMPONENT_EXTRAS: Map<String, Any> = mapOf("component_tag" to "vito2")

private val SHORTCUT_EXTRAS: Map<String, Any> =
    mapOf("origin" to "memory_bitmap", "origin_sub" to "shortcut")

fun KFrescoVitoDrawable.obtainExtras(
    dataSource: DataSource<CloseableReference<CloseableImage>>? = null,
    image: CloseableReference<CloseableImage>? = null,
): ControllerListener2.Extras {
  return MiddlewareUtils.obtainExtras(
      COMPONENT_EXTRAS,
      SHORTCUT_EXTRAS,
      dataSource?.extras,
      null, // FIXME
      viewportDimensions,
      imageRequest?.imageOptions?.actualImageScaleType.toString(),
      imageRequest?.imageOptions?.actualImageFocusPoint,
      image?.get()?.extras,
      callerContext,
      imageRequest?.finalImageRequest?.sourceUri)
}


/*
 * 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.vito.core.impl

import android.content.res.Resources
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.datasource.DataSubscriber
import com.facebook.drawee.backends.pipeline.info.ImageOrigin
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.renderer.ImageDataModel
import com.facebook.imagepipeline.image.CloseableImage
import java.util.concurrent.Executor

class ImageFetchSubscriber(
    private val imageId: Long,
    private val drawable: KFrescoVitoDrawable,
    private val imageToDataModelMapper:
        (Resources, CloseableImage, ImageOptions) -> ImageDataModel?,
    private var debugOverlayHandler: DebugOverlayHandler? = null,
    private val invalidationExecutor: Executor? = null,
) : DataSubscriber<CloseableReference<CloseableImage>> {
  override fun onNewResult(dataSource: DataSource<CloseableReference<CloseableImage>>) {
    if (imageId != drawable.imageId) {
      return
    }
    val request = drawable.imageRequest ?: return

    val result: CloseableReference<CloseableImage>? = dataSource.result

    if (result == null || !result.isValid) {
      onFailure(dataSource)
      result?.close()
      return
    }

    // We avoid cloning result and closing the original for performance reasons
    drawable.closeable = result
    val image = result.get()
    drawable.actualImageLayer.setActualImage(
        request.resources, request.imageOptions, image, imageToDataModelMapper)
    invalidationExecutor?.execute {
      drawable.actualImageLayer.fadeIn(request.imageOptions.fadeDurationMs)
      drawable.placeholderLayer.fadeOut(request.imageOptions.fadeDurationMs, true)
    }
    // Remove the progress image
    if (dataSource.isFinished) {
      drawable.hideProgressLayer()
    }
    val imageInfo = image.imageInfo
    if (notifyFinalResult(dataSource)) {
      drawable.listenerManager.onFinalImageSet(
          imageId,
          request,
          ImageOrigin.UNKNOWN,
          imageInfo,
          drawable.obtainExtras(dataSource, result),
          drawable.actualImageDrawable)
    } else {
      drawable.listenerManager.onIntermediateImageSet(imageId, request, imageInfo)
    }
    invalidate(drawable)
  }

  override fun onFailure(dataSource: DataSource<CloseableReference<CloseableImage>>) {
    if (imageId != drawable.imageId) {
      return
    }
    val request = drawable.imageRequest ?: return
    drawable.actualImageLayer.setError(request.resources, request.imageOptions)
    if (dataSource.isFinished) {
      drawable.hideProgressLayer()
    }
    if (notifyFinalResult(dataSource)) {
      dataSource.result.use { result ->
        drawable.listenerManager.onFailure(
            imageId,
            request,
            drawable.actualImageLayer.getDataModel().maybeGetDrawable(),
            dataSource.failureCause,
            drawable.obtainExtras(dataSource, result))
      }
    } else {
      drawable.listenerManager.onIntermediateImageFailed(
          imageId,
          request,
          dataSource.failureCause,
      )
    }
    drawable.imagePerfListener.onImageError(drawable)
    invalidate(drawable)
  }

  override fun onCancellation(dataSource: DataSource<CloseableReference<CloseableImage>>) = Unit

  override fun onProgressUpdate(dataSource: DataSource<CloseableReference<CloseableImage>>) {
    drawable.updateProgress(dataSource)
    debugOverlayHandler?.update(drawable)
  }

  private fun invalidate(drawable: KFrescoVitoDrawable) {
    invalidationExecutor?.execute { drawable.invalidateSelf() } ?: drawable.invalidateSelf()
    debugOverlayHandler?.update(drawable)
  }

  private fun notifyFinalResult(
      dataSource: DataSource<CloseableReference<CloseableImage>>?
  ): Boolean {
    return dataSource == null || dataSource.isFinished || dataSource.hasMultipleResults()
  }
}


/*
 * 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.vito.core.impl

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import com.facebook.fresco.vito.options.BorderOptions
import com.facebook.fresco.vito.options.RoundingOptions
import com.facebook.fresco.vito.renderer.CanvasTransformation
import com.facebook.fresco.vito.renderer.CanvasTransformationHandler
import com.facebook.fresco.vito.renderer.ImageDataModel
import com.facebook.fresco.vito.renderer.RenderCommand

class ImageLayerDataModel(
    var drawableCallbackProvider: (() -> Drawable.Callback?)? = null,
    var invalidateLayerCallback: (() -> Unit)? = null
) {
  private var dataModel: ImageDataModel? = null
  private var roundingOptions: RoundingOptions? = null
  private var borderOptions: BorderOptions? = null
  private var currentBounds: Rect? = null
  private val canvasTransformationHandler: CanvasTransformationHandler =
      CanvasTransformationHandler(null)
  private val paint: Paint = Paint()

  private var renderCommand: RenderCommand? = null
  private var colorFilter: ColorFilter? = null

  private var fadeAnimator: ValueAnimator? = null

  private val animatorUpdateListener =
      ValueAnimator.AnimatorUpdateListener {
        paint.alpha = it.animatedValue as Int
        invalidateLayerCallback?.invoke()
      }

  fun getDataModel(): ImageDataModel? {
    return dataModel
  }

  fun configure(
      dataModel: ImageDataModel? = this.dataModel,
      roundingOptions: RoundingOptions? = this.roundingOptions,
      borderOptions: BorderOptions? = this.borderOptions,
      canvasTransformation: CanvasTransformation? =
          canvasTransformationHandler.canvasTransformation,
      bounds: Rect? = this.currentBounds,
      colorFilter: ColorFilter? = this.colorFilter
  ) {
    if (dataModel != this.dataModel) {
      this.dataModel?.apply {
        onDetach()
        setCallback(null)
      }
      this.dataModel =
          dataModel?.apply {
            setCallback(drawableCallbackProvider?.invoke())
            onAttach()
          }
    }
    this.roundingOptions = roundingOptions
    this.borderOptions = borderOptions
    this.currentBounds = bounds
    this.colorFilter = colorFilter
    canvasTransformationHandler.canvasTransformation = canvasTransformation
    // TODO(T105148151): only invalidate if changed
    invalidateRenderCommand()
    if (bounds != null) {
      computeRenderCommand(bounds)
    }
  }

  fun invalidateRenderCommand() {
    renderCommand = null
  }

  private fun computeRenderCommand(bounds: Rect, alpha: Int = 255) {
    val model = dataModel
    if (model == null) {
      renderCommand = null
      return
    }
    if (renderCommand != null && currentBounds == bounds) {
      return
    }

    currentBounds = bounds
    canvasTransformationHandler.configure(bounds, model.width, model.height)
    paint.colorFilter = colorFilter
    paint.flags = model.defaultPaintFlags
    renderCommand =
        ImageWithTransformationAndBorderRenderer.createRenderCommand(
            model,
            roundingOptions,
            borderOptions,
            canvasTransformationHandler.getMatrix(),
            bounds,
            paint,
            alpha)
  }

  fun draw(canvas: Canvas) {
    renderCommand?.let { it(canvas) }
  }

  fun reset(endAnimator: Boolean = true) {
    canvasTransformationHandler.canvasTransformation = null
    dataModel?.apply {
      onDetach()
      setCallback(null)
    }
    dataModel = null
    roundingOptions = null
    borderOptions = null
    renderCommand = null
    currentBounds = null
    paint.reset()
    colorFilter = null

    if (endAnimator) {
      // Placed inside if block to prevent recursion (T134570663)
      fadeAnimator?.end()
    }
    fadeAnimator = null
  }

  fun fadeIn(durationMs: Int) {
    fadeAnimator?.end()
    if (durationMs == 0) {
      paint.alpha = 255
      // TODO: Do we need to invalidate?
      return
    }
    fadeAnimator =
        ValueAnimator.ofInt(0, 255).apply {
          duration = durationMs.toLong()
          addUpdateListener(animatorUpdateListener)
          start()
        }
  }

  fun fadeOut(durationMs: Int, resetLayerWhenInvisible: Boolean = false) {
    fadeAnimator?.end()
    if (durationMs == 0) {
      paint.alpha = 0
      return
    }
    fadeAnimator =
        ValueAnimator.ofInt(255, 0).apply {
          duration = durationMs.toLong()
          addUpdateListener(animatorUpdateListener)
          if (resetLayerWhenInvisible) {
            addListener(
                object : AnimatorListenerAdapter() {
                  override fun onAnimationEnd(animation: Animator) {
                    reset(false)
                  }
                })
          }
          start()
        }
    // TODO use fadeAnimator.reverse() and reuse
  }

  fun getAlpha() = paint.alpha
}


/*
 * 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.vito.core.impl

import android.graphics.drawable.Drawable
import com.facebook.fresco.vito.renderer.DrawableImageDataModel
import com.facebook.fresco.vito.renderer.ImageDataModel

fun ImageDataModel?.maybeGetDrawable(): Drawable? = (this as? DrawableImageDataModel)?.drawable


/*
 * 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.vito.core.impl

import android.os.Handler
import android.os.Looper
import com.facebook.drawee.components.DeferredReleaser

object ImageReleaseScheduler {
  private const val RELEASE_DELAY: Long = 16 * 5 // Roughly 5 frames.

  class ImageReleaseState(val drawable: KFrescoVitoDrawable) :
      Runnable, DeferredReleaser.Releasable {

    var delayedReleasePending = false

    override fun run() {
      // the Runnable interface is used to release next frame
      releaseNextFrame(drawable)
    }

    override fun release() {
      // The releasable is used to clean up immediately
      drawable.reset()
    }
  }

  private val handler = Handler(Looper.getMainLooper())
  private val deferredReleaser = DeferredReleaser.getInstance()

  fun releaseImmediately(drawable: KFrescoVitoDrawable) {
    drawable.imagePerfListener.onReleaseImmediately(drawable)
    drawable.reset()
  }

  fun releaseDelayed(drawable: KFrescoVitoDrawable) {
    if (drawable.releaseState.delayedReleasePending) {
      return
    }
    drawable.imagePerfListener.onScheduleReleaseDelayed(drawable)
    handler.postDelayed(drawable.releaseState, RELEASE_DELAY)
  }

  fun releaseNextFrame(drawable: KFrescoVitoDrawable) {
    cancelReleaseDelayed(drawable)
    drawable.imagePerfListener.onScheduleReleaseNextFrame(drawable)
    deferredReleaser.scheduleDeferredRelease(drawable.releaseState)
  }

  fun cancelAllReleasing(drawable: KFrescoVitoDrawable) {
    cancelReleaseDelayed(drawable)
    cancelReleaseNextFrame(drawable)
  }

  fun cancelReleaseDelayed(drawable: KFrescoVitoDrawable) {
    handler.removeCallbacks(drawable.releaseState)
    drawable.releaseState.delayedReleasePending = false
  }

  fun cancelReleaseNextFrame(drawable: KFrescoVitoDrawable) {
    deferredReleaser.cancelDeferredRelease(drawable.releaseState)
  }

  fun createReleaseState(drawable: KFrescoVitoDrawable): ImageReleaseState =
      ImageReleaseState(drawable)
}


/*
 * 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.vito.core.impl

import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import com.facebook.fresco.vito.options.BorderOptions
import com.facebook.fresco.vito.options.RoundingOptions
import com.facebook.fresco.vito.renderer.ImageDataModel
import com.facebook.fresco.vito.renderer.ImageRenderer
import com.facebook.fresco.vito.renderer.RenderCommand
import com.facebook.fresco.vito.renderer.Shape

class ImageWithTransformationAndBorderRenderer {
  companion object {
    fun createRenderCommand(
        model: ImageDataModel,
        roundingOptions: RoundingOptions?,
        borderOptions: BorderOptions?,
        canvasTransform: Matrix?,
        bounds: Rect,
        paint: Paint,
        alpha: Int = 255
    ): RenderCommand {
      val borderPadding: Float = borderOptions?.padding ?: 0f
      val borderWidth: Float = borderOptions?.width ?: 0f
      val hasBorders: Boolean = borderWidth > 0f
      val scaleDownInsideBorders = borderOptions?.scaleDownInsideBorders ?: false
      val layerBounds = RectF(bounds)
      val cornerRadiusAdjustment = borderWidth + borderPadding

      val imageTransform: Matrix?
      val imageShape: Shape
      if (hasBorders && (scaleDownInsideBorders || borderPadding != 0f)) {
        val insideBorderBounds =
            if (scaleDownInsideBorders) {
              RectF(layerBounds).apply { inset(cornerRadiusAdjustment, cornerRadiusAdjustment) }
            } else {
              RectF(layerBounds).apply { inset(borderWidth, borderWidth) }
            }
        val insideBorderTransform =
            Matrix().apply {
              setRectToRect(layerBounds, insideBorderBounds, Matrix.ScaleToFit.FILL)
            }
        imageTransform = Matrix(canvasTransform).apply { postConcat(insideBorderTransform) }
        imageShape =
            ShapeCalculator.getShape(insideBorderBounds, roundingOptions, -cornerRadiusAdjustment)
      } else {
        imageTransform = canvasTransform
        imageShape = ShapeCalculator.getShape(layerBounds, roundingOptions, -cornerRadiusAdjustment)
      }

      val imageRenderCommand =
          ImageRenderer.createImageDataModelRenderCommand(model, imageShape, paint, imageTransform)

      val imageClipRect =
          if (model.width > 0 && model.height > 0) {
            // Prevent repeated pixels from CLAMP shader tile mode by clipping the rect to the
            // transformed
            // image dimensions
            // We could optimize this to only clip if we actually would have repeated pixels.
            val imageRect = RectF(0f, 0f, model.width.toFloat(), model.height.toFloat())
            imageTransform?.mapRect(imageRect)
            imageRect
          } else {
            null
          }

      val borderShape =
          if (hasBorders) {
            val halfBorder = borderWidth / 2
            val borderBounds = RectF(layerBounds).apply { inset(halfBorder, halfBorder) }
            val borderShape = ShapeCalculator.getShape(borderBounds, roundingOptions, -halfBorder)
            borderShape
          } else {
            null
          }

      return { canvas ->
        val saveCount = canvas.save()
        canvas.clipRect(bounds)
        if (imageClipRect != null) {
          canvas.clipRect(imageClipRect)
        }
        imageRenderCommand(canvas)
        canvas.restoreToCount(saveCount)

        if (borderShape != null) {
          borderOptions?.apply { BorderRenderer.renderBorder(canvas, this, borderShape, alpha) }
        }
      }
    }
  }
}


/*
 * 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.vito.core.impl

import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Rect
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import com.facebook.common.callercontext.ContextChain
import com.facebook.common.internal.Supplier
import com.facebook.common.logging.FLog
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.info.ImageOrigin
import com.facebook.fresco.ui.common.ControllerListener2
import com.facebook.fresco.ui.common.ImagePerfDataListener
import com.facebook.fresco.ui.common.ImagePerfDataNotifier
import com.facebook.fresco.ui.common.OnFadeListener
import com.facebook.fresco.ui.common.VitoUtils
import com.facebook.fresco.vito.core.FrescoController2
import com.facebook.fresco.vito.core.FrescoDrawableInterface
import com.facebook.fresco.vito.core.FrescoVitoConfig
import com.facebook.fresco.vito.core.VitoImagePerfListener
import com.facebook.fresco.vito.core.VitoImagePipeline
import com.facebook.fresco.vito.core.VitoImageRequest
import com.facebook.fresco.vito.core.VitoImageRequestListener
import com.facebook.fresco.vito.listener.ImageListener
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.options.ImageOptionsDrawableFactory
import com.facebook.fresco.vito.renderer.AnimatedDrawableImageDataModel
import com.facebook.fresco.vito.renderer.BitmapImageDataModel
import com.facebook.fresco.vito.renderer.DrawableImageDataModel
import com.facebook.fresco.vito.renderer.ImageDataModel
import com.facebook.fresco.vito.source.BitmapImageSource
import com.facebook.imagepipeline.image.CloseableBitmap
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.CloseableStaticBitmap
import com.facebook.imagepipeline.image.ImageInfo
import com.facebook.imagepipeline.image.ImmutableQualityInfo
import java.util.concurrent.Executor

class KFrescoController(
    private val config: FrescoVitoConfig,
    private val vitoImagePipeline: VitoImagePipeline,
    private val uiThreadExecutor: Executor,
    private val lightweightBackgroundThreadExecutor: Executor,
    private val globalImageRequestListener: VitoImageRequestListener? = null,
    private val imagePerfControllerListenerSupplier: Supplier<ControllerListener2<ImageInfo>>? =
        null,
    private val imagePerfListener: VitoImagePerfListener = BaseVitoImagePerfListener(),
    private val drawableFactory: ImageOptionsDrawableFactory? = null
) : FrescoController2 {

  private val imageToDataModelMapper: (Resources, CloseableImage, ImageOptions) -> ImageDataModel? =
      { r, a, b ->
        b.customDrawableFactory?.createDrawable(r, a, b)?.let { createDrawableModel(it, b) }
            ?: when (a) {
              is CloseableBitmap ->
                  BitmapImageDataModel(
                      a.underlyingBitmap,
                      java.lang.Boolean.TRUE.equals(a.getExtras()["is_rounded"]))
              // TODO(T105148151): handle rotation for closeable static bitmap, handle other types
              else -> {
                drawableFactory?.createDrawable(r, a, b)?.let { createDrawableModel(it, b) }
              }
            }
      }

  var debugOverlayHandler: DebugOverlayHandler? = null

  override fun <T> createDrawable(): T where T : Drawable, T : FrescoDrawableInterface {
    val drawable = KFrescoVitoDrawable(imagePerfListener)
    imagePerfControllerListenerSupplier?.get()?.let {
      drawable.listenerManager.setImagePerfControllerListener(it)
    }
    return drawable as T
  }

  override fun fetch(
      drawable: FrescoDrawableInterface,
      imageRequest: VitoImageRequest,
      callerContext: Any?,
      contextChain: ContextChain?,
      listener: ImageListener?,
      perfDataListener: ImagePerfDataListener?,
      onFadeListener: OnFadeListener?,
      viewportDimensions: Rect?
  ): Boolean {
    if (drawable !is KFrescoVitoDrawable) {
      FLog.e(TAG, "Drawable not supported $drawable")
      return false
    }

    // Check if we already fetched that image
    if (isAlreadyLoadingImage(imageRequest, drawable)) {
      ImageReleaseScheduler.cancelAllReleasing(drawable)
      return true
    }

    if (drawable.isFetchSubmitted) {
      drawable.imagePerfListener.onDrawableReconfigured(drawable)
    }

    // We didn't -> reset everything and set up new fetch
    // TODO(T105148151): move to new package so that no legacy impl dep
    val imageId: Long = VitoUtils.generateIdentifier()
    drawable.apply {
      reset()
      this.imageRequest = imageRequest
      this.callerContext = callerContext
      imageListener = listener
      listenerManager.setVitoImageRequestListener(globalImageRequestListener)

      // Setup local perf data listener
      val localPerfStateListener = perfDataListener?.let { ImagePerfDataNotifier(it) }
      listenerManager.setLocalImagePerfStateListener(localPerfStateListener)

      _imageId = imageId
      this.viewportDimensions = viewportDimensions
    }

    val options: ImageOptions = imageRequest.imageOptions

    drawable.listenerManager.onSubmit(imageId, imageRequest, callerContext, drawable.obtainExtras())
    drawable.imagePerfListener.onImageFetch(drawable)
    drawable.overlayImageLayer.setOverlay(imageRequest.resources, options)

    // Direct bitmap available
    if (imageRequest.imageSource is BitmapImageSource) {
      val bitmap: Bitmap = (imageRequest.imageSource as BitmapImageSource).bitmap
      val closeableBitmap: CloseableBitmap =
          CloseableStaticBitmap.of(bitmap, {}, ImmutableQualityInfo.FULL_QUALITY, 0)
      val bitmapRef = CloseableReference.of<CloseableImage>(closeableBitmap)
      return drawable.setActualImage(imageRequest, bitmapRef)
    }

    // Check if the image is in cache
    val cachedImage = vitoImagePipeline.getCachedImage(imageRequest)
    if (drawable.setActualImage(imageRequest, cachedImage)) {
      return true
    }

    // The image is not in cache -> Set up layers visible until the image is available
    drawable.placeholderLayer.setPlaceholder(imageRequest.resources, options)
    drawable.listenerManager.onPlaceholderSet(
        imageId, imageRequest, drawable.placeholderLayer.getDataModel().maybeGetDrawable())

    drawable.setupProgressLayer(imageRequest.resources, options)

    // Fetch the image
    lightweightBackgroundThreadExecutor.execute {
      if (imageId != drawable.imageId) {
        return@execute
      }
      val dataSource: DataSource<CloseableReference<CloseableImage>> =
          vitoImagePipeline.fetchDecodedImage(imageRequest, callerContext, null, imageId)
      dataSource.subscribe(
          ImageFetchSubscriber(
              imageId, drawable, imageToDataModelMapper, debugOverlayHandler, uiThreadExecutor),
          uiThreadExecutor) // Keyframes require callbacks to be on the main thread.
      drawable.dataSource = dataSource
    }
    drawable.setFetchSubmitted(true)
    drawable.invalidateSelf()
    debugOverlayHandler?.update(drawable)
    return false
  }

  override fun releaseDelayed(drawable: FrescoDrawableInterface) {
    if (drawable !is KFrescoVitoDrawable) {
      FLog.e(TAG, "Drawable not supported $drawable")
      return
    }
    ImageReleaseScheduler.releaseDelayed(drawable)
  }

  override fun release(drawable: FrescoDrawableInterface) {
    if (drawable !is KFrescoVitoDrawable) {
      FLog.e(TAG, "Drawable not supported $drawable")
      return
    }
    ImageReleaseScheduler.releaseNextFrame(drawable)
  }

  override fun releaseImmediately(drawable: FrescoDrawableInterface) {
    if (drawable !is KFrescoVitoDrawable) {
      FLog.e(TAG, "Drawable not supported $drawable")
      return
    }
    ImageReleaseScheduler.releaseImmediately(drawable)
  }

  /**
   * Set the actual image to be the given image reference.
   *
   * @return true if the image has been set, false if it failed (e.g. invalid/null image reference)
   */
  private fun KFrescoVitoDrawable.setActualImage(
      imageRequest: VitoImageRequest,
      imageReference: CloseableReference<CloseableImage>?
  ): Boolean {
    try {
      if (CloseableReference.isValid(imageReference)) {
        // Immediately display the actual image.
        val image = imageReference?.get()
        if (image != null) {
          setFetchSubmitted(true)
          closeable = imageReference.clone()
          actualImageLayer.setActualImage(
              imageRequest.resources, imageRequest.imageOptions, image, imageToDataModelMapper)
          // TODO(T105148151): trigger listeners
          invalidateSelf()
          val imageInfo = image.imageInfo
          listenerManager.onFinalImageSet(
              imageId,
              imageRequest,
              ImageOrigin.MEMORY_BITMAP_SHORTCUT,
              imageInfo,
              obtainExtras(null, imageReference),
              actualImageDrawable)
          debugOverlayHandler?.update(this)
          return true
        }
      }
    } finally {
      CloseableReference.closeSafely(imageReference)
    }
    return false
  }

  private fun isAlreadyLoadingImage(
      imageRequest: VitoImageRequest,
      drawable: KFrescoVitoDrawable
  ): Boolean =
      if (config.useSmartPropertyDiffing()) {
        imageRequest.equalsIfHasImage(drawable.imageRequest, drawable.hasImage())
      } else {
        imageRequest == drawable.imageRequest
      }

  private fun createDrawableModel(drawable: Drawable, options: ImageOptions): ImageDataModel =
      if (drawable is Animatable) {
        AnimatedDrawableImageDataModel(drawable, drawable, options.shouldAutoPlay())
      } else {
        DrawableImageDataModel(drawable)
      }

  companion object {
    private const val TAG = "KFrescoController"
  }
}


/*
 * 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.vito.core.impl

import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import com.facebook.common.closeables.AutoCleanupDelegate
import com.facebook.datasource.DataSource
import com.facebook.drawee.drawable.VisibilityCallback
import com.facebook.fresco.vito.core.CombinedImageListener
import com.facebook.fresco.vito.core.FrescoDrawableInterface
import com.facebook.fresco.vito.core.VitoImagePerfListener
import com.facebook.fresco.vito.core.VitoImageRequest
import com.facebook.fresco.vito.listener.ImageListener
import com.facebook.fresco.vito.renderer.DrawableImageDataModel
import java.io.Closeable
import java.io.IOException

class KFrescoVitoDrawable(
    private val _imagePerfListener: VitoImagePerfListener = NopImagePerfListener()
) : Drawable(), FrescoDrawableInterface, Drawable.Callback {

  var _imageId: Long = 0
  var _isLoading: Boolean = false
  override var callerContext: Any? = null
  var _visibilityCallback: VisibilityCallback? = null
  var _fetchSubmitted: Boolean = false
  val listenerManager: CombinedImageListener = CombinedImageListenerImpl()
  override var extras: Any? = null
  var viewportDimensions: Rect? = null
  var dataSource: DataSource<out Any>? by DataSourceCleanupDelegate()

  val releaseState = ImageReleaseScheduler.createReleaseState(this)
  private var hasBoundsSet = false

  override var imageRequest: VitoImageRequest? = null

  private val closeableCleanupFunction: (Closeable) -> Unit = {
    ImageReleaseScheduler.cancelAllReleasing(this)
    try {
      it.close()
    } catch (e: IOException) {
      // swallow
    }
  }

  var closeable: Closeable? by AutoCleanupDelegate(null, closeableCleanupFunction)

  override var refetchRunnable: Runnable? = null

  override val imageId: Long
    get() = _imageId

  override val imagePerfListener: VitoImagePerfListener
    get() = _imagePerfListener

  override fun setMutateDrawables(mutateDrawables: Boolean) {
    // No-op since we never mutate Drawables
  }

  override val actualImageDrawable: Drawable?
    get() {
      return when (val model = actualImageLayer.getDataModel()) {
        is DrawableImageDataModel -> model.drawable
        else -> null
      }
    }

  override fun hasImage(): Boolean = actualImageLayer.getDataModel() != null

  fun setFetchSubmitted(fetchSubmitted: Boolean) {
    _fetchSubmitted = fetchSubmitted
  }

  override val isFetchSubmitted: Boolean
    get() = _fetchSubmitted

  override fun setVisibilityCallback(visibilityCallback: VisibilityCallback?) {
    _visibilityCallback = visibilityCallback
  }

  override var imageListener: ImageListener?
    get() = listenerManager.imageListener
    set(value) {
      listenerManager.imageListener = value
    }

  override fun setOverlayDrawable(drawable: Drawable?): Drawable? {
    overlayImageLayer.apply {
      configure(
          dataModel = if (drawable == null) null else DrawableImageDataModel(drawable),
          roundingOptions = null,
          borderOptions = null)
    }
    return drawable
  }

  override fun setVisible(visible: Boolean, restart: Boolean): Boolean {
    _visibilityCallback?.onVisibilityChange(visible)
    return super.setVisible(visible, restart)
  }

  fun reset() {
    imageRequest?.let { listenerManager.onRelease(imageId, it, obtainExtras()) }
    imagePerfListener.onImageRelease(this)
    ImageReleaseScheduler.cancelAllReleasing(this)
    _imageId = 0
    closeable = null
    dataSource = null
    imageRequest = null
    _isLoading = false
    callerContext = null

    placeholderLayer.reset()
    actualImageLayer.reset()
    progressLayer?.reset()
    overlayImageLayer.reset()
    debugOverlayImageLayer?.reset()
    hasBoundsSet = false

    listenerManager.onReset()
    listenerManager.imageListener = null
  }

  private var drawableAlpha: Int = 255
  private var drawableColorFilter: ColorFilter? = null

  val callbackProvider: (() -> Callback?) = { this }
  val invalidateLayerCallback: (() -> Unit) = { invalidateSelf() }

  val placeholderLayer = createLayer()
  val actualImageLayer = createLayer()
  var progressLayer: ImageLayerDataModel? = null
  val overlayImageLayer = createLayer()
  var debugOverlayImageLayer: ImageLayerDataModel? = null

  override fun draw(canvas: Canvas) {
    if (!hasBoundsSet) {
      setLayerBounds(bounds)
    }
    placeholderLayer.draw(canvas)
    actualImageLayer.draw(canvas)
    progressLayer?.draw(canvas)
    overlayImageLayer.draw(canvas)
    debugOverlayImageLayer?.draw(canvas)
  }

  override fun onBoundsChange(bounds: Rect) {
    super.onBoundsChange(bounds)
    setLayerBounds(bounds)
  }

  private fun setLayerBounds(bounds: Rect?) {
    if (bounds != null) {
      placeholderLayer.configure(bounds = bounds)
      actualImageLayer.configure(bounds = bounds)
      progressLayer?.configure(bounds = bounds)
      overlayImageLayer.configure(bounds = bounds)
      debugOverlayImageLayer?.configure(bounds = bounds)
      hasBoundsSet = true
    }
  }

  override fun setAlpha(alpha: Int) {
    drawableAlpha = alpha
  }

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

  // TODO(T105148151) Calculate opacity based on layers
  override fun getOpacity(): Int = PixelFormat.TRANSPARENT

  internal fun createLayer() = ImageLayerDataModel(callbackProvider, invalidateLayerCallback)

  override fun invalidateDrawable(who: Drawable) {
    invalidateSelf()
  }

  override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
    scheduleSelf(what, `when`)
  }

  override fun unscheduleDrawable(who: Drawable, what: Runnable) {
    unscheduleSelf(what)
  }
}


/*
 * 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.vito.core.impl

import android.content.res.Resources
import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.util.TypedValue
import com.facebook.drawee.drawable.ScalingUtils
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.renderer.CanvasTransformation
import com.facebook.fresco.vito.renderer.ColorIntImageDataModel
import com.facebook.fresco.vito.renderer.DrawableImageDataModel
import com.facebook.fresco.vito.renderer.ImageDataModel

// Models
fun ImageOptions.createPlaceholderModel(resources: Resources): ImageDataModel? =
    toModel(resources, placeholderDrawable, placeholderRes, placeholderColor)

fun ImageOptions.createOverlayModel(resources: Resources): ImageDataModel? =
    toModel(createOverlayDrawable(resources))

fun ImageOptions.createErrorModel(resources: Resources): ImageDataModel? =
    toModel(resources, errorDrawable, errorRes, errorColor)

// Drawables
fun ImageOptions.createProgressDrawable(resources: Resources): Drawable? =
    create(resources, progressDrawable, progressRes)

fun ImageOptions.createOverlayDrawable(resources: Resources): Drawable? =
    create(resources, overlayDrawable, overlayRes)

// Transformations
fun ImageOptions.createPlaceholderCanvasTransformation(): CanvasTransformation? =
    placeholderScaleType?.getCanvasTransformation(placeholderFocusPoint)

fun ImageOptions.createActualImageCanvasTransformation(): CanvasTransformation =
    actualImageScaleType.getCanvasTransformation(actualImageFocusPoint)

fun ImageOptions.createProgressCanvasTransformation(): CanvasTransformation? =
    progressScaleType?.getCanvasTransformation(null)

fun ImageOptions.createErrorCanvasTransformation(): CanvasTransformation? =
    errorScaleType?.getCanvasTransformation(errorFocusPoint)

private fun create(resources: Resources, drawable: Drawable?, drawableRes: Int): Drawable? {
  return drawable ?: if (drawableRes != 0) resources.getDrawable(drawableRes) else null
}

private fun toModel(drawable: Drawable?): ImageDataModel? {
  return when (drawable) {
    null -> null // Empty image
    // Rendering the color directly is much faster, especially if rounding etc. is needed
    is ColorDrawable -> ColorIntImageDataModel(drawable.color)
    else -> DrawableImageDataModel(drawable)
  }
}

private fun toModel(
    resources: Resources,
    drawable: Drawable?,
    colorOrDrawableRes: Int,
    colorInt: Int?
): ImageDataModel? =
    when {
      drawable != null -> toModel(drawable)
      colorOrDrawableRes != 0 -> resources.getColorOrDrawableModel(colorOrDrawableRes)
      colorInt != null -> ColorIntImageDataModel(colorInt)
      else -> null
    }

private fun Resources.getColorOrDrawableModel(colorOrDrawableRes: Int): ImageDataModel? =
    if (colorOrDrawableRes != 0) {
      val value = TypedValue()
      getValue(colorOrDrawableRes, value, true)

      if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
          value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
        ColorIntImageDataModel(value.data)
      } else {
        toModel(getDrawable(colorOrDrawableRes))
      }
    } else {
      null
    }

fun ScalingUtils.ScaleType.getCanvasTransformation(
    focusPoint: PointF? = null
): CanvasTransformation =
    object : CanvasTransformation {
      override fun calculateTransformation(
          outTransform: Matrix,
          parentBounds: Rect,
          childWidth: Int,
          childHeight: Int
      ): Matrix {
        getTransform(
            outTransform,
            parentBounds,
            childWidth,
            childHeight,
            focusPoint?.x ?: 0.5f,
            focusPoint?.y ?: 0.5f)
        return outTransform
      }
    }


/*
 * 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.vito.core.impl

import com.facebook.fresco.vito.core.FrescoDrawableInterface
import com.facebook.fresco.vito.core.VitoImagePerfListener

class NopImagePerfListener : VitoImagePerfListener {
  override fun onImageMount(drawable: FrescoDrawableInterface) = Unit

  override fun onImageUnmount(drawable: FrescoDrawableInterface) = Unit

  override fun onImageBind(drawable: FrescoDrawableInterface) = Unit

  override fun onImageUnbind(drawable: FrescoDrawableInterface) = Unit

  override fun onImageFetch(drawable: FrescoDrawableInterface) = Unit

  override fun onImageSuccess(drawable: FrescoDrawableInterface, wasImmediate: Boolean) = Unit

  override fun onImageError(drawable: FrescoDrawableInterface) = Unit

  override fun onImageRelease(drawable: FrescoDrawableInterface) = Unit

  override fun onScheduleReleaseDelayed(drawable: FrescoDrawableInterface) = Unit

  override fun onScheduleReleaseNextFrame(drawable: FrescoDrawableInterface) = Unit

  override fun onReleaseImmediately(drawable: FrescoDrawableInterface) = Unit

  override fun onDrawableReconfigured(drawable: FrescoDrawableInterface) = Unit

  override fun onIgnoreResult(drawable: FrescoDrawableInterface) = Unit

  override fun onIgnoreFailure(drawable: FrescoDrawableInterface) = 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.vito.core.impl

import android.content.res.Resources
import android.graphics.drawable.Drawable
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.renderer.DrawableImageDataModel
import com.facebook.imagepipeline.image.CloseableImage
import kotlin.math.roundToInt

fun KFrescoVitoDrawable.setupProgressLayer(resources: Resources, imageOptions: ImageOptions) {
  val drawable = imageOptions.createProgressDrawable(resources)
  if (drawable == null) {
    progressLayer?.reset()
    progressLayer = null
    return
  }
  var layer = progressLayer
  if (layer == null) {
    layer = createLayer()
    progressLayer = layer
  }
  drawable.setProgress(0f)
  layer.configure(
      dataModel = DrawableImageDataModel(drawable),
      canvasTransformation = imageOptions.createProgressCanvasTransformation())
}

fun KFrescoVitoDrawable.hideProgressLayer() {
  progressLayer?.reset()
}

fun KFrescoVitoDrawable.updateProgress(dataSource: DataSource<CloseableReference<CloseableImage>>) {
  progressLayer?.getDataModel().maybeGetDrawable()?.apply {
    if (dataSource.isFinished) {
      return@apply
    }
    setProgress(dataSource.progress)
  }
}

/** Set the progress for a given progress Drawable. The value must be between 0 and 1 */
fun Drawable.setProgress(progress: Float) {
  level = (progress * 10000).roundToInt()
  toggleAnimation(level <= 9990)
}


/*
 * 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.vito.core.impl

import com.facebook.common.closeables.AutoCleanupDelegate
import com.facebook.datasource.DataSource

class DataSourceCleanupDelegate :
    AutoCleanupDelegate<DataSource<out Any>>(null, dataSourceCleanupFunction)

private val dataSourceCleanupFunction: (DataSource<out Any>) -> Unit = { it.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.vito.core.impl

import android.graphics.Path
import android.graphics.RectF
import com.facebook.fresco.vito.options.RoundingOptions
import com.facebook.fresco.vito.renderer.CircleShape
import com.facebook.fresco.vito.renderer.PathShape
import com.facebook.fresco.vito.renderer.RectShape
import com.facebook.fresco.vito.renderer.RoundedRectShape
import com.facebook.fresco.vito.renderer.Shape
import kotlin.math.min

class ShapeCalculator {
  companion object {
    fun getShape(
        bounds: RectF,
        roundingOptions: RoundingOptions?,
        cornerRadiusAdjustment: Float = 0f
    ): Shape {
      return when {
        roundingOptions == null -> RectShape(bounds)
        roundingOptions.isCircular -> {
          CircleShape(bounds.centerX(), bounds.centerY(), min(bounds.width(), bounds.height()) / 2f)
        }
        roundingOptions.hasRoundedCorners() -> {
          when {
            roundingOptions.cornerRadius != RoundingOptions.CORNER_RADIUS_UNSET -> {
              val radius = roundingOptions.cornerRadius + cornerRadiusAdjustment
              RoundedRectShape(bounds, radius, radius)
            }
            else -> {
              var radii = roundingOptions.cornerRadii
              if (radii != null && cornerRadiusAdjustment != 0f) {
                val insideBorderRadii = FloatArray(8)
                for (i in radii.indices) {
                  insideBorderRadii[i] = radii[i] + cornerRadiusAdjustment
                }
                radii = insideBorderRadii
              }
              if (radii == null) {
                throw IllegalArgumentException("Malformed rounding options $roundingOptions")
              }
              PathShape(
                  Path().apply {
                    addRoundRect(bounds, radii, Path.Direction.CW)
                    fillType = Path.FillType.WINDING
                  })
            }
          }
        }
        else -> RectShape(bounds)
      }
    }
  }
}


/*
 * 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.vito.core.impl

import android.graphics.Bitmap
import android.os.Build
import com.facebook.common.logging.FLog
import com.facebook.fresco.vito.core.impl.ImagePipelineUtilsImpl.CircularBitmapRounding
import com.facebook.fresco.vito.core.impl.ImagePipelineUtilsImpl.ImageDecodeOptionsProvider
import com.facebook.fresco.vito.options.DecodedImageOptions
import com.facebook.fresco.vito.options.RoundingOptions
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.request.ImageRequestBuilder

class DefaultImageDecodeOptionsProviderImpl(
    private val circularBitmapRounding: CircularBitmapRounding?
) : ImageDecodeOptionsProvider {

  override fun create(
      imageRequestBuilder: ImageRequestBuilder,
      imageOptions: DecodedImageOptions
  ): ImageDecodeOptions? =
      maybeCreateFromConfigAndCustomDecoder(imageOptions)
          ?: maybeSetupPipelineRounding(
              imageOptions.roundingOptions, imageOptions.bitmapConfig, circularBitmapRounding)

  companion object {
    private const val TAG = "DefaultImageOptionsProvider"

    @JvmStatic
    fun maybeCreateFromConfigAndCustomDecoder(
        imageOptions: DecodedImageOptions
    ): ImageDecodeOptions? {
      val bitmapConfig = imageOptions.bitmapConfig
      val imageDecodeOptions = imageOptions.imageDecodeOptions
      if (bitmapConfig != null) {
        if (imageOptions.roundingOptions != null || imageOptions.postprocessor != null) {
          FLog.wtf(TAG, "Trying to use bitmap config incompatible with rounding.")
        } else {
          return ImageDecodeOptions.newBuilder()
              .setBitmapConfig(bitmapConfig)
              .setCustomImageDecoder(imageOptions.imageDecodeOptions?.customImageDecoder)
              .build()
        }
      } else if (imageDecodeOptions?.customImageDecoder != null) {
        return ImageDecodeOptions.newBuilder()
            .setCustomImageDecoder(imageDecodeOptions.customImageDecoder)
            .build()
      }

      return null
    }

    @JvmStatic
    fun maybeSetupPipelineRounding(
        roundingOptions: RoundingOptions?,
        bitmapConfig: Bitmap.Config?,
        circularBitmapRounding: CircularBitmapRounding?
    ): ImageDecodeOptions? =
        if (roundingOptions == null ||
            roundingOptions.isForceRoundAtDecode ||
            !roundingOptions.isCircular ||
            circularBitmapRounding == null ||
            pipelineRoundingUnsupportedForBitmapConfig(bitmapConfig)) {
          null
        } else {
          circularBitmapRounding.getDecodeOptions(roundingOptions.isAntiAliased)
        }

    private fun pipelineRoundingUnsupportedForBitmapConfig(bitmapConfig: Bitmap.Config?): Boolean =
        bitmapConfig == Bitmap.Config.RGB_565 ||
            (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
                bitmapConfig == Bitmap.Config.HARDWARE)
  }
}


/*
 * 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.vito.core.impl

import android.graphics.PointF
import android.graphics.Rect
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import com.facebook.drawee.components.DeferredReleaser.Releasable
import com.facebook.drawee.drawable.FadeDrawable
import com.facebook.drawee.drawable.ScaleTypeDrawable
import com.facebook.drawee.drawable.ScalingUtils
import com.facebook.drawee.drawable.TransformAwareDrawable
import com.facebook.drawee.drawable.TransformCallback
import com.facebook.drawee.drawable.VisibilityCallback
import com.facebook.fresco.vito.core.FrescoDrawableInterface
import java.io.Closeable

abstract class FrescoDrawable2 :
    FadeDrawable(arrayOfNulls(LAYER_COUNT), false, IMAGE_DRAWABLE_INDEX),
    Drawable.Callback,
    TransformCallback,
    TransformAwareDrawable,
    Closeable,
    Releasable,
    FrescoDrawableInterface {

  var viewportDimensions: Rect? = null
  private var visibilityCallback: VisibilityCallback? = null

  override fun hasImage(): Boolean = getDrawable(IMAGE_DRAWABLE_INDEX) != null

  override fun setOverlayDrawable(drawable: Drawable?): Drawable? {
    val result = setDrawable(OVERLAY_DRAWABLE_INDEX, drawable)
    showLayerImmediately(OVERLAY_DRAWABLE_INDEX)
    return result
  }

  fun setProgressDrawable(drawable: Drawable?): Drawable? =
      setDrawable(PROGRESS_DRAWABLE_INDEX, drawable)

  fun setProgress(progress: Float) {
    val progressBarDrawable = getDrawable(PROGRESS_DRAWABLE_INDEX) ?: return
    // display progressbar when not fully loaded, hide otherwise
    if (progress >= 0.999f) {
      maybeStopAnimation(progressBarDrawable)
    } else {
      maybeStartAnimation(progressBarDrawable)
    }
    // set drawable level, scaled to [0, 10000] per drawable specification
    progressBarDrawable.level = Math.round(progress * 10_000)
  }

  fun setPlaceholderDrawable(drawable: Drawable?): Drawable? =
      setDrawable(PLACEHOLDER_DRAWABLE_INDEX, drawable)

  fun fadeInImage(durationMs: Int) {
    transitionDuration = durationMs
    beginBatchMode()
    fadeOutLayer(PLACEHOLDER_DRAWABLE_INDEX)
    fadeOutLayer(PROGRESS_DRAWABLE_INDEX)
    fadeInLayer(IMAGE_DRAWABLE_INDEX)
    endBatchMode()
  }

  fun showImageImmediately() {
    beginBatchMode()
    hideLayerImmediately(PLACEHOLDER_DRAWABLE_INDEX)
    hideLayerImmediately(PROGRESS_DRAWABLE_INDEX)
    showLayerImmediately(IMAGE_DRAWABLE_INDEX)
    endBatchMode()
  }

  fun showProgressImmediately() {
    showLayerImmediately(PROGRESS_DRAWABLE_INDEX)
  }

  override fun close() {
    maybeStopAnimation(getDrawable(PLACEHOLDER_DRAWABLE_INDEX))
    for (i in 0 until LAYER_COUNT) {
      setDrawable(i, null)
    }
  }

  override fun setVisible(visible: Boolean, restart: Boolean): Boolean {
    visibilityCallback?.onVisibilityChange(visible)
    return super.setVisible(visible, restart)
  }

  override fun setVisibilityCallback(visibilityCallback: VisibilityCallback?) {
    this.visibilityCallback = visibilityCallback
  }

  val overlayDrawable: Drawable?
    get() = getDrawable(OVERLAY_DRAWABLE_INDEX)

  val actualImageScaleType: ScalingUtils.ScaleType?
    get() {
      val actual = getDrawable(IMAGE_DRAWABLE_INDEX)
      return if (actual !is ScaleTypeDrawable) null else actual.scaleType
    }

  val actualImageFocusPoint: PointF?
    get() {
      val actual = getDrawable(IMAGE_DRAWABLE_INDEX)
      return if (actual !is ScaleTypeDrawable) null else actual.focusPoint
    }

  /** @return the width of the underlying actual image or -1 if unset */
  abstract val actualImageWidthPx: Int

  /** @return the width of the underlying actual image or -1 if unset */
  abstract val actualImageHeightPx: Int
  abstract val actualImageWrapper: ScaleTypeDrawable?

  abstract fun cancelReleaseDelayed()

  abstract fun cancelReleaseNextFrame()

  companion object {
    private const val LAYER_COUNT = 4
    private const val PLACEHOLDER_DRAWABLE_INDEX = 0
    const val IMAGE_DRAWABLE_INDEX = 1
    private const val PROGRESS_DRAWABLE_INDEX = 2
    private const val OVERLAY_DRAWABLE_INDEX = 3

    private fun maybeStopAnimation(drawable: Drawable?) {
      if (drawable is Animatable) {
        (drawable as Animatable).stop()
      }
    }

    private fun maybeStartAnimation(drawable: Drawable?) {
      if (drawable is Animatable) {
        (drawable as Animatable).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.fresco.vito.core.impl

import android.net.Uri
import com.facebook.callercontext.CallerContextVerifier
import com.facebook.datasource.DataSource
import com.facebook.datasource.DataSources
import com.facebook.fresco.vito.core.FrescoVitoPrefetcher
import com.facebook.fresco.vito.core.ImagePipelineUtils
import com.facebook.fresco.vito.core.PrefetchTarget
import com.facebook.fresco.vito.core.VitoImageRequest
import com.facebook.fresco.vito.options.DecodedImageOptions
import com.facebook.fresco.vito.options.EncodedImageOptions
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.options.ImageOptions.Companion.defaults
import com.facebook.imagepipeline.core.ImagePipeline
import com.facebook.imagepipeline.listener.RequestListener
import com.facebook.imagepipeline.request.ImageRequest
import java.lang.NullPointerException
import java.util.concurrent.CancellationException

class FrescoVitoPrefetcherImpl(
    private val imagePipeline: ImagePipeline,
    private val imagePipelineUtils: ImagePipelineUtils,
    private val callerContextVerifier: CallerContextVerifier?
) : FrescoVitoPrefetcher {

  override fun prefetch(
      prefetchTarget: PrefetchTarget,
      uri: Uri,
      imageOptions: ImageOptions?,
      callerContext: Any?,
      callsite: String
  ): DataSource<Void> {
    return when (prefetchTarget) {
      PrefetchTarget.MEMORY_DECODED ->
          prefetchToBitmapCache(uri, imageOptions, callerContext, callsite)
      PrefetchTarget.MEMORY_ENCODED ->
          prefetchToEncodedCache(uri, imageOptions, callerContext, callsite)
      PrefetchTarget.DISK -> prefetchToDiskCache(uri, imageOptions, callerContext, callsite)
      else ->
          DataSources.immediateFailedDataSource(CancellationException("Prefetching is not enabled"))
    }
  }

  override fun prefetchToBitmapCache(
      uri: Uri,
      imageOptions: DecodedImageOptions?,
      callerContext: Any?,
      callsite: String
  ): DataSource<Void> {
    val imageRequest = imagePipelineUtils.buildImageRequest(uri, imageOptions ?: defaults())
    return prefetch(PrefetchTarget.MEMORY_DECODED, imageRequest, callerContext, null)
  }

  override fun prefetchToEncodedCache(
      uri: Uri,
      imageOptions: EncodedImageOptions?,
      callerContext: Any?,
      callsite: String
  ): DataSource<Void> {
    val imageRequest = imagePipelineUtils.buildEncodedImageRequest(uri, imageOptions ?: defaults())
    return prefetch(PrefetchTarget.MEMORY_ENCODED, imageRequest, callerContext, null)
  }

  override fun prefetchToDiskCache(
      uri: Uri,
      imageOptions: ImageOptions?,
      callerContext: Any?,
      callsite: String
  ): DataSource<Void> {
    val imageRequest = imagePipelineUtils.buildEncodedImageRequest(uri, imageOptions ?: defaults())
    return prefetch(PrefetchTarget.DISK, imageRequest, callerContext, null)
  }

  override fun prefetch(
      prefetchTarget: PrefetchTarget,
      imageRequest: VitoImageRequest,
      callerContext: Any?,
      requestListener: RequestListener?,
      callsite: String
  ): DataSource<Void> =
      prefetch(prefetchTarget, imageRequest.finalImageRequest, callerContext, requestListener)

  private fun prefetch(
      prefetchTarget: PrefetchTarget,
      imageRequest: ImageRequest?,
      callerContext: Any?,
      requestListener: RequestListener?
  ): DataSource<Void> {
    callerContextVerifier?.verifyCallerContext(callerContext, false)
    return if (imageRequest == null) {
      DataSources.immediateFailedDataSource(NULL_IMAGE_MESSAGE)
    } else {
      when (prefetchTarget) {
        PrefetchTarget.MEMORY_DECODED ->
            imagePipeline.prefetchToBitmapCache(imageRequest, callerContext, requestListener)
        PrefetchTarget.MEMORY_ENCODED ->
            imagePipeline.prefetchToEncodedCache(imageRequest, callerContext, requestListener)
        PrefetchTarget.DISK ->
            imagePipeline.prefetchToDiskCache(imageRequest, callerContext, requestListener)
      }
    }
  }

  override fun setDistanceToViewport(
      distance: Int,
      callerContext: Any?,
      uri: Uri?,
      callsite: String
  ) {
    // empty
  }

  companion object {
    private val NULL_IMAGE_MESSAGE: Throwable = NullPointerException("No image to prefetch.")
  }
}


/*
 * 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.vito.core.impl

import android.net.Uri
import com.facebook.fresco.vito.core.ImagePipelineUtils
import com.facebook.fresco.vito.options.DecodedImageOptions
import com.facebook.fresco.vito.options.EncodedImageOptions
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.common.Priority
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder

/**
 * Utility methods to create [ImageRequest]s for [com.facebook.fresco.vito.options.ImageOptions].
 */
class ImagePipelineUtilsImpl(private val imageDecodeOptionsProvider: ImageDecodeOptionsProvider) :
    ImagePipelineUtils {

  fun interface CircularBitmapRounding {
    fun getDecodeOptions(antiAliased: Boolean): ImageDecodeOptions?
  }

  fun interface ImageDecodeOptionsProvider {
    fun create(
        imageRequestBuilder: ImageRequestBuilder,
        imageOptions: DecodedImageOptions
    ): ImageDecodeOptions?
  }

  override fun buildImageRequest(uri: Uri?, imageOptions: DecodedImageOptions): ImageRequest? {
    if (uri == null) {
      return null
    }
    return createDecodedImageRequestBuilder(
            createEncodedImageRequestBuilder(uri, imageOptions), imageOptions)
        ?.build()
  }

  override fun wrapDecodedImageRequest(
      originalRequest: ImageRequest,
      imageOptions: DecodedImageOptions
  ): ImageRequest? =
      createDecodedImageRequestBuilder(
              createEncodedImageRequestBuilder(originalRequest, imageOptions), imageOptions)
          ?.build()

  override fun buildEncodedImageRequest(
      uri: Uri?,
      imageOptions: EncodedImageOptions
  ): ImageRequest? = createEncodedImageRequestBuilder(uri, imageOptions)?.build()

  protected fun createDecodedImageRequestBuilder(
      imageRequestBuilder: ImageRequestBuilder?,
      imageOptions: DecodedImageOptions
  ): ImageRequestBuilder? =
      imageRequestBuilder?.apply {
        imageOptions.resizeOptions?.let { resizeOptions = it }
        imageOptions.rotationOptions?.let { rotationOptions = it }
        imageDecodeOptionsProvider.create(imageRequestBuilder, imageOptions)?.let {
          imageDecodeOptions = it
        }
        isLocalThumbnailPreviewsEnabled = imageOptions.areLocalThumbnailPreviewsEnabled()
        loadThumbnailOnly = imageOptions.loadThumbnailOnly
        imageOptions.postprocessor?.let { postprocessor = it }
        imageOptions.isProgressiveDecodingEnabled?.let { isProgressiveRenderingEnabled = it }
      }

  protected fun createEncodedImageRequestBuilder(
      uri: Uri?,
      imageOptions: EncodedImageOptions
  ): ImageRequestBuilder? {
    if (uri == null) {
      return null
    }
    val builder = ImageRequestBuilder.newBuilderWithSource(uri)
    maybeSetRequestPriority(builder, imageOptions.priority)
    if (imageOptions.cacheChoice != null) {
      builder.cacheChoice = imageOptions.cacheChoice
    }
    return builder
  }

  protected fun createEncodedImageRequestBuilder(
      imageRequest: ImageRequest?,
      imageOptions: EncodedImageOptions
  ): ImageRequestBuilder? {
    if (imageRequest == null) {
      return null
    }
    val builder = ImageRequestBuilder.fromRequest(imageRequest)
    maybeSetRequestPriority(builder, imageOptions.priority)
    return builder
  }

  companion object {
    private fun maybeSetRequestPriority(builder: ImageRequestBuilder, priority: Priority?) {
      if (priority != null) {
        builder.requestPriority = 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.vito.core.impl

import com.facebook.common.internal.Supplier
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.datasource.DataSources
import com.facebook.datasource.FirstAvailableDataSourceSupplier
import com.facebook.datasource.IncreasingQualityDataSourceSupplier
import com.facebook.fresco.middleware.Extras
import com.facebook.fresco.vito.core.ImagePipelineUtils
import com.facebook.fresco.vito.core.impl.source.ImagePipelineImageSource
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.source.EmptyImageSource
import com.facebook.fresco.vito.source.FirstAvailableImageSource
import com.facebook.fresco.vito.source.ImageSource
import com.facebook.fresco.vito.source.IncreasingQualityImageSource
import com.facebook.fresco.vito.source.SingleImageSource
import com.facebook.imagepipeline.core.ImagePipeline
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.listener.RequestListener
import com.facebook.imagepipeline.request.ImageRequest
import java.lang.NullPointerException
import java.util.ArrayList

object ImageSourceToImagePipelineAdapter {

  val NO_REQUEST_EXCEPTION = NullPointerException("No image request was specified!")

  private val NO_REQUEST_SUPPLIER: Supplier<DataSource<CloseableReference<CloseableImage>>> =
      Supplier {
        DataSources.immediateFailedDataSource(NO_REQUEST_EXCEPTION)
      }

  /**
   * Get the final image request for the last image if known. In some cases, like for @{link
   * FirstAvailableImageSource}, we do not know the final image request, so this method will return
   * null.
   *
   * @param imagePipelineUtils util class to create the final image request
   * @param imageOptions the image options to use, important if for example rounding is done at
   *   decode time
   * @return the final image request or null if not possible to determine
   */
  @JvmStatic
  fun maybeExtractFinalImageRequest(
      imageSource: ImageSource,
      imagePipelineUtils: ImagePipelineUtils,
      imageOptions: ImageOptions
  ): ImageRequest? {
    return when (imageSource) {
      is SingleImageSource -> imageSource.extractSingleRequest(imagePipelineUtils, imageOptions)
      is EmptyImageSource -> null
      is FirstAvailableImageSource ->
          imageSource.extractFirstAvailableRequest(imagePipelineUtils, imageOptions)
      is IncreasingQualityImageSource ->
          maybeExtractFinalImageRequest(imageSource.highResSource, imagePipelineUtils, imageOptions)
      is ImagePipelineImageSource ->
          imageSource.maybeExtractFinalImageRequest(imagePipelineUtils, imageOptions)
      else -> null
    }
  }

  @JvmStatic
  fun createDataSourceSupplier(
      imageSource: ImageSource,
      imagePipeline: ImagePipeline,
      imagePipelineUtils: ImagePipelineUtils,
      imageOptions: ImageOptions,
      callerContext: Any?,
      requestListener: RequestListener?,
      uiComponentId: String,
      extras: MutableMap<String, Any?>
  ): Supplier<DataSource<CloseableReference<CloseableImage>>> {
    return when (imageSource) {
      is SingleImageSource -> {
        return Supplier<DataSource<CloseableReference<CloseableImage>>> {
          val imageRequest = imageSource.extractSingleRequest(imagePipelineUtils, imageOptions)
          createDataSource(
              imageRequest,
              imagePipeline,
              callerContext,
              requestListener,
              uiComponentId,
              ImageRequest.RequestLevel.FULL_FETCH,
              extras)
        }
      }
      is ImagePipelineImageSource -> {
        return Supplier<DataSource<CloseableReference<CloseableImage>>> {
          val imageRequest =
              imageSource.maybeExtractFinalImageRequest(imagePipelineUtils, imageOptions)
          createDataSource(
              imageRequest,
              imagePipeline,
              callerContext,
              requestListener,
              uiComponentId,
              imageSource.getRequestLevelForFetch(),
              extras)
        }
      }
      is EmptyImageSource -> NO_REQUEST_SUPPLIER
      is FirstAvailableImageSource ->
          FirstAvailableDataSourceSupplier.create(
              imageSource.imageSources.mapTo(ArrayList(imageSource.imageSources.size)) {
                createDataSourceSupplier(
                    it,
                    imagePipeline,
                    imagePipelineUtils,
                    imageOptions,
                    callerContext,
                    requestListener,
                    uiComponentId,
                    extras)
              })
      is IncreasingQualityImageSource ->
          return IncreasingQualityDataSourceSupplier.create(
              arrayListOf(
                  createDataSourceSupplier(
                      imageSource.highResSource,
                      imagePipeline,
                      imagePipelineUtils,
                      imageOptions,
                      callerContext,
                      requestListener,
                      uiComponentId,
                      extras),
                  createDataSourceSupplier(
                      imageSource.lowResSource,
                      imagePipeline,
                      imagePipelineUtils,
                      imageOptions,
                      callerContext,
                      requestListener,
                      uiComponentId,
                      extras)))
      else -> NO_REQUEST_SUPPLIER
    }
  }

  @JvmStatic
  fun createDataSource(
      imageRequest: ImageRequest?,
      imagePipeline: ImagePipeline,
      callerContext: Any?,
      requestListener: RequestListener?,
      uiComponentId: String,
      requestLevel: ImageRequest.RequestLevel = ImageRequest.RequestLevel.FULL_FETCH,
      extras: Extras
  ): DataSource<CloseableReference<CloseableImage>> {
    return if (imageRequest != null) {
      imagePipeline.fetchDecodedImage(
          imageRequest,
          callerContext,
          requestLevel,
          requestListener, // TODO: Check if this is correct !!
          uiComponentId,
          extras)
    } else {
      NO_REQUEST_SUPPLIER.get()
    }
  }

  fun SingleImageSource.extractSingleRequest(
      imagePipelineUtils: ImagePipelineUtils,
      imageOptions: ImageOptions
  ): ImageRequest? = imagePipelineUtils.buildImageRequest(uri, imageOptions)

  fun FirstAvailableImageSource.extractFirstAvailableRequest(
      imagePipelineUtils: ImagePipelineUtils,
      imageOptions: ImageOptions
  ): ImageRequest? {
    imageSources.forEach {
      val request = maybeExtractFinalImageRequest(it, imagePipelineUtils, imageOptions)
      if (request != null) {
        return request
      }
    }
    return 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.vito.core.impl

import android.net.Uri
import com.facebook.datasource.DataSource
import com.facebook.fresco.vito.core.FrescoVitoPrefetcher
import com.facebook.fresco.vito.core.PrefetchTarget
import com.facebook.fresco.vito.core.VitoImageRequest
import com.facebook.fresco.vito.options.DecodedImageOptions
import com.facebook.fresco.vito.options.EncodedImageOptions
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.imagepipeline.listener.RequestListener
import java.lang.UnsupportedOperationException

class NoOpFrescoVitoPrefetcher : FrescoVitoPrefetcher {

  override fun prefetch(
      prefetchTarget: PrefetchTarget,
      uri: Uri,
      imageOptions: ImageOptions?,
      callerContext: Any?,
      callsite: String
  ): DataSource<Void> = throwUnsupportedOperationException()

  override fun prefetchToBitmapCache(
      uri: Uri,
      imageOptions: DecodedImageOptions?,
      callerContext: Any?,
      callsite: String
  ): DataSource<Void> = throwUnsupportedOperationException()

  override fun prefetchToEncodedCache(
      uri: Uri,
      imageOptions: EncodedImageOptions?,
      callerContext: Any?,
      callsite: String
  ): DataSource<Void> = throwUnsupportedOperationException()

  override fun prefetchToDiskCache(
      uri: Uri,
      imageOptions: ImageOptions?,
      callerContext: Any?,
      callsite: String
  ): DataSource<Void> = throwUnsupportedOperationException()

  override fun prefetch(
      prefetchTarget: PrefetchTarget,
      imageRequest: VitoImageRequest,
      callerContext: Any?,
      requestListener: RequestListener?,
      callsite: String
  ): DataSource<Void> = throwUnsupportedOperationException()

  override fun setDistanceToViewport(
      distance: Int,
      callerContext: Any?,
      uri: Uri?,
      callsite: String
  ) = throwUnsupportedOperationException()

  private fun throwUnsupportedOperationException(): Nothing {
    throw UnsupportedOperationException(EXCEPTION_MSG)
  }

  private companion object {
    private const val EXCEPTION_MSG = "Image prefetching with Fresco Vito is disabled!"
  }
}


/*
 * 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.vito.core.impl

import android.content.res.Resources
import android.graphics.Rect
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.fresco.middleware.HasExtraData
import com.facebook.fresco.ui.common.VitoUtils
import com.facebook.fresco.urimod.Dimensions
import com.facebook.fresco.urimod.UriModifier
import com.facebook.fresco.vito.core.ImagePipelineUtils
import com.facebook.fresco.vito.core.VitoImagePipeline
import com.facebook.fresco.vito.core.VitoImageRequest
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.options.ImageOptions.Companion.defaults
import com.facebook.fresco.vito.source.ImageSource
import com.facebook.fresco.vito.source.ImageSourceProvider
import com.facebook.fresco.vito.source.SingleImageSource
import com.facebook.imagepipeline.core.ImagePipeline
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.listener.RequestListener
import com.facebook.imagepipeline.systrace.FrescoSystrace

/** Vito image pipeline to fetch an image for a given VitoImageRequest. */
class VitoImagePipelineImpl(
    private val imagePipeline: ImagePipeline,
    private val imagePipelineUtils: ImagePipelineUtils
) : VitoImagePipeline {

  override fun createImageRequest(
      resources: Resources,
      imageSource: ImageSource,
      options: ImageOptions?,
      viewport: Rect?
  ): VitoImageRequest {
    val imageOptions = options ?: defaults()
    val extras: MutableMap<String, Any?> = mutableMapOf()
    var finalImageSource = imageSource
    if (imageSource is SingleImageSource) {
      val uri = imageSource.uri
      val maybeModifiedUri =
          UriModifier.INSTANCE.modifyUri(
              uri,
              viewport?.let { Dimensions(it.width(), it.height()) },
              imageOptions.actualImageScaleType)

      if (maybeModifiedUri != uri) {
        extras[HasExtraData.KEY_MODIFIED_URL] = true
        finalImageSource = ImageSourceProvider.forUri(maybeModifiedUri)
      }
      if (imageSource.extras != null) {
        extras[HasExtraData.KEY_IMAGE_SOURCE_EXTRAS] = imageSource.extras
      }
    }

    val finalImageRequest =
        ImageSourceToImagePipelineAdapter.maybeExtractFinalImageRequest(
            finalImageSource, imagePipelineUtils, imageOptions)
    val finalImageCacheKey = finalImageRequest?.let { imagePipeline.getCacheKey(it, null) }

    return VitoImageRequest(
        resources, finalImageSource, imageOptions, finalImageRequest, finalImageCacheKey, extras)
  }

  override fun getCachedImage(imageRequest: VitoImageRequest): CloseableReference<CloseableImage>? {
    if (FrescoSystrace.isTracing()) {
      FrescoSystrace.beginSection("VitoImagePipeline#getCachedImage")
    }

    return try {
      val cachedImageReference = imagePipeline.getCachedImage(imageRequest.finalImageCacheKey)
      if (CloseableReference.isValid(cachedImageReference)) {
        cachedImageReference
      } else {
        null
      }
    } finally {
      if (FrescoSystrace.isTracing()) {
        FrescoSystrace.endSection()
      }
    }
  }

  override fun fetchDecodedImage(
      imageRequest: VitoImageRequest,
      callerContext: Any?,
      requestListener: RequestListener?,
      uiComponentId: Long
  ): DataSource<CloseableReference<CloseableImage>> =
      ImageSourceToImagePipelineAdapter.createDataSourceSupplier(
              imageRequest.imageSource,
              imagePipeline,
              imagePipelineUtils,
              imageRequest.imageOptions,
              callerContext,
              requestListener,
              VitoUtils.getStringId(uiComponentId),
              imageRequest.extras)
          .get()
}


/*
 * 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.vito.core.impl.debug

import com.facebook.fresco.ui.common.ControllerListener2.Extras
import com.facebook.fresco.vito.core.impl.FrescoDrawable2

class NoOpDebugOverlayFactory2 : DebugOverlayFactory2 {
  override fun update(drawable: FrescoDrawable2, extras: Extras?) {
    // no-op
  }
}


/*
 * 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.vito.core.impl.source

import android.net.Uri
import com.facebook.fresco.vito.core.ImagePipelineUtils
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.source.ImageSource
import com.facebook.imagepipeline.request.ImageRequest

/** Image source specific for the Fresco ImagePipeline, based on an ImageRequest. */
interface ImagePipelineImageSource : ImageSource {
  fun maybeExtractFinalImageRequest(
      imagePipelineUtils: ImagePipelineUtils,
      imageOptions: ImageOptions
  ): ImageRequest?

  fun getRequestLevelForFetch(): ImageRequest.RequestLevel = ImageRequest.RequestLevel.FULL_FETCH

  fun getUri(): 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.fresco.vito.draweesupport

import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import com.facebook.drawee.backends.pipeline.info.ImageOrigin
import com.facebook.drawee.controller.ControllerListener
import com.facebook.fresco.ui.common.DimensionsInfo
import com.facebook.fresco.ui.common.OnDrawControllerListener
import com.facebook.fresco.vito.listener.ImageListener
import com.facebook.imagepipeline.image.ImageInfo
import com.facebook.infer.annotation.Assertions
import com.facebook.infer.annotation.PropagatesNullable

class ControllerListenerWrapper(private val controllerListener: ControllerListener<ImageInfo>) :
    ImageListener {

  override fun onSubmit(id: Long, callerContext: Any?) {
    controllerListener.onSubmit(
        toStringId(id),
        Assertions.nullsafeFIXME(callerContext, "Legacy ControllerListener is not nullsafe"))
  }

  override fun onPlaceholderSet(id: Long, placeholder: Drawable?) {
    // Not present in old API
  }

  override fun onFinalImageSet(
      id: Long,
      @ImageOrigin imageOrigin: Int,
      imageInfo: ImageInfo?,
      drawable: Drawable?
  ) {
    controllerListener.onFinalImageSet(
        toStringId(id), imageInfo, if (drawable is Animatable) drawable else null)
  }

  override fun onIntermediateImageSet(id: Long, imageInfo: ImageInfo?) {
    controllerListener.onIntermediateImageSet(toStringId(id), imageInfo)
  }

  override fun onIntermediateImageFailed(id: Long, throwable: Throwable?) {
    controllerListener.onIntermediateImageFailed(
        toStringId(id),
        Assertions.nullsafeFIXME(throwable, "Legacy ControllerListener is not nullsafe"))
  }

  override fun onFailure(id: Long, error: Drawable?, throwable: Throwable?) {
    controllerListener.onFailure(
        toStringId(id),
        Assertions.nullsafeFIXME(throwable, "Legacy ControllerListener is not nullsafe"))
  }

  override fun onRelease(id: Long) {
    controllerListener.onRelease(toStringId(id))
  }

  override fun onImageDrawn(id: String, imageInfo: ImageInfo, dimensionsInfo: DimensionsInfo) {
    (controllerListener as? OnDrawControllerListener<ImageInfo>)?.onImageDrawn(
        id, imageInfo, dimensionsInfo)
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false

    other as ControllerListenerWrapper

    if (controllerListener != other.controllerListener) return false

    return true
  }

  override fun hashCode(): Int {
    return controllerListener.hashCode()
  }

  companion object {
    /**
     * Create a new controller listener wrapper or return null if the listener is null.
     *
     * @param controllerListener the controller listener to wrap
     * @return the wrapped controller listener or null if no wrapping required
     */
    @JvmStatic
    fun create(
        @PropagatesNullable controllerListener: ControllerListener<ImageInfo>?
    ): ControllerListenerWrapper? = controllerListener?.let { ControllerListenerWrapper(it) }

    private fun toStringId(id: Long): String = "v$id"
  }
}


/*
 * 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.vito.draweesupport

import android.content.res.Resources
import android.graphics.drawable.Drawable
import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.options.ImageOptionsDrawableFactory
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage

class DrawableFactoryWrapper(private val drawableFactory: DrawableFactory) :
    ImageOptionsDrawableFactory {
  override fun createDrawable(
      resources: Resources,
      closeableImage: CloseableImage,
      imageOptions: ImageOptions
  ): Drawable? =
      if (drawableFactory.supportsImageType(closeableImage)) {
        if (drawableFactory is ImageOptionsDrawableFactory) {
          drawableFactory.createDrawable(resources, closeableImage, imageOptions)
        } else {
          drawableFactory.createDrawable(closeableImage)
        }
      } else {
        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.vito.draweesupport

import com.facebook.drawee.generic.RoundingParams
import com.facebook.fresco.vito.options.BorderOptions
import com.facebook.fresco.vito.options.RoundingOptions

object RoundingParamsWrapper {

  @JvmStatic
  fun roundingOptionsFromRoundingParams(roundingParams: RoundingParams?): RoundingOptions? {
    return when {
      roundingParams == null -> null
      roundingParams.roundAsCircle -> RoundingOptions.asCircle(true)
      roundingParams.cornersRadii != null ->
          RoundingOptions.forCornerRadii(roundingParams.cornersRadii!!, true)
      // TODO: we also have an overlay color mode where you specify a color to be drawn on top. Vito
      // does not support this.
      // Probably only used for borders
      else -> null
    }
  }

  @JvmStatic
  fun borderOptionsFromRoundingParams(roundingParams: RoundingParams?): BorderOptions? =
      if (roundingParams == null || roundingParams.borderWidth <= 0f) {
        null
      } else {
        BorderOptions.create(
            roundingParams.borderColor,
            roundingParams.borderWidth,
            roundingParams.padding,
            roundingParams.scaleDownInsideBorders)
      }
}