/*
 * 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.animated

import com.facebook.litho.ComponentScope
import com.facebook.litho.DerivedDynamicValue
import com.facebook.litho.DynamicValue
import com.facebook.litho.annotations.Hook
import com.facebook.litho.useState

/**
 * Creates a [DynamicValue] with a static initial value. This DynamicValue can be passed to the
 * variants of [Style] properties that take DynamicValue, like [Style.backgroundColor] or
 * [Style.translationY]. You can then imperatively set a new value on the main thread to update that
 * property without a state update, or use the collection of [Animated] APIs to drive animations on
 * this DynamicValue, which will in turn animate the property.
 *
 * Note: You can only change or animate a DynamicValue on the main thread.
 */
@Hook
fun <T> ComponentScope.useBinding(initialValue: T): DynamicValue<T> =
    useState { DynamicValue(initialValue) }.value

/**
 * Creates a [DynamicValue] deriving from an existing binding param, with modifications applied by
 * transform function.
 *
 * For example:
 * ```
 * val bgColor = useBinding(colorProgress) { progress -> Color.HSVToColor(floatArrayOf(100f *
 * progress, 100f, 255f)) }
 * ```
 */
@Hook
fun <T, S> ComponentScope.useBinding(
    binding: DynamicValue<T>,
    transform: (T) -> S
): DynamicValue<S> {
  return useState { DerivedDynamicValue(binding, transform) }.value
}


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

package com.facebook.litho.animated

import com.facebook.infer.annotation.ThreadConfined

/**
 * Creates animation that will run in a loop [repeatCount] times when [LoopAnimation.start] is
 * triggered
 */
class LoopAnimation(private var repeatCount: Int, private val animation: AnimatedAnimation) :
    AnimatedAnimation {
  private val loopAnimatorListeners = mutableListOf<AnimationFinishListener>()
  private var currentLoop = 0
  private var _isActive: Boolean = false

  init {
    animation.addListener(
        object : AnimationFinishListener {
          override fun onFinish(cancelled: Boolean) {
            if (cancelled && _isActive) {
              // child was cancelled, main animation is still active
              cancel()
              return
            }
            loopOrEndAnimation(cancelled)
          }
        })
  }

  @ThreadConfined(ThreadConfined.UI) override fun isActive(): Boolean = _isActive

  @ThreadConfined(ThreadConfined.UI)
  override fun start() {
    check(!_isActive) { "start() called more than once" }
    _isActive = true
    animation.start()
  }

  @ThreadConfined(ThreadConfined.UI)
  override fun cancel() {
    if (!_isActive) {
      // ignore multiple cancel calls
      return
    }
    _isActive = false
    if (animation.isActive()) {
      animation.cancel()
    }
    loopAnimatorListeners.forEach { it.onFinish(true) }
    finish()
  }

  override fun addListener(finishListener: AnimationFinishListener) {
    if (!loopAnimatorListeners.contains(finishListener)) {
      loopAnimatorListeners.add(finishListener)
    }
  }

  private fun loopOrEndAnimation(cancelled: Boolean) {
    if (!_isActive || cancelled) {
      return
    }
    currentLoop++
    if (currentLoop == repeatCount) {
      loopAnimatorListeners.forEach { it.onFinish(false) }
      finish()
      return
    }
    animation.start()
  }

  private fun finish() {
    _isActive = false
    currentLoop = 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.litho.animated

import com.facebook.infer.annotation.ThreadConfined

/**
 * Creates collection of animators that will run in parallel when [ParallelAnimation.start] is
 * triggered
 */
class ParallelAnimation(private val animators: Array<out AnimatedAnimation>) : AnimatedAnimation {
  private val parallelAnimationListeners = mutableListOf<AnimationFinishListener>()
  private var parallelFinishedAnimators = 0
  private var _isActive: Boolean = false

  init {
    animators.forEach {
      it.addListener(
          object : AnimationFinishListener {
            override fun onFinish(cancelled: Boolean) {
              if (cancelled && _isActive) {
                // child was cancelled, main animation is still active
                cancel()
                return
              }
              animatorFinished(cancelled)
            }
          })
    }
  }

  @ThreadConfined(ThreadConfined.UI) override fun isActive(): Boolean = _isActive

  @ThreadConfined(ThreadConfined.UI)
  override fun start() {
    check(!_isActive) { "start() called more than once" }
    if (animators.isEmpty()) {
      throw IllegalArgumentException("Empty animators collection")
    }
    _isActive = true
    startAnimators()
  }

  @ThreadConfined(ThreadConfined.UI)
  override fun cancel() {
    if (!_isActive) {
      // ignore multiple cancel calls
      return
    }
    _isActive = false
    animators.forEach {
      if (it.isActive()) {
        it.cancel()
      }
    }
    parallelAnimationListeners.forEach { it.onFinish(true) }
    finish()
  }

  override fun addListener(finishListener: AnimationFinishListener) {
    if (!parallelAnimationListeners.contains(finishListener)) {
      parallelAnimationListeners.add(finishListener)
    }
  }

  private fun animatorFinished(cancelled: Boolean) {
    if (!_isActive || cancelled) {
      return
    }
    parallelFinishedAnimators++
    if (animators.size == parallelFinishedAnimators) {
      finish()
      parallelAnimationListeners.forEach { it.onFinish(false) }
    }
  }

  private fun startAnimators() {
    animators.forEach { it.start() }
  }

  private fun finish() {
    _isActive = false
    parallelFinishedAnimators = 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.litho.animated

import com.facebook.infer.annotation.ThreadConfined

/**
 * Creates collection of animators that will run one after another in sequence when
 * [SequenceAnimation.start] is triggered
 */
class SequenceAnimation(private val animators: Array<out AnimatedAnimation>) : AnimatedAnimation {
  private val sequenceAnimatorListeners = mutableListOf<AnimationFinishListener>()
  private var currentIndex = 0
  private var _isActive: Boolean = false

  init {
    animators.forEach {
      it.addListener(
          object : AnimationFinishListener {
            override fun onFinish(cancelled: Boolean) {
              if (cancelled && _isActive) {
                // child was cancelled, main animation is still active
                cancel()
                return
              }
              animatorFinished(cancelled)
            }
          })
    }
  }

  @ThreadConfined(ThreadConfined.UI) override fun isActive(): Boolean = _isActive

  @ThreadConfined(ThreadConfined.UI)
  override fun start() {
    check(!_isActive) { "start() called more than once" }
    if (animators.isEmpty()) {
      throw IllegalArgumentException("Empty animators collection")
    }
    _isActive = true
    startAnimator()
  }

  @ThreadConfined(ThreadConfined.UI)
  override fun cancel() {
    if (!_isActive) {
      // ignore multiple cancel calls
      return
    }
    _isActive = false
    if (animators[currentIndex].isActive()) {
      animators[currentIndex].cancel()
    }
    sequenceAnimatorListeners.forEach { it.onFinish(true) }
    finish()
  }

  override fun addListener(finishListener: AnimationFinishListener) {
    if (!sequenceAnimatorListeners.contains(finishListener)) {
      sequenceAnimatorListeners.add(finishListener)
    }
  }

  private fun animatorFinished(cancelled: Boolean) {
    if (!_isActive || cancelled) {
      return
    }
    currentIndex++
    if (currentIndex == animators.size) {
      finish()
      sequenceAnimatorListeners.forEach { it.onFinish(false) }
      return
    }
    startAnimator()
  }

  private fun startAnimator() {
    animators[currentIndex].start()
  }

  private fun finish() {
    _isActive = false
    currentIndex = 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.litho.annotations

import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy

/** Marker for the Litho hooks */
@Retention(RetentionPolicy.RUNTIME) annotation class Hook


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

import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.litho.CommonProps
import com.facebook.litho.ComponentContext
import com.facebook.litho.Dimen
import com.facebook.litho.Style
import com.facebook.litho.StyleItem
import com.facebook.litho.StyleItemField
import com.facebook.yoga.YogaEdge

/** Enums for [CoreDimenStyleItem]. */
@PublishedApi
internal enum class CoreDimenField : StyleItemField {
  WIDTH,
  HEIGHT,
  MIN_WIDTH,
  MAX_WIDTH,
  MIN_HEIGHT,
  MAX_HEIGHT,
  PADDING_START,
  PADDING_TOP,
  PADDING_END,
  PADDING_BOTTOM,
  PADDING_LEFT,
  PADDING_RIGHT,
  PADDING_HORIZONTAL,
  PADDING_VERTICAL,
  PADDING_ALL,
  MARGIN_START,
  MARGIN_TOP,
  MARGIN_END,
  MARGIN_BOTTOM,
  MARGIN_LEFT,
  MARGIN_RIGHT,
  MARGIN_HORIZONTAL,
  MARGIN_VERTICAL,
  MARGIN_ALL,
}

/** Enums for [CoreFloatStyleItem]. */
@PublishedApi
internal enum class CoreFloatField : StyleItemField {
  WIDTH_PERCENT,
  HEIGHT_PERCENT,
  MIN_WIDTH_PERCENT,
  MAX_WIDTH_PERCENT,
  MIN_HEIGHT_PERCENT,
  MAX_HEIGHT_PERCENT,
}

/** Common style item for all core dimen styles. See note on [CoreDimenField] about this pattern. */
@PublishedApi
@DataClassGenerate
internal data class CoreDimenStyleItem(
    override val field: CoreDimenField,
    override val value: Dimen
) : StyleItem<Dimen> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    val pixelValue = value.toPixels(context.resourceResolver)
    when (field) {
      // TODO(t89044330): When yoga is decoupled from Litho, implement these more generically.
      CoreDimenField.WIDTH -> commonProps.widthPx(pixelValue)
      CoreDimenField.HEIGHT -> commonProps.heightPx(pixelValue)
      CoreDimenField.MIN_WIDTH -> commonProps.minWidthPx(pixelValue)
      CoreDimenField.MAX_WIDTH -> commonProps.maxWidthPx(pixelValue)
      CoreDimenField.MIN_HEIGHT -> commonProps.minHeightPx(pixelValue)
      CoreDimenField.MAX_HEIGHT -> commonProps.maxHeightPx(pixelValue)
      CoreDimenField.PADDING_START -> commonProps.paddingPx(YogaEdge.START, pixelValue)
      CoreDimenField.PADDING_TOP -> commonProps.paddingPx(YogaEdge.TOP, pixelValue)
      CoreDimenField.PADDING_END -> commonProps.paddingPx(YogaEdge.END, pixelValue)
      CoreDimenField.PADDING_BOTTOM -> commonProps.paddingPx(YogaEdge.BOTTOM, pixelValue)
      CoreDimenField.PADDING_LEFT -> commonProps.paddingPx(YogaEdge.LEFT, pixelValue)
      CoreDimenField.PADDING_RIGHT -> commonProps.paddingPx(YogaEdge.RIGHT, pixelValue)
      CoreDimenField.PADDING_HORIZONTAL -> commonProps.paddingPx(YogaEdge.HORIZONTAL, pixelValue)
      CoreDimenField.PADDING_VERTICAL -> commonProps.paddingPx(YogaEdge.VERTICAL, pixelValue)
      CoreDimenField.PADDING_ALL -> commonProps.paddingPx(YogaEdge.ALL, pixelValue)
      CoreDimenField.MARGIN_START -> commonProps.marginPx(YogaEdge.START, pixelValue)
      CoreDimenField.MARGIN_TOP -> commonProps.marginPx(YogaEdge.TOP, pixelValue)
      CoreDimenField.MARGIN_END -> commonProps.marginPx(YogaEdge.END, pixelValue)
      CoreDimenField.MARGIN_BOTTOM -> commonProps.marginPx(YogaEdge.BOTTOM, pixelValue)
      CoreDimenField.MARGIN_LEFT -> commonProps.marginPx(YogaEdge.LEFT, pixelValue)
      CoreDimenField.MARGIN_RIGHT -> commonProps.marginPx(YogaEdge.RIGHT, pixelValue)
      CoreDimenField.MARGIN_HORIZONTAL -> commonProps.marginPx(YogaEdge.HORIZONTAL, pixelValue)
      CoreDimenField.MARGIN_VERTICAL -> commonProps.marginPx(YogaEdge.VERTICAL, pixelValue)
      CoreDimenField.MARGIN_ALL -> commonProps.marginPx(YogaEdge.ALL, pixelValue)
    }
  }
}

/** Common style item for all core float styles. See note on [CoreDimenField] about this pattern. */
@PublishedApi
internal class CoreFloatStyleItem(override val field: CoreFloatField, override val value: Float) :
    StyleItem<Float> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    when (field) {
      // TODO(t89044330): When yoga is decoupled from Litho, implement these more generically.
      CoreFloatField.WIDTH_PERCENT -> commonProps.widthPercent(value)
      CoreFloatField.HEIGHT_PERCENT -> commonProps.heightPercent(value)
      CoreFloatField.MIN_WIDTH_PERCENT -> commonProps.minWidthPercent(value)
      CoreFloatField.MAX_WIDTH_PERCENT -> commonProps.maxWidthPercent(value)
      CoreFloatField.MIN_HEIGHT_PERCENT -> commonProps.minHeightPercent(value)
      CoreFloatField.MAX_HEIGHT_PERCENT -> commonProps.maxHeightPercent(value)
    }
  }
}

/** Sets a specific preferred width for this component when its parent lays it out. */
inline fun Style.width(width: Dimen): Style = this + CoreDimenStyleItem(CoreDimenField.WIDTH, width)

/** Sets a specific preferred height for this component when its parent lays it out. */
inline fun Style.height(height: Dimen): Style =
    this + CoreDimenStyleItem(CoreDimenField.HEIGHT, height)

/** Sets a specific preferred percent width for this component when its parent lays it out. */
inline fun Style.widthPercent(widthPercent: Float): Style =
    this + CoreFloatStyleItem(CoreFloatField.WIDTH_PERCENT, widthPercent)

/** Sets a specific preferred percent height for this component when its parent lays it out. */
inline fun Style.heightPercent(heightPercent: Float): Style =
    this + CoreFloatStyleItem(CoreFloatField.HEIGHT_PERCENT, heightPercent)

/** Sets a preferred minimum width for this component when its parent lays it out. */
inline fun Style.minWidth(minWidth: Dimen): Style =
    this + CoreDimenStyleItem(CoreDimenField.MIN_WIDTH, minWidth)

/** Sets a preferred maximum width for this component when its parent lays it out. */
inline fun Style.maxWidth(maxWidth: Dimen): Style =
    this + CoreDimenStyleItem(CoreDimenField.MAX_WIDTH, maxWidth)

/** Sets a preferred minimum height for this component when its parent lays it out. */
inline fun Style.minHeight(minHeight: Dimen): Style =
    this + CoreDimenStyleItem(CoreDimenField.MIN_HEIGHT, minHeight)

/** Sets a preferred maximum height for this component when its parent lays it out. */
inline fun Style.maxHeight(maxHeight: Dimen): Style =
    this + CoreDimenStyleItem(CoreDimenField.MAX_HEIGHT, maxHeight)

/** Sets a specific minimum percent width for this component when its parent lays it out. */
inline fun Style.minWidthPercent(minWidthPercent: Float): Style =
    this + CoreFloatStyleItem(CoreFloatField.MIN_WIDTH_PERCENT, minWidthPercent)

/** Sets a specific maximum percent width for this component when its parent lays it out. */
inline fun Style.maxWidthPercent(maxWidthPercent: Float): Style =
    this + CoreFloatStyleItem(CoreFloatField.MAX_WIDTH_PERCENT, maxWidthPercent)

/** Sets a specific minimum percent height for this component when its parent lays it out. */
inline fun Style.minHeightPercent(minHeightPercent: Float): Style =
    this + CoreFloatStyleItem(CoreFloatField.MIN_HEIGHT_PERCENT, minHeightPercent)

/** Sets a specific maximum percent height for this component when its parent lays it out. */
inline fun Style.maxHeightPercent(maxHeightPercent: Float): Style =
    this + CoreFloatStyleItem(CoreFloatField.MAX_HEIGHT_PERCENT, maxHeightPercent)

/** Defines padding on the component on a per-edge basis. */
inline fun Style.padding(
    all: Dimen? = null,
    horizontal: Dimen? = null,
    vertical: Dimen? = null,
    start: Dimen? = null,
    top: Dimen? = null,
    end: Dimen? = null,
    bottom: Dimen? = null,
    left: Dimen? = null,
    right: Dimen? = null,
): Style =
    this +
        all?.let { CoreDimenStyleItem(CoreDimenField.PADDING_ALL, it) } +
        horizontal?.let { CoreDimenStyleItem(CoreDimenField.PADDING_HORIZONTAL, it) } +
        vertical?.let { CoreDimenStyleItem(CoreDimenField.PADDING_VERTICAL, it) } +
        start?.let { CoreDimenStyleItem(CoreDimenField.PADDING_START, it) } +
        top?.let { CoreDimenStyleItem(CoreDimenField.PADDING_TOP, it) } +
        end?.let { CoreDimenStyleItem(CoreDimenField.PADDING_END, it) } +
        bottom?.let { CoreDimenStyleItem(CoreDimenField.PADDING_BOTTOM, it) } +
        left?.let { CoreDimenStyleItem(CoreDimenField.PADDING_LEFT, it) } +
        right?.let { CoreDimenStyleItem(CoreDimenField.PADDING_RIGHT, it) }

/** Defines margin around the component on a per-edge basis. */
inline fun Style.margin(
    all: Dimen? = null,
    horizontal: Dimen? = null,
    vertical: Dimen? = null,
    start: Dimen? = null,
    top: Dimen? = null,
    end: Dimen? = null,
    bottom: Dimen? = null,
    left: Dimen? = null,
    right: Dimen? = null,
): Style =
    this +
        all?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_ALL, it) } +
        horizontal?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_HORIZONTAL, it) } +
        vertical?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_VERTICAL, it) } +
        start?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_START, it) } +
        top?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_TOP, it) } +
        end?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_END, it) } +
        bottom?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_BOTTOM, it) } +
        left?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_LEFT, it) } +
        right?.let { CoreDimenStyleItem(CoreDimenField.MARGIN_RIGHT, 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.flexbox

import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.litho.Border
import com.facebook.litho.CommonProps
import com.facebook.litho.ComponentContext
import com.facebook.litho.Dimen
import com.facebook.litho.Style
import com.facebook.litho.StyleItem
import com.facebook.litho.StyleItemField
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaDirection
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaPositionType

/** Enums for [FlexboxDimenStyleItem]. */
@PublishedApi
internal enum class FlexboxDimenField : StyleItemField {
  FLEX_BASIS,
  POSITION_ALL,
  POSITION_START,
  POSITION_TOP,
  POSITION_END,
  POSITION_BOTTOM,
  POSITION_LEFT,
  POSITION_RIGHT,
  POSITION_HORIZONTAL,
  POSITION_VERTICAL,
}

/** Enums for [FlexboxFloatStyleItem]. */
@PublishedApi
internal enum class FlexboxFloatField : StyleItemField {
  FLEX,
  FLEX_GROW,
  FLEX_SHRINK,
  FLEX_BASIS_PERCENT,
  ASPECT_RATIO,
}

/** Enums for [FlexboxObjectStyleItem]. */
@PublishedApi
internal enum class FlexboxObjectField : StyleItemField {
  ALIGN_SELF,
  BORDER,
  LAYOUT_DIRECTION,
  MARGIN_AUTO,
  POSITION_TYPE,
  IS_REFERENCE_BASELINE,
  USE_HEIGHT_AS_BASELINE,
}

/** Common style item for all dimen styles. See note on [FlexboxDimenField] about this pattern. */
@PublishedApi
@DataClassGenerate
internal data class FlexboxDimenStyleItem(
    override val field: FlexboxDimenField,
    override val value: Dimen
) : StyleItem<Dimen> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    val pixelValue = value.toPixels(context.resourceResolver)
    when (field) {
      FlexboxDimenField.FLEX_BASIS -> commonProps.flexBasisPx(pixelValue)
      FlexboxDimenField.POSITION_ALL -> commonProps.positionPx(YogaEdge.ALL, pixelValue)
      FlexboxDimenField.POSITION_START -> commonProps.positionPx(YogaEdge.START, pixelValue)
      FlexboxDimenField.POSITION_END -> commonProps.positionPx(YogaEdge.END, pixelValue)
      FlexboxDimenField.POSITION_TOP -> commonProps.positionPx(YogaEdge.TOP, pixelValue)
      FlexboxDimenField.POSITION_BOTTOM -> commonProps.positionPx(YogaEdge.BOTTOM, pixelValue)
      FlexboxDimenField.POSITION_LEFT -> commonProps.positionPx(YogaEdge.LEFT, pixelValue)
      FlexboxDimenField.POSITION_RIGHT -> commonProps.positionPx(YogaEdge.RIGHT, pixelValue)
      FlexboxDimenField.POSITION_HORIZONTAL ->
          commonProps.positionPx(YogaEdge.HORIZONTAL, pixelValue)
      FlexboxDimenField.POSITION_VERTICAL -> commonProps.positionPx(YogaEdge.VERTICAL, pixelValue)
    }
  }
}

/** Common style item for all float styles. See note on [FlexboxDimenField] about this pattern. */
@PublishedApi
internal class FloatStyleItem(override val field: FlexboxFloatField, override val value: Float) :
    StyleItem<Float> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    when (field) {
      FlexboxFloatField.FLEX -> commonProps.flex(value)
      FlexboxFloatField.FLEX_GROW -> commonProps.flexGrow(value)
      FlexboxFloatField.FLEX_SHRINK -> commonProps.flexShrink(value)
      FlexboxFloatField.FLEX_BASIS_PERCENT -> commonProps.flexBasisPercent(value)
      FlexboxFloatField.ASPECT_RATIO -> commonProps.aspectRatio(value)
    }
  }
}

/** Common style item for all object styles. See note on [FlexboxDimenField] about this pattern. */
@PublishedApi
internal class FlexboxObjectStyleItem(
    override val field: FlexboxObjectField,
    override val value: Any?
) : StyleItem<Any?> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    when (field) {
      FlexboxObjectField.ALIGN_SELF -> value?.let { commonProps.alignSelf(it as YogaAlign) }
      FlexboxObjectField.BORDER -> commonProps.border(value as Border?)
      FlexboxObjectField.LAYOUT_DIRECTION -> commonProps.layoutDirection(value as YogaDirection)
      FlexboxObjectField.POSITION_TYPE ->
          value?.let { commonProps.positionType(it as YogaPositionType) }
      FlexboxObjectField.MARGIN_AUTO -> value?.let { commonProps.marginAuto(it as YogaEdge) }
      FlexboxObjectField.IS_REFERENCE_BASELINE ->
          value?.let { commonProps.isReferenceBaseline(it as Boolean) }
      FlexboxObjectField.USE_HEIGHT_AS_BASELINE ->
          value?.let { commonProps.useHeightAsBaseline(it as Boolean) }
    }
  }
}

/**
 * Flex allows you to define how this component should take up space within its parent. It's
 * comprised of the following properties:
 *
 * **flex-grow**: This component should take up remaining space in its parent. If multiple children
 * of the parent have a flex-grow set, the extra space is divided up based on proportions of
 * flex-grow values, i.e. a child with flex-grow of 2 will get twice as much of the space as its
 * sibling with flex-grow of 1.
 *
 * **flex-shrink**: This component should shrink if necessary. Similar to flex-grow, the value
 * determines the proportion of space *taken* from each child. Setting a flex-shink of 0 means the
 * child won't shrink.
 *
 * **flex-basis**: Defines the default size of the component before extra space is distributed. If
 * omitted, the measured size of the content (or the width/height styles) will be used instead.
 *
 * **flex-basis-percent**: see **flex-basis**. Defines the default size as a percentage of its
 * parent's size. Values should be from 0 to 100.
 * - See https://css-tricks.com/snippets/css/a-guide-to-flexbox/ for more documentation on flexbox
 *   properties.
 * - See https://yogalayout.com/ for a web-based playground for trying out flexbox layouts.
 *
 * Defaults: flex-grow = 0, flex-shrink = 1, flex-basis = null, flex-basis-percent = null
 */
inline fun Style.flex(
    grow: Float? = null,
    shrink: Float? = null,
    basis: Dimen? = null,
    basisPercent: Float? = null
): Style =
    this +
        grow?.let { FloatStyleItem(FlexboxFloatField.FLEX_GROW, it) } +
        shrink?.let { FloatStyleItem(FlexboxFloatField.FLEX_SHRINK, it) } +
        basis?.let { FlexboxDimenStyleItem(FlexboxDimenField.FLEX_BASIS, it) } +
        basisPercent?.let { FloatStyleItem(FlexboxFloatField.FLEX_BASIS_PERCENT, it) }

/**
 * Defines how a child should be aligned with a Row or Column, overriding the parent's align-items
 * property for this child.
 * - See https://css-tricks.com/snippets/css/a-guide-to-flexbox/ for more documentation on flexbox
 *   properties.
 * - See https://yogalayout.com/ for a web-based playground for trying out flexbox layouts.
 */
inline fun Style.alignSelf(align: YogaAlign): Style =
    this + FlexboxObjectStyleItem(FlexboxObjectField.ALIGN_SELF, align)

/**
 * Defines an aspect ratio for this component, meaning the ratio of width to height. This means if
 * aspectRatio is set to 2 and width is calculated to be 50px, then height will be 100px.
 *
 * Note: This property is not part of the flexbox standard.
 */
inline fun Style.aspectRatio(aspectRatio: Float): Style =
    this + FloatStyleItem(FlexboxFloatField.ASPECT_RATIO, aspectRatio)

/**
 * Used in conjunction with [positionType] to define how a component should be positioned in its
 * parent.
 *
 * For positionType of ABSOLUTE: the values specified here will define how inset the child is from
 * the same edge on its parent. E.g. for `position(0.px, 0.px, 0.px, 0.px)`, it will be the full
 * size of the parent (no insets). For `position(0.px, 10.px, 0.px, 10.px)`, the child will be the
 * full width of the parent, but inset by 10px on the top and bottom.
 *
 * For positionType of RELATIVE: the values specified here will define how the child is positioned
 * relative to where that edge would have normally been positioned.
 *
 * See https://yogalayout.com/ for a web-based playground for trying out flexbox layouts.
 */
inline fun Style.position(
    all: Dimen? = null,
    start: Dimen? = null,
    top: Dimen? = null,
    end: Dimen? = null,
    bottom: Dimen? = null,
    left: Dimen? = null,
    right: Dimen? = null,
    vertical: Dimen? = null,
    horizontal: Dimen? = null
): Style =
    this +
        all?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_ALL, it) } +
        start?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_START, it) } +
        top?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_TOP, it) } +
        end?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_END, it) } +
        bottom?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_BOTTOM, it) } +
        left?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_LEFT, it) } +
        right?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_RIGHT, it) } +
        vertical?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_VERTICAL, it) } +
        horizontal?.let { FlexboxDimenStyleItem(FlexboxDimenField.POSITION_HORIZONTAL, it) }

/** See docs in [position]. */
inline fun Style.positionType(positionType: YogaPositionType): Style =
    this + FlexboxObjectStyleItem(FlexboxObjectField.POSITION_TYPE, positionType)

/**
 * Describes how a [Border] should be drawn around this component. Setting this property will cause
 * the Component to be represented as a View at mount time if it wasn't going to already.
 */
inline fun Style.border(border: Border): Style =
    this + FlexboxObjectStyleItem(FlexboxObjectField.BORDER, border)

/**
 * Describes the RTL/LTR direction of component. Determines whether {@link YogaEdge#START} and
 * {@link YogaEdge#END} will resolve to the left or right side, among other things. INHERIT
 * indicates this setting will be inherited from this component's parent. Setting this property will
 * cause the Component to be represented as a View at mount time if it wasn't going to already.
 *
 * <p>Default: {@link YogaDirection#INHERIT}
 */
inline fun Style.layoutDirection(layoutDirection: YogaDirection): Style =
    this + FlexboxObjectStyleItem(FlexboxObjectField.LAYOUT_DIRECTION, layoutDirection)

/**
 * Sets margin value for specified edge to auto. The item will extend the margin for this edge to
 * occupy the extra space in the parent, depending on the direction (Row or Column).
 */
inline fun Style.marginAuto(edge: YogaEdge): Style =
    this + FlexboxObjectStyleItem(FlexboxObjectField.MARGIN_AUTO, edge)

inline fun Style.isReferenceBaseline(isReferenceBaseline: Boolean): Style =
    this + FlexboxObjectStyleItem(FlexboxObjectField.IS_REFERENCE_BASELINE, isReferenceBaseline)

inline fun Style.useHeightAsBaseline(useHeightAsBaseline: Boolean): Style =
    this + FlexboxObjectStyleItem(FlexboxObjectField.USE_HEIGHT_AS_BASELINE, useHeightAsBaseline)


/*
 * 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.transition

import com.facebook.litho.ComponentScope
import com.facebook.litho.Transition
import com.facebook.litho.annotations.Hook
import com.facebook.rendercore.transitions.TransitionUtils

/** Defines single or multiple [Transition] animations for the given component */
@Hook
fun ComponentScope.useTransition(transition: Transition?) {
  transition ?: return
  TransitionUtils.setOwnerKey(transition, context.globalKey)
  val transitionsList = transitions ?: ArrayList<Transition>()
  transitionsList.add(transition)
  transitions = transitionsList
}


/*
 * 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.transition

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.Transition
import com.facebook.litho.Transition.SPRING_WITH_OVERSHOOT
import com.facebook.litho.animation.AnimatedProperties
import com.facebook.litho.core.height
import com.facebook.litho.dp
import com.facebook.litho.view.alpha

private const val CMPONENT_A_KEY = "componentAKey"
private const val COMPONENT_B_KEY = "componentBKey"
private const val EXPAND_TO_REVEAL_KEY = "expandToReveal"

/**
 * Component implementing the Expand to Reveal behavior
 *
 * @param isExpanded is the component currently expanded or hidden
 * @param component that will be revealed
 * @param transitionAnimator optional param for the custom [TransitionAnimator]
 */
class ExpandToReveal(
    private val isExpanded: Boolean,
    private val component: Component,
    private val transitionAnimator: Transition.TransitionAnimator = SPRING_WITH_OVERSHOOT
) : KComponent() {

  override fun ComponentScope.render(): Component? {
    val collapsedStyle = Style.height(0.dp).alpha(0f).transitionKey(context, EXPAND_TO_REVEAL_KEY)
    val normalStyle = Style.alpha(1f).transitionKey(context, EXPAND_TO_REVEAL_KEY)
    useTransition(
        Transition.parallel<Transition.BaseTransitionUnitsBuilder>(
            Transition.create(EXPAND_TO_REVEAL_KEY)
                .animate(AnimatedProperties.ALPHA)
                .animator(transitionAnimator),
            Transition.allLayout()))
    return Column(style = if (isExpanded) collapsedStyle else normalStyle) { child(component) }
  }
}

class CrossFade(
    private val showComponentB: Boolean,
    private val componentA: Component,
    private val componentB: Component,
    private val transitionType: TransitionType = TransitionType.FADE_IN_FADE_OUT
) : KComponent() {
  override fun ComponentScope.render(): Component? {
    useTransition(getAppearDisappearTransition(transitionType))

    return if (showComponentB) {
      Column(style = Style.transitionKey(context, COMPONENT_B_KEY)) { child(componentB) }
    } else {
      Column(style = Style.transitionKey(context, CMPONENT_A_KEY)) { child(componentA) }
    }
  }

  private fun getAppearDisappearTransition(transitionType: TransitionType): Transition =
      when (transitionType) {
        TransitionType.FADE_IN_FADE_OUT ->
            Transition.parallel<Transition.BaseTransitionUnitsBuilder>(
                Transition.create(CMPONENT_A_KEY)
                    .animate(AnimatedProperties.ALPHA)
                    .appearFrom(0f)
                    .disappearTo(0f),
                Transition.create(COMPONENT_B_KEY)
                    .animate(AnimatedProperties.ALPHA)
                    .appearFrom(0f)
                    .disappearTo(0f),
                Transition.allLayout())
      }
}

enum class TransitionType {
  FADE_IN_FADE_OUT,
}


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

@file:Suppress("MatchingDeclarationName")

package com.facebook.litho.transition

import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.litho.CommonProps
import com.facebook.litho.ComponentContext
import com.facebook.litho.Style
import com.facebook.litho.StyleItem
import com.facebook.litho.StyleItemField
import com.facebook.litho.Transition
import com.facebook.litho.Transition.TransitionKeyType

@DataClassGenerate
data class TransitionKeyStyleItemValue(
    val transitionKey: String?,
    val transitionKeyType: TransitionKeyType
)

enum class TransitionKeyFields : StyleItemField {
  TRANSITION_KEY,
}

@PublishedApi
@DataClassGenerate
internal data class TransitionKeyStyleItem(
    val context: ComponentContext,
    val transitionKey: String?,
    val transitionKeyType: TransitionKeyType
) : StyleItem<TransitionKeyStyleItemValue> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    commonProps.transitionKey(transitionKey, this.context.globalKey)
    commonProps.transitionKeyType(transitionKeyType)
  }

  override val value: TransitionKeyStyleItemValue =
      TransitionKeyStyleItemValue(transitionKey, transitionKeyType)
  override val field: TransitionKeyFields = TransitionKeyFields.TRANSITION_KEY
}

/**
 * Sets transition key and [Transition.TransitionKeyType] on the View this Component mounts to.
 * Setting this property will cause the Component to be represented as a View at mount time if it
 * wasn't going to already.
 */
inline fun Style.transitionKey(
    context: ComponentContext,
    transitionKey: String?,
    transitionKeyType: TransitionKeyType = TransitionKeyType.LOCAL
): Style = this + TransitionKeyStyleItem(context, transitionKey, transitionKeyType)


/*
 * 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.view

import android.animation.StateListAnimator
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.SparseArray
import android.view.ViewOutlineProvider
import androidx.annotation.ColorInt
import androidx.annotation.IdRes
import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.litho.ClickEvent
import com.facebook.litho.CommonProps
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Dimen
import com.facebook.litho.InterceptTouchEvent
import com.facebook.litho.LongClickEvent
import com.facebook.litho.Style
import com.facebook.litho.StyleItem
import com.facebook.litho.StyleItemField
import com.facebook.litho.TouchEvent
import com.facebook.litho.drawable.ComparableColorDrawable
import com.facebook.litho.eventHandler
import com.facebook.litho.eventHandlerWithReturn
import com.facebook.yoga.YogaEdge

/** Enums for [ObjectStyleItem]. */
@PublishedApi
internal enum class ObjectField : StyleItemField {
  BACKGROUND,
  CLICKABLE,
  CLIP_CHILDREN,
  CLIP_TO_OUTLINE,
  DUPLICATE_CHILDREN_STATES,
  DUPLICATE_PARENT_STATE,
  FOCUSABLE,
  FOREGROUND,
  ON_CLICK,
  ON_INTERCEPT_TOUCH,
  ON_LONG_CLICK,
  ON_TOUCH,
  OUTLINE_PROVIDER,
  SELECTED,
  STATE_LIST_ANIMATOR,
  TEST_KEY,
  TRANSITION_NAME,
  WRAP_IN_VIEW,
  VIEW_ID,
  VIEW_TAG,
  VIEW_TAGS,
  ENABLED,
}

/** Enums for [FloatStyleItem]. */
@PublishedApi
internal enum class FloatField : StyleItemField {
  ALPHA,
  ROTATION,
  ROTATION_X,
  ROTATION_Y,
  SCALE,
}

/** Enums for [FloatStyleItem]. */
@PublishedApi
internal enum class DimenField : StyleItemField {
  TOUCH_EXPANSION_START,
  TOUCH_EXPANSION_TOP,
  TOUCH_EXPANSION_END,
  TOUCH_EXPANSION_BOTTOM,
  TOUCH_EXPANSION_LEFT,
  TOUCH_EXPANSION_RIGHT,
  TOUCH_EXPANSION_HORIZONTAL,
  TOUCH_EXPANSION_VERTICAL,
  TOUCH_EXPANSION_ALL,
  ELEVATION,
}

/** Common style item for all object styles. See note on [DimenField] about this pattern. */
@PublishedApi
@DataClassGenerate
internal data class ObjectStyleItem(override val field: ObjectField, override val value: Any?) :
    StyleItem<Any?> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    when (field) {
      ObjectField.BACKGROUND -> commonProps.background(value as Drawable?)
      ObjectField.CLICKABLE -> commonProps.clickable(value as Boolean)
      ObjectField.CLIP_CHILDREN -> commonProps.clipChildren(value as Boolean)
      ObjectField.CLIP_TO_OUTLINE -> commonProps.clipToOutline(value as Boolean)
      ObjectField.DUPLICATE_CHILDREN_STATES -> commonProps.duplicateChildrenStates(value as Boolean)
      ObjectField.DUPLICATE_PARENT_STATE -> commonProps.duplicateParentState(value as Boolean)
      ObjectField.FOCUSABLE -> commonProps.focusable(value as Boolean)
      ObjectField.FOREGROUND -> commonProps.foreground(value as Drawable?)
      ObjectField.ON_CLICK -> {
        val clickHandler = if (value != null) eventHandler(value as (ClickEvent) -> Unit) else null
        commonProps.clickHandler(clickHandler)
      }
      ObjectField.ON_LONG_CLICK -> {
        val longClickHandler =
            if (value != null) eventHandlerWithReturn(value as ((LongClickEvent) -> Boolean))
            else null
        commonProps.longClickHandler(longClickHandler)
      }
      ObjectField.ON_INTERCEPT_TOUCH -> {
        val interceptTouchHandler =
            if (value != null) eventHandlerWithReturn(value as ((InterceptTouchEvent) -> Boolean))
            else null
        commonProps.interceptTouchHandler(interceptTouchHandler)
      }
      ObjectField.ON_TOUCH -> {
        val touchHandler =
            if (value != null) eventHandler(value as ((TouchEvent) -> Unit)) else null
        commonProps.touchHandler(touchHandler)
      }
      ObjectField.SELECTED -> commonProps.selected(value as Boolean)
      ObjectField.STATE_LIST_ANIMATOR -> commonProps.stateListAnimator(value as StateListAnimator?)
      ObjectField.TEST_KEY -> commonProps.testKey(value as String?)
      ObjectField.TRANSITION_NAME -> commonProps.transitionName(value as String?)
      ObjectField.WRAP_IN_VIEW -> commonProps.wrapInView()
      ObjectField.VIEW_ID -> commonProps.viewId(value as Int)
      ObjectField.VIEW_TAG -> commonProps.viewTag(value)
      ObjectField.VIEW_TAGS -> commonProps.viewTags(value as SparseArray<Any>)
      ObjectField.OUTLINE_PROVIDER -> commonProps.outlineProvider(value as ViewOutlineProvider?)
      ObjectField.ENABLED -> commonProps.enabled(value as Boolean)
    }
  }
}

/** Common style item for all float styles. See note on [FloatField] about this pattern. */
@PublishedApi
@DataClassGenerate
internal data class FloatStyleItem(override val field: FloatField, override val value: Float) :
    StyleItem<Float> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    when (field) {
      FloatField.ALPHA -> commonProps.alpha(value)
      FloatField.ROTATION -> commonProps.rotation(value)
      FloatField.ROTATION_X -> commonProps.rotationX(value)
      FloatField.ROTATION_Y -> commonProps.rotationY(value)
      FloatField.SCALE -> commonProps.scale(value)
    }
  }
}

/** Common style item for all float styles. See note on [FloatField] about this pattern. */
@PublishedApi
@DataClassGenerate
internal data class DimenStyleItem(override val field: DimenField, override val value: Dimen) :
    StyleItem<Dimen> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    val pixelValue = value.toPixels(context.resourceResolver)
    when (field) {
      DimenField.TOUCH_EXPANSION_START -> commonProps.touchExpansionPx(YogaEdge.START, pixelValue)
      DimenField.TOUCH_EXPANSION_TOP -> commonProps.touchExpansionPx(YogaEdge.TOP, pixelValue)
      DimenField.TOUCH_EXPANSION_END -> commonProps.touchExpansionPx(YogaEdge.END, pixelValue)
      DimenField.TOUCH_EXPANSION_BOTTOM -> commonProps.touchExpansionPx(YogaEdge.BOTTOM, pixelValue)
      DimenField.TOUCH_EXPANSION_LEFT -> commonProps.touchExpansionPx(YogaEdge.LEFT, pixelValue)
      DimenField.TOUCH_EXPANSION_RIGHT -> commonProps.touchExpansionPx(YogaEdge.RIGHT, pixelValue)
      DimenField.TOUCH_EXPANSION_HORIZONTAL ->
          commonProps.touchExpansionPx(YogaEdge.HORIZONTAL, pixelValue)
      DimenField.TOUCH_EXPANSION_VERTICAL ->
          commonProps.touchExpansionPx(YogaEdge.VERTICAL, pixelValue)
      DimenField.TOUCH_EXPANSION_ALL -> commonProps.touchExpansionPx(YogaEdge.ALL, pixelValue)
      DimenField.ELEVATION -> commonProps.shadowElevationPx(pixelValue.toFloat())
    }
  }
}

/**
 * Sets an alpha on the View this Component mounts to. Setting this property will cause the
 * Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setAlpha]
 */
inline fun Style.alpha(alpha: Float): Style = this + FloatStyleItem(FloatField.ALPHA, alpha)

/**
 * Sets a background on the View this Component mounts to. Setting this property will cause the
 * Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setBackground]
 */
inline fun Style.background(background: Drawable?): Style =
    this + ObjectStyleItem(ObjectField.BACKGROUND, background)

/**
 * Sets ColorDrawable of the given color as a background on the View this Component mounts to.
 * Setting this property will cause the Component to be represented as a View at mount time if it
 * wasn't going to already.
 *
 * See [android.view.View.setBackgroundColor]
 */
inline fun Style.backgroundColor(@ColorInt backgroundColor: Int): Style =
    this + ObjectStyleItem(ObjectField.BACKGROUND, ComparableColorDrawable.create(backgroundColor))

inline fun Style.backgroundColor(@ColorInt backgroundColor: Long): Style =
    backgroundColor(backgroundColor.toInt())

/**
 * Sets if the View this Component mounts to should be clickable. Setting this property will cause
 * the Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setClickable]
 */
inline fun Style.clickable(isClickable: Boolean): Style =
    this + ObjectStyleItem(ObjectField.CLICKABLE, isClickable)

/**
 * Sets if the View this Component mounts to should be enabled. Setting this property will cause the
 * Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setClickable]
 */
inline fun Style.enabled(isEnabled: Boolean): Style =
    this + ObjectStyleItem(ObjectField.ENABLED, isEnabled)

/**
 * Setting this to false allows child views of this view to draw outside its bounds, overriding the
 * default behavior. It only applies to direct children. Setting this property will cause the
 * Component to be represented as a View at mount time if it wasn't going to already.
 */
inline fun Style.clipChildren(clipChildren: Boolean): Style =
    this + ObjectStyleItem(ObjectField.CLIP_CHILDREN, clipChildren)

/**
 * Sets whether the View's Outline should be used to clip the contents of the View. Setting this
 * property will cause the Component to be represented as a View at mount time if it wasn't going to
 * already.
 *
 * See [android.view.View.setClipToOutline]
 */
inline fun Style.clipToOutline(clipToOutline: Boolean): Style =
    this + ObjectStyleItem(ObjectField.CLIP_TO_OUTLINE, clipToOutline)

/**
 * Sets whether this ViewGroup's drawable states also include its children's drawable states. This
 * is used, for example, to make a group appear to be focused when its child EditText or button is
 * focused. Setting this property will cause the Component to be represented as a View at mount time
 * if it wasn't going to already.
 *
 * See [android.view.ViewGroup.setAddStatesFromChildren]
 */
inline fun Style.duplicateChildrenStates(duplicateChildrenStates: Boolean): Style =
    this + ObjectStyleItem(ObjectField.DUPLICATE_CHILDREN_STATES, duplicateChildrenStates)

/**
 * Sets whether the View gets its drawable state (focused, pressed, etc.) from its direct parent
 * rather than from itself. Setting this property will cause the Component to be represented as a
 * View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setDuplicateParentStateEnabled]
 */
inline fun Style.duplicateParentState(duplicateParentState: Boolean): Style =
    this + ObjectStyleItem(ObjectField.DUPLICATE_PARENT_STATE, duplicateParentState)

/**
 * Sets an elevation on the View this Component mounts to. Setting this property will cause the
 * Component to be represented as a View at mount time if it wasn't going to already.
 *
 * NOTE: This style will be ignored pre-API 21.
 *
 * See [android.view.View.setElevation]
 */
inline fun Style.elevation(elevation: Dimen): Style =
    this + DimenStyleItem(DimenField.ELEVATION, elevation)

/**
 * Setting this property will cause the Component to be focusable. Setting this property will cause
 * the Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setFocusable]
 */
inline fun Style.focusable(isFocusable: Boolean): Style =
    this + ObjectStyleItem(ObjectField.FOCUSABLE, isFocusable)

/**
 * Sets a foreground on the View this Component mounts to. Setting this property will cause the
 * Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setForeground]
 */
inline fun Style.foreground(foreground: Drawable?): Style =
    this + ObjectStyleItem(ObjectField.FOREGROUND, foreground)

/**
 * Sets ColorDrawable of the given color as a background on the View this Component mounts to.
 * Setting this property will cause the Component to be represented as a View at mount time if it
 * wasn't going to already.
 *
 * See [android.view.View.setForeground]
 */
inline fun Style.foregroundColor(@ColorInt foregroundColor: Int): Style =
    this + ObjectStyleItem(ObjectField.FOREGROUND, ComparableColorDrawable.create(foregroundColor))

/**
 * Sets a listener that will invoke the given lambda when this Component is clicked but only if
 * [enabled] is true. If enabled, setting this property will cause the Component to be represented
 * as a View at mount time if it wasn't going to already.
 */
inline fun Style.onClick(enabled: Boolean = true, noinline action: (ClickEvent) -> Unit): Style =
    this + ObjectStyleItem(ObjectField.ON_CLICK, if (enabled) action else null)

/**
 * Sets a listener that will invoke the given lambda when this Component is long clicked but only if
 * [enabled] is true. If enabled, setting this property will cause the Component to be represented
 * as a View at mount time if it wasn't going to already.
 */
inline fun Style.onLongClick(
    enabled: Boolean = true,
    noinline action: (LongClickEvent) -> Boolean
): Style = this + ObjectStyleItem(ObjectField.ON_LONG_CLICK, if (enabled) action else null)

/**
 * Sets a listener that will invoke the given lambda when this Component is touched but only if
 * [enabled] is true. If enabled, setting this property will cause the Component to be represented
 * as a View at mount time if it wasn't going to already.
 */
inline fun Style.onTouch(enabled: Boolean = true, noinline action: (TouchEvent) -> Boolean): Style =
    this + ObjectStyleItem(ObjectField.ON_TOUCH, if (enabled) action else null)

/**
 * Sets a listener that will intercept all touch screen motion events but only if [enabled] is true.
 * This allows you to watch events as they are dispatched to your children, and take ownership of
 * the current gesture at any point. Implementations should return true if they intercepted the
 * event and wish to receive subsequent events, and false otherwise. If [enabled] is true, setting
 * this property will cause the Component to be represented as a View at mount time if it wasn't
 * going to already.
 *
 * See [android.view.ViewGroup.onInterceptTouchEvent]
 */
inline fun Style.onInterceptTouch(
    enabled: Boolean = true,
    noinline action: (InterceptTouchEvent) -> Boolean
): Style = this + ObjectStyleItem(ObjectField.ON_INTERCEPT_TOUCH, if (enabled) action else null)

/**
 * Sets the degree that this component is rotated around the pivot point. Increasing the value
 * results in clockwise rotation. By default, the pivot point is centered on the component. Setting
 * this property will cause the Component to be represented as a View at mount time if it wasn't
 * going to already.
 *
 * See [android.view.View.setRotation]
 */
inline fun Style.rotation(rotation: Float): Style =
    this + FloatStyleItem(FloatField.ROTATION, rotation)

/**
 * Sets the degree that this component is rotated around the horizontal axis through the pivot
 * point. Setting this property will cause the Component to be represented as a View at mount time
 * if it wasn't going to already.
 *
 * See [android.view.View.setRotationX]
 */
inline fun Style.rotationX(rotationX: Float): Style =
    this + FloatStyleItem(FloatField.ROTATION_X, rotationX)

/**
 * Sets the degree that this component is rotated around the vertical axis through the pivot point.
 * Setting this property will cause the Component to be represented as a View at mount time if it
 * wasn't going to already.
 *
 * See [android.view.View.setRotationY]
 */
inline fun Style.rotationY(rotationY: Float): Style =
    this + FloatStyleItem(FloatField.ROTATION_Y, rotationY)

/**
 * Sets the scale (scaleX and scaleY) on this component. This is mostly relevant for animations and
 * being able to animate size changes. Otherwise for non-animation usecases, you should use the
 * standard layout properties to control the size of your component. Setting this property will
 * cause the Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setScaleX] [android.view.View.setScaleY]
 */
inline fun Style.scale(scale: Float): Style = this + FloatStyleItem(FloatField.SCALE, scale)

/**
 * Changes the selection state of this Component. Setting this property will cause the Component to
 * be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setSelected]
 */
inline fun Style.selected(isSelected: Boolean): Style =
    this + ObjectStyleItem(ObjectField.SELECTED, isSelected)

/**
 * Attaches the provided StateListAnimator to this Component. Setting this property will cause the
 * Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setStateListAnimator]
 *
 * NOTE: This style will be ignored pre-API 21.
 */
inline fun Style.stateListAnimator(stateListAnimator: StateListAnimator?): Style =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      this + ObjectStyleItem(ObjectField.STATE_LIST_ANIMATOR, stateListAnimator)
    } else {
      this
    }

/**
 * Sets testKey on the View this Component mounts to. Setting this property will cause the Component
 * to be represented as a View at mount time if it wasn't going to already.
 */
inline fun Style.testKey(testKey: String?): Style =
    this + ObjectStyleItem(ObjectField.TEST_KEY, testKey)

/**
 * Sets Activity transition name on the View this Component mounts to. Setting this property will
 * cause the Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setTransitionName]
 */
inline fun Style.transitionName(transitionName: String?): Style =
    this + ObjectStyleItem(ObjectField.TRANSITION_NAME, transitionName)

/**
 * Specifies that this Component should be represented as a View when this hierarchy is mounted.
 *
 * By default a Component does not mount any content. The exceptions to this are if:
 * - The Component renders to a [com.facebook.litho.annotations.MountSpec] with View mount content
 * - The Component has styles that require a View (like [background])
 * - The Component has the wrapInView style
 */
inline fun Style.wrapInView(): Style = this + ObjectStyleItem(ObjectField.WRAP_IN_VIEW, null)

/**
 * Adds a View tag to the [View] this [Component] mounts to. Setting this property will cause the
 * [Component] to be represented as a [View] at mount time if it wasn't going to already.
 *
 * See [android.view.View.setId]
 */
inline fun Style.viewId(@IdRes viewId: Int): Style =
    this + ObjectStyleItem(ObjectField.VIEW_ID, viewId)

/**
 * Adds a View tag to the View this Component mounts to. Setting this property will cause the
 * Component to be represented as a View at mount time if it wasn't going to already.
 *
 * See [android.view.View.setTag]
 */
inline fun Style.viewTag(viewTag: Any): Style =
    this + ObjectStyleItem(ObjectField.VIEW_TAG, viewTag)

/**
 * Adds a set of View tags to the View this Component mounts to. Setting this property will cause
 * the Component to be represented as a View at mount time if it wasn't going to already.
 *
 * For each Int, Any pair in the given SparseArray, `View.setTag(Int, Any)` will be called. These
 * tags can be retrieved with `View.getTag(Int)`
 *
 * See [android.view.View.setTag]
 */
inline fun Style.viewTags(viewTags: SparseArray<out Any>): Style =
    this + ObjectStyleItem(ObjectField.VIEW_TAGS, viewTags)

/**
 * Sets a [ViewOutlineProvider] on the View this Component mounts to. Setting this property will
 * cause the Component to be represented as a View at mount time if it wasn't going to already.
 *
 * NOTE: This style will be ignored pre-API 21.
 *
 * See [android.view.View.setOutlineProvider]
 */
inline fun Style.outlineProvider(outlineProvider: ViewOutlineProvider?): Style =
    this + ObjectStyleItem(ObjectField.OUTLINE_PROVIDER, outlineProvider)

/** Defines touch Expansion area around the component on a per-edge basis. */
inline fun Style.touchExpansion(
    all: Dimen? = null,
    horizontal: Dimen? = null,
    vertical: Dimen? = null,
    start: Dimen? = null,
    top: Dimen? = null,
    end: Dimen? = null,
    bottom: Dimen? = null,
    left: Dimen? = null,
    right: Dimen? = null,
): Style =
    this +
        all?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_ALL, it) } +
        horizontal?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_HORIZONTAL, it) } +
        vertical?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_VERTICAL, it) } +
        start?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_START, it) } +
        top?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_TOP, it) } +
        end?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_END, it) } +
        bottom?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_BOTTOM, it) } +
        left?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_LEFT, it) } +
        right?.let { DimenStyleItem(DimenField.TOUCH_EXPANSION_RIGHT, it) }

enum class ShadowStyleField : StyleItemField {
  SHADOW_ITEM,
}

@DataClassGenerate
data class ShadowStyleItemParams(
    val elevation: Dimen,
    val outlineProvider: ViewOutlineProvider,
    @ColorInt val ambientShadowColor: Int,
    @ColorInt val spotShadowColor: Int
)

@PublishedApi
@DataClassGenerate
internal data class ShadowStyleItem(
    val elevation: Dimen,
    val outlineProvider: ViewOutlineProvider,
    @ColorInt val ambientShadowColor: Int,
    @ColorInt val spotShadowColor: Int
) : StyleItem<ShadowStyleItemParams> {

  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    commonProps.shadowElevationPx(elevation.toPixels(context.resourceResolver).toFloat())
    commonProps.outlineProvider(outlineProvider)
    commonProps.ambientShadowColor(ambientShadowColor)
    commonProps.spotShadowColor(spotShadowColor)
  }

  override val field: ShadowStyleField = ShadowStyleField.SHADOW_ITEM
  override val value: ShadowStyleItemParams =
      ShadowStyleItemParams(elevation, outlineProvider, ambientShadowColor, spotShadowColor)
}

/**
 * Style for attaching a standard Material Design shadow to a component. Refer to
 * https://material.io/design/environment/light-shadows.html for more information.
 * - **elevation**: Sets the elevation of this component above the surface using
 *   https://developer.android.com/reference/android/view/View#setElevation(float). Larger elevation
 *   values result in larger shadows.
 * - **outlineProvider**: Used to determine the shape of the shadow. If not specified,
 *   https://developer.android.com/reference/android/view/ViewOutlineProvider#BOUNDS will be used to
 *   target the component's bounds.
 * - **ambientShadowColor**: Sets the color of the ambient shadow. Ignored on < API 28 devices. See
 *   https://developer.android.com/reference/android/view/View#setOutlineAmbientShadowColor(int)
 * - **outlineShadowColor**: Sets the color of the spotlight shadow. Ignored on < API 28 devices.
 *   See https://developer.android.com/reference/android/view/View#setOutlineSpotShadowColor(int)
 */
inline fun Style.shadow(
    elevation: Dimen,
    outlineProvider: ViewOutlineProvider = ViewOutlineProvider.BOUNDS,
    @ColorInt ambientShadowColor: Int = Color.BLACK,
    @ColorInt spotShadowColor: Int = Color.BLACK
): Style = this + ShadowStyleItem(elevation, outlineProvider, ambientShadowColor, spotShadowColor)


/*
 * 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.visibility

import com.facebook.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import com.facebook.litho.CommonProps
import com.facebook.litho.ComponentContext
import com.facebook.litho.FocusedVisibleEvent
import com.facebook.litho.FullImpressionVisibleEvent
import com.facebook.litho.InvisibleEvent
import com.facebook.litho.Style
import com.facebook.litho.StyleItem
import com.facebook.litho.StyleItemField
import com.facebook.litho.UnfocusedVisibleEvent
import com.facebook.litho.VisibilityChangedEvent
import com.facebook.litho.VisibleEvent
import com.facebook.litho.eventHandler

/** Enums for [VisibilityStyleItem]. */
@PublishedApi
internal enum class VisibilityField : StyleItemField {
  ON_VISIBLE,
  ON_INVISIBLE,
  ON_FOCUSED,
  ON_UNFOCUSED,
  ON_FULL_IMPRESSION,
  ON_VISIBILITY_CHANGED,
}

/** Enums for [VisibilityFloatStyleItem]. */
@PublishedApi
internal enum class VisibilityFloatField : StyleItemField {
  VISIBLE_HEIGHT_RATIO,
  VISIBLE_WIDTH_RATIO,
}

@PublishedApi
@DataClassGenerate
internal data class VisibilityStyleItem(
    override val field: VisibilityField,
    override val value: Any?,
    val tag: String? = null
) : StyleItem<Any?> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    when (field) {
      VisibilityField.ON_VISIBLE ->
          commonProps.visibleHandler(eventHandler(value as (VisibleEvent) -> Unit, tag))
      VisibilityField.ON_INVISIBLE ->
          commonProps.invisibleHandler(eventHandler(value as (InvisibleEvent) -> Unit, tag))
      VisibilityField.ON_FOCUSED ->
          commonProps.focusedHandler(eventHandler(value as (FocusedVisibleEvent) -> Unit, tag))
      VisibilityField.ON_UNFOCUSED ->
          commonProps.unfocusedHandler(eventHandler(value as (UnfocusedVisibleEvent) -> Unit, tag))
      VisibilityField.ON_FULL_IMPRESSION ->
          commonProps.fullImpressionHandler(
              eventHandler(value as (FullImpressionVisibleEvent) -> Unit, tag))
      VisibilityField.ON_VISIBILITY_CHANGED, ->
          commonProps.visibilityChangedHandler(
              eventHandler(value as (VisibilityChangedEvent) -> Unit, tag))
    }
  }
}

@PublishedApi
internal class VisibilityFloatStyleItem(
    override val field: VisibilityFloatField,
    override val value: Float
) : StyleItem<Float> {
  override fun applyCommonProps(context: ComponentContext, commonProps: CommonProps) {
    when (field) {
      VisibilityFloatField.VISIBLE_HEIGHT_RATIO -> commonProps.visibleHeightRatio(value)
      VisibilityFloatField.VISIBLE_WIDTH_RATIO -> commonProps.visibleWidthRatio(value)
    }
  }
}

/** Registers a callback to be called when any part of the Component becomes visible on screen. */
inline fun Style.onVisible(noinline onVisible: (VisibleEvent) -> Unit): Style =
    this + VisibilityStyleItem(VisibilityField.ON_VISIBLE, onVisible)

@Deprecated("This will be removed soon")
inline fun Style.onVisibleWithTag(tag: String?, noinline onVisible: (VisibleEvent) -> Unit): Style =
    this + VisibilityStyleItem(VisibilityField.ON_VISIBLE, onVisible, tag)

/**
 * Registers a callback to be called when a Component becomes fully invisible (e.g. scrolled off or
 * unmounted)
 */
inline fun Style.onInvisible(noinline onInvisible: (InvisibleEvent) -> Unit): Style =
    this + VisibilityStyleItem(VisibilityField.ON_INVISIBLE, onInvisible)

/**
 * Registers a callback to be called when either the Component occupies at least half of the
 * viewport, or, if the Component is smaller than half the viewport, when it is fully visible.
 */
inline fun Style.onFocusedVisible(noinline onFocused: (FocusedVisibleEvent) -> Unit): Style =
    this + VisibilityStyleItem(VisibilityField.ON_FOCUSED, onFocused)

/**
 * Registers a callback to be called when the Component is no longer focused, i.e. it is not fully
 * visible and does not occupy at least half the viewport.
 */
inline fun Style.onUnfocusedVisible(noinline onUnfocused: (UnfocusedVisibleEvent) -> Unit): Style =
    this + VisibilityStyleItem(VisibilityField.ON_UNFOCUSED, onUnfocused)

/**
 * Registers a callback to be called when all parts of a Component have been made visible at some
 * point, termed a "full impression". A full impression is defined as:
 * - if the Component is smaller than the viewport, when the entire Component is visible
 * - if the Component is bigger than the viewport, when all the edges have passed through the
 *   viewport once
 */
inline fun Style.onFullImpression(
    noinline onFullImpression: (FullImpressionVisibleEvent) -> Unit
): Style = this + VisibilityStyleItem(VisibilityField.ON_FULL_IMPRESSION, onFullImpression)

/** Registers a callback to be called when the visible rect of a Component changes. */
inline fun Style.onVisibilityChanged(
    noinline onVisibilityChanged: (VisibilityChangedEvent) -> Unit
): Style = this + VisibilityStyleItem(VisibilityField.ON_VISIBILITY_CHANGED, onVisibilityChanged)
/**
 * Defines a ratio of the Component height for the visibility callback to be dispatched use together
 * with [onVisible] and/or with [onInvisible].
 */
inline fun Style.visibleHeightRatio(visibleHeightRatio: Float): Style =
    this + VisibilityFloatStyleItem(VisibilityFloatField.VISIBLE_HEIGHT_RATIO, visibleHeightRatio)

/**
 * Defines a ratio of the Component width for the visibility callback to be dispatched use together
 * with [onVisible] and/or with [onInvisible].
 */
inline fun Style.visibleWidthRatio(visibleWidthRatio: Float): Style =
    this + VisibilityFloatStyleItem(VisibilityFloatField.VISIBLE_WIDTH_RATIO, visibleWidthRatio)


/*
 * 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

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.lang.IllegalArgumentException
import org.assertj.core.api.Java6Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith

const val DENSITY = 2f
const val SCALED_DENSITY = 10f

/** Unit tests [Dimen]. */
@RunWith(LithoTestRunner::class)
class DimenTest {

  @Rule @JvmField val expectedException = ExpectedException.none()

  private val resourceResolver = MockResourceResolver(DENSITY, SCALED_DENSITY)

  @Test
  fun `non-negative integer px values are correctly encoded and decoded`() {
    assertThat(0.px.toPixels(resourceResolver)).isEqualTo(0)
    assertThat(1.px.toPixels(resourceResolver)).isEqualTo(1)
    assertThat(10.px.toPixels(resourceResolver)).isEqualTo(10)
    assertThat(101.px.toPixels(resourceResolver)).isEqualTo(101)
    assertThat(9001.px.toPixels(resourceResolver)).isEqualTo(9001)
  }

  @Test
  fun `negative integer px values are correctly encoded and decoded`() {
    assertThat(-1.px.toPixels(resourceResolver)).isEqualTo(-1)
    assertThat(-10.px.toPixels(resourceResolver)).isEqualTo(-10)
    assertThat(-101.px.toPixels(resourceResolver)).isEqualTo(-101)
    assertThat(-9001.px.toPixels(resourceResolver)).isEqualTo(-9001)
  }

  @Test
  fun `non-negative integer dp values are correctly encoded and decoded`() {
    assertThat(0.dp.toPixels(resourceResolver)).isEqualTo(0)
    assertThat(1.dp.toPixels(resourceResolver)).isEqualTo(2)
    assertThat(10.dp.toPixels(resourceResolver)).isEqualTo(20)
    assertThat(101.dp.toPixels(resourceResolver)).isEqualTo(202)
    assertThat(9001.dp.toPixels(resourceResolver)).isEqualTo(18002)
  }

  @Test
  fun `negative integer dp values are correctly encoded and decoded`() {
    assertThat(-1.dp.toPixels(resourceResolver)).isEqualTo(-2)
    assertThat(-10.dp.toPixels(resourceResolver)).isEqualTo(-20)
    assertThat(-101.dp.toPixels(resourceResolver)).isEqualTo(-202)
    assertThat(-9001.dp.toPixels(resourceResolver)).isEqualTo(-18002)
  }

  @Test
  fun `non-negative integer sp values are correctly encoded and decoded`() {
    assertThat(0.sp.toPixels(resourceResolver)).isEqualTo(0)
    assertThat(1.sp.toPixels(resourceResolver)).isEqualTo(10)
    assertThat(10.sp.toPixels(resourceResolver)).isEqualTo(100)
    assertThat(101.sp.toPixels(resourceResolver)).isEqualTo(1010)
    assertThat(9001.sp.toPixels(resourceResolver)).isEqualTo(90010)
  }

  @Test
  fun `negative integer sp values are correctly encoded and decoded`() {
    assertThat(-1.sp.toPixels(resourceResolver)).isEqualTo(-10)
    assertThat(-10.sp.toPixels(resourceResolver)).isEqualTo(-100)
    assertThat(-101.sp.toPixels(resourceResolver)).isEqualTo(-1010)
    assertThat(-9001.sp.toPixels(resourceResolver)).isEqualTo(-90010)
  }

  // todo here

  @Test
  fun `non-negative float px values are correctly encoded and decoded`() {
    assertThat(0f.px.toPixels(resourceResolver)).isEqualTo(0)
    assertThat(1.5f.px.toPixels(resourceResolver)).isEqualTo(1)
    assertThat(10.2f.px.toPixels(resourceResolver)).isEqualTo(10)
    assertThat(101.11f.px.toPixels(resourceResolver)).isEqualTo(101)
    assertThat(9001.9f.px.toPixels(resourceResolver)).isEqualTo(9001)
  }

  @Test
  fun `negative float px values are correctly encoded and decoded`() {
    assertThat(-1.5f.px.toPixels(resourceResolver)).isEqualTo(-1)
    assertThat(-10.2f.px.toPixels(resourceResolver)).isEqualTo(-10)
    assertThat(-101.11f.px.toPixels(resourceResolver)).isEqualTo(-101)
    assertThat(-9001.9f.px.toPixels(resourceResolver)).isEqualTo(-9001)
  }

  @Test
  fun `non-negative float dp values are correctly encoded and decoded`() {
    assertThat(0f.dp.toPixels(resourceResolver)).isEqualTo(0)
    assertThat(1.5f.dp.toPixels(resourceResolver)).isEqualTo(3)
    assertThat(10.2f.dp.toPixels(resourceResolver)).isEqualTo(20)
    assertThat(101.11f.dp.toPixels(resourceResolver)).isEqualTo(202)
    assertThat(9001.9f.dp.toPixels(resourceResolver)).isEqualTo(18004)
  }

  @Test
  fun `negative float dp values are correctly encoded and decoded`() {
    assertThat(-1.5f.dp.toPixels(resourceResolver)).isEqualTo(-3)
    assertThat(-10.2f.dp.toPixels(resourceResolver)).isEqualTo(-20)
    assertThat(-101.11f.dp.toPixels(resourceResolver)).isEqualTo(-202)
    assertThat(-9001.9f.dp.toPixels(resourceResolver)).isEqualTo(-18004)
  }

  @Test
  fun `non-negative float sp values are correctly encoded and decoded`() {
    assertThat(0f.sp.toPixels(resourceResolver)).isEqualTo(0)
    assertThat(1.5f.sp.toPixels(resourceResolver)).isEqualTo(15)
    assertThat(10.2f.sp.toPixels(resourceResolver)).isEqualTo(102)
    assertThat(101.11f.sp.toPixels(resourceResolver)).isEqualTo(1011)
    assertThat(9001.9f.sp.toPixels(resourceResolver)).isEqualTo(90019)
  }

  @Test
  fun `negative float sp values are correctly encoded and decoded`() {
    assertThat(-1.5f.sp.toPixels(resourceResolver)).isEqualTo(-15)
    assertThat(-10.2f.sp.toPixels(resourceResolver)).isEqualTo(-102)
    assertThat(-101.11f.sp.toPixels(resourceResolver)).isEqualTo(-1011)
    assertThat(-9001.9f.sp.toPixels(resourceResolver)).isEqualTo(-90019)
  }

  @Test
  fun `px values exhibit equality`() {
    assertThat(0.px).isEqualTo(0.px)
    assertThat(1.px).isEqualTo(1.px)
    assertThat((-1).px).isEqualTo((-1).px)
    assertThat(1.5f.px).isEqualTo(1.5f.px)
    assertThat((-1.5f).px).isEqualTo((-1.5f).px)

    assertThat(1f.px).isEqualTo(1.px)

    assertThat(0.px).isNotEqualTo(1.px)
    assertThat(1.px).isNotEqualTo((-1).px)
    assertThat(1.5f.px).isNotEqualTo((-1.5f).px)
    assertThat(1.px).isNotEqualTo(10.px)
  }

  @Test
  fun `dp values exhibit equality`() {
    assertThat(0.dp).isEqualTo(0.dp)
    assertThat(1.dp).isEqualTo(1.dp)
    assertThat((-1).dp).isEqualTo((-1).dp)
    assertThat(1.5f.dp).isEqualTo(1.5f.dp)
    assertThat((-1.5f).dp).isEqualTo((-1.5f).dp)

    assertThat(1f.dp).isEqualTo(1.dp)

    assertThat(0.dp).isNotEqualTo(1.dp)
    assertThat(1.dp).isNotEqualTo((-1).dp)
    assertThat(1.5f.dp).isNotEqualTo((-1.5f).dp)
    assertThat(1.dp).isNotEqualTo(10.dp)
  }

  @Test
  fun `sp values exhibit equality`() {
    assertThat(0.sp).isEqualTo(0.sp)
    assertThat(1.sp).isEqualTo(1.sp)
    assertThat((-1).sp).isEqualTo((-1).sp)
    assertThat(1.5f.sp).isEqualTo(1.5f.sp)
    assertThat((-1.5f).sp).isEqualTo((-1.5f).sp)

    assertThat(1f.sp).isEqualTo(1.sp)

    assertThat(0.sp).isNotEqualTo(1.sp)
    assertThat(1.sp).isNotEqualTo((-1).sp)
    assertThat(1.5f.sp).isNotEqualTo((-1.5f).sp)
    assertThat(1.sp).isNotEqualTo(10.sp)
  }

  @Test
  fun `invalid encoded Dimen throws`() {
    expectedException.expect(IllegalArgumentException::class.java)
    expectedException.expectMessage("Got unexpected NaN")

    Dimen(Double.NaN.toRawBits()).toPixels(resourceResolver)
  }

  private class MockResourceResolver(val density: Float, val scaledDensity: Float) :
      ResourceResolver(
          ApplicationProvider.getApplicationContext<Context>(),
          ResourceCache.getLatest(
              ApplicationProvider.getApplicationContext<Context>()
                  .getResources()
                  .getConfiguration())) {
    override fun dipsToPixels(dips: Float): Int = FastMath.round(dips * density)

    override fun sipsToPixels(sips: Float): Int = FastMath.round(sips * scaledDensity)
  }
}


/*
 * 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

import android.content.Context
import android.widget.TextView
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.BackgroundLayoutLooperRule
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.assertj.LithoAssertions
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.view.onClick
import com.facebook.litho.view.viewTag
import com.facebook.rendercore.MeasureResult
import com.facebook.rendercore.primitives.Primitive
import com.facebook.rendercore.primitives.ViewAllocator
import java.util.concurrent.atomic.AtomicInteger
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper

/** Unit tests for [useState]. */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(LithoTestRunner::class)
class KBatchedStateUpdatesTest {

  @get:Rule val lithoViewRule = LithoViewRule()

  @get:Rule val expectedException = ExpectedException.none()

  @get:Rule
  val backgroundLayoutLooperRule: BackgroundLayoutLooperRule = BackgroundLayoutLooperRule()

  @Test
  fun stateUpdates_numberOfRendersTriggeredCorrectly() {
    val renderCount = AtomicInteger(0)

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return Row(
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  // have to block the main looper while we are queueing updates to make
                  // sure that if any scheduling is posted to the main thread, it only
                  // happens after the work inside the onClick finishes.
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                  }
                }) {
              child(Text(text = "Counter: ${counter.value}"))
            }
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 2")

    /* initial render + render triggered by batched updates */
    val expectedRenderCount = 2
    assertThat(renderCount.get()).isEqualTo(expectedRenderCount)
  }

  /**
   * In this test we:
   * 1. Run two async updates (which are being batched when batching is enabled).
   * 2. Run on sync update. This sync update will end up consuming the currently batched updates and
   *    trigger one layout calculation.
   * 3. Then we start another batching with two async updates, which will trigger another layout
   *    calculation.
   *
   * The goal of this test is that the batching mechanism still works correctly, even when
   * interrupted by another sync update.
   */
  @Test
  fun stateUpdates_concurrent_sync_update_consumes_pending_state_updates_and_batching_can_be_resumed() {
    val renderCount = AtomicInteger(0)

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return Row(
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }

                    /* this sync should consume all pending batched updates when batching is enabled */
                    executeAndRunPendingBgTasks { counter.updateSync { it + 1 } }
                  }

                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                  }
                }) {
              child(Text(text = "Counter: ${counter.value}"))
            }
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 5")
    /* initial render + render triggered by sync update (which interrupts batching) + render triggered by async update */
    val expectedCount = 3

    assertThat(renderCount.get()).isEqualTo(expectedCount)
  }

  /**
   * In this test we:
   * 1. Execute two async updates (which are being batched when batching is enabled)
   * 2. Execute one sync update. This sync update actually ends up on consuming the already enqueued
   *    updates.
   * 3. In this scenario we only have one layout calculation (besides the initial one).
   *
   * This test guarantees that we don't do any extra layout calculation due to the batching.
   */
  @Test
  fun stateUpdates_concurrent_sync_update_consumes_all_updates_and_consumes_all_batched_updates() {
    val renderCount = AtomicInteger(0)

    class TestComponent : KComponent() {
      override fun ComponentScope.render(): Component {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return Row(
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }

                    /* this sync should consume all pending batched updates when batching is enabled */
                    executeAndRunPendingBgTasks { counter.updateSync { it + 1 } }
                  }
                }) {
              child(Text(text = "Counter: ${counter.value}"))
            }
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    executeAndRunPendingBgTasks { lithoViewRule.act(testLithoView) { clickOnTag("test_view") } }

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 3")

    /* initial render + render triggered by sync update */
    val expectedCount = 2
    assertThat(renderCount.get()).isEqualTo(expectedCount)
  }

  @Test
  fun stateUpdates_numberOfRendersTriggeredCorrectly_forMountable() {
    val renderCount = AtomicInteger(0)

    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return MountableRenderResult(
            TestTextMountable(text = "Counter: ${counter.value}"),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  // have to block the main looper while we are queueing updates to make
                  // sure that if any scheduling is posted to the main thread, it only
                  // happens after the work inside the onClick finishes.
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                  }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 2")

    /* initial render + render triggered by batched updates */
    val expectedRenderCount = 2
    assertThat(renderCount.get()).isEqualTo(expectedRenderCount)
  }

  @Test
  fun stateUpdates_numberOfRendersTriggeredCorrectly_forPrimitive() {
    val renderCount = AtomicInteger(0)

    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return LithoPrimitive(
            TestTextPrimitive(text = "Counter: ${counter.value}"),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  // have to block the main looper while we are queueing updates to make
                  // sure that if any scheduling is posted to the main thread, it only
                  // happens after the work inside the onClick finishes.
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                  }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 2")

    /* initial render + render triggered by batched updates */
    val expectedRenderCount = 2
    assertThat(renderCount.get()).isEqualTo(expectedRenderCount)
  }

  /**
   * In this test we:
   * 1. Run two async updates (which are being batched when batching is enabled).
   * 2. Run on sync update. This sync update will end up consuming the currently batched updates and
   *    trigger one layout calculation.
   * 3. Then we start another batching with two async updates, which will trigger another layout
   *    calculation.
   *
   * The goal of this test is that the batching mechanism still works correctly, even when
   * interrupted by another sync update.
   */
  @Test
  fun stateUpdates_concurrent_sync_update_consumes_pending_state_updates_and_batching_can_be_resumed_forMountable() {
    val renderCount = AtomicInteger(0)

    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return MountableRenderResult(
            TestTextMountable(text = "Counter: ${counter.value}"),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }

                    /* this sync should consume all pending batched updates when batching is enabled */
                    executeAndRunPendingBgTasks { counter.updateSync { it + 1 } }
                  }

                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                  }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 5")
    /* initial render + render triggered by sync update (which interrupts batching) + render triggered by async update */
    val expectedCount = 3

    assertThat(renderCount.get()).isEqualTo(expectedCount)
  }

  /**
   * In this test we:
   * 1. Run two async updates (which are being batched when batching is enabled).
   * 2. Run on sync update. This sync update will end up consuming the currently batched updates and
   *    trigger one layout calculation.
   * 3. Then we start another batching with two async updates, which will trigger another layout
   *    calculation.
   *
   * The goal of this test is that the batching mechanism still works correctly, even when
   * interrupted by another sync update.
   */
  @Test
  fun stateUpdates_concurrent_sync_update_consumes_pending_state_updates_and_batching_can_be_resumed_forPrimitive() {
    val renderCount = AtomicInteger(0)

    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return LithoPrimitive(
            TestTextPrimitive(text = "Counter: ${counter.value}"),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }

                    /* this sync should consume all pending batched updates when batching is enabled */
                    executeAndRunPendingBgTasks { counter.updateSync { it + 1 } }
                  }

                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                  }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    lithoViewRule.act(testLithoView) { clickOnTag("test_view") }
    backgroundLayoutLooperRule.runToEndOfTasksSync()

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 5")
    /* initial render + render triggered by sync update (which interrupts batching) + render triggered by async update */
    val expectedCount = 3

    assertThat(renderCount.get()).isEqualTo(expectedCount)
  }

  /**
   * In this test we:
   * 1. Execute two async updates (which are being batched when batching is enabled)
   * 2. Execute one sync update. This sync update actually ends up on consuming the already enqueued
   *    updates.
   * 3. In this scenario we only have one layout calculation (besides the initial one).
   *
   * This test guarantees that we don't do any extra layout calculation due to the batching.
   */
  @Test
  fun stateUpdates_concurrent_sync_update_consumes_all_updates_and_consumes_all_batched_updates_forMountable() {
    val renderCount = AtomicInteger(0)

    class TestComponent : MountableComponent() {
      override fun MountableComponentScope.render(): MountableRenderResult {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return MountableRenderResult(
            TestTextMountable(text = "Counter: ${counter.value}"),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }

                    /* this sync should consume all pending batched updates when batching is enabled */
                    executeAndRunPendingBgTasks { counter.updateSync { it + 1 } }
                  }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    executeAndRunPendingBgTasks { lithoViewRule.act(testLithoView) { clickOnTag("test_view") } }

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 3")

    /* initial render + render triggered by sync update */
    val expectedCount = 2
    assertThat(renderCount.get()).isEqualTo(expectedCount)
  }

  /**
   * In this test we:
   * 1. Execute two async updates (which are being batched when batching is enabled)
   * 2. Execute one sync update. This sync update actually ends up on consuming the already enqueued
   *    updates.
   * 3. In this scenario we only have one layout calculation (besides the initial one).
   *
   * This test guarantees that we don't do any extra layout calculation due to the batching.
   */
  @Test
  fun stateUpdates_concurrent_sync_update_consumes_all_updates_and_consumes_all_batched_updates_forPrimitive() {
    val renderCount = AtomicInteger(0)

    class TestComponent : PrimitiveComponent() {
      override fun PrimitiveComponentScope.render(): LithoPrimitive {
        renderCount.incrementAndGet()

        val counter = useState { 0 }

        return LithoPrimitive(
            TestTextPrimitive(text = "Counter: ${counter.value}"),
            style =
                Style.height(100.dp).width(100.dp).viewTag("test_view").onClick {
                  withMainLooperPaused {
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }
                    executeAndRunPendingBgTasks { counter.update { it + 1 } }

                    /* this sync should consume all pending batched updates when batching is enabled */
                    executeAndRunPendingBgTasks { counter.updateSync { it + 1 } }
                  }
                })
      }
    }

    val testLithoView = lithoViewRule.render { TestComponent() }
    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 0")
    assertThat(renderCount.get()).isEqualTo(1)

    executeAndRunPendingBgTasks { lithoViewRule.act(testLithoView) { clickOnTag("test_view") } }

    LithoAssertions.assertThat(testLithoView).hasVisibleText("Counter: 3")

    /* initial render + render triggered by sync update */
    val expectedCount = 2
    assertThat(renderCount.get()).isEqualTo(expectedCount)
  }

  private fun withMainLooperPaused(body: () -> Unit) {
    ShadowLooper.pauseMainLooper()
    body()
    ShadowLooper.unPauseMainLooper()
  }

  private fun executeAndRunPendingBgTasks(body: () -> Unit) {
    body()
    backgroundLayoutLooperRule.runToEndOfTasksSync()
  }
}

internal class TestTextMountable(private val text: String, private val tag: String? = null) :
    SimpleMountable<TextView>(RenderType.VIEW) {

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

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

  override fun mount(c: Context, content: TextView, layoutData: Any?) {
    content.setText(text)
    content.tag = tag
    content.contentDescription = tag
  }

  override fun unmount(c: Context, content: TextView, layoutData: Any?) {
    content.setText("")
    content.tag = null
  }

  override fun shouldUpdate(
      newMountable: SimpleMountable<TextView>,
      currentLayoutData: Any?,
      nextLayoutData: Any?
  ): Boolean {
    newMountable as TestTextMountable
    return text != newMountable.text || tag != newMountable.tag
  }
}

@Suppress("TestFunctionName")
internal fun PrimitiveComponentScope.TestTextPrimitive(
    text: String,
    tag: String? = null
): Primitive {
  return Primitive(
      layoutBehavior = FixedSizeLayoutBehavior(100.px, 100.px),
      mountBehavior =
          MountBehavior(ViewAllocator { context -> TextView(context) }) {
            text.bindTo(TextView::setText, "")
            tag.bindTo(TextView::setTag)
            tag.bindTo(TextView::setContentDescription)
          })
}