/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.app.Activity
import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.util.DisplayMetrics

object ActivityDescriptor : ChainedDescriptor<Activity>() {

  override fun onGetBounds(node: Activity): Bounds = DisplayMetrics.getDisplayBounds()

  override fun onGetName(node: Activity): String {
    return node.javaClass.simpleName
  }

  override fun onGetChildren(node: Activity): List<Any> {
    val children = mutableListOf<Any>()

    node.window?.let { window -> children.add(window) }

    val fragments = FragmentTracker.getDialogFragments(node)
    fragments.forEach { fragment -> children.add(fragment) }

    return children
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.app.Activity
import android.view.View
import android.view.ViewGroup
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.util.DisplayMetrics

object ApplicationRefDescriptor : ChainedDescriptor<ApplicationRef>() {

  override fun onGetActiveChild(node: ApplicationRef): Any? {
    val children = onGetChildren(node)
    if (children.isNotEmpty()) {
      val last = children.last()
      if (last.javaClass.simpleName.contains("OverlayHandlerView")) {
        return children.getOrNull(children.size - 2)
      }
      return last
    }

    return null
  }

  override fun onGetBounds(node: ApplicationRef): Bounds = DisplayMetrics.getDisplayBounds()

  override fun onGetName(node: ApplicationRef): String {
    val applicationInfo = node.application.applicationInfo
    val stringId = applicationInfo.labelRes
    return if (stringId == 0) applicationInfo.nonLocalizedLabel.toString()
    else node.application.getString(stringId)
  }

  override fun onGetChildren(node: ApplicationRef): List<Any> {
    val children = mutableListOf<Any>()

    val activeRoots = node.rootsResolver.rootViews()

    val decorViewToActivity: Map<View, Activity> =
        node.activitiesStack.toList().map { it.window.decorView to it }.toMap()

    for (root in activeRoots) {
      // if there is an activity for this root view use that,
      // if not just return the mystery floating decor view
      val activity = decorViewToActivity[root]
      if (activity != null) {
        children.add(activity)
      } else {
        if (root is ViewGroup && root.childCount > 0) {
          // sometimes there is a root view on top that has no children and we dont want to add
          // these as they will become active
          children.add(root)
        }
      }
    }

    return children
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.Immediate
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred

/**
 * A chained descriptor is a special type of descriptor that models the inheritance hierarchy in
 * native UI frameworks. With this setup you can define a descriptor for each class in the
 * inheritance chain and given that we record the super class's descriptor we can automatically
 * aggregate the results for each descriptor in the inheritance hierarchy in a chain.
 *
 * The result is that each descriptor in the inheritance chain only exports the attributes that it
 * knows about but we still get all the attributes from the parent classes
 */
abstract class ChainedDescriptor<T> : NodeDescriptor<T> {
  private var mSuper: ChainedDescriptor<T>? = null

  fun setSuper(superDescriptor: ChainedDescriptor<T>) {
    if (superDescriptor !== mSuper) {
      check(mSuper == null)
      mSuper = superDescriptor
    }
  }

  fun getSuper(): ChainedDescriptor<T>? {
    return mSuper
  }

  final override fun getActiveChild(node: T): Any? {
    // ask each descriptor in the chain for an active child, if none available look up the chain
    // until no more super descriptors
    return onGetActiveChild(node) ?: mSuper?.getActiveChild(node)
  }

  /**
   * The name used to identify this node in the inspector. Does not need to be unique. A good
   * default is to use the class name of the node.
   */
  final override fun getName(node: T): String {
    return onGetName(node)
  }

  final override fun getQualifiedName(node: T): String {
    node?.let { n ->
      return n::class.qualifiedName ?: ""
    }
    return ""
  }

  final override fun getTags(node: T): Set<String> {
    val tags = onGetTags(node) ?: mSuper?.getTags(node)
    return tags ?: setOf()
  }

  open fun onGetTags(node: T): Set<String>? = null

  open fun onGetActiveChild(node: T): Any? = null

  abstract fun onGetName(node: T): String

  final override fun getBounds(node: T): Bounds {
    val bounds = onGetBounds(node) ?: mSuper?.getBounds(node)
    return bounds ?: Bounds(0, 0, 0, 0)
  }

  open fun onGetBounds(node: T): Bounds? = null

  final override fun getChildren(node: T): List<Any> {
    val children = onGetChildren(node) ?: mSuper?.getChildren(node)
    return children ?: listOf()
  }

  open fun onGetChildren(node: T): List<Any>? = null

  final override fun getAttributes(node: T): MaybeDeferred<Map<MetadataId, InspectableObject>> {
    val builder = mutableMapOf<MetadataId, InspectableObject>()
    onGetAttributes(node, builder)

    var curDescriptor: ChainedDescriptor<T>? = mSuper

    while (curDescriptor != null) {
      curDescriptor.onGetAttributes(node, builder)
      curDescriptor = curDescriptor.mSuper
    }

    return Immediate(builder)
  }

  /**
   * Get the data to show for this node in the sidebar of the inspector. Each key will be a have its
   * own section
   */
  open fun onGetAttributes(node: T, attributeSections: MutableMap<MetadataId, InspectableObject>) {}

  /** Get a snapshot of the node. */
  final override fun getSnapshot(node: T, bitmap: Bitmap?): Bitmap? {
    return onGetSnapshot(node, bitmap) ?: mSuper?.onGetSnapshot(node, bitmap)
  }

  open fun onGetSnapshot(node: T, bitmap: Bitmap?): Bitmap? {
    return null
  }

  final override fun getInlineAttributes(node: T): Map<String, String> {

    val builder = mutableMapOf<String, String>()
    onGetInlineAttributes(node, builder)

    var curDescriptor: ChainedDescriptor<T>? = mSuper

    while (curDescriptor != null) {
      curDescriptor.onGetInlineAttributes(node, builder)
      curDescriptor = curDescriptor.mSuper
    }

    return builder
  }

  open fun onGetInlineAttributes(node: T, attributes: MutableMap<String, String>) {}
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.graphics.drawable.ColorDrawable
import com.facebook.flipper.plugins.uidebugger.model.*

object ColorDrawableDescriptor : ChainedDescriptor<ColorDrawable>() {

  private const val NAMESPACE = "ColorDrawable"
  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
  private var ColorAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "color")

  override fun onGetName(node: ColorDrawable): String = node.javaClass.simpleName

  override fun onGetAttributes(
      node: ColorDrawable,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {
    val props = mutableMapOf<Int, Inspectable>()
    props[ColorAttributeId] = InspectableValue.Color(Color.fromColor(node.color))

    attributeSections[SectionId] = InspectableObject(props.toMap())
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.app.Activity
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.ImageView
import android.widget.TextView
import androidx.viewpager.widget.ViewPager
import com.facebook.flipper.plugins.uidebugger.common.UIDebuggerException
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef

class DescriptorRegister {
  private val register: MutableMap<Class<*>, NodeDescriptor<*>> = HashMap()

  companion object {

    fun withDefaults(): DescriptorRegister {
      val mapping = DescriptorRegister()
      mapping.register(Any::class.java, ObjectDescriptor)
      mapping.register(ApplicationRef::class.java, ApplicationRefDescriptor)
      mapping.register(Activity::class.java, ActivityDescriptor)
      mapping.register(Window::class.java, WindowDescriptor)
      mapping.register(ViewGroup::class.java, ViewGroupDescriptor)
      mapping.register(View::class.java, ViewDescriptor)
      mapping.register(TextView::class.java, TextViewDescriptor)
      mapping.register(ImageView::class.java, ImageViewDescriptor)
      mapping.register(ViewPager::class.java, ViewPagerDescriptor)
      mapping.register(Drawable::class.java, DrawableDescriptor)
      mapping.register(ColorDrawable::class.java, ColorDrawableDescriptor)
      mapping.register(OffsetChild::class.java, OffsetChildDescriptor)
      mapping.register(android.app.Fragment::class.java, FragmentFrameworkDescriptor(mapping))
      mapping.register(
          androidx.fragment.app.Fragment::class.java, FragmentSupportDescriptor(mapping))

      return mapping
    }
  }

  fun <T> register(clazz: Class<T>, descriptor: NodeDescriptor<T>) {
    register[clazz] = descriptor
    setSuper()
  }

  private fun setSuper() {
    @Suppress("unchecked_cast")
    for (clazz in this.register.keys) {
      val maybeDescriptor: NodeDescriptor<*>? = this.register[clazz]
      maybeDescriptor?.let { descriptor ->
        if (descriptor is ChainedDescriptor<*>) {

          val chainedDescriptor = descriptor as ChainedDescriptor<Any>
          val superClass: Class<*> = clazz.superclass

          val maybeSuperDescriptor: NodeDescriptor<*>? = this.descriptorForClass(superClass)

          maybeSuperDescriptor?.let { superDescriptor ->
            if (superDescriptor is ChainedDescriptor<*>) {
              chainedDescriptor.setSuper(superDescriptor as ChainedDescriptor<Any>)
            }
          }
        }
      }
    }
  }

  fun <T> descriptorForClass(clazz: Class<T>): NodeDescriptor<T>? {
    var mutableClass: Class<*> = clazz
    while (!register.containsKey(mutableClass)) {
      mutableClass = mutableClass.superclass
    }

    return if (register[mutableClass] != null) {
      @Suppress("unchecked_cast")
      register[mutableClass] as NodeDescriptor<T>
    } else {
      null
    }
  }

  fun <T> descriptorForClassUnsafe(clazz: Class<T>): NodeDescriptor<T> {
    return descriptorForClass(clazz)
        ?: throw UIDebuggerException("No descriptor found for ${clazz.name}")
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.graphics.drawable.Drawable
import android.os.Build
import com.facebook.flipper.plugins.uidebugger.model.*

object DrawableDescriptor : ChainedDescriptor<Drawable>() {

  private const val NAMESPACE = "Drawable"
  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
  private var AlphaAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "alpha")
  private var BoundsAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "bounds")

  override fun onGetName(node: Drawable): String = node.javaClass.simpleName

  override fun onGetBounds(node: Drawable): Bounds =
      Bounds(node.bounds.left, node.bounds.top, node.bounds.width(), node.bounds.height())

  override fun onGetAttributes(
      node: Drawable,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      val props = mutableMapOf<Int, Inspectable>()
      props[AlphaAttributeId] = InspectableValue.Number(node.alpha)

      val bounds = node.bounds
      props[BoundsAttributeId] = InspectableValue.Bounds(Bounds.fromRect(bounds))

      attributeSections[SectionId] = InspectableObject(props.toMap())
    }
  }

  override fun onGetTags(node: Drawable): Set<String> = BaseTags.NativeAndroid
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.os.Bundle
import androidx.fragment.app.Fragment
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import com.facebook.flipper.plugins.uidebugger.model.MetadataId

class FragmentFrameworkDescriptor(val register: DescriptorRegister) :
    ChainedDescriptor<android.app.Fragment>() {

  private val NAMESPACE = "Fragment"

  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)

  override fun onGetName(node: android.app.Fragment): String {
    return node.javaClass.simpleName
  }

  override fun onGetBounds(node: android.app.Fragment): Bounds? = Bounds(0, 0, 0, 0)

  override fun onGetChildren(node: android.app.Fragment): List<Any> =
      node.view?.let { view -> listOf(view) } ?: listOf()

  override fun onGetAttributes(
      node: android.app.Fragment,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {
    val args: Bundle = node.arguments

    val props = mutableMapOf<Int, Inspectable>()
    for (key in args.keySet()) {
      val metadata = MetadataRegister.get(NAMESPACE, key)
      val identifier =
          metadata?.id ?: MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, key)

      when (val value = args[key]) {
        is Number -> props[identifier] = InspectableValue.Number(value)
        is Boolean -> props[identifier] = InspectableValue.Boolean(value)
        is String -> props[identifier] = InspectableValue.Text(value)
      }
    }

    attributeSections[SectionId] = InspectableObject(props.toMap())
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import androidx.fragment.app.Fragment
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import com.facebook.flipper.plugins.uidebugger.model.MetadataId

class FragmentSupportDescriptor(val register: DescriptorRegister) :
    ChainedDescriptor<androidx.fragment.app.Fragment>() {

  private val NAMESPACE = "Fragment"

  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)

  private var IsInLayout =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isInLayout")

  private var IsAdded =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isAdded")
  private var IsDetatched =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isDetached")
  private var IsHidden =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isHidden")
  private var IsVisible =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isVisible")

  private var IsResumed =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isResumed")

  private var IsMenuVisible =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isMenuVisible")

  private var Arguements =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "arguements")

  override fun onGetName(node: androidx.fragment.app.Fragment): String {
    return node.javaClass.simpleName
  }

  override fun onGetBounds(node: Fragment): Bounds = Bounds(0, 0, 0, 0)

  override fun onGetChildren(node: androidx.fragment.app.Fragment): List<Any> {
    val view = node.view
    return if (view != null && node.isVisible) {
      listOf(view)
    } else {
      listOf()
    }
  }

  override fun onGetAttributes(
      node: androidx.fragment.app.Fragment,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {

    val props = mutableMapOf<Int, Inspectable>()

    props[IsInLayout] = InspectableValue.Boolean(node.isInLayout)
    props[IsAdded] = InspectableValue.Boolean(node.isAdded)
    props[IsDetatched] = InspectableValue.Boolean(node.isDetached)
    props[IsHidden] = InspectableValue.Boolean(node.isHidden)
    props[IsVisible] = InspectableValue.Boolean(node.isVisible)
    props[IsResumed] = InspectableValue.Boolean(node.isResumed)
    props[IsMenuVisible] = InspectableValue.Boolean(node.isMenuVisible)

    val arguements = mutableMapOf<MetadataId, Inspectable>()
    val args = node.arguments
    args?.let { bundle ->
      for (key in bundle.keySet()) {
        val metadata = MetadataRegister.get(NAMESPACE, key)
        val identifier =
            metadata?.id
                ?: MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, key)
        when (val value = bundle[key]) {
          is Number -> arguements[identifier] = InspectableValue.Number(value)
          is Boolean -> arguements[identifier] = InspectableValue.Boolean(value)
          is String -> arguements[identifier] = InspectableValue.Text(value)
        }
      }
    }
    if (arguements.size > 0) {
      props[Arguements] = InspectableObject(arguements)
    }
    attributeSections[SectionId] = InspectableObject(props)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.widget.ImageView
import android.widget.ImageView.ScaleType
import com.facebook.flipper.plugins.uidebugger.common.EnumMapping
import com.facebook.flipper.plugins.uidebugger.common.enumMapping
import com.facebook.flipper.plugins.uidebugger.common.enumToInspectableSet
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId

object ImageViewDescriptor : ChainedDescriptor<ImageView>() {

  private const val NAMESPACE = "ImageView"

  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
  private var ScaleTypeAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "scaleType",
          false,
          enumToInspectableSet<ScaleType>())

  override fun onGetName(node: ImageView): String = node.javaClass.simpleName

  override fun onGetAttributes(
      node: ImageView,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {
    val props = mutableMapOf<Int, Inspectable>()
    props[ScaleTypeAttributeId] = scaleTypeMapping.toInspectable(node.scaleType)

    attributeSections[SectionId] = InspectableObject(props)
  }

  private val scaleTypeMapping: EnumMapping<ScaleType> = enumMapping<ScaleType>()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import com.facebook.flipper.plugins.uidebugger.model.Metadata
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import kotlinx.serialization.json.JsonPrimitive

/**
 * Registry of attribute metadata. There's two types of attributes:
 * - Static attributes. Those that are known at build time.
 * - Dynamic attributes. Those that are known at runtime.
 */
object MetadataRegister {

  const val TYPE_IDENTITY = "identity"
  const val TYPE_ATTRIBUTE = "attribute"

  private val lock = "lock"

  private var generator: MetadataId = 0
  private val register: MutableMap<String, Metadata> = mutableMapOf()
  private val pendingKeys: MutableSet<String> = mutableSetOf()

  private fun key(namespace: String, name: String): String = "${namespace}_$name"

  fun register(
      type: String,
      namespace: String,
      name: String,
      mutable: Boolean = false,
      possibleValues: Set<InspectableValue>? = null,
      customAttributes: Map<String, JsonPrimitive>? = null
  ): MetadataId {
    val key = key(namespace, name)
    register[key]?.let { m ->
      return m.id
    }

    synchronized(lock) {
      val identifier = ++generator
      val metadata =
          Metadata(identifier, type, namespace, name, mutable, possibleValues, customAttributes)

      register[key] = metadata
      pendingKeys.add(key)

      return identifier
    }
  }

  fun get(namespace: String, name: String): Metadata? {
    val key = key(namespace, name)
    return register[key]
  }

  /** gets all pending metadata to be sent and resets the pending list */
  fun extractPendingMetadata(): Map<MetadataId, Metadata> {
    synchronized(lock) {
      val pendingMetadata: MutableMap<MetadataId, Metadata> = mutableMapOf()

      pendingKeys.forEach { key ->
        register[key]?.let { metadata -> pendingMetadata[metadata.id] = metadata }
      }
      pendingKeys.clear()

      return pendingMetadata
    }
  }

  fun reset() {
    synchronized(lock) {
      pendingKeys.clear()
      register.forEach { entry -> pendingKeys.add(entry.key) }
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
import kotlinx.serialization.json.JsonObject

/*
 Descriptors are an extension point used during traversal to extract data out of arbitrary
 objects in the hierarchy. Descriptors can represent native view or declarative components or
 any type of object such as an activity

 Descriptors should be stateless and each descriptor should be a singleton
*/

object BaseTags {

  const val Declarative = "Declarative"
  const val Native = "Native"
  const val Accessibility = "Accessibility"
  const val Android = "Android"
  const val Unknown = "Unknown"
  val NativeAndroid = setOf(Android, Native)
  val AccessibleNativeAndroid = setOf(Android, Native, Accessibility)
}

typealias Id = Int

interface NodeDescriptor<T> {

  /** Used to detect the same node across traversals */
  fun getId(node: T): Id = System.identityHashCode(node)

  /** Should be w.r.t the direct parent */
  fun getBounds(node: T): Bounds

  /**
   * The name used to identify this node in the inspector. Does not need to be unique. A good
   * default is to use the class name of the node.
   */
  fun getName(node: T): String

  /**
   * The qualified name used to identify this node in the inspector. Does not need to be unique. A
   * good default is to use the class qualified name of the node.
   */
  fun getQualifiedName(node: T): String

  /** The children this node exposes in the inspector. */
  fun getChildren(node: T): List<Any>

  /**
   * Get a snapshot of the node. Bitmaps are not cheap to create, so accept one as an optional
   * parameter. If a bitmap is provided, it will be used by the canvas to draw on it. Otherwise, a
   * bitmap will be created.
   */
  fun getSnapshot(node: T, bitmap: Bitmap?): Bitmap? = null

  /**
   * If you have overlapping children this indicates which child is active / on top, we will only
   * listen to / traverse this child. If return null we assume all children are 'active'
   */
  fun getActiveChild(node: T): Any?

  /**
   * Get the attribute to show for this node in the sidebar of the inspector. The object first level
   * is a section and subsequent objects within are the first level of that section. Nested objects
   * will nest in the sidebar
   */
  fun getAttributes(node: T): MaybeDeferred<Map<MetadataId, InspectableObject>>

  /**
   * Set of tags to describe this node in an abstract way for the UI Unfortunately this can't be an
   * enum as we have to plugin 3rd party frameworks dynamically
   */
  fun getTags(node: T): Set<String>

  /**
   * These are shown inline in the tree view on the desktop, will likely be removed in the future
   */
  fun getInlineAttributes(node: T): Map<String, String> = mapOf()

  fun getHiddenAttributes(node: T): JsonObject? = null
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.Immediate

object ObjectDescriptor : NodeDescriptor<Any> {

  override fun getActiveChild(node: Any): Any? = null

  override fun getName(node: Any): String {
    return node.javaClass.simpleName
  }

  override fun getQualifiedName(node: Any): String {
    return node::class.qualifiedName ?: ""
  }

  override fun getChildren(node: Any) = listOf<Any>()

  override fun getAttributes(node: Any) = Immediate(mapOf<MetadataId, InspectableObject>())

  override fun getBounds(node: Any): Bounds = Bounds(0, 0, 0, 0)

  override fun getTags(node: Any): Set<String> = setOf(BaseTags.Unknown)

  override fun getSnapshot(node: Any, bitmap: Bitmap?): Bitmap? = null
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.graphics.Bitmap
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred

/** a drawable or view that is mounted, along with the correct descriptor */
class OffsetChild(val child: Any, val descriptor: NodeDescriptor<Any>, val x: Int, val y: Int) {
  companion object {
    fun zero(child: Any, descriptor: NodeDescriptor<Any>) = OffsetChild(child, descriptor, 0, 0)
  }
}

object OffsetChildDescriptor : NodeDescriptor<OffsetChild> {

  override fun getId(node: OffsetChild): Id = node.descriptor.getId(node.child)

  override fun getBounds(node: OffsetChild): Bounds {
    val bounds = node.descriptor.getBounds(node.child)
    return Bounds(node.x, node.y, bounds.width, bounds.height)
  }

  override fun getName(node: OffsetChild): String = node.descriptor.getName(node.child)

  override fun getQualifiedName(node: OffsetChild): String =
      node.descriptor.getQualifiedName(node.child)

  override fun getChildren(node: OffsetChild): List<Any> = node.descriptor.getChildren(node.child)

  override fun getActiveChild(node: OffsetChild): Any? = node.descriptor.getActiveChild(node.child)

  override fun getAttributes(node: OffsetChild): MaybeDeferred<Map<MetadataId, InspectableObject>> =
      node.descriptor.getAttributes(node.child)

  override fun getTags(node: OffsetChild): Set<String> = node.descriptor.getTags(node.child)

  override fun getSnapshot(node: OffsetChild, bitmap: Bitmap?): Bitmap? =
      node.descriptor.getSnapshot(node.child, bitmap)
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.os.Build
import android.widget.TextView
import com.facebook.flipper.plugins.uidebugger.model.Color
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import com.facebook.flipper.plugins.uidebugger.model.MetadataId

object TextViewDescriptor : ChainedDescriptor<TextView>() {

  private const val NAMESPACE = "TextView"

  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
  private val TextAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "text")
  private val TextSizeAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "textSize")
  private val TextColorAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "textColor")
  private val IsBoldAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isBold")
  private val IsItalicAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "isItalic")
  private val WeightAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "weight")
  private val TypefaceAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "typeface")
  private val MinLinesAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "minLines")
  private val MaxLinesAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "maxLines")
  private val MinWidthAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "minWidth")
  private val MaxWidthAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "maxWidth")

  override fun onGetName(node: TextView): String = node.javaClass.simpleName

  override fun onGetAttributes(
      node: TextView,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {

    val props =
        mutableMapOf<Int, Inspectable>(
            TextAttributeId to InspectableValue.Text(node.text.toString()),
            TextSizeAttributeId to InspectableValue.Number(node.textSize),
            TextColorAttributeId to
                InspectableValue.Color(Color.fromColor(node.textColors.defaultColor)))

    val typeface = node.typeface
    if (typeface != null) {
      val typeFaceProp =
          mutableMapOf<Int, InspectableValue>(
              IsBoldAttributeId to InspectableValue.Boolean(typeface.isBold),
              IsItalicAttributeId to InspectableValue.Boolean(typeface.isItalic),
          )

      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        typeFaceProp[WeightAttributeId] = InspectableValue.Number(typeface.weight)
      }

      props[TypefaceAttributeId] = InspectableObject(typeFaceProp)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
      props[MinLinesAttributeId] = InspectableValue.Number(node.minLines)
      props[MaxLinesAttributeId] = InspectableValue.Number(node.maxLines)
      props[MinWidthAttributeId] = InspectableValue.Number(node.minWidth)
      props[MaxWidthAttributeId] = InspectableValue.Number(node.maxWidth)
    }

    attributeSections[SectionId] = InspectableObject(props)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.SparseArray
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.viewpager.widget.ViewPager
import com.facebook.flipper.plugins.uidebugger.common.*
import com.facebook.flipper.plugins.uidebugger.model.*
import com.facebook.flipper.plugins.uidebugger.util.ResourcesUtil
import java.lang.reflect.Field

object ViewDescriptor : ChainedDescriptor<View>() {

  private val LayoutParamsMapping: EnumMapping<Int> =
      object :
          EnumMapping<Int>(
              mapOf(
                  "WRAP_CONTENT" to ViewGroup.LayoutParams.WRAP_CONTENT,
                  "MATCH_PARENT" to ViewGroup.LayoutParams.MATCH_PARENT,
                  "FILL_PARENT" to ViewGroup.LayoutParams.FILL_PARENT,
              )) {}

  private val VisibilityMapping: EnumMapping<Int> =
      object :
          EnumMapping<Int>(
              mapOf(
                  "VISIBLE" to View.VISIBLE,
                  "INVISIBLE" to View.INVISIBLE,
                  "GONE" to View.GONE,
              )) {}

  private val LayoutDirectionMapping: EnumMapping<Int> =
      when {
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> {
          object :
              EnumMapping<Int>(
                  mapOf(
                      "LAYOUT_DIRECTION_INHERIT" to View.LAYOUT_DIRECTION_INHERIT,
                      "LAYOUT_DIRECTION_LOCALE" to View.LAYOUT_DIRECTION_LOCALE,
                      "LAYOUT_DIRECTION_LTR" to View.LAYOUT_DIRECTION_LTR,
                      "LAYOUT_DIRECTION_RTL" to View.LAYOUT_DIRECTION_RTL,
                  )) {}
        }
        else -> {
          object : EnumMapping<Int>(emptyMap()) {}
        }
      }

  private val TextDirectionMapping: EnumMapping<Int> =
      when {
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> {
          object :
              EnumMapping<Int>(
                  mapOf(
                      "TEXT_DIRECTION_INHERIT" to View.TEXT_DIRECTION_INHERIT,
                      "TEXT_DIRECTION_FIRST_STRONG" to View.TEXT_DIRECTION_FIRST_STRONG,
                      "TEXT_DIRECTION_ANY_RTL" to View.TEXT_DIRECTION_ANY_RTL,
                      "TEXT_DIRECTION_LTR" to View.TEXT_DIRECTION_LTR,
                      "TEXT_DIRECTION_RTL" to View.TEXT_DIRECTION_RTL,
                      "TEXT_DIRECTION_LOCALE" to View.TEXT_DIRECTION_LOCALE,
                  )) {}
        }
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
          object :
              EnumMapping<Int>(
                  mapOf(
                      "TEXT_DIRECTION_INHERIT" to View.TEXT_DIRECTION_INHERIT,
                      "TEXT_DIRECTION_FIRST_STRONG" to View.TEXT_DIRECTION_FIRST_STRONG,
                      "TEXT_DIRECTION_ANY_RTL" to View.TEXT_DIRECTION_ANY_RTL,
                      "TEXT_DIRECTION_LTR" to View.TEXT_DIRECTION_LTR,
                      "TEXT_DIRECTION_RTL" to View.TEXT_DIRECTION_RTL,
                      "TEXT_DIRECTION_LOCALE" to View.TEXT_DIRECTION_LOCALE,
                      "TEXT_DIRECTION_FIRST_STRONG_LTR" to View.TEXT_DIRECTION_FIRST_STRONG_LTR,
                      "TEXT_DIRECTION_FIRST_STRONG_RTL" to View.TEXT_DIRECTION_FIRST_STRONG_RTL,
                  )) {}
        }
        else -> {
          object : EnumMapping<Int>(emptyMap()) {}
        }
      }

  private val TextAlignmentMapping: EnumMapping<Int> =
      when {
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> {
          object :
              EnumMapping<Int>(
                  mapOf(
                      "TEXT_ALIGNMENT_INHERIT" to View.TEXT_ALIGNMENT_INHERIT,
                      "TEXT_ALIGNMENT_GRAVITY" to View.TEXT_ALIGNMENT_GRAVITY,
                      "TEXT_ALIGNMENT_TEXT_START" to View.TEXT_ALIGNMENT_TEXT_START,
                      "TEXT_ALIGNMENT_TEXT_END" to View.TEXT_ALIGNMENT_TEXT_END,
                      "TEXT_ALIGNMENT_CENTER" to View.TEXT_ALIGNMENT_CENTER,
                      "TEXT_ALIGNMENT_VIEW_START" to View.TEXT_ALIGNMENT_VIEW_START,
                      "TEXT_ALIGNMENT_VIEW_END" to View.TEXT_ALIGNMENT_VIEW_END,
                  )) {}
        }
        else -> {
          object : EnumMapping<Int>(emptyMap()) {}
        }
      }

  private val GravityMapping =
      object :
          EnumMapping<Int>(
              mapOf(
                  "NONE" to -1,
                  "NO_GRAVITY" to Gravity.NO_GRAVITY,
                  "LEFT" to Gravity.LEFT,
                  "TOP" to Gravity.TOP,
                  "RIGHT" to Gravity.RIGHT,
                  "BOTTOM" to Gravity.BOTTOM,
                  "CENTER" to Gravity.CENTER,
                  "CENTER_VERTICAL" to Gravity.CENTER_VERTICAL,
                  "FILL_VERTICAL" to Gravity.FILL_VERTICAL,
                  "CENTER_HORIZONTAL" to Gravity.CENTER_HORIZONTAL,
                  "FILL_HORIZONTAL" to Gravity.FILL_HORIZONTAL,
              )) {}

  private const val NAMESPACE = "View"

  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
  private val PositionAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "position")
  private val GlobalPositionAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "globalPosition")
  private val SizeAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "size")
  private val BoundsAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "bounds")
  private val PaddingAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "padding")
  private val LocalVisibleRectAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "localVisibleRect")
  private val RotationAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "rotation")
  private val ScaleAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "scale")
  private val PivotAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "pivot")
  private val ScrollAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "scroll")
  private val LayoutParamsAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "layoutParams")
  private val LayoutDirectionAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "layoutDirection",
          false,
          LayoutDirectionMapping.getInspectableValues())
  private val TranslationAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "translation")
  private val ElevationAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "elevation")
  private val VisibilityAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "visibility",
          false,
          VisibilityMapping.getInspectableValues())

  private val BackgroundAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "background")
  private val ForegroundAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "foreground")

  private val AlphaAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "alpha")
  private val StateAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "state")

  private val StateEnabledAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "enabled")
  private val StateActivatedAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "activated")
  private val StateFocusedAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "focused")
  private val StateSelectedAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "selected")

  private val TextDirectionAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "textDirection",
          false,
          TextDirectionMapping.getInspectableValues())
  private val TextAlignmentAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "textAlignment",
          false,
          TextAlignmentMapping.getInspectableValues())

  private val TagAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "tag")
  private val KeyedTagsAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "keyedTags")

  private val WidthAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "width",
          false,
          LayoutParamsMapping.getInspectableValues())
  private val HeightAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "height",
          false,
          LayoutParamsMapping.getInspectableValues())

  private val MarginAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "margin")
  private val WeightAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "weight")
  private val GravityAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "gravity",
          false,
          GravityMapping.getInspectableValues())

  override fun onGetName(node: View): String = node.javaClass.simpleName

  override fun onGetBounds(node: View): Bounds {

    val parent = node.parent
    if (parent is ViewPager) {
      // override
      return Bounds(0, 0, node.width, node.height)
    }

    var xScrollOffset = 0
    var yScrollOffset = 0

    if (parent is View) {
      // NestedScrollView, HorizontalScrollView and ScrollView all set scroll x and y
      // we need to account for this in the childs bounds
      xScrollOffset = parent.scrollX
      yScrollOffset = parent.scrollY
    }

    return Bounds(
        node.left + node.translationX.toInt() - xScrollOffset,
        node.top + node.translationY.toInt() - yScrollOffset,
        node.width,
        node.height)
  }

  override fun onGetTags(node: View): Set<String> = BaseTags.NativeAndroid

  override fun onGetAttributes(
      node: View,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {

    val props = mutableMapOf<Int, Inspectable>()

    val positionOnScreen = IntArray(2)
    node.getLocationOnScreen(positionOnScreen)

    val localVisible = Rect()
    node.getLocalVisibleRect(localVisible)

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
      props[PositionAttributeId] =
          InspectableValue.Coordinate3D(Coordinate3D(node.x, node.y, node.z))
    } else {
      props[PositionAttributeId] = InspectableValue.Coordinate(Coordinate(node.x, node.y))
    }

    props[GlobalPositionAttributeId] =
        InspectableValue.Coordinate(Coordinate(positionOnScreen[0], positionOnScreen[1]))

    props[SizeAttributeId] = InspectableValue.Size(Size(node.width, node.height))

    props[BoundsAttributeId] =
        InspectableValue.Bounds(Bounds(node.left, node.top, node.width, node.height))
    props[PaddingAttributeId] =
        InspectableValue.SpaceBox(
            SpaceBox(node.paddingTop, node.paddingRight, node.paddingBottom, node.paddingLeft))

    props[LocalVisibleRectAttributeId] =
        InspectableObject(
            mapOf(
                PositionAttributeId to
                    InspectableValue.Coordinate(Coordinate(localVisible.left, localVisible.top)),
                SizeAttributeId to
                    InspectableValue.Size(Size(localVisible.width(), localVisible.height()))),
        )

    props[RotationAttributeId] =
        InspectableValue.Coordinate3D(Coordinate3D(node.rotationX, node.rotationY, node.rotation))
    props[ScaleAttributeId] = InspectableValue.Coordinate(Coordinate(node.scaleX, node.scaleY))
    props[PivotAttributeId] = InspectableValue.Coordinate(Coordinate(node.pivotX, node.pivotY))

    props[ScrollAttributeId] = InspectableValue.Coordinate(Coordinate(node.scrollX, node.scrollY))

    props[LayoutParamsAttributeId] = getLayoutParams(node)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
      props[LayoutDirectionAttributeId] = LayoutDirectionMapping.toInspectable(node.layoutDirection)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      props[TranslationAttributeId] =
          InspectableValue.Coordinate3D(
              Coordinate3D(node.translationX, node.translationY, node.translationZ))
    } else {
      props[TranslationAttributeId] =
          InspectableValue.Coordinate(Coordinate(node.translationX, node.translationY))
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      props[ElevationAttributeId] = InspectableValue.Number(node.elevation)
    }

    props[VisibilityAttributeId] = VisibilityMapping.toInspectable(node.visibility)

    fromDrawable(node.background)?.let { background -> props[BackgroundAttributeId] = background }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      fromDrawable(node.foreground)?.let { foreground -> props[ForegroundAttributeId] = foreground }
    }

    props[AlphaAttributeId] = InspectableValue.Number(node.alpha)
    props[StateAttributeId] =
        InspectableObject(
            mapOf(
                StateEnabledAttributeId to InspectableValue.Boolean(node.isEnabled),
                StateActivatedAttributeId to InspectableValue.Boolean(node.isActivated),
                StateFocusedAttributeId to InspectableValue.Boolean(node.isFocused),
                StateSelectedAttributeId to InspectableValue.Boolean(node.isSelected)))

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
      props[TextDirectionAttributeId] = TextDirectionMapping.toInspectable(node.textDirection)
      props[TextAlignmentAttributeId] = TextAlignmentMapping.toInspectable(node.textAlignment)
    }

    node.tag
        ?.let { InspectableValue.fromAny(it, mutable = false) }
        ?.let { tag -> props.put(TagAttributeId, tag) }

    props[KeyedTagsAttributeId] = InspectableObject(getViewTags(node))

    attributeSections[SectionId] = InspectableObject(props.toMap())
  }

  override fun onGetInlineAttributes(node: View, attributes: MutableMap<String, String>) {
    val id = node.id
    if (id == View.NO_ID) {
      return
    }

    val value = ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id)
    attributes["id"] = value
  }

  override fun onGetSnapshot(node: View, bitmap: Bitmap?): Bitmap? {
    if (node.width <= 0 || node.height <= 0) {
      return null
    }
    var workingBitmap = bitmap

    try {
      val differentSize =
          if (bitmap != null) (node.width != bitmap.width || node.height != bitmap.height)
          else false
      if (workingBitmap == null || differentSize) {
        val viewWidth: Int = node.width
        val viewHeight: Int = node.height

        workingBitmap = BitmapPool.createBitmapWithDefaultConfig(viewWidth, viewHeight)
      }

      val canvas = Canvas(workingBitmap)
      node.draw(canvas)
    } catch (e: OutOfMemoryError) {}

    return workingBitmap
  }

  private fun fromDrawable(d: Drawable?): Inspectable? {
    return if (d is ColorDrawable) {
      InspectableValue.Color(Color.fromColor(d.color))
    } else null
  }

  private fun getLayoutParams(node: View): InspectableObject {
    val layoutParams = node.layoutParams

    val params = mutableMapOf<Int, Inspectable>()
    params[WidthAttributeId] = LayoutParamsMapping.toInspectable(layoutParams.width)
    params[HeightAttributeId] = LayoutParamsMapping.toInspectable(layoutParams.height)

    if (layoutParams is ViewGroup.MarginLayoutParams) {
      params[MarginAttributeId] =
          InspectableValue.SpaceBox(
              SpaceBox(
                  layoutParams.topMargin,
                  layoutParams.rightMargin,
                  layoutParams.bottomMargin,
                  layoutParams.leftMargin))
    }
    if (layoutParams is FrameLayout.LayoutParams) {
      params[GravityAttributeId] = GravityMapping.toInspectable(layoutParams.gravity)
    }
    if (layoutParams is LinearLayout.LayoutParams) {
      params[WeightAttributeId] = InspectableValue.Number(layoutParams.weight)
      params[GravityAttributeId] = GravityMapping.toInspectable(layoutParams.gravity)
    }
    return InspectableObject(params)
  }

  private fun getViewTags(node: View): MutableMap<Int, Inspectable> {
    val tags = mutableMapOf<Int, Inspectable>()

    KeyedTagsField?.let { field ->
      val keyedTags = field.get(node) as SparseArray<*>?
      if (keyedTags != null) {
        var i = 0
        val count = keyedTags.size()
        while (i < count) {
          val id =
              ResourcesUtil.getIdStringQuietly(node.context, node.resources, keyedTags.keyAt(i))
          keyedTags
              .valueAt(i)
              ?.let { InspectableValue.fromAny(it, false) }
              ?.let {
                val metadata = MetadataRegister.get(NAMESPACE, id)
                val identifier =
                    metadata?.id
                        ?: MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, id)
                tags.put(identifier, it)
              }
          i++
        }
      }
    }

    return tags
  }

  private var KeyedTagsField: Field? = null
  private var ListenerInfoField: Field? = null
  private var OnClickListenerField: Field? = null

  init {
    try {
      @SuppressLint("DiscouragedPrivateApi")
      KeyedTagsField = View::class.java.getDeclaredField("mKeyedTags")
      KeyedTagsField?.let { field -> field.isAccessible = true }
      @SuppressLint("DiscouragedPrivateApi")
      ListenerInfoField = View::class.java.getDeclaredField("mListenerInfo")
      ListenerInfoField?.let { field -> field.isAccessible = true }
      val viewInfoClassName = View::class.java.name + "\$ListenerInfo"
      OnClickListenerField = Class.forName(viewInfoClassName).getDeclaredField("mOnClickListener")
      OnClickListenerField?.let { field -> field.isAccessible = true }
    } catch (ignored: Exception) {}
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.os.Build
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewGroupCompat
import com.facebook.flipper.plugins.uidebugger.common.EnumMapping
import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker
import com.facebook.flipper.plugins.uidebugger.model.*

object ViewGroupDescriptor : ChainedDescriptor<ViewGroup>() {

  private val LayoutModeMapping: EnumMapping<Int> =
      object :
          EnumMapping<Int>(
              mapOf(
                  "LAYOUT_MODE_CLIP_BOUNDS" to ViewGroupCompat.LAYOUT_MODE_CLIP_BOUNDS,
                  "LAYOUT_MODE_OPTICAL_BOUNDS" to ViewGroupCompat.LAYOUT_MODE_OPTICAL_BOUNDS,
              )) {}

  private const val NAMESPACE = "ViewGroup"
  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)

  override fun onGetName(node: ViewGroup): String {
    return node.javaClass.simpleName
  }

  override fun onGetChildren(node: ViewGroup): List<Any> {
    val children = mutableListOf<Any>()

    val count = node.childCount - 1
    for (i in 0..count) {
      val child: View = node.getChildAt(i)
      val fragment = FragmentTracker.getFragment(child)

      if (fragment != null) {
        children.add(fragment)
      } else children.add(child)
    }

    return children
  }

  private val LayoutModeAttributeId =
      MetadataRegister.register(
          MetadataRegister.TYPE_ATTRIBUTE,
          NAMESPACE,
          "layoutMode",
          false,
          LayoutModeMapping.getInspectableValues())
  private val ClipChildrenAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "clipChildren")
  private val ClipToPaddingAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "clipToPadding")

  override fun onGetAttributes(
      node: ViewGroup,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {
    val props = mutableMapOf<Int, Inspectable>()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
      props[LayoutModeAttributeId] = LayoutModeMapping.toInspectable(node.layoutMode)
      props[ClipChildrenAttributeId] = InspectableValue.Boolean(node.clipChildren)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      props[ClipToPaddingAttributeId] = InspectableValue.Boolean(node.clipToPadding)
    }

    attributeSections[SectionId] = InspectableObject(props)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import androidx.viewpager.widget.ViewPager
import com.facebook.flipper.plugins.uidebugger.core.FragmentTracker
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import com.facebook.flipper.plugins.uidebugger.model.MetadataId

object ViewPagerDescriptor : ChainedDescriptor<ViewPager>() {

  private const val NAMESPACE = "ViewPager"
  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)

  override fun onGetName(node: ViewPager): String = node.javaClass.simpleName

  override fun onGetActiveChild(node: ViewPager): Any? {
    val child = node.getChildAt(node.currentItem)
    val fragment = FragmentTracker.getFragment(child)
    if (fragment != null) {
      return fragment
    }
    return child
  }

  private val CurrentItemIndexAttributeId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, "currentItemIndex")

  override fun onGetAttributes(
      node: ViewPager,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {
    val props =
        InspectableObject(
            mapOf(CurrentItemIndexAttributeId to InspectableValue.Number(node.currentItem)))

    attributeSections[SectionId] = props
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.descriptors

import android.annotation.SuppressLint
import android.util.TypedValue
import android.view.Window
import com.facebook.flipper.plugins.uidebugger.model.Bounds
import com.facebook.flipper.plugins.uidebugger.model.Color
import com.facebook.flipper.plugins.uidebugger.model.Inspectable
import com.facebook.flipper.plugins.uidebugger.model.InspectableObject
import com.facebook.flipper.plugins.uidebugger.model.InspectableValue
import com.facebook.flipper.plugins.uidebugger.model.MetadataId
import com.facebook.flipper.plugins.uidebugger.util.DisplayMetrics
import java.lang.reflect.Field

object WindowDescriptor : ChainedDescriptor<Window>() {

  private const val NAMESPACE = "Window"
  private var SectionId =
      MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, NAMESPACE)
  private var internalRStyleableClass: Class<*>? = null
  private var internalRStyleableFields: Array<Field>? = null
  private var internalRStyleableWindowField: Field? = null
  private var internalRStyleable: Any? = null

  override fun onGetName(node: Window): String {
    return node.javaClass.simpleName
  }

  override fun onGetBounds(node: Window): Bounds = DisplayMetrics.getDisplayBounds()

  override fun onGetChildren(node: Window): List<Any> = listOf(node.decorView)

  @SuppressLint("PrivateApi")
  override fun onGetAttributes(
      node: Window,
      attributeSections: MutableMap<MetadataId, InspectableObject>
  ) {
    try {
      if (internalRStyleableClass == null) {
        internalRStyleableClass = Class.forName("com.android.internal.R\$styleable")
        internalRStyleableClass?.let { clazz ->
          internalRStyleable = clazz.newInstance()
          internalRStyleableFields = clazz.declaredFields
          internalRStyleableWindowField = clazz.getDeclaredField("Window")
          internalRStyleableWindowField?.isAccessible = true
        }
      }
    } catch (e: Exception) {
      return
    }

    internalRStyleableWindowField?.let { field ->
      val windowStyleable = field[internalRStyleable] as IntArray? ?: return

      val indexToName: MutableMap<Int, String> = mutableMapOf()
      internalRStyleableFields?.forEach { f ->
        if (f.name.startsWith("Window_") && f.type == Int::class.javaPrimitiveType) {
          indexToName[f.getInt(internalRStyleable)] = f.name
        }
      }

      val props = mutableMapOf<Int, Inspectable>()

      val typedValue = TypedValue()
      for ((index, attr) in windowStyleable.withIndex()) {
        val fieldName = indexToName[index] ?: continue

        if (node.context.theme.resolveAttribute(attr, typedValue, true)) {
          // Strip 'Windows_' (length: 7) from the name.
          val name = fieldName.substring(7)

          val metadata = MetadataRegister.get(NAMESPACE, name)
          val identifier =
              metadata?.id
                  ?: MetadataRegister.register(MetadataRegister.TYPE_ATTRIBUTE, NAMESPACE, name)

          when (typedValue.type) {
            TypedValue.TYPE_STRING ->
                props[identifier] = InspectableValue.Text(typedValue.string.toString())
            TypedValue.TYPE_INT_BOOLEAN ->
                props[identifier] = InspectableValue.Boolean(typedValue.data != 0)
            TypedValue.TYPE_INT_HEX ->
                props[identifier] =
                    InspectableValue.Text("0x" + Integer.toHexString(typedValue.data))
            TypedValue.TYPE_FLOAT ->
                props[identifier] =
                    InspectableValue.Number(java.lang.Float.intBitsToFloat(typedValue.data))
            TypedValue.TYPE_REFERENCE -> {
              val resId = typedValue.data
              if (resId != 0) {
                props[identifier] =
                    InspectableValue.Text(node.context.resources.getResourceName(resId))
              }
            }
            else -> {
              if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
                  typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                try {
                  val hexColor = "#" + Integer.toHexString(typedValue.data)
                  val color = android.graphics.Color.parseColor(hexColor)
                  props[identifier] = InspectableValue.Color(Color.fromColor(color))
                } catch (e: Exception) {}
              } else if (typedValue.type >= TypedValue.TYPE_FIRST_INT &&
                  typedValue.type <= TypedValue.TYPE_LAST_INT) {
                props[identifier] = InspectableValue.Number(typedValue.data)
              }
            }
          }
        }
      }

      attributeSections[SectionId] = InspectableObject(props.toMap())
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.model

import com.facebook.flipper.plugins.uidebugger.descriptors.Id

@kotlinx.serialization.Serializable
data class InitEvent(val rootId: Id, val frameworkEventMetadata: List<FrameworkEventMetadata>) {
  companion object {
    const val name = "init"
  }
}

@kotlinx.serialization.Serializable
data class MetadataUpdateEvent(val attributeMetadata: Map<MetadataId, Metadata> = emptyMap()) {
  companion object {
    const val name = "metadataUpdate"
  }
}

@kotlinx.serialization.Serializable
data class FrameScanEvent(
    val frameTime: Long,
    val nodes: List<Node>,
    val snapshot: Snapshot?,
    val frameworkEvents: List<FrameworkEvent>?
) {
  companion object {
    const val name = "frameScan"
  }
}

@kotlinx.serialization.Serializable data class Snapshot(val nodeId: Id, val data: String)

/** Separate optional performance statistics event */
@kotlinx.serialization.Serializable
data class PerfStatsEvent(
    val txId: Long,
    val observerType: String,
    val nodesCount: Int,
    val start: Long,
    val traversalMS: Long,
    val snapshotMS: Long,
    val queuingMS: Long,
    val deferredComputationMS: Long,
    val serializationMS: Long,
    val socketMS: Long,
    val payloadSize: Int,
) {
  companion object {
    const val name = "performanceStats"
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.model

import com.facebook.flipper.plugins.uidebugger.descriptors.Id

@kotlinx.serialization.Serializable
data class FrameworkEventMetadata(
    val type: String,
    val documentation: String,
)

@kotlinx.serialization.Serializable
data class FrameworkEvent(
    val nodeId: Id,
    val type: String,
    val timestamp: Long,
)


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.model

import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@kotlinx.serialization.Serializable sealed class Inspectable {}

// In this context, mutable means you can add/remove items,
// for native android this should probably be false.
@SerialName("array")
@Serializable
data class InspectableArray(val items: List<Inspectable>) : Inspectable()

// In this context, mutable means you can add / remove keys,
// for native android this should probably be false.
@SerialName("object")
@Serializable
data class InspectableObject(val fields: Map<Int, Inspectable>) : Inspectable()

@kotlinx.serialization.Serializable
sealed class InspectableValue : Inspectable() {

  @kotlinx.serialization.Serializable
  @SerialName("text")
  class Text(val value: String) : InspectableValue()

  @kotlinx.serialization.Serializable
  @SerialName("boolean")
  class Boolean(val value: kotlin.Boolean) : InspectableValue()

  @SerialName("number")
  @kotlinx.serialization.Serializable
  data class Number(
      @Serializable(with = NumberSerializer::class) val value: kotlin.Number,
  ) : InspectableValue()

  @SerialName("color")
  @kotlinx.serialization.Serializable
  data class Color(
      val value: com.facebook.flipper.plugins.uidebugger.model.Color,
  ) : InspectableValue()

  @SerialName("coordinate")
  @kotlinx.serialization.Serializable
  data class Coordinate(
      val value: com.facebook.flipper.plugins.uidebugger.model.Coordinate,
  ) : InspectableValue()

  @SerialName("coordinate3d")
  @kotlinx.serialization.Serializable
  data class Coordinate3D(
      val value: com.facebook.flipper.plugins.uidebugger.model.Coordinate3D,
  ) : InspectableValue()

  @SerialName("size")
  @kotlinx.serialization.Serializable
  data class Size(
      val value: com.facebook.flipper.plugins.uidebugger.model.Size,
  ) : InspectableValue()

  @SerialName("bounds")
  @kotlinx.serialization.Serializable
  data class Bounds(
      val value: com.facebook.flipper.plugins.uidebugger.model.Bounds,
  ) : InspectableValue()

  @SerialName("space")
  @kotlinx.serialization.Serializable
  data class SpaceBox(
      val value: com.facebook.flipper.plugins.uidebugger.model.SpaceBox,
  ) : InspectableValue()

  @SerialName("enum")
  @kotlinx.serialization.Serializable
  data class Enum(val value: String) : InspectableValue()

  @SerialName("unknown")
  @kotlinx.serialization.Serializable
  data class Unknown(val value: String?) : InspectableValue() {}

  companion object {
    /**
     * Will attempt to convert Any ref to a suitable primitive inspectable value. Only use if you
     * are dealing with an Any / object type. Prefer the specific constructors
     */
    fun fromAny(any: Any, mutable: kotlin.Boolean = false): Inspectable? {
      return when (any) {
        is kotlin.Number -> InspectableValue.Number(any)
        is kotlin.Boolean -> InspectableValue.Boolean(any)
        is kotlin.String -> InspectableValue.Text(any)
        else -> null
      }
    }
  }
}

object NumberSerializer : KSerializer<Number> {
  override val descriptor: SerialDescriptor =
      PrimitiveSerialDescriptor("com.meta.NumberSerializer", PrimitiveKind.DOUBLE)

  override fun serialize(encoder: Encoder, value: Number) {
    when (value) {
      is Double -> encoder.encodeDouble(value.toDouble())
      is Float -> encoder.encodeFloat(value.toFloat())
      is Long -> encoder.encodeLong(value.toLong())
      is Int -> encoder.encodeInt(value.toInt())
    }
  }

  override fun deserialize(decoder: Decoder): Number {
    return decoder.decodeFloat()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.model

import kotlinx.serialization.json.JsonPrimitive

typealias MetadataId = Int

/**
 * Represent metadata associated for an attribute. MetadataId is a unique identifier used by
 * attributes to refer to its metadata. Type refers to attribute semantics. It can represent:
 * identity, attributes, layout, documentation, or a custom type.
 */
@kotlinx.serialization.Serializable
data class Metadata(
    val id: MetadataId,
    val type: String,
    val namespace: String,
    val name: String,
    val mutable: kotlin.Boolean,
    val possibleValues: Set<InspectableValue>? = emptySet(),
    val customAttributes: Map<String, JsonPrimitive>? = null
)


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.model

import com.facebook.flipper.plugins.uidebugger.descriptors.Id
import kotlinx.serialization.json.JsonObject

@kotlinx.serialization.Serializable
data class Node(
    val id: Id,
    val parent: Id?,
    val qualifiedName: String,
    val name: String,
    val attributes: Map<MetadataId, InspectableObject>,
    val inlineAttributes: Map<String, String>,
    val hiddenAttributes: JsonObject?,
    val bounds: Bounds,
    val tags: Set<String>,
    val children: List<Id>,
    val activeChild: Id?,
)


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.model

import android.graphics.Rect
import kotlinx.serialization.Serializable

@kotlinx.serialization.Serializable
data class Bounds(val x: Int, val y: Int, val width: Int, val height: Int) {
  companion object {
    fun fromRect(rect: Rect): Bounds {
      return Bounds(rect.left, rect.top, rect.width(), rect.height())
    }
  }
}

@kotlinx.serialization.Serializable
data class SpaceBox(val top: Int, val right: Int, val bottom: Int, val left: Int) {
  companion object {
    fun fromRect(rect: Rect): SpaceBox {
      return SpaceBox(rect.top, rect.right, rect.bottom, rect.left)
    }
  }
}

@kotlinx.serialization.Serializable
data class Color(val r: Int, val g: Int, val b: Int, val a: Int) {
  companion object {
    fun fromColor(color: Int): Color {
      val alpha: Int = (color shr 24) and 0xFF / 255
      val red: Int = (color shr 16) and 0xFF
      val green: Int = (color shr 8) and 0xFF
      val blue: Int = (color shr 0) and 0xFF
      return Color(red, green, blue, alpha)
    }

    fun fromColor(color: android.graphics.Color): Color {
      return fromColor(color.toArgb())
    }
  }
}

@kotlinx.serialization.Serializable
data class Coordinate(
    @Serializable(with = NumberSerializer::class) val x: Number,
    @Serializable(with = NumberSerializer::class) val y: Number
) {}

@kotlinx.serialization.Serializable
data class Coordinate3D(
    @Serializable(with = NumberSerializer::class) val x: Number,
    @Serializable(with = NumberSerializer::class) val y: Number,
    @Serializable(with = NumberSerializer::class) val z: Number
) {}

@kotlinx.serialization.Serializable
data class Size(
    @Serializable(with = NumberSerializer::class) val width: Number,
    @Serializable(with = NumberSerializer::class) val height: Number
) {}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.observers

import android.util.Log
import android.view.View
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef
import com.facebook.flipper.plugins.uidebugger.core.RootViewResolver
import com.facebook.flipper.plugins.uidebugger.core.UIDContext
import com.facebook.flipper.plugins.uidebugger.util.objectIdentity

/**
 * Responsible for observing the activity stack and managing the subscription to the top most
 * content view (decor view)
 */
class ApplicationTreeObserver(val context: UIDContext) : TreeObserver<ApplicationRef>() {

  override val type = "Application"

  override fun subscribe(node: Any) {
    Log.i(LogTag, "Subscribing activity / root view changes")

    val applicationRef = node as ApplicationRef

    val rootViewListener =
        object : RootViewResolver.Listener {
          override fun onRootViewAdded(rootView: View) {}

          override fun onRootViewRemoved(rootView: View) {}

          override fun onRootViewsChanged(rootViews: List<View>) {
            Log.i(LogTag, "Root views updated, num ${rootViews.size}")
            context.sharedThrottle.trigger()
          }
        }

    context.sharedThrottle.registerCallback(this.objectIdentity()) {
      traverseAndSend(context, applicationRef)
    }

    context.applicationRef.rootsResolver.attachListener(rootViewListener)
    // On subscribe, trigger a traversal on whatever roots we have
    rootViewListener.onRootViewsChanged(applicationRef.rootsResolver.rootViews())

    Log.i(LogTag, "${context.applicationRef.rootsResolver.rootViews().size} root views")
    Log.i(LogTag, "${context.applicationRef.activitiesStack.size} activities")
  }

  override fun unsubscribe() {
    context.applicationRef.rootsResolver.attachListener(null)
    context.sharedThrottle.deregisterCallback(this.objectIdentity())
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.observers

import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.common.BitmapPool
import com.facebook.flipper.plugins.uidebugger.core.UIDContext
import com.facebook.flipper.plugins.uidebugger.util.objectIdentity
import java.lang.ref.WeakReference

typealias DecorView = View

/** Responsible for subscribing to updates to the content view of an activity */
class DecorViewObserver(val context: UIDContext) : TreeObserver<DecorView>() {

  private var nodeRef: WeakReference<View>? = null
  private var preDrawListener: ViewTreeObserver.OnPreDrawListener? = null

  override val type = "DecorView"

  override fun subscribe(node: Any) {
    node as View
    nodeRef = WeakReference(node)

    Log.i(LogTag, "Subscribing to decor view changes")

    context.sharedThrottle.registerCallback(this.objectIdentity()) {
      nodeRef?.get()?.let { traverseAndSendWithSnapshot() }
    }

    preDrawListener =
        ViewTreeObserver.OnPreDrawListener {
          context.sharedThrottle.trigger()
          true
        }

    node.viewTreeObserver.addOnPreDrawListener(preDrawListener)

    // It can be the case that the DecorView the current observer owns has already
    // drawn. In this case, manually trigger an update.
    traverseAndSendWithSnapshot()
  }

  private fun traverseAndSendWithSnapshot() {
    nodeRef?.get()?.let { view ->
      var snapshotBitmap: BitmapPool.ReusableBitmap? = null
      if (view.width > 0 && view.height > 0) {
        snapshotBitmap = context.bitmapPool.getBitmap(view.width, view.height)
      }
      traverseAndSend(context, view, snapshotBitmap)
    }
  }

  override fun unsubscribe() {
    Log.i(LogTag, "Unsubscribing from decor view changes")

    preDrawListener.let {
      nodeRef?.get()?.viewTreeObserver?.removeOnPreDrawListener(it)
      preDrawListener = null
    }

    context.sharedThrottle.deregisterCallback(this.objectIdentity())
    nodeRef?.clear()
    nodeRef = null
  }
}

object DecorViewTreeObserverBuilder : TreeObserverBuilder<DecorView> {
  override fun canBuildFor(node: Any): Boolean {
    return node.javaClass.simpleName.contains("DecorView")
  }

  override fun build(context: UIDContext): TreeObserver<DecorView> {
    Log.i(LogTag, "Building DecorView observer")
    return DecorViewObserver(context)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.observers

import android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.common.BitmapPool
import com.facebook.flipper.plugins.uidebugger.core.UIDContext
import com.facebook.flipper.plugins.uidebugger.descriptors.Id
import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor
import com.facebook.flipper.plugins.uidebugger.model.FrameworkEvent
import com.facebook.flipper.plugins.uidebugger.util.objectIdentity

/*
 * Represents a stateful observer that manages some subtree in the UI Hierarchy.
 * It is responsible for:
 *  1. Listening to the relevant framework events
 *  2. Traversing the hierarchy of the managed nodes
 *  3. Diffing to previous state (optional)
 *  4. Pushing out updates for its entire set of managed nodes
 *
 * If while traversing it encounters a node type which has its own TreeObserver, it
 * does not traverse that, instead it sets up a Tree observer responsible for that subtree
 *
 * The parent is responsible for detecting when a child observer needs to be cleaned up.
 */
abstract class TreeObserver<T> {

  protected val children: MutableMap<Int, TreeObserver<*>> = mutableMapOf()

  abstract val type: String

  abstract fun subscribe(node: Any)

  abstract fun unsubscribe()

  /** Traverses the layout hierarchy while managing any encountered child observers. */
  fun traverseAndSend(
      context: UIDContext,
      root: Any,
      snapshotBitmap: BitmapPool.ReusableBitmap? = null,
      frameworkEvents: List<FrameworkEvent>? = null
  ) {
    val traversalStartTimestamp = System.currentTimeMillis()
    val (visitedNodes, observableRoots) = context.layoutTraversal.traverse(root)

    // Add any new observers
    observableRoots.forEach { observable ->
      if (!children.containsKey(observable.objectIdentity())) {
        context.observerFactory.createObserver(observable, context)?.let { observer ->
          Log.d(
              LogTag,
              "Observer ${this.type} discovered new child of type ${observer.type} Node ID ${observable.objectIdentity()}")
          observer.subscribe(observable)
          children[observable.objectIdentity()] = observer
        }
      }
    }

    // Remove any old observers
    val observableRootsIdentifiers = observableRoots.map { it.objectIdentity() }
    val removables = mutableListOf<Id>()
    children.keys.forEach { key ->
      if (!observableRootsIdentifiers.contains(key)) {
        children[key]?.let { observer ->
          Log.d(
              LogTag,
              "Observer ${this.type} cleaning up child of type ${observer.type} Node ID $key")

          observer.cleanUpRecursive()
        }
        removables.add(key)
      }
    }
    removables.forEach { key -> children.remove(key) }

    val traversalEndTimestamp = System.currentTimeMillis()

    if (snapshotBitmap != null) {
      @Suppress("unchecked_cast")
      val descriptor =
          context.descriptorRegister.descriptorForClassUnsafe(root::class.java)
              as NodeDescriptor<Any>
      descriptor.getSnapshot(root, snapshotBitmap.bitmap)
    }

    val snapshotEndTimestamp = System.currentTimeMillis()

    context.treeObserverManager.enqueueUpdate(
        SubtreeUpdate(
            type,
            root.objectIdentity(),
            visitedNodes,
            traversalStartTimestamp,
            snapshotEndTimestamp,
            (traversalEndTimestamp - traversalStartTimestamp),
            (snapshotEndTimestamp - traversalEndTimestamp),
            frameworkEvents,
            snapshotBitmap))
  }

  fun cleanUpRecursive() {
    Log.i(LogTag, "Cleaning up observer $this")
    children.values.forEach { it.cleanUpRecursive() }
    unsubscribe()
    children.clear()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.observers

import com.facebook.flipper.plugins.uidebugger.core.UIDContext

interface TreeObserverBuilder<T> {
  fun canBuildFor(node: Any): Boolean

  fun build(context: UIDContext): TreeObserver<T>
}

class TreeObserverFactory {

  private val builders = mutableListOf<TreeObserverBuilder<*>>()

  fun <T> register(builder: TreeObserverBuilder<T>) {
    builders.add(builder)
  }

  // TODO: Not very efficient, need to cache this. Builders cannot be removed
  fun hasObserverFor(node: Any): Boolean {
    return builders.any { it.canBuildFor(node) }
  }

  // TODO: Not very efficient, need to cache this. Builders cannot be removed.
  fun createObserver(node: Any, context: UIDContext): TreeObserver<*>? {
    return builders.find { it.canBuildFor(node) }?.build(context)
  }

  companion object {
    fun withDefaults(): TreeObserverFactory {
      val factory = TreeObserverFactory()
      // TODO: Only builder for DecorView, maybe more are needed.
      factory.register(DecorViewTreeObserverBuilder)

      return factory
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.observers

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Looper
import android.util.Base64
import android.util.Base64OutputStream
import android.util.Log
import android.view.Choreographer
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.common.BitmapPool
import com.facebook.flipper.plugins.uidebugger.core.UIDContext
import com.facebook.flipper.plugins.uidebugger.descriptors.Id
import com.facebook.flipper.plugins.uidebugger.descriptors.MetadataRegister
import com.facebook.flipper.plugins.uidebugger.model.FrameScanEvent
import com.facebook.flipper.plugins.uidebugger.model.FrameworkEvent
import com.facebook.flipper.plugins.uidebugger.model.MetadataUpdateEvent
import com.facebook.flipper.plugins.uidebugger.model.Node
import com.facebook.flipper.plugins.uidebugger.model.PerfStatsEvent
import com.facebook.flipper.plugins.uidebugger.model.Snapshot
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred
import java.io.ByteArrayOutputStream
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.json.Json

data class SubtreeUpdate(
    val observerType: String,
    val rootId: Id,
    val deferredNodes: List<MaybeDeferred<Node>>,
    val timestamp: Long,
    val queuedTimestamp: Long,
    val traversalMS: Long,
    val snapshotMS: Long,
    val frameworkEvents: List<FrameworkEvent>?,
    val snapshot: BitmapPool.ReusableBitmap?
)

data class BatchedUpdate(val updates: List<SubtreeUpdate>, val frameTimeMs: Long)

/** Holds the root observer and manages sending updates to desktop */
class TreeObserverManager(val context: UIDContext) {

  private val rootObserver = ApplicationTreeObserver(context)
  private lateinit var batchedUpdates: Channel<BatchedUpdate>

  private val subtreeUpdateBuffer = SubtreeUpdateBuffer(this::enqueueBatch)

  private var job: Job? = null
  private val workerScope = CoroutineScope(Dispatchers.IO)
  private val mainScope = CoroutineScope(Dispatchers.Main)
  private val txId = AtomicInteger()

  fun enqueueUpdate(update: SubtreeUpdate) {
    subtreeUpdateBuffer.bufferUpdate(update)
  }

  private fun enqueueBatch(batchedUpdate: BatchedUpdate) {
    batchedUpdates.trySend(batchedUpdate)
  }

  /**
   * 1. Sets up the root observer
   * 2. Starts worker to listen to channel, which serializers and sends data over connection
   */
  @SuppressLint("NewApi")
  fun start() {

    if (Looper.myLooper() != Looper.getMainLooper()) {
      mainScope.launch { start() }
    }
    batchedUpdates = Channel(Channel.UNLIMITED)
    rootObserver.subscribe(context.applicationRef)

    job =
        workerScope.launch {
          while (isActive) {
            try {
              val update = batchedUpdates.receive()
              sendBatchedUpdate(update)
            } catch (e: CancellationException) {} catch (e: java.lang.Exception) {
              Log.e(LogTag, "Unexpected Error in channel ", e)
            }
          }
          Log.i(LogTag, "Shutting down worker")
        }
  }

  fun stop() {
    rootObserver.cleanUpRecursive()
    job?.cancel()
    batchedUpdates.cancel()
  }

  private fun sendBatchedUpdate(batchedUpdate: BatchedUpdate) {
    Log.i(
        LogTag,
        "Got update from ${batchedUpdate.updates.size} observers at time ${batchedUpdate.frameTimeMs}")

    val workerThreadStartTimestamp = System.currentTimeMillis()

    val nodes = batchedUpdate.updates.flatMap { it.deferredNodes.map { it.value() } }
    val frameworkEvents = batchedUpdate.updates.flatMap { it.frameworkEvents ?: listOf() }
    val snapshotUpdate = batchedUpdate.updates.find { it.snapshot != null }
    val deferredComputationEndTimestamp = System.currentTimeMillis()

    var snapshot: Snapshot? = null
    if (snapshotUpdate?.snapshot != null) {
      val stream = ByteArrayOutputStream()
      val base64Stream = Base64OutputStream(stream, Base64.DEFAULT)
      snapshotUpdate.snapshot.bitmap?.compress(Bitmap.CompressFormat.PNG, 100, base64Stream)
      snapshot = Snapshot(snapshotUpdate.rootId, stream.toString())
      snapshotUpdate.snapshot.readyForReuse()
    }

    // it is important this comes after deferred processing since the deferred processing can create
    // metadata
    sendMetadata()

    val serialized =
        Json.encodeToString(
            FrameScanEvent.serializer(),
            FrameScanEvent(batchedUpdate.frameTimeMs, nodes, snapshot, frameworkEvents))

    val serialisationEndTimestamp = System.currentTimeMillis()

    context.connectionRef.connection?.send(FrameScanEvent.name, serialized)

    val socketEndTimestamp = System.currentTimeMillis()
    Log.i(LogTag, "Sent event for batched subtree update  with  nodes with ${nodes.size}")

    // Note about payload size:
    // Payload size is an approximation as it assumes all characters
    // are ASCII encodable, this should be true for most of the payload content.
    // So, assume each character will at most occupy one byte.
    val perfStats =
        PerfStatsEvent(
            txId = batchedUpdate.frameTimeMs,
            observerType = "batched",
            nodesCount = nodes.size,
            start = batchedUpdate.updates.minOf { it.timestamp },
            traversalMS = batchedUpdate.updates.maxOf { it.traversalMS },
            snapshotMS = batchedUpdate.updates.maxOf { it.snapshotMS },
            queuingMS =
                workerThreadStartTimestamp - batchedUpdate.updates.minOf { it.queuedTimestamp },
            deferredComputationMS = (deferredComputationEndTimestamp - workerThreadStartTimestamp),
            serializationMS = (serialisationEndTimestamp - deferredComputationEndTimestamp),
            socketMS = (socketEndTimestamp - serialisationEndTimestamp),
            payloadSize = serialized.length)

    context.connectionRef.connection?.send(
        PerfStatsEvent.name, Json.encodeToString(PerfStatsEvent.serializer(), perfStats))
  }

  private fun sendMetadata() {
    val metadata = MetadataRegister.extractPendingMetadata()
    if (metadata.isNotEmpty()) {
      context.connectionRef.connection?.send(
          MetadataUpdateEvent.name,
          Json.encodeToString(MetadataUpdateEvent.serializer(), MetadataUpdateEvent(metadata)))
    }
  }
}

/** Buffers up subtree updates until the frame is complete, should only be called on main thread */
private class SubtreeUpdateBuffer(private val onBatchReady: (BatchedUpdate) -> Unit) {

  private val bufferedSubtreeUpdates = mutableListOf<SubtreeUpdate>()

  fun bufferUpdate(update: SubtreeUpdate) {
    if (bufferedSubtreeUpdates.isEmpty()) {

      Choreographer.getInstance().postFrameCallback { frameTime ->
        val updatesCopy = bufferedSubtreeUpdates.toList()
        bufferedSubtreeUpdates.clear()

        onBatchReady(BatchedUpdate(updatesCopy, frameTime / 1000000))
      }
    }
    bufferedSubtreeUpdates.add(update)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.scheduler

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
 * The class makes the following guarantees
 * 1. All registered callbacks will be called on the same frame at the same time
 * 2. The callbacks will never be called more often than the min interval
 * 3. If it has been > min interval since the callbacks was last called we will call the callbacks
 *    immediately
 * 4. If an event comes in within the min interval of the last firing we will schedule another
 *    firing at the next possible moment
 *
 * The reason we need this is because with an independent throttle per observer you end up with
 * updates occurring across different frames,
 *
 * WARNING: Not thread safe, should only be called on main thread. Also It is important to
 * deregister to avoid leaks
 */
class SharedThrottle(
    private val executionScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
) {

  private var job: Job? = null
  private val callbacks = mutableMapOf<Int, () -> Unit>()
  private var latestInvocationId: Long = 0

  fun registerCallback(id: Int, callback: () -> Unit) {
    callbacks[id] = callback
  }

  fun deregisterCallback(id: Int) {
    callbacks.remove(id)
  }

  fun trigger() {
    latestInvocationId += 1

    if (job == null || job?.isCompleted == true) {
      job =
          executionScope.launch {
            var i = 0
            do {
              val thisInvocationId = latestInvocationId

              callbacks.values.toList().forEach { callback -> callback() }

              val delayTime = exponentialBackOff(base = 250, exp = 1.5, max = 1000, i = i).toLong()
              delay(delayTime)
              i++

              // if we haven't received an call since we executed break out and let a new job be
              // created which, otherwise we loop which executes again at the next appropriate time
              // since we have already waited
            } while (thisInvocationId != latestInvocationId)
          }
    }
  }

  private fun exponentialBackOff(base: Long, exp: Double, max: Long, i: Int): Double {
    return Math.min(base * Math.pow(exp, i.toDouble()), max.toDouble())
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.flipper.plugins.uidebugger.traversal

import android.util.Log
import com.facebook.flipper.plugins.uidebugger.LogTag
import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister
import com.facebook.flipper.plugins.uidebugger.descriptors.Id
import com.facebook.flipper.plugins.uidebugger.descriptors.NodeDescriptor
import com.facebook.flipper.plugins.uidebugger.model.Node
import com.facebook.flipper.plugins.uidebugger.observers.TreeObserverFactory
import com.facebook.flipper.plugins.uidebugger.util.Immediate
import com.facebook.flipper.plugins.uidebugger.util.MaybeDeferred

/**
 * This will traverse the layout hierarchy until it sees a node that has an observer registered for
 * it.
 * - The first item in the pair is the visited nodes.
 * - The second item are any observable roots discovered.
 */
class PartialLayoutTraversal(
    private val descriptorRegister: DescriptorRegister,
    private val treeObserverFactory: TreeObserverFactory,
) {

  @Suppress("unchecked_cast")
  internal fun NodeDescriptor<*>.asAny(): NodeDescriptor<Any> = this as NodeDescriptor<Any>

  fun traverse(root: Any): Pair<List<MaybeDeferred<Node>>, List<Any>> {

    val visited = mutableListOf<MaybeDeferred<Node>>()
    val observableRoots = mutableListOf<Any>()

    // cur and parent Id
    val stack = mutableListOf<Pair<Any, Id?>>()
    stack.add(Pair(root, null))

    val shallow = mutableSetOf<Any>()

    while (stack.isNotEmpty()) {
      val (node, parentId) = stack.removeLast()

      try {
        // If we encounter a node that has it own observer, don't traverse
        if (node != root && treeObserverFactory.hasObserverFor(node)) {
          observableRoots.add(node)
          continue
        }

        val descriptor = descriptorRegister.descriptorForClassUnsafe(node::class.java).asAny()

        val curId = descriptor.getId(node)
        if (shallow.contains(node)) {
          visited.add(
              Immediate(
                  Node(
                      curId,
                      parentId,
                      descriptor.getQualifiedName(node),
                      descriptor.getName(node),
                      emptyMap(),
                      emptyMap(),
                      null,
                      descriptor.getBounds(node),
                      emptySet(),
                      emptyList(),
                      null)))

          shallow.remove(node)
          continue
        }

        val children = descriptor.getChildren(node)

        val activeChild = descriptor.getActiveChild(node)

        var activeChildId: Id? = null
        if (activeChild != null) {
          val activeChildDescriptor =
              descriptorRegister.descriptorForClassUnsafe(activeChild.javaClass)
          activeChildId = activeChildDescriptor.getId(activeChild)
        }

        val childrenIds = mutableListOf<Id>()
        children.forEach { child ->
          val childDescriptor = descriptorRegister.descriptorForClassUnsafe(child.javaClass)
          childrenIds.add(childDescriptor.getId(child))
          stack.add(Pair(child, curId))
          // If there is an active child then don't traverse it
          if (activeChild != null && activeChild != child) {
            shallow.add(child)
          }
        }

        val attributes = descriptor.getAttributes(node)
        val bounds = descriptor.getBounds(node)
        val tags = descriptor.getTags(node)
        visited.add(
            attributes.map { attrs ->
              Node(
                  curId,
                  parentId,
                  descriptor.getQualifiedName(node),
                  descriptor.getName(node),
                  attrs,
                  descriptor.getInlineAttributes(node),
                  descriptor.getHiddenAttributes(node),
                  bounds,
                  tags,
                  childrenIds,
                  activeChildId)
            })
      } catch (exception: Exception) {
        Log.e(LogTag, "Error while processing node ${node.javaClass.name} $node", exception)
      }
    }

    return Pair(visited, observableRoots)
  }
}