/*
 * 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.litho.sections.widget

import androidx.recyclerview.widget.RecyclerView.ItemAnimator
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener
import com.facebook.litho.Component
import com.facebook.litho.Dimen
import com.facebook.litho.Handle
import com.facebook.litho.LithoStartupLogger
import com.facebook.litho.ResourcesScope
import com.facebook.litho.Style
import com.facebook.litho.kotlinStyle
import com.facebook.litho.sections.Section
import com.facebook.litho.widget.LithoRecyclerView
import com.facebook.litho.widget.SectionsRecyclerView.SectionsRecyclerViewLogger
import com.facebook.litho.widget.StickyHeaderControllerFactory

/**
 * Kotlin wrapper for the Component generated by RecyclerCollectionComponentSpec.
 *
 * Note: Since RecyclerCollectionComponent returns a container around a RecyclerView (for
 * pull-to-refresh), Style/common props apply to that container instead of the underlying
 * RecyclerView. The properties named "recyclerView*" allow you to apply some of these properties to
 * the underlying RecyclerView instead.
 */
@Suppress("FunctionName")
inline fun ResourcesScope.RecyclerCollectionComponent(
    section: Section,
    recyclerConfiguration: RecyclerConfiguration =
        RecyclerCollectionComponentSpec.recyclerConfiguration,
    itemAnimator: ItemAnimator? = RecyclerCollectionComponentSpec.itemAnimator,
    itemDecoration: ItemDecoration? = null,
    canMeasureRecycler: Boolean = false,
    loadingComponent: Component? = null,
    emptyComponent: Component? = null,
    errorComponent: Component? = null,
    recyclerViewClipToPadding: Boolean = RecyclerCollectionComponentSpec.clipToPadding,
    recyclerViewClipChildren: Boolean = RecyclerCollectionComponentSpec.clipChildren,
    recyclerViewStartPadding: Dimen? = null,
    recyclerViewEndPadding: Dimen? = null,
    recyclerViewTopPadding: Dimen? = null,
    recyclerViewBottomPadding: Dimen? = null,
    disablePTR: Boolean = false,
    nestedScrollingEnabled: Boolean = RecyclerCollectionComponentSpec.nestedScrollingEnabled,
    scrollBarStyle: Int = RecyclerCollectionComponentSpec.scrollBarStyle,
    recyclerViewId: Int = RecyclerCollectionComponentSpec.recyclerViewId,
    overScrollMode: Int = RecyclerCollectionComponentSpec.overScrollMode,
    refreshProgressBarColor: Int = RecyclerCollectionComponentSpec.refreshProgressBarColor,
    touchInterceptor: LithoRecyclerView.TouchInterceptor? = null,
    itemTouchListener: OnItemTouchListener? = null,
    eventsController: RecyclerCollectionEventsController? = null,
    sectionTreeTag: String? = null,
    startupLogger: LithoStartupLogger? = null,
    stickyHeaderControllerFactory: StickyHeaderControllerFactory? = null,
    sectionsViewLogger: SectionsRecyclerViewLogger? = null,
    handle: Handle? = null,
    style: Style? = null,
): RecyclerCollectionComponent =
    RecyclerCollectionComponent.create(context)
        .section(section)
        .recyclerConfiguration(recyclerConfiguration)
        .itemAnimator(itemAnimator)
        .itemDecoration(itemDecoration)
        .canMeasureRecycler(canMeasureRecycler)
        .loadingComponent(loadingComponent)
        .emptyComponent(emptyComponent)
        .errorComponent(errorComponent)
        .clipToPadding(recyclerViewClipToPadding)
        .clipChildren(recyclerViewClipChildren)
        .leftPaddingPx(recyclerViewStartPadding?.toPixels(resourceResolver) ?: 0)
        .rightPaddingPx(recyclerViewEndPadding?.toPixels(resourceResolver) ?: 0)
        .topPaddingPx(recyclerViewTopPadding?.toPixels(resourceResolver) ?: 0)
        .bottomPaddingPx(recyclerViewBottomPadding?.toPixels(resourceResolver) ?: 0)
        .disablePTR(disablePTR)
        .nestedScrollingEnabled(nestedScrollingEnabled)
        .scrollBarStyle(scrollBarStyle)
        .recyclerViewId(recyclerViewId)
        .overScrollMode(overScrollMode)
        .refreshProgressBarColor(refreshProgressBarColor)
        .touchInterceptor(touchInterceptor)
        .itemTouchListener(itemTouchListener)
        .eventsController(eventsController)
        .sectionTreeTag(sectionTreeTag)
        .startupLogger(startupLogger)
        .stickyHeaderControllerFactory(stickyHeaderControllerFactory)
        .sectionsViewLogger(sectionsViewLogger)
        .handle(handle)
        .kotlinStyle(style)
        .build()


/*
 * 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.litho.widget.canvas

import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.Style
import com.facebook.litho.useCached
import com.facebook.mountable.canvas.CanvasMountable
import com.facebook.mountable.canvas.CanvasState
import com.facebook.mountable.utils.types.CanvasLayerType
import com.facebook.mountable.utils.types.DEFAULT_CANVAS_LAYER_TYPE
import com.facebook.mountable.utils.types.Size

/**
 * The Canvas Component which provides a means for drawing 2D graphics.
 *
 * @property 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. Default is [CanvasLayerType.Auto]
 * @property matrixPoolSize The Matrix pool max size
 * @property pathCacheSize The Path cache max size
 * @property block The lambda callback to issue drawing commands
 */
class CanvasComponent(
    private val layerType: CanvasLayerType = DEFAULT_CANVAS_LAYER_TYPE,
    private val matrixPoolSize: Int = CanvasState.DEFAULT_MATRIX_POOL_SIZE,
    private val pathCacheSize: Int = CanvasState.DEFAULT_PATH_CACHE_SIZE,
    private val style: Style? = null,
    private val block: CanvasScope.() -> Unit
) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    val canvasState = useCached { CanvasState(matrixPoolSize, pathCacheSize) }

    return MountableRenderResult(
        CanvasMountable(
            layerType = layerType,
            modelProvider = { canvasSize: Size ->
              CanvasScope().createModel(canvasSize, canvasState, block)
            }),
        style)
  }
}


/*
 * 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.litho.widget.canvas

import android.annotation.SuppressLint
import android.graphics.Canvas
import androidx.annotation.FloatRange
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.mountable.canvas.CanvasState
import com.facebook.mountable.canvas.model.CanvasDrawIntoCanvas
import com.facebook.mountable.canvas.model.CanvasFill
import com.facebook.mountable.canvas.model.CanvasGroup
import com.facebook.mountable.canvas.model.CanvasLayer
import com.facebook.mountable.canvas.model.CanvasModel
import com.facebook.mountable.canvas.model.CanvasNodeModel
import com.facebook.mountable.canvas.model.CanvasStroke
import com.facebook.mountable.utils.types.BlendingMode
import com.facebook.mountable.utils.types.DEFAULT_BLENDING_MODE
import com.facebook.mountable.utils.types.DEFAULT_LINE_CAP
import com.facebook.mountable.utils.types.DEFAULT_LINE_JOIN
import com.facebook.mountable.utils.types.LineCap
import com.facebook.mountable.utils.types.LineJoin
import com.facebook.mountable.utils.types.Point
import com.facebook.mountable.utils.types.Size
import com.facebook.mountable.utils.types.center

class CanvasScope {
  @PublishedApi
  @DataClassGenerate
  internal data class DrawParams(
      var children: MutableList<CanvasNodeModel> = mutableListOf(),
      var size: Size = Size(0f, 0f)
  )

  @PublishedApi internal val drawParams: DrawParams = DrawParams()

  /** The size of the current drawing scope (group, layer or whole canvas) */
  val size: Size
    inline get() = drawParams.size

  /** The center of the current bounds of the drawing scope (group, layer or whole canvas) */
  val center: Point
    inline get() = drawParams.size.center

  /**
   * Fills the given Shape.
   *
   * @param shape The shape that will be drawn with fill style. A shape can be either a [Path] or
   *   [Shape]. For simple shapes prefer using [Shape] if possible because it offers better drawing
   *   performance.
   * @param shading The shading definition that describes how to fill the path. For example solid
   *   color or a gradient.
   * @param blendingMode The blend mode that will be used when filling the shape
   * @param shadow The shadow that will be drawn below the path
   */
  inline fun fill(
      shape: Shape,
      shading: Shading,
      blendingMode: BlendingMode = DEFAULT_BLENDING_MODE,
      shadow: Shadow? = null
  ) {
    drawParams.children.add(
        CanvasFill(shape.shapeModel, shading.shadingModel, blendingMode, shadow?.shadowModel))
  }

  /**
   * Strokes the given Shape.
   *
   * 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.
   *
   * @param shape The shape that will be drawn with stroke style. A shape can be either a [Path] or
   *   [Shape]. For simple shapes prefer using [Shape] if possible because it offers better drawing
   *   performance.
   * @param shading The shading definition that describes how to stroke the path. For example solid
   *   color or a gradient.
   * @param blendingMode The blend mode that will be used when stroking the shape
   * @param shadow The shadow that will be drawn below the path
   * @param lineWidth The width for stroking. Pass 0 to stroke in hairline mode. Hairlines always
   *   draws a single pixel.
   * @param lineCap The line cap style
   * @param lineJoin The line join style
   * @param 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.
   * @param 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.
   * @param 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.
   */
  inline fun stroke(
      shape: Shape,
      shading: Shading,
      blendingMode: BlendingMode = DEFAULT_BLENDING_MODE,
      shadow: Shadow? = null,
      lineWidth: Float = CanvasStroke.DEFAULT_LINE_WIDTH,
      lineCap: LineCap = DEFAULT_LINE_CAP,
      lineJoin: LineJoin = DEFAULT_LINE_JOIN,
      miterLimit: Float = CanvasStroke.DEFAULT_MITER_LIMIT,
      dashLengths: FloatArray? = null,
      dashPhase: Float = 0f
  ) {
    drawParams.children.add(
        CanvasStroke(
            shape.shapeModel,
            shading.shadingModel,
            blendingMode,
            shadow?.shadowModel,
            lineWidth,
            lineCap,
            lineJoin,
            miterLimit,
            dashLengths,
            dashPhase))
  }

  /**
   * Creates a group. 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.
   *
   * @param transform The transform that will be applied to the children of the group
   * @param size The size of the group
   * @param clip The path describing the shape of the clip that will be applied to the children of
   *   the group
   * @param clipToBounds The group will be clipped to its bounds if this is set to true and [clip]
   *   is null. Defaults to false.
   * @param block The lambda callback to issue drawing commands
   */
  @SuppressLint("KotlinScopeFunctionsMisuse")
  inline fun group(
      transform: Transform = Transform.IDENTITY,
      size: Size? = null,
      clip: Path? = null,
      clipToBounds: Boolean = false,
      block: CanvasScope.() -> Unit
  ) {
    val groupChildren = mutableListOf<CanvasNodeModel>()
    val (prevChildren, prevSize) = drawParams

    drawParams.apply {
      this.children = groupChildren
      this.size = size ?: prevSize
    }
    block()
    drawParams.apply {
      this.children = prevChildren
      this.size = prevSize
    }

    drawParams.children.add(
        CanvasGroup(
            transform.transformModel,
            size ?: prevSize,
            clip?.pathModel,
            clipToBounds,
            groupChildren))
  }

  /**
   * Creates a layer which 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.
   *
   * @param transform The transform that will be applied to the children of the layer
   * @param size The size of the layer
   * @param clip The path describing the shape of the clip that will be applied to the children of
   *   the layer
   * @param alpha The alpha that will be applied to the layer when compositing it with the parent
   *   layer
   * @param blendingMode The blend mode that will be applied to the layer when compositing it with
   *   the parent layer
   * @param block The lambda callback to issue drawing commands
   */
  @SuppressLint("KotlinScopeFunctionsMisuse")
  inline fun layer(
      transform: Transform = Transform.IDENTITY,
      size: Size? = null,
      clip: Path? = null,
      @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1f,
      blendingMode: BlendingMode = DEFAULT_BLENDING_MODE,
      block: CanvasScope.() -> Unit
  ) {
    val layerChildren = mutableListOf<CanvasNodeModel>()
    val (prevChildren, prevSize) = drawParams

    drawParams.apply {
      this.children = layerChildren
      this.size = size ?: prevSize
    }
    block()
    drawParams.apply {
      this.children = prevChildren
      this.size = prevSize
    }

    drawParams.children.add(
        CanvasLayer(
            transform.transformModel,
            size ?: prevSize,
            clip?.pathModel,
            alpha,
            blendingMode,
            layerChildren))
  }

  /**
   * Provides access to draw directly with the underlying [Canvas]. This is helpful for situations
   * to re-use the existing [Canvas] drawing logic.
   *
   * @param block The lambda callback to issue drawing commands on the provided [Canvas]
   * @see <a
   *   href="https://developer.android.com/topic/performance/hardware-accel#drawing-support">Drawing
   *   Support</a>
   */
  inline fun drawIntoCanvas(noinline block: (Canvas) -> Unit) {
    drawParams.children.add(CanvasDrawIntoCanvas(block))
  }

  /**
   * Draws into the provided [Canvas] with the commands specified in the lambda with this
   * [CanvasScope] as a receiver.
   *
   * @param canvas The target canvas to render into
   * @param size The size relative to the current canvas transform in which the [CanvasScope] should
   *   draw within
   * @param canvasState The canvas state, refer to the documentation of [CanvasState] to learn how
   *   to instantiate it properly.
   * @param block The lambda that is called to issue drawing commands on this [CanvasScope]
   */
  inline fun draw(
      canvas: Canvas,
      size: Size,
      canvasState: CanvasState,
      block: CanvasScope.() -> Unit
  ) {
    val scope = CanvasScope()
    val canvasRootModel = scope.createModel(size, canvasState, block)
    canvasRootModel.draw(canvas)
  }

  inline fun createModel(
      size: Size,
      canvasState: CanvasState,
      block: CanvasScope.() -> Unit
  ): CanvasModel {
    drawParams.size = size
    block()
    return CanvasModel(canvasState, drawParams.children)
  }
}


/*
 * 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.litho.widget.canvas

import com.facebook.mountable.canvas.model.CanvasPath
import com.facebook.mountable.canvas.model.CanvasPathAdd
import com.facebook.mountable.canvas.model.CanvasPathArc
import com.facebook.mountable.canvas.model.CanvasPathChildModel
import com.facebook.mountable.canvas.model.CanvasPathCircle
import com.facebook.mountable.canvas.model.CanvasPathClose
import com.facebook.mountable.canvas.model.CanvasPathCubicTo
import com.facebook.mountable.canvas.model.CanvasPathEllipse
import com.facebook.mountable.canvas.model.CanvasPathLineTo
import com.facebook.mountable.canvas.model.CanvasPathModel
import com.facebook.mountable.canvas.model.CanvasPathMoveTo
import com.facebook.mountable.canvas.model.CanvasPathQuadTo
import com.facebook.mountable.canvas.model.CanvasPathRect
import com.facebook.mountable.utils.types.DEFAULT_FILL_RULE
import com.facebook.mountable.utils.types.FillRule
import com.facebook.mountable.utils.types.Point
import com.facebook.mountable.utils.types.Size

@JvmInline
value class Path internal constructor(@PublishedApi internal val pathModel: CanvasPathModel)

/**
 * A path which contains a mathematical description of shapes or lines to be drawn on a Canvas.
 *
 * @param fillRule The path's fill type. Defines how "inside" is computed
 * @param block The lambda callback to issue path commands
 */
fun Path(fillRule: FillRule = DEFAULT_FILL_RULE, block: PathScope.() -> Unit): Path {
  return Path(PathScope().createModel(fillRule, block))
}

class PathScope {
  @PublishedApi internal val children: MutableList<CanvasPathChildModel> = mutableListOf()

  /**
   * Sets the beginning of the next contour to the point (x,y).
   *
   * @param point The start point of a new contour
   */
  inline fun moveTo(point: Point) {
    children.add(CanvasPathMoveTo(point))
  }

  /**
   * Adds a line from the last point to the specified point (x,y). If no [moveTo] children has been
   * added to this Path, then the first point is automatically set to (0,0).
   *
   * @param point The end point of a line
   */
  inline fun lineTo(point: Point) {
    children.add(CanvasPathLineTo(point))
  }

  /**
   * Adds a quadratic bezier from the last point, approaching control point
   * (controlPoint.x,controlPoint.y), and ending at (endPoint.x,endPoint.y). If no [moveTo] children
   * has been added to this Path, then the first point is automatically set to (0,0).
   *
   * @param controlPoint The control point on a quadratic curve
   * @param endPoint The end point on a quadratic curve
   */
  inline fun quadTo(controlPoint: Point, endPoint: Point) {
    children.add(CanvasPathQuadTo(controlPoint, endPoint))
  }

  /**
   * 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 [moveTo] children has been added to this Path, then the first
   * point is automatically set to (0,0).
   *
   * @param controlPoint1 The 1st control point on a cubic curve
   * @param controlPoint2 The 2nd control point on a cubic curve
   * @param endPoint The end point on a cubic curve
   */
  inline fun cubicTo(controlPoint1: Point, controlPoint2: Point, endPoint: Point) {
    children.add(CanvasPathCubicTo(controlPoint1, controlPoint2, endPoint))
  }

  /**
   * Adds the specified arc to the path as a new contour.
   *
   * @param center The center of the arc
   * @param radius The radius of the arc
   * @param startDegrees The angle to the starting point of the arc
   * @param endDegrees The angle to the end point of the arc
   * @param clockwise true to make a clockwise arc; false to make a counterclockwise arc
   */
  inline fun arc(
      center: Point,
      radius: Float,
      startDegrees: Float,
      endDegrees: Float,
      clockwise: Boolean = true,
  ) {
    children.add(CanvasPathArc(center, radius, startDegrees, endDegrees, clockwise))
  }

  /**
   * Adds a closed (optionally) round-rectangle contour.
   *
   * @param topLeft The coordinates of the top left point of the rectangle
   * @param size The size of the rectangle
   * @param cornerRadius The radius of the rounded corners
   */
  inline fun rect(topLeft: Point, size: Size, cornerRadius: Float = 0f) {
    children.add(CanvasPathRect(topLeft, size, cornerRadius))
  }

  /**
   * Adds closed oval contour to the contour.
   *
   * @param topLeft The coordinates of the top left point of the ellipse
   * @param size The size of the ellipse
   */
  inline fun ellipse(topLeft: Point, size: Size) {
    children.add(CanvasPathEllipse(topLeft, size))
  }

  /**
   * Adds a closed circle contour to the contour.
   *
   * @param center The center of the circle
   * @param radius The radius of the circle
   */
  inline fun circle(center: Point, radius: Float) {
    children.add(CanvasPathCircle(center, radius))
  }

  /**
   * Closes the current contour. If the current point is not equal to the first point of the
   * contour, a line segment is automatically added.
   */
  inline fun close() {
    children.add(CanvasPathClose)
  }

  /**
   * Adds a copy of src path to the current path, transformed by transformModel.
   *
   * @param src The path to add as a new contour to the current path
   * @param transform The transform that will be applied to the src before adding it to the current
   *   path
   */
  inline fun add(src: Path, transform: Transform = Transform.IDENTITY) {
    children.add(CanvasPathAdd(src.pathModel, transform.transformModel))
  }

  /**
   * Creates the PathModel described by the operations invoked in PathScope.
   *
   * @param fillRule The path's fill type. Defines how "inside" is computed
   * @param block The lambda callback to issue path commands
   * @return The PathModel object created from the operations invoked in this scope.
   */
  inline fun createModel(fillRule: FillRule, block: PathScope.() -> Unit): CanvasPathModel {
    block()
    return CanvasPath(fillRule, children)
  }
}


/*
 * 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.litho.widget.canvas

import android.graphics.Shader
import androidx.annotation.ColorInt
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.mountable.canvas.model.CanvasGradient
import com.facebook.mountable.canvas.model.CanvasGradientShading
import com.facebook.mountable.canvas.model.CanvasLinearGradient
import com.facebook.mountable.canvas.model.CanvasRadialGradient
import com.facebook.mountable.canvas.model.CanvasShadingModel
import com.facebook.mountable.canvas.model.CanvasSolidColorShading
import com.facebook.mountable.utils.types.Point

@JvmInline
value class Shading
private constructor(@PublishedApi internal val shadingModel: CanvasShadingModel) {
  companion object {
    /**
     * Solid color shading.
     *
     * @param color The color (including alpha)
     */
    fun solidColor(@ColorInt color: Int): Shading {
      return Shading(CanvasSolidColorShading(color))
    }

    /**
     * A linear gradient that varies along the line defined by the provided starting and ending
     * points.
     *
     * @param gradient The colors and positions of the gradient
     * @param startPoint The start of the gradient line
     * @param endPoint The end of the gradient line
     * @param tileMode The tiling mode
     */
    fun linearGradient(
        gradient: Gradient,
        startPoint: Point,
        endPoint: Point,
        tileMode: Shader.TileMode = Shader.TileMode.CLAMP
    ): Shading {
      return Shading(
          CanvasGradientShading(
              CanvasLinearGradient(gradient.canvasGradient, startPoint, endPoint, tileMode)))
    }

    /**
     * A radial gradient given the center and radius.
     *
     * @param gradient The colors and positions of the gradient
     * @param center The center of the radius
     * @param radius Must be positive. The radius of the circle for this gradient
     * @param tileMode The tiling mode
     */
    fun radialGradient(
        gradient: Gradient,
        center: Point,
        radius: Float,
        tileMode: Shader.TileMode = Shader.TileMode.CLAMP
    ): Shading {
      return Shading(
          CanvasGradientShading(
              CanvasRadialGradient(gradient.canvasGradient, center, radius, tileMode)))
    }
  }
}

@JvmInline
value class Gradient
internal constructor(@PublishedApi internal val canvasGradient: CanvasGradient)

/**
 * A smooth transition between colors for drawing gradients.
 *
 * @param colors The sRGB colors (including alpha) to be evenly distributed along the gradient
 */
fun Gradient(@ColorInt vararg colors: Int): Gradient {
  return Gradient(CanvasGradient(colors, null))
}

/**
 * A smooth transition between colors for drawing gradients.
 *
 * @param colorsAndPositions The colors and positions that should be used to create a gradient.
 */
fun Gradient(vararg colorsAndPositions: GradientColorAndPosition): Gradient {
  val colors = colorsAndPositions.map { it.color }.toIntArray()
  val positions = colorsAndPositions.map { it.position }.toFloatArray()
  return Gradient(CanvasGradient(colors, positions))
}

/**
 * A definition for a position and its corresponding color in a gradient.
 *
 * @property color The sRGB colors (including alpha) to be distributed along the gradient
 * @property position 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 GradientColorAndPosition(@ColorInt val color: Int, val position: Float)

infix fun @receiver:ColorInt Int.at(position: Float): GradientColorAndPosition {
  return GradientColorAndPosition(this, position)
}


/*
 * 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.litho.widget.canvas

import androidx.annotation.ColorInt
import com.facebook.mountable.canvas.model.CanvasShadowModel

@JvmInline
value class Shadow internal constructor(@PublishedApi internal val shadowModel: CanvasShadowModel)

/**
 * A shadow which should be drawn under a shape on a Canvas.
 *
 * @param dx horizontal offset
 * @param dy vertical offset
 * @param radius blur radius
 * @param color color of the shadow
 */
fun Shadow(dx: Float, dy: Float, radius: Float, @ColorInt color: Int): Shadow {
  return Shadow(CanvasShadowModel(dx, dy, radius, color))
}


/*
 * 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.litho.widget.canvas

import com.facebook.mountable.canvas.model.CanvasShape
import com.facebook.mountable.canvas.model.CanvasShapeArc
import com.facebook.mountable.canvas.model.CanvasShapeCircle
import com.facebook.mountable.canvas.model.CanvasShapeEllipse
import com.facebook.mountable.canvas.model.CanvasShapeLine
import com.facebook.mountable.canvas.model.CanvasShapeRect
import com.facebook.mountable.utils.types.Point
import com.facebook.mountable.utils.types.Size

@JvmInline
value class Shape private constructor(@PublishedApi internal val shapeModel: CanvasShape) {
  companion object {

    /**
     * A line segment with the specified start and stop x,y coordinates that can be drawn on a
     * Canvas.
     *
     * @param topLeft The coordinates of the top left point of the rectangle
     * @param size The size of the rectangle
     * @param cornerRadius The radius of the rounded corners
     */
    fun line(startPoint: Point, endPoint: Point): Shape {
      return Shape(CanvasShapeLine(startPoint, endPoint))
    }

    /**
     * A closed (optionally) round-rectangle that can be drawn on a Canvas.
     *
     * @param topLeft The coordinates of the top left point of the rectangle
     * @param size The size of the rectangle
     * @param cornerRadius The radius of the rounded corners
     */
    fun rect(topLeft: Point, size: Size, cornerRadius: Float = 0f): Shape {
      return Shape(CanvasShapeRect(topLeft, size, cornerRadius))
    }

    /**
     * A closed circle that can be drawn on a Canvas.
     *
     * @param center The center of the circle
     * @param radius The radius of the circle
     */
    fun circle(center: Point, radius: Float): Shape {
      return Shape(CanvasShapeCircle(center, radius))
    }

    /**
     * An arc shape that can be drawn on a Canvas.
     *
     * @param center The center of the arc
     * @param radius The radius of the arc
     * @param startDegrees The angle to the starting point of the arc
     * @param endDegrees The angle to the end point of the arc
     * @param clockwise true to make a clockwise arc; false to make a counterclockwise arc
     */
    fun arc(
        center: Point,
        radius: Float,
        startDegrees: Float,
        endDegrees: Float,
        clockwise: Boolean = true,
    ): Shape {
      return Shape(CanvasShapeArc(center, radius, startDegrees, endDegrees, clockwise))
    }

    /**
     * A closed ellipse that can be drawn on a Canvas.
     *
     * @param topLeft The coordinates of the top left point of the ellipse
     * @param size The size of the ellipse
     */
    fun ellipse(topLeft: Point, size: Size): Shape {
      return Shape(CanvasShapeEllipse(topLeft, size))
    }

    /**
     * A path which contains a mathematical description of shapes or lines to be drawn on a Canvas.
     *
     * @param path The path object
     */
    fun path(path: Path): Shape {
      return Shape(path.pathModel)
    }
  }
}


/*
 * 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.litho.widget.canvas

import com.facebook.mountable.canvas.model.CanvasInverseTransform
import com.facebook.mountable.canvas.model.CanvasRotate
import com.facebook.mountable.canvas.model.CanvasScale
import com.facebook.mountable.canvas.model.CanvasSkew
import com.facebook.mountable.canvas.model.CanvasTransform
import com.facebook.mountable.canvas.model.CanvasTransformChildModel
import com.facebook.mountable.canvas.model.CanvasTransformModel
import com.facebook.mountable.canvas.model.CanvasTranslate
import com.facebook.mountable.utils.types.Point

/** A transform which holds a 3x3 matrix for transforming coordinates. */
@JvmInline
value class Transform
internal constructor(@PublishedApi internal val transformModel: CanvasTransformModel) {
  companion object {
    val IDENTITY: Transform = Transform(CanvasTransform.IDENTITY)
  }
}

/**
 * Creates a transform which holds a 3x3 matrix for transforming coordinates.
 *
 * @param a The horizontal scaling. A value of 1 results in no scaling
 * @param b The vertical skewing
 * @param c The horizontal skewing
 * @param d The vertical scaling. A value of 1 results in no scaling
 * @param tx The horizontal translation (moving)
 * @param ty The vertical translation (moving)
 * @param block The lambda callback to issue transform commands
 */
fun Transform(
    a: Float = 1f,
    b: Float = 0f,
    c: Float = 0f,
    d: Float = 1f,
    tx: Float = 0f,
    ty: Float = 0f,
    block: TransformScope.() -> Unit
): Transform {
  val scope = TransformScope()
  block(scope)
  return Transform(CanvasTransform(a, b, c, d, tx, ty, scope.children))
}

class TransformScope {
  val children: MutableList<CanvasTransformChildModel> = mutableListOf()

  /**
   * Inverts the specified transform.
   *
   * @param a The horizontal scaling. A value of 1 results in no scaling
   * @param b The vertical skewing
   * @param c The horizontal skewing
   * @param d The vertical scaling. A value of 1 results in no scaling
   * @param tx The horizontal translation (moving)
   * @param ty The vertical translation (moving)
   * @param block The lambda callback to issue transform commands
   */
  inline fun inverse(
      a: Float = 1f,
      b: Float = 0f,
      c: Float = 0f,
      d: Float = 1f,
      tx: Float = 0f,
      ty: Float = 0f,
      block: TransformScope.() -> Unit
  ) {
    val scope = TransformScope()
    block(scope)
    children.add(CanvasInverseTransform(CanvasTransform(a, b, c, d, tx, ty, scope.children)))
  }

  /**
   * Postconcats the current transform with the specified translation.
   *
   * M' = T(dx, dy) * M
   *
   * @param dx The horizontal translation
   * @param dy The vertical translation
   */
  inline fun translate(dx: Float = 0f, dy: Float = 0f) {
    children.add(CanvasTranslate(dx, dy))
  }

  /**
   * Postconcats the current transform with the specified scale.
   *
   * M' = S(sx, sy, pivotX, pivotY) * M
   *
   * @param sx The horizontal scale
   * @param sy The vertical scale
   * @param pivot The point around which scale will be applied
   */
  inline fun scale(sx: Float = 1f, sy: Float = 1f, pivot: Point = Point(0f, 0f)) {
    children.add(CanvasScale(sx, sy, pivot))
  }

  /**
   * Postconcats the current transform with the specified rotation.
   *
   * M' = R(degrees, pivotX, pivotY) * M
   *
   * @param degrees The rotation degrees
   * @param pivot The point around which scale will be applied
   */
  inline fun rotate(degrees: Float = 0f, pivot: Point = Point(0f, 0f)) {
    children.add(CanvasRotate(degrees, pivot))
  }

  /**
   * Postconcats the current transform with the specified skewing.
   *
   * M' = K(kx, ky, pivotX, pivotY) * M
   *
   * @param kx The horizontal skewing
   * @param ky The vertical skewing
   * @param pivot The point around which scale will be applied
   */
  inline fun skew(kx: Float = 0f, ky: Float = 0f, pivot: Point = Point(0f, 0f)) {
    children.add(CanvasSkew(kx, ky, pivot))
  }
}


/*
 * 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.litho.widget.collection

import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.litho.Component

@DataClassGenerate
data class CollectionChild(
    val id: Any,
    val component: Component? = null,
    val componentFunction: (() -> Component?)? = null,
    val isSticky: Boolean = false,
    val isFullSpan: Boolean = false,
    val spanSize: Int? = null,
    val deps: Array<Any?>? = null,
    val onNearViewport: OnNearCallback? = 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.litho.widget.collection

import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.ChangesInfo
import com.facebook.litho.sections.Children
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.annotations.GroupSectionSpec
import com.facebook.litho.sections.annotations.OnCreateChildren
import com.facebook.litho.sections.annotations.OnDataBound
import com.facebook.litho.sections.annotations.OnRefresh

@GroupSectionSpec
object CollectionGroupSectionSpec {

  @JvmStatic
  @OnCreateChildren
  fun onCreateChildren(c: SectionContext, @Prop childrenBuilder: Children.Builder): Children =
      childrenBuilder.build()

  @JvmStatic
  @OnDataBound
  fun onDataBound(c: SectionContext, @Prop(optional = true) onDataBound: (() -> Unit)?) {
    onDataBound?.invoke()
  }

  @JvmStatic
  @com.facebook.litho.sections.annotations.OnViewportChanged
  fun onViewportChanged(
      c: SectionContext,
      firstVisibleIndex: Int,
      lastVisibleIndex: Int,
      totalCount: Int,
      firstFullyVisibleIndex: Int,
      lastFullyVisibleIndex: Int,
      @Prop(optional = true) onViewportChanged: OnViewportChanged?
  ) {
    onViewportChanged?.invoke(
        firstVisibleIndex,
        lastVisibleIndex,
        totalCount,
        firstFullyVisibleIndex,
        lastFullyVisibleIndex)
  }

  @JvmStatic
  @OnRefresh
  fun onRefresh(c: SectionContext, @Prop(optional = true) onPullToRefresh: (() -> Unit)?) {
    onPullToRefresh?.invoke()
  }

  @JvmStatic
  @com.facebook.litho.sections.annotations.OnDataRendered
  fun onDataRendered(
      c: SectionContext,
      isDataChanged: Boolean,
      isMounted: Boolean,
      monoTimestampMs: Long,
      firstVisibleIndex: Int,
      lastVisibleIndex: Int,
      changesInfo: ChangesInfo,
      globalOffset: Int,
      @Prop(optional = true) onDataRendered: OnDataRendered?
  ) {
    onDataRendered?.invoke(
        isDataChanged,
        isMounted,
        monoTimestampMs,
        firstVisibleIndex,
        lastVisibleIndex,
        changesInfo,
        globalOffset)
  }
}


/*
 * 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.litho.widget.collection

import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.facebook.litho.sections.widget.GridRecyclerConfiguration
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerBinderConfiguration
import com.facebook.litho.sections.widget.RecyclerConfiguration
import com.facebook.litho.sections.widget.StaggeredGridRecyclerConfiguration
import com.facebook.litho.widget.SnapUtil

/**
 * Provide layout configuration options to a [Collection]
 *
 * @param orientation @see [RecyclerView.Orientation]
 * @param snapMode How contents snaps to position after a scroll @see [SnapUtil.SnapMode]
 * @param reverse Reverse item traversal and layout
 *   order @see [LinearLayoutManager#setReverseLayout]
 */
abstract class CollectionLayout(
    @RecyclerView.Orientation orientation: Int,
    reverse: Boolean,
    rangeRatio: Float? = null,
    useBackgroundChangeSets: Boolean = false,
    isReconciliationEnabled: Boolean = false,
    hasDynamicItemHeight: Boolean = false,
    val canMeasureRecycler: Boolean = false,
    mainAxisWrapContent: Boolean = false,
) {
  internal abstract fun createRecyclerConfigurationBuilder(): RecyclerConfiguration.Builder

  val recyclerConfiguration: RecyclerConfiguration =
      createRecyclerConfigurationBuilder()
          .orientation(orientation)
          .reverseLayout(reverse)
          .recyclerBinderConfiguration(
              RecyclerBinderConfiguration.create()
                  .apply {
                    if (hasDynamicItemHeight) {
                      hasDynamicItemHeight(hasDynamicItemHeight)
                    }
                    rangeRatio?.let { rangeRatio(it) }
                  }
                  .wrapContent(mainAxisWrapContent)
                  .useBackgroundChangeSets(useBackgroundChangeSets)
                  .isReconciliationEnabled(isReconciliationEnabled)
                  .build())
          .build()
}

/**
 * Specifies how a [Collection] will wrap its contents across the cross axis. For example, in a
 * horizontal list, the cross axis is vertical, meaning this enum controls how the Collection will
 * determine its height.
 */
enum class CrossAxisWrapMode(val canMeasureRecycler: Boolean, val hasDynamicItemHeight: Boolean) {
  /** No wrapping specified. The size should be specified on the [Collection]'s style parameter. */
  NoWrap(false, false),

  /** The cross axis dimension will match the first child in the [Collection] */
  MatchFirstChild(true, false),

  /**
   * The cross axis dimension will match the largest item in the [Collection]. Measuring all the
   * children comes with a high performance cost, especially for infinite scrolls. This should only
   * be used if absolutely necessary.
   */
  Dynamic(true, true),
}

/** Provide [CollectionLayout]s that can be applied to [Collection]'s `layout` parameter. */
@PublishedApi
internal object CollectionLayouts {

  /**
   * Provide layout configuration options for a linear [Collection].
   *
   * @param orientation @see CollectionLayout
   * @param snapMode @see CollectionLayout
   * @param reverse @see CollectionLayout
   * @param crossAxisWrapMode Specify how the [Collection] will wrap its contents across the main
   *   axis.
   * @param mainAxisWrapContent If set, the size of the [Collection] along the main axis will match
   *   the size of its children
   */
  fun Linear(
      @RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL,
      @SnapUtil.SnapMode snapMode: Int = SnapUtil.SNAP_NONE,
      reverse: Boolean = false,
      rangeRatio: Float? = null,
      useBackgroundChangeSets: Boolean = false,
      isReconciliationEnabled: Boolean = false,
      crossAxisWrapMode: CrossAxisWrapMode = CrossAxisWrapMode.NoWrap,
      mainAxisWrapContent: Boolean = false,
  ): CollectionLayout =
      object :
          CollectionLayout(
              orientation,
              reverse,
              rangeRatio,
              useBackgroundChangeSets,
              isReconciliationEnabled,
              crossAxisWrapMode.hasDynamicItemHeight,
              crossAxisWrapMode.canMeasureRecycler,
              mainAxisWrapContent,
          ) {
        override fun createRecyclerConfigurationBuilder(): RecyclerConfiguration.Builder =
            ListRecyclerConfiguration.create().snapMode(snapMode)
      }

  /**
   * Provide layout configuration options for a grid [Collection].
   *
   * @param orientation @see CollectionLayout
   * @param snapMode @see CollectionLayout
   * @param reverse @see CollectionLayout
   * @param columns Number of columns in the grid
   */
  fun Grid(
      @RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL,
      @SnapUtil.SnapMode snapMode: Int = SnapUtil.SNAP_NONE,
      reverse: Boolean = false,
      rangeRatio: Float? = null,
      useBackgroundChangeSets: Boolean = false,
      isReconciliationEnabled: Boolean = false,
      columns: Int = 2,
  ): CollectionLayout =
      object :
          CollectionLayout(
              orientation, reverse, rangeRatio, useBackgroundChangeSets, isReconciliationEnabled) {
        override fun createRecyclerConfigurationBuilder(): RecyclerConfiguration.Builder =
            GridRecyclerConfiguration.create().snapMode(snapMode).numColumns(columns)
      }

  /**
   * Provide layout configuration options for a staggered grid [Collection].
   *
   * @param orientation @see CollectionLayout
   * @param snapMode @see CollectionLayout
   * @param reverse @see CollectionLayout
   * @param spans Number of spans in the grid
   * @param gapStrategy @see [StaggeredGridLayoutManager#setGapStrategy]
   */
  fun StaggeredGrid(
      @RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL,
      reverse: Boolean = false,
      rangeRatio: Float? = null,
      useBackgroundChangeSets: Boolean = false,
      isReconciliationEnabled: Boolean = false,
      spans: Int = 2,
      gapStrategy: Int = StaggeredGridLayoutManager.GAP_HANDLING_NONE
  ): CollectionLayout =
      object :
          CollectionLayout(
              orientation, reverse, rangeRatio, useBackgroundChangeSets, isReconciliationEnabled) {
        override fun createRecyclerConfigurationBuilder(): RecyclerConfiguration.Builder =
            StaggeredGridRecyclerConfiguration.create().numSpans(spans).gapStrategy(gapStrategy)
      }
}


/*
 * 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.litho.widget.collection

import androidx.annotation.IdRes
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.EventHandler
import com.facebook.litho.LithoStartupLogger
import com.facebook.litho.StateValue
import com.facebook.litho.TouchEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateInitialState
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnDetached
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.PropDefault
import com.facebook.litho.annotations.ResType
import com.facebook.litho.annotations.State
import com.facebook.litho.sections.Section
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.SectionTree
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerBinderConfiguration
import com.facebook.litho.sections.widget.RecyclerConfiguration
import com.facebook.litho.sections.widget.SectionBinderTarget
import com.facebook.litho.widget.Binder
import com.facebook.litho.widget.LithoRecyclerView.TouchInterceptor
import com.facebook.litho.widget.PTRRefreshEvent
import com.facebook.litho.widget.Recycler
import com.facebook.litho.widget.RecyclerBinder
import com.facebook.litho.widget.RecyclerEventsController
import com.facebook.litho.widget.SectionsRecyclerView.SectionsRecyclerViewLogger
import com.facebook.litho.widget.ViewportInfo.ViewportChanged

@LayoutSpec(events = [PTRRefreshEvent::class])
object CollectionRecyclerSpec {

  @get:PropDefault
  val recyclerConfiguration: RecyclerConfiguration = ListRecyclerConfiguration.create().build()

  @JvmStatic
  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop section: Section?,
      @Prop(optional = true, varArg = "onScrollListener")
      onScrollListeners: List<RecyclerView.OnScrollListener?>?,
      @Prop(optional = true) clipToPadding: Boolean?,
      @Prop(optional = true) clipChildren: Boolean?,
      @Prop(optional = true) nestedScrollingEnabled: Boolean?,
      @Prop(optional = true) scrollBarStyle: Int?,
      @Prop(optional = true) itemDecoration: RecyclerView.ItemDecoration?,
      @Prop(optional = true) itemAnimator: RecyclerView.ItemAnimator?,
      @Prop(optional = true) @IdRes recyclerViewId: Int?,
      @Prop(optional = true) overScrollMode: Int?,
      @Prop(optional = true, resType = ResType.DIMEN_SIZE) startPadding: Int,
      @Prop(optional = true, resType = ResType.DIMEN_SIZE) endPadding: Int,
      @Prop(optional = true, resType = ResType.DIMEN_SIZE) topPadding: Int,
      @Prop(optional = true, resType = ResType.DIMEN_SIZE) bottomPadding: Int,
      @Prop(optional = true) recyclerTouchEventHandler: EventHandler<TouchEvent?>?,
      @Prop(optional = true) horizontalFadingEdgeEnabled: Boolean,
      @Prop(optional = true) verticalFadingEdgeEnabled: Boolean,
      @Prop(optional = true, resType = ResType.DIMEN_SIZE) fadingEdgeLength: Int,
      @Prop(optional = true, resType = ResType.COLOR) refreshProgressBarBackgroundColor: Int?,
      @Prop(optional = true, resType = ResType.COLOR) refreshProgressBarColor: Int?,
      @Prop(optional = true) touchInterceptor: TouchInterceptor?,
      @Prop(optional = true) itemTouchListener: RecyclerView.OnItemTouchListener?,
      @Prop(optional = true) pullToRefreshEnabled: Boolean,
      @Prop(optional = true) recyclerConfiguration: RecyclerConfiguration,
      @Prop(optional = true) sectionsViewLogger: SectionsRecyclerViewLogger?,
      @State internalRecyclerEventsController: RecyclerEventsController?,
      @State binder: Binder<RecyclerView>,
      @State sectionTree: SectionTree
  ): Component {
    sectionTree.setRoot(section)
    val internalPullToRefreshEnabled =
        (recyclerConfiguration.orientation != OrientationHelper.HORIZONTAL && pullToRefreshEnabled)
    return Recycler.create(c)
        .leftPadding(startPadding)
        .rightPadding(endPadding)
        .topPadding(topPadding)
        .bottomPadding(bottomPadding)
        .recyclerEventsController(internalRecyclerEventsController)
        .refreshHandler(if (!pullToRefreshEnabled) null else CollectionRecycler.onRefresh(c))
        .pullToRefresh(internalPullToRefreshEnabled)
        .itemDecoration(itemDecoration)
        .horizontalFadingEdgeEnabled(horizontalFadingEdgeEnabled)
        .verticalFadingEdgeEnabled(verticalFadingEdgeEnabled)
        .fadingEdgeLengthDip(fadingEdgeLength.toFloat())
        .onScrollListeners(onScrollListeners)
        .refreshProgressBarBackgroundColor(refreshProgressBarBackgroundColor)
        .snapHelper(recyclerConfiguration.snapHelper)
        .touchInterceptor(touchInterceptor)
        .onItemTouchListener(itemTouchListener)
        .binder(binder)
        .touchHandler(recyclerTouchEventHandler)
        .sectionsViewLogger(sectionsViewLogger)
        .apply {
          clipToPadding?.let { clipToPadding(it) }
          clipChildren?.let { clipChildren(it) }
          nestedScrollingEnabled?.let { nestedScrollingEnabled(it) }
          scrollBarStyle?.let { scrollBarStyle(it) }
          recyclerViewId?.let { recyclerViewId(it) }
          overScrollMode?.let { overScrollMode(it) }
          refreshProgressBarColor?.let { refreshProgressBarColor(it) }
          itemAnimator?.let { itemAnimator(it) }
        }
        .build()
  }

  @JvmStatic
  @OnCreateInitialState
  fun createInitialState(
      c: ComponentContext,
      sectionTree: StateValue<SectionTree?>,
      binder: StateValue<Binder<RecyclerView>>,
      internalRecyclerEventsController: StateValue<RecyclerEventsController?>,
      @Prop section: Section,
      @Prop(optional = true) recyclerConfiguration: RecyclerConfiguration,
      @Prop(optional = true) sectionTreeTag: String?,
      @Prop(optional = true) canMeasureRecycler: Boolean,
      @Prop(optional = true) startupLogger: LithoStartupLogger?,
      @Prop(optional = true) lazyCollectionController: LazyCollectionController?
  ) {
    val binderConfiguration = recyclerConfiguration.recyclerBinderConfiguration
    val recyclerBinder =
        RecyclerBinder.Builder()
            .layoutInfo(recyclerConfiguration.getLayoutInfo(c))
            .startupLogger(startupLogger)
            .apply {
              with(binderConfiguration) {
                rangeRatio(rangeRatio)
                layoutHandlerFactory(layoutHandlerFactory)
                wrapContent(isWrapContent)
                enableStableIds(enableStableIds)
                invalidStateLogParamsList(invalidStateLogParamsList)
                threadPoolConfig(threadPoolConfiguration)
                hscrollAsyncMode(hScrollAsyncMode)
                isCircular(isCircular)
                hasDynamicItemHeight(hasDynamicItemHeight())
                componentsConfiguration(componentsConfiguration)
                isReconciliationEnabled(isReconciliationEnabled)
                isLayoutDiffingEnabled(isLayoutDiffingEnabled)
                componentWarmer(componentWarmer)
                lithoViewFactory(lithoViewFactory)
                errorEventHandler(errorEventHandler)
                if (estimatedViewportCount != RecyclerBinderConfiguration.Builder.UNSET) {
                  estimatedViewportCount(estimatedViewportCount)
                }
              }
            }
            .build(c)

    val targetBinder =
        SectionBinderTarget(recyclerBinder, binderConfiguration.useBackgroundChangeSets)
    binder.set(targetBinder)
    val sectionTreeInstance =
        SectionTree.create(SectionContext(c), targetBinder)
            .tag(if (sectionTreeTag.isNullOrEmpty()) section.simpleName else sectionTreeTag)
            .changeSetThreadHandler(binderConfiguration.changeSetThreadHandler)
            .postToFrontOfQueueForFirstChangeset(
                binderConfiguration.isPostToFrontOfQueueForFirstChangeset)
            .build()
    sectionTree.set(sectionTreeInstance)
    lazyCollectionController?.sectionTree = sectionTreeInstance
    val recyclerEventsController = RecyclerEventsController()
    lazyCollectionController?.recyclerEventsController = recyclerEventsController
    internalRecyclerEventsController.set(recyclerEventsController)
    val viewPortChanged =
        ViewportChanged {
            firstVisibleIndex,
            lastVisibleIndex,
            firstFullyVisibleIndex,
            lastFullyVisibleIndex,
            state ->
          sectionTreeInstance.viewPortChanged(
              firstVisibleIndex,
              lastVisibleIndex,
              firstFullyVisibleIndex,
              lastFullyVisibleIndex,
              state)
        }
    targetBinder.setViewportChangedListener(viewPortChanged)
    targetBinder.setCanMeasure(canMeasureRecycler)
  }

  @JvmStatic
  @OnEvent(PTRRefreshEvent::class)
  fun onRefresh(c: ComponentContext, @State sectionTree: SectionTree): Boolean {
    val ptrEventHandler = CollectionRecycler.getPTRRefreshEventHandler(c)
    if (ptrEventHandler == null) {
      sectionTree.refresh()
      return true
    }
    val isHandled = CollectionRecycler.dispatchPTRRefreshEvent(ptrEventHandler)
    if (!isHandled) {
      sectionTree.refresh()
    }
    return true
  }

  @JvmStatic
  @OnDetached
  fun onDetached(c: ComponentContext, @State binder: Binder<RecyclerView>): Unit = binder.detach()
}


/*
 * 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.litho.widget.collection

import com.facebook.litho.ComponentContext
import com.facebook.litho.ContainerDsl
import com.facebook.litho.ResourcesScope

/**
 * A receiver for lambdas that create a component within a ContainerDsl. Prevents accidental calls
 * to child()
 *
 * ```
 * LazyList {
 *   child(id = 1, deps = arrayOf) {
 *     child(Text("")) // Prevent this
 *     Text("")
 *   }
 * }
 * ```
 */
@ContainerDsl class ComponentCreationScope(override val context: ComponentContext) : ResourcesScope


/*
 * 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.litho.widget.collection

import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.Dimen
import com.facebook.litho.Handle
import com.facebook.litho.KComponent
import com.facebook.litho.LithoStartupLogger
import com.facebook.litho.Style
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.litho.eventHandlerWithReturn
import com.facebook.litho.kotlinStyle
import com.facebook.litho.sections.ChangesInfo
import com.facebook.litho.sections.Children
import com.facebook.litho.sections.Section
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.OnCheckIsSameContentEvent
import com.facebook.litho.sections.common.OnCheckIsSameItemEvent
import com.facebook.litho.useState
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.LithoRecyclerView
import com.facebook.litho.widget.RecyclerBinder

typealias OnViewportChanged =
    (
        firstVisibleIndex: Int,
        lastVisibleIndex: Int,
        totalCount: Int,
        firstFullyVisibleIndex: Int,
        lastFullyVisibleIndex: Int) -> Unit

typealias OnDataRendered =
    (
        isDataChanged: Boolean,
        isMounted: Boolean,
        monoTimestampMs: Long,
        firstVisibleIndex: Int,
        lastVisibleIndex: Int,
        changesInfo: ChangesInfo,
        globalOffset: Int) -> Unit

class LazyCollection(
    private val layout: CollectionLayout,
    private val itemAnimator: RecyclerView.ItemAnimator? = null,
    private val itemDecoration: RecyclerView.ItemDecoration? = null,
    private val clipToPadding: Boolean? = null,
    private val clipChildren: Boolean? = null,
    private val startPadding: Dimen? = null,
    private val endPadding: Dimen? = null,
    private val topPadding: Dimen? = null,
    private val bottomPadding: Dimen? = null,
    private val nestedScrollingEnabled: Boolean? = null,
    private val scrollBarStyle: Int? = null,
    private val recyclerViewId: Int? = null,
    private val overScrollMode: Int? = null,
    private val refreshProgressBarColor: Int? = null,
    private val touchInterceptor: LithoRecyclerView.TouchInterceptor? = null,
    private val itemTouchListener: RecyclerView.OnItemTouchListener? = null,
    private val sectionTreeTag: String? = null,
    private val startupLogger: LithoStartupLogger? = null,
    private val style: Style? = null,
    private val onViewportChanged: OnViewportChanged? = null,
    private val onDataBound: (() -> Unit)? = null,
    handle: Handle? = null,
    private val onPullToRefresh: (() -> Unit)? = null,
    private val onNearEnd: OnNearCallback? = null,
    private val onScrollListener: RecyclerView.OnScrollListener? = null,
    private val onScrollListeners: List<RecyclerView.OnScrollListener?>? = null,
    private val lazyCollectionController: LazyCollectionController? = null,
    private val onDataRendered: OnDataRendered? = null,
    private val childEquivalenceIncludesCommonProps: Boolean = true,
    private val overlayRenderCount: Boolean = false,
    private val alwaysDetectDuplicates: Boolean = false,
    private val lazyCollectionChildren: LazyCollectionChildren
) : KComponent() {

  // There's a conflict with Component.handle, so use a different name
  private val recyclerHandle: Handle? = handle

  override fun ComponentScope.render(): Component {
    val sectionContext = SectionContext(context)
    val childTracker = useState { ChildVisibilityTracker() }.value

    val combinedOnViewportChanged: OnViewportChanged =
        {
            firstVisibleIndex,
            lastVisibleIndex,
            totalCount,
            firstFullyVisibleIndex,
            lastFullyVisibleIndex ->
          childTracker.onScrollOrUpdated(
              lazyCollectionChildren.effectiveIndexToId,
              lazyCollectionChildren.idToChild,
              firstVisibleIndex,
              lastVisibleIndex)
          onNearEnd?.let {
            if (lastVisibleIndex >= totalCount - 1 - it.offset) {
              it.callback()
            }
          }
          onViewportChanged?.invoke(
              firstVisibleIndex,
              lastVisibleIndex,
              totalCount,
              firstFullyVisibleIndex,
              lastFullyVisibleIndex)
        }

    val combinedOnDataRendered: OnDataRendered =
        {
            isDataChanged: Boolean,
            isMounted: Boolean,
            monoTimestampMs: Long,
            firstVisibleIndex: Int,
            lastVisibleIndex: Int,
            changesInfo: ChangesInfo,
            globalOffset: Int ->
          childTracker.onScrollOrUpdated(
              lazyCollectionChildren.effectiveIndexToId,
              lazyCollectionChildren.idToChild,
              firstVisibleIndex,
              lastVisibleIndex)
          onDataRendered?.invoke(
              isDataChanged,
              isMounted,
              monoTimestampMs,
              firstVisibleIndex,
              lastVisibleIndex,
              changesInfo,
              globalOffset)
        }

    val section =
        CollectionGroupSection.create(sectionContext)
            .childrenBuilder(
                Children.create()
                    .child(
                        createDataDiffSection(
                            sectionContext,
                            lazyCollectionChildren.collectionChildren,
                            alwaysDetectDuplicates)))
            .apply { onDataBound?.let { onDataBound(it) } }
            .onViewportChanged(combinedOnViewportChanged)
            .onPullToRefresh(onPullToRefresh)
            .onDataRendered(combinedOnDataRendered)
            .build()

    return CollectionRecycler.create(context)
        .section(section)
        .recyclerConfiguration(layout.recyclerConfiguration)
        .itemAnimator(itemAnimator)
        .itemDecoration(itemDecoration)
        .canMeasureRecycler(layout.canMeasureRecycler)
        .clipToPadding(clipToPadding)
        .clipChildren(clipChildren)
        .startPaddingPx(startPadding?.toPixels(resourceResolver) ?: 0)
        .endPaddingPx(endPadding?.toPixels(resourceResolver) ?: 0)
        .topPaddingPx(topPadding?.toPixels(resourceResolver) ?: 0)
        .bottomPaddingPx(bottomPadding?.toPixels(resourceResolver) ?: 0)
        .pullToRefreshEnabled(onPullToRefresh != null)
        .nestedScrollingEnabled(nestedScrollingEnabled)
        .scrollBarStyle(scrollBarStyle)
        .recyclerViewId(recyclerViewId)
        .overScrollMode(overScrollMode)
        .refreshProgressBarColor(refreshProgressBarColor)
        .touchInterceptor(touchInterceptor)
        .itemTouchListener(itemTouchListener)
        .sectionTreeTag(sectionTreeTag)
        .startupLogger(startupLogger)
        .handle(recyclerHandle)
        .onScrollListener(onScrollListener)
        .onScrollListeners(onScrollListeners)
        .lazyCollectionController(lazyCollectionController)
        .kotlinStyle(style)
        .build()
  }

  private fun createDataDiffSection(
      sectionContext: SectionContext,
      children: List<CollectionChild>,
      alwaysDetectDuplicates: Boolean,
  ): Section {
    return DataDiffSection.create<CollectionChild>(sectionContext)
        .alwaysDetectDuplicates(alwaysDetectDuplicates)
        .data(children)
        .renderEventHandler(
            eventHandlerWithReturn { renderEvent ->
              val item = renderEvent.model
              val component =
                  item.component
                      ?: item.componentFunction?.invoke() ?: return@eventHandlerWithReturn null
              ComponentRenderInfo.create()
                  .apply {
                    if (item.isSticky) {
                      isSticky(item.isSticky)
                    }
                    if (item.isFullSpan) {
                      isFullSpan(item.isFullSpan)
                    }
                    item.spanSize?.let { spanSize(it) }
                    customAttribute(RecyclerBinder.ID_CUSTOM_ATTR_KEY, item.id)
                  }
                  .component(
                      if (ComponentsConfiguration.isDebugModeEnabled && overlayRenderCount)
                          component.overlayRenderCount
                      else component)
                  .build()
            })
        .onCheckIsSameItemEventHandler(eventHandlerWithReturn(::isSameID))
        .onCheckIsSameContentEventHandler(eventHandlerWithReturn(::isChildEquivalent))
        .build()
  }

  private fun isSameID(event: OnCheckIsSameItemEvent<CollectionChild>): Boolean {
    return event.previousItem.id == event.nextItem.id
  }

  private fun componentsEquivalent(first: Component?, second: Component?): Boolean {
    if (first == null && second == null) return true
    return first?.isEquivalentTo(second, childEquivalenceIncludesCommonProps) == true
  }

  private fun isChildEquivalent(event: OnCheckIsSameContentEvent<CollectionChild>): Boolean =
      isChildEquivalent(event.previousItem, event.nextItem)

  fun isChildEquivalent(previous: CollectionChild, next: CollectionChild): Boolean {
    if (previous.deps != null || next.deps != null) {
      return previous.deps?.contentDeepEquals(next.deps) == true
    }

    return componentsEquivalent(previous.component, next.component)
  }
}

/**
 * Track which children are visible after an update or scroll, and trigger any necessary callbacks.
 */
private class ChildVisibilityTracker {

  private var previouslyVisibleIds: Set<Any> = setOf()

  fun onScrollOrUpdated(
      effectiveIndexToId: Map<Int, MutableSet<Any>>,
      idToChild: Map<Any, CollectionChild>,
      firstVisibleIndex: Int,
      lastVisibleIndex: Int
  ) {
    val visibleIds =
        mutableSetOf<Any>()
            .apply {
              (firstVisibleIndex..lastVisibleIndex).forEach { visibleIndex ->
                effectiveIndexToId[visibleIndex]?.let { ids -> addAll(ids) }
              }
            }
            .toSet()

    val enteredIds = visibleIds - previouslyVisibleIds
    enteredIds.forEach { id -> idToChild[id]?.onNearViewport?.callback?.invoke() }

    previouslyVisibleIds = visibleIds
  }
}


/*
 * 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.litho.widget.collection

import com.facebook.litho.Component

/**
 * A container for the children of a Lazy Collection. Manages the creation of ids, and callback
 * registration.
 */
class LazyCollectionChildren {

  val collectionChildren
    get() = collectionChildrenBuilder.toList()

  internal val effectiveIndexToId: MutableMap<Int, MutableSet<Any>> by lazy { mutableMapOf() }
  internal val idToChild: MutableMap<Any, CollectionChild> by lazy { mutableMapOf() }

  private val collectionChildrenBuilder = mutableListOf<CollectionChild>()
  private var nextStaticId = 0
  private val typeToFreq: MutableMap<Int, Int> by lazy { mutableMapOf() }

  /** Prepare the final id that will be assigned to the child. */
  private fun getResolvedId(id: Any?, component: Component? = null): Any {
    // Generate an id that is unique to the [CollectionContainerScope] in which it was defined
    // If an id has been explicitly defined on the child, use that
    // If the child has a component generate an id including the type and frequency
    // Otherwise the child has a null component or a lambda generator, so generate an id.
    return id ?: generateIdForComponent(component) ?: generateStaticId()
  }

  private fun generateStaticId(): Any {
    return "staticId:${nextStaticId++}"
  }

  /** Generate an id for a non-null Component. */
  private fun generateIdForComponent(component: Component?): Any? {
    val typeId = component?.typeId ?: return null
    typeToFreq[typeId] = (typeToFreq[typeId] ?: 0) + 1
    return "${typeId}:${typeToFreq[typeId]}"
  }

  private fun maybeRegisterForOnEnterOrNearCallbacks(child: CollectionChild) {
    child.onNearViewport ?: return

    idToChild[child.id] = child
    val index = collectionChildrenBuilder.lastIndex
    val effectiveIndex = index - child.onNearViewport.offset
    effectiveIndexToId.getOrPut(effectiveIndex) { mutableSetOf() }.add(child.id)
  }

  fun add(
      component: Component?,
      id: Any? = null,
      isSticky: Boolean = false,
      isFullSpan: Boolean = false,
      spanSize: Int? = null,
      onNearViewport: OnNearCallback? = null,
  ) {
    val resolvedId = getResolvedId(id, component)
    component ?: return
    val child =
        CollectionChild(
            resolvedId,
            component,
            null,
            isSticky,
            isFullSpan,
            spanSize,
            null,
            onNearViewport,
        )
    collectionChildrenBuilder.add(child)
    maybeRegisterForOnEnterOrNearCallbacks(child)
  }

  fun add(
      id: Any? = null,
      isSticky: Boolean = false,
      isFullSpan: Boolean = false,
      spanSize: Int? = null,
      onNearViewport: OnNearCallback? = null,
      deps: Array<Any?>,
      componentFunction: () -> Component?,
  ) {
    val child =
        CollectionChild(
            getResolvedId(id),
            null,
            { componentFunction() },
            isSticky,
            isFullSpan,
            spanSize,
            deps,
            onNearViewport,
        )
    collectionChildrenBuilder.add(child)
    maybeRegisterForOnEnterOrNearCallbacks(child)
  }
}


/*
 * 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.litho.widget.collection

import androidx.annotation.Px
import androidx.annotation.UiThread
import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Dimen
import com.facebook.litho.ResourcesScope
import com.facebook.litho.px
import com.facebook.litho.sections.SectionTree
import com.facebook.litho.widget.RecyclerEventsController
import com.facebook.litho.widget.SmoothScrollAlignmentType

/**
 * A controller that can be set on a [LazyCollection] to trigger external events. Most calls should
 * be made on the main thread e.g. within a `Style.onClick { }` callback.
 */
class LazyCollectionController {

  internal var sectionTree: SectionTree? = null
  internal var recyclerEventsController: RecyclerEventsController? = null

  /** Request a call back when a [RecyclerView] is bound or unbound. */
  fun setRecyclerUpdate(listener: (RecyclerView?) -> Unit) {
    recyclerEventsController?.setOnRecyclerUpdateListener(listener)
  }

  /**
   * Retrieve a reference to the RecyclerView. This should be avoided if possible, but may be
   * necessary e.g. for custom animations.
   */
  val recyclerView: RecyclerView?
    @UiThread get() = recyclerEventsController?.recyclerView

  /**
   * Toggle the refresh indicator based off a boolean. It is not necessary to call this within the
   * [LazyCollection]'s `onPullToRefresh` callback as it will be triggered automatically.
   */
  @UiThread
  fun setRefreshing(isRefreshing: Boolean) {
    if (isRefreshing) {
      recyclerEventsController?.showRefreshing()
    } else {
      recyclerEventsController?.clearRefreshing()
    }
  }

  /**
   * Scroll the [LazyCollection] by a given number of pixels. The Scroll is instant. For an animated
   * scroll use [smoothScrollBy].
   *
   * @param dx Pixel distance to scroll along the x axis
   * @param dy Pixel distance to scroll along the y axis
   */
  @UiThread
  fun scrollBy(@Px dx: Int, @Px dy: Int) {
    recyclerEventsController?.recyclerView?.scrollBy(dx, dy)
  }

  /**
   * Perform and animated scroll on the [LazyCollection] by a given number of pixels. For an
   * instantaneous scroll use [scrollBy].
   *
   * @param dx Pixel distance to scroll along the x axis
   * @param dy Pixel distance to scroll along the y axis
   */
  @UiThread
  fun smoothScrollBy(@Px dx: Int, @Px dy: Int) {
    recyclerEventsController?.recyclerView?.smoothScrollBy(dx, dy)
  }

  /**
   * Scroll the [LazyCollection] so that the child at the given index is fully visible. For an
   * animated scroll use [smoothScrollToIndex].
   *
   * @param index The index of the child to scroll to
   * @param offset Attempt to offset the child by this number of pixels from the start of the
   *   Collection.
   */
  @UiThread
  fun scrollToIndex(index: Int, @Px offset: Int = 0) {
    sectionTree?.requestFocusOnRoot(index, offset)
  }

  /**
   * Perform an animated scroll on the [LazyCollection] so that the child at the given index is
   * fully visible. For an instantaneous scroll use [scrollToIndex].
   *
   * @param index The index of the child to scroll to
   * @param offset Attempt to offset the child by this number of pixels from the start of the
   *   Collection.
   * @param smoothScrollAlignmentType Attempt to position the child based on this alignment type.
   */
  @UiThread
  fun smoothScrollToIndex(
      index: Int,
      @Px offset: Int = 0,
      smoothScrollAlignmentType: SmoothScrollAlignmentType? = SmoothScrollAlignmentType.DEFAULT,
  ) {
    sectionTree?.requestSmoothFocusOnRoot(index, offset, smoothScrollAlignmentType)
  }

  /**
   * Scroll the [LazyCollection] so that the child with the given id is fully visible. For an
   * animated scroll use [smoothScrollToId].
   *
   * @param id The id of the child to scroll to
   * @param offset Attempt to offset the child by this number of pixels from the start of the
   *   Collection.
   */
  @UiThread
  fun scrollToId(
      id: Any,
      @Px offset: Int = 0,
  ) {
    sectionTree?.requestFocusOnRoot(id, offset)
  }

  /**
   * Perform an animated scroll on the [LazyCollection] so that the child with the given id is fully
   * visible. For an animated scroll use [smoothScrollToId].
   *
   * @param id The id of the child to scroll to
   * @param offset Attempt to offset the child by this number of pixels from the start of the
   *   Collection.
   * @param smoothScrollAlignmentType Attempt to position the child based on this alignment type.
   */
  @UiThread
  fun smoothScrollToId(
      id: Any,
      @Px offset: Int = 0,
      smoothScrollAlignmentType: SmoothScrollAlignmentType? = SmoothScrollAlignmentType.DEFAULT,
  ) {
    sectionTree?.requestSmoothFocusOnRoot(id, offset, smoothScrollAlignmentType)
  }
}

/**
 * Scroll the [LazyCollection] by a given number of pixels. The Scroll is instant. For an animated
 * scroll use [smoothScrollBy].
 *
 * @param controller The controller for the [LazyCollection] being scrolled.
 * @param dx Distance to scroll along the x axis
 * @param dy Distance to scroll along the y axis
 */
@UiThread
fun ResourcesScope.scrollBy(controller: LazyCollectionController, dx: Dimen, dy: Dimen) =
    controller.scrollBy(dx.toPixels(), dy.toPixels())

/**
 * Perform and animated scroll on the [LazyCollection] by a given number of pixels. For an
 * instantaneous scroll use [scrollBy].
 *
 * @param controller The controller for the [LazyCollection] being scrolled.
 * @param dx Distance to scroll along the x axis
 * @param dy Distance to scroll along the y axis
 */
@UiThread
fun ResourcesScope.smoothScrollBy(controller: LazyCollectionController, dx: Dimen, dy: Dimen) =
    controller.smoothScrollBy(dx.toPixels(), dy.toPixels())

/**
 * Scroll the [LazyCollection] so that the child at the given index is fully visible. For an
 * animated scroll use [smoothScrollToIndex].
 *
 * @param controller The controller for the [LazyCollection] being scrolled.
 * @param index The index of the child to scroll to
 * @param offset Attempt to offset the child by this distance from the start of the Collection.
 */
@UiThread
fun ResourcesScope.scrollToIndex(
    controller: LazyCollectionController,
    index: Int,
    offset: Dimen = 0.px
) = controller.scrollToIndex(index, offset.toPixels())

/**
 * Perform an animated scroll on the [LazyCollection] so that the child at the given index is fully
 * visible. For an instantaneous scroll use [scrollToIndex].
 *
 * @param controller The controller for the [LazyCollection] being scrolled.
 * @param index The index of the child to scroll to
 * @param offset Attempt to offset the child by this distance from the start of the Collection.
 * @param smoothScrollAlignmentType Attempt to position the child based on this alignment type.
 */
@UiThread
fun ResourcesScope.smoothScrollToIndex(
    controller: LazyCollectionController,
    index: Int,
    offset: Dimen = 0.px,
    smoothScrollAlignmentType: SmoothScrollAlignmentType? = SmoothScrollAlignmentType.DEFAULT,
) = controller.smoothScrollToIndex(index, offset.toPixels(), smoothScrollAlignmentType)

/**
 * Scroll the [LazyCollection] so that the child with the given id is fully visible. For an animated
 * scroll use [smoothScrollToId].
 *
 * @param controller The controller for the [LazyCollection] being scrolled.
 * @param id The id of the child to scroll to
 * @param offset Attempt to offset the child by this distance from the start of the Collection.
 */
@UiThread
fun ResourcesScope.scrollToId(
    controller: LazyCollectionController,
    id: Any,
    offset: Dimen = 0.px,
) = controller.scrollToId(id, offset.toPixels())

/**
 * Perform an animated scroll on the [LazyCollection] so that the child with the given id is fully
 * visible. For an animated scroll use [smoothScrollToId].
 *
 * @param controller The controller for the [LazyCollection] being scrolled.
 * @param id The id of the child to scroll to
 * @param offset Attempt to offset the child this distance from the start of the Collection.
 * @param smoothScrollAlignmentType Attempt to position the child based on this alignment type.
 */
@UiThread
fun ResourcesScope.smoothScrollToId(
    controller: LazyCollectionController,
    id: Any,
    offset: Dimen = 0.px,
    smoothScrollAlignmentType: SmoothScrollAlignmentType? = SmoothScrollAlignmentType.DEFAULT,
) = controller.smoothScrollToId(id, offset.toPixels(), smoothScrollAlignmentType)


/*
 * 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.litho.widget.collection

import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Component
import com.facebook.litho.Dimen
import com.facebook.litho.Handle
import com.facebook.litho.LithoStartupLogger
import com.facebook.litho.ResourcesScope
import com.facebook.litho.Style
import com.facebook.litho.widget.LithoRecyclerView
import com.facebook.litho.widget.SnapUtil

/** A scrollable collection of components arranged in a grid */
@Suppress("FunctionName")
inline fun ResourcesScope.LazyGrid(
    @RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL,
    @SnapUtil.SnapMode snapMode: Int = SnapUtil.SNAP_NONE,
    reverse: Boolean = false,
    columns: Int = 2,
    itemAnimator: RecyclerView.ItemAnimator? = null,
    itemDecoration: RecyclerView.ItemDecoration? = null,
    clipToPadding: Boolean? = null,
    clipChildren: Boolean? = null,
    startPadding: Dimen? = null,
    endPadding: Dimen? = null,
    topPadding: Dimen? = null,
    bottomPadding: Dimen? = null,
    nestedScrollingEnabled: Boolean? = null,
    scrollBarStyle: Int? = null,
    recyclerViewId: Int? = null,
    overScrollMode: Int? = null,
    refreshProgressBarColor: Int? = null,
    touchInterceptor: LithoRecyclerView.TouchInterceptor? = null,
    itemTouchListener: RecyclerView.OnItemTouchListener? = null,
    sectionTreeTag: String? = null,
    startupLogger: LithoStartupLogger? = null,
    style: Style? = null,
    noinline onViewportChanged: OnViewportChanged? = null,
    noinline onDataBound: (() -> Unit)? = null,
    handle: Handle? = null,
    noinline onPullToRefresh: (() -> Unit)? = null,
    onNearEnd: OnNearCallback? = null,
    onScrollListener: RecyclerView.OnScrollListener? = null,
    onScrollListeners: List<RecyclerView.OnScrollListener?>? = null,
    lazyCollectionController: LazyCollectionController? = null,
    noinline onDataRendered: OnDataRendered? = null,
    rangeRatio: Float? = null,
    useBackgroundChangeSets: Boolean = false,
    isReconciliationEnabled: Boolean = false,
    childEquivalenceIncludesCommonProps: Boolean = true,
    overlayRenderCount: Boolean = false,
    alwaysDetectDuplicates: Boolean = false,
    init: LazyGridScope.() -> Unit
): Component {
  val lazyGridScope = LazyGridScope(context).apply { init() }
  return LazyCollection(
      layout =
          CollectionLayouts.Grid(
              orientation,
              snapMode,
              reverse,
              rangeRatio,
              useBackgroundChangeSets,
              isReconciliationEnabled,
              columns),
      itemAnimator,
      itemDecoration,
      clipToPadding,
      clipChildren,
      startPadding,
      endPadding,
      topPadding,
      bottomPadding,
      nestedScrollingEnabled,
      scrollBarStyle,
      recyclerViewId,
      overScrollMode,
      refreshProgressBarColor,
      touchInterceptor,
      itemTouchListener,
      sectionTreeTag,
      startupLogger,
      style,
      onViewportChanged,
      onDataBound,
      handle,
      onPullToRefresh,
      onNearEnd,
      onScrollListener,
      onScrollListeners,
      lazyCollectionController,
      onDataRendered,
      childEquivalenceIncludesCommonProps,
      overlayRenderCount,
      alwaysDetectDuplicates,
      lazyGridScope.children)
}


/*
 * 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.litho.widget.collection

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.ContainerDsl
import com.facebook.litho.ResourcesScope

@ContainerDsl
class LazyGridScope(override val context: ComponentContext) : ResourcesScope {

  val children = LazyCollectionChildren()

  /**
   * Add a child [Component] to the collection.
   *
   * @param component The component to add
   * @param id A unique identifier for the child
   * @param isSticky Fix the child to the top of the collection if it is scrolled out of view
   * @param isFullSpan Span the child across all columns
   * @param spanSize Span the specified number of columns
   * @param onNearViewport A callback that will be invoked when the child is close to or enters the
   *   visible area.
   */
  fun child(
      component: Component?,
      id: Any? = null,
      isSticky: Boolean = false,
      isFullSpan: Boolean = false,
      spanSize: Int? = null,
      onNearViewport: OnNearCallback? = null,
  ) = children.add(component, id, isSticky, isFullSpan, spanSize, onNearViewport)

  /**
   * Add a child [Component] created by the provided [componentFunction] function.
   *
   * The [Component] will be created by invoking [componentFunction] when the child is first
   * rendered. The [Component] will be reused in subsequent renders unless there is a change to
   * [deps]. [deps] is an array of dependencies that should contain any props or state that are used
   * inside [componentFunction].
   *
   * @param id A unique identifier for the child
   * @param isSticky Fix the child to the top of the collection if it is scrolled out of view
   * @param isFullSpan Span the child across all columns
   * @param spanSize Span the specified number of columns
   * @param onNearViewport A callback that will be invoked when the child is close to or enters the
   *   visible area.
   * @param deps An array of prop and state values used by [componentFunction] to create the
   *   [Component]. A change to one of these values will cause [componentFunction] to recreate the
   *   [Component].
   * @param componentFunction A function that returns a [Component]
   */
  fun child(
      id: Any? = null,
      isSticky: Boolean = false,
      isFullSpan: Boolean = false,
      spanSize: Int? = null,
      onNearViewport: OnNearCallback? = null,
      deps: Array<Any?>,
      componentFunction: ComponentCreationScope.() -> Component?
  ) =
      children.add(id, isSticky, isFullSpan, spanSize, onNearViewport, deps) {
        ComponentCreationScope(context).componentFunction()
      }

  /**
   * Add a list of children generated by applying [componentFunction] to each item in a list of
   * models.
   *
   * @param items Data models to be rendered as children
   * @param id A function to create a unique id from each data model
   * @param componentFunction A function that generates a [Component] from a data model
   */
  fun <T> children(
      items: Iterable<T>,
      id: (T) -> Any,
      componentFunction: ComponentCreationScope.(T) -> Component?,
  ) {
    val componentCreationScope = ComponentCreationScope(context)
    items.forEach { item ->
      children.add(id = id(item), component = componentCreationScope.componentFunction(item))
    }
  }

  /**
   * Add a list of children generated by applying [componentFunction] to each item in a list of
   * models.
   *
   * @param items Data models to be rendered as children
   * @param id A function to create a unique id from each data model
   * @param deps A function to create a list of deps from each data model
   * @param componentFunction A function that generates a [Component] from a data model
   */
  fun <T> children(
      items: List<T>,
      id: (T) -> Any,
      deps: (T) -> Array<Any?>,
      componentFunction: ComponentCreationScope.(T) -> Component?,
  ) {
    val componentCreationScope = ComponentCreationScope(context)
    items.forEach { item ->
      children.add(
          id = id(item),
          deps = deps(item),
          componentFunction = { componentCreationScope.componentFunction(item) })
    }
  }
}


/*
 * 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.litho.widget.collection

import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Component
import com.facebook.litho.Dimen
import com.facebook.litho.Handle
import com.facebook.litho.LithoStartupLogger
import com.facebook.litho.ResourcesScope
import com.facebook.litho.Style
import com.facebook.litho.widget.LithoRecyclerView
import com.facebook.litho.widget.SnapUtil

/** A scrollable collection of components arranged linearly */
@Suppress("FunctionName")
inline fun ResourcesScope.LazyList(
    @RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL,
    @SnapUtil.SnapMode snapMode: Int = SnapUtil.SNAP_NONE,
    reverse: Boolean = false,
    crossAxisWrapMode: CrossAxisWrapMode = CrossAxisWrapMode.NoWrap,
    mainAxisWrapContent: Boolean = false,
    itemAnimator: RecyclerView.ItemAnimator? = null,
    itemDecoration: RecyclerView.ItemDecoration? = null,
    clipToPadding: Boolean? = null,
    clipChildren: Boolean? = null,
    startPadding: Dimen? = null,
    endPadding: Dimen? = null,
    topPadding: Dimen? = null,
    bottomPadding: Dimen? = null,
    nestedScrollingEnabled: Boolean? = null,
    scrollBarStyle: Int? = null,
    recyclerViewId: Int? = null,
    overScrollMode: Int? = null,
    refreshProgressBarColor: Int? = null,
    touchInterceptor: LithoRecyclerView.TouchInterceptor? = null,
    itemTouchListener: RecyclerView.OnItemTouchListener? = null,
    sectionTreeTag: String? = null,
    startupLogger: LithoStartupLogger? = null,
    style: Style? = null,
    noinline onViewportChanged: OnViewportChanged? = null,
    noinline onDataBound: (() -> Unit)? = null,
    handle: Handle? = null,
    noinline onPullToRefresh: (() -> Unit)? = null,
    onNearEnd: OnNearCallback? = null,
    onScrollListener: RecyclerView.OnScrollListener? = null,
    onScrollListeners: List<RecyclerView.OnScrollListener?>? = null,
    lazyCollectionController: LazyCollectionController? = null,
    noinline onDataRendered: OnDataRendered? = null,
    rangeRatio: Float? = null,
    useBackgroundChangeSets: Boolean = false,
    isReconciliationEnabled: Boolean = false,
    childEquivalenceIncludesCommonProps: Boolean = true,
    overlayRenderCount: Boolean = false,
    alwaysDetectDuplicates: Boolean = false,
    init: LazyListScope.() -> Unit
): Component {

  val lazyListScope = LazyListScope(context).apply { init() }
  return LazyCollection(
      layout =
          CollectionLayouts.Linear(
              orientation,
              snapMode,
              reverse,
              rangeRatio,
              useBackgroundChangeSets,
              isReconciliationEnabled,
              crossAxisWrapMode,
              mainAxisWrapContent),
      itemAnimator,
      itemDecoration,
      clipToPadding,
      clipChildren,
      startPadding,
      endPadding,
      topPadding,
      bottomPadding,
      nestedScrollingEnabled,
      scrollBarStyle,
      recyclerViewId,
      overScrollMode,
      refreshProgressBarColor,
      touchInterceptor,
      itemTouchListener,
      sectionTreeTag,
      startupLogger,
      style,
      onViewportChanged,
      onDataBound,
      handle,
      onPullToRefresh,
      onNearEnd,
      onScrollListener,
      onScrollListeners,
      lazyCollectionController,
      onDataRendered,
      childEquivalenceIncludesCommonProps,
      overlayRenderCount,
      alwaysDetectDuplicates,
      lazyListScope.children)
}