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

package com.facebook.samples.litho.kotlin.lithography.components

import android.graphics.Color
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Row
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.width
import com.facebook.litho.dp
import com.facebook.litho.kotlin.widget.Progress
import com.facebook.yoga.YogaJustify.CENTER

class LoadingComponent : KComponent() {
  override fun ComponentScope.render() =
      Row(justifyContent = CENTER) {
        child(Progress(color = Color.DKGRAY, style = Style.width(50.dp).height(50.dp)))
      }
}


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

package com.facebook.samples.litho.kotlin.lithography.components

import com.facebook.fresco.vito.options.ImageOptions
import com.facebook.fresco.vito.source.ImageSourceProvider
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.fresco.FrescoVitoImage

class SingleImageComponent(val imageUri: String, val imageAspectRatio: Float = 1f) : KComponent() {
  override fun ComponentScope.render() =
      FrescoVitoImage(
          ImageSourceProvider.forUri(imageUri),
          ImageOptions.defaults(),
          "SingleImageComponent",
          imageAspectRatio = imageAspectRatio)
}


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

package com.facebook.samples.litho.kotlin.lithography.data

data class Artist(val name: String, val biography: String, val images: List<String>)


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

package com.facebook.samples.litho.kotlin.lithography.data

object DataCreator {
  fun createPageOfData(baseYear: Int): List<Decade> =
      listOf(
          Decade(
              year = 1800 + baseYear,
              artists =
                  listOf(
                      Artist(
                          name = "Richard James Lane",
                          biography =
                              "Lithographer to Queen Victoria and Prince " +
                                  "Albert, Lane produced over a thousand prints of " +
                                  "his various lithographs of several hundred " +
                                  "portraitures.  One of his better known works is " +
                                  "of a eighteen year old Queen Victoria.",
                          images =
                              listOf(
                                  "https://upload.wikimedia.org/wikipedia/commons" +
                                      "/3/33/George_Francis_Lyon.jpg")),
                      Artist(
                          name = "Louis and Fritz Wolff",
                          biography =
                              "Deaf brothers from Heilbronn, Louis (Ludwig)" +
                                  " and Fritz (Fredrich) Wolff were nineteenth " +
                                  "century lithographers who composed scenes of " +
                                  "buildings, squares and everyday urban life.",
                          images =
                              listOf(
                                  "https://upload.wikimedia.org/wikipedia/commons" +
                                      "/thumb/8/82/B\u00F6ckingen_am_See_1848_" +
                                      "Gebr_Wolff.jpg/512px-B\u00F6ckingen_am_" +
                                      "See_1848_Gebr_Wolff.jpg")))),
          Decade(
              year = 1810 + baseYear,
              artists =
                  listOf(
                      Artist(
                          name = "\u00C9mile Lassalle",
                          biography =
                              "A recipient of the French National Order of " +
                                  "the Legion of Honour and a student of Pierre " +
                                  "Lacour, Lassalle was a painter and lithographer " +
                                  "in the mid-1800s.",
                          images =
                              listOf(
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "thumb/4/4a/Atlas_pittoresque_pl_004.jpg/" +
                                      "512px-Atlas_pittoresque_pl_004.jpg",
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "thumb/7/72/Atlas_pittoresque_pl_119.jpg/" +
                                      "512px-Atlas_pittoresque_pl_119.jpg")),
                      Artist(
                          name = "Jule Arnout",
                          biography =
                              "Jule Arnout, pupil of his father, Jean " +
                                  "Baptiste Arnout, captured " +
                                  "landscapes, " +
                                  "monuments and cities from France, Switzerland, " +
                                  "Italy and England.",
                          images =
                              listOf(
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "0/0e/Sommerset_house_by_ARNOUT%2C_LOUIS" +
                                      "_JULES_-_GMII.jpg",
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "2/2d/Arnout_Boulevard_St_Martin.jpg",
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "a/ac/Jules_Arnout_Saint_Isaac%27" +
                                      "s_Cathedral.jpg",
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "1/1a/Trafalgar_square_by_ARNOUT%2C_LOUIS" +
                                      "_JULES_-_GMII.jpg",
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "8/84/Windsor_castle_by_ARNOUT%2C_LOUIS_" +
                                      "JULES_-_GMII.jpg")),
                      Artist(
                          name = "Anastas Jovanovi\u0107",
                          biography =
                              "Serbian Jovanovi\u0107 is best known as a " +
                                  "historic and pioneering photographer, but his " +
                                  "efforts to capture his hometown of Belgrade " +
                                  "started with paint and lithography.",
                          images =
                              listOf(
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "thumb/2/21/Victory_of_King_Milutin_over_" +
                                      "the_Tatars%2C_Anastas_Jovanovi\u0107_" +
                                      "%281853%29.jpg/512px-Victory_of_King_" +
                                      "Milutin_over_the_Tatars%2C_Anastas_" +
                                      "Jovanovi\u0107_%281853%29.jpg")))),
          Decade(
              year = 1830 + baseYear,
              artists =
                  listOf(
                      Artist(
                          name = "Gaston Marichal",
                          biography =
                              "Little is known about this French " +
                                  "lithographer, except that he worked in Spain " +
                                  "in the latter half of the  1800s.",
                          images =
                              listOf(
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "thumb/9/94/Iglesia_Compa\u00F1ia_Chile" +
                                      ".JPG/512px-Iglesia_Compa\u00F1ia_" +
                                      "Chile.JPG")))),
          Decade(
              year = 1850 + baseYear,
              artists =
                  listOf(
                      Artist(
                          name = "Vincent van Gogh",
                          biography =
                              "The renowned post-impressionist painter " +
                                  "dabbled briefly in lithography, producing ten " +
                                  "works in 1882/3.  Many of his prints, such as" +
                                  " \"Sorrow\" and \"At Eternity's Gate\" he also" +
                                  " captured in other media.",
                          images =
                              listOf(
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "thumb/6/61/Van_Gogh_-_In_the_Orchard_-_" +
                                      "1883.jpg/512px-Van_Gogh_-_In_the_Orchard" +
                                      "_-_1883.jpg",
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "thumb/0/0e/Vincent_Van_Gogh_27.JPG/512px" +
                                      "-Vincent_Van_Gogh_27.JPG")),
                      Artist(
                          name = "Juan Comba Garc\u00EDa",
                          biography =
                              "A Spanish cartoonist, photographer and " +
                                  "painter, Juan Comba Garc\u00EDa was bestowed " +
                                  "the title of \"Graphic Chronicler of the " +
                                  "Restoration\" due to his 683 graphic works " +
                                  "during the time of King Alfonso XII.",
                          images =
                              listOf(
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "c/cf/Duque_de_Sesto_pide_la_mano_de_" +
                                      "Mar%C3%ADa_de_las_Mercedes_de_Orleans.jpg",
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "thumb/2/29/Inauguraci\u00F3n_de_la_" +
                                      "Estaci\u00F3n_definitiva_del_ferrocarril" +
                                      "_de_Madrid_a_Ciudad_Real_y_Badajoz_%28" +
                                      "Comba%29.jpg/512px-Inauguraci\u00F3n_de" +
                                      "_la_Estaci\u00F3n_definitiva_del_" +
                                      "ferrocarril_de_Madrid_a_Ciudad_" +
                                      "Real_y_Badajoz_%28Comba%29.jpg",
                                  "https://upload.wikimedia.org/wikipedia/commons/" +
                                      "thumb/3/3f/Palacio_del_pardo_XIX.jpg/" +
                                      "512px-Palacio_del_pardo_XIX.jpg")))))
}


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

package com.facebook.samples.litho.kotlin.lithography.data

import android.os.AsyncTask
import androidx.lifecycle.MutableLiveData

class DataFetcher(val model: MutableLiveData<Model>) : Fetcher {
  private var fetching = -1

  override fun invoke(lastFetchedDecade: Int) {
    if (fetching == lastFetchedDecade + 1) {
      return
    }

    fetching = lastFetchedDecade + 1

    model.value = model.value?.let { Model(it.decades, true) }

    FetchTask(model, lastFetchedDecade).execute()
  }
}

class FetchTask(val model: MutableLiveData<Model>, private val lastFetchedDecade: Int) :
    AsyncTask<Void, Void, List<Decade>>() {
  override fun doInBackground(vararg params: Void?): List<Decade> {
    // Let's simulate a network call here.
    Thread.sleep(2000)
    return DataCreator.createPageOfData(lastFetchedDecade + 1)
  }

  override fun onPostExecute(result: List<Decade>?) {
    result?.let {
      val decades = model.value?.decades ?: emptyList()
      model.value = Model(decades + it, false)
    }
  }
}


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

package com.facebook.samples.litho.kotlin.lithography.data

data class Decade(val year: Int, val artists: List<Artist>)


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

package com.facebook.samples.litho.kotlin.lithography.data

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

data class DecadeViewModel(
    val model: MutableLiveData<Model> =
        MutableLiveData<Model>().apply { value = Model(DataCreator.createPageOfData(0), false) }
) : ViewModel()


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

package com.facebook.samples.litho.kotlin.lithography.data

typealias Fetcher = (lastFetchedDecade: Int) -> Unit


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

package com.facebook.samples.litho.kotlin.lithography.data

data class Model(val decades: List<Decade>, val isLoading: Boolean)


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

package com.facebook.samples.litho.kotlin.lithography.sections

import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.Children
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.annotations.GroupSectionSpec
import com.facebook.litho.sections.annotations.OnCreateChildren
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.OnCheckIsSameItemEvent
import com.facebook.litho.sections.common.RenderEvent
import com.facebook.litho.sections.common.SingleComponentSection
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.RenderInfo
import com.facebook.samples.litho.kotlin.lithography.components.DecadeSeparator
import com.facebook.samples.litho.kotlin.lithography.components.FeedItemCard
import com.facebook.samples.litho.kotlin.lithography.data.Artist
import com.facebook.samples.litho.kotlin.lithography.data.Decade

@GroupSectionSpec
object DecadeSectionSpec {

  @OnCreateChildren
  fun onCreateChildren(c: SectionContext, @Prop decade: Decade): Children =
      Children.create()
          .child(
              SingleComponentSection.create(c)
                  .component(DecadeSeparator(decade = decade))
                  .sticky(true))
          .child(
              DataDiffSection.create<Artist>(c)
                  .data(decade.artists)
                  .renderEventHandler(DecadeSection.render(c))
                  .onCheckIsSameItemEventHandler(DecadeSection.isSameItem(c)))
          .build()

  @OnEvent(RenderEvent::class)
  fun render(c: SectionContext, @FromEvent model: Artist): RenderInfo =
      ComponentRenderInfo.create().component(FeedItemCard(artist = model)).build()

  @OnEvent(OnCheckIsSameItemEvent::class)
  fun isSameItem(
      c: SectionContext,
      @FromEvent previousItem: Artist,
      @FromEvent nextItem: Artist
  ): Boolean = previousItem.name == nextItem.name
}


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

package com.facebook.samples.litho.kotlin.lithography.sections

import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.Children
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.annotations.GroupSectionSpec
import com.facebook.litho.sections.annotations.OnCreateChildren
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.RenderEvent
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.RenderInfo
import com.facebook.samples.litho.kotlin.lithography.components.SingleImageComponent

@GroupSectionSpec
object ImagesSectionSpec {

  @OnCreateChildren
  fun onCreateChildren(c: SectionContext, @Prop images: List<String>): Children =
      Children.create()
          .child(
              DataDiffSection.create<String>(c)
                  .data(images)
                  .renderEventHandler(ImagesSection.onRender(c)))
          .build()

  @OnEvent(RenderEvent::class)
  fun onRender(c: SectionContext, @FromEvent model: String): RenderInfo =
      ComponentRenderInfo.create()
          .component(SingleImageComponent(imageUri = model, imageAspectRatio = 2f))
          .build()
}


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

package com.facebook.samples.litho.kotlin.lithography.sections

import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.Children
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.annotations.GroupSectionSpec
import com.facebook.litho.sections.annotations.OnCreateChildren
import com.facebook.litho.sections.annotations.OnViewportChanged
import com.facebook.litho.sections.common.SingleComponentSection
import com.facebook.samples.litho.kotlin.lithography.components.LoadingComponent
import com.facebook.samples.litho.kotlin.lithography.data.Decade
import com.facebook.samples.litho.kotlin.lithography.data.Fetcher

@GroupSectionSpec
object LithoFeedSectionSpec {

  @OnCreateChildren
  fun onCreateChildren(
      c: SectionContext,
      @Prop decades: List<Decade>,
      @Prop loading: Boolean
  ): Children {
    val children = Children.create()
    val decadeSections =
        decades.map { DecadeSection.create(c).decade(it).key("${it.year}").build() }

    children.child(decadeSections)

    if (loading) {
      children.child(SingleComponentSection.create(c).component(LoadingComponent()))
    }

    return children.build()
  }

  @OnViewportChanged
  fun onViewportChanged(
      c: SectionContext,
      firstVisible: Int,
      lastVisible: Int,
      totalCount: Int,
      fistFullyVisible: Int,
      lastFullyVisible: Int,
      @Prop fetcher: Fetcher,
      @Prop decades: List<Decade>
  ) {
    val threshold = 2
    if (totalCount - lastVisible < threshold) {
      fetcher(decades.last().year)
    }
  }
}


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

package com.facebook.samples.litho.kotlin.logging

data class EventData(val startTimeNs: Long) {
  private val annotations: MutableMap<String, Any?> = hashMapOf()
  private val markers: MutableList<Pair<Long, String>> = mutableListOf()

  var endTimeNs: Long? = null

  fun addAnnotation(annotationKey: String, annotationValue: Any?): EventData = apply {
    annotations[annotationKey] = annotationValue
  }

  fun getAnnotations(): Map<String, Any?> = annotations

  fun addMarker(timeNs: Long, eventName: String): EventData = apply {
    markers.add(Pair(timeNs, eventName))
  }

  fun getMarkers(): List<Pair<Long, String>> = markers
}


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

package com.facebook.samples.litho.kotlin.logging

data class LogContext(val s: String) {
  override fun toString(): String = s
}

fun LogContext?.append(s: String): LogContext =
    if (this == null) LogContext(s) else LogContext(this.s + ":" + s)


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

package com.facebook.samples.litho.kotlin.logging

import android.os.Bundle
import android.util.Log
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentTree
import com.facebook.litho.ComponentTreeDebugEventListener
import com.facebook.litho.LithoView
import com.facebook.litho.debug.LithoDebugEvent.LayoutCommitted
import com.facebook.litho.debug.LithoDebugEvent.StateUpdateEnqueued
import com.facebook.rendercore.debug.DebugEvent
import com.facebook.rendercore.debug.DebugEvent.Companion.MountItemMount
import com.facebook.samples.litho.NavigatableDemoActivity

class LoggingActivity : NavigatableDemoActivity() {

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

    val c = ComponentContext(this, "LITHOSAMPLE", SampleComponentsLogger())
    val lithoView =
        LithoView.create(
            c,
            ComponentTree.create(c, LoggingRootComponent())
                .withComponentTreeDebugEventListener(
                    object : ComponentTreeDebugEventListener {
                      override fun onEvent(debugEvent: DebugEvent) {
                        Log.d("litho-events", debugEvent.toString())
                      }

                      override val events: Set<String> =
                          setOf(MountItemMount, StateUpdateEnqueued, LayoutCommitted)
                    })
                .build())

    setContentView(lithoView)
  }
}


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

package com.facebook.samples.litho.kotlin.logging

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.TreePropProvider
import com.facebook.litho.getTreeProp
import com.facebook.litho.kotlin.widget.Text

class LoggingChildComponent : KComponent() {
  override fun ComponentScope.render(): Component? {
    val parent = getTreeProp<LogContext>()

    return TreePropProvider(LogContext::class.java to parent.append("child")) {
      Text(text = "Hello, Logger.")
    }
  }
}


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

package com.facebook.samples.litho.kotlin.logging

import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.TreePropProvider

class LoggingRootComponent : KComponent() {
  override fun ComponentScope.render(): Component? {
    return TreePropProvider(LogContext::class.java to LogContext("root")) {
      LoggingChildComponent()
    }
  }
}


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

package com.facebook.samples.litho.kotlin.logging

import com.facebook.litho.PerfEvent

/**
 * The Litho logging system is designed to be stateless and have batches of data sent to the server
 * for processing, to avoid overhead on the device. This sample implementation is to illustrate the
 * data you can work with, not on how to actually implement logging on the client.
 */
object PerfEventStore {
  private val events: MutableMap<PerfEvent, EventData> = hashMapOf()

  fun obtain(eventId: Int, instanceKey: Int): PerfEvent {
    val event = SamplePerfEvent(eventId, instanceKey)
    events[event] = EventData(System.nanoTime())
    return event
  }

  fun markerAnnotate(event: SamplePerfEvent, annotationKey: String, annotationValue: Any?) {
    // We can be pretty lose with lookup here as we control event creation.
    // Also, if we misuse the APIs in the framework, we don't want that to silently fail.
    events[event]!!.addAnnotation(annotationKey, annotationValue)
  }

  fun markerPoint(event: SamplePerfEvent, eventName: String) {
    events[event]!!.addMarker(System.nanoTime(), eventName)
  }

  fun release(event: PerfEvent): EventData {
    val data = events.remove(event)!!
    data.endTimeNs = System.nanoTime()
    return data
  }
}


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

package com.facebook.samples.litho.kotlin.logging

import android.util.Log
import com.facebook.litho.ComponentsLogger
import com.facebook.litho.FrameworkLogEvents
import com.facebook.litho.PerfEvent
import com.facebook.litho.TreePropertyProvider
import java.util.concurrent.atomic.AtomicInteger

class SampleComponentsLogger : ComponentsLogger {
  private val tag = "LITHOSAMPLE"
  private val instanceCounter: AtomicInteger = AtomicInteger(0)

  override fun newPerformanceEvent(eventId: Int): PerfEvent {
    return PerfEventStore.obtain(eventId, instanceCounter.incrementAndGet())
  }

  override fun logPerfEvent(event: PerfEvent) {
    printEventData(event, PerfEventStore.release(event))
  }

  override fun cancelPerfEvent(event: PerfEvent) {
    PerfEventStore.release(event)
  }

  override fun getExtraAnnotations(
      treePropertyProvider: TreePropertyProvider
  ): Map<String, String> {
    val logContext = treePropertyProvider.getProperty(LogContext::class.java)
    return buildMap { logContext?.let { put("log_context", "$it") } }
  }

  override fun isTracing(logEvent: PerfEvent): Boolean = true

  private fun printEventData(event: PerfEvent, data: EventData) {
    val totalTimeMs = (data.endTimeNs!! - data.startTimeNs) / (1000 * 1000f)
    var msg =
        """
        |--- <PERFEVENT> ---
        |type: ${getEventNameById(event.markerId)}
        |total time: ${totalTimeMs.withDigits(2)}ms
        """

    val annotations = formatAnnotations(data)
    if (annotations.isNotEmpty()) {
      msg += "|annotations: $annotations\n"
    }

    val markers = formatMarkers(data)
    if (markers.isNotEmpty()) {
      msg += "|markers: $markers\n"
    }

    msg += "|--- </PERFEVENT> ---\n"

    Log.v(tag, msg.trimMargin())
  }

  private fun formatMarkers(data: EventData): String =
      data.getMarkers().joinToString {
        "${((it.first - data.startTimeNs) / (1000 * 1000f)).withDigits(2)}ms -> ${it.second}"
      }

  private fun formatAnnotations(data: EventData): String =
      data.getAnnotations().map { "${it.key}=${it.value}" }.joinToString()

  private fun getEventNameById(@FrameworkLogEvents.LogEventId markerId: Int): String =
      when (markerId) {
        FrameworkLogEvents.EVENT_PRE_ALLOCATE_MOUNT_CONTENT -> "PRE_ALLOCATE_MOUNT_CONTENT"
        FrameworkLogEvents.EVENT_CALCULATE_RESOLVE -> "RESOLVE"
        FrameworkLogEvents.EVENT_INIT_RANGE -> "EVENT_INIT_RANGE"
        FrameworkLogEvents.EVENT_COMPONENT_RESOLVE -> "EVENT_COMPONENT_RESOLVE"
        FrameworkLogEvents.EVENT_COMPONENT_PREPARE -> "EVENT_COMPONENT_PREPARE"
        else -> "UNKNOWN"
      }

  private fun Float.withDigits(digits: Int) = "%.${digits}f".format(this)
}


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

package com.facebook.samples.litho.kotlin.logging

import android.util.Log
import com.facebook.rendercore.ErrorReporterDelegate
import com.facebook.rendercore.LogLevel

class SampleComponentsReporter : ErrorReporterDelegate {
  private val tag = "LITHOSAMPLE"

  override fun report(
      level: LogLevel,
      categoryKey: String,
      message: String,
      cause: Throwable?,
      samplingFrequency: Int,
      metadata: Map<String, Any>?
  ) {
    when (level) {
      LogLevel.VERBOSE -> {
        Log.v(tag, message)
      }
      LogLevel.DEBUG -> {
        Log.d(tag, message)
      }
      LogLevel.WARNING -> {
        Log.w(tag, message)
      }
      LogLevel.ERROR -> {
        Log.e(tag, message)
      }
      LogLevel.FATAL -> {
        Log.wtf(tag, message)
        throw RuntimeException(message)
      }
    }
  }
}


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

package com.facebook.samples.litho.kotlin.logging

import com.facebook.litho.PerfEvent

data class SamplePerfEvent(override val markerId: Int, override val instanceKey: Int) : PerfEvent {

  override fun markerAnnotate(annotationKey: String, annotationValue: String?) {
    PerfEventStore.markerAnnotate(this, annotationKey, annotationValue)
  }

  override fun markerAnnotate(annotationKey: String, annotationValue: Double) {
    PerfEventStore.markerAnnotate(this, annotationKey, annotationValue)
  }

  override fun markerAnnotate(annotationKey: String, annotationValue: Int) {
    PerfEventStore.markerAnnotate(this, annotationKey, annotationValue)
  }

  override fun markerAnnotate(annotationKey: String, annotationValue: Boolean) {
    PerfEventStore.markerAnnotate(this, annotationKey, annotationValue)
  }

  override fun markerAnnotate(annotationKey: String, annotationValue: Array<String>) {
    // Using Lists here only to make formatting easier. Otherwise we'd have to
    // keep separate stores for the types (which would be the right thing to do).
    PerfEventStore.markerAnnotate(this, annotationKey, annotationValue.toList())
  }

  override fun markerAnnotate(annotationKey: String, annotationValue: Array<Double>) {
    PerfEventStore.markerAnnotate(this, annotationKey, annotationValue.toList())
  }

  override fun markerAnnotate(annotationKey: String, annotationValue: IntArray) {
    PerfEventStore.markerAnnotate(this, annotationKey, annotationValue.toList())
  }

  override fun markerPoint(eventName: String) {
    PerfEventStore.markerPoint(this, eventName)
  }
}


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

package com.facebook.samples.litho.kotlin.mountables

import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.widget.EditText
import androidx.appcompat.widget.AppCompatEditText
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.SimpleMountable
import com.facebook.litho.Size
import com.facebook.litho.SizeSpec
import com.facebook.litho.Style
import com.facebook.litho.utils.MeasureUtils
import com.facebook.rendercore.MeasureResult

/**
 * This class is an oversimplified version of a TextInput MountableComponent. It was created for the
 * purpose of showcasing measure() function implemented using creating a View and measuring it.
 */
class SampleTextInput(
    private val initialText: CharSequence = "",
    private val hint: CharSequence? = "",
    private val inputBackground: Drawable = ColorDrawable(Color.TRANSPARENT),
    private val style: Style? = null
) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    return MountableRenderResult(
        SampleTextInputMountable(initialText, hint, inputBackground), style)
  }
}

internal class SampleTextInputMountable(
    private val initialText: CharSequence,
    private val hint: CharSequence?,
    private val inputBackground: Drawable,
) : SimpleMountable<EditText>(RenderType.VIEW) {

  override fun createContent(context: Context): EditText {
    return EditText(context)
  }

  // start_measure_with_view_measurement
  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult {
    // The height should be the measured height of EditText with relevant params
    val editTextForMeasure: EditText = AppCompatEditText(androidContext)

    editTextForMeasure.hint = hint
    editTextForMeasure.background =
        getBackgroundOrDefault(
            androidContext,
            if (inputBackground === ColorDrawable(Color.TRANSPARENT)) editTextForMeasure.background
            else inputBackground)
    editTextForMeasure.setText(initialText)

    editTextForMeasure.measure(
        MeasureUtils.getViewMeasureSpec(widthSpec), MeasureUtils.getViewMeasureSpec(heightSpec))

    val size = Size()
    size.height = editTextForMeasure.measuredHeight

    // For width we always take all available space, or collapse to 0 if unspecified.
    if (SizeSpec.getMode(widthSpec) == SizeSpec.UNSPECIFIED) {
      size.width = 0
    } else {
      size.width = Math.min(SizeSpec.getSize(widthSpec), editTextForMeasure.measuredWidth)
    }
    return MeasureResult(size.width, size.height, null)
  }
  // end_measure_with_view_measurement

  override fun mount(c: Context, content: EditText, layoutData: Any?) {
    content.hint = hint
    content.background = getBackgroundOrDefault(c, inputBackground)
    content.setText(initialText)
  }

  override fun unmount(c: Context, content: EditText, layoutData: Any?) {
    content.hint = ""
    content.background = ColorDrawable(Color.TRANSPARENT)
    content.setText("")
  }

  override fun shouldUpdate(
      newMountable: SimpleMountable<EditText>,
      currentLayoutData: Any?,
      nextLayoutData: Any?
  ): Boolean {
    newMountable as SampleTextInputMountable
    return newMountable.initialText != initialText ||
        newMountable.hint != hint ||
        newMountable.inputBackground != inputBackground
  }

  // start_preallocation_example
  override fun canPreallocate(): Boolean = true

  override fun poolSize(): Int {
    return 10
  }
  // end_preallocation_example
}

private fun getBackgroundOrDefault(context: Context, specifiedBackground: Drawable): Drawable? {
  if (specifiedBackground === ColorDrawable(Color.TRANSPARENT)) {
    val attrs = intArrayOf(android.R.attr.background)
    val a = context.obtainStyledAttributes(null, attrs, android.R.attr.editTextStyle, 0)
    val defaultBackground = a.getDrawable(0)
    a.recycle()
    return defaultBackground
  }
  return specifiedBackground
}


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

package com.facebook.samples.litho.kotlin.mountables

import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.graphics.Color
import android.view.animation.LinearInterpolator
import com.facebook.litho.ClickEvent
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.useCached
import com.facebook.litho.useRef
import com.facebook.litho.useState
import com.facebook.litho.view.onClick
import com.facebook.litho.widget.canvas.CanvasComponent
import com.facebook.litho.widget.canvas.CanvasScope
import com.facebook.litho.widget.canvas.Gradient
import com.facebook.litho.widget.canvas.Path
import com.facebook.litho.widget.canvas.Shading
import com.facebook.litho.widget.canvas.Shape
import com.facebook.litho.widget.canvas.Transform
import com.facebook.litho.widget.canvas.at
import com.facebook.mountable.utils.types.BlendingMode
import com.facebook.mountable.utils.types.Point
import com.facebook.mountable.utils.types.Size
import com.facebook.mountable.utils.types.center

class SimpleCanvasExampleComponent : KComponent() {

  override fun ComponentScope.render(): Component {
    val dashPhase = useState { 0f }
    val ringsRotation = useState { 0f }
    val heartScale = useState { 1f }

    val animators = useRef<AnimatorSet?> { null }

    val startAnimatorOnClick: (ClickEvent) -> Unit = {
      animators.value?.cancel()
      animators.value = AnimatorSet()

      animators.value?.playTogether(
          ValueAnimator.ofFloat(0f, 40f).apply {
            duration = 600
            interpolator = LinearInterpolator()
            repeatCount = ValueAnimator.INFINITE
            addUpdateListener { dashPhase.update(it.animatedValue as Float) }
          },
          ValueAnimator.ofFloat(0f, 360f).apply {
            duration = 2000
            interpolator = LinearInterpolator()
            repeatCount = ValueAnimator.INFINITE
            addUpdateListener { ringsRotation.update(it.animatedValue as Float) }
          },
          ValueAnimator.ofFloat(1f, 1.5f).apply {
            duration = 400
            interpolator = LinearInterpolator()
            repeatCount = ValueAnimator.INFINITE
            addUpdateListener { heartScale.update(it.animatedValue as Float) }
          })

      animators.value?.start()
    }

    val circleRadius = 40f
    val circlePath = useCached { Path { circle(Point(circleRadius, circleRadius), circleRadius) } }

    // create 3 rings shape
    val ringsSize = Size(130f, 130f)
    val ringsPath = useCached {
      Path {
        add(
            Path {
              add(circlePath)
              add(circlePath, Transform { translate(dx = 50f) })
              add(circlePath, Transform { translate(dx = 30f, dy = 50f) })
            },
            // translate to avoid stroke clipping
            Transform { translate(dx = 4f, dy = 4f) })
      }
    }

    // A heart shape, the path data taken from some random svg found online
    val heartSize = Size(110f, 95f)
    val heartPath = useCached {
      Path {
        add(
            Path {
              moveTo(Point(75f, 40f))
              cubicTo(Point(75f, 37f), Point(70f, 25f), Point(50f, 25f))
              cubicTo(Point(20f, 25f), Point(20f, 62.5f), Point(20f, 62.5f))
              cubicTo(Point(20f, 80f), Point(40f, 102f), Point(75f, 120f))
              cubicTo(Point(110f, 102f), Point(130f, 80f), Point(130f, 62.5f))
              cubicTo(Point(130f, 62.5f), Point(130f, 25f), Point(100f, 25f))
              cubicTo(Point(85f, 25f), Point(75f, 37f), Point(75f, 40f))
            },
            // the heart path starts at 20,25 so translate it to make it start at 0,0 in order to
            // make
            // positioning easier
            Transform { translate(dx = -20f, dy = -25f) })
      }
    }

    val squareSize = Size(80f, 80f)

    return Column {
      child(
          CanvasComponent(style = Style.onClick(action = startAnimatorOnClick)) {
            // fill 4 circles in 4 canvas corners
            drawTopLeftCircle(circleRadius)
            drawTopRightCircle(circleRadius)
            drawBottomLeftCircle(circleRadius)
            drawBottomRightCircle(circleRadius)

            // draw filled group clipped to round rect shape with a rotating, gradient stroked three
            // rings shape inside
            drawRotatingRingsInsideGrayShape(ringsPath, ringsSize, ringsRotation.value)

            //  draw rect with a "marching ants" effect https://en.wikipedia.org/wiki/Marching_ants
            drawMarchingAntsRect(dashPhase.value)

            // draw a circle shape filled with a radial gradient that will be used as a background
            // for a layer
            drawGradientCircle()

            // use a layer to draw a rectangle with an animated heart shaped hole on top of the
            // gradient circle
            drawLayerWithHeartShapedHole(heartPath, heartSize, heartScale.value)

            // draw two rotated intersected squares with multiply blending mode
            drawLeftRect(squareSize)
            drawRightRect(squareSize)
          })
    }
  }

  private fun CanvasScope.drawTopLeftCircle(circleRadius: Float) {
    fill(
        Shape.circle(Point(circleRadius, circleRadius), circleRadius),
        Shading.solidColor(Color.RED))
  }

  private fun CanvasScope.drawTopRightCircle(circleRadius: Float) {
    group(
        Transform {
          translate(dx = size.width)
          translate(dx = -circleRadius)
        }) {
          fill(Shape.circle(Point(0f, circleRadius), circleRadius), Shading.solidColor(Color.CYAN))
        }
  }

  private fun CanvasScope.drawBottomLeftCircle(circleRadius: Float) {
    group(Transform { translate(dy = size.height) }) {
      fill(
          Shape.circle(Point(circleRadius, -circleRadius), circleRadius),
          Shading.solidColor(Color.BLUE))
    }
  }

  private fun CanvasScope.drawBottomRightCircle(circleRadius: Float) {
    group(Transform { translate(dx = size.width, dy = size.height) }) {
      fill(
          Shape.circle(Point(-circleRadius, -circleRadius), circleRadius),
          Shading.solidColor(Color.GREEN))
    }
  }

  private fun CanvasScope.drawRotatingRingsInsideGrayShape(
      ringsPath: Path,
      ringsSize: Size,
      ringsRotation: Float
  ) {
    val roundRectSize = Size(size.width * 0.75f, size.height * 0.33f)
    val roundedRectPath = Path {
      rect(
          topLeft = Point.Zero,
          size = roundRectSize,
          // radius will be clipped to at most half of the width of shorter border
          cornerRadius = 5000f)
    }
    group(
        transform = Transform { translate(dx = size.width * 0.125f, dy = 100f) },
        size = roundRectSize,
        clip = roundedRectPath) {
          // fill rect that is the same size as current group, it'll be clipped to the group clip
          // path
          fill(Shape.rect(Point.Zero, size), Shading.solidColor(Color.GRAY))

          // stroke the rings using a linear gradient in the center of the current group
          drawRotatingRings(ringsPath, ringsSize, ringsRotation)
        }
  }

  private fun CanvasScope.drawRotatingRings(
      ringsPath: Path,
      ringsSize: Size,
      ringsRotation: Float
  ) {
    group(
        Transform {
          translate(
              dx = (center - ringsSize.center).x - 4f, dy = (center - ringsSize.center).y - 4f)
          rotate(ringsRotation, center)
        }) {
          stroke(
              Shape.path(ringsPath),
              Shading.linearGradient(
                  Gradient(Color.RED, Color.GREEN, Color.BLUE),
                  Point.Zero,
                  Point(ringsSize.width, ringsSize.height)),
              lineWidth = 4f,
              dashLengths = floatArrayOf(8f, 4f))
        }
  }

  private fun CanvasScope.drawMarchingAntsRect(dashPhase: Float) {
    val antsRectSize = Size(400f, 80f)
    group(
        transform =
            Transform {
              translate(
                  dx = (center - antsRectSize.center).x - 2f,
                  dy = (center - antsRectSize.center).y - 2f)
            }) {
          stroke(
              shape = Shape.rect(topLeft = Point(2f, 2f), size = antsRectSize),
              shading = Shading.solidColor(Color.BLACK),
              lineWidth = 2f,
              dashLengths = floatArrayOf(4f, 6f),
              dashPhase = -dashPhase)
        }
  }

  private fun CanvasScope.drawGradientCircle() {
    val bigCircleRadius = 330f
    val bigCircleSize = Size(bigCircleRadius * 2f, bigCircleRadius * 2f)
    group(
        transform =
            Transform {
              translate(
                  dx = center.x - bigCircleRadius,
                  dy = size.height * 0.83f - bigCircleRadius - 100f)
            },
        size = bigCircleSize) {
          fill(
              Shape.rect(topLeft = Point.Zero, size = size, cornerRadius = bigCircleRadius),
              Shading.radialGradient(
                  gradient =
                      Gradient(Color.CYAN at 0f, Color.MAGENTA at 0.5f, Color.YELLOW at 1.0f),
                  center = center,
                  radius = bigCircleRadius))
        }
  }

  private fun CanvasScope.drawLayerWithHeartShapedHole(
      heartPath: Path,
      heartSize: Size,
      heartScale: Float
  ) {
    val layerSize = heartSize * 2f
    layer(
        Transform { translate(dx = center.x - heartSize.width, dy = size.height * 0.66f - 100f) },
        size = layerSize) {
          // fill rect that is the same size as the layer, it'll be clipped to the layer
          // bounds
          fill(Shape.rect(Point.Zero, size), Shading.solidColor(Color.GREEN))

          // cut the hole in the middle of the layer using heart shape
          drawAnimatedHeartShapedHole(heartPath, heartSize, heartScale)
        }
  }

  private fun CanvasScope.drawAnimatedHeartShapedHole(
      heartPath: Path,
      heartSize: Size,
      heartScale: Float
  ) {
    group(
        Transform {
          scale(heartScale, heartScale, heartSize.center)
          translate((size.center - heartSize.center).x, (size.center - heartSize.center).y)
        }) {
          fill(Shape.path(heartPath), Shading.solidColor(Color.BLUE), BlendingMode.Xor)
        }
  }

  private fun CanvasScope.drawLeftRect(squareSize: Size) {
    group(
        Transform {
          rotate(45f, squareSize.center)
          translate(dx = size.center.x - squareSize.width, dy = size.height * 0.83f)
        }) {
          fill(
              Shape.rect(Point.Zero, squareSize),
              Shading.solidColor(Color.BLUE),
              blendingMode = BlendingMode.Multiply)
        }
  }

  private fun CanvasScope.drawRightRect(squareSize: Size) {
    group(
        Transform {
          rotate(45f, squareSize.center)
          translate(dx = size.center.x - 10f, dy = size.height * 0.83f)
        }) {
          fill(
              Shape.rect(Point.Zero, squareSize),
              Shading.solidColor(Color.GREEN),
              blendingMode = BlendingMode.Multiply)
        }
  }
}


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

package com.facebook.samples.litho.kotlin.mountables

import android.content.Context
import android.widget.ImageView
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.SimpleMountable
import com.facebook.litho.SizeSpec
import com.facebook.litho.Style
import com.facebook.rendercore.MeasureResult
import com.facebook.samples.litho.R

// start_simple_mountable_component_example
class SimpleImageViewComponent(private val style: Style? = null) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    return MountableRenderResult(SimpleImageViewMountable(), style)
  }
}
// end_simple_mountable_component_example

private const val defaultSize: Int = 150

// start_simple_mountable_example
internal class SimpleImageViewMountable() : SimpleMountable<ImageView>(RenderType.VIEW) {

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

  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult {
    return if (SizeSpec.getMode(widthSpec) == SizeSpec.UNSPECIFIED &&
        SizeSpec.getMode(heightSpec) == SizeSpec.UNSPECIFIED) {
      MeasureResult(defaultSize, defaultSize)
    } else {
      withEqualSize(widthSpec, heightSpec)
    }
  }

  override fun mount(c: Context, content: ImageView, layoutData: Any?) {
    content.setImageDrawable(c.resources.getDrawable(R.drawable.ic_launcher))
  }

  override fun unmount(c: Context, content: ImageView, layoutData: Any?) {
    content.setImageResource(0)
  }
}
// end_simple_mountable_example


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

package com.facebook.samples.litho.kotlin.mountables

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.margin
import com.facebook.litho.core.padding
import com.facebook.litho.core.width
import com.facebook.litho.dp
import com.facebook.litho.kotlin.widget.Text

class SimpleImageViewExampleComponent : KComponent() {

  override fun ComponentScope.render(): Component {
    return Column(style = Style.padding(all = 20.dp)) {
      child(
          SimpleImageViewComponent(style = Style.width(100.dp).height(100.dp).margin(all = 50.dp)))
      child(Text("Litho logo rendered using a Mountable Component", textSize = 16f.dp))
    }
  }
}


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

package com.facebook.samples.litho.kotlin.mountables

import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import com.facebook.litho.AccessibilityRole
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.Style
import com.facebook.litho.accessibility.ImportantForAccessibility
import com.facebook.litho.accessibility.accessibilityRole
import com.facebook.litho.accessibility.accessibilityRoleDescription
import com.facebook.litho.accessibility.importantForAccessibility
import com.facebook.litho.accessibility.onInitializeAccessibilityNodeInfo

// start_simple_mountable_component_with_a11y_example
class SimpleImageViewWithAccessibility(private val style: Style? = null) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    val a11y =
        Style.accessibilityRole(AccessibilityRole.IMAGE)
            .accessibilityRoleDescription("Image View")
            .importantForAccessibility(ImportantForAccessibility.YES)
            .onInitializeAccessibilityNodeInfo {
              it.superDelegate.onInitializeAccessibilityNodeInfo(it.host, it.info)
              it.info.addAction(
                  AccessibilityNodeInfoCompat.AccessibilityActionCompat(
                      AccessibilityNodeInfoCompat.ACTION_CLICK, "actionDescriptionText"))
            }

    return MountableRenderResult(SimpleImageViewMountable(), a11y + style)
  }
}
// end_simple_mountable_component_with_a11y_example


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

package com.facebook.samples.litho.kotlin.mountables

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.core.height
import com.facebook.litho.core.margin
import com.facebook.litho.core.padding
import com.facebook.litho.core.width
import com.facebook.litho.dp
import com.facebook.litho.kotlin.widget.Text

class SimpleImageViewWithAccessibilityExampleComponent : KComponent() {

  override fun ComponentScope.render(): Component {
    return Column(style = Style.padding(all = 20.dp)) {
      child(
          SimpleImageViewWithAccessibility(
              style = Style.width(100.dp).height(100.dp).margin(all = 50.dp)))
      child(
          Text(
              "Litho logo with a11y features rendered using a Mountable Component",
              textSize = 16f.dp))
    }
  }
}


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

package com.facebook.samples.litho.kotlin.mountables.bindto

import android.content.Context
import android.graphics.Color
import android.widget.ImageView
import com.facebook.litho.DynamicValue
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.SimpleMountable
import com.facebook.litho.SizeSpec
import com.facebook.litho.Style
import com.facebook.rendercore.MeasureResult
import com.facebook.samples.litho.R
import com.facebook.samples.litho.R.drawable.ic_launcher

// start_bindTo_imagecomponent_code
class ImageViewComponent(
    private val rotation: DynamicValue<Float>,
    private val background: DynamicValue<Float>,
    private val scale: DynamicValue<Float>,
    private val style: Style? = null
) : MountableComponent() {
  // end_bindTo_imagecomponent_code
  // start_bindTo_binding_code
  override fun MountableComponentScope.render(): MountableRenderResult {
    // simple binding
    rotation.bindTo(0f, ImageView::setRotation)
    scale.bindTo(1f, ImageView::setScaleX)
    scale.bindTo(1f, ImageView::setScaleY)

    // complex binding
    background.bindTo(0f) { view: ImageView, value ->
      view.setBackgroundColor(Color.HSVToColor(floatArrayOf(evaluate(value, 0f, 360f), 1f, 1f)))
    }

    // end_bindTo_binding_code

    return MountableRenderResult(ImageViewMountable(), style)
  }
}

private const val defaultSize: Int = 150

internal class ImageViewMountable() : SimpleMountable<ImageView>(RenderType.VIEW) {

  // create_content_example_start
  override fun createContent(context: Context): ImageView = ImageView(context)
  // create_content_example_end

  // measure_example_start
  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult {
    // if the sizes are unspecified, set default size; otherwise enforce equal sizes
    return if (SizeSpec.getMode(widthSpec) == SizeSpec.UNSPECIFIED &&
        SizeSpec.getMode(heightSpec) == SizeSpec.UNSPECIFIED) {
      MeasureResult(defaultSize, defaultSize)
    } else {
      withEqualSize(widthSpec, heightSpec)
    }
  }
  // measure_example_end

  // mount_unmount_example_start
  override fun mount(c: Context, content: ImageView, layoutData: Any?) {
    content.setImageDrawable(c.getResources().getDrawable(R.drawable.ic_launcher))
  }

  override fun unmount(c: Context, content: ImageView, layoutData: Any?) {
    content.setImageResource(0)
    content.rotation = 0f
    content.scaleX = 1f
    content.scaleY = 1f
    content.setBackgroundColor(Color.BLACK)
  }
  // mount_unmount_example_end
}

fun evaluate(fraction: Float, start: Float, end: Float): Float = start + fraction * (end - start)


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

package com.facebook.samples.litho.kotlin.mountables.bindto

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.animated.useBinding
import com.facebook.litho.core.height
import com.facebook.litho.core.margin
import com.facebook.litho.core.padding
import com.facebook.litho.core.width
import com.facebook.litho.dp
import com.facebook.samples.litho.kotlin.animations.dynamicprops.SeekBar

class MountableBindToExampleComponent : KComponent() {

  // start_bindTo_seekbar_code
  override fun ComponentScope.render(): Component? {
    val background = useBinding(50f)
    val rotation = useBinding(0f)
    val scale = useBinding(1f)
    return Column(style = Style.padding(all = 20.dp)) {
      child(
          SeekBar(
              initialValue = 0f,
              label = "background",
              onProgressChanged = { backgroundValue -> background.set(backgroundValue) }))
      child(
          SeekBar(
              initialValue = 0f,
              label = "rotation",
              onProgressChanged = { rotationValue ->
                rotation.set(evaluate(rotationValue, 0f, 360f))
              }))
      child(
          SeekBar(
              initialValue = 1f,
              label = "scale",
              onProgressChanged = { scaleValue -> scale.set(evaluate(scaleValue, .75f, 1.25f)) }))
      // end_bindTo_seekbar_code
      // start_bindTo_imagecomponent_def
      child(
          Column(style = Style.width(100.dp).height(100.dp).margin(all = 50.dp)) {
            child(ImageViewComponent(background = background, rotation = rotation, scale = scale))
          })
      // end_bindTo_imagecomponent_def

    }
  }
}


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

package com.facebook.samples.litho.kotlin.mountables.controllers

import android.widget.Toast
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentScope
import com.facebook.litho.KComponent
import com.facebook.litho.Style
import com.facebook.litho.core.padding
import com.facebook.litho.dp
import com.facebook.litho.useCached
import com.facebook.samples.litho.kotlin.collection.Button

class ControllersExampleComponent : KComponent() {
  // start_controllers_example
  override fun ComponentScope.render(): Component {
    return Column(style = Style.padding(16.dp)) {
      val initialHour = 14
      val initialMinute = 30
      val controller =
          useCached(initialHour, initialMinute) { TimePickerController(initialHour, initialMinute) }
      child(TimePickerComponent(controller = controller))
      child(
          Button(
              text = "Set random time ",
              onClick = {
                controller.setHour((0..24).random())
                controller.setMinute((0..60).random())
              }))
      // end_controllers_example
      child(
          Button(
              text = "Get current time",
              onClick = {
                Toast.makeText(
                        androidContext,
                        String.format("%02d:%02d", controller.getHour(), controller.getMinute()),
                        Toast.LENGTH_SHORT)
                    .show()
              }))
    }
  }
}


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

package com.facebook.samples.litho.kotlin.mountables.controllers

import android.content.Context
import android.os.Build
import android.widget.TimePicker
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.SimpleMountable
import com.facebook.litho.Style
import com.facebook.litho.ThreadUtils
import com.facebook.rendercore.MeasureResult

class TimePickerComponent(
    private val controller: TimePickerController? = null,
    private val style: Style? = null
) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    return MountableRenderResult(TimePickerMountable(controller), style)
  }
}
// mountable_component_start
internal class TimePickerMountable(
    private val controller: TimePickerController?,
) : SimpleMountable<TimePicker>(RenderType.VIEW) {

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

  override fun mount(c: Context, content: TimePicker, layoutData: Any?) {
    controller?.bind(content)
  }

  override fun unmount(c: Context, content: TimePicker, layoutData: Any?) {
    controller?.unbind()
  }
  // mountable_component_end

  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult {
    return withEqualSize(widthSpec, heightSpec)
  }

  override fun shouldUpdate(
      newMountable: SimpleMountable<TimePicker>,
      currentLayoutData: Any?,
      nextLayoutData: Any?
  ): Boolean {
    newMountable as TimePickerMountable
    return newMountable.controller != controller
  }
}

// start_controller_code_example
class TimePickerController(var currentHour: Int, var currentMinute: Int) {
  private val onTimeChangedListener =
      object : TimePicker.OnTimeChangedListener {
        override fun onTimeChanged(p0: TimePicker?, hour: Int, minute: Int) {
          currentHour = hour
          currentMinute = minute
        }
      }
  var content: TimePicker? = null

  fun getMinute(): Int? {
    ThreadUtils.assertMainThread()
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      content?.minute ?: currentMinute
    } else {
      content?.currentMinute ?: currentMinute
    }
  }

  fun setMinute(minute: Int) {
    ThreadUtils.assertMainThread()
    currentMinute = minute
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      content?.minute = minute
    } else {
      content?.currentMinute = minute
    }
  }
  // end_controller_code_example

  fun getHour(): Int? {
    ThreadUtils.assertMainThread()
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      content?.hour ?: currentHour
    } else {
      content?.currentHour ?: currentHour
    }
  }

  fun setHour(hour: Int) {
    ThreadUtils.assertMainThread()
    currentHour = hour
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      content?.hour = hour
    } else {
      content?.currentHour = hour
    }
  }
  // start_controller_bind_code_example
  fun bind(content: TimePicker) {
    this.content = content
    setHour(currentHour)
    setMinute(currentMinute)
    setTimeChangedListener()
  }

  fun unbind() {
    this.content?.setOnTimeChangedListener(null)
    this.content = null
  }
  // end_controller_unbind_code_example

  private fun setTimeChangedListener() {
    content?.setOnTimeChangedListener(onTimeChangedListener)
  }
}


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

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

package com.facebook.samples.litho.kotlin.mountables.widgets

import android.content.Context
import android.graphics.Color
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.SimpleMountable
import com.facebook.litho.Style
import com.facebook.litho.widget.CardClipDrawable
import com.facebook.litho.widget.CardClipDrawable.BOTTOM_LEFT
import com.facebook.litho.widget.CardClipDrawable.BOTTOM_RIGHT
import com.facebook.litho.widget.CardClipDrawable.NONE
import com.facebook.litho.widget.CardClipDrawable.TOP_LEFT
import com.facebook.litho.widget.CardClipDrawable.TOP_RIGHT
import com.facebook.rendercore.MeasureResult

/**
 * A component that paints rounded edges to mimic a clipping operation on the component being
 * rendered below it. Used in {@link CardSpec}.
 *
 * @param clippingColor Color for corner clipping.
 * @param cornerRadius Radius for corner clipping.
 * @param disableClipTopLeft If set, opt out of clipping the top-left corner
 * @param disableClipTopRight If set, opt out of clipping the top-right corner
 * @param disableClipBottomLeft If set, opt out of clipping the bottom-left corner
 * @param disableClipBottomRight If set, opt out of clipping the bottom-right corner
 */
class CardClip(
    private val clippingColor: Int = Color.WHITE,
    private val cornerRadius: Float? = null,
    private val disableClipTopLeft: Boolean = false,
    private val disableClipTopRight: Boolean = false,
    private val disableClipBottomLeft: Boolean = false,
    private val disableClipBottomRight: Boolean = false,
    private val style: Style? = null
) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult =
      MountableRenderResult(
          CardClipMountable(
              clippingColor = clippingColor,
              cornerRadius = cornerRadius,
              disableClipTopLeft = disableClipTopLeft,
              disableClipTopRight = disableClipTopRight,
              disableClipBottomLeft = disableClipBottomLeft,
              disableClipBottomRight = disableClipBottomRight),
          style)
}

internal class CardClipMountable(
    private val clippingColor: Int,
    private val cornerRadius: Float? = null,
    private val disableClipTopLeft: Boolean,
    private val disableClipTopRight: Boolean,
    private val disableClipBottomLeft: Boolean,
    private val disableClipBottomRight: Boolean,
) : SimpleMountable<CardClipDrawable>(RenderType.DRAWABLE) {

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

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

  override fun mount(c: Context, content: CardClipDrawable, layoutData: Any?) {
    clippingColor?.let { content.setClippingColor(clippingColor) }
    cornerRadius?.let { content.setCornerRadius(cornerRadius) }
    val clipEdge =
        ((if (disableClipTopLeft) TOP_LEFT else NONE) or
            (if (disableClipTopRight) TOP_RIGHT else NONE) or
            (if (disableClipBottomLeft) BOTTOM_LEFT else NONE) or
            if (disableClipBottomRight) BOTTOM_RIGHT else NONE)
    content.setDisableClip(clipEdge)
  }

  override fun unmount(c: Context, content: CardClipDrawable, layoutData: Any?) {
    content.setCornerRadius(0f)
    content.setClippingColor(Color.WHITE)
    content.setDisableClip(NONE)
  }
}


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

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

package com.facebook.samples.litho.kotlin.mountables.widgets

import android.content.Context
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.SimpleMountable
import com.facebook.litho.Style
import com.facebook.litho.widget.CardShadowDrawable
import com.facebook.rendercore.MeasureResult

/**
 * A component that is able to render the card's shadow.
 *
 * @param shadowStartColor Start color for the shadow.
 * @param shadowEndColor End color for the shadow.
 * @param cornerRadius Corner radius for the card that shows the shadow.
 * @param shadowSize Size of the shadow.
 * @param shadowDx The x offset of the shadow.
 * @param shadowDy The y offset of the shadow.
 */
class CardShadow(
    private val shadowStartColor: Int? = null,
    private val shadowEndColor: Int? = null,
    private val cornerRadius: Float? = null,
    private val shadowSize: Float? = null,
    private val shadowDx: Float = CardShadowDrawable.UNDEFINED,
    private val shadowDy: Float = CardShadowDrawable.UNDEFINED,
    private val hideTopShadow: Boolean = false,
    private val hideBottomShadow: Boolean = false,
    private val shadowLeftSizeOverride: Float = CardShadowDrawable.UNDEFINED,
    private val shadowRightSizeOverride: Float = CardShadowDrawable.UNDEFINED,
    private val style: Style? = null
) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    return MountableRenderResult(
        CardShadowMountable(
            shadowStartColor = shadowStartColor,
            shadowEndColor = shadowEndColor,
            cornerRadius = cornerRadius,
            shadowSize = shadowSize,
            shadowDx = shadowDx,
            shadowDy = shadowDy,
            hideTopShadow = hideTopShadow,
            hideBottomShadow = hideBottomShadow,
            shadowLeftSizeOverride = shadowLeftSizeOverride,
            shadowRightSizeOverride = shadowRightSizeOverride),
        style)
  }
}

internal class CardShadowMountable(
    private val shadowStartColor: Int?,
    private val shadowEndColor: Int?,
    private val cornerRadius: Float?,
    private val shadowSize: Float?,
    private val shadowDx: Float,
    private val shadowDy: Float,
    private val hideTopShadow: Boolean,
    private val hideBottomShadow: Boolean,
    private val shadowLeftSizeOverride: Float,
    private val shadowRightSizeOverride: Float,
) : SimpleMountable<CardShadowDrawable>(RenderType.DRAWABLE) {

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

  override fun mount(c: Context, content: CardShadowDrawable, layoutData: Any?) {
    shadowStartColor?.let { content.setShadowStartColor(shadowStartColor) }
    shadowEndColor?.let { content.setShadowEndColor(shadowEndColor) }
    cornerRadius?.let { content.setCornerRadius(cornerRadius) }
    shadowSize?.let { content.setShadowSize(shadowSize) }
    content.setHideTopShadow(hideTopShadow)
    content.setHideBottomShadow(hideBottomShadow)
    content.setShadowLeftSizeOverride(shadowLeftSizeOverride)
    content.setShadowRightSizeOverride(shadowRightSizeOverride)
    content.setShadowDx(shadowDx)
    content.setShadowDy(shadowDy)
  }

  override fun unmount(c: Context, content: CardShadowDrawable, layoutData: Any?) {
    content.setCornerRadius(0f)
    content.setShadowSize(0f)
    content.setHideTopShadow(false)
    content.setHideBottomShadow(false)
    content.setShadowLeftSizeOverride(0f)
    content.setShadowRightSizeOverride(0f)
    content.setShadowDx(0f)
    content.setShadowDy(0f)
  }

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


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

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

package com.facebook.samples.litho.kotlin.mountables.widgets

import android.content.Context
import android.view.View
import android.view.ViewTreeObserver
import android.widget.HorizontalScrollView
import com.facebook.litho.Component
import com.facebook.litho.ComponentTree
import com.facebook.litho.LithoLayoutContextExtraData.LithoLayoutExtraData
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.R
import com.facebook.litho.SimpleMountable
import com.facebook.litho.Size
import com.facebook.litho.SizeSpec
import com.facebook.litho.Style
import com.facebook.litho.useState
import com.facebook.litho.widget.HorizontalScrollEventsController
import com.facebook.litho.widget.HorizontalScrollLithoView
import com.facebook.litho.widget.ScrollStateListener
import com.facebook.rendercore.MeasureResult
import com.facebook.yoga.YogaDirection
import kotlin.math.max

private const val LAST_SCROLL_POSITION_UNSET = -1
private const val DEFAULT_OVER_SCROLL_MODE = View.OVER_SCROLL_IF_CONTENT_SCROLLS

fun HorizontalScroll(
    scrollbarEnabled: Boolean = true,
    eventsController: HorizontalScrollEventsController? = null,
    onScrollChangeListener: HorizontalScrollLithoView.OnScrollChangeListener? = null,
    scrollStateListener: ScrollStateListener? = null,
    incrementalMountEnabled: Boolean = false,
    overScrollMode: Int = DEFAULT_OVER_SCROLL_MODE,
    initialScrollPosition: Int = LAST_SCROLL_POSITION_UNSET,
    fillViewport: Boolean = false,
    style: Style? = null,
    child: () -> Component,
): Component {

  return HorizontalScroll(
      scrollbarEnabled = scrollbarEnabled,
      eventsController = eventsController,
      onScrollChangeListener = onScrollChangeListener,
      scrollStateListener = scrollStateListener,
      incrementalMountEnabled = incrementalMountEnabled,
      overScrollMode = overScrollMode,
      initialScrollPosition = initialScrollPosition,
      fillViewport = fillViewport,
      child = child(),
      style = style,
  )
}

class HorizontalScroll(
    private val scrollbarEnabled: Boolean = true,
    private val eventsController: HorizontalScrollEventsController? = null,
    private val onScrollChangeListener: HorizontalScrollLithoView.OnScrollChangeListener? = null,
    private val scrollStateListener: ScrollStateListener? = null,
    private val incrementalMountEnabled: Boolean = false,
    private val overScrollMode: Int = DEFAULT_OVER_SCROLL_MODE,
    private val initialScrollPosition: Int = LAST_SCROLL_POSITION_UNSET,
    private val fillViewport: Boolean = false,
    private val child: Component,
    private val style: Style? = null
) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult {
    val lastScrollPosition = useState {
      HorizontalScrollLithoView.ScrollPosition(initialScrollPosition)
    }

    val childComponentTree = useState {
      ComponentTree.createNestedComponentTree(context, child)
          .incrementalMount(incrementalMountEnabled)
          .build()
    }

    val resolvedScrollbarEnabled =
        getIntAttrValue(
            context,
            R.styleable.HorizontalScroll_android_scrollbars,
            R.styleable.HorizontalScroll,
            scrollbarEnabled)

    return MountableRenderResult(
        mountable =
            HorizontalScrollMountable(
                scrollbarEnabled = resolvedScrollbarEnabled,
                eventsController = eventsController,
                onScrollChangeListener = onScrollChangeListener,
                scrollStateListener = scrollStateListener,
                overScrollMode = overScrollMode,
                lastScrollPosition = lastScrollPosition.value,
                fillViewport = fillViewport,
                childComponent = child,
                childComponentTree = childComponentTree.value),
        style = style)
  }
}

internal class HorizontalScrollMountable(
    private val scrollbarEnabled: Boolean,
    private val eventsController: HorizontalScrollEventsController?,
    private val onScrollChangeListener: HorizontalScrollLithoView.OnScrollChangeListener?,
    private val scrollStateListener: ScrollStateListener?,
    private val overScrollMode: Int,
    private val lastScrollPosition: HorizontalScrollLithoView.ScrollPosition,
    private val fillViewport: Boolean,
    private val childComponent: Component,
    private val childComponentTree: ComponentTree
) : SimpleMountable<HorizontalScrollLithoView>(RenderType.VIEW) {

  override fun doesMountRenderTreeHosts(): Boolean = true

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

  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult {
    val size = Size()
    val width = max(0, SizeSpec.getSize(widthSpec))

    // Measure the component with undefined width spec, as the contents of the
    // hscroll have unlimited horizontal space.
    childComponentTree.setRootAndSizeSpecSync(
        childComponent, SizeSpec.makeSizeSpec(0, SizeSpec.UNSPECIFIED), heightSpec, size)

    size.width = max(0, max(size.width, if (fillViewport) width else 0))
    size.height = max(0, size.height)

    val extraLayoutData: LithoLayoutExtraData? =
        layoutContext.layoutContextExtraData?.extraLayoutData as? LithoLayoutExtraData?
    val direction = extraLayoutData?.layoutDirection ?: DEFAULT_LAYOUT_DIRECTION
    return MeasureResult(
        size.width, size.height, HorizontalScrollLayoutData(size.width, size.height, direction))
  }

  override fun mount(c: Context, content: HorizontalScrollLithoView, layoutData: Any?) {
    val horizontalScrollLayoutData = layoutData as HorizontalScrollLayoutData

    content.isHorizontalScrollBarEnabled = scrollbarEnabled
    content.overScrollMode = overScrollMode
    content.mount(
        childComponentTree,
        lastScrollPosition,
        horizontalScrollLayoutData.measuredWidth,
        horizontalScrollLayoutData.measuredHeight,
        onScrollChangeListener,
        scrollStateListener)

    val viewTreeObserver: ViewTreeObserver = content.viewTreeObserver
    viewTreeObserver.addOnPreDrawListener(
        object : ViewTreeObserver.OnPreDrawListener {
          override fun onPreDraw(): Boolean {
            content.viewTreeObserver.removeOnPreDrawListener(this)
            if (lastScrollPosition.x == LAST_SCROLL_POSITION_UNSET) {
              if (horizontalScrollLayoutData.layoutDirection == YogaDirection.RTL) {
                content.fullScroll(HorizontalScrollView.FOCUS_RIGHT)
              }
              lastScrollPosition.x = content.scrollX
            } else {
              content.scrollX = lastScrollPosition.x
            }
            return true
          }
        })

    eventsController?.setScrollableView(content)
  }

  override fun unmount(c: Context, content: HorizontalScrollLithoView, layoutData: Any?) {
    content.unmount()

    eventsController?.setScrollableView(null)
  }

  companion object {
    private val DEFAULT_LAYOUT_DIRECTION = YogaDirection.LTR
  }
}

internal data class HorizontalScrollLayoutData(
    val measuredWidth: Int,
    val measuredHeight: Int,
    val layoutDirection: YogaDirection
)


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

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

package com.facebook.samples.litho.kotlin.mountables.widgets

import android.content.Context
import android.graphics.drawable.Drawable
import android.widget.ImageView.ScaleType
import com.facebook.litho.DrawableMatrix
import com.facebook.litho.MatrixDrawable
import com.facebook.litho.MeasureScope
import com.facebook.litho.MountableComponent
import com.facebook.litho.MountableComponentScope
import com.facebook.litho.MountableRenderResult
import com.facebook.litho.SimpleMountable
import com.facebook.litho.SizeSpec
import com.facebook.litho.SizeSpec.UNSPECIFIED
import com.facebook.litho.Style
import com.facebook.litho.drawable.DrawableUtils
import com.facebook.rendercore.MeasureResult

/**
 * A component to render a [Drawable].
 *
 * Tip: Use the same instance of the [drawable] to avoid remounting it in subsequent layouts to
 * improve performance.
 *
 * @param drawable The [Drawable] to render.
 * @param scaleType The [ScaleType] to scale or transform the [drawable].
 */
class Image(
    val drawable: Drawable,
    val scaleType: ScaleType? = ScaleType.FIT_XY,
    val style: Style? = null,
) : MountableComponent() {

  override fun MountableComponentScope.render(): MountableRenderResult =
      MountableRenderResult(ImageMountable(drawable, scaleType ?: ScaleType.FIT_XY), style)
}

/**
 * The [SimpleMountable] used by the [Image]. It uses a [MatrixDrawable], and [DrawableMatrix] to
 * correctly render the actual [drawable] inside it.
 */
internal class ImageMountable(
    val drawable: Drawable,
    val scaleType: ScaleType,
) : SimpleMountable<MatrixDrawable<Drawable>>(RenderType.DRAWABLE) {

  override fun createContent(context: Context): MatrixDrawable<Drawable> {
    return MatrixDrawable()
  }

  override fun MeasureScope.measure(widthSpec: Int, heightSpec: Int): MeasureResult {
    val intrinsicWidth = drawable.intrinsicWidth
    val intrinsicHeight = drawable.intrinsicHeight

    val result =
        if (SizeSpec.getMode(widthSpec) == UNSPECIFIED &&
            SizeSpec.getMode(heightSpec) == UNSPECIFIED) {
          MeasureResult(intrinsicWidth, intrinsicHeight)
        } else {
          forAspectRatio(
              widthSpec,
              heightSpec,
              intrinsicWidth,
              intrinsicHeight,
              intrinsicWidth.toFloat() / intrinsicHeight.toFloat())
        }

    val matrix =
        if (scaleType == ScaleType.FIT_XY || intrinsicWidth <= 0 || intrinsicHeight <= 0) {
          null
        } else {
          DrawableMatrix.create(drawable, scaleType, result.width, result.height)
        }

    val useLayoutSize = ScaleType.FIT_XY == scaleType || intrinsicWidth <= 0 || intrinsicHeight <= 0

    return MeasureResult(
        result.width,
        result.height,
        ImageLayoutData(
            if (useLayoutSize) result.width else intrinsicWidth,
            if (useLayoutSize) result.height else intrinsicHeight,
            matrix))
  }

  override fun mount(c: Context, content: MatrixDrawable<Drawable>, layoutData: Any?) {
    layoutData as ImageLayoutData
    content.mount(drawable, layoutData.matrix)
    content.bind(layoutData.width, layoutData.height)
  }

  override fun unmount(c: Context, content: MatrixDrawable<Drawable>, layoutData: Any?) {
    content.unmount()
  }

  override fun shouldUpdate(
      newMountable: SimpleMountable<MatrixDrawable<Drawable>>,
      currentLayoutData: Any?,
      nextLayoutData: Any?
  ): Boolean {
    newMountable as ImageMountable
    return (newMountable.scaleType != scaleType ||
        !DrawableUtils.isEquivalentTo(newMountable.drawable, drawable))
  }

  override fun poolSize(): Int = 30
}

/** The layout data required by the [ImageMountable] to mount, and bind the drawable in the host. */
class ImageLayoutData(val width: Int, val height: Int, val matrix: DrawableMatrix? = null)