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

import android.annotation.SuppressLint
import android.graphics.Color
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.Row
import com.facebook.litho.StateCaller
import com.facebook.litho.Style
import com.facebook.litho.Transition
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.dp
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.TransitionTestRule
import com.facebook.litho.testing.testrunner.LithoTestRunner
import com.facebook.litho.transition.transitionKey
import com.facebook.litho.view.backgroundColor
import com.facebook.litho.view.viewTag
import com.facebook.litho.widget.StateWithTransitionTestComponent
import com.facebook.litho.widget.TestAnimationsComponent
import com.facebook.yoga.YogaAlign
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/**
 * We test the different kind of Transitions and how they compose different animations.
 *
 * All transitions are timed to 144 ms which translates to 9 frames (we actually step 10 because
 * * the first one does not count as it only initializes the values in the [*]) based on
 *   [MockTimingSource].FRAME_TIME_MS) which * is 16ms per frame.
 */
@LooperMode(LooperMode.Mode.LEGACY)
@SuppressLint("ColorConstantUsageIssue")
@RunWith(LithoTestRunner::class)
class TransitionsTest {
  @JvmField @Rule val lithoViewRule = LithoViewRule()

  @JvmField @Rule val transitionTestRule = TransitionTestRule()

  private val stateCaller = StateCaller()

  private val testComponent: ComponentScope.(Boolean) -> Component = { state ->
    Column(alignItems = if (state) YogaAlign.FLEX_START else YogaAlign.FLEX_END) {
      child(
          Row(
              style =
                  Style.height(40.dp)
                      .width(40.dp)
                      .backgroundColor(Color.parseColor("#ee1111"))
                      .transitionKey(context, RED_TRANSITION_KEY)
                      .viewTag(RED_TRANSITION_KEY)))
      child(
          Row(
              style =
                  Style.height(40.dp)
                      .width(40.dp)
                      .backgroundColor(Color.parseColor("#11ee11"))
                      .transitionKey(context, GREEN_TRANSITION_KEY)
                      .viewTag(GREEN_TRANSITION_KEY)))
      child(
          Row(
              style =
                  Style.height(40.dp)
                      .width(40.dp)
                      .backgroundColor(Color.parseColor("#1111ee"))
                      .transitionKey(context, BLUE_TRANSITION_KEY)
                      .viewTag(BLUE_TRANSITION_KEY)))
    }
  }

  @Test
  fun transitionAnimations_runTransitionsInSequence_elementsShouldAnimateOneAfterTheOtherOnUpdateStateWithTransition() {
    val component = StateWithTransitionTestComponent(stateCaller, testComponent)
    val testLithoView = lithoViewRule.render { component }
    val redView = testLithoView.findViewWithTag(RED_TRANSITION_KEY)
    val greenView = testLithoView.findViewWithTag(GREEN_TRANSITION_KEY)
    val blueView = testLithoView.findViewWithTag(BLUE_TRANSITION_KEY)
    Assertions.assertThat(redView.x)
        .describedAs("redView should be at start position")
        .isEqualTo(1040f)
    Assertions.assertThat(greenView.x)
        .describedAs("greenView should be at start position")
        .isEqualTo(1040f)
    Assertions.assertThat(blueView.x)
        .describedAs("blueView should be at start position")
        .isEqualTo(1040f)
    stateCaller.update()
    lithoViewRule.idle()

    Assertions.assertThat(redView.x).describedAs("redView after 10 frames").isEqualTo(1040f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 10 frames").isEqualTo(1040f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 10 frames").isEqualTo(1040f)
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 10 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 10 frames").isEqualTo(1040f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 10 frames").isEqualTo(1040f)
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 20 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 20 frames").isEqualTo(0f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 20 frames").isEqualTo(1040f)
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 30 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 30 frames").isEqualTo(0f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 30 frames").isEqualTo(0f)
  }

  @Test
  fun transitionAnimations_runTransitionsInSequence_elementsShouldAnimateOneAfterTheOther() {
    val component =
        TestAnimationsComponent(
            stateCaller,
            Transition.sequence(
                Transition.create(RED_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X),
                Transition.create(GREEN_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X),
                Transition.create(BLUE_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X)),
            testComponent)

    val testLithoView = lithoViewRule.render { component }
    val redView = testLithoView.findViewWithTag(RED_TRANSITION_KEY)
    val greenView = testLithoView.findViewWithTag(GREEN_TRANSITION_KEY)
    val blueView = testLithoView.findViewWithTag(BLUE_TRANSITION_KEY)
    Assertions.assertThat(redView.x)
        .describedAs("redView should be at start position")
        .isEqualTo(1040f)
    Assertions.assertThat(greenView.x)
        .describedAs("greenView should be at start position")
        .isEqualTo(1040f)
    Assertions.assertThat(blueView.x)
        .describedAs("blueView should be at start position")
        .isEqualTo(1040f)
    stateCaller.update()
    lithoViewRule.idle()
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 10 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 10 frames").isEqualTo(1040f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 10 frames").isEqualTo(1040f)
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 20 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 20 frames").isEqualTo(0f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 20 frames").isEqualTo(1040f)
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 30 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 30 frames").isEqualTo(0f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 30 frames").isEqualTo(0f)
  }

  @Test
  fun transitionAnimations_runTransitionsInParallel_elementsShouldAnimateAtTheSameTime() {
    val component =
        TestAnimationsComponent(
            stateCaller,
            Transition.parallel(
                Transition.create(RED_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X),
                Transition.create(GREEN_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X),
                Transition.create(BLUE_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X)),
            testComponent)

    val testLithoView = lithoViewRule.render { component }
    val redView = testLithoView.findViewWithTag(RED_TRANSITION_KEY)
    val greenView = testLithoView.findViewWithTag(GREEN_TRANSITION_KEY)
    val blueView = testLithoView.findViewWithTag(BLUE_TRANSITION_KEY)
    Assertions.assertThat(redView.x)
        .describedAs("redView should be at start position")
        .isEqualTo(1040f)
    Assertions.assertThat(greenView.x)
        .describedAs("greenView should be at start position")
        .isEqualTo(1040f)
    Assertions.assertThat(blueView.x)
        .describedAs("blueView should be at start position")
        .isEqualTo(1040f)
    stateCaller.update()
    lithoViewRule.idle()
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 10 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 10 frames").isEqualTo(0f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 10 frames").isEqualTo(0f)
  }

  @Test
  fun transitionAnimations_runTransitionsInStagger_elementsShouldAnimateStaggered() {
    val component =
        TestAnimationsComponent(
            stateCaller,
            Transition.stagger(
                50,
                Transition.create(RED_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X),
                Transition.create(GREEN_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X),
                Transition.create(BLUE_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X)),
            testComponent)

    val testLithoView = lithoViewRule.render { component }
    val redView = testLithoView.findViewWithTag(RED_TRANSITION_KEY)
    val greenView = testLithoView.findViewWithTag(GREEN_TRANSITION_KEY)
    val blueView = testLithoView.findViewWithTag(BLUE_TRANSITION_KEY)
    Assertions.assertThat(redView.x)
        .describedAs("redView should be at start position")
        .isEqualTo(1040f)
    Assertions.assertThat(greenView.x)
        .describedAs("greenView should be at start position")
        .isEqualTo(1040f)
    Assertions.assertThat(blueView.x)
        .describedAs("blueView should be at start position")
        .isEqualTo(1040f)
    stateCaller.update()
    lithoViewRule.idle()
    transitionTestRule.step(5)
    Assertions.assertThat(redView.x).describedAs("redView after 5 frames").isEqualTo(610.2971f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 5 frames").isEqualTo(1040f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 5 frames").isEqualTo(1040f)
    transitionTestRule.step(5)
    Assertions.assertThat(redView.x).describedAs("redView after 10 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x)
        .describedAs("greenView after 10 frames")
        .isEqualTo(429.70294f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 10 frames").isEqualTo(1008.64014f)
    transitionTestRule.step(5)
    Assertions.assertThat(redView.x).describedAs("redView after 15 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 15 frames").isEqualTo(0f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 15 frames").isEqualTo(259.99988f)
    transitionTestRule.step(5)
    Assertions.assertThat(redView.x).describedAs("redView after 20 frames").isEqualTo(0f)
    Assertions.assertThat(greenView.x).describedAs("greenView after 20 frames").isEqualTo(0f)
    Assertions.assertThat(blueView.x).describedAs("blueView after 20 frames").isEqualTo(0f)
  }

  @Test
  fun transitionAnimations_runDelayedTransition_elementsShouldAnimateAfterDelay() {
    val component =
        TestAnimationsComponent(
            stateCaller,
            Transition.delay(
                144,
                Transition.create(RED_TRANSITION_KEY)
                    .animator(Transition.timing(144))
                    .animate(AnimatedProperties.X)),
            testComponent)

    val testLithoView = lithoViewRule.render { component }
    val redView = testLithoView.findViewWithTag(RED_TRANSITION_KEY)
    Assertions.assertThat(redView.x)
        .describedAs("redView should be at start position")
        .isEqualTo(1040f)
    stateCaller.update()
    lithoViewRule.idle()
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 10 frames").isEqualTo(1040f)
    transitionTestRule.step(10)
    Assertions.assertThat(redView.x).describedAs("redView after 20 frames").isEqualTo(0f)
  }

  companion object {
    const val RED_TRANSITION_KEY = "red"
    const val GREEN_TRANSITION_KEY = "green"
    const val BLUE_TRANSITION_KEY = "blue"
  }
}


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

import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper

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

  @Test
  fun testCreationFromMainThread() {
    ShadowLooper.pauseMainLooper()
    val firstCallback = AtomicBoolean(false)
    val choreographerCompat = ChoreographerCompatImpl()
    assertThat(choreographerCompat.isUsingChoreographer).isTrue
    ChoreographerCompatImpl()
        .postFrameCallback(
            object : ChoreographerCompat.FrameCallback() {
              override fun doFrame(frameTimeNanos: Long) {
                firstCallback.set(true)
              }
            })
    assertThat(firstCallback.get()).isFalse
    ShadowLooper.runUiThreadTasks()
    assertThat(firstCallback.get()).isTrue
  }

  @Test
  fun testCreationFromBGThread() {
    ShadowLooper.pauseMainLooper()
    val latch = CountDownLatch(1)
    val ref = AtomicReference<ChoreographerCompatImpl>()
    Thread {
          ref.set(ChoreographerCompatImpl())
          latch.countDown()
        }
        .start()
    assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue
    assertThat(ref.get().isUsingChoreographer).isFalse
    val firstCallback = AtomicBoolean(false)
    ref.get()
        .postFrameCallback(
            object : ChoreographerCompat.FrameCallback() {
              override fun doFrame(frameTimeNanos: Long) {
                firstCallback.set(true)
              }
            })
    assertThat(firstCallback.get()).isFalse
    ShadowLooper.runUiThreadTasks()
    assertThat(firstCallback.get()).isTrue
    ShadowLooper.pauseMainLooper()
    assertThat(ref.get().isUsingChoreographer).isTrue
    val secondCallback = AtomicBoolean(false)
    ref.get()
        .postFrameCallback(
            object : ChoreographerCompat.FrameCallback() {
              override fun doFrame(frameTimeNanos: Long) {
                secondCallback.set(true)
              }
            })
    assertThat(secondCallback.get()).isFalse
    ShadowLooper.runUiThreadTasks()
    assertThat(secondCallback.get()).isTrue
  }
}


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

/** Multi-input node for unit tests */
class AdditionNode : ValueNode() {
  override fun calculateValue(frameTimeNanos: Long): Float =
      getInput("a").value + getInput("b").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.dataflow

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  private lateinit var testTimingSource: MockTimingSource
  private lateinit var dataFlowGraph: DataFlowGraph

  @Before
  fun setUp() {
    testTimingSource = MockTimingSource()
    dataFlowGraph = DataFlowGraph.create(testTimingSource)
  }

  @Test
  fun testSimpleGraph() {
    val source = SettableNode()
    val middle = SimpleNode()
    val destination = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(source, middle)
    binding.addBinding(middle, destination)
    binding.activate()
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(0f)
    assertThat(source.value).isEqualTo(0f)
    source.value = 37f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(37f)
    assertThat(source.value).isEqualTo(37f)
  }

  @Test
  fun testSimpleUpdatingGraph() {
    val source = NumFramesNode()
    val middle = SimpleNode()
    val destination = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(source, middle)
    binding.addBinding(middle, destination)
    binding.activate()
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(1f)
    assertThat(source.value).isEqualTo(1f)
    testTimingSource.step(39)
    assertThat(destination.value).isEqualTo(40f)
    assertThat(source.value).isEqualTo(40f)
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(41f)
    assertThat(source.value).isEqualTo(41f)
  }

  @Test
  fun testGraphWithMultipleOutputs() {
    val source = NumFramesNode()
    val middle = SimpleNode()
    val dest1 = OutputOnlyNode()
    val dest2 = OutputOnlyNode()
    val dest3 = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(source, middle)
    binding.addBinding(middle, dest1)
    binding.addBinding(middle, dest2)
    binding.addBinding(source, dest3)
    binding.activate()
    testTimingSource.step(1)
    assertThat(dest1.value).isEqualTo(1f)
    assertThat(dest2.value).isEqualTo(1f)
    assertThat(dest3.value).isEqualTo(1f)
    testTimingSource.step(39)
    assertThat(dest1.value).isEqualTo(40f)
    assertThat(dest2.value).isEqualTo(40f)
    assertThat(dest3.value).isEqualTo(40f)
  }

  @Test
  fun testRebindNode() {
    val source = SettableNode()
    val middle = SimpleNode()
    val destination = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(source, middle)
    binding.addBinding(middle, destination)
    binding.activate()
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(0f)
    val newSource = SettableNode()
    val secondBinding = GraphBinding.create(dataFlowGraph)
    secondBinding.addBinding(newSource, destination)
    secondBinding.activate()
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(0f)
    newSource.value = 11f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(11f)
  }

  @Test
  fun testMultipleInputs() {
    val dest = AdditionNode()
    val a = SettableNode()
    val b = SettableNode()
    a.value = 1_776f
    b.value = 1_812f
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(a, dest, "a")
    binding.addBinding(b, dest, "b")
    binding.activate()
    testTimingSource.step(1)
    assertThat(dest.value).isEqualTo(3_588f)
  }

  @Test(expected = DetectedCycleException::class)
  fun testSimpleCycle() {
    val node1 = SimpleNode()
    val node2 = SimpleNode()
    val node3 = SimpleNode()
    val node4 = SimpleNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(node1, node2)
    binding.addBinding(node2, node3)
    binding.addBinding(node3, node1)
    binding.addBinding(node1, node4)
    binding.activate()
    testTimingSource.step(1)
  }

  @Test(expected = DetectedCycleException::class)
  fun testCycleWithoutLeaves() {
    val node1 = SimpleNode()
    val node2 = SimpleNode()
    val node3 = SimpleNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(node1, node2)
    binding.addBinding(node2, node3)
    binding.addBinding(node3, node1)
    binding.activate()
    testTimingSource.step(1)
  }
}


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

package com.facebook.litho.dataflow

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  private lateinit var testTimingSource: MockTimingSource
  private lateinit var dataFlowGraph: DataFlowGraph

  private class TrackingBindingListener : BindingListener {
    var numFinishCalls: Int = 0
      private set

    override fun onAllNodesFinished(binding: GraphBinding) {
      numFinishCalls++
    }
  }

  @Before
  fun setUp() {
    testTimingSource = MockTimingSource()
    dataFlowGraph = DataFlowGraph.create(testTimingSource)
  }

  @Test
  fun testBindingThatFinishes() {
    val durationMs = 300
    val numExpectedFrames = durationMs / MockTimingSource.FRAME_TIME_MS + 2
    val timingNode = TimingNode(durationMs)
    val middle = SimpleNode()
    val destination = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(timingNode, middle)
    binding.addBinding(middle, destination)
    val testListener = TrackingBindingListener()
    binding.setListener(testListener)
    binding.activate()
    testTimingSource.step(numExpectedFrames / 2)
    assertThat(destination.value < 1).isTrue
    assertThat(destination.value > 0).isTrue
    assertThat(testListener.numFinishCalls).isEqualTo(0)
    testTimingSource.step(numExpectedFrames / 2 + 1)
    assertThat(destination.value).isEqualTo(1f)
    assertThat(testListener.numFinishCalls).isEqualTo(1)
    assertThat(dataFlowGraph.hasReferencesToNodes()).isFalse
  }
}


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

package com.facebook.litho.dataflow

import android.view.animation.Interpolator
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  private lateinit var testTimingSource: MockTimingSource
  private lateinit var dataFlowGraph: DataFlowGraph

  @Before
  fun setUp() {
    testTimingSource = MockTimingSource()
    dataFlowGraph = DataFlowGraph.create(testTimingSource)
  }

  @Test
  fun testInterpolatorNode() {
    val settableNode = SettableNode()
    val interpolatorNode = InterpolatorNode(Interpolator { input -> input })
    val middle = SimpleNode()
    val destination = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(settableNode, interpolatorNode)
    binding.addBinding(interpolatorNode, middle)
    binding.addBinding(middle, destination)
    binding.activate()
    settableNode.value = 0f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(0f)
    settableNode.value = 0.5f
    testTimingSource.step(1)
    assertThat(destination.value < 1).isTrue
    assertThat(destination.value > 0).isTrue
    settableNode.value = 1.0f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(1f)
  }
}


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

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  private lateinit var testTimingSource: MockTimingSource
  private lateinit var dataFlowGraph: DataFlowGraph

  @Before
  fun setUp() {
    testTimingSource = MockTimingSource()
    dataFlowGraph = DataFlowGraph.create(testTimingSource)
  }

  @Test
  fun testMappingNodeWithinRange() {
    val settableNode = SettableNode()
    val mappingNode = MappingNode()
    val middle = SimpleNode()
    val destination = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(settableNode, mappingNode)
    binding.addBinding(mappingNode, middle)
    binding.addBinding(ConstantNode(0f), mappingNode, MappingNode.INITIAL_INPUT)
    binding.addBinding(ConstantNode(100f), mappingNode, MappingNode.END_INPUT)
    binding.addBinding(middle, destination)
    binding.activate()
    settableNode.value = 0f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(0f)
    settableNode.value = 0.2f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(20f)
    settableNode.value = 0.7f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(70f)
    settableNode.value = 1.0f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(100f)
  }

  @Test
  fun testMappingNodeOutsideRange() {
    val settableNode = SettableNode()
    val mappingNode = MappingNode()
    val middle = SimpleNode()
    val destination = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(settableNode, mappingNode)
    binding.addBinding(mappingNode, middle)
    binding.addBinding(ConstantNode(0f), mappingNode, MappingNode.INITIAL_INPUT)
    binding.addBinding(ConstantNode(100f), mappingNode, MappingNode.END_INPUT)
    binding.addBinding(middle, destination)
    binding.activate()
    settableNode.value = -0.1f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(-10f)
    settableNode.value = 0f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(0f)
    settableNode.value = 0.5f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(50f)
    settableNode.value = 1.5f
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(150f)
  }
}


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

import com.facebook.litho.choreographercompat.ChoreographerCompat
import com.facebook.litho.choreographercompat.ChoreographerCompat.FrameCallback
import com.facebook.litho.testing.testrunner.LithoTestRunner
import java.util.ArrayList
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.robolectric.annotation.LooperMode

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

  private lateinit var timingSource: MockTimingSource

  @Before
  fun setUp() {
    timingSource = MockTimingSource()
    DataFlowGraph.create(timingSource)
    timingSource.start()
  }

  @Test
  fun testPostFrameCallback() {
    val callbacks = ArrayList<ChoreographerCompat.FrameCallback>()
    for (i in 0..2) {
      val callback = mock<ChoreographerCompat.FrameCallback>()
      callbacks.add(callback)
      timingSource.postFrameCallback(callback)
    }
    timingSource.step(1)
    for (i in callbacks.indices) {
      verify(callbacks[i]).doFrame(ArgumentMatchers.anyLong())
    }
  }

  @Test
  fun testPostFrameCallbackDelayed() {
    val callback1 = mock<ChoreographerCompat.FrameCallback>()
    val callback2 = mock<ChoreographerCompat.FrameCallback>()
    val delayedCallback = mock<ChoreographerCompat.FrameCallback>()
    timingSource.postFrameCallback(callback1)
    timingSource.postFrameCallbackDelayed(delayedCallback, 20)
    timingSource.postFrameCallback(callback2)
    timingSource.step(1)
    verify(callback1).doFrame(ArgumentMatchers.anyLong())
    verify(callback2).doFrame(ArgumentMatchers.anyLong())
    verify(delayedCallback, never()).doFrame(ArgumentMatchers.anyLong())
    timingSource.step(1)
    verify(delayedCallback).doFrame(ArgumentMatchers.anyLong())
  }

  @Test
  fun testNestedFrameCallbacks() {
    val callback: ChoreographerCompat.FrameCallback =
        object : ChoreographerCompat.FrameCallback() {
          override fun doFrame(frameTimeNanos: Long) {
            timingSource.postFrameCallback(
                object : ChoreographerCompat.FrameCallback() {
                  override fun doFrame(frameTimeNanos: Long) {
                    fail("Nested FrameCallback should not be called in the same step")
                  }
                })
          }
        }
    timingSource.postFrameCallback(callback)
    timingSource.step(1)
  }
}


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

package com.facebook.litho.dataflow

/** Test node whose value is based on the number of frames it's seen. */
class NumFramesNode : ValueNode(), NodeCanFinish {

  private var numFramesSeen = 0
  private var lastFrameTime = Long.MIN_VALUE

  override fun calculateValue(frameTimeNanos: Long): Float {
    if (lastFrameTime != frameTimeNanos) {
      lastFrameTime = frameTimeNanos
      numFramesSeen++
    }
    return numFramesSeen.toFloat()
  }

  override fun isFinished(): Boolean = false
}


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

/** Test node that only serves as an output node. */
class OutputOnlyNode : ValueNode() {
  override fun calculateValue(frameTimeNanos: Long): Float = input.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.dataflow

/** Test node that allows setting of its next value. */
class SettableNode : ValueNode(), NodeCanFinish {

  private var value = 0f

  override fun setValue(value: Float) {
    this.value = value
  }

  override fun calculateValue(frameTimeNanos: Long): Float = value

  override fun isFinished(): Boolean = false
}


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

import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  private lateinit var testTimingSource: MockTimingSource
  private lateinit var dataFlowGraph: DataFlowGraph

  @Before
  fun setUp() {
    testTimingSource = MockTimingSource()
    dataFlowGraph = DataFlowGraph.create(testTimingSource)
  }

  @Test
  fun testTimingNode() {
    val durationMs = 300
    val numExpectedFrames = 300 / MockTimingSource.FRAME_TIME_MS + 1
    val timingNode = TimingNode(durationMs)
    val middle = SimpleNode()
    val destination = OutputOnlyNode()
    val binding = GraphBinding.create(dataFlowGraph)
    binding.addBinding(timingNode, middle)
    binding.addBinding(middle, destination)
    binding.activate()
    testTimingSource.step(1)
    assertThat(destination.value).isEqualTo(0f)
    testTimingSource.step(numExpectedFrames / 2)
    assertThat(destination.value < 1).isTrue
    assertThat(destination.value > 0).isTrue
    testTimingSource.step(numExpectedFrames / 2 + 1)
    assertThat(destination.value).isEqualTo(1f)
  }
}


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

import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  @Test
  fun ComparableDrawable_create() {
    val comparable = ComparableGradientDrawable()
    assertThat(comparable).isNotNull
  }

  @Test
  fun ComparableDrawable_equal_simple() {
    val comparable1 = ComparableGradientDrawable()
    val comparable2 = ComparableGradientDrawable()
    assertThat(comparable1.isEquivalentTo(comparable1)).isTrue
    assertThat(comparable1.isEquivalentTo(comparable2)).isTrue
  }

  @Test
  fun ComparableDrawable_equal_detailed() {
    val comparable1 = ComparableGradientDrawable()
    comparable1.orientation = GradientDrawable.Orientation.BOTTOM_TOP
    comparable1.setColors(intArrayOf(0, 0))
    comparable1.setCornerRadii(floatArrayOf(0f, 1f, 2f, 4f))
    comparable1.setSize(1, 2)
    comparable1.setShape(GradientDrawable.OVAL)
    comparable1.setGradientRadius(2f)
    comparable1.setGradientType(GradientDrawable.LINEAR_GRADIENT)
    val comparable2 = ComparableGradientDrawable()
    comparable2.orientation = GradientDrawable.Orientation.BOTTOM_TOP
    comparable2.setColors(intArrayOf(0, 0))
    comparable2.setCornerRadii(floatArrayOf(0f, 1f, 2f, 4f))
    comparable2.setSize(1, 2)
    comparable2.setShape(GradientDrawable.OVAL)
    comparable2.setGradientRadius(2f)
    comparable2.setGradientType(GradientDrawable.LINEAR_GRADIENT)
    assertThat(comparable1.isEquivalentTo(comparable1)).isTrue
    assertThat(comparable1.isEquivalentTo(comparable2)).isTrue
  }

  @Test
  fun ComparableDrawable_equal_detailed_with_null() {
    val comparable1 = ComparableGradientDrawable()
    comparable1.orientation = null
    comparable1.setColors(null)
    comparable1.setColor(0xFF)
    comparable1.setCornerRadii(null)
    val comparable2 = ComparableGradientDrawable()
    comparable2.orientation = null
    comparable2.setColors(null)
    comparable2.setColor(0xFF)
    comparable2.setCornerRadii(null)
    assertThat(comparable1.isEquivalentTo(comparable1)).isTrue
    assertThat(comparable1.isEquivalentTo(comparable2)).isTrue
  }

  @Test
  fun ComparableDrawable_not_equal_simple() {
    val comparable1 = ComparableGradientDrawable()
    val comparable2 = ComparableColorDrawable.create(Color.BLACK)
    assertThat(comparable1.isEquivalentTo(comparable2)).isFalse
  }

  @Test
  fun ComparableDrawable_not_equal_detailed() {
    val comparable1 = ComparableGradientDrawable()
    comparable1.orientation = GradientDrawable.Orientation.BOTTOM_TOP
    comparable1.setColors(intArrayOf(0, 0))
    comparable1.setCornerRadii(floatArrayOf(0f, 1f, 2f, 4f))
    comparable1.setSize(1, 2)
    comparable1.setShape(GradientDrawable.OVAL)
    comparable1.setGradientRadius(2f)
    comparable1.setGradientType(GradientDrawable.LINEAR_GRADIENT)
    val comparable2 = ComparableGradientDrawable()
    comparable2.orientation = GradientDrawable.Orientation.BOTTOM_TOP
    comparable2.setColors(intArrayOf(1, 1))
    comparable2.setCornerRadii(floatArrayOf(4f, 3f, 2f, 1f))
    comparable2.setSize(2, 1)
    comparable2.setShape(GradientDrawable.RECTANGLE)
    comparable2.setGradientRadius(1f)
    comparable2.setGradientType(GradientDrawable.RADIAL_GRADIENT)
    assertThat(comparable1.isEquivalentTo(comparable2)).isFalse
  }

  @Test
  fun ComparableDrawable_not_equal_detailed_with_null() {
    val comparable1 = ComparableGradientDrawable()
    comparable1.orientation = GradientDrawable.Orientation.BOTTOM_TOP
    comparable1.setColors(intArrayOf(0, 0))
    comparable1.setCornerRadii(floatArrayOf(0f, 1f, 2f, 4f))
    val comparable2 = ComparableGradientDrawable()
    comparable2.orientation = null
    comparable2.setColors(null)
    comparable2.setCornerRadii(null)
    assertThat(comparable1.isEquivalentTo(comparable2)).isFalse
  }
}


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

package com.facebook.litho.internal

import java.lang.IndexOutOfBoundsException
import org.assertj.core.api.Assertions
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class SparseFloatArrayTest {
  @Test
  fun test_get() {
    val array = SparseFloatArray()
    Assertions.assertThat(array[1]).isEqualTo(0f)
    Assertions.assertThat(array[1, 1f]).isEqualTo(1f)
    Assertions.assertThat(array[1, 1f]).isEqualTo(1f)
  }

  @Test
  fun test_append() {
    val array = SparseFloatArray()
    array.append(4, 1f)
    Assertions.assertThat(array[4]).isEqualTo(1f)

    // override
    array.append(4, 2f)
    Assertions.assertThat(array[4]).isEqualTo(2f)

    // do not use default value if found
    array.append(6, 1f)
    Assertions.assertThat(array[6, -1f]).isEqualTo(1f)

    // use default value if NOT found
    array.append(1, 1f)
    array.append(2, 1f)
    array.append(3, 1f)
    array.append(5, 1f)
    array.append(6, 1f)
    array.append(8, 1f)
    Assertions.assertThat(array[7, -1f]).isEqualTo(-1f)
  }

  @Test
  fun test_insert() {
    val array = SparseFloatArray()
    array.put(4, 1f)
    Assertions.assertThat(array[4]).isEqualTo(1f)

    // override
    array.put(4, 2f)
    Assertions.assertThat(array[4]).isEqualTo(2f)

    // do not use default value if found
    array.put(6, 1f)
    Assertions.assertThat(array[6, -1f]).isEqualTo(1f)

    // use default value if NOT found
    array.put(1, 1f)
    array.put(2, 1f)
    array.put(3, 1f)
    array.put(5, 1f)
    array.put(6, 1f)
    array.put(8, 1f)
    Assertions.assertThat(array[7, -1f]).isEqualTo(-1f)
  }

  @Test
  fun test_delete() {
    val array = SparseFloatArray()
    array.put(1, 1f)
    array.put(2, 1f)
    array.put(3, 1f)
    array.put(4, 1f)
    array.put(5, 1f)
    array.delete(2)
    Assertions.assertThat(array[2, -1f]).isEqualTo(-1f)
    array.put(2, 1f)
    Assertions.assertThat(array[2, -1f]).isEqualTo(1f)
  }

  @Test
  fun test_removeAt() {
    val array = SparseFloatArray()
    array.put(1, 5f)
    array.put(2, 4f)
    array.put(3, 3f)
    array.put(4, 2f)
    array.put(5, 1f)
    Assertions.assertThat(array[2, -1f]).isEqualTo(4f)
    array.removeAt(1)
    Assertions.assertThat(array[2, -1f]).isEqualTo(-1f)
  }

  @Test
  fun test_keyAt_and_valueAt() {
    val array = SparseFloatArray()
    array.put(1, 5f)
    array.put(2, 4f)
    array.put(3, 3f)
    array.put(4, 2f)
    array.put(5, 1f)
    Assertions.assertThat(array.keyAt(0)).isEqualTo(1)
    Assertions.assertThat(array.valueAt(0)).isEqualTo(5f)
    Assertions.assertThat(array.keyAt(4)).isEqualTo(5)
    Assertions.assertThat(array.valueAt(4)).isEqualTo(1f)
  }

  @Test
  fun test_setValueAt() {
    val array = SparseFloatArray()
    array.put(1, 1f)
    array.put(2, 2f)
    array.put(3, 3f)
    array.put(4, 4f)
    array.put(5, 5f)
    array.setValueAt(2, 0f)
    Assertions.assertThat(array[3, -1f]).isEqualTo(0f)
    Assertions.assertThat(array.valueAt(2)).isEqualTo(0f)
  }

  @Test
  fun test_indexOfKey() {
    val array = SparseFloatArray()
    array.put(5, 1f)
    array.put(4, 2f)
    array.put(3, 3f)
    array.put(2, 4f)
    array.put(1, 5f)
    Assertions.assertThat(array.indexOfKey(1)).isEqualTo(0)
    Assertions.assertThat(array.indexOfKey(5)).isEqualTo(4)
    Assertions.assertThat(array.indexOfKey(3)).isEqualTo(2)
  }

  @Test(expected = IndexOutOfBoundsException::class)
  fun test_out_of_bounds_check_keyAt() {
    val array = SparseFloatArray()
    array.put(1, 5f)
    array.put(2, 4f)
    array.put(3, 3f)
    array.put(4, 2f)
    array.put(5, 1f)
    array.keyAt(8)
  }

  @Test(expected = IndexOutOfBoundsException::class)
  fun test_out_of_bounds_check_valueAt() {
    val array = SparseFloatArray()
    array.put(1, 5f)
    array.put(2, 4f)
    array.put(3, 3f)
    array.put(4, 2f)
    array.put(5, 1f)
    array.valueAt(8)
  }

  @Test
  fun test_growth_upto_growth() {
    val array = SparseFloatArray()
    array.put(1, 1f)
    Assertions.assertThat(array.valueAt(0)).isEqualTo(1f)
    Assertions.assertThat(array.valueAt(1)).isEqualTo(0f)
    array.put(2, 1f)
    Assertions.assertThat(array.valueAt(1)).isEqualTo(1f)
    array.put(3, 1f)
    Assertions.assertThat(array.valueAt(2)).isEqualTo(1f)
  }

  @Test(expected = IndexOutOfBoundsException::class)
  fun test_growth_at_limit() {
    val array = SparseFloatArray()
    array.put(1, 1f)
    Assertions.assertThat(array.valueAt(0)).isEqualTo(1f)

    // next should be zero
    Assertions.assertThat(array.valueAt(1)).isEqualTo(0f)
    array.put(2, 1f)
    Assertions.assertThat(array.valueAt(1)).isEqualTo(1f)

    // next should throw IndexOutOfBoundsException
    array.valueAt(2)
  }

  @Test(expected = IndexOutOfBoundsException::class)
  fun test_growth_greater_than() {
    val array = SparseFloatArray()
    array.put(1, 1f)
    array.put(2, 1f)
    array.put(3, 1f)
    array.put(4, 1f)
    Assertions.assertThat(array.valueAt(0)).isEqualTo(1f)
    Assertions.assertThat(array.valueAt(1)).isEqualTo(1f)
    Assertions.assertThat(array.valueAt(2)).isEqualTo(1f)
    Assertions.assertThat(array.valueAt(3)).isEqualTo(1f)
    array.put(5, 1f)
    Assertions.assertThat(array.valueAt(4)).isEqualTo(1f)

    // rest should be zero
    Assertions.assertThat(array.valueAt(5)).isEqualTo(0f)
    Assertions.assertThat(array.valueAt(6)).isEqualTo(0f)
    Assertions.assertThat(array.valueAt(7)).isEqualTo(0f)

    // next should throw IndexOutOfBoundsException
    array.valueAt(8)
  }
}


/*
 * 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.specmodels.generator

import android.graphics.Color
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.specmodels.internal.RunMode
import com.facebook.litho.specmodels.processor.LayoutSpecModelFactory
import com.google.testing.compile.CompilationRule
import javax.annotation.processing.Messager
import javax.lang.model.element.TypeElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations

@LayoutSpec
object VarArgsWildcardPropTestSpec {
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop(varArg = "color") colors: List<Color>): Component {
    return Column.create(c).build()
  }
}

@LayoutSpec
object WildcardOutPropTestSpec {
  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop numWildCard: List<out @JvmWildcard Number>
  ): Component? {
    return Column.create(c).build()
  }
}

@RunWith(JUnit4::class)
class BuilderGeneratorKtTest {

  @Rule @JvmField val compilationRule = CompilationRule()

  private val layoutSpecModelFactory = LayoutSpecModelFactory()
  private val elements: Elements
    get() = compilationRule.elements

  private val types: Types
    get() = compilationRule.types

  private val messager = mock(Messager::class.java)

  @Before
  fun setup() {
    MockitoAnnotations.initMocks(this)
  }

  @Test
  fun specWithWildcardOutProp_generate() {
    val typeElement = elements.getTypeElement(WildcardOutPropTestSpec::class.java.canonicalName)

    val specModel =
        layoutSpecModelFactory.create(
            elements, types, typeElement, messager, RunMode.normal(), null, null)
    val dataHolder = BuilderGenerator.generate(specModel)
    assertThat(dataHolder.typeSpecs.size).isEqualTo(1)
    assertThat(dataHolder.typeSpecs.get(0).toString())
        .isEqualTo(
            """
                    @com.facebook.litho.annotations.Generated
                    public static final class Builder extends com.facebook.litho.Component.Builder<Builder> {
                      WildcardOutPropTest mWildcardOutPropTest;

                      com.facebook.litho.ComponentContext mContext;

                      private final java.lang.String[] REQUIRED_PROPS_NAMES = new String[] {"numWildCard"};

                      private final int REQUIRED_PROPS_COUNT = 1;

                      private final java.util.BitSet mRequired = new java.util.BitSet(REQUIRED_PROPS_COUNT);

                      private Builder(com.facebook.litho.ComponentContext context, int defStyleAttr, int defStyleRes,
                          WildcardOutPropTest wildcardOutPropTestRef) {
                        super(context, defStyleAttr, defStyleRes, wildcardOutPropTestRef);
                        mWildcardOutPropTest = wildcardOutPropTestRef;
                        mContext = context;
                        mRequired.clear();
                      }

                      @java.lang.Override
                      protected void setComponent(com.facebook.litho.Component component) {
                        mWildcardOutPropTest = (com.facebook.litho.specmodels.generator.WildcardOutPropTest) component;
                      }

                      @com.facebook.litho.annotations.PropSetter(
                          value = "numWildCard",
                          required = true
                      )
                      @com.facebook.litho.annotations.RequiredProp("numWildCard")
                      public Builder numWildCard(
                          @org.jetbrains.annotations.NotNull java.util.List<? extends java.lang.Number> numWildCard) {
                        this.mWildcardOutPropTest.numWildCard = numWildCard;
                        mRequired.set(0);
                        return this;
                      }

                      @java.lang.Override
                      public Builder getThis() {
                        return this;
                      }

                      @java.lang.Override
                      public com.facebook.litho.specmodels.generator.WildcardOutPropTest build() {
                        checkArgs(REQUIRED_PROPS_COUNT, mRequired, REQUIRED_PROPS_NAMES);
                        return mWildcardOutPropTest;
                      }
                    }

                    """
                .trimIndent())
  }

  @Test
  fun specWithVarArgWildcardProp_generate() {
    val typeElement: TypeElement =
        elements.getTypeElement(VarArgsWildcardPropTestSpec::class.java.canonicalName)

    val specModel =
        layoutSpecModelFactory.create(
            elements, types, typeElement, messager, RunMode.normal(), null, null)
    val dataHolder = BuilderGenerator.generate(specModel)
    assertThat(dataHolder.typeSpecs.size).isEqualTo(1)
    assertThat(dataHolder.typeSpecs.get(0).toString())
        .isEqualTo(
            """
                    @com.facebook.litho.annotations.Generated
                    public static final class Builder extends com.facebook.litho.Component.Builder<Builder> {
                      VarArgsWildcardPropTest mVarArgsWildcardPropTest;

                      com.facebook.litho.ComponentContext mContext;

                      private Builder(com.facebook.litho.ComponentContext context, int defStyleAttr, int defStyleRes,
                          VarArgsWildcardPropTest varArgsWildcardPropTestRef) {
                        super(context, defStyleAttr, defStyleRes, varArgsWildcardPropTestRef);
                        mVarArgsWildcardPropTest = varArgsWildcardPropTestRef;
                        mContext = context;
                      }

                      @java.lang.Override
                      protected void setComponent(com.facebook.litho.Component component) {
                        mVarArgsWildcardPropTest = (com.facebook.litho.specmodels.generator.VarArgsWildcardPropTest) component;
                      }

                      @com.facebook.litho.annotations.PropSetter(
                          value = "colors",
                          required = false
                      )
                      public Builder color(@org.jetbrains.annotations.NotNull android.graphics.Color color) {
                        if (color == null) {
                          return this;
                        }
                        if (this.mVarArgsWildcardPropTest.colors == java.util.Collections.EMPTY_LIST) {
                          this.mVarArgsWildcardPropTest.colors = new java.util.ArrayList<android.graphics.Color>();
                        }
                        this.mVarArgsWildcardPropTest.colors.add(color);
                        return this;
                      }

                      @com.facebook.litho.annotations.PropSetter(
                          value = "colors",
                          required = false
                      )
                      public Builder colors(
                          @org.jetbrains.annotations.NotNull java.util.List<android.graphics.Color> colors) {
                        if (colors == null) {
                          return this;
                        }
                        if (this.mVarArgsWildcardPropTest.colors.isEmpty()) {
                          this.mVarArgsWildcardPropTest.colors = colors;
                        } else {
                          this.mVarArgsWildcardPropTest.colors.addAll(colors);
                        }
                        return this;
                      }

                      @java.lang.Override
                      public Builder getThis() {
                        return this;
                      }

                      @java.lang.Override
                      public com.facebook.litho.specmodels.generator.VarArgsWildcardPropTest build() {
                        return mVarArgsWildcardPropTest;
                      }
                    }

                """
                .trimIndent())
  }
}


/*
 * 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.specmodels.generator

import com.facebook.litho.annotations.MountSpec
import com.facebook.litho.annotations.OnBind
import com.facebook.litho.annotations.Prop
import com.facebook.litho.specmodels.internal.RunMode
import com.facebook.litho.specmodels.processor.MountSpecModelFactory
import com.google.testing.compile.CompilationRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@MountSpec
object MountTestSpec {
  @OnBind
  fun onBind(
      @Prop(dynamic = true) intArg: Int,
      @Prop(dynamic = true) longArg: Long,
      @Prop(dynamic = true) floatArg: Float,
      @Prop(dynamic = true) doubleArg: Double,
      @Prop(dynamic = true) objectArg: Any?
  ) = Unit
}

@RunWith(JUnit4::class)
class DynamicPropsEquivalenceGeneratorTest {

  @Rule @JvmField val compilationRule = CompilationRule()

  @Test
  fun `test dynamic prop equivalency checks only compare refs`() {
    val mountSpecModel =
        MountSpecModelFactory()
            .create(
                compilationRule.elements,
                compilationRule.types,
                compilationRule.elements.getTypeElement(MountTestSpec::class.java.canonicalName),
                null,
                RunMode.normal(),
                null,
                null)

    val generatedIsEquivalentMethod =
        ComponentBodyGenerator.generateIsEquivalentPropsMethod(mountSpecModel, RunMode.normal())
            .toString()

    assertThat(generatedIsEquivalentMethod)
        .isEqualTo(
            """
            @java.lang.Override
            public boolean isEquivalentProps(com.facebook.litho.Component other,
                boolean shouldCompareCommonProps) {
              if (this == other) {
                return true;
              }
              if (other == null || getClass() != other.getClass()) {
                return false;
              }
              MountTest mountTestRef = (MountTest) other;
              if (doubleArg != null ? !doubleArg.equals(mountTestRef.doubleArg) : mountTestRef.doubleArg != null) {
                return false;
              }
              if (floatArg != null ? !floatArg.equals(mountTestRef.floatArg) : mountTestRef.floatArg != null) {
                return false;
              }
              if (intArg != null ? !intArg.equals(mountTestRef.intArg) : mountTestRef.intArg != null) {
                return false;
              }
              if (longArg != null ? !longArg.equals(mountTestRef.longArg) : mountTestRef.longArg != null) {
                return false;
              }
              if (objectArg != null ? !objectArg.equals(mountTestRef.objectArg) : mountTestRef.objectArg != null) {
                return false;
              }
              return true;
            }

            """
                .trimIndent())
  }
}


/*
 * 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.testing.helper

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentTree
import com.facebook.litho.EventHandler
import com.facebook.litho.FocusedVisibleEvent
import com.facebook.litho.FullImpressionVisibleEvent
import com.facebook.litho.InvisibleEvent
import com.facebook.litho.Row
import com.facebook.litho.UnfocusedVisibleEvent
import com.facebook.litho.VisibleEvent
import com.facebook.litho.testing.TestLayoutComponent
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.reset
import org.mockito.kotlin.verify

/** Tests [VisibilityEventsHelper] */
@RunWith(LithoTestRunner::class)
class VisibilityEventsHelperTest {

  @Mock private val visibleEventEventHandler: EventHandler<VisibleEvent> = mock()

  @Mock private val invisibleEventEventHandler: EventHandler<InvisibleEvent> = mock()

  @Mock private val focusedVisibleEventEventHandler: EventHandler<FocusedVisibleEvent> = mock()

  @Mock private val unfocusedVisibleEventEventHandler: EventHandler<UnfocusedVisibleEvent> = mock()

  @Mock
  private val fullImpressionEventEventHandler: EventHandler<FullImpressionVisibleEvent> = mock()
  private lateinit var context: ComponentContext
  private lateinit var _componentTree: ComponentTree

  @Before
  fun setup() {
    MockitoAnnotations.initMocks(this)
    context = ComponentContext(ApplicationProvider.getApplicationContext<Context>())
    _componentTree = componentTreeWithHandlers
    reset(visibleEventEventHandler)
    reset(invisibleEventEventHandler)
    reset(focusedVisibleEventEventHandler)
    reset(unfocusedVisibleEventEventHandler)
    reset(fullImpressionEventEventHandler)
  }

  @Test
  fun triggerVisibleEventForEventsShouldDispatchHandler() {
    assertThat(
            VisibilityEventsHelper.triggerVisibilityEvent(_componentTree, VisibleEvent::class.java))
        .isTrue
    verify(visibleEventEventHandler).call(any<VisibleEvent>())
  }

  @Test
  fun triggerInvisibleEventForEventsShouldDispatchHandler() {
    assertThat(
            VisibilityEventsHelper.triggerVisibilityEvent(
                _componentTree, InvisibleEvent::class.java))
        .isTrue
    verify(invisibleEventEventHandler).call(any<InvisibleEvent>())
  }

  @Test
  fun triggerFocusedEventForEventsShouldDispatchHandler() {
    assertThat(
            VisibilityEventsHelper.triggerVisibilityEvent(
                _componentTree, FocusedVisibleEvent::class.java))
        .isTrue
    verify(focusedVisibleEventEventHandler).call(any<FocusedVisibleEvent>())
  }

  @Test
  fun triggerUnfocusedEventForEventsShouldDispatchHandler() {
    assertThat(
            VisibilityEventsHelper.triggerVisibilityEvent(
                _componentTree, UnfocusedVisibleEvent::class.java))
        .isTrue
    verify(unfocusedVisibleEventEventHandler).call(any<UnfocusedVisibleEvent>())
  }

  @Test
  fun triggerFullImpressionEventForEventsShouldDispatchHandler() {
    assertThat(
            VisibilityEventsHelper.triggerVisibilityEvent(
                _componentTree, FullImpressionVisibleEvent::class.java))
        .isTrue
    verify(fullImpressionEventEventHandler).call(any<FullImpressionVisibleEvent>())
  }

  @Test
  fun triggerEventWithoutHandlerShouldNotDispatchHandler() {
    val component = TestLayoutComponent.create(context).build()
    val componentTree = getComponentTree(component)
    reset(visibleEventEventHandler)
    assertThat(
            VisibilityEventsHelper.triggerVisibilityEvent(componentTree, VisibleEvent::class.java))
        .isFalse
  }

  @Test
  fun triggerEventShouldFindFirstHandler() {
    val component =
        Row.create(context)
            .child(Row.create(context).build())
            .child(Row.create(context).visibleHandler(visibleEventEventHandler).build())
            .build()
    val componentTree = getComponentTree(component)
    reset(visibleEventEventHandler)
    assertThat(
            VisibilityEventsHelper.triggerVisibilityEvent(componentTree, VisibleEvent::class.java))
        .isTrue
    verify(visibleEventEventHandler).call(any<VisibleEvent>())
  }

  private val componentWithHandlers: Component
    get() =
        Row.create(context)
            .visibleHandler(visibleEventEventHandler)
            .invisibleHandler(invisibleEventEventHandler)
            .focusedHandler(focusedVisibleEventEventHandler)
            .unfocusedHandler(unfocusedVisibleEventEventHandler)
            .fullImpressionHandler(fullImpressionEventEventHandler)
            .build()

  private fun getComponentTree(component: Component): ComponentTree =
      ComponentTestHelper.mountComponent(context, component).componentTree!!

  private val componentTreeWithHandlers: ComponentTree
    get() = getComponentTree(componentWithHandlers)
}


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

package com.facebook.litho.widget

import android.content.Context
import android.view.View
import com.facebook.litho.FixedSizeLayoutBehavior
import com.facebook.litho.LithoPrimitive
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.PrimitiveComponent
import com.facebook.litho.PrimitiveComponentScope
import com.facebook.litho.SimpleMountable
import com.facebook.litho.px
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.widget.collection.LazyList
import com.facebook.rendercore.MeasureResult
import com.facebook.rendercore.primitives.ViewAllocator
import com.facebook.rendercore.utils.MeasureSpecUtils.atMost
import com.facebook.rendercore.utils.MeasureSpecUtils.exactly
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.LooperMode

/** Tests to verify that we always prepare layouts in the same order we mount them. */
@LooperMode(LooperMode.Mode.LEGACY)
@RunWith(RobolectricTestRunner::class)
class RecyclerBinderPreparationOrderTest {

  @JvmField @Rule val lithoViewRule = LithoViewRule()

  @Test
  fun `default traversal order should match RecyclerView layout order for mountable`() {
    val prepareTracking = mutableListOf<Int>()
    val mountTracking = mutableListOf<Int>()

    val lithoView =
        lithoViewRule.createTestLithoView {
          LazyList {
            (1..10).forEach { tag ->
              child(
                  id = tag,
                  component =
                      PrepareTrackingMountableComponent(
                          prepareTracking = prepareTracking,
                          mountTracking = mountTracking,
                          tag = tag))
            }
          }
        }

    lithoViewRule.act(lithoView) {
      lithoView.setSizeSpecs(exactly(1000), atMost(400))
      lithoView.measure()
    }

    assertThat(prepareTracking).hasSize(10)
    assertThat(mountTracking).isEmpty()

    lithoView.layout()

    // Assert that what was mounted was also the first set of rows prepared
    assertThat(mountTracking).isEqualTo(prepareTracking.subList(0, mountTracking.size))
  }

  @Test
  fun `default traversal order should match RecyclerView layout order for primitive`() {
    val prepareTracking = mutableListOf<Int>()
    val mountTracking = mutableListOf<Int>()

    val lithoView =
        lithoViewRule.createTestLithoView {
          LazyList {
            (1..10).forEach { tag ->
              child(
                  id = tag,
                  component =
                      PrepareTrackingPrimitiveComponent(
                          prepareTracking = prepareTracking,
                          mountTracking = mountTracking,
                          tag = tag))
            }
          }
        }

    lithoViewRule.act(lithoView) {
      lithoView.setSizeSpecs(exactly(1000), atMost(400))
      lithoView.measure()
    }

    assertThat(prepareTracking).hasSize(10)
    assertThat(mountTracking).isEmpty()

    lithoView.layout()

    // Assert that what was mounted was also the first set of rows prepared
    assertThat(mountTracking).isEqualTo(prepareTracking.subList(0, mountTracking.size))
  }

  @Test
  fun `reverseLayout traversal order should match RecyclerView layout order for mountable`() {
    val prepareTracking = mutableListOf<Int>()
    val mountTracking = mutableListOf<Int>()

    val lithoView =
        lithoViewRule.createTestLithoView {
          LazyList(reverse = true) {
            (1..10).forEach { tag ->
              child(
                  id = tag,
                  component =
                      PrepareTrackingMountableComponent(
                          prepareTracking = prepareTracking,
                          mountTracking = mountTracking,
                          tag = tag))
            }
          }
        }

    lithoViewRule.act(lithoView) {
      lithoView.setSizeSpecs(exactly(1000), atMost(400))
      lithoView.measure()
    }

    assertThat(prepareTracking).hasSize(10)
    assertThat(mountTracking).isEmpty()

    lithoView.layout()

    // Assert that what was mounted was also the first set of rows prepared
    assertThat(mountTracking).isEqualTo(prepareTracking.subList(0, mountTracking.size))
  }

  @Test
  fun `reverseLayout traversal order should match RecyclerView layout order for primitive`() {
    val prepareTracking = mutableListOf<Int>()
    val mountTracking = mutableListOf<Int>()

    val lithoView =
        lithoViewRule.createTestLithoView {
          LazyList(reverse = true) {
            (1..10).forEach { tag ->
              child(
                  id = tag,
                  component =
                      PrepareTrackingPrimitiveComponent(
                          prepareTracking = prepareTracking,
                          mountTracking = mountTracking,
                          tag = tag))
            }
          }
        }

    lithoViewRule.act(lithoView) {
      lithoView.setSizeSpecs(exactly(1000), atMost(400))
      lithoView.measure()
    }

    assertThat(prepareTracking).hasSize(10)
    assertThat(mountTracking).isEmpty()

    lithoView.layout()

    // Assert that what was mounted was also the first set of rows prepared
    assertThat(mountTracking).isEqualTo(prepareTracking.subList(0, mountTracking.size))
  }
}

private class PrepareTrackingMountableComponent(
    val prepareTracking: MutableList<Int>,
    val mountTracking: MutableList<Int>,
    val tag: Int
) : MountableComponent() {
  override fun MountableComponentScope.render(): MountableRenderResult {
    prepareTracking.add(tag)
    return MountableRenderResult(
        PrepareTrackingMountable(mountTracking = mountTracking, tag = tag), style = null)
  }
}

private class PrepareTrackingMountable(val mountTracking: MutableList<Int>, val tag: Int) :
    SimpleMountable<View>(RenderType.VIEW) {

  override fun mount(c: Context, content: View, layoutData: Any?) {
    mountTracking.add(tag)
  }

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

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

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

private class PrepareTrackingPrimitiveComponent(
    val prepareTracking: MutableList<Int>,
    val mountTracking: MutableList<Int>,
    val tag: Int
) : PrimitiveComponent() {
  override fun PrimitiveComponentScope.render(): LithoPrimitive {
    prepareTracking.add(tag)
    return LithoPrimitive(
        layoutBehavior = FixedSizeLayoutBehavior(100.px, 100.px),
        mountBehavior =
            MountBehavior(ViewAllocator { context -> View(context) }) {
              bind(mountTracking, tag) {
                mountTracking.add(tag)
                onUnbind {}
              }
            },
        style = null)
  }
}


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

package com.facebook.litho.livedata

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.facebook.litho.AOSPLithoLifecycleProvider
import com.facebook.litho.ComponentScope
import com.facebook.litho.ComponentTree
import com.facebook.litho.LithoView
import com.facebook.litho.State
import com.facebook.litho.annotations.Hook
import com.facebook.litho.getTreeProp
import com.facebook.litho.onCleanup
import com.facebook.litho.useEffect
import com.facebook.litho.useState

/**
 * Uses the current value of a given [LiveData] in a Litho Kotlin component.
 *
 * The live data will be observed following the [AOSPLithoLifecycleProvider] lifecycle definition,
 * and it's usage requires the [LithoView] or [ComponentTree] to be using the
 * [AOSPLithoLifecycleProvider]
 */
@Hook
fun <T> ComponentScope.useLiveData(liveData: LiveData<T>): T? {
  return useLiveData(liveData = liveData, initialValue = { liveData.value })
}

/**
 * Uses the current value of a given [LiveData] in a Litho Kotlin component.
 *
 * The live data will be observed following the [AOSPLithoLifecycleProvider] lifecycle definition,
 * and it's usage requires the [LithoView] or [ComponentTree] to be using the
 * [AOSPLithoLifecycleProvider].
 *
 * This observation will also be canceled whenever [deps] change.
 */
@Hook
fun <T> ComponentScope.useLiveData(
    liveData: LiveData<T>,
    vararg deps: Any?,
    initialValue: () -> T?
): T? {
  val lifecycleOwner: LifecycleOwner =
      getTreeProp<LifecycleOwner>()
          ?: error(
              "There is no lifecycle owner. Make you created your LithoView with an AOSPLithoLifecycleProvider.")

  val state: State<T?> = useState { initialValue() }

  useEffect(lifecycleOwner, deps) {
    val observer = Observer<T> { liveDataResult -> state.update { liveDataResult } }
    liveData.observe(lifecycleOwner, observer)
    onCleanup { liveData.removeObserver(observer) }
  }

  return state.value
}


// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.

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

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.facebook.litho.AOSPLithoLifecycleProvider
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.ComponentTree
import com.facebook.litho.KComponent
import com.facebook.litho.kotlin.widget.Text
import com.facebook.litho.testing.LithoViewRule
import com.facebook.litho.testing.assertj.LithoAssertions.assertThat
import com.facebook.litho.testing.testrunner.LithoTestRunner
import org.assertj.core.api.Assertions
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

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

  private val fakeLifecycleOwner = FakeLifecycleOwner(Lifecycle.State.INITIALIZED)

  @get:Rule
  val rule: LithoViewRule =
      LithoViewRule(lithoLifecycleProvider = { AOSPLithoLifecycleProvider(fakeLifecycleOwner) })

  @Test
  fun `should observe initial live data value`() {
    val liveData = MutableLiveData("hello")

    val component = MyComponent(liveData)

    val lithoView = rule.render { component }

    assertThat(lithoView).hasVisibleText("hello")
  }

  @Test
  fun `should observe live data changes when in active state`() {
    val liveData = MutableLiveData("hello")

    val component = MyComponent(liveData)

    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_START)
    val lithoView = rule.render { component }

    assertThat(lithoView).hasVisibleText("hello")

    rule.act(lithoView) { liveData.value = "world" }

    assertThat(lithoView).hasVisibleText("world")
  }

  @Test
  fun `should not observe live data changes if not in active state`() {
    val liveData = MutableLiveData("hello")

    val component = MyComponent(liveData)

    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_CREATE)
    val lithoView = rule.render { component }

    assertThat(lithoView).hasVisibleText("hello")

    rule.act(lithoView) { liveData.value = "world" }

    assertThat(lithoView).doesNotHaveVisibleText("world")
  }

  @Test
  fun `should observe same live data from different components`() {
    val liveData = MutableLiveData("hello")

    val firstComponent = MyComponent(liveData)
    val secondComponent = MyComponent(liveData)

    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_START)

    // 1. in the beginning the first litho view will render the initial live data value
    val firstLithoView = rule.render { firstComponent }
    assertThat(firstLithoView).hasVisibleText("hello")

    // 2. we update the live data value, and that should be reflected in the first litho view
    liveData.value = "world"
    rule.idle()
    assertThat(firstLithoView).hasVisibleText("world")

    // 3. we render the second component (in a second litho view) and it should render the last live
    // data value
    val secondLithoView = rule.render { secondComponent }
    assertThat(secondLithoView).hasVisibleText("world")

    // 4. we update the live data value, and it should be reflected in both observing components
    liveData.value = "people"
    rule.idle()
    assertThat(firstLithoView).hasVisibleText("people")
    assertThat(secondLithoView).hasVisibleText("people")
  }

  @Test
  fun `should re-observe after becoming active again`() {
    val liveData = MutableLiveData("hello")

    val component = MyComponent(liveData)

    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_START)

    // 1. the litho view should reflect the initial live data value
    val lithoView = rule.render { component }
    assertThat(lithoView).hasVisibleText("hello")

    // 2. we change the lifecycle to an inactive state, and update the live data.
    // in this case, the live data won't emit as there is no active observer.
    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_STOP)
    liveData.value = "world"
    rule.idle()
    assertThat(lithoView).doesNotHaveVisibleText("world")

    // 3. we resume the lifecycle, and therefore it should show the latest store live data change
    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_RESUME)
    rule.idle()
    assertThat(lithoView).hasVisibleText("world")
  }

  @Test
  fun `should stop observing when lifecycle moves to destroyed`() {
    val liveData = MutableLiveData("hello")

    val component = MyComponent(liveData)

    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_START)

    val lithoView = rule.render { component }
    assertThat(lithoView).hasVisibleText("hello")
    Assertions.assertThat(liveData.hasObservers()).isTrue

    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_DESTROY)
    Assertions.assertThat(liveData.hasActiveObservers()).isFalse
  }

  @Test
  fun `should stop observing when component tree is released`() {
    val liveData = MutableLiveData("hello")

    val component = MyComponent(liveData)

    fakeLifecycleOwner.onEvent(Lifecycle.Event.ON_START)

    val componentTree =
        ComponentTree.create(
                rule.context, component, AOSPLithoLifecycleProvider(fakeLifecycleOwner))
            .build()

    val lithoView = rule.render(componentTree = componentTree) { component }
    assertThat(lithoView).hasVisibleText("hello")
    Assertions.assertThat(liveData.hasObservers()).isTrue

    componentTree.release()
    Assertions.assertThat(liveData.hasObservers()).isFalse
  }

  private class MyComponent(private val liveData: LiveData<String>) : KComponent() {

    override fun ComponentScope.render(): Component {
      val text = useLiveData(liveData)

      return Text(text = text)
    }
  }

  private class FakeLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {

    override val lifecycle: FakeLifecycle = FakeLifecycle(this, initialState)

    fun onEvent(lifecycleEvent: Lifecycle.Event) {
      lifecycle.onEvent(lifecycleEvent)
    }
  }

  private class FakeLifecycle(private val owner: LifecycleOwner, initialState: State) :
      LifecycleRegistry(owner) {

    private var lastEvent: Event? = null

    private val observers: MutableList<LifecycleObserver> = mutableListOf()

    init {
      currentState = initialState
    }

    fun onEvent(event: Event) {
      val newState =
          when (event) {
            Event.ON_CREATE -> State.CREATED
            Event.ON_START -> State.STARTED
            Event.ON_RESUME -> State.RESUMED
            Event.ON_PAUSE -> State.CREATED
            Event.ON_STOP -> State.CREATED
            Event.ON_DESTROY -> State.DESTROYED
            else -> State.INITIALIZED
          }

      currentState = newState
      lastEvent = event

      observers.filterIsInstance<LifecycleEventObserver>().forEach {
        it.onStateChanged(owner, event)
      }
    }

    override fun addObserver(observer: LifecycleObserver) {
      observers.add(observer)

      if (observer is LifecycleEventObserver) {
        lastEvent?.let { observer.onStateChanged(owner, it) }
      }
    }

    override fun removeObserver(observer: LifecycleObserver) {
      observers.remove(observer)
    }
  }
}


/*
 * 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 com.facebook.litho.FrameworkLogEvents.LogEventId

/**
 * An interface for logging events and performance events in litho as well as in user defined
 * components. The ComponentsLogger is set on the [ComponentContext]. See [FrameworkLogEvents] for a
 * list of events and parameters logged internally by Litho.
 */
interface ComponentsLogger {

  enum class LogLevel {
    WARNING,
    ERROR,
    FATAL
  }

  /**
   * Create a new performance event with the given event id and start counting the time. If the
   * logger doesn't care about this event id, it may return `null`.
   */
  fun newPerformanceEvent(@LogEventId eventId: Int): PerfEvent?

  /** Write a [PerfEvent] to storage. This also marks the end of the event. */
  fun logPerfEvent(event: PerfEvent)

  /** Release a previously obtained [PerfEvent] without logging it. */
  fun cancelPerfEvent(event: PerfEvent)

  /**
   * Provide additional log metadata based on the tree props of the component hierarchy currently
   * being logged. This can be useful if information about the component hierarchy is needed.
   *
   * @param treeProps is a map that contains the data that is held in the [TreeProps] of the
   *   [ComponentTree]
   * @return `null` efficiency purposes when no data needs to be logged, associative map otherwise.
   */
  @JvmSuppressWildcards
  fun getExtraAnnotations(treePropertyProvider: TreePropertyProvider): Map<String, String>? = null

  /** @return whether this event is being traced and getting logged. */
  fun isTracing(logEvent: PerfEvent): Boolean
}

/**
 * This is an abstraction that helps to decouple from the content in core litho APIs.
 *
 * From this, you should be able to get the value for a given tree property of a [ComponentTree].
 */
fun interface TreePropertyProvider {

  fun getProperty(key: Class<*>): Any?
}


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

/** Empty implementation of [PerfEvent] performing no actions. */
class NoOpPerfEvent : PerfEvent {

  override val instanceKey: Int = 0

  override val markerId: Int = 0

  override fun markerAnnotate(annotationKey: String, annotationValue: String?) = Unit

  override fun markerAnnotate(annotationKey: String, annotationValue: Int) = Unit

  override fun markerAnnotate(annotationKey: String, annotationValue: Boolean) = Unit

  override fun markerAnnotate(annotationKey: String, annotationValue: Array<String>) = Unit

  override fun markerAnnotate(annotationKey: String, annotationValue: Array<Double>) = Unit

  override fun markerAnnotate(annotationKey: String, annotationValue: IntArray) = Unit

  override fun markerPoint(eventName: String) = Unit

  override fun markerAnnotate(annotationKey: String, annotationValue: Double) = Unit
}


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

package com.facebook.litho

/**
 * A logging event created through [ComponentsLogger.newPerformanceEvent] to track performance
 * metrics in the framework.
 */
interface PerfEvent {

  /**
   * Identify a particular instance of a performance event if there are multiple, parallel events of
   * the same marker id.
   */
  val instanceKey: Int

  /** Identify the type of a performance event. */
  val markerId: Int

  /** Adds a key:value annotation to an already active marker. */
  fun markerAnnotate(annotationKey: String, annotationValue: String?)

  /** @see .markerAnnotate */
  fun markerAnnotate(annotationKey: String, annotationValue: Double)

  /** @see .markerAnnotate */
  fun markerAnnotate(annotationKey: String, annotationValue: Int)

  /** @see .markerAnnotate */
  fun markerAnnotate(annotationKey: String, annotationValue: Boolean)

  /** @see .markerAnnotate */
  fun markerAnnotate(annotationKey: String, annotationValue: Array<String>)

  /** @see .markerAnnotate */
  fun markerAnnotate(annotationKey: String, annotationValue: Array<Double>)

  /** @see .markerAnnotate */
  fun markerAnnotate(annotationKey: String, annotationValue: IntArray)

  /**
   * Annotate the current event with a sub-routing at the point of invocation under the given name.
   */
  fun markerPoint(eventName: String)
}


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

package com.facebook.rendercore

/**
 * A class storing data that was created in [RenderUnit.Binder.bind] and will be passed to
 * [RenderUnit.Binder.unbind].
 */
class BindData {
  var fixedBindersBindData: Array<Any?>? = null
    private set

  var optionalMountBindersBindData: MutableMap<Class<*>, Any?>? = null
    private set

  var attachBindersBindData: MutableMap<Class<*>, Any?>? = null
    private set

  /**
   * Sets the bindData at specified [index] to the provided value. If [fixedBindersBindData] doesn't
   * exist, it creates it and then sets the bindData value.
   */
  fun setFixedBindersBindData(bindData: Any?, index: Int, fixedMountBindersSize: Int) {
    if (bindData == null) {
      return
    }

    val bindersBindData: Array<Any?> = fixedBindersBindData ?: arrayOfNulls(fixedMountBindersSize)
    bindersBindData[index] = bindData
    if (fixedBindersBindData == null) {
      fixedBindersBindData = bindersBindData
    }
  }

  /**
   * Removes the bindData at specified [index] and returns it if it was present or `null` if it
   * wasn't present or [fixedBindersBindData] array was `null`.
   */
  fun removeFixedBinderBindData(index: Int): Any? {
    return fixedBindersBindData?.let {
      val bindData = it[index]
      it[index] = null
      bindData
    }
  }

  /**
   * Sets the bindData at specified [index] to the provided value. If [optionalMountBindersBindData]
   * doesn't exist, it creates it and then sets the bindData value.
   */
  fun setOptionalMountBindersBindData(
      bindData: Any?,
      key: Class<*>,
      optionalMountBindersSize: Int
  ) {
    if (bindData == null) {
      return
    }

    val bindersBindData: MutableMap<Class<*>, Any?> =
        optionalMountBindersBindData ?: LinkedHashMap(optionalMountBindersSize)
    bindersBindData[key] = bindData
    if (optionalMountBindersBindData == null) {
      optionalMountBindersBindData = bindersBindData
    }
  }

  /**
   * Removes the bindData for specified [key] and returns it if it was present or `null` if it
   * wasn't present or [optionalMountBindersBindData] map was `null`.
   */
  fun removeOptionalMountBindersBindData(key: Class<*>): Any? {
    return optionalMountBindersBindData?.remove(key)
  }

  /**
   * Sets the bindData at specified [index] to the provided value. If [attachBindersBindData]
   * doesn't exist, it creates it and then sets the bindData value.
   */
  fun setAttachBindersBindData(bindData: Any?, key: Class<*>, attachBindersSize: Int) {
    if (bindData == null) {
      return
    }

    val bindersBindData: MutableMap<Class<*>, Any?> =
        attachBindersBindData ?: LinkedHashMap(attachBindersSize)
    bindersBindData[key] = bindData
    if (attachBindersBindData == null) {
      attachBindersBindData = bindersBindData
    }
  }

  /**
   * Removes the bindData for specified [key] and returns it if it was present or `null` if it
   * wasn't present or [attachBindersBindData] map was `null`.
   */
  fun removeAttachBindersBindData(key: Class<*>): Any? {
    return attachBindersBindData?.remove(key)
  }
}


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

package com.facebook.rendercore

interface RenderTreeUpdateListener {
  fun onRenderTreeUpdated(host: RenderCoreExtensionHost)
}


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

package com.facebook.rendercore

import com.facebook.rendercore.Size.Companion.BitMask

/**
 * A class representing 2D size.
 *
 * In order to avoid runtime overhead [Size] is a Kotlin value class that wraps a Long value. Width
 * and height are encoded in a single Long value.
 */
@JvmInline
value class Size internal constructor(@PublishedApi internal val encodedValue: Long) {
  val width: Int
    get() = encodedValue.shr(32).and(BitMask).toInt()

  val height: Int
    get() {
      return encodedValue.and(BitMask).toInt()
    }

  operator fun component1(): Int = width

  operator fun component2(): Int = height

  val isValid: Boolean
    get() {
      return encodedValue != Invalid.encodedValue
    }

  fun copy(
      width: Int = this.width,
      height: Int = this.height,
  ): Size {
    return Size(width, height)
  }

  override fun toString(): String {
    return if (isValid) {
      "Size[width = $width, height = $height]"
    } else {
      "Size[Invalid]"
    }
  }

  companion object {
    /**
     * A value representing an invalid [Size]. Accessing width and height of an invalid [Size] is
     * not possible.
     */
    val Invalid: Size = Size(width = -1, height = -1)

    internal const val BitMask: Long = 0xFFFFFFFF
  }
}

/** Creates [Size] from the provided width and height. */
fun Size(width: Int, height: Int): Size {
  val v1 = width.toLong()
  val v2 = height.toLong()
  val packed = v1.shl(32) or (v2 and BitMask)
  return Size(packed)
}

/**
 * Returns the current size if it's valid. Otherwise executes [action] lambda and returns its
 * result.
 */
inline fun Size.getOrElse(action: () -> Size): Size {
  return if (isValid) {
    this
  } else {
    action()
  }
}