/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.rendercore.incrementalmount

import android.os.Handler
import android.os.Looper
import android.view.Choreographer
import android.view.Display
import com.facebook.rendercore.MountDelegate
import com.facebook.rendercore.Systracer
import com.facebook.rendercore.incrementalmount.IncrementalMountExtensionConfigs.gapWorkerDeadlineBufferMs
import java.util.concurrent.TimeUnit

class IncrementalMountGapWorker
private constructor(
    private val frameIntervalNs: Long,
    private val tracer: Systracer,
) : Choreographer.FrameCallback, Runnable {

  private val handler: Handler = Handler(Looper.getMainLooper())
  private val delegates: MutableSet<MountDelegate> by lazy { mutableSetOf() }

  private var latestFrameTimeNs: Long = 0
  private var isRunning: Boolean = false

  /**
   * On every frame update the frame time. If there are items to mount then it also posts a
   * [Runnable] on the UI Thread to premount content during any gaps.
   */
  override fun doFrame(frameTimeNanos: Long) {
    latestFrameTimeNs = frameTimeNanos // update the frame time

    if (delegates.isNotEmpty()) {
      val isTracing = tracer.isTracing
      if (isTracing) {
        tracer.beginSection("IncrementalMountGapWorker::doFrame")
      }
      handler.post(this)
      Choreographer.getInstance().postFrameCallback(this)
      if (isTracing) {
        tracer.endSection()
      }
    } else {
      isRunning = false
    }
  }

  /** The deadline is [deadlineBufferNs] nanoseconds before the next frame. */
  override fun run() {
    premount(latestFrameTimeNs + frameIntervalNs - deadlineBufferNs)
  }

  private fun premount(deadlineNs: Long) {
    val isTracing = tracer.isTracing
    if (isTracing) {
      tracer.beginSection("premount")
    }
    try {
      val iterator = delegates.iterator()
      while (iterator.hasNext() && !isTimeUp(deadlineNs)) {
        if (!premountNext(iterator.next(), deadlineNs)) {
          iterator.remove()
        }
      }
    } finally {
      if (isTracing) {
        tracer.endSection()
      }
    }
  }

  /** returns `true` iff there are still items left to mount */
  private fun premountNext(
      delegate: MountDelegate,
      deadlineNs: Long,
  ): Boolean {
    val isTracing = tracer.isTracing
    if (isTracing) {
      tracer.beginSection("premount-item")
    }
    try {
      // While there are items to mount and there is time left to mount
      while (delegate.hasItemToMount() && !isTimeUp(deadlineNs)) {
        delegate.premountNext()
      }
    } finally {
      if (isTracing) {
        tracer.endSection()
      }
    }

    return delegate.hasItemToMount()
  }

  private fun startIfNotRunning(updatedFrameTimeMs: Long?) {
    if (updatedFrameTimeMs != null) {
      val updatedFrameTimeNs = TimeUnit.MILLISECONDS.toNanos(updatedFrameTimeMs)
      if (updatedFrameTimeNs > latestFrameTimeNs) {
        latestFrameTimeNs = updatedFrameTimeNs
      }
    }
    if (isRunning) {
      return
    }
    if (delegates.isNotEmpty()) {
      isRunning = true
      handler.post(this) // post to premount in this frame
      Choreographer.getInstance().postFrameCallback(this)
    }
  }

  /**
   * Registers a [MountDelegate] with the [IncrementalMountGapWorker]. This will also start the
   * [IncrementalMountGapWorker] if it isn't already running.
   */
  fun add(root: MountDelegate, frameTimeMs: Long?) {
    delegates.add(root)
    startIfNotRunning(frameTimeMs)
  }

  /**
   * Removes a [MountDelegate] registered with the [IncrementalMountGapWorker]. This will also
   * disable the [IncrementalMountGapWorker] if there are no more [MountDelegate]s.
   */
  fun remove(root: MountDelegate) {
    delegates.remove(root)
  }

  companion object {

    private var worker: IncrementalMountGapWorker? = null

    @JvmStatic
    fun get(display: Display?, tracer: Systracer): IncrementalMountGapWorker {
      val frameInterval = getFrameIntervalInNs(display)
      return IncrementalMountGapWorker(frameInterval, tracer).apply { worker = this }
    }

    @JvmStatic
    private fun getFrameIntervalInNs(display: Display?): Long {

      // This logic was been copied from the RecyclerView
      // break 60 fps assumption if data from display appears valid
      // NOTE: Do only once because it's very expensive (> 1ms)
      var refreshRate = 60.0f
      if (display != null) {
        val displayRefreshRate = display.refreshRate
        if (displayRefreshRate >= 30.0f) {
          refreshRate = displayRefreshRate
        }
      }

      return (1000000000 / refreshRate).toLong()
    }

    fun isTimeUp(deadlineNs: Long): Boolean = System.nanoTime() >= deadlineNs

    private val deadlineBufferNs: Long = TimeUnit.MILLISECONDS.toNanos(gapWorkerDeadlineBufferMs)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:JvmName("IncrementalMountUtils")

package com.facebook.rendercore.incrementalmount

import com.facebook.rendercore.RenderUnit

val RenderUnit<*>.shouldExcludeFromIncrementalMount: Boolean
  get() = getExtra<Boolean>(R.id.should_exclude_from_incremental_mount) == true

val RenderUnit<*>.doesMountRenderTreeHosts: Boolean
  get() =
      getExtra<Boolean>(com.facebook.rendercore.extensions.R.id.does_mount_render_tree_hosts) ==
          true || doesMountRenderTreeHosts()


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.rendercore.incrementalmount

import com.facebook.rendercore.primitives.MountConfigurationScope

/**
 * Indicates whether the component skips Incremental Mount. If this is true then the Component will
 * not be involved in Incremental Mount.
 */
var <ContentType : Any> MountConfigurationScope<ContentType>.shouldExcludeFromIncrementalMount:
    Boolean
  get() {
    throw IllegalStateException(
        "Getting the current value of excludeFromIncrementalMount is not supported.")
  }
  set(value) {
    addExtra(R.id.should_exclude_from_incremental_mount, true)
  }


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas

import android.content.Context
import android.view.View
import com.facebook.mountable.canvas.model.CanvasModel
import com.facebook.mountable.utils.types.CanvasLayerType
import com.facebook.mountable.utils.types.Size
import com.facebook.rendercore.ContentAllocator
import com.facebook.rendercore.LayoutContext
import com.facebook.rendercore.MeasureResult
import com.facebook.rendercore.Mountable
import com.facebook.rendercore.RenderUnit

/**
 * A mountable canvas component that can be shared across frameworks built on top of RenderCore such
 * as Litho and Bloks.
 *
 * @param layerType The layer type of the underlying Canvas View. Some of the drawing commands may
 *   require to use [CanvasLayerType.Software] in order work correctly on older the Android
 *   versions.
 * @property modelProvider The lambda that returns an instance of the CanvasModel describing the
 *   drawing commands.
 */
class CanvasMountable(
    layerType: CanvasLayerType,
    private val modelProvider: (canvasSize: Size) -> CanvasModel
) : Mountable<CanvasView>(RenderType.VIEW), ContentAllocator<CanvasView> {

  init {
    addOptionalMountBinder(DelegateBinder.createDelegateBinder(Unit, CANVAS_MODEL_BINDER))
    addOptionalMountBinder(DelegateBinder.createDelegateBinder(layerType, CANVAS_LAYER_TYPE_BINDER))
  }

  override fun createContent(context: Context): CanvasView = CanvasView(context)

  override fun measure(
      context: LayoutContext<*>,
      widthSpec: Int,
      heightSpec: Int,
      previousLayoutData: Any?
  ): MeasureResult {
    val width = fillOrGone(widthSpec)
    val height = fillOrGone(heightSpec)
    val canvasModel = modelProvider(Size(width.toFloat(), height.toFloat()))
    return MeasureResult(width, height, canvasModel)
  }

  override fun getContentAllocator(): ContentAllocator<CanvasView> {
    return this
  }

  private fun fillOrGone(measureSpec: Int): Int {
    val defaultSize = 0

    val specMode = View.MeasureSpec.getMode(measureSpec)
    val specSize = View.MeasureSpec.getSize(measureSpec)
    return when (specMode) {
      View.MeasureSpec.AT_MOST,
      View.MeasureSpec.EXACTLY -> specSize
      View.MeasureSpec.UNSPECIFIED -> defaultSize
      else -> defaultSize
    }
  }
}

private val CANVAS_MODEL_BINDER =
    object : RenderUnit.Binder<Unit, CanvasView, Any?> {
      override fun shouldUpdate(
          currentModel: Unit,
          newModel: Unit,
          currentLayoutData: Any?,
          nextLayoutData: Any?
      ): Boolean {
        val currentCanvasModel = currentLayoutData as CanvasModel
        val newCanvasModel = nextLayoutData as CanvasModel
        return currentCanvasModel != newCanvasModel
      }

      override fun bind(
          context: Context,
          content: CanvasView,
          model: Unit,
          layoutData: Any?
      ): Any? {
        content.canvasModel = layoutData as CanvasModel
        return null
      }

      override fun unbind(
          context: Context,
          content: CanvasView,
          model: Unit,
          layoutData: Any?,
          bindData: Any?
      ) {
        content.canvasModel = null
      }
    }

private val CANVAS_LAYER_TYPE_BINDER =
    object : RenderUnit.Binder<CanvasLayerType, CanvasView, Any?> {
      override fun shouldUpdate(
          currentModel: CanvasLayerType,
          newModel: CanvasLayerType,
          currentLayoutData: Any?,
          nextLayoutData: Any?
      ): Boolean {
        return currentModel != newModel
      }

      override fun bind(
          context: Context,
          content: CanvasView,
          model: CanvasLayerType,
          layoutData: Any?
      ): Any? {
        val canvasModel = layoutData as CanvasModel
        val layerType = model.toLayerType(canvasModel.needsSoftwareLayer())
        if (content.layerType != layerType) {
          content.setLayerType(layerType, null)
        }
        return null
      }

      override fun unbind(
          context: Context,
          content: CanvasView,
          model: CanvasLayerType,
          layoutData: Any?,
          bindData: Any?
      ) {
        content.setLayerType(View.LAYER_TYPE_NONE, null)
      }
    }


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas

import android.annotation.SuppressLint
import android.graphics.DashPathEffect
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import androidx.collection.LruCache
import androidx.core.util.Pools
import com.facebook.mountable.canvas.model.CanvasFill
import com.facebook.mountable.canvas.model.CanvasGradientShading
import com.facebook.mountable.canvas.model.CanvasLayer
import com.facebook.mountable.canvas.model.CanvasPathModel
import com.facebook.mountable.canvas.model.CanvasSolidColorShading
import com.facebook.mountable.canvas.model.CanvasStroke
import com.facebook.mountable.canvas.model.CanvasTransformModel

/**
 * The state of the mountable canvas component.
 *
 * Android [Paint] is a "heavy" object that is expensive to create and destroy. This class is
 * responsible for caching fill, stroke and layer Paint objects and reusing them for all drawing
 * commands. The Paint objects are updated only when necessary.
 *
 * Similarly [Matrix] and [Path] are expensive to create and destroy. [Matrix] objects are often
 * updated after they're created. [Path] objects on the other hand are updated less often. In order
 * to improve performance [Matrix] objects will be pooled and [Path] objects will be cached.
 *
 * In order to get the best performance, on instance of this class should be a component-scoped
 * object that survives state updates. When creating an instance of this class you should use:
 * - for Litho: [https://www.internalfb.com/intern/staticdocs/litho/docs/mainconcepts/use-cached/]
 * - for Bloks:
 *   [https://www.internalfb.com/intern/staticdocs/bloks/docs/bridging/android_binder_utils#controllerforcomponent]
 *
 * @param matrixPoolSize The Matrix pool max size
 * @param pathCacheSize The Path cache max size
 */
class CanvasState(
    matrixPoolSize: Int = DEFAULT_MATRIX_POOL_SIZE,
    pathCacheSize: Int = DEFAULT_PATH_CACHE_SIZE,
) {
  // The paint object used for filling shapes
  private var fillPaint: Paint? = null
  private var fillModel: CanvasFill? = null

  // The paint object used for stroking shapes
  private var strokePaint: Paint? = null
  private var strokeModel: CanvasStroke? = null

  // The paint object used for compositing layers
  private var layerPaint: Paint? = null
  private var prevLayerModel: CanvasLayer? = null

  private val matrixPool = Pools.SimplePool<Matrix>(matrixPoolSize)

  // A Path may be transformed with a transform Matrix and there is no way to reset that transform
  // once it has been applied, that's why the cache key is the CanvasPathModel and an optional
  // CanvasTransformModel
  private val pathCache =
      LruCache<Pair<CanvasPathModel, CanvasTransformModel?>, Path>(pathCacheSize)

  internal fun getMatrix(transformModel: CanvasTransformModel, block: (Matrix) -> Unit) {
    val matrix = matrixPool.acquire() ?: Matrix()
    transformModel.applyTo(matrix)
    block(matrix)
    matrix.reset()
    matrixPool.release(matrix)
  }

  @SuppressLint("KotlinPreferElvis")
  internal fun getOrCreatePath(
      pathModel: CanvasPathModel,
      transformModel: CanvasTransformModel? = null
  ): Path {
    val fromCache = pathCache.get(Pair(pathModel, transformModel))
    return if (fromCache != null) {
      fromCache
    } else {
      val newPath = pathModel.toAndroidPath(this)
      pathCache.put(Pair(pathModel, transformModel), newPath)
      newPath
    }
  }

  internal fun configureFillPaint(newFillModel: CanvasFill): Paint =
      getOrCreateFillPaint()
          .apply {
            if (newFillModel.shadow != fillModel?.shadow) {
              val shadow = newFillModel.shadow
              if (shadow != null) {
                setShadowLayer(shadow.radius, shadow.dx, shadow.dy, shadow.color)
              } else {
                clearShadowLayer()
              }
            }

            if (newFillModel.blendingMode != fillModel?.blendingMode) {
              newFillModel.blendingMode.applyTo(this)
            }

            if (newFillModel.shading != fillModel?.shading) {
              when (val shading = newFillModel.shading) {
                is CanvasSolidColorShading -> {
                  if (shader != null) {
                    shader = null
                  }
                  if (color != shading.color) {
                    color = shading.color
                  }
                }
                is CanvasGradientShading -> {
                  shader = shading.gradient.toShader()
                }
              }
            }
          }
          .also { fillModel = newFillModel }

  internal fun configureStrokePaint(newStrokeModel: CanvasStroke): Paint =
      getOrCreateStrokePaint()
          .apply {
            if (newStrokeModel.shadow != strokeModel?.shadow) {
              val shadow = newStrokeModel.shadow
              if (shadow != null) {
                setShadowLayer(shadow.radius, shadow.dx, shadow.dy, shadow.color)
              } else {
                clearShadowLayer()
              }
            }

            if (newStrokeModel.blendingMode != strokeModel?.blendingMode) {
              newStrokeModel.blendingMode.applyTo(this)
            }

            if (newStrokeModel.shading != strokeModel?.shading) {
              when (val shading = newStrokeModel.shading) {
                is CanvasSolidColorShading -> {
                  if (shader != null) {
                    shader = null
                  }
                  if (color != shading.color) {
                    color = shading.color
                  }
                }
                is CanvasGradientShading -> {
                  shader = shading.gradient.toShader()
                }
              }
            }

            if (newStrokeModel.lineWidth != strokeModel?.lineWidth &&
                strokeWidth != newStrokeModel.lineWidth) {
              strokeWidth = newStrokeModel.lineWidth
            }

            if (newStrokeModel.miterLimit != strokeModel?.miterLimit &&
                strokeMiter != newStrokeModel.miterLimit) {
              strokeMiter = newStrokeModel.miterLimit
            }

            if (newStrokeModel.lineCap != strokeModel?.lineCap) {
              newStrokeModel.lineCap.applyTo(this)
            }

            if (newStrokeModel.lineJoin != strokeModel?.lineJoin) {
              newStrokeModel.lineJoin.applyTo(this)
            }

            if (newStrokeModel.dashLengths != null) {
              val dashLengths = newStrokeModel.dashLengths
              if (!dashLengths.contentEquals(strokeModel?.dashLengths) ||
                  newStrokeModel.dashPhase != strokeModel?.dashPhase) {
                pathEffect = DashPathEffect(dashLengths, newStrokeModel.dashPhase)
              }
            } else if (pathEffect != null) {
              pathEffect = null
            }
          }
          .also { strokeModel = newStrokeModel }

  internal fun configureLayerPaint(newLayerModel: CanvasLayer): Paint =
      getOrCreateLayerPaint()
          .apply {
            if (newLayerModel.alpha != prevLayerModel?.alpha) {
              val newAlpha = (newLayerModel.alpha * 255f).toInt()
              if (alpha != newAlpha) {
                alpha = newAlpha
              }
            }
            if (newLayerModel.blendingMode != prevLayerModel?.blendingMode) {
              newLayerModel.blendingMode.applyTo(this)
            }
          }
          .also { prevLayerModel = newLayerModel }

  private fun getOrCreateFillPaint(): Paint {
    return fillPaint ?: createPaint().apply { style = Paint.Style.FILL }.also { fillPaint = it }
  }

  private fun getOrCreateStrokePaint(): Paint {
    return strokePaint
        ?: createPaint().apply { style = Paint.Style.STROKE }.also { strokePaint = it }
  }

  private fun getOrCreateLayerPaint(): Paint {
    return layerPaint ?: createPaint().also { layerPaint = it }
  }

  private fun createPaint(): Paint {
    return Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG or Paint.FILTER_BITMAP_FLAG)
  }

  companion object {
    const val DEFAULT_MATRIX_POOL_SIZE = 5
    const val DEFAULT_PATH_CACHE_SIZE = 10
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas

import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
import com.facebook.mountable.canvas.model.CanvasModel

/** A View that executes drawing commands represented as CanvasModel. */
class CanvasView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
    View(context, attrs) {

  var canvasModel: CanvasModel? = null
    set(value) {
      if (field != value) {
        field = value
        invalidate()
      }
    }

  init {
    setWillNotDraw(false)
  }

  override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    canvasModel?.draw(canvas)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas

import android.annotation.SuppressLint
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint

/** Wrap the specified [block] in calls to [Canvas.saveLayer] and [Canvas.restoreToCount]. */
@Suppress("DEPRECATION")
@SuppressLint("DeprecatedMethod")
inline fun Canvas.withLayer(
    x: Float,
    y: Float,
    width: Float,
    height: Float,
    paint: Paint?,
    block: Canvas.() -> Unit
) {
  val checkpoint = saveLayer(x, y, x + width, y + height, paint, Canvas.ALL_SAVE_FLAG)
  try {
    block()
  } finally {
    restoreToCount(checkpoint)
  }
}

/** Copies 9 values from the matrix into the array and returns the array. */
inline var Matrix.values: FloatArray
  get() {
    val values = FloatArray(9)
    getValues(values)
    return values
  }
  set(value) {
    setValues(value)
  }


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas.model

import android.annotation.SuppressLint
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.os.Build
import androidx.annotation.FloatRange
import androidx.core.graphics.withClip
import androidx.core.graphics.withMatrix
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.mountable.canvas.CanvasState
import com.facebook.mountable.canvas.withLayer
import com.facebook.mountable.utils.types.BlendingMode
import com.facebook.mountable.utils.types.DEFAULT_BLENDING_MODE
import com.facebook.mountable.utils.types.LineCap
import com.facebook.mountable.utils.types.LineJoin
import com.facebook.mountable.utils.types.Size

sealed interface CanvasNodeModel {
  fun draw(canvas: Canvas, state: CanvasState)

  fun needsSoftwareLayer(): Boolean
}

/**
 * A definition for a canvas model which describes a tree of drawing commands.
 *
 * @property canvasState The state which is responsible for managing [Paint], [Matrix] and
 *   [android.graphics.Path] objects.
 * @property children The child nodes of the canvas
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasModel(
    private val canvasState: CanvasState,
    private val children: List<CanvasNodeModel>
) {

  /**
   * Draw the model onto the provided canvas.
   *
   * @param canvas a canvas which should be used for executing the drawing commands
   */
  fun draw(canvas: Canvas) {
    for (i in children.indices) {
      children[i].draw(canvas, canvasState)
    }
  }

  /**
   * Not all drawing commands supported by this API are hardware accelerated on all supported
   * versions of Android. For operations which are not hardware accelerated, a software layer needs
   * to be enabled.This method analyzes the tree and checks if a software layer needs to be enabled
   * or not.
   *
   * @return true if software layer needs to be enabled, false otherwise
   * @see <a
   *   href="https://developer.android.com/topic/performance/hardware-accel#drawing-support">Drawing
   *   Support</a>
   */
  fun needsSoftwareLayer(): Boolean {
    return checkIfSoftwareLayerNeeded(children = children)
  }
}

/**
 * A definition for a canvas model which describes a group of drawing commands. Groups can be used
 * to treat multiple drawing commands as a single one and applying transforms or clips to all of
 * them at the same time. For a group, first the transform is applied, then the content is drawn
 * inside the transformed coordinates and then the clip is applied to the drawn content.
 *
 * @property transform The transform that will be applied to the children of the group
 * @property size The size of the group
 * @property clip The path describing the shape of the clip that will be applied to the children of
 *   the group
 * @property clipToBounds The group will be clipped to its bounds if this is set to true and [clip]
 *   is null
 * @property children The child nodes of the group
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasGroup(
    private val transform: CanvasTransformModel,
    private val size: Size,
    private val clip: CanvasPathModel?,
    private val clipToBounds: Boolean,
    private val children: List<CanvasNodeModel>
) : CanvasNodeModel {
  override fun draw(canvas: Canvas, state: CanvasState) {
    state.getMatrix(transform) { matrix ->
      canvas.withMatrix(matrix) {
        if (clip != null) {
          val clipPath = state.getOrCreatePath(clip)
          canvas.withClip(clipPath) {
            for (i in children.indices) {
              children[i].draw(canvas, state)
            }
          }
        } else if (clipToBounds) {
          canvas.withClip(0f, 0f, size.width, size.height) {
            for (i in children.indices) {
              children[i].draw(canvas, state)
            }
          }
        } else {
          for (i in children.indices) {
            children[i].draw(canvas, state)
          }
        }
      }
    }
  }

  override fun needsSoftwareLayer(): Boolean {
    return checkIfSoftwareLayerNeeded(children = children)
  }
}

/**
 * A definition for a canvas model which describes a layer containing drawing commands. A Layer
 * allocates and redirects drawing to an offscreen buffer. It may be useful in cases like applying
 * transparency or blending modes to multiple drawing commands at once. Layers should be as small as
 * possible and should be used only when necessary because they may cause performance issues if used
 * incorrectly.
 *
 * @property transform The transform that will be applied to the children of the layer
 * @property size The size of the layer
 * @property clip The path describing the shape of the clip that will be applied to the children of
 *   the layer
 * @property alpha The alpha that will be applied to the layer when compositing it with the parent
 *   layer
 * @property blendingMode The blend mode that will be applied to the layer when compositing it with
 *   the parent layer
 * @property children The child nodes of the layer
 */
@Suppress("DEPRECATION")
@SuppressLint("NotInvokedPrivateMethod", "DeprecatedMethod")
@DataClassGenerate
data class CanvasLayer(
    val transform: CanvasTransformModel,
    val size: Size,
    val clip: CanvasPathModel?,
    @FloatRange(from = 0.0, to = 1.0) val alpha: Float,
    val blendingMode: BlendingMode,
    val children: List<CanvasNodeModel>
) : CanvasNodeModel {
  override fun draw(canvas: Canvas, state: CanvasState) {
    state.getMatrix(transform) { matrix ->
      val paint =
          if (alpha != DEFAULT_ALPHA || blendingMode != DEFAULT_BLENDING_MODE) {
            state.configureLayerPaint(this)
          } else {
            null
          }

      canvas.withMatrix(matrix) {
        canvas.withLayer(0f, 0f, size.width, size.height, paint) {
          if (clip != null) {
            val clipPath = state.getOrCreatePath(clip)
            canvas.withClip(clipPath) {
              for (i in children.indices) {
                children[i].draw(canvas, state)
              }
            }
          } else {
            for (i in children.indices) {
              children[i].draw(canvas, state)
            }
          }
        }
      }
    }
  }

  override fun needsSoftwareLayer(): Boolean {
    return checkIfSoftwareLayerNeeded(blendingMode = blendingMode, children = children)
  }

  companion object {
    const val DEFAULT_ALPHA = 1.0f
  }
}

/**
 * A definition for a canvas model which describes a fill drawing command. Paths drawn with this
 * command will be filled.
 *
 * @property shape The shape that will be drawn with fill style. A shape can be either a
 *   [CanvasPathModel] or [CanvasShapeModel]. Prefer using [CanvasPathModel] if possible because it
 *   offers better drawing performance.
 * @property shading The shading definition that describes how to fill the path. For example solid
 *   color or a gradient.
 * @property blendingMode The blend mode that will be used when filling the shape
 * @property shadow The shadow that will be drawn below the path
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasFill(
    val shape: CanvasShape,
    val shading: CanvasShadingModel,
    val blendingMode: BlendingMode,
    val shadow: CanvasShadowModel?
) : CanvasNodeModel {
  override fun draw(canvas: Canvas, state: CanvasState) {
    val fillPaint = state.configureFillPaint(this)
    when (shape) {
      is CanvasPathModel -> {
        val androidPath = state.getOrCreatePath(shape)
        canvas.drawPath(androidPath, fillPaint)
      }
      is CanvasShapeModel -> {
        shape.draw(canvas, fillPaint)
      }
    }
  }

  override fun needsSoftwareLayer(): Boolean {
    return checkIfSoftwareLayerNeeded(shadow = shadow, blendingMode = blendingMode)
  }
}

/**
 * A definition for a canvas model which describes a stroke drawing command. Paths drawn with this
 * command will be stroked.
 *
 * Note: Strokes are rendered in such a way where half of the stroke width is drawn inside of the
 * shape and half is drawn outside of the shape. Keep this in mind when drawing stroked shapes close
 * to the clip or screen bounds and make sure that the whole stroke width is rendered properly along
 * the entire shape.
 *
 * @property shape The shape that will be drawn with stroke style. A shape can be either a
 *   [CanvasPathModel] or [CanvasShapeModel]. Prefer using [CanvasPathModel] if possible because it
 *   offers better drawing performance.
 * @property shading The shading definition that describes how to stroke the path. For example solid
 *   color or a gradient.
 * @property blendingMode The blend mode that will be used when stroking the shape
 * @property shadow The shadow that will be drawn below the path
 * @property lineWidth The width for stroking. Pass 0 to stroke in hairline mode. Hairlines always
 *   draws a single pixel.
 * @property lineCap The line cap style
 * @property lineJoin The line join style
 * @property miterLimit The stroke miter value. This is used to control the behavior of miter joins
 *   when the joins angle is sharp. This value must be >= 0.
 * @property dashLengths The array of ON and OFF distances. It must contain an even number of
 *   entries (>=2), with the even indices specifying the "on" intervals, and the odd indices
 *   specifying the "off" intervals. This array controls the length of the dashes. The [lineWidth]
 *   controls the thickness of the dashes.
 * @property dashPhase The value which specifies how far into the dash pattern the line starts. For
 *   example, passing a value of 3 means the line is drawn with the dash pattern starting at three
 *   units from its beginning. Passing a value of 0 draws a line starting with the beginning of a
 *   dash pattern. Ignored if dash-lengths is null.
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasStroke(
    val shape: CanvasShape,
    val shading: CanvasShadingModel,
    val blendingMode: BlendingMode,
    val shadow: CanvasShadowModel?,
    val lineWidth: Float,
    val lineCap: LineCap,
    val lineJoin: LineJoin,
    val miterLimit: Float,
    val dashLengths: FloatArray?,
    val dashPhase: Float
) : CanvasNodeModel {
  override fun draw(canvas: Canvas, state: CanvasState) {
    val strokePaint = state.configureStrokePaint(this)
    when (shape) {
      is CanvasPathModel -> {
        val androidPath = state.getOrCreatePath(shape)
        canvas.drawPath(androidPath, strokePaint)
      }
      is CanvasShapeModel -> {
        shape.draw(canvas, strokePaint)
      }
    }
  }

  override fun needsSoftwareLayer(): Boolean {
    return checkIfSoftwareLayerNeeded(shadow = shadow, blendingMode = blendingMode)
  }

  // equals and hashCode has to be explicitly declared because of dashLength property which is of
  // FloatArray type and we want to compare it by its contents
  override fun equals(other: Any?): Boolean {
    if (this === other) {
      return true
    }
    if (javaClass != other?.javaClass) {
      return false
    }

    other as CanvasStroke

    if (shape != other.shape) {
      return false
    }
    if (shading != other.shading) {
      return false
    }
    if (blendingMode != other.blendingMode) {
      return false
    }
    if (shadow != other.shadow) {
      return false
    }
    if (lineWidth != other.lineWidth) {
      return false
    }
    if (lineCap != other.lineCap) {
      return false
    }
    if (lineJoin != other.lineJoin) {
      return false
    }
    if (miterLimit != other.miterLimit) {
      return false
    }
    if (dashLengths != null) {
      if (other.dashLengths == null) {
        return false
      }
      if (!dashLengths.contentEquals(other.dashLengths)) {
        return false
      }
    } else if (other.dashLengths != null) {
      return false
    }
    if (dashPhase != other.dashPhase) {
      return false
    }

    return true
  }

  override fun hashCode(): Int {
    var result = shape.hashCode()
    result = 31 * result + shading.hashCode()
    result = 31 * result + blendingMode.hashCode()
    result = 31 * result + (shadow?.hashCode() ?: 0)
    result = 31 * result + lineWidth.hashCode()
    result = 31 * result + lineCap.hashCode()
    result = 31 * result + lineJoin.hashCode()
    result = 31 * result + miterLimit.hashCode()
    result = 31 * result + (dashLengths?.contentHashCode() ?: 0)
    result = 31 * result + dashPhase.hashCode()
    return result
  }

  companion object {
    const val DEFAULT_LINE_WIDTH: Float = 0f
    const val DEFAULT_MITER_LIMIT: Float = 4f
  }
}

/**
 * A definition for a canvas model which describes a drawing command that provides access to draw
 * directly with the underlying [Canvas]. This is helpful for situations to re-use the existing
 * [Canvas] drawing logic.
 *
 * @property block The lambda callback to issue drawing commands on the provided [Canvas]
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasDrawIntoCanvas(private val block: (Canvas) -> Unit) : CanvasNodeModel {
  override fun draw(canvas: Canvas, state: CanvasState) {
    block(canvas)
  }

  override fun needsSoftwareLayer(): Boolean {
    return false
  }
}

/**
 * According to: https://developer.android.com/topic/performance/hardware-accel#drawing-support
 * shadowLayer, DARKEN, LIGHTEN and OVERLAY blend modes are not hardware accelerated prior to
 * Android P.
 *
 * This table mentions also dash effect, but it seems to work fine without software layer. It
 * probably silently fallbacks to software rendering under the hoods but since it works fine without
 * software layer, we're not checking it here.
 */
@Suppress("NOTHING_TO_INLINE")
private inline fun checkIfSoftwareLayerNeeded(
    shadow: CanvasShadowModel? = null,
    blendingMode: BlendingMode? = null,
    children: List<CanvasNodeModel> = listOf()
): Boolean {
  return (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) &&
      (blendingMode == BlendingMode.Darken ||
          blendingMode == BlendingMode.Lighten ||
          blendingMode == BlendingMode.Overlay ||
          shadow != null ||
          children.any { child -> child.needsSoftwareLayer() })
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas.model

import androidx.annotation.ColorInt
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate

/**
 * A definition for a shadow which should be drawn under a shape on a Canvas.
 *
 * @property dx horizontal offset
 * @property dy vertical offset
 * @property radius blur radius
 * @property color color of the shadow
 */
@DataClassGenerate
data class CanvasShadowModel(
    val dx: Float,
    val dy: Float,
    val radius: Float,
    @ColorInt val color: Int
)


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas.model

import android.annotation.SuppressLint
import android.graphics.Path
import android.graphics.RectF
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.mountable.canvas.CanvasState
import com.facebook.mountable.utils.types.FillRule
import com.facebook.mountable.utils.types.Point
import com.facebook.mountable.utils.types.Size

sealed interface CanvasPathModel : CanvasShape {
  fun toAndroidPath(state: CanvasState): Path
}

sealed interface CanvasPathChildModel {
  fun applyTo(androidPath: Path, state: CanvasState)
}

/**
 * A definition for a path which contains a mathematical description of shapes or lines to be drawn
 * on a Canvas.
 *
 * @property fillRule The path's fill type. Defines how "inside" is computed.
 * @property children child path definitions that will be added or applied on the path in the same
 *   order as they're specified
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPath(
    private val fillRule: FillRule,
    private val children: List<CanvasPathChildModel>
) : CanvasPathModel {
  override fun toAndroidPath(state: CanvasState): Path {
    return Path().apply {
      fillRule.applyTo(this)
      for (i in children.indices) {
        children[i].applyTo(this, state)
      }
    }
  }
}

/**
 * A definition for a child path which sets the beginning of the next contour to the point (x,y).
 *
 * @property point The start point of a new contour
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathMoveTo(private val point: Point) : CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    androidPath.moveTo(point.x, point.y)
  }
}

/**
 * A definition for a child path which adds a line from the last point to the specified point (x,y).
 * If no [CanvasPathMoveTo] children has been added to this Path, then the first point is
 * automatically set to (0,0).
 *
 * @property point The end point of a line
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathLineTo(private val point: Point) : CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    androidPath.lineTo(point.x, point.y)
  }
}

/**
 * A definition for a child path which adds a quadratic bezier from the last point, approaching
 * control point (controlPoint.x,controlPoint.y), and ending at (endPoint.x,endPoint.y). If no
 * [CanvasPathMoveTo] children has been added to this Path, then the first point is automatically
 * set to (0,0).
 *
 * @property controlPoint The control point on a quadratic curve
 * @property endPoint The end point on a quadratic curve
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathQuadTo(private val controlPoint: Point, private val endPoint: Point) :
    CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    androidPath.quadTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y)
  }
}

/**
 * A definition for a child path which adds a cubic bezier from the last point, approaching control
 * points (controlPoint1.x,controlPoint1.y) and (controlPoint2.x,controlPoint2.y), and ending at
 * (endPoint.x,endPoint.y). If no [CanvasPathMoveTo] children has been added to this Path, then the
 * first point is automatically set to (0,0).
 *
 * @property controlPoint1 The 1st control point on a cubic curve
 * @property controlPoint2 The 2nd control point on a cubic curve
 * @property endPoint The end point on a cubic curve
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathCubicTo(
    private val controlPoint1: Point,
    private val controlPoint2: Point,
    private val endPoint: Point
) : CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    androidPath.cubicTo(
        controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, endPoint.x, endPoint.y)
  }
}

/**
 * A definition for a child path which adds the specified arc to the path as a new contour.
 *
 * @property center The center of the arc
 * @property radius The radius of the arc
 * @property startDegrees The angle to the starting point of the arc
 * @property endDegrees The angle to the end point of the arc
 * @property clockwise true to make a clockwise arc; false to make a counterclockwise arc
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathArc(
    private val center: Point,
    private val radius: Float,
    private val startDegrees: Float,
    private val endDegrees: Float,
    private val clockwise: Boolean,
) : CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    val startAngle = if (clockwise) startDegrees else -startDegrees
    val endAngle = if (clockwise) endDegrees - startDegrees else startDegrees - endDegrees
    androidPath.addArc(
        RectF(center.x - radius, center.y - radius, center.x + radius, center.y + radius),
        startAngle,
        endAngle)
  }
}

/**
 * A definition for a child path which adds a closed (optionally) round-rectangle contour.
 *
 * @property topLeft The coordinates of the top left point of the rectangle
 * @property size The size of the rectangle
 * @property cornerRadius The radius of the rounded corners
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathRect(
    private val topLeft: Point,
    private val size: Size,
    private val cornerRadius: Float
) : CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    androidPath.addRoundRect(
        RectF(topLeft.x, topLeft.y, topLeft.x + size.width, topLeft.y + size.height),
        cornerRadius,
        cornerRadius,
        Path.Direction.CW)
  }
}

/**
 * A definition for a child path which adds closed ellipse contour to the contour.
 *
 * @property topLeft The coordinates of the top left point of the ellipse
 * @property size The size of the ellipse
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathEllipse(private val topLeft: Point, private val size: Size) :
    CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    androidPath.addOval(
        RectF(topLeft.x, topLeft.y, topLeft.x + size.width, topLeft.y + size.height),
        Path.Direction.CW)
  }
}

/**
 * A definition for a child path which adds a closed circle contour to the contour.
 *
 * @property center The center of the circle
 * @property radius The radius of the circle
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathCircle(private val center: Point, private val radius: Float) :
    CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    androidPath.addCircle(center.x, center.y, radius, Path.Direction.CW)
  }
}

/**
 * A definition for a child path which closes the current contour. If the current point is not equal
 * to the first point of the contour, a line segment is automatically added.
 */
object CanvasPathClose : CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    androidPath.close()
  }
}

/**
 * A definition for a child path which adds a copy of src path to the current path, transformed by
 * transformModel.
 *
 * @property src The path to add as a new contour to the current path
 * @property transformModel The transform that will be applied to the src before adding it to the
 *   current path
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasPathAdd(
    private val src: CanvasPathModel,
    private val transformModel: CanvasTransformModel
) : CanvasPathChildModel {
  override fun applyTo(androidPath: Path, state: CanvasState) {
    val srcPath =
        state.getOrCreatePath(src, transformModel).apply {
          state.getMatrix(transformModel) { matrix -> transform(matrix) }
        }
    androidPath.addPath(srcPath)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas.model

import android.graphics.LinearGradient
import android.graphics.RadialGradient
import android.graphics.Shader
import androidx.annotation.ColorInt
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.mountable.utils.types.Point

sealed interface CanvasShadingModel

/**
 * A definition for a solid color shading.
 *
 * @property color The color (including alpha)
 */
@DataClassGenerate
data class CanvasSolidColorShading(@ColorInt val color: Int) : CanvasShadingModel

/**
 * A definition for a gradient shading.
 *
 * @property gradient The gradient definition
 */
@DataClassGenerate
data class CanvasGradientShading(val gradient: CanvasGradientModel) : CanvasShadingModel

sealed interface CanvasGradientModel {
  fun toShader(): Shader
}

/**
 * A definition for a linear gradient that varies along the line defined by the provided starting
 * and ending points.
 *
 * @property gradient The colors and positions of the gradient
 * @property startPoint The start of the gradient line
 * @property endPoint The end of the gradient line
 * @property tileMode The tiling mode
 */
@DataClassGenerate
data class CanvasLinearGradient(
    val gradient: CanvasGradient,
    val startPoint: Point,
    val endPoint: Point,
    val tileMode: Shader.TileMode
) : CanvasGradientModel {
  override fun toShader(): Shader {
    return LinearGradient(
        startPoint.x,
        startPoint.y,
        endPoint.x,
        endPoint.y,
        gradient.colors,
        gradient.positions,
        tileMode)
  }
}

/**
 * A definition for a radial gradient given the center and radius.
 *
 * @property gradient The colors and positions of the gradient
 * @property center The center of the radius
 * @property radius Must be positive. The radius of the circle for this gradient
 * @property tileMode The tiling mode
 */
@DataClassGenerate
data class CanvasRadialGradient(
    val gradient: CanvasGradient,
    val center: Point,
    val radius: Float,
    val tileMode: Shader.TileMode
) : CanvasGradientModel {
  override fun toShader(): Shader {
    return RadialGradient(center.x, center.y, radius, gradient.colors, gradient.positions, tileMode)
  }
}

/**
 * A definition for a smooth transition between colors for drawing gradients.
 *
 * @property colors The sRGB colors (including alpha) to be distributed along the gradient
 * @property positions The relative positions [0..1] of each corresponding color in the colors
 *   array. If this is null, the the colors are distributed evenly along the gradient
 */
@DataClassGenerate
data class CanvasGradient(@ColorInt val colors: IntArray, val positions: FloatArray?) {
  override fun equals(other: Any?): Boolean {
    if (this === other) {
      return true
    }
    if (javaClass != other?.javaClass) {
      return false
    }

    other as CanvasGradient

    if (!colors.contentEquals(other.colors)) {
      return false
    }
    if (positions != null) {
      if (other.positions == null) {
        return false
      }
      if (!positions.contentEquals(other.positions)) {
        return false
      }
    } else if (other.positions != null) {
      return false
    }

    return true
  }

  override fun hashCode(): Int {
    var result = colors.contentHashCode()
    result = 31 * result + (positions?.contentHashCode() ?: 0)
    return result
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas.model

import android.annotation.SuppressLint
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.mountable.utils.types.Point
import com.facebook.mountable.utils.types.Size

sealed interface CanvasShape

sealed interface CanvasShapeModel : CanvasShape {
  fun draw(canvas: Canvas, paint: Paint)
}

/**
 * A line segment with the specified start and stop x,y coordinates that can be drawn on a Canvas.
 *
 * @param startPoint The start point of the line
 * @param endPoint The end point of the line
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasShapeLine(private val startPoint: Point, private val endPoint: Point) :
    CanvasShapeModel {
  override fun draw(canvas: Canvas, paint: Paint) {
    canvas.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y, paint)
  }
}

/**
 * A closed (optionally) round-rectangle that can be drawn on a Canvas.
 *
 * @property topLeft The coordinates of the top left point of the rectangle
 * @property size The size of the rectangle
 * @property cornerRadius The radius of the rounded corners
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasShapeRect(
    private val topLeft: Point,
    private val size: Size,
    private val cornerRadius: Float
) : CanvasShapeModel {
  override fun draw(canvas: Canvas, paint: Paint) {
    canvas.drawRoundRect(
        RectF(topLeft.x, topLeft.y, topLeft.x + size.width, topLeft.y + size.height),
        cornerRadius,
        cornerRadius,
        paint)
  }
}

/**
 * A closed circle that can be drawn on a Canvas.
 *
 * @property center The center of the circle
 * @property radius The radius of the circle
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasShapeCircle(private val center: Point, private val radius: Float) :
    CanvasShapeModel {
  override fun draw(canvas: Canvas, paint: Paint) {
    canvas.drawCircle(center.x, center.y, radius, paint)
  }
}

/**
 * An arc shape that can be drawn on a Canvas.
 *
 * @property center The center of the arc
 * @property radius The radius of the arc
 * @property startDegrees The angle to the starting point of the arc
 * @property endDegrees The angle to the end point of the arc
 * @property clockwise true to make a clockwise arc; false to make a counterclockwise arc
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasShapeArc(
    private val center: Point,
    private val radius: Float,
    private val startDegrees: Float,
    private val endDegrees: Float,
    private val clockwise: Boolean,
) : CanvasShapeModel {
  override fun draw(canvas: Canvas, paint: Paint) {
    val startAngle = if (clockwise) startDegrees else -startDegrees
    val endAngle = if (clockwise) endDegrees - startDegrees else startDegrees - endDegrees
    canvas.drawArc(
        RectF(center.x - radius, center.y - radius, center.x + radius, center.y + radius),
        startAngle,
        endAngle,
        false,
        paint)
  }
}

/**
 * A closed ellipse that can be drawn on a Canvas.
 *
 * @property topLeft The coordinates of the top left point of the ellipse
 * @property size The size of the ellipse
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasShapeEllipse(private val topLeft: Point, private val size: Size) :
    CanvasShapeModel {
  override fun draw(canvas: Canvas, paint: Paint) {
    canvas.drawOval(
        RectF(topLeft.x, topLeft.y, topLeft.x + size.width, topLeft.y + size.height), paint)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.canvas.model

import android.annotation.SuppressLint
import android.graphics.Matrix
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.mountable.canvas.values
import com.facebook.mountable.utils.types.Point

sealed interface CanvasTransformModel {
  fun applyTo(matrix: Matrix)
}

sealed interface CanvasTransformChildModel {
  fun applyTo(matrix: Matrix)
}

/**
 * A definition for a transform which holds a 3x3 matrix for transforming coordinates.
 *
 * @property a The horizontal scaling. A value of 1 results in no scaling
 * @property b The vertical skewing
 * @property c The horizontal skewing
 * @property d The vertical scaling. A value of 1 results in no scaling
 * @property tx The horizontal translation (moving)
 * @property ty The vertical translation (moving)
 * @property children The child transforms which will be post applied in the same order as they're
 *   specified
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasTransform(
    private val a: Float,
    private val b: Float,
    private val c: Float,
    private val d: Float,
    private val tx: Float,
    private val ty: Float,
    private val children: List<CanvasTransformChildModel>
) : CanvasTransformModel {
  override fun applyTo(matrix: Matrix) {
    val matrixValues = matrix.values
    matrixValues[Matrix.MSCALE_X] = a
    matrixValues[Matrix.MSKEW_X] = c
    matrixValues[Matrix.MTRANS_X] = tx
    matrixValues[Matrix.MSKEW_Y] = b
    matrixValues[Matrix.MSCALE_Y] = d
    matrixValues[Matrix.MTRANS_Y] = ty
    matrix.values = matrixValues

    for (i in children.indices) {
      children[i].applyTo(matrix)
    }
  }

  companion object {
    val IDENTITY: CanvasTransform =
        CanvasTransform(a = 1f, b = 0f, c = 0f, d = 1f, tx = 0f, ty = 0f, children = listOf())
  }
}

/**
 * A definition for a child transform which inverts the specified transform.
 *
 * @property transform The transform that should be inverted
 * @throws IllegalArgumentException If the specified transform can't be inverted
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasInverseTransform(private val transform: CanvasTransformModel) :
    CanvasTransformChildModel {
  override fun applyTo(matrix: Matrix) {
    transform.applyTo(matrix)
    if (!matrix.invert(matrix)) {
      throw IllegalArgumentException("Can't invert matrix: ${matrix.toShortString()} ")
    }
  }
}

/**
 * A definition for a child transform which postconcats the current transform with the specified
 * translation. M' = T(dx, dy) * M
 *
 * @property dx The horizontal translation
 * @property dy The vertical translation
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasTranslate(private val dx: Float, private val dy: Float) :
    CanvasTransformChildModel {
  override fun applyTo(matrix: Matrix) {
    matrix.postTranslate(dx, dy)
  }
}

/**
 * A definition for a child transform which postconcats the current transform with the specified
 * scale. M' = S(sx, sy, pivot.x, pivot.y) * M
 *
 * @property sx The horizontal scale
 * @property sy The vertical scale
 * @property pivot The point around which scale will be applied
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasScale(
    private val sx: Float,
    private val sy: Float,
    private val pivot: Point,
) : CanvasTransformChildModel {
  override fun applyTo(matrix: Matrix) {
    matrix.postScale(sx, sy, pivot.x, pivot.y)
  }
}

/**
 * A definition for a child transform which postconcats the current transform with the specified
 * rotation. M' = R(degrees, pivot.x, pivot.y) * M
 *
 * @property degrees The rotation degrees
 * @property pivot The point around which scale will be applied
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasRotate(
    private val degrees: Float,
    private val pivot: Point,
) : CanvasTransformChildModel {
  override fun applyTo(matrix: Matrix) {
    matrix.postRotate(degrees, pivot.x, pivot.y)
  }
}

/**
 * A definition for a child transform which postconcats the current transform with the specified
 * skewing. M' = K(kx, ky, pivot.x, pivot.y) * M
 *
 * @property kx The horizontal skewing
 * @property ky The vertical skewing
 * @property pivot The point around which scale will be applied
 */
@SuppressLint("NotInvokedPrivateMethod")
@DataClassGenerate
data class CanvasSkew(
    private val kx: Float,
    private val ky: Float,
    private val pivot: Point,
) : CanvasTransformChildModel {
  override fun applyTo(matrix: Matrix) {
    matrix.postSkew(kx, ky, pivot.x, pivot.y)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.utils.types

import android.annotation.SuppressLint
import android.graphics.BlendMode
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.os.Build
import androidx.annotation.RequiresApi
import com.facebook.proguard.annotations.DoNotStrip

/** This class contains the list of alpha compositing and blending modes. */
@SuppressLint("NotAccessedPrivateField")
@JvmInline
value class BlendingMode internal constructor(@DoNotStrip private val value: Int) {

  companion object {
    /**
     * Destination pixels covered by the source are cleared to 0.
     *
     * ```
     * alpha_out = 0
     *
     * C_out = 0
     * ```
     */
    val Clear: BlendingMode = BlendingMode(0)

    /**
     * The source pixels replace the destination pixels.
     *
     * ```
     * alpha_out = alpha_src
     *
     * C_out = C_src
     * ```
     */
    val Src: BlendingMode = BlendingMode(1)

    /**
     * The source pixels are drawn over the destination pixels.
     *
     * ```
     * alpha_out = alpha_src + (1 - alpha_src) * alpha_dst
     *
     * C_out = C_src + (1 - alpha_src) * C_dst
     * ```
     */
    val SrcOver: BlendingMode = BlendingMode(3)

    /**
     * The source pixels are drawn behind the destination pixels.
     *
     * ```
     * alpha_out = alpha_dst + (1 - alpha_dst) * alpha_src
     *
     * C_out = C_dst + (1 - alpha_dst) * C_src
     * ```
     */
    val DstOver: BlendingMode = BlendingMode(4)

    /**
     * Keeps the source pixels that cover the destination pixels, discards the remaining source and
     * destination pixels.
     *
     * ```
     * alpha_out = alpha_src * alpha_dst
     *
     * C_out = C_src * alpha_dst
     * ```
     */
    val SrcIn: BlendingMode = BlendingMode(5)

    /**
     * Keeps the destination pixels that cover source pixels, discards the remaining source and
     * destination pixels.
     *
     * ```
     * alpha_out = alpha_src * alpha_dst
     *
     * C_out = C_dst * alpha_src
     * ```
     */
    val DstIn: BlendingMode = BlendingMode(6)

    /**
     * Keeps the source pixels that do not cover destination pixels. Discards source pixels that
     * cover destination pixels. Discards all destination pixels.
     *
     * ```
     * alpha_out = (1 - alpha_dst) * alpha_src
     *
     * C_out = (1 - alpha_dst) * C_src
     * ```
     */
    val SrcOut: BlendingMode = BlendingMode(7)

    /**
     * Keeps the destination pixels that are not covered by source pixels. Discards destination
     * pixels that are covered by source pixels. Discards all source pixels.
     *
     * ```
     * alpha_out = (1 - alpha_src) * alpha_dst
     *
     * C_out = (1 - alpha_src) * C_dst
     * ```
     */
    val DstOut: BlendingMode = BlendingMode(8)

    /**
     * Discards the source pixels that do not cover destination pixels. Draws remaining source
     * pixels over destination pixels.
     *
     * ```
     * alpha_out = alpha_dst
     *
     * C_out = alpha_dst * C_src + (1 - alpha_src) * C_dst
     * ```
     */
    val SrcAtop: BlendingMode = BlendingMode(9)

    /**
     * Discards the destination pixels that are not covered by source pixels. Draws remaining
     * destination pixels over source pixels.
     *
     * ```
     * alpha_out = alpha_src
     *
     * C_out = alpha_src * C_dst + (1 - alpha_dst) * C_src
     * ```
     */
    val DstAtop: BlendingMode = BlendingMode(10)

    /**
     * Discards the source and destination pixels where source pixels cover destination pixels.
     * Draws remaining source pixels.
     *
     * ```
     * alpha_out = (1 - alpha_dst) * alpha_src + (1 - alpha_src) * alpha_dst
     *
     * C_out = (1 - alpha_dst) * C_src + (1 - alpha_src) * C_dst
     * ```
     */
    val Xor: BlendingMode = BlendingMode(11)

    /**
     * Retains the smallest component of the source and destination pixels.
     *
     * ```
     * alpha_out = alpha_src + alpha_dst - alpha_src * alpha_dst
     *
     * C_out = (1 - alpha_dst) * C_src + (1 - alpha_src) * C_dst + min(C_src, C_dst)
     * ```
     */
    val Darken: BlendingMode = BlendingMode(16)

    /**
     * Retains the largest component of the source and destination pixel.
     *
     * ```
     * alpha_out = alpha_src + alpha_dst - alpha_src * alpha_dst
     *
     * C_out = (1 - alpha_dst) * C_src + (1 - alpha_src) * C_dst + max(C_src, C_dst)
     * ```
     */
    val Lighten: BlendingMode = BlendingMode(17)

    /**
     * Multiplies the source and destination pixels.
     *
     * ```
     * alpha_out = alpha_src + alpha_dst - alpha_src * alpha_dst
     *
     * C_out = C_src * (1 - alpha_dst) + C_dst * (1 - alpha_src) + (C_src * C_dst)
     * ```
     */
    val Multiply: BlendingMode = BlendingMode(13)

    /**
     * Adds the source and destination pixels, then subtracts the source pixels multiplied by the
     * destination.
     *
     * ```
     * alpha_out = alpha_src + alpha_dst - alpha_src * alpha_dst`
     *
     * C_out = C_src + C_dst - C_src * C_dst
     * ```
     */
    val Screen: BlendingMode = BlendingMode(14)

    /**
     * Multiplies or screens the source and destination depending on the destination color.
     *
     * ```
     * alpha_out = alpha_src + alpha_dst - alpha_src * alpha_dst
     *
     * C_out = if(2 * C_dst < alpha_dst) {
     *   2 * C_src * C_dst
     * } else {
     *   alpha_src * alpha_dst - 2 (alpha_dst - C_src) * (alpha_src - C_dst)
     * }
     * ```
     */
    val Overlay: BlendingMode = BlendingMode(15)

    /** Returns [BlendingMode] that matches the given [name]. */
    fun fromString(name: String?): BlendingMode {
      return when (name?.lowercase()) {
        "clear" -> Clear
        "src" -> Src
        "srcOver" -> SrcOver
        "dstOver" -> DstOver
        "srcIn" -> SrcIn
        "dstIn" -> DstIn
        "srcOut" -> SrcOut
        "dstOut" -> DstOut
        "srcAtop" -> SrcAtop
        "dstAtop" -> DstAtop
        "xor" -> Xor
        "screen" -> Screen
        "overlay" -> Overlay
        "darken" -> Darken
        "lighten" -> Lighten
        "multiply" -> Multiply
        else -> DEFAULT_BLENDING_MODE
      }
    }
  }

  override fun toString(): String =
      when (this) {
        Clear -> "Clear"
        Src -> "Src"
        SrcOver -> "SrcOver"
        DstOver -> "DstOver"
        SrcIn -> "SrcIn"
        DstIn -> "DstIn"
        SrcOut -> "SrcOut"
        DstOut -> "DstOut"
        SrcAtop -> "SrcAtop"
        DstAtop -> "DstAtop"
        Xor -> "Xor"
        Screen -> "Screen"
        Overlay -> "Overlay"
        Darken -> "Darken"
        Lighten -> "Lighten"
        Multiply -> "Multiply"
        else -> "Unknown" // Should not get here since we have an internal constructor
      }

  fun applyTo(paint: Paint) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
      val blendMode = this.toBlendMode()
      if (paint.blendMode != blendMode) {
        paint.blendMode = blendMode
      }
    } else {
      val xfermode = PorterDuffXfermode(toPorterDuff())
      if (paint.xfermode != xfermode) {
        paint.xfermode = xfermode
      }
    }
  }

  private fun toPorterDuff(): PorterDuff.Mode {
    return when (this) {
      Clear -> PorterDuff.Mode.CLEAR
      Src -> PorterDuff.Mode.SRC
      SrcOver -> PorterDuff.Mode.SRC_OVER
      DstOver -> PorterDuff.Mode.DST_OVER
      SrcIn -> PorterDuff.Mode.SRC_IN
      DstIn -> PorterDuff.Mode.DST_IN
      SrcOut -> PorterDuff.Mode.SRC_OUT
      DstOut -> PorterDuff.Mode.DST_OUT
      SrcAtop -> PorterDuff.Mode.SRC_ATOP
      DstAtop -> PorterDuff.Mode.DST_ATOP
      Xor -> PorterDuff.Mode.XOR
      Darken -> PorterDuff.Mode.DARKEN
      Lighten -> PorterDuff.Mode.LIGHTEN
      Multiply -> PorterDuff.Mode.MULTIPLY
      Screen -> PorterDuff.Mode.SCREEN
      Overlay -> PorterDuff.Mode.OVERLAY
      else -> PorterDuff.Mode.SRC_OVER
    }
  }

  @RequiresApi(Build.VERSION_CODES.Q)
  private fun toBlendMode(): BlendMode {
    return when (this) {
      Clear -> BlendMode.CLEAR
      Src -> BlendMode.SRC
      SrcOver -> BlendMode.SRC_OVER
      DstOver -> BlendMode.DST_OVER
      SrcIn -> BlendMode.SRC_IN
      DstIn -> BlendMode.DST_IN
      SrcOut -> BlendMode.SRC_OUT
      DstOut -> BlendMode.DST_OUT
      SrcAtop -> BlendMode.SRC_ATOP
      DstAtop -> BlendMode.DST_ATOP
      Xor -> BlendMode.XOR
      Darken -> BlendMode.DARKEN
      Lighten -> BlendMode.LIGHTEN
      Multiply -> BlendMode.MULTIPLY
      Screen -> BlendMode.SCREEN
      Overlay -> BlendMode.OVERLAY
      else -> BlendMode.SRC_OVER
    }
  }
}

val DEFAULT_BLENDING_MODE: BlendingMode = BlendingMode.SrcOver


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.utils.types

import android.annotation.SuppressLint
import android.graphics.Path
import android.view.View
import com.facebook.proguard.annotations.DoNotStrip

/**
 * This class contains the list of possible layer types for the underlying Canvas View.
 *
 * See [Path.FillType].
 */
@SuppressLint("NotAccessedPrivateField")
@JvmInline
value class CanvasLayerType internal constructor(@DoNotStrip private val value: Int) {
  companion object {
    /**
     * The canvas view does not have a layer.
     *
     * @see android.view.View.LAYER_TYPE_NONE
     */
    val None: CanvasLayerType = CanvasLayerType(0)

    /**
     * The canvas view has a software layer.
     *
     * @see android.view.View.LAYER_TYPE_SOFTWARE
     */
    val Software: CanvasLayerType = CanvasLayerType(1)

    /**
     * The canvas view has a hardware layer.
     *
     * @see android.view.View.LAYER_TYPE_HARDWARE
     */
    val Hardware: CanvasLayerType = CanvasLayerType(2)

    /**
     * Some of the drawing operations require software layer on older versions of Android. This mode
     * will automatically detect that and use [SOFTWARE] type if needed. Otherwise [NONE] type will
     * be used.
     *
     * @see <a
     *   href="https://developer.android.com/topic/performance/hardware-accel#drawing-support">Drawing
     *   Support</a>
     */
    val Auto: CanvasLayerType = CanvasLayerType(-1)
  }

  override fun toString(): String =
      when (this) {
        None -> "None"
        Software -> "Software"
        Hardware -> "Hardware"
        Auto -> "Auto"
        else -> "Unknown"
      }

  fun toLayerType(needsSoftwareLayer: Boolean): Int {
    return when (this) {
      Software -> View.LAYER_TYPE_SOFTWARE
      Hardware -> View.LAYER_TYPE_HARDWARE
      Auto -> if (needsSoftwareLayer) View.LAYER_TYPE_SOFTWARE else View.LAYER_TYPE_NONE
      else -> View.LAYER_TYPE_NONE
    }
  }
}

val DEFAULT_CANVAS_LAYER_TYPE: CanvasLayerType = CanvasLayerType.Auto


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.utils.types

import android.annotation.SuppressLint
import android.graphics.Path
import com.facebook.proguard.annotations.DoNotStrip

/**
 * This class contains the list of the ways a path may be filled.
 *
 * See [Path.FillType].
 */
@SuppressLint("NotAccessedPrivateField")
@JvmInline
value class FillRule internal constructor(@DoNotStrip private val value: Int) {
  companion object {
    /**
     * Also known as WINDING. Specifies that "inside" is computed by a non-zero sum of signed edge
     * crossings.
     */
    val NonZero: FillRule = FillRule(0)

    /** Specifies that "inside" is computed by an odd number of edge crossings. */
    val EvenOdd: FillRule = FillRule(1)

    /** Returns [FillRule] that matches the given [name]. */
    fun fromString(name: String?): FillRule {
      return when (name?.lowercase()) {
        "nonzero" -> NonZero
        "evenodd" -> EvenOdd
        else -> DEFAULT_FILL_RULE
      }
    }
  }

  override fun toString(): String =
      when (this) {
        NonZero -> "NonZero"
        EvenOdd -> "EvenOdd"
        else -> "Unknown"
      }

  fun applyTo(path: Path) {
    val fillType = this.toFillType()
    if (path.fillType != fillType) {
      path.fillType = fillType
    }
  }

  private fun toFillType(): Path.FillType =
      when (this) {
        NonZero -> Path.FillType.WINDING
        EvenOdd -> Path.FillType.EVEN_ODD
        else -> Path.FillType.WINDING
      }
}

val DEFAULT_FILL_RULE: FillRule = FillRule.NonZero


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.utils.types

import android.annotation.SuppressLint
import android.graphics.Paint
import com.facebook.proguard.annotations.DoNotStrip

/**
 * Styles to use for line endings.
 *
 * See [Paint.strokeCap].
 */
@SuppressLint("NotAccessedPrivateField")
@JvmInline
value class LineCap internal constructor(@DoNotStrip private val value: Int) {
  companion object {
    /** The stroke ends with the path, and does not project beyond it. */
    val Butt: LineCap = LineCap(0)

    /** The stroke projects out as a semicircle, with the center at the end of the path. */
    val Round: LineCap = LineCap(1)

    /** The stroke projects out as a square, with the center at the end of the path. */
    val Square: LineCap = LineCap(2)

    /** Returns [LineCap] that matches the given [name]. */
    fun fromString(name: String?): LineCap {
      return when (name?.lowercase()) {
        "butt" -> Butt
        "round" -> Round
        "square" -> Square
        else -> DEFAULT_LINE_CAP
      }
    }
  }

  override fun toString(): String =
      when (this) {
        Butt -> "Butt"
        Round -> "Round"
        Square -> "Square"
        else -> "Unknown"
      }

  fun applyTo(paint: Paint) {
    val paintCap = this.toPaintCap()
    if (paint.strokeCap != paintCap) {
      paint.strokeCap = paintCap
    }
  }

  private fun toPaintCap(): Paint.Cap =
      when (this) {
        Butt -> Paint.Cap.BUTT
        Round -> Paint.Cap.ROUND
        Square -> Paint.Cap.SQUARE
        else -> Paint.Cap.BUTT
      }
}

val DEFAULT_LINE_CAP: LineCap = LineCap.Butt


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.utils.types

import android.annotation.SuppressLint
import android.graphics.Paint
import com.facebook.proguard.annotations.DoNotStrip

/**
 * Styles to use for line endings.
 *
 * See [Paint.strokeCap].
 */
@SuppressLint("NotAccessedPrivateField")
@JvmInline
value class LineJoin internal constructor(@DoNotStrip private val value: Int) {
  companion object {
    /** The outer edges of a join meet at a sharp angle */
    val Miter: LineJoin = LineJoin(0)

    /** The outer edges of a join meet in a circular arc. */
    val Round: LineJoin = LineJoin(1)

    /** The outer edges of a join meet with a straight line. */
    val Bevel: LineJoin = LineJoin(2)

    /** Returns [LineJoin] that matches the given [name]. */
    fun fromString(name: String?): LineJoin {
      return when (name?.lowercase()) {
        "miter" -> Miter
        "round" -> Round
        "bevel" -> Bevel
        else -> DEFAULT_LINE_JOIN
      }
    }
  }

  override fun toString(): String =
      when (this) {
        Miter -> "Miter"
        Round -> "Round"
        Bevel -> "Bevel"
        else -> "Unknown"
      }

  fun applyTo(paint: Paint) {
    val paintJoin = this.toPaintJoin()
    if (paint.strokeJoin != paintJoin) {
      paint.strokeJoin = paintJoin
    }
  }

  private fun toPaintJoin(): Paint.Join =
      when (this) {
        Miter -> Paint.Join.MITER
        Round -> Paint.Join.ROUND
        Bevel -> Paint.Join.BEVEL
        else -> Paint.Join.MITER
      }
}

val DEFAULT_LINE_JOIN: LineJoin = LineJoin.Miter


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.utils.types

import android.annotation.SuppressLint

@SuppressLint("HexColorValueUsage")
@JvmInline
value class Point internal constructor(@PublishedApi internal val packedValue: Long) {
  val x: Float
    get() {
      return Float.fromBits(packedValue.shr(32).toInt())
    }

  val y: Float
    get() {
      return Float.fromBits(packedValue.and(0xFFFFFFFF).toInt())
    }

  operator fun minus(other: Point): Point = Point(x - other.x, y - other.y)

  operator fun plus(other: Point): Point = Point(x + other.x, y + other.y)

  operator fun times(operand: Float): Point = Point(x * operand, y * operand)

  operator fun div(operand: Float): Point = Point(x / operand, y / operand)

  companion object {
    val Zero: Point = Point(0.0f, 0.0f)
  }
}

@SuppressLint("HexColorValueUsage")
fun Point(x: Float, y: Float): Point {
  // pack two Float values into one Long value
  val v1 = x.toBits().toLong()
  val v2 = y.toBits().toLong()
  val packed = v1.shl(32) or (v2 and 0xFFFFFFFF)
  return Point(packed)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.mountable.utils.types

import android.annotation.SuppressLint

@SuppressLint("HexColorValueUsage")
@JvmInline
value class Size internal constructor(@PublishedApi internal val packedValue: Long) {
  val width: Float
    get() {
      return Float.fromBits(packedValue.shr(32).toInt())
    }

  val height: Float
    get() {
      return Float.fromBits(packedValue.and(0xFFFFFFFF).toInt())
    }

  operator fun times(operand: Float): Size = Size(width * operand, height * operand)

  operator fun div(operand: Float): Size = Size(width / operand, height / operand)
}

@SuppressLint("HexColorValueUsage")
fun Size(width: Float, height: Float): Size {
  // pack two Float values into one Long value
  val v1 = width.toBits().toLong()
  val v2 = height.toBits().toLong()
  val packed = v1.shl(32) or (v2 and 0xFFFFFFFF)
  return Size(packed)
}

val Size.center: Point
  get() {
    return Point(width / 2f, height / 2f)
  }


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.rendercore.primitives

import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import com.facebook.rendercore.ContentAllocator
import com.facebook.rendercore.ContentAllocator.DEFAULT_MAX_PREALLOCATION
import com.facebook.rendercore.RenderUnit

/**
 * A [ContentAllocator] that allocates an instance of a [View].
 *
 * @property poolSize The size of the content pool. The default is [DEFAULT_MAX_PREALLOCATION].
 * @property canPreallocate The flag indicating if the content pool for this Primitive should be
 *   filled ahead of time. See [ContentAllocator.canPreallocate]. The default is false.
 * @property allocator The lambda callback that returns the actual [View].
 */
class ViewAllocator<Content : View>(
    private val poolSize: Int = DEFAULT_MAX_PREALLOCATION,
    private val canPreallocate: Boolean = false,
    private val allocator: Allocator<Content>
) : ContentAllocator<Content> {
  override fun createContent(context: Context): Content = allocator.allocate(context)

  override fun getRenderType(): RenderUnit.RenderType = RenderUnit.RenderType.VIEW

  override fun poolSize(): Int = poolSize

  override fun canPreallocate(): Boolean = canPreallocate

  override fun getPoolableContentType(): Any = allocator.javaClass
}

/**
 * A [ContentAllocator] that allocates an instance of a [Drawable].
 *
 * @property poolSize The size of the content pool. The default is [DEFAULT_MAX_PREALLOCATION].
 * @property canPreallocate The flag indicating if the content pool for this Primitive should be
 *   filled ahead of time. See [ContentAllocator.canPreallocate]. The default is false.
 * @property allocator The lambda callback that returns the actual [Drawable].
 */
class DrawableAllocator<Content : Drawable>(
    private val poolSize: Int = DEFAULT_MAX_PREALLOCATION,
    private val canPreallocate: Boolean = false,
    private val allocator: Allocator<Content>
) : ContentAllocator<Content> {
  override fun createContent(context: Context): Content = allocator.allocate(context)

  override fun getRenderType(): RenderUnit.RenderType = RenderUnit.RenderType.DRAWABLE

  override fun poolSize(): Int = poolSize

  override fun canPreallocate(): Boolean = canPreallocate

  override fun getPoolableContentType(): Any = allocator.javaClass
}

/**
 * An interface used to mark the allocator lambdas, so that they're not optimized. Each [Allocator]
 * lambda should be compiled to a separate class without being merged with others.
 */
fun interface Allocator<ContentType> {
  fun allocate(context: Context): ContentType
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.rendercore.primitives

/** Equivalence is a concept to have a custom equal-like method. */
fun interface Equivalence<in T> {
  fun isEquivalentTo(other: T): Boolean
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.rendercore.primitives

import com.facebook.rendercore.LayoutResult
import com.facebook.rendercore.RenderUnit
import com.facebook.rendercore.Size
import com.facebook.rendercore.SizeConstraints
import com.facebook.rendercore.primitives.utils.hasEquivalentFields
import com.facebook.rendercore.utils.exact
import com.facebook.rendercore.utils.fillSpace
import com.facebook.rendercore.utils.withAspectRatio
import com.facebook.rendercore.utils.withEqualDimensions

/**
 * LayoutBehavior defines how large a primitive is and, if it has children, how it lays out and
 * places those children. Implementations should take exactly what data they need for layout -
 * LayoutBehaviors are considered equivalent via field-wise comparison (i.e. are all fields also
 * equivalent?). This will be used to determine whether re-layouts need to happen.
 */
interface LayoutBehavior : Equivalence<LayoutBehavior> {
  fun LayoutScope.layout(sizeConstraints: SizeConstraints): PrimitiveLayoutResult

  // By default, LayoutBehavior have field-wise equivalence, like Components
  override fun isEquivalentTo(other: LayoutBehavior): Boolean = hasEquivalentFields(this, other)
}

/**
 * Returns a [LayoutBehavior] with sizes set based on the provided [SizeConstraints].
 *
 * This method should only be used for Primitive Components which do not measure themselves - it's
 * the parent that has determined the exact size for this child.
 *
 * @throws IllegalArgumentException if the [SizeConstraints] are not exact
 */
object ExactSizeConstraintsLayoutBehavior : LayoutBehavior {
  override fun LayoutScope.layout(sizeConstraints: SizeConstraints): PrimitiveLayoutResult {
    return PrimitiveLayoutResult(size = Size.exact(sizeConstraints))
  }
}

/**
 * Returns a [LayoutBehavior] with sizes set based on the [SizeConstraints] if the constraints are
 * bounded, otherwise uses default values.
 *
 * @param defaultWidth The pixel value for the width of the measured component when the provided
 *   maxWidth constraint is equal to Infinity.
 * @param defaultHeight The pixel value for the height of the measured component when the provided
 *   maxHeight constraint is equal to Infinity.
 * @param layoutData The data to be returned from the layout pass.
 */
class FillLayoutBehavior(
    private val defaultWidth: Int,
    private val defaultHeight: Int,
    private val layoutData: Any? = null
) : LayoutBehavior {
  override fun LayoutScope.layout(sizeConstraints: SizeConstraints): PrimitiveLayoutResult {
    return PrimitiveLayoutResult(
        size = Size.fillSpace(sizeConstraints, defaultWidth, defaultHeight),
        layoutData = layoutData)
  }
}

/**
 * Returns a [LayoutBehavior] with sizes set based provided values.
 *
 * @param width The pixel value for the width of the measured component.
 * @param height The pixel value for the height of the measured component.
 * @param layoutData The data to be returned from the layout pass.
 */
class FixedSizeLayoutBehavior(
    private val width: Int,
    private val height: Int,
    private val layoutData: Any? = null,
) : LayoutBehavior {
  override fun LayoutScope.layout(sizeConstraints: SizeConstraints): PrimitiveLayoutResult {
    return PrimitiveLayoutResult(width = width, height = height, layoutData = layoutData)
  }
}

/**
 * Returns a [LayoutBehavior] with sizes set according to an aspect ratio and [SizeConstraints]. It
 * will respect the intrinsic size of the component being measured.
 *
 * @param aspectRatio The aspect ratio for calculating size.
 * @param intrinsicWidth The pixel value for the intrinsic width of the measured component.
 * @param intrinsicHeight The pixel value for the intrinsic height of the measured component.
 * @param layoutData The data to be returned from the layout pass.
 */
class AspectRatioLayoutBehavior(
    private val aspectRatio: Float,
    private val intrinsicWidth: Int,
    private val intrinsicHeight: Int,
    private val layoutData: Any? = null
) : LayoutBehavior {
  override fun LayoutScope.layout(sizeConstraints: SizeConstraints): PrimitiveLayoutResult {
    return PrimitiveLayoutResult(
        size = Size.withAspectRatio(sizeConstraints, aspectRatio, intrinsicWidth, intrinsicHeight),
        layoutData = layoutData)
  }
}

/**
 * Returns a [LayoutBehavior] that respects both [SizeConstraints] and tries to keep both width and
 * height equal. This will only not guarantee equal width and height if the constraints don't allow
 * for it.
 *
 * @param layoutData The data to be returned from the layout pass.
 */
class EqualDimensionsLayoutBehavior(private val layoutData: Any? = null) : LayoutBehavior {
  override fun LayoutScope.layout(sizeConstraints: SizeConstraints): PrimitiveLayoutResult {
    return PrimitiveLayoutResult(
        size = Size.withEqualDimensions(sizeConstraints), layoutData = layoutData)
  }
}

class PrimitiveLayoutResult(
    val width: Int,
    val height: Int,
    private val paddingTop: Int = 0,
    private val paddingRight: Int = 0,
    private val paddingBottom: Int = 0,
    private val paddingLeft: Int = 0,
    private val layoutData: Any? = null,
) {

  constructor(
      size: Size,
      paddingTop: Int = 0,
      paddingRight: Int = 0,
      paddingBottom: Int = 0,
      paddingLeft: Int = 0,
      layoutData: Any? = null
  ) : this(
      size.width, size.height, paddingTop, paddingRight, paddingBottom, paddingLeft, layoutData)

  init {
    if (width < 0) {
      throw IllegalArgumentException("width must be >= 0, but was: $width")
    }
    if (height < 0) {
      throw IllegalArgumentException("height must be >= 0, but was: $height")
    }
    if (width >= SizeConstraints.MaxValue) {
      throw IllegalArgumentException(
          "width must be < ${SizeConstraints.MaxValue}, but was: $width. Components this big may affect performance and lead to out of memory errors.")
    }
    if (height >= SizeConstraints.MaxValue) {
      throw IllegalArgumentException(
          "height must be < ${SizeConstraints.MaxValue}, but was: $height. Components this big may affect performance and lead to out of memory errors.")
    }
  }

  internal fun toNodeLayoutResult(
      widthSpec: Int,
      heightSpec: Int,
      renderUnit: RenderUnit<*>?
  ): LayoutResult {
    return object : LayoutResult {
      override fun getRenderUnit(): RenderUnit<*>? = renderUnit

      override fun getLayoutData(): Any? = this@PrimitiveLayoutResult.layoutData

      override fun getChildrenCount(): Int = 0

      override fun getChildAt(index: Int): LayoutResult {
        throw UnsupportedOperationException("A PrimitiveLayoutResult has no children")
      }

      override fun getXForChildAtIndex(index: Int): Int {
        throw UnsupportedOperationException("A PrimitiveLayoutResult has no children")
      }

      override fun getYForChildAtIndex(index: Int): Int {
        throw UnsupportedOperationException("A PrimitiveLayoutResult has no children")
      }

      override fun getWidth(): Int = this@PrimitiveLayoutResult.width

      override fun getHeight(): Int = this@PrimitiveLayoutResult.height

      override fun getPaddingTop(): Int = this@PrimitiveLayoutResult.paddingTop

      override fun getPaddingRight(): Int = this@PrimitiveLayoutResult.paddingRight

      override fun getPaddingBottom(): Int = this@PrimitiveLayoutResult.paddingBottom

      override fun getPaddingLeft(): Int = this@PrimitiveLayoutResult.paddingLeft

      override fun getWidthSpec(): Int = widthSpec

      override fun getHeightSpec(): Int = heightSpec
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.facebook.rendercore.primitives

import android.content.Context
import com.facebook.rendercore.LayoutContext

/**
 * The scope for the [Primitive] layout method. Provides access to [LayoutContext], the previous
 * layout data and utility methods that may help to compute the [PrimitiveLayoutResult].
 */
class LayoutScope
internal constructor(val layoutContext: LayoutContext<Any?>, val previousLayoutData: Any?) {
  val androidContext: Context
    get() = layoutContext.androidContext
}