/*
 * 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.codelab.textinput

import android.content.Context
import androidx.annotation.UiThread
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentLayout
import com.facebook.litho.Size
import com.facebook.litho.annotations.MountSpec
import com.facebook.litho.annotations.OnCreateMountContent
import com.facebook.litho.annotations.OnMeasure
import com.facebook.litho.annotations.OnMount
import com.facebook.litho.annotations.Prop

@MountSpec(isPureRender = true)
object ClockSpec {

  @OnMeasure
  fun onMeasure(
      context: ComponentContext,
      layout: ComponentLayout,
      widthSpec: Int,
      heightSpec: Int,
      size: Size,
      @Prop radius: Int
  ) {
    size.height = radius * 2
    size.width = radius * 2
  }

  @UiThread
  @OnCreateMountContent
  fun onCreateMountContent(c: Context): ClockDrawable {
    return ClockDrawable()
  }

  @UiThread
  @OnMount
  fun onMount(
      context: ComponentContext,
      mountedDrawable: ClockDrawable,
      @Prop radius: Int,
      @Prop timeMillis: Long
  ) {
    mountedDrawable.radius = radius
    mountedDrawable.setTime(timeMillis)
  }
}


/*
 * 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.codelab.textinput

import android.app.Application
import com.facebook.soloader.SoLoader

class LithoApp : Application() {

  override fun onCreate() {
    super.onCreate()

    SoLoader.init(this, 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.codelab.textinput

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.facebook.litho.ComponentContext
import com.facebook.litho.LithoView

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val componentContext = ComponentContext(this)
    setContentView(LithoView.create(this, RootComponent.create(componentContext).build()))
  }
}


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

package com.facebook.litho.codelab.textinput

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout

@LayoutSpec
object RootComponentSpec {
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext): Component {
    return TimeConverter.create(c).textInputKey("text_input").build()
  }
}


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

package com.facebook.litho.codelab.textinput

import android.text.InputType
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Row
import com.facebook.litho.VisibleEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.TextInput
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaJustify
import java.util.Calendar
import java.util.TimeZone

@Suppress("MagicNumber")
@LayoutSpec
object TimeConverterSpec {

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop textInputKey: String): Component {
    val calendar = Calendar.getInstance(TimeZone.getTimeZone("Europe/London"))

    val londonTimeStr = timeStr(calendar)

    val londonTime = timeMillis(calendar)

    calendar.setTimeZone(TimeZone.getTimeZone("America/New_York"))

    val newYorkTime = timeMillis(calendar)

    calendar.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"))

    val sanFranciscoTime = timeMillis(calendar)

    return Row.create(c)
        .child(
            Column.create(c)
                .child(
                    Clock.create(c).timeMillis(londonTime).radius(210).marginDip(YogaEdge.ALL, 24f))
                .child(
                    Clock.create(c)
                        .timeMillis(newYorkTime)
                        .radius(210)
                        .marginDip(YogaEdge.ALL, 24f))
                .child(
                    Clock.create(c)
                        .timeMillis(sanFranciscoTime)
                        .radius(210)
                        .marginDip(YogaEdge.ALL, 24f))
                .justifyContent(YogaJustify.SPACE_AROUND))
        .child(
            Column.create(c)
                .child(
                    Column.create(c)
                        .child(Text.create(c).text("London").textSizeDip(24f))
                        .child(
                            TextInput.create(c)
                                .key(textInputKey)
                                .inputType(InputType.TYPE_CLASS_DATETIME)
                                .textSizeDip(24f)
                                .initialText(londonTimeStr)
                                .visibleHandler(TimeConverter.onVisibleEvent(c))))
                .child(Text.create(c).text("New York").textSizeDip(24f))
                .child(Text.create(c).text("San Francisco").textSizeDip(24f))
                .justifyContent(YogaJustify.SPACE_AROUND))
        .build()
  }

  private fun timeMillis(calendar: Calendar): Long {
    var timeOfDay: Long = 0
    calendar.apply {
      timeOfDay += get(Calendar.HOUR)
      timeOfDay *= 60
      timeOfDay += get(Calendar.MINUTE)
      timeOfDay *= 60
      timeOfDay += get(Calendar.SECOND)
      timeOfDay *= 1000
      timeOfDay += get(Calendar.MILLISECOND)
    }
    return timeOfDay
  }

  @OnEvent(VisibleEvent::class)
  fun onVisibleEvent(c: ComponentContext, @Prop textInputKey: String) {
    TextInput.requestFocus(c, textInputKey)
  }

  private fun timeStr(calendar: Calendar) =
      "${calendar.get(Calendar.HOUR_OF_DAY)} : ${calendar.get(Calendar.MINUTE)}"
}


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

import android.app.Application
import com.facebook.soloader.SoLoader

class LithoApp : Application() {

  override fun onCreate() {
    super.onCreate()

    SoLoader.init(this, 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.codelab

import android.os.Bundle
import android.os.CountDownTimer
import androidx.appcompat.app.AppCompatActivity
import com.facebook.litho.ComponentContext
import com.facebook.litho.LithoView

@Suppress("MagicNumber")
class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val componentContext = ComponentContext(this)
    val lithoView =
        LithoView.create(
            this, RootComponent.create(componentContext).labelText("Starting countdown").build())
    setContentView(lithoView)

    val timer =
        object : CountDownTimer(30000, 1000) {
          override fun onTick(millisUntilFinished: Long) {
            lithoView.setComponentAsync(
                RootComponent.create(componentContext)
                    .labelText("Ms until finished: " + millisUntilFinished.toString())
                    .build())
          }

          override fun onFinish() {
            lithoView.setComponentAsync(
                RootComponent.create(componentContext).labelText("Done!").build())
          }
        }
    timer.start()
  }
}


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

import com.facebook.litho.ClickEvent
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Row
import com.facebook.litho.StateValue
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateInitialState
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.OnUpdateState
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.State
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaEdge

@Suppress("MagicNumber")
@LayoutSpec
object RootComponentSpec {

  @OnCreateInitialState
  fun onCreateInitialState(c: ComponentContext, toggleState: StateValue<Boolean>) {
    toggleState.set(true)
  }

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop labelText: String,
      @State toggleState: Boolean
  ): Component {
    return Column.create(c)
        .child(Text.create(c).textSizeSp(20f).text(labelText))
        .child(
            Row.create(c)
                .child(
                    Text.create(c)
                        .textSizeSp(20f)
                        .text("Toggle state: ")
                        .marginPx(YogaEdge.RIGHT, 30)
                        .clickHandler(RootComponent.onClick(c)))
                .child(Text.create(c).textSizeSp(20f).text(toggleState.toString())))
        .build()
  }

  @OnEvent(ClickEvent::class)
  fun onClick(c: ComponentContext) {
    RootComponent.updateToggle(c)
  }

  @OnUpdateState
  fun updateToggle(toggleState: StateValue<Boolean>) {
    val toggleStateVal: Boolean? = toggleState.get()
    if (toggleStateVal == true) {
      toggleState.set(false)
    } else {
      toggleState.set(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 androidx.annotation.StringDef

/**
 * A list of all roles used for accessibility. There is no API for these in Android yet, so roles
 * are currently based off of the class being rendered. These roles are defined by Google's TalkBack
 * screen reader, and this list should be kept up to date with their implementation.
 *
 * @see
 *   [https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java](https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java)
 */
object AccessibilityRole {

  const val NONE = ""
  const val BUTTON = "android.widget.Button"
  const val CHECK_BOX = "android.widget.CompoundButton"
  const val DROP_DOWN_LIST = "android.widget.Spinner"
  const val EDIT_TEXT = "android.widget.EditText"
  const val GRID = "android.widget.GridView"
  const val IMAGE = "android.widget.ImageView"
  const val IMAGE_BUTTON = "android.widget.ImageView"
  const val LIST = "android.widget.AbsListView"
  const val PAGER = "androidx.viewpager.widget.ViewPager"
  const val RADIO_BUTTON = "android.widget.RadioButton"
  const val SEEK_CONTROL = "android.widget.SeekBar"
  const val SWITCH = "android.widget.Switch"
  const val TAB_BAR = "android.widget.TabWidget"
  const val TOGGLE_BUTTON = "android.widget.ToggleButton"
  const val VIEW_GROUP = "android.view.ViewGroup"
  const val WEB_VIEW = "android.webkit.WebView"
  const val CHECKED_TEXT_VIEW = "android.widget.CheckedTextView"
  const val PROGRESS_BAR = "android.widget.ProgressBar"
  const val ACTION_BAR_TAB = "android.app.ActionBar\$Tab"
  const val DRAWER_LAYOUT = "androidx.drawerlayout.widget.DrawerLayout"
  const val SLIDING_DRAWER = "android.widget.SlidingDrawer"
  const val ICON_MENU = "com.android.internal.view.menu.IconMenuView"
  const val TOAST = "android.widget.Toast\$TN"
  const val DATE_PICKER_DIALOG = "android.app.DatePickerDialog"
  const val TIME_PICKER_DIALOG = "android.app.TimePickerDialog"
  const val DATE_PICKER = "android.widget.DatePicker"
  const val TIME_PICKER = "android.widget.TimePicker"
  const val NUMBER_PICKER = "android.widget.NumberPicker"
  const val SCROLL_VIEW = "android.widget.ScrollView"
  const val HORIZONTAL_SCROLL_VIEW = "android.widget.HorizontalScrollView"
  const val KEYBOARD_KEY = "android.inputmethodservice.Keyboard\$Key"

  @Retention(AnnotationRetention.SOURCE)
  @StringDef(
      NONE,
      BUTTON,
      CHECK_BOX,
      DROP_DOWN_LIST,
      EDIT_TEXT,
      GRID,
      IMAGE,
      IMAGE_BUTTON,
      LIST,
      PAGER,
      RADIO_BUTTON,
      SEEK_CONTROL,
      SWITCH,
      TAB_BAR,
      TOGGLE_BUTTON,
      VIEW_GROUP,
      WEB_VIEW,
      CHECKED_TEXT_VIEW,
      PROGRESS_BAR,
      ACTION_BAR_TAB,
      DRAWER_LAYOUT,
      SLIDING_DRAWER,
      ICON_MENU,
      TOAST,
      DATE_PICKER_DIALOG,
      TIME_PICKER_DIALOG,
      DATE_PICKER,
      TIME_PICKER,
      NUMBER_PICKER,
      SCROLL_VIEW,
      HORIZONTAL_SCROLL_VIEW,
      KEYBOARD_KEY)
  annotation class AccessibilityRoleType
}


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

/**
 * This type of key is used to represent an attribute that reflects an Action (such as click, long
 * click, etc).
 */
class ActionAttributeKey<T>(description: String) : AttributeKey<T>(description)


/*
 * 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.provider.Settings
import android.util.Log
import com.facebook.litho.config.ComponentsConfiguration
import kotlin.jvm.JvmField

/** Utilities for animations debug. */
object AnimationsDebug {
  @JvmField val ENABLED = ComponentsConfiguration.isEndToEndTestRun
  const val TAG = "LithoAnimationDebug"

  @JvmStatic
  fun debugPrintLayoutState(layoutState: LayoutState) {
    if (!ENABLED) {
      return
    }
    Log.d(TAG, layoutState.dumpAsString())
  }

  @JvmStatic
  fun areTransitionsEnabled(context: Context?): Boolean {
    if (ComponentsConfiguration.isAnimationDisabled) {
      // mostly for unit tests
      return false
    }
    if (!ComponentsConfiguration.isEndToEndTestRun) {
      return true
    }
    if (!ComponentsConfiguration.CAN_CHECK_GLOBAL_ANIMATOR_SETTINGS) {
      return false
    }
    if (context == null) {
      return false
    }
    val animatorDurationScale =
        Settings.Global.getFloat(
            context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
    return (ComponentsConfiguration.forceEnableTransitionsForInstrumentationTests ||
        animatorDurationScale != 0f)
  }
}


/*
 * 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.lifecycle.LifecycleOwner

/**
 * This abstraction allows us to retrieve a [LifecycleOwner]. This class is meant to be used to
 * identify the closest [LifecycleOwner] and associate it to the [ComponentTree].
 *
 * If there is no knowledge about the current [LifecycleOwner] it will return `null`
 */
interface AOSPLifecycleOwnerProvider {
  val lifecycleOwner: LifecycleOwner?
}


/*
 * 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 something (e.g. a Component or a useEffect registration) which should receive
 * callbacks when it is attached/detached from the tree.
 */
interface Attachable {

  /** @return an id that can uniquely identify an Attachable between different layouts */
  val uniqueId: String

  /**
   * Will be invoked when this Attachable becomes a committed part of the ComponentTree. E.g. for a
   * Component, when that Component first appears as part of a committed layout.
   */
  fun attach()

  /**
   * Will be invoked when this Attachable is no longer part of the ComponentTree. This can be
   * because the ComponentTree is released (and all Attachables are detached), or because this
   * Attachable no longer appears in the ComponentTree in the most recent committed layout.
   */
  fun detach()

  /**
   * Given an Attachable (nextAttachable) with the same unique id as this Attachable and which is in
   * the next layout, return whether it should be attached instead of the current Attachable. If
   * true is returned, this Attachable will be detached and nextAttachable will be attached. If
   * false is returned, this Attachable will remain attached and nextAttachable will be thrown away.
   */
  fun shouldUpdate(nextAttachable: Attachable): Boolean

  /**
   * Legacy update behavior is that if [shouldUpdate(Attachable)] returns false, we should still
   * replace the Attachable with the latest version of the Attachable. Practically speaking, this is
   * needed for Layout Specs and the OnAttached/OnDetached API where OnAttached can be called with
   * the initial component props, but OnDetached is expected to be called with the latest Component
   * props.
   */
  fun useLegacyUpdateBehavior(): 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 androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting
import com.facebook.infer.annotation.ThreadSafe
import com.facebook.litho.annotations.OnAttached
import com.facebook.litho.annotations.OnDetached
import java.util.LinkedHashMap

/**
 * Manages dispatching attach/detach events to a set of [Attachable].
 *
 * For Spec Components, this will invoke [OnAttached] when a component is attached to the
 * [ComponentTree] and [OnDetached] when a component is detached from the tree.
 *
 * For Kotlin components, this will handle dispatching callbacks registered with the `useEffect`
 * hook.
 */
@ThreadSafe
class AttachDetachHandler {

  private var _attached: MutableMap<String, Attachable>? = null

  @get:VisibleForTesting
  val attached: Map<String, Attachable>?
    get() = _attached

  /**
   * Marks the given Attachables as attached, invoking attach if they weren't already attached. Any
   * Attachables that were attached and are no longer attached will be detached. Note that identity
   * is determined by [Attachable#getUniqueId()].
   */
  @UiThread
  fun onAttached(attachables: List<Attachable>?) {
    ThreadUtils.assertMainThread()
    if (_attached == null && attachables == null) {
      return
    }

    // 1. if there is nothing to attach, then we must detach all current attachables
    if (attachables == null) {
      _attached?.let { detachAll(it) }
      _attached = null
      return
    }

    val newAttachablesMap = LinkedHashMap<String, Attachable>()
    attachables.associateByTo(newAttachablesMap) { it.uniqueId }

    // 2. if we have no current attachables, we can simply attach all the new ones.
    if (_attached.isNullOrEmpty()) {
      attachAll(newAttachablesMap)
      _attached = newAttachablesMap
      return
    }

    // 3.1 detach attachables no longer present
    val currentAttached = _attached ?: return

    for ((id, attachable) in currentAttached) {
      if (id !in newAttachablesMap) {
        attachable.detach()
      }
    }

    // 3.2. attach or update attachables
    for (entry in newAttachablesMap) {
      val (id, newAttachable) = entry
      val existing = currentAttached[id]

      when {
        existing == null -> newAttachable.attach()
        existing.shouldUpdate(newAttachable) -> {
          existing.detach()
          newAttachable.attach()
        }
        !existing.useLegacyUpdateBehavior() -> {
          // If the attachable already exists and it doesn't need to update, make sure to use the
          // existing one.
          entry.setValue(existing)
        }
      }
    }

    // 3. update the map reference
    _attached = newAttachablesMap
  }

  /** Detaches all Attachables currently attached. */
  @UiThread
  fun onDetached() {
    ThreadUtils.assertMainThread()
    _attached?.let { detachAll(it) }
    _attached = null
  }

  private fun attachAll(toAttach: Map<String, Attachable>) {
    for (entry in toAttach.values) {
      entry.attach()
    }
  }

  private fun detachAll(toDetach: Map<String, Attachable>) {
    for (entry in toDetach.values) {
      entry.detach()
    }
  }
}


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

package com.facebook.litho

/**
 * This abstraction is used as an identifier for a given attribute of type T. Attributes are used
 * for testing purposes, allowing the clients to filter and assert on Component according to them.
 */
open class AttributeKey<T>(val description: String)


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

package com.facebook.litho

/**
 * Defines an abstraction that knows how to store attributes.
 *
 * This is used so that the clients can only set attributes and not read them.
 *
 * Reading attributes is not allowed for the clients, as only the testing APIs should use it.
 */
interface AttributesAcceptor {
  fun <T> setDebugAttributeKey(attributeKey: AttributeKey<T>, value: 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

import java.util.LinkedHashMap

/** Simple holder which allows to store and retrieve attributes. */
class AttributesHolder : AttributesAcceptor {

  private val _attributes: MutableMap<AttributeKey<*>, Any?> = LinkedHashMap()

  operator fun <T> get(key: AttributeKey<T>): T? = _attributes[key] as T?

  override fun <T> setDebugAttributeKey(attributeKey: AttributeKey<T>, value: T) {
    _attributes[attributeKey] = value
  }

  val attributes: Map<AttributeKey<*>, Any?>
    get() = _attributes
}


/*
 * 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 Batched State Update Strategy will define when do we schedule a final state update and
 * corresponding layout calculation, so that we can enqueue the most reasonable number of state
 * updates without incurring with an excessive number of layout calculations.
 */
interface BatchedStateUpdatesStrategy {

  /**
   * This method is called whenever an async state update was enqueued and added to the
   * [StateHandler].
   *
   * This method should return `true` if it is considering this update to its batching strategy;
   * otherwise it should simply return `false`.
   */
  fun onAsyncStateUpdateEnqueued(attribution: String?, isCreateLayoutInProgress: Boolean): Boolean

  /**
   * This method is called whenever [ComponentTree#updateStateInternal(boolean, String, boolean)] is
   * called.
   *
   * This will help the batching strategy on decide how to update its internal state by knowing that
   * all current enqueued updates will be consumed soon.
   */
  fun onInternalStateUpdateStart()

  /**
   * This method should be called whenever the resources held by this strategy should be freed or
   * reset.
   *
   * This can be pending `Runnable`, state variables, etc.
   */
  fun release()
}


/*
 * 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 implementation of working range that uses offset to define the upper bound and the lower
 * boundary. If the position is inside the boundary, it's in the range, otherwise it's out of the
 * range.
 */
class BoundaryWorkingRange @JvmOverloads constructor(private val offset: Int = OFFSET) :
    WorkingRange {

  override fun shouldEnterRange(
      position: Int,
      firstVisibleIndex: Int,
      lastVisibleIndex: Int,
      firstFullyVisibleIndex: Int,
      lastFullyVisibleIndex: Int
  ): Boolean = isInRange(position, firstVisibleIndex, lastVisibleIndex, offset)

  override fun shouldExitRange(
      position: Int,
      firstVisibleIndex: Int,
      lastVisibleIndex: Int,
      firstFullyVisibleIndex: Int,
      lastFullyVisibleIndex: Int
  ): Boolean = !isInRange(position, firstVisibleIndex, lastVisibleIndex, offset)

  companion object {
    private const val OFFSET = 1

    private fun isInRange(
        position: Int,
        firstVisiblePosition: Int,
        lastVisiblePosition: Int,
        offset: Int
    ): Boolean {
      val lowerBound = firstVisiblePosition - offset
      val upperBound = lastVisiblePosition + offset
      return position in lowerBound..upperBound
    }
  }
}


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

interface CalculationStateContext {

  val treeId: Int
  val cache: MeasuredResultCache
  val treeState: TreeState
  val layoutVersion: Int
  val rootComponentId: Int
  val isFutureReleased: Boolean
  val layoutStateFuture: TreeFuture<*>?
  val createdEventHandlers: List<Pair<String, EventHandler<*>>>?
  val isAccessibilityEnabled: Boolean

  /**
   * Records a Spec-generated EventHandler. This EventHandlers are an output of the calculation and
   * will be used by [EventHandlersController] to rebind existing EventHandlers.
   */
  fun recordEventHandler(globalKey: String, eventHandler: EventHandler<*>)
}


/*
 * 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.annotations.Prop
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaFlexDirection
import com.facebook.yoga.YogaJustify
import com.facebook.yoga.YogaWrap

/** A [Component] that renders its children in a column. */
class Column
private constructor(
    customSimpleName: String?,
    @field:Prop(optional = true) private var alignContent: YogaAlign?,
    @field:Prop(optional = true) private var alignItems: YogaAlign?,
    @field:Prop(optional = true) private var justifyContent: YogaJustify?,
    @field:Prop(optional = true) private var wrap: YogaWrap?,
    @field:Prop(optional = true) private var reverse: Boolean,
    @field:Prop(optional = true) private var children: MutableList<Component>?,
) : SpecGeneratedComponent(customSimpleName ?: "Column") {

  constructor(
      customSimpleName: String?
  ) : this(customSimpleName, null, null, null, null, false, null)

  @JvmOverloads
  constructor(
      alignContent: YogaAlign?,
      alignItems: YogaAlign?,
      justifyContent: YogaJustify?,
      wrap: YogaWrap?,
      reverse: Boolean,
      children: MutableList<Component>? = null
  ) : this(null, alignContent, alignItems, justifyContent, wrap, reverse, children)

  override fun canResolve(): Boolean = true

  public override fun resolve(
      resolveStateContext: ResolveStateContext,
      c: ComponentContext
  ): LithoNode? {
    val node = LithoNode()
    node.flexDirection(if (reverse) YogaFlexDirection.COLUMN_REVERSE else YogaFlexDirection.COLUMN)
    alignItems?.let { node.alignItems(it) }
    alignContent?.let { node.alignContent(it) }
    justifyContent?.let { node.justifyContent(it) }
    wrap?.let { node.wrap(it) }
    children?.let { children ->
      for (child in children) {
        if (resolveStateContext.isFutureReleased) {
          return null
        }
        if (resolveStateContext.isLayoutInterrupted) {
          node.appendUnresolvedComponent(child)
        } else {
          node.child(resolveStateContext, c, child)
        }
      }
    }
    return node
  }

  public override fun resolveWithResult(
      resolveStateContext: ResolveStateContext,
      c: ComponentContext
  ): ComponentResolveResult {
    val lithoNode = resolve(resolveStateContext, c)
    return ComponentResolveResult(lithoNode, commonProps)
  }

  override fun isEquivalentProps(other: Component?, shouldCompareCommonProps: Boolean): Boolean {
    if (this === other) {
      return true
    }
    if (other !is Column) {
      return false
    }
    if (this.id == other.id) {
      return true
    }
    val children = children
    val otherChildren = other.children
    if (children != null) {
      if (otherChildren == null || children.size != otherChildren.size) {
        return false
      }
      children.forEachIndexed { index, child ->
        if (!child.isEquivalentTo(otherChildren[index], shouldCompareCommonProps)) {
          return false
        }
      }
    } else if (otherChildren != null) {
      return false
    }
    if (alignItems != other.alignItems) {
      return false
    }
    if (alignContent != other.alignContent) {
      return false
    }
    if (justifyContent != other.justifyContent) {
      return false
    }
    return reverse == other.reverse
  }

  override fun usesLocalStateContainer(): Boolean = true

  class Builder
  internal constructor(
      context: ComponentContext,
      defStyleAttr: Int,
      defStyleRes: Int,
      @JvmField var column: Column
  ) : ContainerBuilder<Builder>(context, defStyleAttr, defStyleRes, column) {
    override fun setComponent(component: Component) {
      column = component as Column
    }

    override fun child(child: Component?): Builder {
      if (child == null) {
        return this
      }
      val children = column.children ?: ArrayList<Component>().also { column.children = it }
      children.add(child)
      return this
    }

    override fun child(child: Component.Builder<*>?): Builder =
        if (child == null) {
          this
        } else {
          child(child.build())
        }

    override fun alignContent(alignContent: YogaAlign?): Builder = apply {
      column.alignContent = alignContent
    }

    override fun alignItems(alignItems: YogaAlign?): Builder = apply {
      column.alignItems = alignItems
    }

    override fun justifyContent(justifyContent: YogaJustify?): Builder = apply {
      column.justifyContent = justifyContent
    }

    override fun wrap(wrap: YogaWrap?): Builder = apply { column.wrap = wrap }

    override fun reverse(reverse: Boolean): Builder = apply { column.reverse = reverse }

    override fun getThis(): Builder = this

    override fun build(): Column = column
  }

  companion object {
    @JvmStatic
    fun create(context: ComponentContext?, simpleName: String?): Builder =
        create(context, 0, 0, simpleName)

    @JvmStatic
    @JvmOverloads
    fun create(
        context: ComponentContext?,
        defStyleAttr: Int = 0,
        defStyleRes: Int = 0,
        simpleName: String? = null
    ): Builder = Builder(requireNotNull(context), defStyleAttr, defStyleRes, Column(simpleName))
  }
}


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

/** Click listener that triggers its underlying event handler. */
internal class ComponentClickListener(private val eventHandler: EventHandler<ClickEvent>) :
    View.OnClickListener {

  override fun onClick(view: View) {
    val tracingEnabled = isTracing

    if (tracingEnabled) {
      val componentClassName =
          (eventHandler.dispatchInfo.hasEventDispatcher?.javaClass?.name ?: "").take(100)

      ComponentsSystrace.beginSection("onClick_<cls>$componentClassName</cls>")
    }

    try {
      EventDispatcherUtils.dispatchOnClick(eventHandler, view)
    } finally {
      if (tracingEnabled) {
        ComponentsSystrace.endSection()
      }
    }
  }
}


/*
 * 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 android.view.View
import androidx.collection.SparseArrayCompat
import com.facebook.rendercore.MountItem
import java.util.ArrayList

internal object ComponentHostUtils {

  /**
   * Moves an item from oldIndex to newIndex. The item is taken from scrapitems if an item exists in
   * scrapItems at oldPosition. Otherwise the item is taken from items. This assumes that there is
   * no item at newIndex for the items array. If that's the case [ComponentHostUtils.scrapItemAt]
   * has to be called before invoking this.
   */
  @JvmStatic
  fun <T> moveItem(
      oldIndex: Int,
      newIndex: Int,
      items: SparseArrayCompat<T?>,
      scrapItems: SparseArrayCompat<T>?
  ) {
    val itemToMove: T?
    if (existsScrapItemAt(oldIndex, scrapItems)) {
      // Before moving the item from items we need to check whether an old item has been put in
      // the scrapItems array. If there is an item at oldIndex there, it means that in
      // items at position oldIndex there's now something else and the correct item to move to
      // newIndex is instead in the scrapItems SparseArray.
      itemToMove = scrapItems?.get(oldIndex)
      scrapItems?.remove(oldIndex)
    } else {
      itemToMove = items[oldIndex]
      items.remove(oldIndex)
    }
    items.put(newIndex, itemToMove)
  }

  /**
   * Takes the item at position index from items and puts it into scrapItems. If no such item exists
   * the invocation of this method will have no effect.
   */
  @JvmStatic
  fun <T> scrapItemAt(index: Int, items: SparseArrayCompat<T>?, scrapItems: SparseArrayCompat<T>?) {
    if (items == null || scrapItems == null) {
      return
    }
    val value = items[index]
    if (value != null) {
      scrapItems.put(index, value)
    }
  }

  /** Returns true if scrapItems is not null and contains an item with key index. */
  private fun <T> existsScrapItemAt(index: Int, scrapItems: SparseArrayCompat<T>?): Boolean =
      scrapItems?.get(index) != null

  /** Sets the state on a drawable if it is clickable or should duplicate its parent's state. */
  @JvmStatic
  fun maybeSetDrawableState(view: View, drawable: Drawable, flags: Int) {
    val shouldSetState =
        LithoRenderUnit.hasTouchEventHandlers(flags) ||
            LithoRenderUnit.isDuplicateParentState(flags)
    if (shouldSetState && drawable.isStateful) {
      drawable.state = view.drawableState
    }
  }

  /**
   * Remove the item at given {@param index}. The item is removed from {@param scrapItems} if the
   * item exists there at given index, otherwise it is removed from {@param items}.
   */
  @JvmStatic
  fun <T> removeItem(index: Int, items: SparseArrayCompat<T>, scrapItems: SparseArrayCompat<T>?) {
    if (existsScrapItemAt(index, scrapItems)) {
      scrapItems?.remove(index)
    } else {
      items.remove(index)
    }
  }

  @JvmStatic
  fun extractContent(items: SparseArrayCompat<MountItem>): List<*> {
    val size = items.size()
    val content: MutableList<Any> = ArrayList(size)
    for (i in 0 until size) {
      content.add(items.valueAt(i).content)
    }
    return content
  }

  /**
   * This is a helper method that makes Java code to get this data in an easier way. Once we
   * finished the Kotlin migration we should remove this method and replace it the code that it
   * calls.
   */
  @JvmStatic
  fun extractTextContent(items: List<*>): List<TextContent> = items.filterIsInstance<TextContent>()

  @JvmStatic
  fun extractImageContent(items: List<*>): ImageContent =
      object : ImageContent {
        override val imageItems: List<Drawable> =
            items.filterIsInstance<ImageContent>().flatMap { it.imageItems }
      }
}


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

object ComponentKeyUtils {

  private const val DUPLICATE_MANUAL_KEY = "ComponentKeyUtils:DuplicateManualKey"
  private const val NULL_PARENT_KEY = "ComponentKeyUtils:NullParentKey"
  private val TYPE_ID_PATTERN = "(\\d+)".toRegex()
  private const val PREFIX_FOR_MANUAL_KEY = "$"
  private const val SEPARATOR = ","
  private const val SEPARATOR_FOR_MANUAL_KEY = ",$"

  /**
   * @param keyParts a list of objects that will be concatenated to form another component's key
   * @return a key formed by concatenating the key parts delimited by a separator.
   */
  @JvmStatic
  fun getKeyWithSeparator(vararg keyParts: Any): String = buildString {
    append(PREFIX_FOR_MANUAL_KEY).append(keyParts[0])
    for (i in 1..keyParts.lastIndex) append(SEPARATOR_FOR_MANUAL_KEY).append(keyParts[i])
  }

  @JvmStatic
  fun getKeyWithSeparator(parentGlobalKey: String, key: String): String {
    return "$parentGlobalKey$SEPARATOR$key"
  }

  @JvmStatic
  fun getKeyForChildPosition(currentKey: String, index: Int): String {
    if (index == 0) {
      return currentKey
    }
    return "$currentKey!$index"
  }

  /**
   * Generate a global key for the given component that is unique among all of this component's
   * children of the same type. If a manual key has been set on the child component using the .key()
   * method, return the manual key.
   *
   * @param parentComponent parent component within the layout context
   * @param childComponent child component with the parent context
   * @return a unique global key for this component relative to its siblings.
   *
   * TODO: (T38237241) remove the usage of the key handler post the nested tree experiment
   */
  @JvmStatic
  fun generateGlobalKey(
      parentContext: ComponentContext,
      parentComponent: Component?,
      childComponent: Component
  ): String {
    val hasManualKey = childComponent.hasManualKey()
    val key = if (hasManualKey) "$${childComponent.key}" else childComponent.key
    val globalKey: String
    if (parentComponent == null) {
      globalKey = key
    } else {
      val parentGlobalKey = parentContext.globalKey
      if (parentGlobalKey == null) {
        logParentHasNullGlobalKey(parentComponent, childComponent)
        globalKey = "null$key"
      } else {
        val childKey = getKeyWithSeparator(parentGlobalKey, key)
        val index: Int
        if (hasManualKey) {
          index = parentContext.scopedComponentInfo.getManualKeyUsagesCountAndIncrement(key)
          if (index != 0) {
            logDuplicateManualKeyWarning(childComponent, key.substring(1))
          }
        } else {
          index = parentContext.scopedComponentInfo.getChildCountAndIncrement(childComponent)
        }
        globalKey = getKeyForChildPosition(childKey, index)
      }
    }
    return globalKey
  }

  private fun logParentHasNullGlobalKey(parentComponent: Component, childComponent: Component) {
    ComponentsReporter.emitMessage(
        ComponentsReporter.LogLevel.ERROR,
        NULL_PARENT_KEY,
        "Trying to generate parent-based key for component ${childComponent.simpleName}" +
            " , but parent ${parentComponent.simpleName} has a null global key \".")
  }

  private fun logDuplicateManualKeyWarning(component: Component, key: String) {
    ComponentsReporter.emitMessage(
        ComponentsReporter.LogLevel.WARNING,
        DUPLICATE_MANUAL_KEY,
        "The manual key $key you are setting on this ${component.simpleName} " +
            "is a duplicate and will be changed into a unique one. " +
            "This will result in unexpected behavior if you don't change it.")
  }

  @JvmStatic
  fun getKeyWithSeparatorForTest(vararg keyParts: Any): String = buildString {
    append(keyParts[0])
    for (i in 1..keyParts.lastIndex) append(SEPARATOR).append(keyParts[i])
  }

  @JvmStatic
  fun mapToSimpleName(key: String, typeToIdMap: Map<Any, Int>): String? {
    // manual keys cannot be resolved.
    if (key.startsWith(PREFIX_FOR_MANUAL_KEY)) {
      return key
    }

    // default to return in case of failure
    val unresolved = "id($key)"

    // find the type id
    val idPart = TYPE_ID_PATTERN.find(key)?.value ?: return unresolved
    val id =
        idPart.toIntOrNull()
            ?: return unresolved // if parsing failed just return the unresolved key.
    for ((type, value) in typeToIdMap) {
      if (value == id) {
        return when (type) {
          is Class<*> -> "<cls>${type.name}</cls>"
          else -> type.toString()
        }
      }
    }
    return unresolved
  }
}


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

/** Data class for params used to log error in LithoView. */
class ComponentLogParams
@JvmOverloads
constructor(
    @JvmField val logProductId: String,
    @JvmField val logType: String,
    @JvmField val samplingFrequency: Int = 0,
    @JvmField val failHarder: 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 android.view.View

/** Long click listener that triggers its underlying event handler. */
class ComponentLongClickListener : View.OnLongClickListener {

  var eventHandler: EventHandler<LongClickEvent>? = null

  override fun onLongClick(view: View): Boolean =
      when (val eventHandler = eventHandler) {
        null -> false
        else -> EventDispatcherUtils.dispatchOnLongClick(eventHandler, 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 kotlin.jvm.JvmField

/**
 * The result of a [Component#resolveWithResult] call. This will hold the [LithoNode] this Component
 * resolved to as well as CommonProps of this component.
 */
class ComponentResolveResult(
    @JvmField val lithoNode: LithoNode?,
    @JvmField val commonProps: CommonProps?
)


/*
 * 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.app.Activity
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import com.facebook.litho.ComponentsLifecycles.LeakDetector
import com.facebook.rendercore.MountItemsPool
import java.lang.RuntimeException
import java.util.WeakHashMap

/**
 * Callbacks that must be invoked to avoid leaking memory if using Components below ICS (API level
 * 14).
 */
object ComponentsLifecycles {

  private var trackedContexts: WeakHashMap<Context, LeakDetector>? = null

  @JvmStatic
  fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
    onContextCreated(activity)
  }

  @JvmStatic
  fun onActivityDestroyed(activity: Activity) {
    onContextDestroyed(activity)
  }

  @JvmStatic
  fun onContextCreated(activity: Activity) {
    setManualCallbacksEnabledForPool()
    onContextCreated((activity as Context))
  }

  @JvmStatic
  fun onContextCreated(context: Context) {
    if (trackedContexts == null) {
      trackedContexts = WeakHashMap()
    }

    val old = trackedContexts!!.put(context, LeakDetector(context))
    if (old != null) {
      throw RuntimeException("Duplicate onContextCreated call for: $context")
    }
    onContextCreatedForPool(context)
  }

  @JvmStatic
  fun onContextDestroyed(context: Context) {
    val removed =
        trackedContexts?.remove(context)
            ?: error("onContextDestroyed called without onContextCreated for :$context")
    removed.clear()

    onContextDestroyedForPool(context)
  }

  private fun setManualCallbacksEnabledForPool() {
    MountItemsPool.sIsManualCallbacks = true
  }

  private fun onContextCreatedForPool(context: Context) {
    MountItemsPool.onContextCreated(context)
  }

  private fun onContextDestroyedForPool(context: Context) {
    MountItemsPool.onContextDestroyed(context)
  }

  private class LeakDetector(private var context: Context?) {
    fun clear() {
      context = null
    }

    fun finalize() {
      context?.let {
        // Post the error to the main thread to bring down the process -
        // exceptions on finalizer threads are ignored by default.
        Handler(Looper.getMainLooper()).post {
          throw RuntimeException("onContextDestroyed method not called for: $it")
        }
      }
    }
  }
}


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

package com.facebook.litho

import com.facebook.rendercore.Systracer
import com.facebook.rendercore.Systracer.ArgsBuilder

/**
 * This is intended as a hook into `android.os.Trace`, but allows you to provide your own
 * functionality. Use it as
 *
 * `ComponentsSystrace.beginSection("tag"); ... ComponentsSystrace.endSection(); ` in Java. Or as
 *
 * `ComponentsSystrace.trace("tag") { ... }` in Kotlin. As a default, it simply calls
 * `android.os.Trace` (see [DefaultComponentsSystrace]). You may supply your own with
 * [ComponentsSystrace.provide].
 */
object ComponentsSystrace {
  @JvmStatic
  var systrace: Systracer = DefaultComponentsSystrace()
    private set

  /** This should be called exactly once at app startup, before any Litho work happens. */
  @JvmStatic
  fun provide(instance: Systracer) {
    systrace = instance
  }

  /**
   * Writes a trace message to indicate that a given section of code has begun. This call must be
   * followed by a corresponding call to [.endSection] on the same thread.
   */
  @JvmStatic
  fun beginSection(name: String) {
    systrace.beginSection(name)
  }

  /**
   * Writes a trace message to indicate that a given section of code has begun. Must be followed by
   * a call to [.endAsyncSection] using the same tag. Unlike [ ][.beginSection] and [.endSection],
   * asynchronous events do not need to be nested. The name and cookie used to begin an event must
   * be used to end it.
   *
   * Depending on provided [Systracer] instance, this method could vary in behavior and in
   * [DefaultComponentsSystrace] it is a no-op.
   */
  @JvmStatic
  fun beginAsyncSection(name: String) {
    systrace.beginAsyncSection(name)
  }

  /**
   * Writes a trace message to indicate that a given section of code has begun. Must be followed by
   * a call to [.endAsyncSection] using the same tag. Unlike [ ][.beginSection] and [.endSection],
   * asynchronous events do not need to be nested. The name and cookie used to begin an event must
   * be used to end it.
   *
   * Depending on provided [Systracer] instance, this method could vary in behavior and in
   * [DefaultComponentsSystrace] it is a no-op.
   */
  @JvmStatic
  fun beginAsyncSection(name: String, cookie: Int) {
    systrace.beginAsyncSection(name, cookie)
  }

  @JvmStatic
  fun beginSectionWithArgs(name: String): ArgsBuilder {
    return systrace.beginSectionWithArgs(name)
  }

  /**
   * Writes a trace message to indicate that a given section of code has ended. This call must be
   * preceded by a corresponding call to [.beginSection]. Calling this method will mark the end of
   * the most recently begun section of code, so care must be taken to ensure that beginSection /
   * endSection pairs are properly nested and called from the same thread.
   */
  @JvmStatic
  fun endSection() {
    systrace.endSection()
  }

  /**
   * Writes a trace message to indicate that the current method has ended. Must be called exactly
   * once for each call to [.beginAsyncSection] using the same tag, name and cookie.
   *
   * Depending on provided [Systracer] instance, this method could vary in behavior and in
   * [DefaultComponentsSystrace] it is a no-op.
   */
  @JvmStatic
  fun endAsyncSection(name: String) {
    systrace.endAsyncSection(name)
  }

  /**
   * Writes a trace message to indicate that the current method has ended. Must be called exactly
   * once for each call to [.beginAsyncSection] using the same tag, name and cookie.
   *
   * Depending on provided [Systracer] instance, this method could vary in behavior and in
   * [DefaultComponentsSystrace] it is a no-op.
   */
  @JvmStatic
  fun endAsyncSection(name: String, cookie: Int) {
    systrace.endAsyncSection(name, cookie)
  }

  /**
   * A convenience Kotlin API that can be used instead of manually calling
   * [ComponentsSystrace.beginSection] and [ComponentsSystrace.endSection].
   *
   * Starts the tracing section, executes the given [tracingBlock] and ends the tracing section.
   */
  @JvmStatic
  inline fun <T> trace(name: String, tracingBlock: () -> T): T {
    try {
      if (isTracing) beginSection(name)
      return tracingBlock()
    } finally {
      if (isTracing) endSection()
    }
  }

  @JvmStatic
  val isTracing: Boolean
    @JvmStatic get() = systrace.isTracing
}


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

/**
 * Helper class in charge of dumping the component hierarchy related to a provided
 * [ComponentContext]
 *
 * This class is intended to be used only in debug environments due to limitations with the
 * preservation of the view hierarchy
 *
 * This will not provide a reliable representation of the hierarchy on non debug build of the app
 */
object ComponentTreeDumpingHelper {

  /** Dumps the tree related to the provided component context */
  @JvmStatic
  fun dumpContextTree(componentTree: ComponentTree?): String? {
    // Getting the base of the tree
    val rootComponent = DebugComponent.getRootInstance(componentTree) ?: return null
    val sb = StringBuilder()
    logComponent(rootComponent, 0, sb)
    return sb.toString()
  }

  /** Logs the content of a single debug component instance */
  private fun logComponent(debugComponent: DebugComponent, depth: Int, sb: StringBuilder) {
    // Logging the component name
    sb.append(debugComponent.component.simpleName)

    // Description of the component status (Visible, Has Click Handler)
    sb.append('{')
    val lithoView = debugComponent.lithoView
    val layout = debugComponent.layoutNode
    sb.append(if (lithoView != null && lithoView.visibility == View.VISIBLE) "V" else "H")
    if (layout != null && layout.clickHandler != null) {
      sb.append(" [clickable]")
    }
    if (layout != null) {
      sb.append(" ")
      sb.append(layout.layoutHeight)
      sb.append("x")
      sb.append(layout.layoutWidth)
    }
    sb.append('}')
    for (child in debugComponent.childComponents) {
      sb.append("\n")
      for (i in 0..depth) {
        sb.append("  ")
      }
      logComponent(child, depth + 1, sb)
    }
  }
}


/*
 * 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.kotlin.compilerplugins.dataclassgenerate.annotation.DataClassGenerate
import java.util.UUID

interface ComponentTreeTimeMachine {

  val originalRootName: String

  fun storeRevision(
      root: Component,
      treeState: TreeState,
      treeProps: TreeProps?,
      source: Int,
      attribution: String?
  )

  fun getCurrentRevision(): Revision

  fun restoreRevision(id: String)

  fun getRevisions(): List<Revision>

  @DataClassGenerate
  data class Revision
  internal constructor(
      val id: String,
      val version: Int,
      val root: Component,
      val treeState: TreeState,
      val treeProps: TreeProps?,
      val timestamp: Long,
      val source: Int,
      val attribution: String?
  )
}

internal class DebugComponentTreeTimeMachine(private val componentTree: ComponentTree) :
    ComponentTreeTimeMachine {

  /**
   * Whenever we restore one revision, the [ComponentTree] will attempt to commit an equivalent
   * revision to the restored one.
   *
   * Therefore we use a controlling mechanism to avoid storing a revision that was restored.
   */
  private var skipNextRevision: Boolean = false

  override val originalRootName: String
    get() =
        revisions.entries.firstOrNull()?.value?.root?.simpleName
            ?: error("There should be at least one revision")

  private val revisions = LinkedHashMap<String, ComponentTreeTimeMachine.Revision>()
  private var selectedRevisionId: String? = null

  override fun storeRevision(
      root: Component,
      treeState: TreeState,
      treeProps: TreeProps?,
      source: Int,
      attribution: String?
  ) {
    if (!skipNextRevision) {
      val revision =
          ComponentTreeTimeMachine.Revision(
              id = UUID.randomUUID().toString(),
              version = revisions.size,
              root = root,
              treeState = treeState,
              treeProps = treeProps,
              timestamp = System.currentTimeMillis(),
              source = source,
              attribution = attribution)

      revisions[revision.id] = revision
      selectedRevisionId = revision.id
    }

    skipNextRevision = false
  }

  override fun restoreRevision(id: String) {
    val revision = revisions[id] ?: error("There should be a revision with id $id")
    selectedRevisionId = id
    skipNextRevision = true
    componentTree.applyRevision(revision)
  }

  override fun getRevisions(): List<ComponentTreeTimeMachine.Revision> = revisions.values.toList()

  override fun getCurrentRevision(): ComponentTreeTimeMachine.Revision {
    return revisions[selectedRevisionId]
        ?: error("There should be a revision with id $selectedRevisionId")
  }
}


/*
 * 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.litho.DebugComponentDescriptionHelper.ExtraDescription
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.ResType
import java.lang.Exception
import java.lang.StringBuilder
import java.util.HashSet
import kotlin.jvm.JvmStatic
import org.json.JSONObject

/**
 * Describes [DebugComponent]s for use in testing and debugging. Note that
 * [com.facebook.litho.config.ComponentsConfiguration#isEndToEndTestRun] must be enabled in order
 * for this data to be collected.
 */
object DebugComponentDescriptionHelper {

  /** Fields to ignore when dumping extra props */
  private val IGNORE_PROP_FIELDS =
      HashSet(
          listOf(
              "delegate",
              "feedPrefetcher",
              "parentFeedContextChain",
              "child",
              "children",
              "childComponent",
              "trackingCode",
              "eventsController",
              "itemAnimator",
              "onScrollListeners",
              "recyclerConfiguration",
              "threadTileViewData",
              "textColorStateList",
              "typeface",
              "text",
              "params"))

  @JvmStatic
  fun addViewDescription(
      debugComponent: DebugComponent,
      sb: StringBuilder,
      leftOffset: Int,
      topOffset: Int,
      embedded: Boolean,
      withProps: Boolean
  ) {
    addViewDescription(debugComponent, sb, leftOffset, topOffset, embedded, withProps, null)
  }

  /**
   * Appends a compact description of a [DebugComponent] for debugging purposes.
   *
   * @param debugComponent The [DebugComponent]
   * @param sb The [StringBuilder] to which the description is appended
   * @param leftOffset Offset of the parent component relative to litho view
   * @param topOffset Offset of the parent component relative to litho view
   * @param embedded Whether the call is embedded in "adb dumpsys activity"
   * @param extraDescription An interface for callsite to append extra description.
   */
  @JvmStatic
  /**
   * Appends a compact description of a [DebugComponent] for debugging purposes.
   *
   * @param debugComponent The [DebugComponent]
   * @param sb The [StringBuilder] to which the description is appended
   * @param leftOffset Offset of the parent component relative to litho view
   * @param topOffset Offset of the parent component relative to litho view
   * @param embedded Whether the call is embedded in "adb dumpsys activity"
   * @param extraDescription An interface for callsite to append extra description.
   */
  fun addViewDescription(
      debugComponent: DebugComponent,
      sb: StringBuilder,
      leftOffset: Int,
      topOffset: Int,
      embedded: Boolean,
      withProps: Boolean,
      extraDescription: ExtraDescription?
  ) {
    sb.append("litho.")
    sb.append(debugComponent.component.simpleName)
    sb.append('{')
    sb.append(Integer.toHexString(debugComponent.hashCode()))
    sb.append(' ')
    val lithoView = debugComponent.lithoView
    val layout = debugComponent.layoutNode
    sb.append(if (lithoView != null && lithoView.visibility == View.VISIBLE) "V" else ".")
    sb.append(if (layout != null && layout.focusable) "F" else ".")
    sb.append(if (lithoView != null && lithoView.isEnabled) "E" else ".")
    sb.append(".")
    sb.append(if (lithoView != null && lithoView.isHorizontalScrollBarEnabled) "H" else ".")
    sb.append(if (lithoView != null && lithoView.isVerticalScrollBarEnabled) "V" else ".")
    sb.append(if (layout?.clickHandler != null) "C" else ".")
    sb.append(". .. ")
    // using position relative to litho view host to handle relative position issues
    // the offset is for the parent component to create proper relative coordinates
    val bounds = debugComponent.boundsInLithoView
    sb.append(bounds.left - leftOffset)
    sb.append(",")
    sb.append(bounds.top - topOffset)
    sb.append("-")
    sb.append(bounds.right - leftOffset)
    sb.append(",")
    sb.append(bounds.bottom - topOffset)
    val testKey = debugComponent.testKey
    if (testKey != null && !testKey.isNullOrEmpty()) {
      sb.append(" litho:id/").append(testKey.replace(' ', '_'))
    }
    val textContent = debugComponent.textContent
    if (textContent != null && !textContent.isNullOrEmpty()) {
      sb.append(" text=\"").append(fixString(textContent, 200)).append("\"")
    }
    if (withProps) {
      addExtraProps(debugComponent.component, sb)
    }
    extraDescription?.applyExtraDescription(debugComponent, sb)
    if (!embedded && layout?.clickHandler != null) {
      sb.append(" [clickable]")
    }
    sb.append('}')
  }

  private fun addExtraProps(node: Any, sb: StringBuilder) {
    val props = getExtraProps(node)
    if (props.length() > 0) {
      sb.append(" props=\"").append(props.toString()).append("\"")
    }
  }

  @JvmStatic
  fun getExtraProps(node: Any): JSONObject {
    val props = JSONObject()
    for (field in node.javaClass.declaredFields) {
      try {
        if (IGNORE_PROP_FIELDS.contains(field.name)) {
          continue
        }
        val annotation = field.getAnnotation(Prop::class.java) ?: continue
        field.isAccessible = true
        when (annotation.resType) {
          ResType.COLOR,
          ResType.DRAWABLE,
          ResType.DIMEN_SIZE,
          ResType.DIMEN_OFFSET -> {}
          ResType.STRING -> {
            val strValue = fixString(field[node], 50)
            if (strValue.isNotEmpty()) {
              props.put(field.name, strValue)
            }
          }
          else -> {
            val value = field[node]
            if (value != null) {
              props.put(field.name, value)
            }
          }
        }
      } catch (e: Exception) {
        try {
          props.put("DUMP-ERROR", fixString(e.message, 50))
        } catch (ex: Exception) {
          // ignore
        }
      }
    }
    return props
  }

  private fun fixString(str: Any?, maxLength: Int): String {
    if (str == null) {
      return ""
    }
    var fixed = str.toString().replace(" \n", " ").replace("\n", " ").replace("\"", "")
    if (fixed.length > maxLength) {
      fixed = "${fixed.substring(0, maxLength)}..."
    }
    return fixed
  }

  /**
   * An fun interface for callsite to append extra description into [StringBuilder] by given
   * [DebugComponent].
   */
  fun interface ExtraDescription {
    fun applyExtraDescription(debugComponent: DebugComponent, sb: StringBuilder)
  }
}


/*
 * 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.Resources
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import com.facebook.litho.config.ComponentsConfiguration

/** Draw operations used in developer options. */
internal object DebugDraw {

  private const val INTERACTIVE_VIEW_COLOR = 0x66C29BFF
  private const val TOUCH_DELEGATE_COLOR = 0x44D3FFCE
  private const val MOUNT_BORDER_COLOR = -0x66010000
  private const val MOUNT_BORDER_COLOR_HOST = -0x6600ff01
  private const val MOUNT_CORNER_COLOR = -0xffff01
  private const val MOUNT_CORNER_COLOR_HOST = -0xff0001

  private val interactiveViewPaint: Paint by lazy {
    Paint().apply { color = INTERACTIVE_VIEW_COLOR }
  }
  private val touchDelegatePaint: Paint by lazy { Paint().apply { color = TOUCH_DELEGATE_COLOR } }

  private val mountBoundsRect: Rect by lazy { Rect() }
  private val mountBoundsBorderPaint: Paint by lazy {
    Paint().apply {
      style = Paint.Style.STROKE
      strokeWidth = 1f
    }
  }

  private val mountBoundsCornerPaint: Paint by lazy {
    Paint().apply {
      style = Paint.Style.FILL
      strokeWidth = 2f
    }
  }

  @JvmStatic
  fun draw(host: ComponentHost, canvas: Canvas) {
    if (ComponentsConfiguration.debugHighlightInteractiveBounds) {
      highlightInteractiveBounds(host, canvas)
    }
    if (ComponentsConfiguration.debugHighlightMountBounds) {
      highlightMountBounds(host, canvas)
    }
  }

  private fun highlightInteractiveBounds(host: ComponentHost, canvas: Canvas) {
    // 1. Highlight root, if applicable.
    if (isInteractive(host)) {
      canvas.drawRect(0f, 0f, host.width.toFloat(), host.height.toFloat(), interactiveViewPaint)
    }

    // 2. Highlight non-host interactive mounted views.
    for (i in host.mountItemCount - 1 downTo 0) {
      val item = host.getMountItemAt(i)
      val component = LithoRenderUnit.getRenderUnit(item).component
      if (!LithoRenderUnit.isMountableView(item.renderTreeNode.renderUnit) ||
          Component.isHostSpec(component)) {
        continue
      }
      val view = item.content as View
      if (!isInteractive(view)) {
        continue
      }
      canvas.drawRect(
          view.left.toFloat(),
          view.top.toFloat(),
          view.right.toFloat(),
          view.bottom.toFloat(),
          touchDelegatePaint)
    }

    // 3. Highlight expanded touch bounds.
    val touchDelegate = host.touchExpansionDelegate
    touchDelegate?.draw(canvas, touchDelegatePaint)
  }

  private fun highlightMountBounds(host: ComponentHost, canvas: Canvas) {
    val resources = host.resources
    for (i in host.mountItemCount - 1 downTo 0) {
      val item = host.getMountItemAt(i)
      val component = LithoRenderUnit.getRenderUnit(item).component
      val content = item.content
      if (!shouldHighlight(component)) {
        continue
      }
      if (content is View) {
        mountBoundsRect.set(content.left, content.top, content.right, content.bottom)
      } else if (content is Drawable) {
        mountBoundsRect.set(content.bounds)
      }

      mountBoundsBorderPaint.color = getBorderColor(component)
      drawMountBoundsBorder(canvas, mountBoundsBorderPaint, mountBoundsRect)
      mountBoundsCornerPaint.color = getCornerColor(component)
      drawMountBoundsCorners(
          canvas,
          mountBoundsCornerPaint,
          mountBoundsRect,
          mountBoundsCornerPaint.strokeWidth.toInt(),
          Math.min(
              Math.min(mountBoundsRect.width(), mountBoundsRect.height()) / 3,
              dipToPixels(resources, 12)))
    }
  }

  private fun drawMountBoundsBorder(canvas: Canvas, paint: Paint, bounds: Rect) {
    val inset = paint.strokeWidth.toInt() / 2
    bounds.inset(inset, inset)
    canvas.drawRect(bounds, paint)
  }

  private fun drawMountBoundsCorners(
      canvas: Canvas,
      paint: Paint,
      bounds: Rect,
      cornerLength: Int,
      cornerWidth: Int
  ) {
    drawCorner(canvas, paint, bounds.left, bounds.top, cornerLength, cornerLength, cornerWidth)
    drawCorner(canvas, paint, bounds.left, bounds.bottom, cornerLength, -cornerLength, cornerWidth)
    drawCorner(canvas, paint, bounds.right, bounds.top, -cornerLength, cornerLength, cornerWidth)
    drawCorner(
        canvas, paint, bounds.right, bounds.bottom, -cornerLength, -cornerLength, cornerWidth)
  }

  private fun shouldHighlight(component: Component): Boolean =
      // Don't highlight bounds of background/foreground components.
      component !is DrawableComponent<*>

  private fun dipToPixels(res: Resources, dips: Int): Int {
    val scale = res.displayMetrics.density
    return (dips * scale + 0.5f).toInt()
  }

  private fun getBorderColor(component: Component): Int =
      if (Component.isHostSpec(component)) MOUNT_BORDER_COLOR_HOST else MOUNT_BORDER_COLOR

  private fun getCornerColor(component: Component): Int =
      if (Component.isHostSpec(component)) MOUNT_CORNER_COLOR_HOST else MOUNT_CORNER_COLOR

  private fun sign(x: Float): Int = if (x >= 0) 1 else -1

  private fun drawCorner(
      c: Canvas,
      paint: Paint,
      x: Int,
      y: Int,
      dx: Int,
      dy: Int,
      cornerWidth: Int
  ) {
    drawCornerLine(c, paint, x, y, x + dx, y + cornerWidth * sign(dy.toFloat()))
    drawCornerLine(c, paint, x, y, x + cornerWidth * sign(dx.toFloat()), y + dy)
  }

  private fun drawCornerLine(
      canvas: Canvas,
      paint: Paint,
      left: Int,
      top: Int,
      right: Int,
      bottom: Int
  ) {
    var left = left
    var top = top
    var right = right
    var bottom = bottom
    if (left > right) {
      val tmp = left
      left = right
      right = tmp
    }
    if (top > bottom) {
      val tmp = top
      top = bottom
      bottom = tmp
    }
    canvas.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), paint)
  }

  private fun isInteractive(view: View): Boolean =
      view.hasOnClickListeners() ||
          LithoViewAttributesExtension.getComponentLongClickListener(view) != null ||
          LithoViewAttributesExtension.getComponentTouchListener(view) != 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.graphics.drawable.Drawable
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaDirection
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaFlexDirection
import com.facebook.yoga.YogaJustify
import com.facebook.yoga.YogaPositionType
import com.facebook.yoga.YogaValue

/**
 * A DebugLayoutNode is a wrapper around InternalNode which allows debug tools to inspect and mutate
 * internal nodes without making InternalNode a public class. This class should never be used in
 * production and only for building debug tools.
 */
class DebugLayoutNode internal constructor(private val result: LithoLayoutResult) {

  private val node: LithoNode = result.node
  val foreground: Drawable?
    get() = node.foreground

  val background: Drawable?
    get() = node.background

  fun hasViewOutput(): Boolean = LithoNode.hasViewOutput(result.node)

  val rotation: Float
    get() = node.nodeInfo?.rotation ?: 0f

  val alpha: Float
    get() = node.nodeInfo?.alpha ?: 1f

  val scale: Float
    get() = node.nodeInfo?.scale ?: 1f

  val importantForAccessibility: Int
    get() = node.importantForAccessibility

  val focusable: Boolean
    get() = node.nodeInfo?.focusState == NodeInfo.FOCUS_SET_TRUE

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

  val layoutDirection: YogaDirection
    get() = result.yogaNode.styleDirection

  val flexDirection: YogaFlexDirection
    get() = result.yogaNode.flexDirection

  val justifyContent: YogaJustify
    get() = result.yogaNode.justifyContent

  val alignItems: YogaAlign
    get() = result.yogaNode.alignItems

  val alignSelf: YogaAlign
    get() = result.yogaNode.alignSelf

  val alignContent: YogaAlign
    get() = result.yogaNode.alignContent

  val positionType: YogaPositionType
    get() = result.yogaNode.positionType

  val flexGrow: Float
    get() = result.yogaNode.flexGrow

  val flexShrink: Float
    get() = result.yogaNode.flexShrink

  val flexBasis: YogaValue
    get() = result.yogaNode.flexBasis

  val width: YogaValue
    get() = result.yogaNode.width

  val minWidth: YogaValue
    get() = result.yogaNode.minWidth

  val maxWidth: YogaValue
    get() = result.yogaNode.maxWidth

  val height: YogaValue
    get() = result.yogaNode.height

  val minHeight: YogaValue
    get() = result.yogaNode.minHeight

  val maxHeight: YogaValue
    get() = result.yogaNode.maxHeight

  val aspectRatio: Float
    get() = result.yogaNode.aspectRatio

  fun getMargin(edge: YogaEdge): YogaValue = result.yogaNode.getMargin(edge)

  fun getPadding(edge: YogaEdge): YogaValue = result.yogaNode.getPadding(edge)

  fun getPosition(edge: YogaEdge): YogaValue = result.yogaNode.getPosition(edge)

  fun getBorderWidth(edge: YogaEdge): Float = result.yogaNode.getBorder(edge)

  val clickHandler: EventHandler<ClickEvent>?
    get() = node.nodeInfo?.clickHandler

  val layoutWidth: Float
    get() = result.yogaNode.layoutWidth

  val layoutHeight: Float
    get() = result.yogaNode.layoutHeight

  fun getLayoutMargin(edge: YogaEdge): Float = result.yogaNode.getLayoutMargin(edge)

  fun getLayoutPadding(edge: YogaEdge): Float = result.yogaNode.getLayoutPadding(edge)
}


/*
 * 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.yoga.YogaAlign
import com.facebook.yoga.YogaConstants
import com.facebook.yoga.YogaDirection
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaFlexDirection
import com.facebook.yoga.YogaJustify
import com.facebook.yoga.YogaPositionType
import com.facebook.yoga.YogaUnit
import com.facebook.yoga.YogaValue

class DebugLayoutNodeEditor(private val node: LithoNode) {

  fun setForegroundColor(color: Int) {
    node.foregroundColor(color)
  }

  fun setBackgroundColor(color: Int) {
    node.backgroundColor(color)
  }

  fun setRotation(value: Float) {
    node.nodeInfo?.rotation = value
  }

  fun setAlpha(value: Float) {
    node.nodeInfo?.alpha = value
  }

  fun setScale(value: Float) {
    node.nodeInfo?.scale = value
  }

  fun setImportantForAccessibility(importantForAccessibility: Int) {
    node.importantForAccessibility(importantForAccessibility)
  }

  fun setFocusable(focusable: Boolean) {
    node.mutableNodeInfo().setFocusable(focusable)
  }

  fun setContentDescription(contentDescription: CharSequence?) {
    node.mutableNodeInfo().contentDescription = contentDescription
  }

  fun setLayoutDirection(yogaDirection: YogaDirection) {
    node.debugLayoutEditor.layoutDirection(yogaDirection)
  }

  fun setFlexDirection(direction: YogaFlexDirection) {
    node.flexDirection(direction)
  }

  fun setJustifyContent(yogaJustify: YogaJustify) {
    node.justifyContent(yogaJustify)
  }

  fun setAlignItems(yogaAlign: YogaAlign) {
    node.alignItems(yogaAlign)
  }

  fun setAlignSelf(yogaAlign: YogaAlign) {
    node.debugLayoutEditor.alignSelf(yogaAlign)
  }

  fun setAlignContent(yogaAlign: YogaAlign?) {
    node.alignContent(yogaAlign)
  }

  fun setPositionType(yogaPositionType: YogaPositionType) {
    node.debugLayoutEditor.positionType(yogaPositionType)
  }

  fun setFlexGrow(value: Float) {
    node.debugLayoutEditor.flexGrow(value)
  }

  fun setFlexShrink(value: Float) {
    node.debugLayoutEditor.flexShrink(value)
  }

  fun setFlexBasis(value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.flexBasisAuto()
      YogaUnit.PERCENT -> node.debugLayoutEditor.flexBasisPercent(value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.flexBasisPx(value.value.toInt())
    }
  }

  fun setWidth(value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.widthAuto()
      YogaUnit.PERCENT -> node.debugLayoutEditor.widthPercent(value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.widthPx(value.value.toInt())
    }
  }

  fun setMinWidth(value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.minWidthPx(Int.MIN_VALUE)
      YogaUnit.PERCENT -> node.debugLayoutEditor.minWidthPercent(value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.minWidthPx(value.value.toInt())
    }
  }

  fun setMaxWidth(value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.maxWidthPx(Int.MAX_VALUE)
      YogaUnit.PERCENT -> node.debugLayoutEditor.maxWidthPercent(value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.maxWidthPx(value.value.toInt())
    }
  }

  fun setHeight(value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.heightAuto()
      YogaUnit.PERCENT -> node.debugLayoutEditor.heightPercent(value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.heightPx(value.value.toInt())
    }
  }

  fun setMinHeight(value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.minHeightPx(Int.MIN_VALUE)
      YogaUnit.PERCENT -> node.debugLayoutEditor.minHeightPercent(value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.minHeightPx(value.value.toInt())
    }
  }

  fun setMaxHeight(value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.maxHeightPx(Int.MAX_VALUE)
      YogaUnit.PERCENT -> node.debugLayoutEditor.maxHeightPercent(value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.maxHeightPx(value.value.toInt())
    }
  }

  fun setAspectRatio(aspectRatio: Float) {
    node.debugLayoutEditor.aspectRatio(aspectRatio)
  }

  fun setMargin(edge: YogaEdge, value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED -> node.debugLayoutEditor.marginPx(edge, 0)
      YogaUnit.AUTO -> node.debugLayoutEditor.marginAuto(edge)
      YogaUnit.PERCENT -> node.debugLayoutEditor.marginPercent(edge, value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.marginPx(edge, value.value.toInt())
    }
  }

  fun setPadding(edge: YogaEdge, value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.paddingPx(edge, 0)
      YogaUnit.PERCENT -> node.debugLayoutEditor.paddingPercent(edge, value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.paddingPx(edge, value.value.toInt())
    }
  }

  fun setPosition(edge: YogaEdge, value: YogaValue) {
    when (value.unit) {
      YogaUnit.UNDEFINED,
      YogaUnit.AUTO -> node.debugLayoutEditor.positionPercent(edge, YogaConstants.UNDEFINED)
      YogaUnit.PERCENT -> node.debugLayoutEditor.positionPercent(edge, value.value)
      YogaUnit.POINT -> node.debugLayoutEditor.positionPx(edge, value.value.toInt())
    }
  }

  fun isReferenceBaseline(isReferenceBaseline: Boolean) {
    node.debugLayoutEditor.isReferenceBaseline(isReferenceBaseline)
  }

  fun setBorderWidth(edge: YogaEdge, value: Float) {
    node.debugLayoutEditor.setBorderWidth(edge, value.toInt().toFloat())
  }
}


/*
 * 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.Log
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.rendercore.ErrorReporterDelegate
import com.facebook.rendercore.LogLevel
import java.lang.RuntimeException

open class DefaultComponentsReporter : ErrorReporterDelegate {

  override fun report(
      level: LogLevel,
      categoryKey: String,
      message: String,
      cause: Throwable?,
      samplingFrequency: Int,
      metadata: Map<String, Any>?
  ) {
    when (level) {
      LogLevel.WARNING ->
          applyOnInternalBuild { Log.w(CATEGORY_PREFIX + categoryKey, message, cause) }
      LogLevel.ERROR ->
          applyOnInternalBuild { Log.e(CATEGORY_PREFIX + categoryKey, message, cause) }
      LogLevel.FATAL -> {
        applyOnInternalBuild { Log.e(CATEGORY_PREFIX + categoryKey, message, cause) }
        throw RuntimeException(message)
      }
      else -> {
        // do nothing
      }
    }
  }

  @JvmOverloads
  fun emitMessage(
      level: ComponentsReporter.LogLevel,
      categoryKey: String,
      message: String,
      samplingFrequency: Int = /* take default*/ 0,
      metadata: Map<String, Any>? = null
  ) {
    report(ComponentsReporter.map(level), categoryKey, message, null, samplingFrequency, metadata)
  }

  private fun applyOnInternalBuild(block: () -> Unit) {
    if (ComponentsConfiguration.IS_INTERNAL_BUILD) {
      block()
    }
  }

  companion object {
    private const val CATEGORY_PREFIX = "Litho:"
  }
}


/*
 * 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.Build
import android.os.Trace
import com.facebook.litho.config.ComponentsConfiguration
import com.facebook.rendercore.RenderCoreSystrace
import com.facebook.rendercore.Systracer
import com.facebook.rendercore.Systracer.ArgsBuilder

open class DefaultComponentsSystrace : Systracer {

  override fun beginSection(name: String) = applyIfTracing {
    val normalizedName =
        if (name.length > MAX_CHARACTERS_SECTION_NAME)
            "${name.substring(0, MAX_CHARACTERS_SECTION_NAME - 1)}â€¦"
        else name
    Trace.beginSection(normalizedName)
  }

  override fun beginAsyncSection(name: String) {
    // no-op
  }

  override fun beginAsyncSection(name: String, cookie: Int) {
    // no-op
  }

  override fun beginSectionWithArgs(name: String): ArgsBuilder {
    beginSection(name)
    return RenderCoreSystrace.NO_OP_ARGS_BUILDER
  }

  override fun endSection() = applyIfTracing { Trace.endSection() }

  override fun endAsyncSection(name: String) {
    // no-op
  }

  override fun endAsyncSection(name: String, cookie: Int) {
    // no-op
  }

  override fun isTracing(): Boolean =
      ComponentsConfiguration.IS_INTERNAL_BUILD &&
          Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
          (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || Trace.isEnabled())

  private fun applyIfTracing(block: () -> Unit) {
    if (isTracing) {
      block()
    }
  }

  companion object {
    /**
     * In android.os.Trace there is a limit to the section name (127 characters). If
     * [Trace#beginSection(String)] is called with a String bigger than 127, it will throw an
     * exception.
     *
     * We handle this case in our scenario and will trim it in case it is bigger than the valid
     * limit.
     *
     * @see [android.os.Trace](https://fburl.com/7bngmv5x)
     */
    private const val MAX_CHARACTERS_SECTION_NAME = 127
  }
}


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

/**
 * Allows a new DynamicValue to be derived from an existing DynamicValue, with modifications
 * applied.
 *
 * @param <I> The type provided
 * @param <O> The type held by the DynamicValue from which this is derived </O></I>
 */
class DerivedDynamicValue<I, O>(dynamicValue: DynamicValue<I>, modifier: Modifier<I, O>) :
    DynamicValue<O>(modifier.modify(dynamicValue.get())) {

  fun interface Modifier<in I, out O> {
    fun modify(input: I): O
  }

  init {
    dynamicValue.attachListener { set(modifier.modify(dynamicValue.get())) }
  }
}