/*
 * 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 LazyListScope(override val context: ComponentContext) : ResourcesScope {
  val children = LazyCollectionChildren()

  /**
   * Add a child [Component] to the Lazy 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 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,
      onNearViewport: OnNearCallback? = null,
  ) = children.add(component, id, isSticky, false, null, onNearViewport)

  /**
   * Add a child [Component] created by the provided [componentFunction] function to the Lazy
   * Collection.
   *
   * 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 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,
      onNearViewport: OnNearCallback? = null,
      deps: Array<Any?>,
      componentFunction: ComponentCreationScope.() -> Component?,
  ) =
      children.add(id, isSticky, false, null, 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: Iterable<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 androidx.recyclerview.widget.StaggeredGridLayoutManager
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 staggered grid */
@Suppress("FunctionName")
inline fun ResourcesScope.LazyStaggeredGrid(
    @RecyclerView.Orientation orientation: Int = RecyclerView.VERTICAL,
    @SnapUtil.SnapMode snapMode: Int = SnapUtil.SNAP_NONE,
    reverse: Boolean = false,
    spans: Int = 2,
    gapStrategy: Int = StaggeredGridLayoutManager.GAP_HANDLING_NONE,
    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 lazyStaggeredGridScope = LazyGridScope(context).apply { init() }
  return LazyCollection(
      layout =
          CollectionLayouts.StaggeredGrid(
              orientation,
              reverse,
              rangeRatio,
              useBackgroundChangeSets,
              isReconciliationEnabled,
              spans,
              gapStrategy),
      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,
      lazyStaggeredGridScope.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

/**
 * A callback triggered when a [LazyCollection] is scrolled "near" to a specific position, as
 * defined by being within [offset] items away.
 *
 * Intended for use with [LazyCollection]'s `onNearEnd` parameter, or child's `onNearViewport`
 * parameter.
 */
class OnNearCallback(val offset: Int = 0, val callback: () -> Unit)


/*
 * 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 android.graphics.Color
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.core.padding
import com.facebook.litho.dp
import com.facebook.litho.flexbox.position
import com.facebook.litho.flexbox.positionType
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.px
import com.facebook.litho.sp
import com.facebook.litho.useEffect
import com.facebook.litho.useRef
import com.facebook.litho.view.alpha
import com.facebook.litho.view.backgroundColor
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaPositionType

/** A component that overlays a render count on top of its children. */
class OverlayRenderCount(val component: Component) : KComponent() {
  override fun ComponentScope.render(): Component {
    val renderCount = useRef { 1 }
    useEffect(Any()) {
      renderCount.value++
      null
    }
    return Column {
      child(component)
      child(
          Column(
              style =
                  Style.positionType(YogaPositionType.ABSOLUTE)
                      .position(bottom = 0.px, end = 0.px)) {
                child(
                    Text(
                        text = "renderCount = ${renderCount.value}",
                        textSize = 8.sp,
                        textColor = Color.WHITE,
                        backgroundColor = Color.DKGRAY,
                        style = Style.alpha(.8f).padding(horizontal = 8.dp)))
              })
    }
  }
}

val Component?.overlayRenderCount: Component?
  get() = this?.let { OverlayRenderCount(it) }


/*
 * 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 android.graphics.Rect
import android.view.View
import androidx.annotation.Px
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.litho.Dimen
import com.facebook.litho.ResourcesScope

/**
 * An `ItemDecoration` to add spacing around list items in a [Collection]. It is intended for use
 * with linear collections (which is the default layout, see [CollectionLayouts.Linear]), and wont
 * work with grids or staggered grids.
 *
 * Features include:
 * 1. equals() implementation (it's a data class) means it will pass prop isEquivalent checks
 *    avoiding unnecessary re-renders.
 * 2. Respects orientation and layout direction.
 * 3. Specify start/end/between spacing separately.
 *
 * Example usage:
 * ```
 * Collection(
 *   itemDecoration = LinearSpacing(all = 10.dp)
 * ) {}
 * ```
 *
 * @param all Spacing between all items and to the start and end item. If other values are set they
 *   will override this value.
 * @param between Spacing between all items
 * @param start Spacing at the front of the first item
 * @param end Spacing at the end of the last item
 */
fun ResourcesScope.LinearSpacing(
    all: Dimen? = null,
    between: Dimen? = null,
    start: Dimen? = null,
    end: Dimen? = null,
): RecyclerView.ItemDecoration =
    LinearSpacingItemDecoration(
        all?.toPixels(), between?.toPixels(), start?.toPixels(), end?.toPixels())

@DataClassGenerate
data class LinearSpacingItemDecoration(
    @Px val all: Int? = null,
    @Px val between: Int? = null,
    @Px val start: Int? = null,
    @Px val end: Int? = null,
) : RecyclerView.ItemDecoration() {
  override fun getItemOffsets(
      outRect: Rect,
      view: View,
      parent: RecyclerView,
      state: RecyclerView.State
  ) {
    val layoutManager = parent.layoutManager as? LinearLayoutManager ?: return
    val isHorizontal = layoutManager.orientation == RecyclerView.HORIZONTAL
    val isRTL = isHorizontal && layoutManager.layoutDirection == View.LAYOUT_DIRECTION_RTL
    val isReverseLayout = layoutManager.reverseLayout
    val isReversed = isRTL xor isReverseLayout

    @Px val resolvedStart = start ?: all ?: 0
    @Px val resolvedEnd = end ?: all ?: 0
    @Px val resolvedBetween = between ?: all ?: 0

    val childAdapterPosition = parent.getChildAdapterPosition(view)
    if (childAdapterPosition == RecyclerView.NO_POSITION) return
    val isFirstItem = childAdapterPosition == 0
    val isLastItem = childAdapterPosition == parent.adapter?.itemCount?.minus(1) ?: -1
    with(outRect) {
      if (isFirstItem) {
        if (isReversed) {
          right = if (isHorizontal) resolvedStart else 0
          bottom = if (isHorizontal) 0 else resolvedStart
        } else {
          left = if (isHorizontal) resolvedStart else 0
          top = if (isHorizontal) 0 else resolvedStart
        }
      }
      if (isLastItem) {
        if (isReversed) {
          left = if (isHorizontal) resolvedEnd else 0
          top = if (isHorizontal) 0 else resolvedEnd
        } else {
          right = if (isHorizontal) resolvedEnd else 0
          bottom = if (isHorizontal) 0 else resolvedEnd
        }
      } else {
        if (isReversed) {
          left = if (isHorizontal) resolvedBetween else 0
          top = if (isHorizontal) 0 else resolvedBetween
        } else {
          right = if (isHorizontal) resolvedBetween else 0
          bottom = if (isHorizontal) 0 else resolvedBetween
        }
      }
    }
  }
}


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

import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.widget.TextView
import com.facebook.litho.Column
import com.facebook.litho.FixedSizeLayoutBehavior
import com.facebook.litho.LithoPrimitive
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.PrimitiveComponent
import com.facebook.litho.PrimitiveComponentScope
import com.facebook.litho.SimpleMountable
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.px
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.TrackedItemPool
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.rendercore.MeasureResult
import com.facebook.rendercore.MountItemsPool
import com.facebook.rendercore.MountItemsPool.getMountItemPools
import com.facebook.rendercore.primitives.DrawableAllocator
import com.facebook.rendercore.primitives.ViewAllocator
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class ContentPoolingTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Before
  fun setup() {
    MountItemsPool.clear()
    MountItemsPool.setMountContentPoolFactory(null)
  }

  @Test
  fun `different mountable components should use different pools`() {

    // Mount an Image component
    lithoViewRule.render {
      Column {
        child(
            TestDrawableMountableComponent(
                drawable = ColorDrawable(Color.RED),
                style = Style.width(100.px).height(100.px),
            ))
      }
    }

    // Should create 1 Pool for TestDrawableMountableComponent
    assertThat(MountItemsPool.getMountItemPools().size).isEqualTo(1)

    // Mount multiple Image components, and a TestTextViewMountableComponent
    val lithoView =
        lithoViewRule
            .render {
              Column {
                child(
                    TestDrawableMountableComponent(
                        drawable = ColorDrawable(Color.RED),
                        style = Style.width(100.px).height(100.px),
                    ))
                child(
                    TestTextViewMountableComponent(
                        style = Style.width(100.px).height(500.px),
                    ))
              }
            }
            .lithoView

    // Should now have 2 Pools; one for the Image, and one for the Vertical Scroll component.
    assertThat(MountItemsPool.getMountItemPools().size).isEqualTo(2)

    // Unmount all content to release all the content to the pools
    lithoView.unmountAllItems()
  }

  @Test
  fun `should use pool size from mountable`() {

    // Mount 40 Image components, and then unmount them all
    lithoViewRule
        .render {
          Column {
            for (i in 1..40) {
              child(
                  TestDrawableMountableComponent(
                      drawable = ColorDrawable(Color.RED),
                      style = Style.width(100.px).height(100.px),
                  ))
            }
          }
        }
        .lithoView
        .unmountAllItems()

    assertThat(getMountItemPools().size).isEqualTo(1)
    assertThat((getMountItemPools()[0] as TrackedItemPool).currentSize).isEqualTo(10)
  }

  @Test
  fun `different primitive components should use different pools`() {

    // Mount an Image component
    lithoViewRule.render {
      Column {
        child(
            TestDrawablePrimitiveComponent(
                drawable = ColorDrawable(Color.RED),
                style = Style.width(100.px).height(100.px),
            ))
      }
    }

    // Should create 1 Pool for TestDrawablePrimitiveComponent
    assertThat(MountItemsPool.getMountItemPools().size).isEqualTo(1)

    // Mount multiple Image components, and a TestTextViewPrimitiveComponent
    val lithoView =
        lithoViewRule
            .render {
              Column {
                child(
                    TestDrawablePrimitiveComponent(
                        drawable = ColorDrawable(Color.RED),
                        style = Style.width(100.px).height(100.px),
                    ))
                child(
                    TestTextViewPrimitiveComponent(
                        style = Style.width(100.px).height(500.px),
                    ))
              }
            }
            .lithoView

    // Should now have 2 Pools; one for the Image, and one for the Vertical Scroll component.
    assertThat(MountItemsPool.getMountItemPools().size).isEqualTo(2)

    // Unmount all content to release all the content to the pools
    lithoView.unmountAllItems()
  }

  @Test
  fun `should use pool size from primitive`() {

    var createContentInvocationCount = 0

    // Mount 40 Image components, and then unmount them all
    lithoViewRule
        .render {
          Column {
            for (i in 1..40) {
              child(
                  TestDrawablePrimitiveComponent(
                      onCreateContent = { createContentInvocationCount++ },
                      drawable = ColorDrawable(Color.RED),
                      style = Style.width(100.px).height(100.px),
                  ))
            }
          }
        }
        .lithoView
        .unmountAllItems()

    assertThat(getMountItemPools().size).isEqualTo(1)
    assertThat(createContentInvocationCount).isEqualTo(40)

    createContentInvocationCount = 0
    // Mount 40 Image components again to check if pooling works, and then unmount them all
    lithoViewRule
        .render {
          Column {
            for (i in 1..40) {
              child(
                  TestDrawablePrimitiveComponent(
                      onCreateContent = { createContentInvocationCount++ },
                      drawable = ColorDrawable(Color.RED),
                      style = Style.width(100.px).height(100.px),
                  ))
            }
          }
        }
        .lithoView
        .unmountAllItems()

    assertThat(getMountItemPools().size).isEqualTo(1)
    // Create content should be called 30 times because 10 components were in the pool
    assertThat(createContentInvocationCount).isEqualTo(30)
  }

  @Test
  fun `should correctly preallocate primitive component`() {
    MountItemsPool.setMountContentPoolFactory(
        createPoolFactory(TestTextViewPrimitiveComponent.ALLOCATOR.poolSize()))

    // initially there is no pool
    assertThat(getMountItemPools().size).isEqualTo(0)

    // preallocate 40 Text components
    MountItemsPool.prefillMountContentPool(
        lithoViewRule.context.androidContext, 40, TestTextViewPrimitiveComponent.ALLOCATOR)

    // Should create 1 Pool for TestTextViewPrimitiveComponent
    assertThat(getMountItemPools().size).isEqualTo(1)
    // There should be 10 items in the pool
    assertThat((getMountItemPools()[0] as TrackedItemPool).currentSize).isEqualTo(10)
  }
}

// Mountable components

class TestDrawableMountableComponent(val drawable: Drawable, val style: Style? = null) :
    MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    return MountableRenderResult(DrawableMountable(drawable), style)
  }
}

class DrawableMountable(
    val drawable: Drawable,
) : SimpleMountable<Drawable>(RenderType.DRAWABLE) {

  init {
    MountItemsPool.setMountContentPoolFactory(createPoolFactory(poolSize()))
  }

  override fun poolSize(): Int = 10

  override fun createContent(context: Context): Drawable {
    return drawable
  }

  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult =
      MeasureResult(100, 100)

  override fun mount(c: Context, content: Drawable, layoutData: Any?) = Unit

  override fun unmount(c: Context, content: Drawable, layoutData: Any?) = Unit
}

class TestTextViewMountableComponent(val style: Style? = null) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    return MountableRenderResult(TextViewMountable(), style)
  }
}

class TextViewMountable : SimpleMountable<TextView>(RenderType.VIEW) {

  override fun poolSize(): Int = 10

  override fun createContent(context: Context): TextView {
    return TextView(context)
  }

  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult =
      MeasureResult(100, 100)

  override fun mount(c: Context, content: TextView, layoutData: Any?) = Unit

  override fun unmount(c: Context, content: TextView, layoutData: Any?) = Unit
}

// Primitive components

class TestDrawablePrimitiveComponent(
    val onCreateContent: (() -> Unit)? = null,
    val drawable: Drawable,
    val style: Style? = null
) : PrimitiveComponent() {
  override fun PrimitiveComponentScope.render(): LithoPrimitive {
    return LithoPrimitive(
        layoutBehavior = FixedSizeLayoutBehavior(100.px, 100.px),
        mountBehavior =
            MountBehavior(
                DrawableAllocator(poolSize = 10) {
                  onCreateContent?.invoke()
                  drawable
                }) {},
        style = style)
  }
}

// start_prefill_mount_content_pool_primitive_component_example
class TestTextViewPrimitiveComponent(val style: Style? = null) : PrimitiveComponent() {
  override fun PrimitiveComponentScope.render(): LithoPrimitive {
    return LithoPrimitive(
        layoutBehavior = FixedSizeLayoutBehavior(100.px, 100.px),
        mountBehavior = MountBehavior(ALLOCATOR) {},
        style = style)
  }

  companion object {
    val ALLOCATOR = ViewAllocator(poolSize = 10) { context -> TextView(context) }
  }
}
// end_prefill_mount_content_pool_primitive_component_example

private fun createPoolFactory(poolSize: Int): MountItemsPool.Factory {
  return object : MountItemsPool.Factory {
    override fun createMountContentPool(): MountItemsPool.ItemPool {
      return TrackedItemPool(this, poolSize)
    }
  }
}


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

import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.px
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.assertj.LithoAssertions.assertThat
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.LinearLayoutInfo
import com.facebook.litho.widget.RecyclerBinder
import com.facebook.litho.widget.RecyclerEventsController
import com.facebook.litho.widget.SectionsRecyclerView
import com.facebook.litho.widget.Text
import junit.framework.Assert.assertNotNull
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Tests for [ExperimentaRecycler] */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class ExperimentaRecyclerTest {

  @Rule @JvmField val lithoViewRule: LithoViewRule = LithoViewRule()

  lateinit var recyclerBinder: RecyclerBinder

  @Before
  fun setup() {
    recyclerBinder =
        RecyclerBinder.Builder()
            .layoutInfo(LinearLayoutInfo(lithoViewRule.context, OrientationHelper.VERTICAL, false))
            .build(lithoViewRule.context)
    for (i in 0 until 10) {
      recyclerBinder.insertItemAt(
          i,
          ComponentRenderInfo.create()
              .component(Text.create(lithoViewRule.context).text("test"))
              .build())
    }
  }

  @Test
  fun `ExperimentalRecycler should render with default values`() {
    val testLithoView =
        lithoViewRule.render {
          ExperimentalRecycler(binder = recyclerBinder, style = Style.width(100.px).height(100.px))
        }

    // should find an ExperimentalRecycler in the tree
    assertNotNull(testLithoView.findComponent(ExperimentalRecycler::class))

    val sectionsRecyclerView =
        testLithoView.lithoView.getMountItemAt(0).content as SectionsRecyclerView

    // values should be set to default
    assertThat(sectionsRecyclerView.recyclerView.hasFixedSize()).isEqualTo(true)
    assertThat(sectionsRecyclerView.recyclerView.clipToPadding).isEqualTo(true)
    assertThat(sectionsRecyclerView.recyclerView.isNestedScrollingEnabled).isEqualTo(true)
    assertThat(sectionsRecyclerView.recyclerView.scrollBarStyle)
        .isEqualTo(View.SCROLLBARS_INSIDE_OVERLAY)
    assertThat(sectionsRecyclerView.recyclerView.id).isEqualTo(View.NO_ID)
    assertThat(sectionsRecyclerView.recyclerView.horizontalFadingEdgeLength).isEqualTo(0)
    assertThat(sectionsRecyclerView.recyclerView.isHorizontalFadingEdgeEnabled).isEqualTo(false)
    assertThat(sectionsRecyclerView.recyclerView.isVerticalFadingEdgeEnabled).isEqualTo(false)
  }

  @Test
  fun `ItemDecorator when set is respected`() {
    val itemDecoration: RecyclerView.ItemDecoration =
        object : RecyclerView.ItemDecoration() {
          override fun getItemOffsets(
              outRect: Rect,
              view: View,
              parent: RecyclerView,
              state: RecyclerView.State
          ) {
            outRect.left = 10
          }
        }

    val testLithoView =
        lithoViewRule.render {
          ExperimentalRecycler(
              binder = recyclerBinder,
              itemDecoration = itemDecoration,
              style = Style.width(100.px).height(100.px))
        }

    val sectionsRecyclerView =
        testLithoView.lithoView.getMountItemAt(0).content as SectionsRecyclerView

    assertThat(sectionsRecyclerView.recyclerView.itemDecorationCount).isEqualTo(1)
    assertThat(sectionsRecyclerView.recyclerView.getItemDecorationAt(0)).isEqualTo(itemDecoration)
  }

  @Test
  fun `RecyclerEventController when set is respected`() {
    val recyclerEventsController = RecyclerEventsController()
    val testLithoView =
        lithoViewRule.render {
          ExperimentalRecycler(
              binder = recyclerBinder,
              recyclerEventsController = recyclerEventsController,
              style = Style.width(100.px).height(100.px))
        }

    val sectionsRecyclerView =
        testLithoView.lithoView.getMountItemAt(0).content as SectionsRecyclerView

    assertThat(recyclerEventsController.recyclerView).isEqualTo(sectionsRecyclerView.recyclerView)
  }

  @Test
  fun `ItemAnimator when set is respected`() {
    val itemAnimator = DefaultItemAnimator()
    val testLithoView =
        lithoViewRule.render {
          ExperimentalRecycler(
              binder = recyclerBinder,
              itemAnimator = itemAnimator,
              style = Style.width(100.px).height(100.px))
        }

    val sectionsRecyclerView =
        testLithoView.lithoView.getMountItemAt(0).content as SectionsRecyclerView

    assertThat(sectionsRecyclerView.recyclerView.itemAnimator).isEqualTo(itemAnimator)
  }
}


/*
 * 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 android.graphics.Color
import com.facebook.litho.ClickEvent
import com.facebook.litho.ComponentScope
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.backgroundColor
import com.facebook.litho.view.onClick
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Tests for [Collection]'s children */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class CollectionChildEquivalenceTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private lateinit var lazyList: LazyCollection

  @Before
  fun before() {
    with(ComponentScope(lithoViewRule.context)) { lazyList = LazyList {} as LazyCollection }
  }

  @Test
  fun `test children with null fields are equivalent`() {
    val next = CollectionChild("", null)
    val previous = CollectionChild("", null)

    assertThat(lazyList.isChildEquivalent(previous, next)).isTrue
  }

  @Test
  fun `test children with equal deps are equivalent`() {
    val next = CollectionChild("", null, deps = arrayOf("A"))
    val previous = CollectionChild("", null, deps = arrayOf("A"))

    assertThat(lazyList.isChildEquivalent(previous, next)).isTrue
  }

  @Test
  fun `test children with unequal deps are not equivalent`() {
    val next = CollectionChild("", null, deps = arrayOf("A"))
    val previous = CollectionChild("", null, deps = arrayOf("B"))

    assertThat(lazyList.isChildEquivalent(previous, next)).isFalse
  }

  @Test
  fun `test isChildEquivalent checks props`() {
    with(ComponentScope(lithoViewRule.context)) {
      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Text("Test")),
                  CollectionChild("", Text("Test")),
              ))
          .isTrue

      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Text("Test")),
                  CollectionChild("", Text("Production")),
              ))
          .isFalse
    }
  }

  @Test
  fun `test isChildEquivalent checks nested props`() {
    with(ComponentScope(lithoViewRule.context)) {
      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Row { child(Text("Test")) }),
                  CollectionChild("", Row { child(Text("Test")) }),
              ))
          .isTrue

      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Row { child(Text("Test")) }),
                  CollectionChild("", Row { child(Text("Production")) }),
              ))
          .isFalse
    }
  }

  @Test
  fun `test isChildEquivalent checks lambdas`() {
    with(ComponentScope(lithoViewRule.context)) {
      val onClick = { _: ClickEvent -> }
      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Text("Test", style = Style.onClick(action = onClick))),
                  CollectionChild("", Text("Test", style = Style.onClick(action = onClick))),
              ))
          .isTrue

      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Text("", style = Style.onClick {})),
                  CollectionChild("", Text("", style = Style.onClick {})),
              ))
          .isFalse
    }
  }

  @Test
  fun `test isChildEquivalent checks nested lambdas`() {
    with(ComponentScope(lithoViewRule.context)) {
      val onClick = { _: ClickEvent -> }
      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild(
                      "", Row { child(Text("Test", style = Style.onClick(action = onClick))) }),
                  CollectionChild(
                      "", Row { child(Text("Test", style = Style.onClick(action = onClick))) }),
              ))
          .isTrue

      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Row { child(Text("Test", style = Style.onClick {})) }),
                  CollectionChild("", Row { child(Text("Test", style = Style.onClick {})) }),
              ))
          .isFalse
    }
  }

  @Test
  fun `test isChildEquivalent checks common props`() {
    with(ComponentScope(lithoViewRule.context)) {
      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Text("Test", style = Style.backgroundColor(Color.RED))),
                  CollectionChild("", Text("Test", style = Style.backgroundColor(Color.RED))),
              ))
          .isTrue

      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild("", Text("Test", style = Style.backgroundColor(Color.RED))),
                  CollectionChild("", Text("Test", style = Style.backgroundColor(Color.BLUE))),
              ))
          .isFalse
    }
  }

  @Test
  fun `test isChildEquivalent checks nested common props`() {
    with(ComponentScope(lithoViewRule.context)) {
      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild(
                      "", Row { child(Text("Test", style = Style.backgroundColor(Color.RED))) }),
                  CollectionChild(
                      "", Row { child(Text("Test", style = Style.backgroundColor(Color.RED))) }),
              ))
          .isTrue

      assertThat(
              lazyList.isChildEquivalent(
                  CollectionChild(
                      "", Row { child(Text("Test", style = Style.backgroundColor(Color.RED))) }),
                  CollectionChild(
                      "", Row { child(Text("Test", style = Style.backgroundColor(Color.BLUE))) }),
              ))
          .isFalse
    }
  }
}


/*
 * 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.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Tests for [Collection]'s children */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class CollectionChildTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Test
  fun `test empty component renders`() {
    class Test : KComponent() {
      override fun ComponentScope.render(): Component {
        return LazyList {}
      }
    }

    lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
  }

  @Test
  fun `test add null component renders`() {
    class Test : KComponent() {
      override fun ComponentScope.render(): Component {
        return LazyList { child(null) }
      }
    }

    lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
  }

  @Test
  fun `test add null component lambda renders`() {
    class Test : KComponent() {
      override fun ComponentScope.render(): Component {
        return LazyList { child(deps = arrayOf()) { null } }
      }
    }

    lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
  }
}


/*
 * 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.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.alpha
import com.facebook.litho.view.rotation
import com.facebook.litho.view.scale
import com.facebook.litho.view.viewTag
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Tests for [Collection]'s deps prop */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class CollectionDepsTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Test
  fun collection_propUpdate_appliedToDependantChildren() {
    class CollectionWithSelectedRows(val alpha: Float, val rotation: Float, val scale: Float) :
        KComponent() {
      override fun ComponentScope.render(): Component = LazyList {
        child(
            Text(
                "deps_null",
                style = Style.viewTag("deps_null").alpha(alpha).rotation(rotation).scale(scale)))
        child(deps = arrayOf(alpha)) {
          Text(
              "deps_alpha",
              style = Style.viewTag("deps_alpha").alpha(alpha).rotation(rotation).scale(scale))
        }
        child(deps = arrayOf(scale)) {
          Text(
              "deps_scale",
              style = Style.viewTag("deps_scale").alpha(alpha).rotation(rotation).scale(scale))
        }
        child(deps = arrayOf()) {
          Text(
              "deps_empty",
              style = Style.viewTag("deps_empty").alpha(alpha).rotation(rotation).scale(scale))
        }
      }
    }

    // Perform an initial layout
    var testLithoView =
        lithoViewRule.render(widthPx = 1000, heightPx = 1000) {
          CollectionWithSelectedRows(.5f, 180f, 0.75f)
        }
    lithoViewRule.idle()

    // Perform a second layout that updates some props. Update alpha and rotation. Do not update
    // scale.
    testLithoView =
        lithoViewRule.render(lithoView = testLithoView.lithoView, widthPx = 1000, heightPx = 1000) {
          CollectionWithSelectedRows(1f, 0f, 0.75f)
        }
    lithoViewRule.idle()

    // Do the layout again. This is a workaround to force layout to happen in a test.
    testLithoView =
        lithoViewRule.render(lithoView = testLithoView.lithoView, widthPx = 1000, heightPx = 1000) {
          CollectionWithSelectedRows(1f, 0f, 0.75f)
        }
    lithoViewRule.idle()

    // childWithNullDeps should re-render all changes
    // Verify childWithNullDeps has been updated
    val childWithNullDeps = testLithoView.findViewWithTag("deps_null")
    assertThat(childWithNullDeps.alpha).isEqualTo(1f)
    assertThat(childWithNullDeps.rotation).isEqualTo(0f)
    assertThat(childWithNullDeps.scaleX).isEqualTo(0.75f)

    // childWithAlphaDeps should re-render when alpha changes
    // Verify childWithAlphaDeps has been updated
    val childWithAlphaDeps = testLithoView.findViewWithTag("deps_alpha")
    assertThat(childWithAlphaDeps.alpha).isEqualTo(1f)
    assertThat(childWithAlphaDeps.rotation).isEqualTo(0f)
    assertThat(childWithAlphaDeps.scaleX).isEqualTo(0.75f)

    // childWithScaleDeps should re-render when scale changes
    // Scale has not changed so verify childWithAlphaDeps was not updated
    val childWithScaleDeps = testLithoView.findViewWithTag("deps_scale")
    assertThat(childWithScaleDeps.alpha).isEqualTo(.5f)
    assertThat(childWithScaleDeps.rotation).isEqualTo(180f)
    assertThat(childWithScaleDeps.scaleX).isEqualTo(0.75f)

    // childWithEmptyDeps has an empty dpes array, so should never update
    // Verify childWithEmptyDeps was not updated
    val childWithEmptyDeps = testLithoView.findViewWithTag("deps_empty")
    assertThat(childWithEmptyDeps.alpha).isEqualTo(.5f)
    assertThat(childWithEmptyDeps.rotation).isEqualTo(180f)
    assertThat(childWithEmptyDeps.scaleX).isEqualTo(0.75f)
  }
}


/*
 * 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.EmptyComponent
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.widget.Text
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Tests for [Collection]'s children */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class CollectionIdTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private fun emptyComponent(): EmptyComponent = EmptyComponent()

  private fun textComponent(): Component = Text.create(lithoViewRule.context).text("Hello").build()

  private fun LazyCollectionChildren.getIds(): List<Any?> = collectionChildren.map { it.id }

  @Test
  fun `test generated ids are unique`() {
    val ids =
        LazyCollectionChildren()
            .apply {
              add(null)
              add(emptyComponent())
              add(id = null, component = emptyComponent())
              add(deps = arrayOf()) { emptyComponent() }
            }
            .getIds()

    assertThat(ids).hasSameSizeAs(ids.distinct())
  }

  @Test
  fun `test generated ids are stable across recreations`() {
    val ids1 =
        LazyCollectionChildren()
            .apply {
              add(null)
              add(emptyComponent())
              add(id = null, component = emptyComponent())
              add(deps = arrayOf()) { emptyComponent() }
            }
            .getIds()

    val ids2 =
        LazyCollectionChildren()
            .apply {
              add(null)
              add(emptyComponent())
              add(id = null, component = emptyComponent())
              add(deps = arrayOf()) { emptyComponent() }
            }
            .getIds()

    assertThat(ids1).hasSameElementsAs(ids2)
  }

  @Test
  fun `test generated ids for same component type are stable`() {
    val ids1 =
        LazyCollectionChildren()
            .apply {
              add(textComponent())
              add(textComponent())
            }
            .getIds()

    val ids2 =
        LazyCollectionChildren()
            .apply {
              add(textComponent())
              // Insert new content. Should not affect ids of existing children
              add(emptyComponent())
              add(id = 1, component = textComponent())
              // End new content.
              add(textComponent())
            }
            .getIds()

    assertThat(ids2.first()).isEqualTo(ids1.first())
    assertThat(ids2.last()).isEqualTo(ids1.last())
  }
}


/*
 * 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.KComponent
import com.facebook.litho.LithoView
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.TestLithoView
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.viewTag
import com.facebook.litho.widget.SectionsRecyclerView
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicIntegerArray
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Tests for [Collection]'s pagination prop */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class CollectionOnNearViewportTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private fun getLazyCollectionRecyclerView(
      testLithoView: TestLithoView,
      lazyCollectionTag: String
  ): RecyclerView? =
      ((testLithoView.findViewWithTagOrNull(lazyCollectionTag) as LithoView?)?.getChildAt(0)
              as SectionsRecyclerView?)
          ?.recyclerView

  @Test
  fun `test onNearViewport is called on scroll`() {
    val enterCounts = AtomicIntegerArray(5)

    class Test : KComponent() {
      override fun ComponentScope.render(): Component =
          LazyList(
              style = Style.viewTag("collection_tag"),
          ) {
            (0..4).forEach {
              child(
                  id = it,
                  onNearViewport = OnNearCallback { enterCounts[it]++ },
                  component = Text("$it"),
              )
            }
          }
    }

    val testLithoView = lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
    lithoViewRule.idle()

    assertThat(enterCounts).containsExactly(1, 1, 1, 0, 0)

    val recyclerView = getLazyCollectionRecyclerView(testLithoView, "collection_tag")

    // Scroll one item
    lithoViewRule.act(testLithoView) { recyclerView?.scrollBy(0, 50) }
    assertThat(enterCounts).containsExactly(1, 1, 1, 1, 0)

    // Scroll another item
    lithoViewRule.act(testLithoView) { recyclerView?.scrollBy(0, 50) }
    assertThat(enterCounts).containsExactly(1, 1, 1, 1, 1)
  }

  @Test
  fun `test onNearViewport respects offsetBeforeTail`() {
    val enterCounts = AtomicIntegerArray(5)

    class Test : KComponent() {
      override fun ComponentScope.render(): Component =
          LazyList(
              style = Style.viewTag("collection_tag"),
          ) {
            (0..3).forEach {
              child(
                  onNearViewport = OnNearCallback { enterCounts[it]++ },
                  component = Text("$it"),
              )
            }
            child(
                onNearViewport = OnNearCallback(offset = 2) { enterCounts[4]++ },
                component = Text("4"),
            )
          }
    }
    val testLithoView = lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
    lithoViewRule.idle()

    // Item 4's enter callback has already been triggered
    assertThat(enterCounts).containsExactly(1, 1, 1, 0, 1)

    val recyclerView = getLazyCollectionRecyclerView(testLithoView, "collection_tag")

    // Scroll one item
    lithoViewRule.act(testLithoView) { recyclerView?.scrollBy(0, 50) }
    assertThat(enterCounts).containsExactly(1, 1, 1, 1, 1)

    // Scroll another item
    lithoViewRule.act(testLithoView) { recyclerView?.scrollBy(0, 50) }
    assertThat(enterCounts).containsExactly(1, 1, 1, 1, 1)
  }

  enum class Item {
    Item1,
    Item2
  }

  @Test
  fun `test onNearViewport triggered by changing ids`() {
    val item1EnterCount = AtomicInteger(0)
    val item2EnterCount = AtomicInteger(0)

    class Test(val item: Item) : KComponent() {
      override fun ComponentScope.render(): Component =
          LazyList(
              style = Style.viewTag("collection_tag"),
          ) {
            when (item) {
              Item.Item1 -> {
                child(
                    id = Item.Item1,
                    onNearViewport = OnNearCallback { item1EnterCount.getAndIncrement() },
                    component = Text("Item"),
                )
              }
              Item.Item2 -> {
                child(
                    id = Item.Item2,
                    onNearViewport = OnNearCallback { item2EnterCount.getAndIncrement() },
                    component = Text("Item"),
                )
              }
            }
          }
    }

    val testLithoView = lithoViewRule.render(widthPx = 100, heightPx = 100) { Test(Item.Item1) }
    lithoViewRule.idle()

    assertThat(item1EnterCount.get()).isEqualTo(1)
    assertThat(item2EnterCount.get()).isEqualTo(0)

    lithoViewRule.render(lithoView = testLithoView.lithoView, widthPx = 100, heightPx = 100) {
      Test(Item.Item2)
    }
    lithoViewRule.idle()

    assertThat(item1EnterCount.get()).isEqualTo(1)
    assertThat(item2EnterCount.get()).isEqualTo(1)
  }
}


/*
 * 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.KComponent
import com.facebook.litho.LithoView
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.TestLithoView
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.viewTag
import com.facebook.litho.widget.SectionsRecyclerView
import java.util.concurrent.atomic.AtomicInteger
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Tests for [Collection]'s pagination prop */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class CollectionOnViewPortChangedTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private fun getLazyCollectionRecyclerView(
      testLithoView: TestLithoView,
      lazyCollectionTag: String
  ): RecyclerView? =
      ((testLithoView.findViewWithTagOrNull(lazyCollectionTag) as LithoView?)?.getChildAt(0)
              as SectionsRecyclerView?)
          ?.recyclerView

  @Test
  fun `test Collection pagination callback receives correct updates`() {
    val firstVisibleIndexValue = AtomicInteger()
    val lastVisibleIndexValue = AtomicInteger()
    val totalCountValue = AtomicInteger()
    val firstFullyVisibleIndexValue = AtomicInteger()
    val lastFullyVisibleIndexValue = AtomicInteger()

    class Test : KComponent() {
      override fun ComponentScope.render(): Component {
        val onViewportChanged =
            {
                firstVisibleIndex: Int,
                lastVisibleIndex: Int,
                totalCount: Int,
                firstFullyVisibleIndex: Int,
                lastFullyVisibleIndex: Int ->
              firstVisibleIndexValue.set(firstVisibleIndex)
              lastVisibleIndexValue.set(lastVisibleIndex)
              totalCountValue.set(totalCount)
              firstFullyVisibleIndexValue.set(firstFullyVisibleIndex)
              lastFullyVisibleIndexValue.set(lastFullyVisibleIndex)
            }
        return LazyList(
            handle = handle,
            onViewportChanged = onViewportChanged,
            style = Style.viewTag("collection_tag")) {
              (0..4).forEach { child(Text("Child $it")) }
            }
      }
    }

    val testLithoView = lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
    lithoViewRule.idle()

    assertThat(firstVisibleIndexValue.get()).isEqualTo(0)
    assertThat(lastVisibleIndexValue.get()).isEqualTo(2)
    assertThat(totalCountValue.get()).isEqualTo(5)
    assertThat(firstFullyVisibleIndexValue.get()).isEqualTo(0)
    assertThat(lastFullyVisibleIndexValue.get()).isEqualTo(1)

    val recyclerView = getLazyCollectionRecyclerView(testLithoView, "collection_tag")
    lithoViewRule.act(testLithoView) {
      // Scroll by a distance less than the item height so the first and last items overlap the
      // edges
      recyclerView?.scrollBy(0, 50)
    }

    assertThat(firstVisibleIndexValue.get()).isEqualTo(1)
    assertThat(lastVisibleIndexValue.get()).isEqualTo(3)
    assertThat(totalCountValue.get()).isEqualTo(5)
    assertThat(firstFullyVisibleIndexValue.get()).isEqualTo(2)
    assertThat(lastFullyVisibleIndexValue.get()).isEqualTo(2)
  }
}


/*
 * 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.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.onClick
import com.facebook.litho.view.viewTag
import java.util.concurrent.atomic.AtomicInteger
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/** Tests for [Collection]'s pagination prop */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class CollectionPaginationTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  @Test
  fun `test Collection pagination callback receives correct updates`() {
    val onNearEndCallbackCount = AtomicInteger()

    class Test : KComponent() {
      override fun ComponentScope.render(): Component {
        val controller = LazyCollectionController()
        return LazyList(
            lazyCollectionController = controller,
            onNearEnd = OnNearCallback { onNearEndCallbackCount.incrementAndGet() },
            style = Style.viewTag("collection_tag").onClick { controller.scrollToIndex(4) }) {
              (0..4).forEach { child(Text("Child $it")) }
            }
      }
    }

    val testLithoView = lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
    lithoViewRule.idle()

    assertThat(onNearEndCallbackCount.get()).isEqualTo(0)

    lithoViewRule.act(testLithoView) { clickOnTag("collection_tag") }

    lithoViewRule.render(lithoView = testLithoView.lithoView, widthPx = 100, heightPx = 100) {
      Test()
    }

    assertThat(onNearEndCallbackCount.get()).isEqualTo(1)
  }
}


/*
 * 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.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.LithoView
import com.facebook.litho.Style
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.sections.SectionTree
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.TestLithoView
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.viewTag
import com.facebook.litho.widget.RecyclerEventsController
import com.facebook.litho.widget.SectionsRecyclerView
import com.facebook.litho.widget.SmoothScrollAlignmentType
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.annotation.LooperMode

@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class LazyCollectionControllerTest {

  @Rule @JvmField val lithoViewRule = LithoViewRule()

  private fun getLazyCollectionRecyclerView(
      testLithoView: TestLithoView,
      lazyCollectionTag: String
  ): RecyclerView? =
      ((testLithoView.findViewWithTagOrNull(lazyCollectionTag) as LithoView?)?.getChildAt(0)
              as SectionsRecyclerView?)
          ?.recyclerView

  fun `test lazyCollectionController recyclerView reference is updated`() {
    val lazyCollectionController = LazyCollectionController()

    class Test : KComponent() {
      override fun ComponentScope.render(): Component {
        return LazyList(
            lazyCollectionController = lazyCollectionController,
            style = Style.viewTag("collection_tag"),
        ) {}
      }
    }

    assertThat(lazyCollectionController.recyclerView).isNull()

    val testLithoView = lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
    lithoViewRule.idle()

    assertThat(lazyCollectionController.recyclerView)
        .isSameAs(getLazyCollectionRecyclerView(testLithoView, "collection_tag"))
  }

  @Test
  fun `test showRefreshing delegates to RecyclerEventsController`() {
    val lazyCollectionController = LazyCollectionController()

    val recyclerEventsController = mock<RecyclerEventsController>()
    lazyCollectionController.recyclerEventsController = recyclerEventsController

    lazyCollectionController.setRefreshing(true)
    verify(recyclerEventsController).showRefreshing()
  }

  @Test
  fun `test clearRefreshing delegates to RecyclerEventsController`() {
    val lazyCollectionController = LazyCollectionController()

    val recyclerEventsController = mock<RecyclerEventsController>()
    lazyCollectionController.recyclerEventsController = recyclerEventsController

    lazyCollectionController.setRefreshing(false)
    verify(recyclerEventsController).clearRefreshing()
  }

  @Test
  fun `test scrollBy delegates to RecyclerView`() {
    val lazyCollectionController = LazyCollectionController()

    val mockRecyclerView = mock<RecyclerView>()
    val recyclerEventsController = mock<RecyclerEventsController>()
    whenever(recyclerEventsController.recyclerView).thenReturn(mockRecyclerView)
    lazyCollectionController.recyclerEventsController = recyclerEventsController

    lazyCollectionController.scrollBy(10, 20)
    verify(mockRecyclerView).scrollBy(10, 20)

    lazyCollectionController.scrollBy(30, 40)
    verify(mockRecyclerView).scrollBy(30, 40)
  }

  @Test
  fun `test smoothScrollBy delegates to RecyclerView`() {
    val lazyCollectionController = LazyCollectionController()

    val mockRecyclerView = mock<RecyclerView>()
    val recyclerEventsController = mock<RecyclerEventsController>()
    whenever(recyclerEventsController.recyclerView).thenReturn(mockRecyclerView)
    lazyCollectionController.recyclerEventsController = recyclerEventsController

    lazyCollectionController.smoothScrollBy(10, 20)
    verify(mockRecyclerView).smoothScrollBy(10, 20)

    lazyCollectionController.smoothScrollBy(30, 40)
    verify(mockRecyclerView).smoothScrollBy(30, 40)
  }

  @Test
  fun `test scrollToIndex delegates to SectionTree`() {
    val lazyCollectionController = LazyCollectionController()

    val mockSectionTree = mock<SectionTree>()
    lazyCollectionController.sectionTree = mockSectionTree

    lazyCollectionController.scrollToIndex(5, 10)
    verify(mockSectionTree).requestFocusOnRoot(5, 10)
  }

  @Test
  fun `test smoothScrollToIndex delegates to SectionTree`() {
    val lazyCollectionController = LazyCollectionController()

    val mockSectionTree = mock<SectionTree>()
    lazyCollectionController.sectionTree = mockSectionTree

    lazyCollectionController.smoothScrollToIndex(5, 10, SmoothScrollAlignmentType.SNAP_TO_START)
    verify(mockSectionTree).requestSmoothFocusOnRoot(5, 10, SmoothScrollAlignmentType.SNAP_TO_START)
  }

  @Test
  fun `test scrollToId brings child with id into viewport`() {
    val lazyCollectionController = LazyCollectionController()

    class Test : KComponent() {
      override fun ComponentScope.render(): Component =
          LazyList(
              lazyCollectionController = lazyCollectionController,
              style = Style.viewTag("collection_tag"),
          ) {
            (0..10).forEach { child(id = it, component = Text("$it", Style.viewTag("$it"))) }
          }
    }

    val testLithoView = lithoViewRule.render(widthPx = 100, heightPx = 100) { Test() }
    lithoViewRule.idle()

    lithoViewRule.act(testLithoView) { lazyCollectionController.scrollToId(9) }

    // Additional `setRoot()` to kick the test infra into applying RecyclerView changes
    testLithoView.setRoot(Test())
    lithoViewRule.idle()

    val recyclerView = getLazyCollectionRecyclerView(testLithoView, "collection_tag")
    assertThat(recyclerView).isNotNull
    recyclerView ?: return

    val visibleRange =
        with(recyclerView.layoutManager as LinearLayoutManager) {
          (findFirstVisibleItemPosition()..findLastVisibleItemPosition())
        }
    assertThat(visibleRange).contains(9)
  }
}


/*
 * 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 android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.annotation.LooperMode

/** Tests for [Collection]'s children */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class SpacingTest {

  lateinit var view: View
  lateinit var adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>
  lateinit var recyclerView: RecyclerView
  lateinit var linearLayoutManager: LinearLayoutManager
  lateinit var state: RecyclerView.State

  @Before
  fun setUp() {
    view = mock {}
    linearLayoutManager = mock {
      on { orientation } doReturn RecyclerView.VERTICAL
      on { layoutDirection } doReturn View.LAYOUT_DIRECTION_LTR
    }
    adapter = mock { on { itemCount } doReturn 4 }
    recyclerView = mock {
      on { layoutManager } doReturn linearLayoutManager
      on { getChildAdapterPosition(any()) } doReturn -1
      on { adapter } doReturn adapter
    }
    state = mock {}
  }

  @Test
  fun `test equality`() {
    /*
     * Defend against the `data` keyword being removed from the class, as this is necessary for
     * passing isEquivalentTo checks.
     */
    assertThat(LinearSpacingItemDecoration(1, 2, 3, 4))
        .isEqualTo(LinearSpacingItemDecoration(1, 2, 3, 4))
  }

  @Test
  fun `test all applied correctly to vertical`() {
    val linearSpacing = LinearSpacingItemDecoration(all = 10)

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(0, 10, 0, 10))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(0, 0, 0, 10))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(0, 0, 0, 10))
  }

  @Test
  fun `test all applied correctly to horizontal`() {
    whenever(linearLayoutManager.orientation).thenReturn(RecyclerView.HORIZONTAL)

    val linearSpacing = LinearSpacingItemDecoration(all = 10)

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(10, 0, 10, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(0, 0, 10, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(0, 0, 10, 0))
  }

  @Test
  fun `test overrides applied correctly to vertical`() {
    val linearSpacing =
        LinearSpacingItemDecoration(
            all = 10,
            between = 1,
            start = 2,
            end = 3,
        )

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(0, 2, 0, 1))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(0, 0, 0, 1))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(0, 0, 0, 3))
  }

  @Test
  fun `test overrides applied correctly to horizontal`() {
    whenever(linearLayoutManager.orientation).thenReturn(RecyclerView.HORIZONTAL)

    val linearSpacing =
        LinearSpacingItemDecoration(
            all = 10,
            between = 1,
            start = 2,
            end = 3,
        )

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(2, 0, 1, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(0, 0, 1, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(0, 0, 3, 0))
  }

  @Test
  fun `test overrides applied correctly for RTL`() {
    whenever(linearLayoutManager.orientation).thenReturn(RecyclerView.HORIZONTAL)
    whenever(linearLayoutManager.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_RTL)

    val linearSpacing =
        LinearSpacingItemDecoration(
            all = 10,
            between = 1,
            start = 2,
            end = 3,
        )

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(1, 0, 2, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(1, 0, 0, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(3, 0, 0, 0))
  }

  @Test
  fun `test overrides applied correctly for vertical reverse layout`() {
    whenever(linearLayoutManager.reverseLayout).thenReturn(true)

    val linearSpacing =
        LinearSpacingItemDecoration(
            all = 10,
            between = 1,
            start = 2,
            end = 3,
        )

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(0, 1, 0, 2))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(0, 1, 0, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(0, 3, 0, 0))
  }

  @Test
  fun `test overrides applied correctly for horizontal reverse layout`() {
    whenever(linearLayoutManager.orientation).thenReturn(RecyclerView.HORIZONTAL)
    whenever(linearLayoutManager.reverseLayout).thenReturn(true)

    val linearSpacing =
        LinearSpacingItemDecoration(
            all = 10,
            between = 1,
            start = 2,
            end = 3,
        )

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(1, 0, 2, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(1, 0, 0, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(3, 0, 0, 0))
  }

  @Test
  fun `test overrides applied correctly for horizontal reversed RTL layout`() {
    whenever(linearLayoutManager.orientation).thenReturn(RecyclerView.HORIZONTAL)
    whenever(linearLayoutManager.reverseLayout).thenReturn(true)
    whenever(linearLayoutManager.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_RTL)

    val linearSpacing =
        LinearSpacingItemDecoration(
            all = 10,
            between = 1,
            start = 2,
            end = 3,
        )

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(2, 0, 1, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(0, 0, 1, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(0, 0, 3, 0))
  }

  @Test
  fun `test overrides applied correctly for vertical reverse RTL layout`() {
    whenever(linearLayoutManager.reverseLayout).thenReturn(true)
    whenever(linearLayoutManager.layoutDirection).thenReturn(View.LAYOUT_DIRECTION_RTL)

    val linearSpacing =
        LinearSpacingItemDecoration(
            all = 10,
            between = 1,
            start = 2,
            end = 3,
        )

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(0)
    val first = Rect()
    linearSpacing.getItemOffsets(first, view, recyclerView, state)
    assertThat(first).isEqualTo(Rect(0, 1, 0, 2))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(2)
    val middle = Rect()
    linearSpacing.getItemOffsets(middle, view, recyclerView, state)
    assertThat(middle).isEqualTo(Rect(0, 1, 0, 0))

    whenever(recyclerView.getChildAdapterPosition(any())).thenReturn(3)
    val last = Rect()
    linearSpacing.getItemOffsets(last, view, recyclerView, state)
    assertThat(last).isEqualTo(Rect(0, 3, 0, 0))
  }
}


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

import android.os.Bundle
import com.facebook.litho.AOSPLithoLifecycleProvider
import com.facebook.litho.LithoView
import com.facebook.samples.litho.Demos.SingleDemo
import java.lang.RuntimeException

class ComponentDemoActivity : NavigatableDemoActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val model = dataModel
    if (model !is SingleDemo || (model.componentCreator == null && model.component == null)) {
      throw RuntimeException("Invalid model: $model")
    }

    val componentCreator = model.componentCreator
    if (componentCreator != null) {
      val lithoView = LithoView(this)
      lithoView.setComponent(componentCreator.create(lithoView.componentContext))
      setContentView(lithoView)
    } else {
      val lithoView = LithoView.create(this, model.component, AOSPLithoLifecycleProvider(this))
      setContentView(lithoView)
    }
  }
}


/*
 * 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.samples.litho.documentation.flexbox

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.core.margin
import com.facebook.litho.core.padding
import com.facebook.litho.dp
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.kotlin.widget.VerticalScroll
import com.facebook.litho.sp

class ContentInVerticalScroll : KComponent() {

  // start_example
  override fun ComponentScope.render(): Component = VerticalScroll {
    Column {
      for (i in 0..20) {
        child(
            Text(
                style = Style.padding(16.dp).margin(top = 8.dp),
                text = "Text counter = " + i,
                textSize = 20.sp,
                maxLines = 1))
      }
    }
  }
  // end_example
}


/*
 * 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.samples.litho.documentation.flexbox

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.margin
import com.facebook.litho.core.padding
import com.facebook.litho.dp
import com.facebook.litho.flexbox.flex
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.sp

class TextInColumnWithExactHeight : KComponent() {

  // start_example
  override fun ComponentScope.render(): Component {
    return Column(style = Style.height(50.dp).padding(16.dp)) { // exact height defined here
      child(
          Text(
              style =
                  Style.flex(
                      shrink =
                          0f), // Even with flexShrink zero, text will be cut as there is not enough
              // space
              text = "This is a really long text.",
              textSize = 20.sp,
              maxLines = 1))
      child(
          Text(
              style = Style.margin(start = 8.dp),
              text = "Another long text",
              textSize = 20.sp,
              maxLines = 1))
    }
  }
  // end_example
}


/*
 * 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.samples.litho.documentation.flexbox

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.core.margin
import com.facebook.litho.core.padding
import com.facebook.litho.dp
import com.facebook.litho.flexbox.flex
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.sp

class TextInRow : KComponent() {

  // start_example
  override fun ComponentScope.render(): Component {
    return Row(style = Style.padding(16.dp)) {
      child(
          Text(
              style =
                  Style.flex(shrink = 0f), // If flexShrink is 1f then this text will be truncated
              text = "This is a really long text.",
              textSize = 20.sp,
              maxLines = 1))
      child(
          Text(
              style = Style.margin(start = 8.dp),
              text = "Another long text",
              textSize = 20.sp,
              maxLines = 1))
    }
  }
  // end_example
}


/*
 * 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.samples.litho.documentation.flexbox

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.heightPercent
import com.facebook.litho.core.margin
import com.facebook.litho.core.padding
import com.facebook.litho.core.widthPercent
import com.facebook.litho.dp
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.sp

class TextInsideContainer : KComponent() {

  // start_example
  override fun ComponentScope.render(): Component {
    return Column(style = Style.height(135.dp).padding(16.dp)) {
      child(
          Column(style = Style.heightPercent(100f).widthPercent(100f)) {
            // either remove widthPercent / heightPercent here so that both siblings can grow
            // equally
            child(Text(text = "This is a really long text.", textSize = 20.sp, maxLines = 1))
            child(
                Text(
                    style = Style.margin(top = 8.dp),
                    text = "Subtitle text",
                    textSize = 20.sp,
                    maxLines = 1))
          })
      child(
          Text(
              // or add flex(shrink = 0f) so that this component does not shrink
              style = Style.margin(top = 16.dp),
              text = "Small footer text",
              textSize = 20.sp,
              maxLines = 1))
    }
  }
  // end_example
}


/*
 * 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.samples.litho.documentation.props

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text

// start_example
class PropDefaultKComponent(private val name: String = "John Doe") : KComponent() {

  override fun ComponentScope.render(): Component {
    return Text(text = "Hello $name!")
  }
}
// end_example


/*
 * 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.samples.litho.documentation.props

import android.content.res.Resources
import androidx.core.content.ContextCompat
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.colorRes
import com.facebook.litho.dimenRes
import com.facebook.litho.stringRes
import com.facebook.samples.litho.R

// start_example
class PropResourceTypeParentKComponent(
    private val useResourceType: Boolean,
) : KComponent() {

  override fun ComponentScope.render(): Component {

    if (!useResourceType) {
      // start_prop_without_resource_type_usage
      val res: Resources = context.getResources()
      return PropWithoutResourceTypeKComponent(
          name = res.getString(R.string.name_string),
          size = dimenRes(R.dimen.primary_text_size),
          color = ContextCompat.getColor(context.getAndroidContext(), R.color.primaryColor))
      // end_prop_without_resource_type_usage
    } else {
      // start_prop_with_resource_type_usage
      return PropWithResourceTypeKComponent(
          name = stringRes(R.string.name_string),
          size = dimenRes(R.dimen.primary_text_size),
          color = colorRes(R.color.primaryColor))
      // end_prop_with_resource_type_usage
    }
  }
}
// end_example


/*
 * 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.samples.litho.documentation.props

import androidx.annotation.ColorInt
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.Dimen
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text

// start_example
class PropWithoutResourceTypeKComponent(
    private val name: CharSequence,
    private val size: Dimen,
    @ColorInt private val color: Int
) : KComponent() {

  override fun ComponentScope.render(): Component {
    return Text(text = "Hello $name!", textSize = size, textColor = color)
  }
}
// end_example


/*
 * 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.samples.litho.documentation.props

import androidx.annotation.ColorInt
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.Dimen
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text

// start_example
class PropWithResourceTypeKComponent(
    private val name: CharSequence,
    private val size: Dimen,
    @ColorInt private val color: Int
) : KComponent() {

  override fun ComponentScope.render(): Component {
    return Text(text = "Hello $name!", textSize = size, textColor = color)
  }
}
// end_example


/*
 * 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.samples.litho.documentation.props

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.sp

// start_example
class VariableArgumentPropKComponent(private vararg val names: String) : KComponent() {

  override fun ComponentScope.render(): Component {
    return Column() {
      for (name in names) {
        child(Text(text = name, textSize = 16.sp))
      }
    }
  }
}
// end_example


/*
 * 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.samples.litho.documentation.props

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.stringRes
import com.facebook.samples.litho.R

class VariableArgumentPropParentKComponent(private val useVarArgWithResType: Boolean) :
    KComponent() {

  override fun ComponentScope.render(): Component {
    if (useVarArgWithResType) {
      // start_var_arg_res_type_usage
      return VariableArgumentPropKComponent(
          "One", stringRes(R.string.app_name), stringRes(R.string.name_string))
      // end_var_arg_res_type_usage
    } else {
      // start_var_arg_usage
      return VariableArgumentPropKComponent("One", "Two", "Three", "Four")
      // end_var_arg_usage
    }
  }
}


/*
 * 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.samples.litho.documentation.treeprops

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.TreePropProvider
import com.facebook.litho.getTreeProp

// start_example
class BottomGroupKComponent : KComponent() {

  override fun ComponentScope.render(): Component? {
    val parentLogContext = getTreeProp<LogContext>()
    val bottomGroupLogContext = LogContext.append(parentLogContext, "bottom")

    return TreePropProvider(LogContext::class.java to bottomGroupLogContext) {
      Column { child(LeafKComponent()) }
    }
  }
}
// end_example


/*
 * 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.samples.litho.documentation.treeprops

import android.net.Uri
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.getTreeProp
import com.facebook.litho.kotlin.widget.Text

// start_example
class ChildKComponent(private val imageUri: Uri) : KComponent() {

  override fun ComponentScope.render(): Component {
    val prefetcher = getTreeProp<Prefetcher>()

    prefetcher?.prefetch(imageUri)
    // ...
    return Text("Prefetch image when this component is created")
  }
}
// end_example