/*
 * 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 androidx.annotation.UiThread
import java.util.concurrent.CopyOnWriteArraySet

/**
 * Class represents a DynamicValue, and provides users with ability to change the value, by exposing
 * one public method [set] It also allows attaching listeners (package level access), and takes care
 * of notifying them when the held value changes.
 *
 * @param <T> type of value held </T>
 */
open class DynamicValue<T>(initialValue: T) {

  private var value: T = initialValue
    set(value) {
      if (field === value || field == value) return

      field = value
      for (listener in listeners) {
        listener.onValueChange(this)
      }
    }

  private val listeners: MutableSet<OnValueChangeListener<T>> = CopyOnWriteArraySet()

  /**
   * Sets current value and notifies all the attached listeners
   *
   * IMPORTANT: This should only be called from the main thread! If it isn't, it can race with other
   * set() calls!
   *
   * @param value the new value
   */
  @UiThread
  fun set(value: T) {
    this.value = value
  }

  /**
   * Retrieves the current value.
   *
   * IMPORTANT: This should only be called from the main thread! If it isn't, it can race with other
   * set() calls! Generally, you shouldn't need to call get() yourself: instead the DynamicValue
   * should be passed into a property or Style which accepts a DynamicValue.
   *
   * @return the current value
   */
  @UiThread fun get(): T = value

  /**
   * Register a callback to be invoked when the value changes.
   *
   * @param listener The callback to invoke.
   */
  fun attachListener(listener: OnValueChangeListener<T>) {
    listeners.add(listener)
  }

  /**
   * Remove a callback.
   *
   * @param listener The listener to detach
   */
  fun detach(listener: OnValueChangeListener<T>) {
    listeners.remove(listener)
  }

  /** Retrieves the amount of listeners attached to this [DynamicValue]. */
  val numberOfListeners: Int
    get() = listeners.size

  /** Interface definition for a callback to be invoked when value changes. */
  fun interface OnValueChangeListener<T> {
    /**
     * Called when a view has been clicked.
     *
     * @param value The dynamic value.
     */
    fun onValueChange(value: DynamicValue<T>)
  }
}


/*
 * 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 component which renders nothing. */
class EmptyComponent : Component() {

  override fun render(
      resolveStateContext: ResolveStateContext,
      c: ComponentContext,
      widthSpec: Int,
      heightSpec: Int
  ): RenderResult = RenderResult(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

import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat

/**
 * This class contains utility methods to send pre-defined events (click, touch, accessibility,
 * etc.) to [EventHandler] instances' [Component]s
 */
internal object EventDispatcherUtils {
  @JvmStatic
  fun dispatchOnClick(clickHandler: EventHandler<ClickEvent>, view: View) {
    ThreadUtils.assertMainThread()
    val clickEvent = ClickEvent()
    clickEvent.view = view
    val eventDispatcher = checkNotNull(clickHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    eventDispatcher.dispatchOnEvent(clickHandler, clickEvent)
  }

  @JvmStatic
  fun dispatchOnFocusChanged(
      focusChangeHandler: EventHandler<FocusChangedEvent>,
      view: View,
      hasFocus: Boolean
  ) {
    ThreadUtils.assertMainThread()
    val focusChangedEvent = FocusChangedEvent()
    focusChangedEvent.view = view
    focusChangedEvent.hasFocus = hasFocus
    val eventDispatcher =
        checkNotNull(focusChangeHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    eventDispatcher.dispatchOnEvent(focusChangeHandler, focusChangedEvent)
  }

  @JvmStatic
  fun dispatchOnLongClick(longClickHandler: EventHandler<LongClickEvent>, view: View): Boolean {
    ThreadUtils.assertMainThread()
    val longClickEvent = LongClickEvent()
    longClickEvent.view = view
    val eventDispatcher =
        checkNotNull(longClickHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    val returnValue = eventDispatcher.dispatchOnEvent(longClickHandler, longClickEvent)
    return returnValue != null && returnValue as Boolean
  }

  @JvmStatic
  fun dispatchOnTouch(
      touchHandler: EventHandler<TouchEvent>,
      view: View,
      event: MotionEvent
  ): Boolean {
    ThreadUtils.assertMainThread()
    val touchEvent = TouchEvent()
    touchEvent.view = view
    touchEvent.motionEvent = event
    val eventDispatcher = checkNotNull(touchHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    val returnValue = eventDispatcher.dispatchOnEvent(touchHandler, touchEvent)
    return returnValue != null && returnValue as Boolean
  }

  @JvmStatic
  fun dispatchOnInterceptTouch(
      interceptTouchHandler: EventHandler<InterceptTouchEvent>,
      view: View,
      event: MotionEvent
  ): Boolean {
    ThreadUtils.assertMainThread()
    val interceptTouchEvent = InterceptTouchEvent()
    interceptTouchEvent.motionEvent = event
    interceptTouchEvent.view = view
    val eventDispatcher =
        checkNotNull(interceptTouchHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    val returnValue = eventDispatcher.dispatchOnEvent(interceptTouchHandler, interceptTouchEvent)
    return returnValue != null && returnValue as Boolean
  }

  @JvmStatic
  fun dispatchDispatchPopulateAccessibilityEvent(
      eventHandler: EventHandler<DispatchPopulateAccessibilityEventEvent>,
      host: View,
      event: AccessibilityEvent,
      superDelegate: AccessibilityDelegateCompat?
  ): Boolean {
    ThreadUtils.assertMainThread()
    val dispatchPopulateAccessibilityEventEvent = DispatchPopulateAccessibilityEventEvent()
    dispatchPopulateAccessibilityEventEvent.host = host
    dispatchPopulateAccessibilityEventEvent.event = event
    dispatchPopulateAccessibilityEventEvent.superDelegate = superDelegate
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    val returnValue =
        eventDispatcher.dispatchOnEvent(eventHandler, dispatchPopulateAccessibilityEventEvent)
    return returnValue != null && returnValue as Boolean
  }

  @JvmStatic
  fun dispatchOnInitializeAccessibilityEvent(
      eventHandler: EventHandler<OnInitializeAccessibilityEventEvent>,
      host: View,
      event: AccessibilityEvent,
      superDelegate: AccessibilityDelegateCompat?
  ) {
    ThreadUtils.assertMainThread()
    val onInitializeAccessibilityEventEvent = OnInitializeAccessibilityEventEvent()
    onInitializeAccessibilityEventEvent.host = host
    onInitializeAccessibilityEventEvent.event = event
    onInitializeAccessibilityEventEvent.superDelegate = superDelegate
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    eventDispatcher.dispatchOnEvent(eventHandler, onInitializeAccessibilityEventEvent)
  }

  @JvmStatic
  fun dispatchOnInitializeAccessibilityNodeInfoEvent(
      eventHandler: EventHandler<OnInitializeAccessibilityNodeInfoEvent>,
      host: View,
      info: AccessibilityNodeInfoCompat,
      superDelegate: AccessibilityDelegateCompat?
  ) {
    ThreadUtils.assertMainThread()
    val onInitializeAccessibilityNodeInfoEvent = OnInitializeAccessibilityNodeInfoEvent()
    onInitializeAccessibilityNodeInfoEvent.host = host
    onInitializeAccessibilityNodeInfoEvent.info = info
    onInitializeAccessibilityNodeInfoEvent.superDelegate = superDelegate
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    eventDispatcher.dispatchOnEvent(eventHandler, onInitializeAccessibilityNodeInfoEvent)
  }

  @JvmStatic
  fun dispatchOnPopulateAccessibilityEvent(
      eventHandler: EventHandler<OnPopulateAccessibilityEventEvent>,
      host: View,
      event: AccessibilityEvent,
      superDelegate: AccessibilityDelegateCompat?
  ) {
    ThreadUtils.assertMainThread()
    val onPopulateAccessibilityEventEvent = OnPopulateAccessibilityEventEvent()
    onPopulateAccessibilityEventEvent.host = host
    onPopulateAccessibilityEventEvent.event = event
    onPopulateAccessibilityEventEvent.superDelegate = superDelegate
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    eventDispatcher.dispatchOnEvent(eventHandler, onPopulateAccessibilityEventEvent)
  }

  @JvmStatic
  fun dispatchOnPopulateAccessibilityNode(
      eventHandler: EventHandler<OnPopulateAccessibilityNodeEvent>,
      host: View,
      info: AccessibilityNodeInfoCompat
  ) {
    ThreadUtils.assertMainThread()
    val onPopulateAccessibilityNodeEvent = OnPopulateAccessibilityNodeEvent()
    onPopulateAccessibilityNodeEvent.host = host
    onPopulateAccessibilityNodeEvent.accessibilityNode = info
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    eventDispatcher.dispatchOnEvent(eventHandler, onPopulateAccessibilityNodeEvent)
  }

  @JvmStatic
  fun dispatchOnRequestSendAccessibilityEvent(
      eventHandler: EventHandler<OnRequestSendAccessibilityEventEvent>,
      host: ViewGroup,
      child: View,
      event: AccessibilityEvent,
      superDelegate: AccessibilityDelegateCompat?
  ): Boolean {
    ThreadUtils.assertMainThread()
    val onRequestSendAccessibilityEventEvent = OnRequestSendAccessibilityEventEvent()
    onRequestSendAccessibilityEventEvent.host = host
    onRequestSendAccessibilityEventEvent.child = child
    onRequestSendAccessibilityEventEvent.event = event
    onRequestSendAccessibilityEventEvent.superDelegate = superDelegate
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    val returnValue =
        eventDispatcher.dispatchOnEvent(eventHandler, onRequestSendAccessibilityEventEvent)
    return returnValue != null && returnValue as Boolean
  }

  @JvmStatic
  fun dispatchPerformAccessibilityActionEvent(
      eventHandler: EventHandler<PerformAccessibilityActionEvent>,
      host: View,
      action: Int,
      args: Bundle?,
      superDelegate: AccessibilityDelegateCompat?
  ): Boolean {
    ThreadUtils.assertMainThread()
    val performAccessibilityActionEvent = PerformAccessibilityActionEvent()
    performAccessibilityActionEvent.host = host
    performAccessibilityActionEvent.action = action
    performAccessibilityActionEvent.args = args
    performAccessibilityActionEvent.superDelegate = superDelegate
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    val returnValue = eventDispatcher.dispatchOnEvent(eventHandler, performAccessibilityActionEvent)
    return returnValue != null && returnValue as Boolean
  }

  @JvmStatic
  fun dispatchSendAccessibilityEvent(
      eventHandler: EventHandler<SendAccessibilityEventEvent>,
      host: View,
      eventType: Int,
      superDelegate: AccessibilityDelegateCompat?
  ) {
    ThreadUtils.assertMainThread()
    val sendAccessibilityEventEvent = SendAccessibilityEventEvent()
    sendAccessibilityEventEvent.host = host
    sendAccessibilityEventEvent.eventType = eventType
    sendAccessibilityEventEvent.superDelegate = superDelegate
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    eventDispatcher.dispatchOnEvent(eventHandler, sendAccessibilityEventEvent)
  }

  @JvmStatic
  fun dispatchSendAccessibilityEventUnchecked(
      eventHandler: EventHandler<SendAccessibilityEventUncheckedEvent>,
      host: View,
      event: AccessibilityEvent,
      superDelegate: AccessibilityDelegateCompat?
  ) {
    ThreadUtils.assertMainThread()
    val sendAccessibilityEventUncheckedEvent = SendAccessibilityEventUncheckedEvent()
    sendAccessibilityEventUncheckedEvent.host = host
    sendAccessibilityEventUncheckedEvent.event = event
    sendAccessibilityEventUncheckedEvent.superDelegate = superDelegate
    val eventDispatcher = checkNotNull(eventHandler.dispatchInfo.hasEventDispatcher).eventDispatcher
    eventDispatcher.dispatchOnEvent(eventHandler, sendAccessibilityEventUncheckedEvent)
  }
}


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

/**
 * Holds onto the mutable data used to dispatch an event. This data is kept up-to-date with the
 * latest version of the owning component or section by [EventHandlersController].
 */
class EventDispatchInfo(
    // These should only be null for manually created EventHandlers (i.e. not using
    // Component.newEventHandler
    @JvmField var hasEventDispatcher: HasEventDispatcher?,
    @JvmField var componentContext: ComponentContext?
) {
  @JvmField var tag: String? = 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

import android.util.Pair
import androidx.annotation.VisibleForTesting
import com.facebook.infer.annotation.ThreadSafe
import java.util.HashMap

/**
 * Controller which makes sure EventHandler instances are bound to the latest instance of the
 * Component/Section that created them.
 *
 * To see the problem this is trying to solve, consider a Section with a button Component with a
 * click handler as one of its rows. When the section updates (e.g. performs a state update),
 * nothing about how the button renders changes, so we don't want to re-resolve it. However, if we
 * don't re-resolve it, the click handler points to an old instance of the Section (the Section
 * before the state update).
 *
 * To get the best of both worlds, we don't re-render the button and use this class to update the
 * EventDispatchInfo on all EventHandlers created in this tree to their latest Component/Section
 * owners.
 *
 * In order to do this, all EventHandlers created by a given component/section (identified by their
 * global key) is given a reference to the same EventDispatchInfo instance. Then on each tree
 * resolution, we update this EventDispatchInfo with the latest instance of the context and
 * component/section.
 *
 * ## Details of the full flow in the context of Components
 * * - During a resolve or layout (or resolve+layout), we record all Spec-generated EventHandlers in
 *   a list local to this computation.
 * * - When we create EventHandlers, we create them with the latest ComponentContext and
 *   EventDispatcher (the two together are what we call DispatchInfo) but don't update any existing
 *   committed EventHandlers.
 * * - The ResolveResult has just the EventHandlers from resolve, but the LayoutState (result of
 *   layout computation) has both the EventHandlers created in resolve and the EventHandlers created
 *   in layout
 * * - If we commit this LayoutState, we 'canonicalize' the DispatchInfos on the EventHandlers we
 *   created. This means we update the collected EventHandler's DispatchInfos to be the 'canonical'
 *   DispatchInfos we already have for that global key (if we don't have one, that DispatchInfo
 *   becomes the canonical DispatchInfo for that key)
 * * - Finally, we update the canonical DispatchInfos for each global key to the latest
 *   ComponentContext/EventDispatcher from the newly committed LayoutState.
 *
 * By having all committed EventHandlers for a global key reference the same DispatchInfo, we can
 * update them all at the same time when a new LayoutState is committed.
 */
@ThreadSafe
class EventHandlersController {

  private val _dispatchInfos: MutableMap<String, DispatchInfoWrapper> = HashMap()

  /**
   * Updates the EventDispatchInfo for this global key with the latest context and
   * component/section. All EventHandlers created by this global key have the same EventDispatchInfo
   * instance.
   */
  @Synchronized
  fun updateEventDispatchInfoForGlobalKey(
      c: ComponentContext,
      dispatcher: HasEventDispatcher,
      globalKey: String?
  ) {
    if (globalKey == null) {
      return
    }
    val dispatchInfoWrapper = _dispatchInfos[globalKey] ?: return

    // Mark that the list of event handlers for this component is still needed.
    dispatchInfoWrapper.usedInCurrentTree = true

    // Set the latest dispatcher and context for all EventHandlers which reference this
    // EventDispatchInfo
    dispatchInfoWrapper.dispatchInfo.hasEventDispatcher = dispatcher
    dispatchInfoWrapper.dispatchInfo.componentContext = c
  }

  /**
   * If a global key doesn't appear in the current tree, remove its EventDispatchInfo from our book
   * keeping.
   */
  @Synchronized
  fun clearUnusedEventDispatchInfos() {
    val iterator = _dispatchInfos.entries.iterator()
    while (iterator.hasNext()) {
      val (_, dispatchInfoWrapper) = iterator.next()
      if (!dispatchInfoWrapper.usedInCurrentTree) {
        iterator.remove()
      } else {
        dispatchInfoWrapper.usedInCurrentTree = false
      }
    }
  }

  /**
   * For a list of Pair(GlobalKey, EventHandler), updates the EventDispatchInfo on each EventHandler
   * to be the canonical EventDispatchInfos for the corresponding global key. This means that from
   * now on, when EventDispatchInfos are re-bound in [.updateEventDispatchInfoForGlobalKey], these
   * EventHandlers will also be updated.
   */
  @Synchronized
  fun canonicalizeEventDispatchInfos(eventHandlers: List<Pair<String, EventHandler<*>>>) {
    for (entry in eventHandlers) {
      val globalKey = entry.first
      val eventHandler = entry.second
      var existingDispatchInfo = _dispatchInfos[globalKey]
      if (existingDispatchInfo == null) {
        existingDispatchInfo = DispatchInfoWrapper(eventHandler.dispatchInfo)
        _dispatchInfos[globalKey] = existingDispatchInfo
      } else {
        eventHandler.dispatchInfo = existingDispatchInfo.dispatchInfo
      }
    }
  }

  @get:Synchronized
  @get:VisibleForTesting
  val dispatchInfos: Map<String, DispatchInfoWrapper>
    get() = _dispatchInfos

  @VisibleForTesting
  class DispatchInfoWrapper(val dispatchInfo: EventDispatchInfo) {
    var usedInCurrentTree: 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

import com.facebook.infer.annotation.ThreadSafe

/**
 * A class implementing this fun interface wll expose a method annotated with
 * [com.facebook.litho.annotations.OnTrigger] to accept an [com.facebook.litho.annotations.Event]
 * given an [EventTrigger]
 */
@ThreadSafe
interface EventTriggerTarget {

  fun acceptTriggerEvent(eventTrigger: EventTrigger<*>, eventState: Any, params: Array<Any>): 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

import com.facebook.rendercore.RunnableHandler
import java.util.HashMap
import java.util.concurrent.Executor
import javax.annotation.concurrent.GuardedBy

/**
 * A `LithoHandler` implementation that runs all layout computations against the provided
 * `Executor`. This `LithoHandler` can be used in apps that have a well established threading model
 * and need to run layout against one of their already available executors.
 */
class ExecutorLithoHandler
/**
 * Instantiates an `ExecutorLithoHandler` that uses the given `Executor` to run all layout tasks
 * against.
 */
(private val executor: Executor) : RunnableHandler {

  @GuardedBy("pendingTasks") private val pendingTasks: MutableMap<Runnable, Int> = HashMap()

  override fun post(runnable: Runnable, tag: String) {
    synchronized(pendingTasks) {
      val runCount = pendingTasks[runnable] ?: 0
      pendingTasks.put(runnable, runCount + 1)
    }

    executor.execute {
      var canRun: Boolean
      synchronized(pendingTasks) {
        var runCount: Int = pendingTasks[runnable] ?: 0
        canRun = runCount > 0
        runCount--
        if (runCount > 0) {
          pendingTasks[runnable] = runCount
        } else {
          pendingTasks.remove(runnable)
        }
      }

      if (canRun) {
        runnable.run()
      }
    }
  }

  /**
   * {@inheritDoc}
   *
   * In this implementation, postAtFront() is equivalent to post().
   */
  override fun postAtFront(runnable: Runnable, tag: String) {
    post(runnable, tag)
  }

  /**
   * {@inheritDoc}
   *
   * This implementation removes all instances of the provided `Runnable` and prevents them from
   * running if they have not started yet.
   */
  override fun remove(runnable: Runnable) {
    synchronized(pendingTasks) { pendingTasks.remove(runnable) }
  }

  override fun isTracing(): 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

/**
 * A class implementing this fun interface will expose a method to record all the [EventTrigger]
 * into a [EventTriggersContainer]
 */
fun interface HasEventTrigger {
  fun recordEventTrigger(c: ComponentContext, container: EventTriggersContainer)
}


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

/**
 * An fun interface that a mountable view can extend which informs that this mountable content has
 * other [LithoView] children. This is used to make sure to unmount this view's children when
 * unmounting this view itself.
 */
@JvmSuppressWildcards
fun interface HasLithoViewChildren {

  fun obtainLithoViewChildren(lithoViews: List<BaseMountingView>)
}


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

/** Interface for a block of code that will initialize a hook value. */
fun interface HookInitializer<T> {
  fun init(): T
}


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

/**
 * EXPERIMENTAL - fun interface for a block of code that will update state created with useState.
 * See KState.kt for more details.
 */
interface HookUpdater {

  /** Get a [KStateContainer] instance with updates applied. */
  fun getUpdatedStateContainer(currentState: KStateContainer): KStateContainer
}


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

package com.facebook.litho

import android.graphics.drawable.Drawable
import kotlin.jvm.JvmField

/** A UI element that contains simple resource drawables. */
interface ImageContent {

  /**
   * @return the list of image drawables that are rendered by this UI element. The list returned
   *   should not be modified and may be unmodifiable.
   */
  val imageItems: List<Drawable>

  companion object {
    /** An empty instance of [ImageContent]. */
    @JvmField
    val EMPTY: ImageContent =
        object : ImageContent {
          override val imageItems: List<Drawable>
            get() = emptyList()
        }
  }
}


/*
 * 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 androidx.annotation.VisibleForTesting
import com.facebook.litho.ComponentsSystrace.beginSection
import com.facebook.litho.ComponentsSystrace.endSection
import com.facebook.litho.ComponentsSystrace.isTracing
import java.util.Collections
import java.util.HashMap
import java.util.HashSet
import javax.annotation.concurrent.GuardedBy
import javax.annotation.concurrent.ThreadSafe

/**
 * The InitialStateContainer is a lookaside table used by a ComponentTree to create initial states
 * for Components. The idea is that the onCreateInitialState result for each component will be
 * cached and stored here so that we can guarantee it's not called multiple times on multiple
 * threads. We keep the initial states cached as long as there is a layout happening for the
 * ComponentTree. As soon as we detect that all in-flights layout have terminated we can clean up
 * the initial states cache.
 */
@ThreadSafe
class InitialStateContainer {

  // All the initial states that have been created and can not yet be released. This is a concurrent
  // map as we can access it from multiple threads. The safety is given by the fact that we will
  // only get and set for a key while holding a lock for that specific key.
  @JvmField
  @VisibleForTesting
  val initialStates = Collections.synchronizedMap(HashMap<String, StateContainer>())

  @GuardedBy("this") private val createInitialStateLocks: MutableMap<String, Any> = HashMap()

  @JvmField
  @GuardedBy("this")
  @VisibleForTesting
  var pendingStateHandlers: MutableSet<StateHandler> = HashSet()

  /**
   * Called when the ComponentTree creates a new StateHandler for a new layout computation. We keep
   * track of this new StateHandler so that we know that we need to wait for this layout computation
   * to finish before we can clear the initial states map.
   */
  @Synchronized
  fun registerStateHandler(stateHandler: StateHandler) {
    pendingStateHandlers.add(stateHandler)
  }

  /**
   * If an initial state for this component has already been created just transfers it to it.
   * Otherwise onCreateInitialState gets called for the component and its result cached.
   */
  fun createOrGetInitialStateForComponent(
      component: Component,
      scopedContext: ComponentContext,
      key: String
  ): StateContainer {
    val stateLock: Any = synchronized(this) { createInitialStateLocks.getOrPut(key) { Any() } }

    return synchronized(stateLock) {
      initialStates.getOrPut(key) {
        createInitialStateContainer(context = scopedContext, component = component)
      }
    }
  }

  private fun createInitialStateContainer(
      context: ComponentContext,
      component: Component
  ): StateContainer {
    val isTracing = isTracing
    if (isTracing) {
      beginSection("create-initial-state:${component.simpleName}")
    }
    val state = (component as SpecGeneratedComponent).createInitialStateContainer(context)
    if (isTracing) {
      endSection()
    }
    return state
  }

  fun getInitialStateForComponent(key: String): StateContainer? {
    val stateLock: Any = synchronized(this) { createInitialStateLocks.getOrPut(key) { Any() } }

    synchronized(stateLock) {
      return initialStates[key]
    }
  }

  /**
   * If an initial state for this component has already been created just return it, otherwise
   * execute the initializer and cache the result.
   */
  fun <T> createOrGetInitialHookState(
      key: String,
      hookIndex: Int,
      initializer: HookInitializer<T>,
      componentName: String,
  ): KStateContainer {
    val stateLock: Any = synchronized(this) { createInitialStateLocks.getOrPut(key) { Any() } }

    return synchronized(stateLock) {
      val initialHookStates = initialStates[key] as KStateContainer?

      // sequences are guaranteed to be used in order. If the states list size is greater than
      // hookIndex we should be guaranteed to find the state
      if (initialHookStates != null && initialHookStates.states.size > hookIndex) {
        return initialHookStates
      }

      val isTracing = isTracing
      if (isTracing) {
        beginSection("create-initial-state:${componentName}[$hookIndex]")
      }
      val initialState = initializer.init()
      if (isTracing) {
        endSection()
      }

      // If the state needed to be initialised it should be guaranteed that it needs to be added at
      // the end of the list. We create a new KStateContainer to guarantee immutability of state
      // containers.
      val hookStates = KStateContainer.withNewState(initialHookStates, initialState)
      check(hookIndex < hookStates.states.size) {
        ("Unexpected useState hook sequence encountered: $hookIndex (states size: ${hookStates.states.size}). This usually indicates that the useState hook is being called from within a conditional, loop, or after an early-exit condition. See https://fblitho.com/docs/mainconcepts/hooks-intro/#rules-for-hooks for more information on the Rules of Hooks.")
      }
      initialStates[key] = hookStates
      hookStates
    }
  }

  /**
   * Called when the ComponentTree commits a new StateHandler or discards one for a discarded layout
   * computation.
   */
  @Synchronized
  fun unregisterStateHandler(stateHandler: StateHandler) {
    pendingStateHandlers.remove(stateHandler)
    if (pendingStateHandlers.isEmpty()) {
      createInitialStateLocks.clear()
      // This is safe as we have a guarantee that by this point there is no layout happening
      // and therefore we can not be executing createOrGetInitialStateForComponent or
      // createOrGetInitialHookState from any thread.
      initialStates.clear()
    }
  }
}


/*
 * 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 java.lang.UnsupportedOperationException
import java.util.ArrayList
import java.util.Collections

/**
 * The StateContainer implementation for Kotlin components. It tracks all the state defined by
 * useState calls for the same component. See KState for how this is being used. This is a purely
 * immutable class and it exposes utilities to create a new instance with either a new piece of
 * state or by changing the value at a given index
 */
class KStateContainer : StateContainer {

  val states: List<Any?>

  private constructor(kStateContainer: KStateContainer?, state: Any?) {
    val states: MutableList<Any?>
    if (kStateContainer != null) {
      states = ArrayList(kStateContainer.states.size + 1)
      states.addAll(kStateContainer.states)
    } else {
      states = ArrayList()
    }
    states.add(state)
    this.states = Collections.unmodifiableList(states)
  }

  private constructor(kStateContainer: KStateContainer, index: Int, newValue: Any?) {
    val states = ArrayList(kStateContainer.states)
    states[index] = newValue
    this.states = Collections.unmodifiableList(states)
  }

  override fun applyStateUpdate(stateUpdate: StateUpdate) {
    throw UnsupportedOperationException(
        "Kotlin states should not be updated through applyStateUpdate calls")
  }

  fun copyAndMutate(index: Int, newValue: Any?): KStateContainer {
    return KStateContainer(this, index, newValue)
  }

  companion object {
    @JvmStatic
    fun withNewState(kStateContainer: KStateContainer?, state: Any?): KStateContainer =
        KStateContainer(kStateContainer, state)
  }
}


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

package com.facebook.litho

import android.view.View
import androidx.annotation.IntDef

/** Enumerates all the valid valid for [Component.Builder#layerType(int, Paint)] */
@IntDef(
    LayerType.LAYER_TYPE_NOT_SET,
    LayerType.LAYER_TYPE_NONE,
    LayerType.LAYER_TYPE_SOFTWARE,
    LayerType.LAYER_TYPE_HARDWARE)
annotation class LayerType {
  companion object {
    const val LAYER_TYPE_NOT_SET = -1
    const val LAYER_TYPE_NONE = View.LAYER_TYPE_NONE
    const val LAYER_TYPE_SOFTWARE = View.LAYER_TYPE_SOFTWARE
    const val LAYER_TYPE_HARDWARE = View.LAYER_TYPE_HARDWARE
  }
}


/*
 * 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 androidx.annotation.Px
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaDirection
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaPositionType

interface LayoutProps {

  fun widthPx(@Px width: Int)

  fun widthPercent(percent: Float)

  fun minWidthPx(@Px minWidth: Int)

  fun maxWidthPx(@Px maxWidth: Int)

  fun minWidthPercent(percent: Float)

  fun maxWidthPercent(percent: Float)

  fun heightPx(@Px height: Int)

  fun heightPercent(percent: Float)

  fun minHeightPx(@Px minHeight: Int)

  fun maxHeightPx(@Px maxHeight: Int)

  fun minHeightPercent(percent: Float)

  fun maxHeightPercent(percent: Float)

  fun layoutDirection(direction: YogaDirection)

  fun alignSelf(alignSelf: YogaAlign)

  fun flex(flex: Float)

  fun flexGrow(flexGrow: Float)

  fun flexShrink(flexShrink: Float)

  fun flexBasisPx(@Px flexBasis: Int)

  fun flexBasisPercent(percent: Float)

  fun aspectRatio(aspectRatio: Float)

  fun positionType(positionType: YogaPositionType)

  fun positionPx(edge: YogaEdge, @Px position: Int)

  fun positionPercent(edge: YogaEdge, percent: Float)

  fun paddingPx(edge: YogaEdge, @Px padding: Int)

  fun paddingPercent(edge: YogaEdge, percent: Float)

  fun marginPx(edge: YogaEdge, @Px margin: Int)

  fun marginPercent(edge: YogaEdge, percent: Float)

  fun marginAuto(edge: YogaEdge)

  fun isReferenceBaseline(isReferenceBaseline: Boolean)

  fun useHeightAsBaseline(useHeightAsBaseline: Boolean)

  fun heightAuto()

  fun widthAuto()

  fun flexBasisAuto()

  /** Used by [DebugLayoutNodeEditor] */
  fun setBorderWidth(edge: YogaEdge, borderWidth: Float)
}


/*
 * 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 java.lang.Exception

/**
 * Implementation of [Attachable] for layout specs. Handles dispatching
 * [com.facebook.litho.annotations.OnAttached] and [com.facebook.litho.annotations.OnDetached] to
 * the given Component when the Attachable is attached/detached.
 */
internal class LayoutSpecAttachable(
    override val uniqueId: String,
    private val component: SpecGeneratedComponent,
    private val scopedComponentInfo: ScopedComponentInfo
) : Attachable {

  override fun attach() {
    val scopedContext = checkNotNull(scopedComponentInfo.context)
    try {
      component.onAttached(scopedContext)
    } catch (e: Exception) {
      ComponentUtils.handle(scopedContext, e)
    }
  }

  override fun detach() {
    val scopedContext = checkNotNull(scopedComponentInfo.context)
    try {
      component.onDetached(scopedContext)
    } catch (e: Exception) {
      ComponentUtils.handle(scopedContext, e)
    }
  }

  override fun shouldUpdate(nextAttachable: Attachable): Boolean = false

  override fun useLegacyUpdateBehavior(): Boolean = true
}


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

package com.facebook.litho

import com.facebook.rendercore.debug.DebugEvent
import com.facebook.rendercore.debug.DebugEventSubscriber

/**
 * This is used as a listener for events that happen on the scope of a given [ComponentTree]. These
 * events can be related with both the `render` UI pipeline, or with the view-side events created by
 * rendercore in the [LithoView] that is associated to the [ComponentTree] to which this listener
 * observes.
 *
 * In order to specify which events you are interested in, you should override the
 * [ComponentTreeDebugEventListener.events] and list any of the events present in [LithoDebugEvent]
 * or [DebugEvent].
 *
 * A client can attach one of these listeners during the creation of a [ComponentTree]:
 * ```
 * val listener = object: ComponentTreeDebugEventListener {
 *   override fun onEvent(debugEvent: DebugEvent) {
 *      /* do your logging / business logic */
 *   }
 *
 *   override val events = setOf(LayoutCommitted, MountItemMount)
 * }
 *
 * val componentTree = ComponentTree.create(context, MyComponent())
 *  .withComponentTreeDebugEventListener(listener)
 *  .create()
 *
 * val lithoView = LithoView.create(context, componentTree)
 * ```
 */
interface ComponentTreeDebugEventListener {

  fun onEvent(debugEvent: DebugEvent)

  val events: Set<String>
}

/**
 * This abstraction acts as a wrapper which guarantees that it will act only in events related to
 * the [componentTreeId].
 *
 * This is meant to be used only by the internals of the [ComponentTree], by wrapping any
 * [ComponentTreeDebugEventListener] passed in by the clients. This is a mechanism so that we can
 * guarantee that the client's listener will only observe events related to the [ComponentTree] to
 * which it is associated.
 *
 * *Note:* ideally this code would belong inside the [ComponentTree], but to keep it in Kotlin we
 * are leaving it outside it until finish the Kotlin migration.
 */
internal class ComponentTreeDebugEventsSubscriber(
    componentTreeId: Int,
    eventsToObserve: Set<String>,
    private val onComponentTreeEvent: (DebugEvent) -> Unit,
) : DebugEventSubscriber(*eventsToObserve.toTypedArray()) {

  private val componentTreeIdStr = componentTreeId.toString()

  override fun onEvent(event: DebugEvent) {
    if (event.renderStateId != componentTreeIdStr) {
      return
    }

    onComponentTreeEvent(event)
  }
}


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

package com.facebook.litho

import android.graphics.Rect
import com.facebook.litho.MountSpecLithoRenderUnit.UpdateState
import com.facebook.rendercore.RenderTreeNode

/**
 * This object will host the data associated with the component which is generated during the
 * measure pass, for example: the [InterStagePropsContainer], and the [UpdateState]. It will be
 * created in [LayoutResult#calculateLayout(LayoutContext, int, int)]. This object will be returned
 * by [LayoutResult#getLayoutData()], then written to the layout data in [RenderTreeNode] during
 * reduce.
 */
class LithoLayoutData(
    @field:JvmField val width: Int,
    @field:JvmField val height: Int,
    @field:JvmField val currentLayoutStateId: Int,
    @field:JvmField val previousLayoutStateId: Int,
    @field:JvmField val expandedTouchBounds: Rect?,
    @field:JvmField val layoutData: Any?
) {
  companion object {
    /**
     * Helper method to throw exception if a provided layout-data is null or not a LithoLayoutData
     * instance. Will return a casted, non-null instance of LithoLayoutData otherwise.
     */
    @JvmStatic
    fun verifyAndGetLithoLayoutData(layoutData: Any?): LithoLayoutData {
      if (layoutData == null) {
        throw RuntimeException("layout data must not be null.")
      }
      if (layoutData !is LithoLayoutData) {
        throw RuntimeException(
            "RenderTreeNode layout data for Litho should be LithoLayoutData but was <cls>${layoutData.javaClass.name}</cls>")
      }
      return layoutData
    }

    @JvmStatic
    fun getInterStageProps(data: Any?): InterStagePropsContainer? {
      val layoutData = verifyAndGetLithoLayoutData(data)
      if (layoutData.layoutData == null) {
        return null
      }
      if (layoutData.layoutData !is InterStagePropsContainer) {
        throw RuntimeException(
            "Layout data was not InterStagePropsContainer but was <cls>${layoutData.layoutData.javaClass.name}</cls>")
      }
      return layoutData.layoutData
    }

    @JvmStatic
    fun getExpandedTouchBounds(data: Any?): Rect? {
      return verifyAndGetLithoLayoutData(data).expandedTouchBounds
    }
  }
}


/*
 * 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.LithoLifecycleProvider.LithoLifecycle

/** Can observe lifecycle changes that a LithoLifecycleProvider goes through. */
fun interface LithoLifecycleListener {
  fun onMovedToState(state: LithoLifecycle)
}


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

/**
 * Manages a Litho ComponentTree lifecycle and informs subscribed LithoLifecycleListeners when a
 * lifecycle state occurs.
 */
interface LithoLifecycleProvider {

  enum class LithoLifecycle(private val text: String) {
    HINT_VISIBLE("HINT_VISIBLE"),
    HINT_INVISIBLE("HINT_INVISIBLE"),
    DESTROYED("DESTROYED");

    fun equalsName(otherName: String): Boolean = text == otherName

    override fun toString(): String = text
  }

  fun moveToLifecycle(lithoLifecycle: LithoLifecycle)

  val lifecycleStatus: LithoLifecycle

  fun addListener(listener: LithoLifecycleListener)

  fun removeListener(listener: LithoLifecycleListener)
}


/*
 * 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 androidx.annotation.IntDef
import androidx.annotation.UiThread
import com.facebook.litho.LithoLifecycleProvider.LithoLifecycle
import com.facebook.litho.LithoLifecycleProviderDelegate.LifecycleTransitionStatus
import java.lang.IllegalStateException
import java.util.ArrayList

/**
 * Default [LithoLifecycleProvider] implementation. Defines the standard state changes for a Litho
 * ComponentTree. A custom LithoLifecycleProvider implementation can change the lifecycle state but
 * delegate to this to handle the effects of the state change. See an example of how this
 * facilitates a custom lifecycle implementation in [AOSPLithoLifecycleProvider].
 */
class LithoLifecycleProviderDelegate : LithoLifecycleProvider {

  private val lithoLifecycleListeners: MutableList<LithoLifecycleListener> = ArrayList(4)
  override var lifecycleStatus = LithoLifecycle.HINT_VISIBLE
    private set

  @IntDef(
      LifecycleTransitionStatus.VALID,
      LifecycleTransitionStatus.NO_OP,
      LifecycleTransitionStatus.INVALID)
  annotation class LifecycleTransitionStatus {
    companion object {
      const val VALID = 0
      const val NO_OP = 1
      const val INVALID = 2
    }
  }

  @UiThread
  override fun moveToLifecycle(newLifecycleState: LithoLifecycle) {
    ThreadUtils.assertMainThread()
    if (newLifecycleState === LithoLifecycle.DESTROYED &&
        lifecycleStatus === LithoLifecycle.HINT_VISIBLE) {
      moveToLifecycle(LithoLifecycle.HINT_INVISIBLE)
    }
    @LifecycleTransitionStatus
    val transitionStatus = getTransitionStatus(lifecycleStatus, newLifecycleState)
    if (transitionStatus == LifecycleTransitionStatus.INVALID) {
      ComponentsReporter.emitMessage(
          ComponentsReporter.LogLevel.WARNING,
          "LithoLifecycleProvider",
          "Cannot move from state $lifecycleStatus to state $newLifecycleState")
      return
    }
    if (transitionStatus == LifecycleTransitionStatus.VALID) {
      lifecycleStatus = newLifecycleState
      when (newLifecycleState) {
        LithoLifecycle.HINT_VISIBLE -> {
          notifyOnResumeVisible()
          return
        }
        LithoLifecycle.HINT_INVISIBLE -> {
          notifyOnPauseVisible()
          return
        }
        LithoLifecycle.DESTROYED -> {
          notifyOnDestroy()
          return
        }
        else -> throw IllegalStateException("State not known")
      }
    }
  }

  @Synchronized
  override fun addListener(listener: LithoLifecycleListener) {
    lithoLifecycleListeners.add(listener)
  }

  @Synchronized
  override fun removeListener(listener: LithoLifecycleListener) {
    lithoLifecycleListeners.remove(listener)
  }

  private fun notifyOnResumeVisible() {
    val lithoLifecycleListeners: List<LithoLifecycleListener>
    synchronized(this) { lithoLifecycleListeners = ArrayList(this.lithoLifecycleListeners) }
    for (lithoLifecycleListener in lithoLifecycleListeners) {
      lithoLifecycleListener.onMovedToState(LithoLifecycle.HINT_VISIBLE)
    }
  }

  private fun notifyOnPauseVisible() {
    val lithoLifecycleListeners: List<LithoLifecycleListener>
    synchronized(this) { lithoLifecycleListeners = ArrayList(this.lithoLifecycleListeners) }
    for (lithoLifecycleListener in lithoLifecycleListeners) {
      lithoLifecycleListener.onMovedToState(LithoLifecycle.HINT_INVISIBLE)
    }
  }

  private fun notifyOnDestroy() {
    val lithoLifecycleListeners: List<LithoLifecycleListener>
    synchronized(this) { lithoLifecycleListeners = ArrayList(this.lithoLifecycleListeners) }
    for (lithoLifecycleListener in lithoLifecycleListeners) {
      lithoLifecycleListener.onMovedToState(LithoLifecycle.DESTROYED)
    }
  }

  companion object {
    @LifecycleTransitionStatus
    private fun getTransitionStatus(currentState: LithoLifecycle, nextState: LithoLifecycle): Int {
      if (currentState === LithoLifecycle.DESTROYED) {
        return LifecycleTransitionStatus.INVALID
      }
      if (nextState === LithoLifecycle.DESTROYED) {
        // You have to move through HINT_INVISIBLE before moving to DESTROYED.
        return if (currentState === LithoLifecycle.HINT_INVISIBLE) LifecycleTransitionStatus.VALID
        else LifecycleTransitionStatus.INVALID
      }
      if (nextState === LithoLifecycle.HINT_VISIBLE) {
        if (currentState === LithoLifecycle.HINT_VISIBLE) {
          return LifecycleTransitionStatus.NO_OP
        }
        return if (currentState === LithoLifecycle.HINT_INVISIBLE) {
          LifecycleTransitionStatus.VALID
        } else {
          LifecycleTransitionStatus.INVALID
        }
      }
      if (nextState === LithoLifecycle.HINT_INVISIBLE) {
        if (currentState === LithoLifecycle.HINT_INVISIBLE) {
          return LifecycleTransitionStatus.NO_OP
        }
        return if (currentState === LithoLifecycle.HINT_VISIBLE) {
          LifecycleTransitionStatus.VALID
        } else {
          LifecycleTransitionStatus.INVALID
        }
      }
      return LifecycleTransitionStatus.INVALID
    }
  }
}


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

package com.facebook.litho

import android.annotation.SuppressLint

/** Exception class used to add additional Litho metadata to a crash. */
class LithoMetadataExceptionWrapper
private constructor(
    private val componentContext: ComponentContext?,
    private val componentTree: ComponentTree?,
    cause: Throwable
) : RuntimeException() {
  @JvmField var lastHandler: EventHandler<ErrorEvent>? = null
  private val componentNameLayoutStack = ArrayList<String>()
  private val customMetadata = HashMap<String, String>()

  init {
    initCause(cause)
    stackTrace = emptyArray()
  }

  internal constructor(cause: Throwable) : this(null, null, cause)

  internal constructor(
      componentContext: ComponentContext?,
      cause: Throwable
  ) : this(componentContext, null, cause)

  constructor(componentTree: ComponentTree?, cause: Throwable) : this(null, componentTree, cause)

  fun addComponentNameForLayoutStack(componentName: String) {
    componentNameLayoutStack.add(componentName)
  }

  // the crashing componentName is always the first in the mComponentNameLayoutStack, if it exists
  val crashingComponentName: String?
    get() = componentNameLayoutStack.firstOrNull()

  fun addCustomMetadata(key: String, value: String) {
    customMetadata[key] = value
  }

  @get:SuppressLint("BadMethodUse-java.lang.Class.getName", "ReflectionMethodUse")
  override val message: String
    get() {
      val msg = StringBuilder("Real Cause")
      val cause = deepestCause
      if (componentContext != null && componentContext.componentScope != null) {
        msg.append(" at <cls>")
            .append(componentContext.componentScope.javaClass.name)
            .append("</cls>")
      }
      msg.append(" => ")
          .append(cause.javaClass.canonicalName)
          .append(": ")
          .appendLine(cause.message)
          .appendLine("Litho Context:")
      if (componentNameLayoutStack.isNotEmpty()) {
        msg.append("  layout_stack: ")
        for (i in componentNameLayoutStack.indices.reversed()) {
          msg.append(componentNameLayoutStack[i])
          if (i != 0) {
            msg.append(" -> ")
          }
        }
        msg.appendLine()
      }
      if (componentContext?.logTag != null) {
        msg.append("  log_tag: ").appendLine(componentContext.logTag)
      } else if (componentTree?.logTag != null) {
        msg.append("  log_tag: ").appendLine(componentTree.logTag)
      }
      componentTree?.root?.let { root ->
        msg.append("  tree_root: <cls>").append(root.javaClass.name).appendLine("</cls>")
      }
      msg.append("  thread_name: ").appendLine(Thread.currentThread().name)
      msg.appendMap(customMetadata)
      return msg.toString().trim { it <= ' ' }
    }

  private val deepestCause: Throwable
    get() {
      var cause = checkNotNull(cause)
      while (true) {
        cause = cause.cause ?: break
      }
      return cause
    }

  companion object {
    const val LITHO_CONTEXT = "Litho Context:"

    private fun StringBuilder.appendMap(map: Map<String, String>) {
      for ((key, value) in map) {
        append("  ").append(key).append(": ").appendLine(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

import android.view.View
import com.facebook.rendercore.MountItem
import java.lang.IllegalArgumentException
import java.lang.RuntimeException
import kotlin.jvm.JvmField

/** This class hosts any extra mount data related to MountItem. */
class LithoMountData(content: Any?) {

  // Flags that track view-related behaviour of mounted view content.
  val defaultAttributeValuesFlags: Int = getViewAttributeFlags(content)

  @JvmField var isReleased = false

  @JvmField var releaseCause: String? = null

  companion object {
    private const val FLAG_VIEW_CLICKABLE = 1 shl 0
    private const val FLAG_VIEW_LONG_CLICKABLE = 1 shl 1
    private const val FLAG_VIEW_FOCUSABLE = 1 shl 2
    private const val FLAG_VIEW_ENABLED = 1 shl 3
    private const val FLAG_VIEW_SELECTED = 1 shl 4
    private const val FLAG_VIEW_LAYER_TYPE_0 = 1 shl 5
    private const val FLAG_VIEW_LAYER_TYPE_1 = 1 shl 6

    /** @return Whether the view associated with this MountItem is clickable. */
    @JvmStatic
    fun isViewClickable(flags: Int): Boolean = flags and FLAG_VIEW_CLICKABLE == FLAG_VIEW_CLICKABLE

    /** @return Whether the view associated with this MountItem is long clickable. */
    @JvmStatic
    fun isViewLongClickable(flags: Int): Boolean =
        flags and FLAG_VIEW_LONG_CLICKABLE == FLAG_VIEW_LONG_CLICKABLE

    /** @return Whether the view associated with this MountItem is setFocusable. */
    @JvmStatic
    fun isViewFocusable(flags: Int): Boolean = flags and FLAG_VIEW_FOCUSABLE == FLAG_VIEW_FOCUSABLE

    /** @return Whether the view associated with this MountItem is setEnabled. */
    @JvmStatic
    fun isViewEnabled(flags: Int): Boolean = flags and FLAG_VIEW_ENABLED == FLAG_VIEW_ENABLED

    /** @return Whether the view associated with this MountItem is setSelected. */
    @JvmStatic
    fun isViewSelected(flags: Int): Boolean = flags and FLAG_VIEW_SELECTED == FLAG_VIEW_SELECTED

    @JvmStatic
    @LayerType
    fun getOriginalLayerType(flags: Int): Int =
        if (flags and FLAG_VIEW_LAYER_TYPE_0 == 0) {
          LayerType.LAYER_TYPE_NOT_SET
        } else if (flags and FLAG_VIEW_LAYER_TYPE_1 == FLAG_VIEW_LAYER_TYPE_1) {
          LayerType.LAYER_TYPE_HARDWARE
        } else {
          LayerType.LAYER_TYPE_SOFTWARE
        }

    @JvmStatic
    fun getMountData(item: MountItem): LithoMountData {
      return item.mountData as? LithoMountData
          ?: throw RuntimeException("MountData should not be null when using Litho's MountState.")
    }

    @JvmStatic
    fun getViewAttributeFlags(content: Any?): Int {
      var flags = 0
      if (content is View) {
        if (content.isClickable) {
          flags = flags or FLAG_VIEW_CLICKABLE
        }
        if (content.isLongClickable) {
          flags = flags or FLAG_VIEW_LONG_CLICKABLE
        }
        if (content.isFocusable) {
          flags = flags or FLAG_VIEW_FOCUSABLE
        }
        if (content.isEnabled) {
          flags = flags or FLAG_VIEW_ENABLED
        }
        if (content.isSelected) {
          flags = flags or FLAG_VIEW_SELECTED
        }
        when (content.layerType) {
          View.LAYER_TYPE_NONE -> {}
          View.LAYER_TYPE_SOFTWARE -> flags = flags or FLAG_VIEW_LAYER_TYPE_0
          View.LAYER_TYPE_HARDWARE -> flags = flags or FLAG_VIEW_LAYER_TYPE_1
          else -> throw IllegalArgumentException("Unhandled layer type encountered.")
        }
      }
      return flags
    }
  }
}


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

class LithoRenderContext(@JvmField val layoutStateContext: LayoutStateContext)


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

package com.facebook.litho

import android.content.Context
import android.util.AttributeSet
import androidx.annotation.UiThread
import com.facebook.litho.config.ComponentsConfiguration

/**
 * An implementation of BaseMountingView that can mount a LayoutState without the need for a
 * ComponentTree
 */
class LithoRenderTreeView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
    BaseMountingView(context, attrs) {

  private var hasNewTree = false

  // Set together
  private var layoutState: LayoutState? = null
  private var treeState: TreeState? = null

  private val requireTreeState: TreeState
    get() {
      return requireNotNull(treeState) { "TreeState not available." }
    }

  private val requireLayoutState: LayoutState
    get() {
      return requireNotNull(layoutState) { "LayoutState not available." }
    }

  override fun getConfiguration(): ComponentsConfiguration? {
    return layoutState?.componentContext?.componentsConfiguration
  }

  override fun isIncrementalMountEnabled(): Boolean {
    val componentContext = layoutState?.componentContext ?: return false
    return ComponentContext.isIncrementalMountEnabled(componentContext)
  }

  public override fun getCurrentLayoutState(): LayoutState? = layoutState

  override fun isVisibilityProcessingEnabled(): Boolean {
    val componentContext = layoutState?.componentContext ?: return false
    return ComponentContext.isVisibilityProcessingEnabled(componentContext)
  }

  override fun getTreeState(): TreeState? = treeState

  override fun hasTree(): Boolean = layoutState != null

  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // mAnimatedWidth/mAnimatedHeight >= 0 if something is driving a width/height animation.
    val isAnimating = mAnimatedWidth != -1 || mAnimatedHeight != -1
    // up to date view sizes, taking into account running animations
    val upToDateWidth = if (mAnimatedWidth != -1) mAnimatedWidth else width
    val upToDateHeight = if (mAnimatedHeight != -1) mAnimatedHeight else height
    mAnimatedWidth = -1
    mAnimatedHeight = -1
    if (isAnimating) {
      // If the mount state is dirty, we want to ignore the current animation and calculate the
      // new LayoutState as normal below. That LayoutState has the opportunity to define its own
      // transition to a new width/height from the current height of the LithoRenderTreeView, or if
      // not we will jump straight to that width/height.
      if (!isMountStateDirty) {
        setMeasuredDimension(upToDateWidth, upToDateHeight)
        return
      }
    }
    if (layoutState == null) {
      setMeasuredDimension(
          if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY)
              MeasureSpec.getSize(widthMeasureSpec)
          else 0,
          if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY)
              MeasureSpec.getSize(heightMeasureSpec)
          else 0)
      return
    }
    val hasMounted = requireTreeState.mountInfo.mHasMounted

    val canAnimateRootBounds = !hasNewTree || !hasMounted
    var width = requireLayoutState.width
    var height = requireLayoutState.height
    if (canAnimateRootBounds) {
      // We might need to collect transitions before mount to know whether this LithoRenderTreeView
      // width or height is animated.
      maybeCollectAllTransitions()
      val initialAnimatedWidth = getInitialAnimatedMountingViewWidth(upToDateWidth, hasNewTree)
      if (initialAnimatedWidth != -1) {
        width = initialAnimatedWidth
      }
      val initialAnimatedHeight = getInitialAnimatedMountingViewHeight(upToDateHeight, hasNewTree)
      if (initialAnimatedHeight != -1) {
        height = initialAnimatedHeight
      }
    }
    setMeasuredDimension(width, height)
    hasNewTree = false
  }

  @UiThread
  fun setLayoutState(layoutState: LayoutState, treeState: TreeState) {
    ThreadUtils.assertMainThread()
    if (this.layoutState === layoutState) {
      require(this.treeState === treeState)
      if (isAttached) {
        rebind()
      }
      return
    }

    hasNewTree =
        this.layoutState == null ||
            (requireLayoutState.componentTreeId != layoutState.componentTreeId)
    setMountStateDirty()
    if (this.layoutState != null) {
      onBeforeSettingNewTree()
    }
    this.layoutState = layoutState
    this.treeState = treeState

    if (this.layoutState == null) {
      clearDebugOverlay(this)
    }

    setupMountExtensions()
    requestLayout()
  }

  fun resetLayoutState() {
    if (this.layoutState != null) {
      hasNewTree = true
      unmountAllItems()
    }
    this.layoutState = null
    this.treeState = null
    requestLayout()
  }
}


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

package com.facebook.litho

import android.util.SparseArray
import com.facebook.litho.annotations.ImportantForAccessibility
import com.facebook.rendercore.MountItem
import com.facebook.rendercore.RenderTreeNode
import com.facebook.rendercore.RenderUnit
import com.facebook.rendercore.transitions.TransitionRenderUnit

abstract class LithoRenderUnit
protected constructor(
    private val _id: Long,
    val component: Component,
    @field:JvmField val commonDynamicProps: SparseArray<DynamicValue<Any?>>?,
    val nodeInfo: NodeInfo?,
    val flags: Int,
    importantForAccessibility: Int,
    renderType: RenderType?,
    @field:JvmField val componentContext: ComponentContext?,
) : RenderUnit<Any?>(renderType), TransitionRenderUnit {

  override fun getId(): Long = _id

  // TODO: remove
  var hierarchy: DebugHierarchy.Node? = null

  // the A11Y prop for descendants has been corrected
  val importantForAccessibility: Int =
      if (importantForAccessibility ==
          ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_YES_HIDE_DESCENDANTS) {
        ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_YES
      } else {
        importantForAccessibility
      }

  override fun getMatchHostBounds(): Boolean = flags and LAYOUT_FLAG_MATCH_HOST_BOUNDS != 0

  val contentDescription: CharSequence?
    get() = nodeInfo?.contentDescription

  val isAccessible: Boolean
    get() {
      if (importantForAccessibility == ImportantForAccessibility.IMPORTANT_FOR_ACCESSIBILITY_NO) {
        return false
      }

      return (this.nodeInfo != null && nodeInfo.needsAccessibilityDelegate()) ||
          (this.component is SpecGeneratedComponent && this.component.implementsAccessibility())
    }

  companion object {
    const val LAYOUT_FLAG_DUPLICATE_PARENT_STATE = 1 shl 0
    const val LAYOUT_FLAG_DISABLE_TOUCHABLE = 1 shl 1
    const val LAYOUT_FLAG_MATCH_HOST_BOUNDS = 1 shl 2
    const val LAYOUT_FLAG_DRAWABLE_OUTPUTS_DISABLED = 1 shl 3
    const val LAYOUT_FLAG_DUPLICATE_CHILDREN_STATES = 1 shl 4
    const val LAYOUT_FLAG_HAS_TOUCH_EVENT_HANDLERS = 1 shl 5

    @JvmStatic
    fun getComponentContext(item: MountItem): ComponentContext? =
        (item.renderTreeNode.renderUnit as LithoRenderUnit).componentContext

    @JvmStatic
    fun getComponentContext(node: RenderTreeNode): ComponentContext? =
        (node.renderUnit as LithoRenderUnit).componentContext

    @JvmStatic
    fun getComponentContext(unit: LithoRenderUnit): ComponentContext? = unit.componentContext

    @JvmStatic
    fun getRenderUnit(item: MountItem): LithoRenderUnit = getRenderUnit(item.renderTreeNode)

    @JvmStatic
    fun getRenderUnit(node: RenderTreeNode): LithoRenderUnit = node.renderUnit as LithoRenderUnit

    @JvmStatic
    fun isDuplicateParentState(flags: Int): Boolean =
        flags and LAYOUT_FLAG_DUPLICATE_PARENT_STATE == LAYOUT_FLAG_DUPLICATE_PARENT_STATE

    @JvmStatic
    fun hasTouchEventHandlers(flags: Int): Boolean =
        flags and LAYOUT_FLAG_HAS_TOUCH_EVENT_HANDLERS == LAYOUT_FLAG_HAS_TOUCH_EVENT_HANDLERS

    @JvmStatic
    fun isDuplicateChildrenStates(flags: Int): Boolean =
        flags and LAYOUT_FLAG_DUPLICATE_CHILDREN_STATES == LAYOUT_FLAG_DUPLICATE_CHILDREN_STATES

    @JvmStatic
    fun isTouchableDisabled(flags: Int): Boolean =
        flags and LAYOUT_FLAG_DISABLE_TOUCHABLE == LAYOUT_FLAG_DISABLE_TOUCHABLE

    @JvmStatic
    fun areDrawableOutputsDisabled(flags: Int): Boolean =
        flags and LAYOUT_FLAG_DRAWABLE_OUTPUTS_DISABLED != 0

    @JvmStatic
    fun isMountableView(unit: RenderUnit<*>): Boolean = unit.renderType == RenderType.VIEW
  }
}


/*
 * 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 java.util.HashSet

/**
 * Logger for tracking Litho events happening during startup.
 *
 * The implementations decide how to log points (via [LithoStartupLogger.markPoint]) and whether
 * this logger is enabled (via [LithoStartupLogger.isEnabled]).
 */
abstract class LithoStartupLogger {

  companion object {
    const val LITHO_PREFIX = "litho"
    const val CHANGESET_CALCULATION = "_changeset"
    const val FIRST_LAYOUT = "_firstlayout"
    const val FIRST_MOUNT = "_firstmount"
    const val LAST_MOUNT = "_lastmount"
    const val START = "_start"
    const val END = "_end"

    private val NEEDS_THREAD_INFO =
        HashSet<String>().apply {
          add(CHANGESET_CALCULATION)
          add(FIRST_LAYOUT)
        }

    @JvmStatic
    fun isEnabled(logger: LithoStartupLogger?): Boolean = logger != null && logger.isEnabled
  }

  /**
   * @return attribution to the rendered events like the network query name, data source
   *   (network/cache) etc.
   */
  var latestDataAttribution: String = ""
    private set

  private val processedEvents = HashSet<String>()
  private val startedEvents = HashSet<String>()

  private fun markPoint(name: String) {
    if (!processedEvents.contains(name)) {
      onMarkPoint(name)
      processedEvents.add(name)
    }
  }

  private fun getFullMarkerName(eventName: String, dataAttribution: String, stage: String): String =
      buildString {
        append(LITHO_PREFIX)
        if (NEEDS_THREAD_INFO.contains(eventName)) {
          append(if (ThreadUtils.isMainThread()) "_ui" else "_bg")
        }
        if (dataAttribution.isNotEmpty()) {
          append('_')
          append(dataAttribution)
        }
        append(eventName)
        append(stage)
      }

  /**
   * Set attribution to the rendered events like the network query name, data source (network/cache)
   * etc.
   */
  fun setDataAttribution(attribution: String) {
    latestDataAttribution = attribution
  }

  /** Mark the event with given name, stage (start/end), and given data attribution. */
  /**
   * Mark the event with given name and stage (start/end). It will use currently assigned data
   * attribution.
   */
  @JvmOverloads
  fun markPoint(eventName: String, stage: String, dataAttribution: String = latestDataAttribution) {
    if (stage == START) {
      startedEvents.add(getFullMarkerName(eventName, dataAttribution, ""))
    } else if (stage == END &&
        !startedEvents.remove(getFullMarkerName(eventName, dataAttribution, ""))) {
      // no matching start point, skip (can happen for changeset end)
      return
    }
    markPoint(getFullMarkerName(eventName, dataAttribution, stage))
  }

  /** Callback to log the event point. */
  protected abstract fun onMarkPoint(name: String)

  /** @return whether this logger is active. */
  protected abstract val isEnabled: Boolean
}


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

package com.facebook.litho

import android.graphics.Rect
import android.view.View

fun interface LithoTooltip {
  /**
   * **Do not call this method directly!** It should only be called by the Litho framework to ensure
   * that the correct parameters are passed. Use the methods exposed by [LithoTooltipController]
   * instead.
   *
   * Display the tooltip on an anchor component with the given bounds relative to the view that
   * contains it.
   *
   * [container] is the view that wraps the Component used as anchor, while [anchorBounds] specifies
   * the bounds of the component relative to the host view. [xOffset] and [yOffset] moves the
   * tooltip horizontally and vertically, respectively, from the default position
   */
  fun showLithoTooltip(container: View?, anchorBounds: Rect, xOffset: Int, yOffset: Int)
}


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

package com.facebook.litho

import com.facebook.infer.annotation.ThreadConfined
import java.util.concurrent.atomic.AtomicReference
import kotlin.jvm.JvmField

/** Represents a pointer to the Tree that a ComponentContext is attached to */
class LithoTree
private constructor(
    @field:ThreadConfined(ThreadConfined.ANY) val stateUpdater: StateUpdater,
    @field:ThreadConfined(ThreadConfined.UI) val mountedViewReference: MountedViewReference,
    val errorComponentReceiver: ErrorComponentReceiver,
    val lithoTreeLifecycleProvider: LithoTreeLifecycleProvider,
    val id: Int
) {

  // Used to lazily store a CoroutineScope, if coroutine helper methods are used.
  @JvmField val internalScopeRef: AtomicReference<Any> = AtomicReference<Any>()

  companion object {

    fun create(componentTree: ComponentTree): LithoTree =
        LithoTree(componentTree, componentTree, componentTree, componentTree, componentTree.mId)
  }
}


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

interface LithoTreeLifecycleProvider {

  fun interface OnReleaseListener {
    /** Called when this ComponentTree is released. */
    fun onReleased()
  }

  val isReleased: Boolean

  fun addOnReleaseListener(onReleaseListener: OnReleaseListener)
}


/*
 * 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 javax.annotation.CheckReturnValue

/**
 * This class provides utilities for extracting information through
 * [ComponentsLogger#getExtraAnnotations(TreeProps)] and transforming them so they can be logged.
 */
object LogTreePopulator {
  /**
   * Annotate a log event with the log tag set in the context, and extract the [TreeProps] from a
   * given [ComponentContext] and convert them into perf event annotations using a
   * [ComponentsLogger] implementation.
   *
   * @return Annotated perf event, or `null` if the resulting event isn't deemed worthy of
   *   reporting.
   */
  @CheckReturnValue
  @JvmStatic
  fun populatePerfEventFromLogger(
      c: ComponentContext,
      logger: ComponentsLogger,
      perfEvent: PerfEvent?
  ): PerfEvent? {
    return populatePerfEventFromLogger(logger, c.logTag, perfEvent, c.treeProps)
  }

  /**
   * Annotate a log event with the log tag set in the context, and extract the [TreeProps] from a
   * given [ComponentContext] and convert them into perf event annotations using a
   * [ComponentsLogger] implementation.
   *
   * @return Annotated perf event, or `null` if the resulting event isn't deemed worthy of
   *   reporting.
   */
  @CheckReturnValue
  @JvmStatic
  fun populatePerfEventFromLogger(
      c: ComponentContext,
      logger: ComponentsLogger,
      logTag: String?,
      perfEvent: PerfEvent?
  ): PerfEvent? {
    return populatePerfEventFromLogger(logger, logTag, perfEvent, c.treeProps)
  }

  /**
   * Annotate a log event with the log tag set in the context, and convert the [TreeProps] into perf
   * event annotations using a [ComponentsLogger] implementation.
   *
   * @return Annotated perf event, or `null` if the resulting event isn't deemed worthy of
   *   reporting.
   */
  @CheckReturnValue
  private fun populatePerfEventFromLogger(
      logger: ComponentsLogger,
      logTag: String?,
      perfEvent: PerfEvent?,
      treeProps: TreeProps?
  ): PerfEvent? {
    if (perfEvent == null) {
      return null
    }
    if (logTag == null) {
      logger.cancelPerfEvent(perfEvent)
      return null
    }
    perfEvent.markerAnnotate(FrameworkLogEvents.PARAM_LOG_TAG, logTag)
    if (treeProps == null) {
      return perfEvent
    }

    val extraAnnotations = logger.getExtraAnnotations { key -> treeProps[key] } ?: return perfEvent
    for ((key, value) in extraAnnotations) {
      perfEvent.markerAnnotate(key, value)
    }
    return perfEvent
  }
}


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

package com.facebook.litho

import android.content.res.Configuration
import androidx.collection.LruCache

open class LruResourceCache(configuration: Configuration) : ResourceCache(configuration) {

  private val cache =
      object : LruCache<Int?, Any?>(500) {

        override fun sizeOf(key: Int, value: Any): Int = if (value is String) value.length else 1
      }

  override fun <T> get(key: Int): T? = cache[key] as T?

  override fun set(key: Int, value: Any) {
    cache.put(key, 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

import android.annotation.TargetApi
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Region
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.RippleDrawable
import android.os.Build
import android.view.MotionEvent
import android.view.View
import com.facebook.rendercore.transitions.TransitionUtils.BoundsCallback
import kotlin.math.roundToInt

/** A Drawable that wraps another drawable. */
class MatrixDrawable<T : Drawable?> : Drawable(), Drawable.Callback, Touchable, BoundsCallback {

  var mountedDrawable: T? = null
    private set

  private var matrix: DrawableMatrix? = null
  private var shouldClipRect = false
  private var width = 0
  private var height = 0

  /**
   * Sets the necessary artifacts to display the given [drawable]. This method should be called in
   * your component's @OnMount method.
   */
  @JvmOverloads
  fun mount(drawable: T?, matrix: DrawableMatrix? = null) {
    if (mountedDrawable === drawable) {
      return
    }
    mountedDrawable?.let {
      setDrawableVisibilitySafe(visible = false, restart = false)
      it.callback = null
    }
    mountedDrawable =
        drawable?.also {
          setDrawableVisibilitySafe(isVisible, restart = false)
          it.callback = this
        }
    this.matrix = matrix

    // We should clip rect if either the transformation matrix needs so or
    // if a ColorDrawable in Gingerbread is being drawn because it doesn't
    // respect its bounds.
    shouldClipRect = (this.matrix?.shouldClipRect() == true || mountedDrawable is InsetDrawable)
    invalidateSelf()
  }

  /**
   * Applies the given dimensions to the drawable. This method should be called in your
   * component's @OnBind method.
   *
   * @param width The width of the drawable to be drawn.
   * @param height The height of the drawable to be drawn.
   */
  fun bind(width: Int, height: Int) {
    this.width = width
    this.height = height
    setInnerDrawableBounds(this.width, this.height)
  }

  private fun setInnerDrawableBounds(width: Int, height: Int) {
    mountedDrawable?.setBounds(0, 0, width, height)
  }

  fun unmount() {
    mountedDrawable?.let {
      setDrawableVisibilitySafe(visible = false, restart = false)
      it.callback = null
    }
    mountedDrawable = null
    matrix = null
    shouldClipRect = false
    height = 0
    width = 0
  }

  fun getVisualBounds(): Rect {
    val untranslatedBounds = copyBounds()
    val matrix = matrix ?: return untranslatedBounds
    val translatedRectF = RectF(untranslatedBounds)
    matrix.mapRect(translatedRectF)
    return Rect(
        translatedRectF.left.roundToInt(),
        translatedRectF.top.roundToInt(),
        translatedRectF.right.roundToInt(),
        translatedRectF.bottom.roundToInt())
  }

  private fun setDrawableVisibilitySafe(visible: Boolean, restart: Boolean) {
    val drawable = mountedDrawable ?: return
    if (drawable.isVisible != visible) {
      try {
        drawable.setVisible(visible, restart)
      } catch (e: NullPointerException) {
        // Swallow. LayerDrawable on KitKat sometimes causes this, if some of its children are null.
        // This should not cause any rendering bugs, since visibility is anyway a "hint".
      }
    }
  }

  override fun draw(canvas: Canvas) {
    val drawable = mountedDrawable ?: return
    val bounds = bounds
    val saveCount = canvas.save()
    canvas.translate(bounds.left.toFloat(), bounds.top.toFloat())
    if (shouldClipRect) {
      canvas.clipRect(0, 0, bounds.width(), bounds.height())
    }
    if (matrix != null) {
      canvas.concat(matrix)
    }
    drawable.draw(canvas)
    canvas.restoreToCount(saveCount)
  }

  override fun setChangingConfigurations(configs: Int) {
    mountedDrawable?.changingConfigurations = configs
  }

  override fun getChangingConfigurations(): Int = mountedDrawable?.changingConfigurations ?: UNSET

  override fun setDither(dither: Boolean) {
    mountedDrawable?.setDither(dither)
  }

  override fun setFilterBitmap(filter: Boolean) {
    mountedDrawable?.isFilterBitmap = filter
  }

  override fun setAlpha(alpha: Int) {
    mountedDrawable?.alpha = alpha
  }

  override fun setColorFilter(cf: ColorFilter?) {
    mountedDrawable?.colorFilter = cf
  }

  override fun isStateful(): Boolean = mountedDrawable?.isStateful == true

  override fun setState(stateSet: IntArray): Boolean = mountedDrawable?.setState(stateSet) == true

  override fun getState(): IntArray = mountedDrawable?.state ?: intArrayOf()

  override fun getCurrent(): Drawable = requireNotNull(mountedDrawable).current

  override fun setVisible(visible: Boolean, restart: Boolean): Boolean {
    val changed = super.setVisible(visible, restart)
    setDrawableVisibilitySafe(visible, restart)
    return changed
  }

  override fun getOpacity(): Int = mountedDrawable?.opacity ?: UNSET

  override fun getTransparentRegion(): Region? = mountedDrawable?.transparentRegion

  override fun getIntrinsicWidth(): Int = mountedDrawable?.intrinsicWidth ?: UNSET

  override fun getIntrinsicHeight(): Int = mountedDrawable?.intrinsicHeight ?: UNSET

  override fun getMinimumWidth(): Int = mountedDrawable?.minimumWidth ?: UNSET

  override fun getMinimumHeight(): Int = mountedDrawable?.minimumHeight ?: UNSET

  override fun getPadding(padding: Rect): Boolean = mountedDrawable?.getPadding(padding) == true

  override fun onLevelChange(level: Int): Boolean = mountedDrawable?.setLevel(level) == true

  override fun invalidateDrawable(who: Drawable) = invalidateSelf()

  override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) =
      scheduleSelf(what, `when`)

  override fun unscheduleDrawable(who: Drawable, what: Runnable) {
    unscheduleSelf(what)
  }

  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  override fun onTouchEvent(event: MotionEvent, host: View): Boolean {
    val bounds = bounds
    val x = event.x.toInt() - bounds.left
    val y = event.y.toInt() - bounds.top
    requireNotNull(mountedDrawable).setHotspot(x.toFloat(), y.toFloat())
    return false
  }

  override fun shouldHandleTouchEvent(event: MotionEvent): Boolean =
      Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
          mountedDrawable is RippleDrawable &&
          event.actionMasked == MotionEvent.ACTION_DOWN &&
          bounds.contains(event.x.toInt(), event.y.toInt())

  override fun onWidthHeightBoundsApplied(width: Int, height: Int) {
    bind(width, height)
  }

  override fun onXYBoundsApplied(x: Int, y: Int) = Unit

  companion object {
    const val UNSET = -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

/**
 * An utility class to verify that an old measured size is still compatible to be used with a new
 * measureSpec.
 */
object MeasureComparisonUtils {
  /**
   * Check if a cached nested tree has compatible SizeSpec to be reused as is or if it needs to be
   * recomputed.
   *
   * The conditions to be able to re-use previous measurements are: 1) The measureSpec is the
   * same 2) The new measureSpec is EXACTLY and the last measured size matches the measureSpec size.
   * 3) The old measureSpec is UNSPECIFIED, the new one is AT_MOST and the old measured size is
   *    smaller that the maximum size the new measureSpec will allow. 4) Both measure specs are
   *    AT_MOST. The old measure spec allows a bigger size than the new and the old measured size is
   *    smaller than the allowed max size for the new sizeSpec.
   */
  @JvmStatic
  fun hasCompatibleSizeSpec(
      oldWidthSpec: Int,
      oldHeightSpec: Int,
      newWidthSpec: Int,
      newHeightSpec: Int,
      oldMeasuredWidth: Float,
      oldMeasuredHeight: Float
  ): Boolean {
    val widthIsCompatible =
        isMeasureSpecCompatible(oldWidthSpec, newWidthSpec, oldMeasuredWidth.toInt())
    val heightIsCompatible =
        isMeasureSpecCompatible(oldHeightSpec, newHeightSpec, oldMeasuredHeight.toInt())
    return widthIsCompatible && heightIsCompatible
  }

  private fun newSizeIsExactAndMatchesOldMeasuredSize(
      newSizeSpecMode: Int,
      newSizeSpecSize: Int,
      oldMeasuredSize: Int
  ): Boolean = newSizeSpecMode == SizeSpec.EXACTLY && newSizeSpecSize == oldMeasuredSize

  private fun oldSizeIsUnspecifiedAndStillFits(
      oldSizeSpecMode: Int,
      newSizeSpecMode: Int,
      newSizeSpecSize: Int,
      oldMeasuredSize: Int
  ): Boolean =
      newSizeSpecMode == SizeSpec.AT_MOST &&
          oldSizeSpecMode == SizeSpec.UNSPECIFIED &&
          newSizeSpecSize >= oldMeasuredSize

  private fun newMeasureSizeIsStricterAndStillValid(
      oldSizeSpecMode: Int,
      newSizeSpecMode: Int,
      oldSizeSpecSize: Int,
      newSizeSpecSize: Int,
      oldMeasuredSize: Int
  ): Boolean =
      oldSizeSpecMode == SizeSpec.AT_MOST &&
          newSizeSpecMode == SizeSpec.AT_MOST &&
          oldSizeSpecSize > newSizeSpecSize &&
          oldMeasuredSize <= newSizeSpecSize

  @JvmStatic
  fun areMeasureSpecsEquivalent(specA: Int, specB: Int): Boolean =
      specA == specB ||
          (SizeSpec.getMode(specA) == SizeSpec.UNSPECIFIED &&
              SizeSpec.getMode(specB) == SizeSpec.UNSPECIFIED)

  @JvmStatic
  fun isMeasureSpecCompatible(oldSizeSpec: Int, sizeSpec: Int, oldMeasuredSize: Int): Boolean {
    val newSpecMode = SizeSpec.getMode(sizeSpec)
    val newSpecSize = SizeSpec.getSize(sizeSpec)
    val oldSpecMode = SizeSpec.getMode(oldSizeSpec)
    val oldSpecSize = SizeSpec.getSize(oldSizeSpec)
    return oldSizeSpec == sizeSpec ||
        (oldSpecMode == SizeSpec.UNSPECIFIED && newSpecMode == SizeSpec.UNSPECIFIED) ||
        newSizeIsExactAndMatchesOldMeasuredSize(newSpecMode, newSpecSize, oldMeasuredSize) ||
        oldSizeIsUnspecifiedAndStillFits(oldSpecMode, newSpecMode, newSpecSize, oldMeasuredSize) ||
        newMeasureSizeIsStricterAndStillValid(
            oldSpecMode, newSpecMode, oldSpecSize, newSpecSize, oldMeasuredSize)
  }
}