/*
 * Copyright (c) Tor Norbye.
 *
 * 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.ktfmt.kdoc

import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class UtilitiesTest {
  @Test
  fun testFindSamePosition() {
    fun check(newWithCaret: String, oldWithCaret: String) {
      val oldCaretIndex = oldWithCaret.indexOf('|')
      val newCaretIndex = newWithCaret.indexOf('|')
      assertThat(oldCaretIndex != -1).isTrue()
      assertThat(newCaretIndex != -1).isTrue()
      val old = oldWithCaret.substring(0, oldCaretIndex) + oldWithCaret.substring(oldCaretIndex + 1)
      val new = newWithCaret.substring(0, newCaretIndex) + newWithCaret.substring(newCaretIndex + 1)
      val newPos = findSamePosition(old, oldCaretIndex, new)

      val actual = new.substring(0, newPos) + "|" + new.substring(newPos)
      assertThat(actual).isEqualTo(newWithCaret)
    }

    // Prefix match
    check("|/** Test\n Different Middle End */", "|/** Test2 End */")
    check("/|** Test\n Different Middle End */", "/|** Test2 End */")
    check("/*|* Test\n Different Middle End */", "/*|* Test2 End */")
    check("/**| Test\n Different Middle End */", "/**| Test2 End */")
    check("/** |Test\n Different Middle End */", "/** |Test2 End */")
    check("/** T|est\n Different Middle End */", "/** T|est2 End */")
    check("/** Te|st\n Different Middle End */", "/** Te|st2 End */")
    check("/** Tes|t\n Different Middle End */", "/** Tes|t2 End */")
    check("/** Test|\n Different Middle End */", "/** Test|2 End */")
    // End match
    check("/** Test\n Different Middle| End */", "/** Test2| End */")
    check("/** Test\n Different Middle E|nd */", "/** Test2 E|nd */")
    check("/** Test\n Different Middle En|d */", "/** Test2 En|d */")
    check("/** Test\n Different Middle End| */", "/** Test2 End| */")
    check("/** Test\n Different Middle End |*/", "/** Test2 End |*/")
    check("/** Test\n Different Middle End *|/", "/** Test2 End *|/")
    check("/** Test\n Different Middle End */|", "/** Test2 End */|")

    check("|/**\nTest End\n*/", "|/** Test End */")
    check("/|**\nTest End\n*/", "/|** Test End */")
    check("/*|*\nTest End\n*/", "/*|* Test End */")
    check("/**|\nTest End\n*/", "/**| Test End */")
    check("/**\n|Test End\n*/", "/** |Test End */")
    check("/**\nT|est End\n*/", "/** T|est End */")
    check("/**\nTe|st End\n*/", "/** Te|st End */")
    check("/**\nTes|t End\n*/", "/** Tes|t End */")
    check("/**\nTest| End\n*/", "/** Test| End */")
    check("/**\nTest |End\n*/", "/** Test |End */")
    check("/**\nTest E|nd\n*/", "/** Test E|nd */")
    check("/**\nTest En|d\n*/", "/** Test En|d */")
    check("/**\nTest End|\n*/", "/** Test End| */")
    check("/**\nTest End\n|*/", "/** Test End |*/")
    check("/**\nTest End\n*|/", "/** Test End *|/")
    check("/**\nTest End\n*/|", "/** Test End */|")

    check("|/** Test End */", "|/** Test2 End */")
    check("/|** Test End */", "/|** Test2 End */")
    check("/*|* Test End */", "/*|* Test2 End */")
    check("/**| Test End */", "/**| Test2 End */")
    check("/** |Test End */", "/** |Test2 End */")
    check("/** T|est End */", "/** T|est2 End */")
    check("/** Te|st End */", "/** Te|st2 End */")
    check("/** Tes|t End */", "/** Tes|t2 End */")
    check("/** Test| End */", "/** Test|2 End */")
    check("/** Test |End */", "/** Test2 |End */")
    check("/** Test E|nd */", "/** Test2 E|nd */")
    check("/** Test En|d */", "/** Test2 En|d */")
    check("/** Test End| */", "/** Test2 End| */")
    check("/** Test End |*/", "/** Test2 End |*/")
    check("/** Test End *|/", "/** Test2 End *|/")
    check("/** Test End */|", "/** Test2 End */|")
  }

  @Test
  fun testGetParamName() {
    assertThat("@param foo".getParamName()).isEqualTo("foo")
    assertThat("@param foo bar".getParamName()).isEqualTo("foo")
    assertThat("@param foo;".getParamName()).isEqualTo("foo")
    assertThat("  \t@param\t   foo  bar.".getParamName()).isEqualTo("foo")
    assertThat("@param[foo]".getParamName()).isEqualTo("foo")
    assertThat("@param  [foo]".getParamName()).isEqualTo("foo")
    assertThat("@param ".getParamName()).isNull()
  }

  @Test
  fun testComputeWords() {
    fun List<String>.describe(): String {
      return "listOf(${this.joinToString(", ") { "\"$it\"" }})"
    }
    fun check(text: String, expected: List<String>, customizeParagraph: (Paragraph) -> Unit = {}) {
      val task = FormattingTask(KDocFormattingOptions(12), "/** $text */", "")
      val paragraph = Paragraph(task)
      paragraph.content.append(text)
      customizeParagraph(paragraph)
      val words = paragraph.computeWords()

      assertThat(words.describe()).isEqualTo(expected.describe())
    }
    check("Foo", listOf("Foo"))
    check("Foo Bar Baz", listOf("Foo", "Bar", "Baz"))
    check("Foo Bar Baz", listOf("Foo Bar", "Baz")) { it.quoted = true }
    check("Foo Bar Baz", listOf("Foo Bar", "Baz")) { it.hanging = true }
    check("1. Foo", listOf("1.", "Foo"))
    // "1." can't start a word; if it ends up at the beginning of a line it becomes
    // a numbered element.
    check("Foo 1.", listOf("Foo 1."))
    check("Foo bar [Link Text] foo bar.", listOf("Foo", "bar", "[Link Text]", "foo", "bar."))
    check("Interval [0, 1) foo bar.", listOf("Interval [0, 1)", "foo", "bar."))

    // ">" cannot start a word; it would become quoted text
    check("if >= 3", listOf("if >=", "3"))
    check("if >= 3.", listOf("if >= 3."))

    check(
        "SDK version - [`Partial(Mode.UseIfAvailable)`](Partial) on API 24+",
        listOf("SDK", "version - [`Partial(Mode.UseIfAvailable)`](Partial)", "on", "API", "24+"))

    check(
        "Z orders can range from Integer.MIN_VALUE to Integer.MAX_VALUE. Default z order " +
            " index is 0. [SurfaceControlWrapper] instances are positioned back-to-front.",
        listOf(
            "Z",
            "orders",
            "can",
            "range",
            "from",
            "Integer.MIN_VALUE",
            "to",
            "Integer.MAX_VALUE.",
            "Default",
            "z",
            "order",
            "index",
            "is 0. [SurfaceControlWrapper]",
            "instances",
            "are",
            "positioned",
            "back-to-front."))
    check(
        "Equates to `cmd package compile -f -m speed <package>` on API 24+.",
        listOf(
            "Equates",
            "to",
            "`cmd",
            "package",
            "compile",
            "-f",
            "-m",
            "speed",
            "<package>`",
            "on",
            "API",
            "24+."))
  }
}


/*
 * 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.ktfmt.testutil

import com.facebook.ktfmt.debughelpers.PrintAstVisitor
import com.facebook.ktfmt.format.Formatter
import com.facebook.ktfmt.format.FormattingOptions
import com.facebook.ktfmt.format.Parser
import com.google.common.truth.FailureMetadata
import com.google.common.truth.Subject
import com.google.common.truth.Truth
import org.junit.Assert

/**
 * Verifies the given code passes through formatting, and stays the same at the end
 *
 * @param code a code string that continas an optional first line made of "---" in the case
 *   [deduceMaxWidth] is true. For example:
 * ```
 * --------------------
 * // exactly 20 `-` above
 * fun f()
 * ```
 *
 * @param deduceMaxWidth if this is true the code string should start with a line of "-----" in the
 *   beginning to indicate the max width to format by
 */
fun assertFormatted(
    code: String,
    formattingOptions: FormattingOptions = FormattingOptions(),
    deduceMaxWidth: Boolean = false
) {
  val first = code.lines().first()
  var deducedCode = code
  var maxWidth = FormattingOptions.DEFAULT_MAX_WIDTH
  val isFirstLineAMaxWidthMarker = first.all { it == '-' }
  if (deduceMaxWidth) {
    if (!isFirstLineAMaxWidthMarker) {
      throw RuntimeException(
          "deduceMaxWidth is false, please remove the first dashes only line from the code (i.e. ---)")
    }
    deducedCode = code.substring(code.indexOf('\n') + 1)
    maxWidth = first.length
  } else {
    if (isFirstLineAMaxWidthMarker) {
      throw RuntimeException(
          "When deduceMaxWidth is true the first line need to be all dashes only (i.e. ---)")
    }
  }
  assertThatFormatting(deducedCode)
      .withOptions(formattingOptions.copy(maxWidth = maxWidth))
      .isEqualTo(deducedCode)
}

fun assertThatFormatting(code: String): FormattedCodeSubject {
  fun codes(): Subject.Factory<FormattedCodeSubject, String> {
    return Subject.Factory { metadata, subject ->
      FormattedCodeSubject(metadata, checkNotNull(subject))
    }
  }
  return Truth.assertAbout(codes()).that(code)
}

class FormattedCodeSubject(metadata: FailureMetadata, private val code: String) :
    Subject(metadata, code) {
  private var options: FormattingOptions = FormattingOptions()
  private var allowTrailingWhitespace = false

  fun withOptions(options: FormattingOptions): FormattedCodeSubject {
    this.options = options
    return this
  }

  fun allowTrailingWhitespace(): FormattedCodeSubject {
    this.allowTrailingWhitespace = true
    return this
  }

  fun isEqualTo(expectedFormatting: String) {
    if (!allowTrailingWhitespace && expectedFormatting.lines().any { it.endsWith(" ") }) {
      throw RuntimeException(
          "Expected code contains trailing whitespace, which the formatter usually doesn't output:\n" +
              expectedFormatting
                  .lines()
                  .map { if (it.endsWith(" ")) "[$it]" else it }
                  .joinToString("\n"))
    }
    val actualFormatting: String
    try {
      actualFormatting = Formatter.format(options, code)
      if (actualFormatting != expectedFormatting) {
        reportError(code)
        println("# Output: ")
        println("#".repeat(20))
        println(actualFormatting)
        println("# Expected: ")
        println("#".repeat(20))
        println(expectedFormatting)
        println("#".repeat(20))
        println(
            "Need more information about the break operations?" +
                "Run test with assertion with \"FormattingOptions(debuggingPrintOpsAfterFormatting = true)\"")
      }
    } catch (e: Error) {
      reportError(code)
      throw e
    }
    Assert.assertEquals(expectedFormatting, actualFormatting)
  }

  private fun reportError(code: String) {
    val file = Parser.parse(code)
    println("# Parse tree of input: ")
    println("#".repeat(20))
    file.accept(PrintAstVisitor())
    println()
    println("# Input: ")
    println("#".repeat(20))
    println(code)
    println()
  }
}


/*
 * 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.ktfmt.onlineformatter

import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.facebook.ktfmt.cli.ParsedArgs
import com.facebook.ktfmt.format.Formatter
import com.google.gson.Gson
import java.io.ByteArrayOutputStream
import java.io.PrintStream

class Handler : RequestHandler<APIGatewayProxyRequestEvent, String> {
  init {
    // Warm up.
    gson.toJson(Formatter.format(Formatter.KOTLINLANG_FORMAT, "/* foo */ fun foo() {}"))
  }

  override fun handleRequest(event: APIGatewayProxyRequestEvent, context: Context?): String {
    return gson.toJson(
        try {
          val request = gson.fromJson(event.body, Request::class.java)
          val parsingErrors = ByteArrayOutputStream()
          val style = request.style
          val parsedArgs =
              ParsedArgs.parseOptions(
                  PrintStream(parsingErrors), if (style == null) arrayOf() else arrayOf(style))
          Response(
              Formatter.format(
                  parsedArgs.formattingOptions.copy(maxWidth = request.maxWidth ?: 100),
                  request.source ?: ""),
              parsingErrors.toString().ifEmpty { null })
        } catch (e: Exception) {
          Response(null, e.message)
        })
  }

  companion object {
    val gson = Gson()
  }
}

class Request {
  var source: String? = ""
  var style: String? = ""
  var maxWidth: Int? = 100
}

data class Response(val source: String?, val err: 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.ktfmt.onlineformatter

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import kotlin.test.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class HandlerTest {

  @Test
  fun dontCrash_success() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( ) { }","style": "--kotlinlang-style"}"""
    val response = Handler().handleRequest(event, null)
    assertEquals("""{"source":"fun foo() {}\n"}""", response)
  }

  @Test
  fun dontCrash_malformedRequestJson() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( ) { }"style": "--kotlinlang-style"}"""
    val response = Handler().handleRequest(event, null)
    assertEquals(
        """{"err":"com.google.gson.stream.MalformedJsonException: """ +
            """Unterminated object at line 1 column 30 path ${'$'}.source"}""",
        response)
  }

  @Test
  fun dontCrash_emptyRequestJson() {
    val event = APIGatewayProxyRequestEvent()
    event.body = ""
    val response = Handler().handleRequest(event, null)
    assertEquals("{}", response)
  }

  @Test
  fun dontCrash_malformedSourceCode() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( { }","style": "--kotlinlang-style"}"""
    val response = Handler().handleRequest(event, null)
    assertEquals("""{"err":"1:10: error: Expecting \u0027)\u0027"}""", response)
  }

  @Test
  fun dontCrash_allNullRequest() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{}"""
    val response = Handler().handleRequest(event, null)
    assertEquals("""{"source":"\n"}""", response)
  }

  @Test
  fun dontCrash_malformedStyle() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( ) { }","style": "blabla"}"""
    val response = Handler().handleRequest(event, null)
    assertEquals("""{"source":"fun foo() {}\n"}""", response)
  }

  @Test
  fun dontCrash_noStyle() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( ) { }"}"""
    val response = Handler().handleRequest(event, null)
    assertEquals("""{"source":"fun foo() {}\n"}""", response)
  }

  @Test
  fun dontCrash_negativeMaxWidth() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( ) { }", "maxWidth": -100}"""
    val response = Handler().handleRequest(event, null)
    assertEquals("""{"source":"fun foo() {}\n"}""", response)
  }

  /** maxWidth == null is treated as if it were 100 */
  @Test
  fun dontCrash_nullMaxWidth() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( ) { }", "maxWidth": null}"""
    val response = Handler().handleRequest(event, null)
    assertEquals("""{"source":"fun foo() {}\n"}""", response)
  }

  @Test
  fun dontCrash_nonIntegerMaxWidth() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( ) { }", "maxWidth": "bla"}"""
    val response = Handler().handleRequest(event, null)
    assertEquals(
        """{"err":"java.lang.NumberFormatException: For input string: \"bla\""}""", response)
  }

  @Test
  fun dontCrash_coerceMaxWidthToInt() {
    val event = APIGatewayProxyRequestEvent()
    event.body = """{"source": "fun foo ( ) { }", "maxWidth": "100"}"""
    val response = Handler().handleRequest(event, null)
    assertEquals("""{"source":"fun foo() {}\n"}""", response)
  }
}


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

package com.facebook.litho.codelab.events

import com.facebook.litho.annotations.Event

@Event
class BoxItemChangedEvent {
  @JvmField var newColor = 0
  @JvmField var newStatus = ""
  @JvmField var highlightedItemIndex = -1
}


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

package com.facebook.litho.codelab.events

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Row
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaAlign

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

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop text: String): Component {
    return Row.create(c, 0, R.style.Widget_AppCompat_Button_Small)
        .clickable(true)
        .child(Text.create(c).alignSelf(YogaAlign.CENTER).textSizeSp(20f).text(text))
        .build()
  }
}


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

package com.facebook.litho.codelab.events

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.InvisibleEvent
import com.facebook.litho.LongClickEvent
import com.facebook.litho.Row
import com.facebook.litho.VisibleEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Param
import com.facebook.litho.annotations.Prop
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaWrap

@Suppress("MagicNumber")
@LayoutSpec(events = [BoxItemChangedEvent::class])
object ColorBoxCollectionSpec {

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop items: IntArray,
      @Prop highlightedIndex: Int
  ): Component {
    val rowBuilder = Row.create(c).wrap(YogaWrap.WRAP)
    items.forEachIndexed { index, color ->
      val isHighlighted = index == highlightedIndex
      rowBuilder.child(
          Row.create(c)
              .marginDip(YogaEdge.ALL, 5f)
              .widthDip(50f * if (isHighlighted) 1.2f else 1f)
              .heightDip(50f * if (isHighlighted) 1.2f else 1f)
              .backgroundColor(color)
              .longClickHandler(ColorBoxCollection.onLongClick(c, color, index))
              .visibleHandler(ColorBoxCollection.onItemVisible(c, color, index))
              .invisibleHandler(ColorBoxCollection.onItemInvisible(c, color, index)))
    }
    return rowBuilder.build()
  }

  @OnEvent(VisibleEvent::class)
  fun onItemVisible(c: ComponentContext, @Param color: Int, @Param index: Int) {
    ColorBoxCollection.dispatchBoxItemChangedEvent(
        ColorBoxCollection.getBoxItemChangedEventHandler(c),
        color,
        "Item at index $index is now visible",
        -1)
  }

  @OnEvent(InvisibleEvent::class)
  fun onItemInvisible(c: ComponentContext, @Param color: Int, @Param index: Int) {
    ColorBoxCollection.dispatchBoxItemChangedEvent(
        ColorBoxCollection.getBoxItemChangedEventHandler(c),
        color,
        "Item at index $index is no longer visible",
        -1)
  }

  @OnEvent(LongClickEvent::class)
  fun onLongClick(c: ComponentContext, @Param color: Int, @Param index: Int): Boolean {
    ColorBoxCollection.dispatchBoxItemChangedEvent(
        ColorBoxCollection.getBoxItemChangedEventHandler(c),
        color,
        "Item at index $index was highlighted",
        index)
    return true
  }
}


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

package com.facebook.litho.codelab.events

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

class LithoApp : Application() {

  override fun onCreate() {
    super.onCreate()

    SoLoader.init(this, false)
  }
}


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

package com.facebook.litho.codelab.events

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

class MainActivity : AppCompatActivity() {

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

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


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

package com.facebook.litho.codelab.events

import android.graphics.Color
import com.facebook.litho.ClickEvent
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Row
import com.facebook.litho.StateValue
import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateInitialState
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.OnUpdateState
import com.facebook.litho.annotations.Param
import com.facebook.litho.annotations.State
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaJustify
import kotlin.random.Random

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

  @OnCreateInitialState
  fun onCreateInitialState(
      c: ComponentContext,
      items: StateValue<IntArray>,
      statusText: StateValue<String>,
      statusColor: StateValue<Int>,
      highlightedIndex: StateValue<Int>
  ) {
    items.set(IntArray(0))
    statusText.set("")
    statusColor.set(Color.TRANSPARENT)
    highlightedIndex.set(-1)
  }

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @State items: IntArray,
      @State statusText: String,
      @State statusColor: Int,
      @State highlightedIndex: Int
  ): Component {
    return Column.create(c)
        .paddingDip(YogaEdge.ALL, 20f)
        .child(
            Row.create(c)
                .justifyContent(YogaJustify.CENTER)
                .child(
                    Button.create(c).text("ADD").clickHandler(RootComponent.onClickEvent(c, true)))
                .child(
                    Button.create(c)
                        .marginDip(YogaEdge.LEFT, 20f)
                        .text("REMOVE")
                        .clickHandler(RootComponent.onClickEvent(c, false))))
        .child(
            Text.create(c)
                .alignSelf(YogaAlign.CENTER)
                .textSizeSp(18f)
                .text(statusText)
                .textColor(statusColor))
        .child(
            ColorBoxCollection.create(c)
                .items(items)
                .highlightedIndex(highlightedIndex)
                .boxItemChangedEventHandler(RootComponent.onBoxItemChangedEvent(c)))
        .build()
  }

  @OnEvent(ClickEvent::class)
  fun onClickEvent(c: ComponentContext, @State items: IntArray, @Param add: Boolean) {
    if (add) {
      val newColor = Color.rgb(Random.nextInt(256), Random.nextInt(256), Random.nextInt(256))
      RootComponent.updateItems(c, items.plus(newColor))
    } else {
      if (items.isNotEmpty()) {
        RootComponent.updateItems(c, items.sliceArray(0..items.size - 2))
      }
    }
  }

  @OnEvent(BoxItemChangedEvent::class)
  fun onBoxItemChangedEvent(
      c: ComponentContext,
      @FromEvent newColor: Int,
      @FromEvent newStatus: String,
      @FromEvent highlightedItemIndex: Int
  ) {
    RootComponent.updateTextStatus(c, newColor, newStatus, highlightedItemIndex)
  }

  @OnUpdateState
  fun updateItems(items: StateValue<IntArray>, @Param newItems: IntArray) {
    items.set(newItems)
  }

  @OnUpdateState
  fun updateTextStatus(
      statusText: StateValue<String>,
      statusColor: StateValue<Int>,
      highlightedIndex: StateValue<Int>,
      @Param newColor: Int,
      @Param newStatus: String,
      @Param newHighlightedIndex: Int
  ) {
    statusText.set(newStatus)
    statusColor.set(newColor)
    highlightedIndex.set(newHighlightedIndex)
  }
}


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

package com.facebook.litho.codelab

import android.os.SystemClock
import com.facebook.litho.StateValue
import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.OnCreateInitialState
import com.facebook.litho.annotations.OnCreateTreeProp
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.OnUpdateState
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.State
import com.facebook.litho.sections.ChangesInfo
import com.facebook.litho.sections.Children
import com.facebook.litho.sections.LoadingEvent
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.SectionLifecycle
import com.facebook.litho.sections.annotations.GroupSectionSpec
import com.facebook.litho.sections.annotations.OnBindService
import com.facebook.litho.sections.annotations.OnCreateChildren
import com.facebook.litho.sections.annotations.OnCreateService
import com.facebook.litho.sections.annotations.OnDataBound
import com.facebook.litho.sections.annotations.OnDataRendered
import com.facebook.litho.sections.annotations.OnRefresh
import com.facebook.litho.sections.annotations.OnUnbindService
import com.facebook.litho.sections.annotations.OnViewportChanged
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.RenderEvent
import com.facebook.litho.widget.Card
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.RenderInfo
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaEdge

@Suppress("MagicNumber")
@GroupSectionSpec
object LifecycleGroupSectionSpec {

  @OnCreateInitialState
  fun onCreateInitialState(
      c: SectionContext,
      startTime: StateValue<Long>,
      @Prop lifecycleListener: LifecycleListener
  ) {
    val timestamp = SystemClock.uptimeMillis()
    startTime.set(timestamp)
    dispatchLifecycleEvent(LifecycleEventType.ON_CREATE_INITIAL_STATE, lifecycleListener, timestamp)
  }

  @OnCreateTreeProp
  fun onCreateTreeProp(
      c: SectionContext,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long
  ): DummyTreeProp {
    dispatchLifecycleEvent(LifecycleEventType.ON_CREATE_TREE_PROP, lifecycleListener, startTime)
    return DummyTreeProp
  }

  @OnCreateChildren
  fun onCreateChildren(
      c: SectionContext,
      @Prop zodiacs: List<Zodiac>,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long,
      @State scramble: Boolean
  ): Children {
    val children =
        Children.create()
            .child(
                DataDiffSection.create<Zodiac>(c)
                    .data(if (scramble) zodiacs.toMutableList().shuffled() else zodiacs)
                    .renderEventHandler(LifecycleGroupSection.onRender(c)))
            .build()
    dispatchLifecycleEvent(LifecycleEventType.ON_CREATE_CHILDREN, lifecycleListener, startTime)
    return children
  }

  @OnEvent(RenderEvent::class)
  fun onRender(c: SectionContext, @FromEvent model: Zodiac): RenderInfo {
    return ComponentRenderInfo.create()
        .component(
            Card.create(c)
                .content(
                    Text.create(c).text(model.animal).textSizeSp(20f).paddingDip(YogaEdge.ALL, 8f)))
        .build()
  }

  @OnDataBound
  fun onDataBound(
      c: SectionContext,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long
  ) {
    dispatchLifecycleEvent(LifecycleEventType.ON_DATA_BOUND, lifecycleListener, startTime)
  }

  @OnDataRendered
  fun onDataRendered(
      c: SectionContext,
      isDataChanged: Boolean,
      isMounted: Boolean,
      uptimeMillis: Long,
      firstVisibleIndex: Int,
      lastVisibleIndex: Int,
      changesInfo: ChangesInfo,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long
  ) {
    dispatchLifecycleEvent(LifecycleEventType.ON_DATA_RENDERED, lifecycleListener, startTime)
  }

  @OnViewportChanged
  fun onViewportChanged(
      c: SectionContext,
      firstVisibleIndex: Int,
      lastVisibleIndex: Int,
      totalCount: Int,
      firstFullyVisibleIndex: Int,
      lastFullyVisibleIndex: Int,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long
  ) {
    dispatchLifecycleEvent(LifecycleEventType.ON_VIEWPORT_CHANGED, lifecycleListener, startTime)
  }

  @OnRefresh
  fun onRefresh(
      c: SectionContext,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long
  ) {
    dispatchLifecycleEvent(LifecycleEventType.ON_REFRESH, lifecycleListener, startTime)
    LifecycleGroupSection.updateScramble(c)
    SectionLifecycle.dispatchLoadingEvent(c, false, LoadingEvent.LoadingState.SUCCEEDED, null)
  }

  @OnUpdateState
  fun updateScramble(scramble: StateValue<Boolean>) {
    scramble.set(scramble.get()?.not())
  }

  @OnCreateService
  fun onCreateService(
      c: SectionContext,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long
  ): Any {
    dispatchLifecycleEvent(LifecycleEventType.ON_CREATE_SERVICE, lifecycleListener, startTime)
    return Any()
  }

  @OnBindService
  fun onBindService(
      c: SectionContext,
      service: Any,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long
  ) {
    dispatchLifecycleEvent(LifecycleEventType.ON_BIND_SERVICE, lifecycleListener, startTime)
  }

  @OnUnbindService
  fun onUnbindService(
      c: SectionContext,
      service: Any,
      @Prop lifecycleListener: LifecycleListener,
      @State startTime: Long
  ) {
    dispatchLifecycleEvent(LifecycleEventType.ON_UNBIND_SERVICE, lifecycleListener, startTime)
  }

  private fun dispatchLifecycleEvent(
      type: LifecycleEventType,
      listener: LifecycleListener,
      startTime: Long
  ) {
    val endTime = SystemClock.uptimeMillis() - startTime
    listener.onLifecycleMethodCalled(type, endTime)
  }
}


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

package com.facebook.litho.codelab

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.widget.RecyclerCollectionComponent

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

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop lifecycleListener: LifecycleListener): Component {
    val zodiacs =
        listOf(
            Zodiac("Rat", 1),
            Zodiac("Ox", 2),
            Zodiac("Tiger", 3),
            Zodiac("Hare", 4),
            Zodiac("Dragon", 5),
            Zodiac("Snake", 6),
            Zodiac("Horse", 7),
            Zodiac("Sheep", 8),
            Zodiac("Monkey", 9),
            Zodiac("Rooster", 10),
            Zodiac("Dog", 11),
            Zodiac("Pig", 12))
    return RecyclerCollectionComponent.create(c)
        .section(
            LifecycleGroupSection.create(SectionContext(c))
                .zodiacs(zodiacs)
                .lifecycleListener(lifecycleListener)
                .build())
        .build()
  }
}


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

package com.facebook.litho.codelab

interface LifecycleListener {
  fun onLifecycleMethodCalled(eventType: LifecycleEventType, endTime: Long)
}

class LifecycleEvent(val eventType: LifecycleEventType, val endTime: Long)

enum class LifecycleEventType {
  ON_CREATE_INITIAL_STATE,
  ON_CREATE_TREE_PROP,
  ON_CREATE_SERVICE,
  ON_BIND_SERVICE,
  ON_UNBIND_SERVICE,
  ON_CREATE_CHILDREN,
  ON_DATA_BOUND,
  ON_DATA_RENDERED,
  ON_VIEWPORT_CHANGED,
  ON_REFRESH
}

object DummyTreeProp

data class Zodiac(val animal: String, val month: Int)


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

package com.facebook.litho.codelab

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

class LithoApp : Application() {

  override fun onCreate() {
    super.onCreate()

    SoLoader.init(this, false)
  }
}


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

package com.facebook.litho.codelab

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

class MainActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)

    val componentContext = ComponentContext(this)
    val lifecycleEvents = mutableListOf<LifecycleEvent>()

    val timelineView = findViewById<LithoView>(R.id.timeline)
    val lifecycleListener =
        object : LifecycleListener {
          override fun onLifecycleMethodCalled(type: LifecycleEventType, endTime: Long) {
            lifecycleEvents.add(LifecycleEvent(type, endTime))

            timelineView.setComponentAsync(
                TimelineRootComponent.create(componentContext)
                    .lifecycleEvents(lifecycleEvents.toList())
                    .build())
          }
        }

    val sectionView = findViewById<LithoView>(R.id.section)
    sectionView.setComponent(
        LifecycleRootComponent.create(componentContext)
            .lifecycleListener(lifecycleListener)
            .build())
  }
}


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

package com.facebook.litho.codelab

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

@LayoutSpec
object RootComponentSpec {

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext): Component {
    return Text.create(c).textSizeSp(20f).text("GroupSection Lifecycle").build()
  }
}


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

package com.facebook.litho.codelab.auxiliary

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Transition
import com.facebook.litho.animation.AnimatedProperties
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnCreateTransition
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.ResType
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaEdge

@Suppress("MagicNumber")
@LayoutSpec
object TimelineComponentSpec {
  private const val TRANSITION_KEY = "transitionKey"

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop(resType = ResType.STRING) type: String,
      @Prop(resType = ResType.COLOR) color: Int,
      @Prop timestamp: Long
  ): Component {
    return Column.create(c)
        .transitionKey(TRANSITION_KEY)
        .child(Text.create(c).text(type).textSizeSp(10f))
        .child(Text.create(c).text("${timestamp}ms").textSizeSp(10f))
        .alignContent(YogaAlign.CENTER)
        .heightDip(50f)
        .paddingDip(YogaEdge.ALL, 8f)
        .backgroundColor(color)
        .build()
  }

  @OnCreateTransition
  fun onCreateTransition(c: ComponentContext): Transition {
    return Transition.create(Transition.TransitionKeyType.GLOBAL, TRANSITION_KEY)
        .animate(AnimatedProperties.ALPHA)
        .appearFrom(0f)
        .animator(Transition.timing(800))
  }
}


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

package com.facebook.litho.codelab.auxiliary

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.StateValue
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateInitialState
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.State
import com.facebook.litho.codelab.LifecycleEvent
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.widget.GridRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.sections.widget.RecyclerCollectionEventsController

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

  @OnCreateInitialState
  fun onCreateInitialState(
      c: ComponentContext,
      eventsController: StateValue<RecyclerCollectionEventsController>
  ) {
    eventsController.set(RecyclerCollectionEventsController())
  }

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop lifecycleEvents: List<LifecycleEvent>,
      @State eventsController: RecyclerCollectionEventsController
  ): Component {
    return RecyclerCollectionComponent.create(c)
        .recyclerConfiguration(GridRecyclerConfiguration.create().numColumns(3).build())
        .disablePTR(true)
        .section(
            TimelineSection.create(SectionContext(c))
                .eventsController(eventsController)
                .events(lifecycleEvents))
        .eventsController(eventsController)
        .build()
  }
}


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

package com.facebook.litho.codelab.auxiliary

import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.codelab.LifecycleEvent
import com.facebook.litho.codelab.LifecycleEventType
import com.facebook.litho.codelab.R
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.OnDataBound
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.RenderEvent
import com.facebook.litho.sections.widget.RecyclerCollectionEventsController
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.RenderInfo

@GroupSectionSpec
object TimelineSectionSpec {

  @OnCreateChildren
  fun onCreateChildren(c: SectionContext, @Prop events: List<LifecycleEvent>): Children {
    return Children.create()
        .child(
            DataDiffSection.create<LifecycleEvent>(c)
                .data(events)
                .renderEventHandler(TimelineSection.onRender(c)))
        .build()
  }

  @OnEvent(RenderEvent::class)
  fun onRender(c: SectionContext, @FromEvent model: LifecycleEvent): RenderInfo {
    val (typeStringId, colorId) = genLifecycleEventPair(model)
    return ComponentRenderInfo.create()
        .component(
            TimelineComponent.create(c)
                .typeRes(typeStringId)
                .colorRes(colorId)
                .timestamp(model.endTime))
        .build()
  }

  @OnDataBound
  fun onDataBound(
      c: SectionContext,
      @Prop events: List<LifecycleEvent>,
      @Prop eventsController: RecyclerCollectionEventsController
  ) {
    eventsController.requestScrollToPosition(events.size - 1, false)
  }

  private fun genLifecycleEventPair(model: LifecycleEvent) =
      when (model.eventType) {
        LifecycleEventType.ON_CREATE_INITIAL_STATE ->
            R.string.onCreateInitialState to R.color.colorOnCreateInitialState
        LifecycleEventType.ON_CREATE_TREE_PROP ->
            R.string.onCreateTreeProp to R.color.colorOnCreateTreeProp
        LifecycleEventType.ON_CREATE_SERVICE ->
            R.string.onCreateService to R.color.colorOnCreateService
        LifecycleEventType.ON_BIND_SERVICE -> R.string.onBindService to R.color.colorOnBindService
        LifecycleEventType.ON_UNBIND_SERVICE ->
            R.string.onUnbindService to R.color.colorOnUnbindService
        LifecycleEventType.ON_CREATE_CHILDREN ->
            R.string.onCreateChildren to R.color.colorOnCreateChildren
        LifecycleEventType.ON_DATA_BOUND -> R.string.onDataBound to R.color.colorOnDataBound
        LifecycleEventType.ON_DATA_RENDERED ->
            R.string.onDataRendered to R.color.colorOnDataRendered
        LifecycleEventType.ON_VIEWPORT_CHANGED ->
            R.string.onViewportChanged to R.color.colorOnViewportChanged
        LifecycleEventType.ON_REFRESH -> R.string.onRefresh to R.color.colorOnRefresh
      }
}


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

package com.facebook.litho.codelab

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

class MainActivity : AppCompatActivity() {

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

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


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

package com.facebook.litho.codelab

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

@LayoutSpec
object RootComponentSpec {

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext): Component {
    return Text.create(c).textSizeSp(20f).text("Hello World").build()
  }
}


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

package com.facebook.litho.codelab

import androidx.recyclerview.widget.OrientationHelper
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.RenderEvent
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerBinderConfiguration
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.RenderInfo

/**
 * This component renders a horizontal list with items of various heights, which can adapt height to
 * always accommodate the height of the tallest item. If the height of the h-scroll is already
 * taller than the highest item it will not shrink to fit. Measuring the height this way is
 * extremely inefficient.
 */
@Suppress("MagicNumber")
@LayoutSpec
object DynamicHeightHscrollComponentSpec {

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop colors: List<Int>): Component {

    return RecyclerCollectionComponent.create(c)
        .recyclerConfiguration(
            ListRecyclerConfiguration.create()
                .recyclerBinderConfiguration(
                    RecyclerBinderConfiguration.create()
                        .hasDynamicItemHeight(true) // This enables dynamic height measurement.
                        .build())
                .orientation(OrientationHelper.HORIZONTAL)
                .build())
        .section(
            DataDiffSection.create<Int>(SectionContext(c))
                .data(colors)
                .renderEventHandler(DynamicHeightHscrollComponent.onRender(c))
                .build())
        .canMeasureRecycler(true)
        .build()
  }

  @OnEvent(RenderEvent::class)
  fun onRender(c: ComponentContext, @FromEvent model: Int, @FromEvent index: Int): RenderInfo {
    if (index == 0) {
      return ComponentRenderInfo.create()
          .component(
              ExpandibleComponent.create(c)
                  .color(model)
                  .collapseHeight(50f)
                  .expandHeight(250f)
                  .initialHeight(100f)
                  .widthDip(100f))
          .build()
    }

    if (index == 1) {
      return ComponentRenderInfo.create()
          .component(
              ExpandibleComponent.create(c)
                  .color(model)
                  .collapseHeight(50f)
                  .expandHeight(200f)
                  .initialHeight(200f)
                  .widthDip(100f))
          .build()
    }

    return ComponentRenderInfo.create()
        .component(
            ExpandibleComponent.create(c)
                .color(model)
                .collapseHeight(50f)
                .expandHeight(250f)
                .initialHeight(50f)
                .widthDip(100f))
        .build()
  }
}


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

package com.facebook.litho.codelab

import com.facebook.litho.ClickEvent
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.StateValue
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateInitialState
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.OnUpdateState
import com.facebook.litho.annotations.Param
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.State
import com.facebook.litho.widget.SolidColor

/**
 * Component which toggles between a maximum and minimum height when clicked, starting from an
 * initial height.
 */
@Suppress("MagicNumber")
@LayoutSpec
object ExpandibleComponentSpec {

  @OnCreateInitialState
  fun onCreateInitialState(
      c: ComponentContext,
      @Prop initialHeight: Float,
      currentHeight: StateValue<Float>
  ) {
    currentHeight.set(initialHeight)
  }

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop color: Int,
      @State currentHeight: Float
  ): Component {
    return SolidColor.create(c)
        .color(color)
        .heightDip(currentHeight)
        .widthDip(100f)
        .clickHandler(ExpandibleComponent.onClick(c))
        .build()
  }

  @OnUpdateState
  fun onUpdateState(
      currentHeight: StateValue<Float>,
      @Param collapse: Float,
      @Param expand: Float
  ) {
    val currentState = currentHeight.get()
    if (currentState != null) {
      if (currentState < expand) {
        currentHeight.set(expand)
      } else {
        currentHeight.set(collapse)
      }
    }
  }

  @OnEvent(ClickEvent::class)
  fun onClick(c: ComponentContext, @Prop collapseHeight: Float, @Prop expandHeight: Float) {
    ExpandibleComponent.onUpdateState(c, collapseHeight, expandHeight)
  }
}


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

package com.facebook.litho.codelab

import androidx.recyclerview.widget.OrientationHelper
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.RenderEvent
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.RenderInfo
import com.facebook.litho.widget.SolidColor

/**
 * This component renders a horizontal list. The height of the list is fixed, and it's set by
 * passing a height prop to the RecyclerCollectionComponent.
 */
@Suppress("MagicNumber")
@LayoutSpec
object FixedHeightHscrollComponentSpec {

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop colors: List<Int>): Component {
    return RecyclerCollectionComponent.create(c)
        .recyclerConfiguration(
            ListRecyclerConfiguration.create().orientation(OrientationHelper.HORIZONTAL).build())
        .section(
            DataDiffSection.create<Int>(SectionContext(c))
                .data(colors)
                .renderEventHandler(FixedHeightHscrollComponent.onRender(c))
                .build())
        .heightDip(150f)
        .build()
  }

  @OnEvent(RenderEvent::class)
  fun onRender(c: ComponentContext, @FromEvent model: Int): RenderInfo {
    return ComponentRenderInfo.create()
        .component(SolidColor.create(c).color(model).heightDip(100f).widthDip(100f))
        .build()
  }
}


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

package com.facebook.litho.codelab

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

class MainActivity : AppCompatActivity() {

  val colors = listOf(Color.BLACK, Color.BLUE, Color.GREEN, Color.YELLOW, Color.RED, Color.MAGENTA)

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

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


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

package com.facebook.litho.codelab

import androidx.recyclerview.widget.OrientationHelper
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.Prop
import com.facebook.litho.sections.SectionContext
import com.facebook.litho.sections.common.DataDiffSection
import com.facebook.litho.sections.common.RenderEvent
import com.facebook.litho.sections.widget.ListRecyclerConfiguration
import com.facebook.litho.sections.widget.RecyclerCollectionComponent
import com.facebook.litho.widget.ComponentRenderInfo
import com.facebook.litho.widget.RenderInfo
import com.facebook.litho.widget.SolidColor

/**
 * Renders a horizontal list who gets its height by measuring the first item in the list. This
 * option is enabled by enabling the `canMeasureRecycler` prop on the RecyclerCollectionComponent.
 */
@LayoutSpec
object MeasureFirstItemForHeightHscrollComponentSpec {

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop colors: List<Int>): Component {

    return RecyclerCollectionComponent.create(c)
        .recyclerConfiguration(
            ListRecyclerConfiguration.create().orientation(OrientationHelper.HORIZONTAL).build())
        .section(
            DataDiffSection.create<Int>(SectionContext(c))
                .data(colors)
                .renderEventHandler(MeasureFirstItemForHeightHscrollComponent.onRender(c))
                .build())
        .canMeasureRecycler(true)
        .build()
  }

  @OnEvent(RenderEvent::class)
  fun onRender(c: ComponentContext, @FromEvent model: Int, @FromEvent index: Int): RenderInfo {
    if (index == 0) {
      return ComponentRenderInfo.create()
          .component(SolidColor.create(c).color(model).heightDip(100f).widthDip(100f))
          .build()
    }

    if (index == 1) {
      return ComponentRenderInfo.create()
          .component(SolidColor.create(c).color(model).heightDip(200f).widthDip(100f))
          .build()
    }

    return ComponentRenderInfo.create()
        .component(SolidColor.create(c).color(model).heightDip(50f).widthDip(100f))
        .build()
  }
}


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

package com.facebook.litho.codelab

import android.graphics.Color
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.widget.Text

@LayoutSpec
object RootComponentSpec {

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @Prop colors: List<Int>): Component {

    return Column.create(c)
        .backgroundColor(0xF0F8FF)
        .child(Text.create(c).text("H-Scroll with fixed height").textSizeDip(15f))
        .child(FixedHeightHscrollComponent.create(c).backgroundColor(Color.GRAY).colors(colors))
        .child(
            Text.create(c).text("H-Scroll height based on height of first item").textSizeDip(15f))
        .child(
            MeasureFirstItemForHeightHscrollComponent.create(c)
                .backgroundColor(Color.GRAY)
                .colors(colors))
        .child(
            Text.create(c).text("H-Scroll takes the height of the tallest item").textSizeDip(15f))
        .child(DynamicHeightHscrollComponent.create(c).backgroundColor(Color.GRAY).colors(colors))
        .build()
  }
}


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

package com.facebook.litho.codelab

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable

class ClockDrawable : Drawable() {
  companion object {
    const val ONE_MINUTE = 60L * 1000
    const val ONE_HOUR = 60 * ONE_MINUTE
    const val TWELVE_HOURS = 12 * ONE_HOUR
  }

  private val paintThin = buildPaint(6)
  private val paintMedium = buildPaint(9)
  private val paintWide = buildPaint(12)

  private var longHandAngle = 0
  private var shortHandAngle = 0

  var radius = 0

  private fun buildPaint(strokeWidthPx: Int): Paint {
    return Paint().apply {
      color = Color.BLACK
      isAntiAlias = true
      style = Paint.Style.STROKE
      strokeWidth = strokeWidthPx.toFloat()
    }
  }

  private fun drawHand(
      canvas: Canvas,
      degrees: Float,
      length: Int,
      paint: Paint,
      centerX: Float,
      centerY: Float
  ) {
    // Save and then rotate canvas
    canvas.run {
      val savedCount = save()
      rotate(degrees, centerX, centerY)

      // Draw a hand
      drawLine(centerX, centerY, centerX, centerY - length, paint)

      // Restore canvas
      restoreToCount(savedCount)
    }
  }

  fun setTime(timeMillis: Long) {
    val inHourProgress = timeMillis.toFloat() % ONE_HOUR / ONE_HOUR
    longHandAngle = (360 * inHourProgress).toInt()

    val inTwelveHoursProgress = timeMillis.toFloat() / TWELVE_HOURS
    shortHandAngle = (360 * inTwelveHoursProgress).toInt()
  }

  override fun draw(canvas: Canvas) {
    val bounds = bounds

    canvas.run {
      val saveCount = save()
      translate(bounds.left.toFloat(), bounds.top.toFloat())

      drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), paintWide)
      drawHand(
          this,
          longHandAngle.toFloat(),
          radius * 2 / 3,
          paintThin,
          radius.toFloat(),
          radius.toFloat())
      drawHand(
          this,
          shortHandAngle.toFloat(),
          radius / 3,
          paintMedium,
          radius.toFloat(),
          radius.toFloat())

      restoreToCount(saveCount)
    }
  }

  override fun setAlpha(alpha: Int) {}

  override fun setColorFilter(colorFilter: ColorFilter?) {}

  override fun getOpacity(): Int {
    return PixelFormat.OPAQUE
  }
}


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

package com.facebook.litho.codelab

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

@MountSpec(isPureRender = true)
object ClockSpec {

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

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

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


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

package com.facebook.litho.codelab

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

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


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

package com.facebook.litho.codelab

import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Row
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.widget.Text
import com.facebook.yoga.YogaEdge
import com.facebook.yoga.YogaJustify
import java.util.Calendar
import java.util.TimeZone

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

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

    val londonTime = timeMillis(calendar)

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

    val newYorkTime = timeMillis(calendar)

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

    val sanFranciscoTime = timeMillis(calendar)

    return Row.create(c)
        .child(
            Column.create(c)
                .child(
                    Clock.create(c).timeMillis(londonTime).radius(210).marginDip(YogaEdge.ALL, 24f))
                .child(
                    Clock.create(c)
                        .timeMillis(newYorkTime)
                        .radius(210)
                        .marginDip(YogaEdge.ALL, 24f))
                .child(
                    Clock.create(c)
                        .timeMillis(sanFranciscoTime)
                        .radius(210)
                        .marginDip(YogaEdge.ALL, 24f))
                .justifyContent(YogaJustify.SPACE_AROUND))
        .child(
            Column.create(c)
                .child(Text.create(c).text("London").textSizeDip(24f))
                .child(Text.create(c).text("New York").textSizeDip(24f))
                .child(Text.create(c).text("San Francisco").textSizeDip(24f))
                .justifyContent(YogaJustify.SPACE_AROUND))
        .build()
  }

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


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

package com.facebook.litho.codelab

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

class LithoApp : Application() {

  override fun onCreate() {
    super.onCreate()

    SoLoader.init(this, false)
  }
}


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

package com.facebook.litho.codelab

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Row
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.yoga.YogaEdge

@LayoutSpec
object RootComponentSpec {
  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext): Component {
    return Row.create(c)
        .child(
            Clock.create(c)
                .timeMillis(System.currentTimeMillis())
                .radius(240)
                .marginDip(YogaEdge.ALL, 24f))
        .build()
  }
}


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

package com.facebook.litho.codelab.props

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

class LithoApp : Application() {

  override fun onCreate() {
    super.onCreate()

    SoLoader.init(this, false)
  }
}


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

package com.facebook.litho.codelab.props

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

class MainActivity : AppCompatActivity() {

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

    val componentContext = ComponentContext(this)
    setContentView(
        LithoView.create(
            this,
            RootComponent.create(componentContext)
                .name("Jane") // Using the prop to set the name.
                .build()))
  }
}


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

package com.facebook.litho.codelab.props

import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.Prop
import com.facebook.litho.widget.Text

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

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @Prop name: String // Creating a prop called 'name' using the '@Prop' annotation
  ): Component {
    return Text.create(c)
        .textSizeSp(20f)
        .text("Hello $name!") // Using name to set the 'text' prop of the Text component.
        .build()
  }
}


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

package com.facebook.litho.codelab

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.facebook.litho.ComponentContext
import com.facebook.litho.ComponentTree
import com.facebook.litho.LithoView
import com.facebook.litho.TreeState

class MainActivity : AppCompatActivity() {

  var mComponentTree: ComponentTree? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val treeStateViewModel = ViewModelProvider(this).get(TreeStateViewModel::class.java)

    val componentContext = ComponentContext(this)

    /**
     * When creating the ComponentTree, pass it the TreeState that you saved before the app
     * configuration changed. This will restore the state value.
     */
    mComponentTree =
        ComponentTree.create(componentContext, RootComponent.create(componentContext).build())
            .treeState(treeStateViewModel.getTreeState())
            .build()

    val lithoView = LithoView(componentContext)
    lithoView.setComponentTree(mComponentTree)

    setContentView(lithoView)
  }

  override fun onDestroy() {
    super.onDestroy()

    val treeStateViewModel = ViewModelProvider(this).get(TreeStateViewModel::class.java)

    /**
     * Before destroying the activity, save the TreeState so we can restore the state value after
     * the configuration change.
     */
    treeStateViewModel.updateTreeState(mComponentTree)
  }

  class TreeStateViewModel : ViewModel() {
    val treeStateData: MutableLiveData<TreeState> = MutableLiveData<TreeState>()

    fun getTreeState(): TreeState? {
      return treeStateData.getValue()
    }

    fun updateTreeState(componentTree: ComponentTree?) {
      if (componentTree != null) {
        /**
         * The current state values are wrapped in a TreeState that lives on the ComponentTree. call
         * acquireTreeState to obtain a copy.
         */
        treeStateData.setValue(componentTree.acquireTreeState())
      }
    }
  }
}


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

package com.facebook.litho.codelab

import com.facebook.litho.ClickEvent
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.StateValue
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.OnUpdateState
import com.facebook.litho.annotations.State
import com.facebook.litho.widget.Text

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

  @OnCreateLayout
  fun onCreateLayout(c: ComponentContext, @State counter: Int): Component {
    return Column.create(c)
        .child(
            Text.create(c)
                .textSizeSp(20f)
                .text("Click to increment")
                .clickHandler(RootComponent.onClick(c)))
        .child(Text.create(c).textSizeSp(20f).text(counter.toString()))
        .build()
  }

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

  @OnUpdateState
  fun increment(counter: StateValue<Int>) {
    val counterVal = counter.get()!!
    counter.set(counterVal + 1)
  }
}


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

package com.facebook.litho.codelab

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

class MainActivity : AppCompatActivity() {

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

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


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

package com.facebook.litho.codelab

import android.graphics.Color
import android.text.Layout
import android.util.Log
import android.widget.Toast
import com.facebook.litho.ClickEvent
import com.facebook.litho.Column
import com.facebook.litho.Component
import com.facebook.litho.ComponentContext
import com.facebook.litho.Row
import com.facebook.litho.StateValue
import com.facebook.litho.annotations.FromEvent
import com.facebook.litho.annotations.LayoutSpec
import com.facebook.litho.annotations.OnCreateInitialState
import com.facebook.litho.annotations.OnCreateLayout
import com.facebook.litho.annotations.OnEvent
import com.facebook.litho.annotations.OnUpdateState
import com.facebook.litho.annotations.Param
import com.facebook.litho.annotations.Prop
import com.facebook.litho.annotations.State
import com.facebook.litho.widget.Text
import com.facebook.litho.widget.TextChangedEvent
import com.facebook.litho.widget.TextInput
import com.facebook.yoga.YogaAlign
import com.facebook.yoga.YogaEdge
import java.lang.NumberFormatException

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

  private val COLOR_BG_DECREMENT = Color.parseColor("#ff7733")
  private val COLOR_TXT_DECREMENT = Color.parseColor("#aa4411")
  private val COLOR_BG_INCREMENT = Color.parseColor("#33ff77")
  private val COLOR_TXT_INCREMENT = Color.parseColor("#11aa77")
  private val COLOR_BG_COUNT = Color.parseColor("#efefef")
  private val COLOR_TXT_COUNT = Color.parseColor("#232323")

  @OnCreateInitialState
  fun onCreateInitialState(
      c: ComponentContext,
      @Prop(optional = true) startCount: Int,
      count: StateValue<Int>,
      step: StateValue<Int>
  ) {
    count.set(startCount) // set the initial value of count.
    step.set(1)
  }

  @OnCreateLayout
  fun onCreateLayout(
      c: ComponentContext,
      @State count: Int,
      @State(canUpdateLazily = true) step: Int
  ): Component {

    Log.d("RootComponent", "OnCreateLayout() called")

    return Column.create(c)
        .child(
            Row.create(c)
                .alignItems(YogaAlign.FLEX_START)
                .child(
                    Text.create(c)
                        .backgroundColor(COLOR_BG_DECREMENT)
                        .paddingDip(YogaEdge.LEFT, 16F)
                        .paddingDip(YogaEdge.RIGHT, 16F)
                        .paddingDip(YogaEdge.TOP, 4F)
                        .paddingDip(YogaEdge.BOTTOM, 4F)
                        .marginDip(YogaEdge.ALL, 8F)
                        .text("-")
                        .textSizeSp(24F)
                        .textColor(COLOR_TXT_DECREMENT)
                        /* Setting the click handler for the decrement button */
                        .clickHandler(RootComponent.onDecrement(c)))
                .child(
                    Text.create(c)
                        .backgroundColor(COLOR_BG_COUNT)
                        .paddingDip(YogaEdge.LEFT, 16F)
                        .paddingDip(YogaEdge.RIGHT, 16F)
                        .paddingDip(YogaEdge.TOP, 4F)
                        .paddingDip(YogaEdge.BOTTOM, 4F)
                        .marginDip(YogaEdge.ALL, 8F)
                        .minWidthDip(64f)
                        .text(count.toString())
                        .textSizeSp(24F)
                        .textColor(COLOR_TXT_COUNT)
                        .textAlignment(Layout.Alignment.ALIGN_CENTER))
                .child(
                    Text.create(c)
                        .backgroundColor(COLOR_BG_INCREMENT)
                        .paddingDip(YogaEdge.LEFT, 16F)
                        .paddingDip(YogaEdge.RIGHT, 16F)
                        .paddingDip(YogaEdge.TOP, 4F)
                        .paddingDip(YogaEdge.BOTTOM, 4F)
                        .marginDip(YogaEdge.ALL, 8F)
                        .text("+")
                        .textSizeSp(24F)
                        .textColor(COLOR_TXT_INCREMENT)
                        /* Setting the click handler for the increment button */
                        .clickHandler(RootComponent.onIncrement(c))))
        /* UI to change the value of `step` */
        .child(
            Row.create(c)
                .child(
                    Text.create(c)
                        .marginDip(YogaEdge.ALL, 8F)
                        .text("Change Step: ")
                        .textSizeSp(24F))
                .child(
                    TextInput.create(c)
                        .initialText(step.toString())
                        .minWidthDip(64F)
                        .textSizeSp(24F)
                        /* Setting the text change handler */
                        .textChangedEventHandler(RootComponent.onChangeStep(c))))
        .build()
  }

  /** The event handler for the decrement button. */
  @OnEvent(ClickEvent::class)
  fun onDecrement(c: ComponentContext) {
    RootComponent.decrement(c) // Call the decrement state update method.
  }

  /** Update method for decrement which receives the state value container for `count`. */
  @OnUpdateState
  fun decrement(count: StateValue<Int>, step: StateValue<Int>) {
    // Actually decrement the value of count.
    count.set(step.get()?.let { count.get()?.minus(it) } ?: count.get()?.minus(1))
  }

  /** The event handler for the increment button. */
  @OnEvent(ClickEvent::class)
  fun onIncrement(c: ComponentContext) {
    RootComponent.increment(c) // Call the increment state update method.
  }

  /** Update method for increment which receives the state value container of for `count`. */
  @OnUpdateState
  fun increment(count: StateValue<Int>, step: StateValue<Int>) {
    // Actually increment the value of count.
    count.set(step.get()?.let { count.get()?.plus(it) } ?: count.get()?.plus(1))
  }

  @OnEvent(TextChangedEvent::class)
  fun onChangeStep(c: ComponentContext, @FromEvent text: String) {
    var value = 1

    if (text.isEmpty()) {
      value = 0
    } else {
      try {
        value = text.toInt()
      } catch (nfe: NumberFormatException) {
        Toast.makeText(c.androidContext, "'$text' is not a valid number.", Toast.LENGTH_SHORT)
            .show()
      }
    }

    /*
      Lazy state update will dispatch the state update, but will only apply it in the next layout
      pass. See the log statement printed in the `OnCreateLayout`.
    */
    RootComponent.lazyUpdateStep(c, value)
  }

  @OnUpdateState
  fun changeStep(step: StateValue<Int>, @Param value: Int) {
    step.set(value)
  }
}


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

package com.facebook.litho.codelab.textinput

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable

class ClockDrawable : Drawable() {
  companion object {
    const val ONE_MINUTE = 60L * 1000
    const val ONE_HOUR = 60 * ONE_MINUTE
    const val TWELVE_HOURS = 12 * ONE_HOUR
  }

  private val paintThin = buildPaint(6)
  private val paintMedium = buildPaint(9)
  private val paintWide = buildPaint(12)

  private var longHandAngle = 0
  private var shortHandAngle = 0

  var radius = 0

  private fun buildPaint(strokeWidthPx: Int): Paint {
    return Paint().apply {
      color = Color.BLACK
      isAntiAlias = true
      style = Paint.Style.STROKE
      strokeWidth = strokeWidthPx.toFloat()
    }
  }

  private fun drawHand(
      canvas: Canvas,
      degrees: Float,
      length: Int,
      paint: Paint,
      centerX: Float,
      centerY: Float
  ) {
    // Save and then rotate canvas
    canvas.run {
      val savedCount = save()
      rotate(degrees, centerX, centerY)

      // Draw a hand
      drawLine(centerX, centerY, centerX, centerY - length, paint)

      // Restore canvas
      restoreToCount(savedCount)
    }
  }

  fun setTime(timeMillis: Long) {
    val inHourProgress = timeMillis.toFloat() % ONE_HOUR / ONE_HOUR
    longHandAngle = (360 * inHourProgress).toInt()

    val inTwelveHoursProgress = timeMillis.toFloat() / TWELVE_HOURS
    shortHandAngle = (360 * inTwelveHoursProgress).toInt()
  }

  override fun draw(canvas: Canvas) {
    val bounds = bounds

    canvas.run {
      val saveCount = save()
      translate(bounds.left.toFloat(), bounds.top.toFloat())

      drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), paintWide)
      drawHand(
          this,
          longHandAngle.toFloat(),
          radius * 2 / 3,
          paintThin,
          radius.toFloat(),
          radius.toFloat())
      drawHand(
          this,
          shortHandAngle.toFloat(),
          radius / 3,
          paintMedium,
          radius.toFloat(),
          radius.toFloat())

      restoreToCount(saveCount)
    }
  }

  override fun setAlpha(alpha: Int) {}

  override fun setColorFilter(colorFilter: ColorFilter?) {}

  override fun getOpacity(): Int {
    return PixelFormat.OPAQUE
  }
}