Sandbox IntegrationPointExampleNode throws exception when Activity recreated

diff --git a/core/src/main/java/com/bumble/appyx/core/integrationpoint/IntegrationPointStub.kt b/core/src/main/java/com/bumble/appyx/core/integrationpoint/IntegrationPointStub.kt
index 3c76e1b6..a0e78a2a 100644
--- a/core/src/main/java/com/bumble/appyx/core/integrationpoint/IntegrationPointStub.kt
+++ b/core/src/main/java/com/bumble/appyx/core/integrationpoint/IntegrationPointStub.kt
@@ -8,7 +8,8 @@ class IntegrationPointStub : IntegrationPoint(savedInstanceState = null) {
         private const val ERROR = "You're accessing an IntegrationPointStub. " +
             "This means you're using a Node without ever integrating it to a proper IntegrationPoint. " +
             "This is fine during tests with limited scope, but it looks like the code that leads here " +
-            "requires interfacing with a valid implementation."
+            "requires interfacing with a valid implementation. " +
+            "You may be attempting to access the IntegrationPoint before it is attached to the Node."
     }

     override val activityStarter: ActivityStarter
diff --git a/sandbox/src/main/java/com/bumble/appyx/sandbox/client/integrationpoint/IntegrationPointExampleNode.kt b/sandbox/src/main/java/com/bumble/appyx/sandbox/client/integrationpoint/IntegrationPointExampleNode.kt
index 695b2d41..e598b4db 100644
--- a/sandbox/src/main/java/com/bumble/appyx/sandbox/client/integrationpoint/IntegrationPointExampleNode.kt
+++ b/sandbox/src/main/java/com/bumble/appyx/sandbox/client/integrationpoint/IntegrationPointExampleNode.kt
@@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
+import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityStarter
 import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester
 import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeClient
 import com.bumble.appyx.core.minimal.reactive.Cancellable
@@ -34,8 +35,8 @@ import java.util.*
 class IntegrationPointExampleNode(buildContext: BuildContext) : Node(buildContext = buildContext),
     RequestCodeClient {

-    private val permissionRequester = integrationPoint.permissionRequester
-    private val activityStarter = integrationPoint.activityStarter
+    private val permissionRequester: PermissionRequester get() = integrationPoint.permissionRequester
+    private val activityStarter: ActivityStarter get() = integrationPoint.activityStarter
     private var permissionsResultCancellable: Cancellable? = null
     private var activityResultsCancellable: Cancellable? = null
     private var permissionsResultState by mutableStateOf("Press request permissions to check permissions state")
@@ -91,13 +92,13 @@ class IntegrationPointExampleNode(buildContext: BuildContext) : Node(buildContex
     }

     private fun launchActivity() {
-        integrationPoint.activityStarter.startActivity {
+        activityStarter.startActivity {
             Intent(this, StartActivityExample::class.java)
         }
     }

     private fun launchActivityForResult() {
-        integrationPoint.activityStarter.startActivityForResult(this, StartActivityForResultCode) {
+        activityStarter.startActivityForResult(this, StartActivityForResultCode) {
             Intent(this, StartActivityExample::class.java)
         }
     }

Sandbox IntegrationPointExampleNode throws exception when Activity recreated
The Sandbox IntegrationPointExampleNode class throws an exception when Activity recreated due to the integrationPoint not being attached yet.

We should change the node to not access the integrationPoint immediately, and also update the error message for clarity.

Demontration
Using the dark mode button to cause the Activity to be recreated.

[appyx-sandbox-lifecycle-crash.webm](https://user-images.githubusercontent.com/1333155/178144662-d73cdf0e-a42a-4687-bf1b-57cb1f213163.webm)

**Stacktrace**
```
java.lang.IllegalStateException: You're accessing an IntegrationPointStub. This means you're using a Node without ever integrating it to a proper IntegrationPoint. This is fine during tests with limited scope, but it looks like the code that leads here requires interfacing with a valid implementation.
	at com.bumble.appyx.core.integrationpoint.IntegrationPointStub.getPermissionRequester(IntegrationPointStub.kt:18)
	at com.bumble.appyx.sandbox.client.integrationpoint.IntegrationPointExampleNode.<init>(IntegrationPointExampleNode.kt:37)
	at com.bumble.appyx.sandbox.client.container.ContainerNode.resolve(ContainerNode.kt:130)
	at com.bumble.appyx.sandbox.client.container.ContainerNode.resolve(ContainerNode.kt:63)
	at com.bumble.appyx.core.children.ChildEntry$Companion.create(ChildEntry.kt:58)
	at com.bumble.appyx.core.node.ParentNode.restoreChildren(ParentNode.kt:121)
	at com.bumble.appyx.core.node.ParentNode.onBuilt(ParentNode.kt:82)
	at com.bumble.appyx.core.node.NodeExtKt.build(NodeExt.kt:3)
	at com.bumble.appyx.core.integration.NodeHostKt$rememberNode$2.invoke(NodeHost.kt:72)
	at com.bumble.appyx.core.integration.NodeHostKt$rememberNode$2.invoke(NodeHost.kt:64)
	at androidx.compose.runtime.saveable.MapSaverKt$mapSaver$2.invoke(MapSaver.kt:52)
	at androidx.compose.runtime.saveable.MapSaverKt$mapSaver$2.invoke(MapSaver.kt:33)
	at androidx.compose.runtime.saveable.SaverKt$Saver$1.restore(Saver.kt:68)
	at androidx.compose.runtime.saveable.RememberSaveableKt$mutableStateSaver$1$2.invoke(RememberSaveable.kt:162)
	at androidx.compose.runtime.saveable.RememberSaveableKt$mutableStateSaver$1$2.invoke(RememberSaveable.kt:151)
	at androidx.compose.runtime.saveable.SaverKt$Saver$1.restore(Saver.kt:68)
	at androidx.compose.runtime.saveable.RememberSaveableKt.rememberSaveable(RememberSaveable.kt:86)
	at androidx.compose.runtime.saveable.RememberSaveableKt.rememberSaveable(RememberSaveable.kt:142)
	at com.bumble.appyx.core.integration.NodeHostKt.rememberNode(NodeHost.kt:62)
	at com.bumble.appyx.core.integration.NodeHostKt.NodeHost(NodeHost.kt:38)
	at com.bumble.appyx.sandbox.MainActivity$onCreate$1$1$1.invoke(MainActivity.kt:25)
	at com.bumble.appyx.sandbox.MainActivity$onCreate$1$1$1.invoke(MainActivity.kt:23)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.material.SurfaceKt$Surface$1.invoke(Surface.kt:134)
	at androidx.compose.material.SurfaceKt$Surface$1.invoke(Surface.kt:117)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.material.SurfaceKt.Surface-F-jzlyU(Surface.kt:114)
	at com.bumble.appyx.sandbox.MainActivity$onCreate$1$1.invoke(MainActivity.kt:23)
	at com.bumble.appyx.sandbox.MainActivity$onCreate$1$1.invoke(MainActivity.kt:21)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.material.MaterialTheme_androidKt.PlatformMaterialTheme(MaterialTheme.android.kt:23)
	at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:82)
	at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:81)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.material.TextKt.ProvideTextStyle(Text.kt:265)
	at androidx.compose.material.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:81)
	at androidx.compose.material.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:80)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.material.MaterialThemeKt.MaterialTheme(MaterialTheme.kt:72)
	at com.bumble.appyx.sandbox.ui.ThemeKt.AppyxSandboxTheme(Theme.kt:39)
	at com.bumble.appyx.sandbox.MainActivity$onCreate$1.invoke(MainActivity.kt:21)
	at com.bumble.appyx.sandbox.MainActivity$onCreate$1.invoke(MainActivity.kt:20)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:402)
	at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:248)
	at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:247)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:177)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:123)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:122)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:114)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:157)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:156)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:156)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:140)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:74)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3193)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3183)
	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:252)
	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source:1)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3183)
	at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3119)
	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:578)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:811)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:513)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:140)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
	at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1015)
	at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:131)
	at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:182)
	at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
	at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:196)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:138)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
	at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1102)
	at android.view.View.dispatchAttachedToWindow(View.java:20479)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3489)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
	at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2417)
	at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1952)
	at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8171)
	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
	at android.view.Choreographer.doCallbacks(Choreographer.java:796)
	at android.view.Choreographer.doFrame(Choreographer.java:731)
	at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
	at android.os.Handler.handleCallback(Handler.java:938)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loop(Looper.java:223)
	at android.app.ActivityThread.main(ActivityThread.java:7656)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
```

package com.bumble.appyx.app.composable

import androidx.compose.foundation.BorderStroke

/**
 * https://gist.github.com/DavidIbrahim/236dadbccd99c4fd328e53587df35a21
 */

/*
 * Copyright 2020 The Android Open Source Project
 *
 * 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.
 */

import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isSimple
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Dp

/**
 * Modify element to add border with appearance specified with a [border] and a [shape], pad the
 * content by the [BorderStroke.width] and clip it.
 *
 * @sample androidx.compose.foundation.samples.BorderSample()
 *
 * @param border [BorderStroke] class that specifies border appearance, such as size and color
 * @param shape shape of the border
 */
fun Modifier.dashedBorder(border: BorderStroke, shape: Shape = RectangleShape, on: Dp, off: Dp) =
    dashedBorder(width = border.width, brush = border.brush, shape = shape, on, off)

/**
 * Returns a [Modifier] that adds border with appearance specified with [width], [color] and a
 * [shape], pads the content by the [width] and clips it.
 *
 * @sample androidx.compose.foundation.samples.BorderSampleWithDataClass()
 *
 * @param width width of the border. Use [Dp.Hairline] for a hairline border.
 * @param color color to paint the border with
 * @param shape shape of the border
 * @param on the size of the solid part of the dashes
 * @param off the size of the space between dashes
 */
fun Modifier.dashedBorder(width: Dp, color: Color, shape: Shape = RectangleShape, on: Dp, off: Dp) =
    dashedBorder(width, SolidColor(color), shape, on, off)

/**
 * Returns a [Modifier] that adds border with appearance specified with [width], [brush] and a
 * [shape], pads the content by the [width] and clips it.
 *
 * @sample androidx.compose.foundation.samples.BorderSampleWithBrush()
 *
 * @param width width of the border. Use [Dp.Hairline] for a hairline border.
 * @param brush brush to paint the border with
 * @param shape shape of the border
 */
fun Modifier.dashedBorder(width: Dp, brush: Brush, shape: Shape, on: Dp, off: Dp): Modifier =
    composed(
        factory = {
            this.then(
                Modifier.drawWithCache {
                    val outline: Outline = shape.createOutline(size, layoutDirection, this)
                    val borderSize = if (width == Dp.Hairline) 1f else width.toPx()

                    var insetOutline: Outline? = null // outline used for roundrect/generic shapes
                    var stroke: Stroke? = null // stroke to draw border for all outline types
                    var pathClip: Path? = null // path to clip roundrect/generic shapes
                    var inset = 0f // inset to translate before drawing the inset outline
                    // path to draw generic shapes or roundrects with different corner radii
                    var insetPath: Path? = null
                    if (borderSize > 0 && size.minDimension > 0f) {
                        if (outline is Outline.Rectangle) {
                            stroke = Stroke(
                                borderSize, pathEffect = PathEffect.dashPathEffect(
                                    floatArrayOf(on.toPx(), off.toPx())
                                )
                            )
                        } else {
                            // Multiplier to apply to the border size to get a stroke width that is
                            // large enough to cover the corners while not being too large to overly
                            // square off the internal shape. The resultant shape will be
                            // clipped to the desired shape. Any value lower will show artifacts in
                            // the corners of shapes. A value too large will always square off
                            // the internal shape corners. For example, for a rounded rect border
                            // a large multiplier will always have squared off edges within the
                            // inner section of the stroke, however, having a smaller multiplier
                            // will still keep the rounded effect for the inner section of the
                            // border
                            val strokeWidth = 1.2f * borderSize
                            inset = borderSize - strokeWidth / 2
                            val insetSize = Size(
                                size.width - inset * 2,
                                size.height - inset * 2
                            )
                            insetOutline = shape.createOutline(insetSize, layoutDirection, this)
                            stroke = Stroke(
                                strokeWidth, pathEffect = PathEffect.dashPathEffect(
                                    floatArrayOf(on.toPx(), off.toPx())
                                )
                            )
                            pathClip = if (outline is Outline.Rounded) {
                                Path().apply { addRoundRect(outline.roundRect) }
                            } else if (outline is Outline.Generic) {
                                outline.path
                            } else {
                                // should not get here because we check for Outline.Rectangle
                                // above
                                null
                            }

                            insetPath =
                                if (insetOutline is Outline.Rounded &&
                                    !insetOutline.roundRect.isSimple
                                ) {
                                    // Rounded rect with non equal corner radii needs a path
                                    // to be pre-translated
                                    Path().apply {
                                        addRoundRect(insetOutline.roundRect)
                                        translate(Offset(inset, inset))
                                    }
                                } else if (insetOutline is Outline.Generic) {
                                    // Generic paths must be created and pre-translated
                                    Path().apply {
                                        addPath(insetOutline.path, Offset(inset, inset))
                                    }
                                } else {
                                    // Drawing a round rect with equal corner radii without
                                    // usage of a path
                                    null
                                }
                        }
                    }

                    onDrawWithContent {
                        drawContent()
                        // Only draw the border if a have a valid stroke parameter. If we have
                        // an invalid border size we will just draw the content
                        if (stroke != null) {
                            if (insetOutline != null && pathClip != null) {
                                val isSimpleRoundRect = insetOutline is Outline.Rounded &&
                                    insetOutline.roundRect.isSimple
                                withTransform({
                                    clipPath(pathClip)
                                    // we are drawing the round rect not as a path so we must
                                    // translate ourselves othe
                                    if (isSimpleRoundRect) {
                                        translate(inset, inset)
                                    }
                                }) {
                                    if (isSimpleRoundRect) {
                                        // If we don't have an insetPath then we are drawing
                                        // a simple round rect with the corner radii all identical
                                        val rrect = (insetOutline as Outline.Rounded).roundRect
                                        drawRoundRect(
                                            brush = brush,
                                            topLeft = Offset(rrect.left, rrect.top),
                                            size = Size(rrect.width, rrect.height),
                                            cornerRadius = rrect.topLeftCornerRadius,
                                            style = stroke
                                        )
                                    } else if (insetPath != null) {
                                        drawPath(insetPath, brush, style = stroke)
                                    }
                                }
                                // Clip rect to ensure the stroke does not extend the bounds
                                // of the composable.
                                clipRect {
                                    // Draw a hairline stroke to cover up non-anti-aliased pixels
                                    // generated from the clip
                                    if (isSimpleRoundRect) {
//                                        val rrect = (outline as Outline.Rounded).roundRect
//                                        drawRoundRect(
//                                            brush = brush,
//                                            topLeft = Offset(rrect.left, rrect.top),
//                                            size = Size(rrect.width, rrect.height),
//                                            cornerRadius = rrect.topLeftCornerRadius,
//                                            style = Stroke(
//                                                Stroke.HairlineWidth,
//                                                pathEffect = PathEffect.dashPathEffect(
//                                                    floatArrayOf(on.toPx(), off.toPx())
//                                                )
//                                            )
//                                        )
                                    } else {
                                        drawPath(
                                            pathClip, brush = brush, style = Stroke(
                                                Stroke.HairlineWidth,
                                                pathEffect = PathEffect.dashPathEffect(
                                                    floatArrayOf(on.toPx(), off.toPx())
                                                )
                                            )
                                        )
                                    }
                                }
                            } else {
                                // Rectangular border fast path
                                val strokeWidth = stroke.width
                                val halfStrokeWidth = strokeWidth / 2
                                drawRect(
                                    brush = brush,
                                    topLeft = Offset(halfStrokeWidth, halfStrokeWidth),
                                    size = Size(
                                        size.width - strokeWidth,
                                        size.height - strokeWidth
                                    ),
                                    style = stroke
                                )
                            }
                        }
                    }
                }
            )
        },
        inspectorInfo = debugInspectorInfo {
            name = "border"
            properties["width"] = width
            if (brush is SolidColor) {
                properties["color"] = brush.value
                value = brush.value
            } else {
                properties["brush"] = brush
            }
            properties["shape"] = shape
        }
    )

package com.bumble.appyx.app.composable

import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.bumble.appyx.app.ui.appyx_yellow1
import com.bumble.appyx.core.routing.transition.TransitionSpec
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.activeIndex
import com.bumble.appyx.routingsource.spotlight.elementsCount

@Composable
fun DotsIndicator(
    totalDots: Int,
    selectedIndex: Int,
    selectedColor: Color = appyx_yellow1,
    unSelectedColor: Color = Color.LightGray,
    selectedSize: Dp = 9.dp,
    unSelectedSize: Dp = 6.dp,
    dotTransitionSpecs: TransitionSpec<MutableState<Boolean>, Dp> = {
        spring()
    }
) {
    LazyRow(
        modifier = Modifier
            .wrapContentWidth()
            .wrapContentHeight(),
        horizontalArrangement = Arrangement.spacedBy(unSelectedSize, Alignment.CenterHorizontally),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        items(totalDots) { index ->
            val isSelected = remember(index, selectedIndex) {
                mutableStateOf(index == selectedIndex)
            }
            val transition = updateTransition(isSelected)
            val color = transition.animateColor { isSelected ->
                when (isSelected.value) {
                    true -> selectedColor
                    false -> unSelectedColor
                }
            }
            val size = transition.animateDp(
                transitionSpec = dotTransitionSpecs
            ) { isSelected ->
                when (isSelected.value) {
                    true -> selectedSize
                    false -> unSelectedSize
                }
            }

            Box(
                modifier = Modifier
                    .size(size.value)
                    .clip(CircleShape)
                    .background(color.value)
            )
        }
    }
}

@Composable
fun SpotlightDotsIndicator(
    spotlight: Spotlight<*>,
    selectedColor: Color = appyx_yellow1,
    unSelectedColor: Color = Color.DarkGray,
    selectedSize: Dp = 9.dp,
    unSelectedSize: Dp = 6.dp,
    dotTransitionSpecs: TransitionSpec<MutableState<Boolean>, Dp> = {
        spring()
    }
) {
    val selectedItemState = spotlight.activeIndex().collectAsState(initial = 0)

    DotsIndicator(
        totalDots = spotlight.elementsCount(),
        selectedIndex = selectedItemState.value,
        selectedColor = selectedColor,
        unSelectedColor = unSelectedColor,
        selectedSize = selectedSize,
        unSelectedSize = unSelectedSize,
        dotTransitionSpecs = dotTransitionSpecs
    )
}

package com.bumble.appyx.app.composable

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp

@Composable
@ExperimentalUnitApi
fun Page(
    modifier: Modifier,
    title: String,
    body: String,
    illustration: @Composable BoxScope.() -> Unit
) {
    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(
                start = 24.dp,
                end = 24.dp,
                top = 24.dp
            )
    ) {
        Box(
            modifier = Modifier
                .weight(0.65f)
                .fillMaxWidth()
        ) {
            illustration()
        }

        Spacer(Modifier.height(12.dp))

        Column(
            modifier = Modifier
                .weight(0.35f)
        ) {
            Text(
                text = title,
                style = MaterialTheme.typography.h4,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 8.dp)
            )
            Column(
                modifier = Modifier
                    .verticalScroll(rememberScrollState())
            ) {
                Text(
                    text = body,
                    style = MaterialTheme.typography.body1.copy(
                        lineHeight = TextUnit(1.5f, TextUnitType.Em)
                    )
                )
            }

        }

    }
}

package com.bumble.appyx.app.composable

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

@Composable
internal fun ScreenCenteredContent(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Box(
        modifier = modifier
            .fillMaxSize()
    ) {
        Box(
            modifier = Modifier.align(Alignment.Center)
        ) {
            content()
        }
    }
}

package com.bumble.appyx.core.builder

import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node

abstract class SimpleBuilder {
    abstract fun build(buildContext: BuildContext): Node
}

package com.bumble.appyx.core.children

import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.lifecycle.MinimumCombinedLifecycle
import com.bumble.appyx.core.lifecycle.isDestroyed
import kotlin.reflect.KClass
import kotlin.reflect.cast
import kotlin.reflect.safeCast

internal sealed class ChildAwareCallbackInfo {

    abstract fun onRegistered(activeNodes: List<Node>)

    class Single<T : Node>(
        private val child: KClass<T>,
        private val callback: ChildCallback<T>,
        private val parentLifecycle: Lifecycle,
    ) : ChildAwareCallbackInfo() {

        fun onNewNodeAppeared(newNode: Node) {
            if (parentLifecycle.isDestroyed) return
            val castedNode = child.safeCast(newNode)
            if (castedNode != null) {
                val lifecycle =
                    MinimumCombinedLifecycle(
                        parentLifecycle,
                        newNode.lifecycle,
                    ).lifecycle
                callback(lifecycle, castedNode)
            }
        }

        override fun onRegistered(activeNodes: List<Node>) {
            activeNodes.forEach { node ->
                onNewNodeAppeared(node)
            }
        }

    }

    class Double<T1 : Node, T2 : Node>(
        private val child1: KClass<T1>,
        private val child2: KClass<T2>,
        private val callback: ChildrenCallback<T1, T2>,
        private val parentLifecycle: Lifecycle,
    ) : ChildAwareCallbackInfo() {

        fun onNewNodeAppeared(
            activeNodes: List<Node>,
            newNode: Node,
            ignoreNodes: Set<Node>,
        ) {
            val second = getOther(newNode) ?: return
            activeNodes
                .filter { second.isInstance(it) && it != newNode && it !in ignoreNodes }
                .forEach { notify(newNode, it) }
        }

        override fun onRegistered(activeNodes: List<Node>) {
            activeNodes.forEachIndexed { index, node ->
                onNewNodeAppeared(
                    // Do not include already handled nodes to avoid call duplication
                    activeNodes = activeNodes.subList(index + 1, activeNodes.size),
                    newNode = node,
                    ignoreNodes = emptySet(),
                )
            }
        }

        private fun notify(node1: Node, node2: Node) {
            if (parentLifecycle.isDestroyed) return
            val lifecycle =
                MinimumCombinedLifecycle(
                    parentLifecycle,
                    node1.lifecycle,
                    node2.lifecycle,
                ).lifecycle
            if (child1.isInstance(node1)) {
                callback(lifecycle, child1.cast(node1), child2.cast(node2))
            } else {
                callback(lifecycle, child1.cast(node2), child2.cast(node1))
            }
        }

        private fun getOther(node: Node): KClass<*>? =
            when {
                child1.isInstance(node) -> child2
                child2.isInstance(node) -> child1
                else -> null
            }

    }

}

package com.bumble.appyx.core.children

import com.bumble.appyx.core.node.Node

inline fun <reified T : Node> ChildAware<*>.whenChildAttached(
    noinline callback: ChildCallback<T>,
) {
    whenChildAttached(T::class, callback)
}

inline fun <reified T1 : Node, reified T2 : Node> ChildAware<*>.whenChildrenAttached(
    noinline callback: ChildrenCallback<T1, T2>,
) {
    whenChildrenAttached(T1::class, T2::class, callback)
}

package com.bumble.appyx.core.children

import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.coroutineScope
import com.bumble.appyx.core.lifecycle.isDestroyed
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.core.withPrevious
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlin.reflect.KClass

class ChildAwareImpl<N: Node> : ChildAware<N> {

    private val callbacks: MutableList<ChildAwareCallbackInfo> = ArrayList()

    private lateinit var children: StateFlow<Map<out RoutingKey<*>, ChildEntry<*>>>
    private lateinit var lifecycle: Lifecycle
    private lateinit var coroutineScope: CoroutineScope

    override lateinit var node: N
        private set

    override fun init(node: N) {
        this.node = node
        lifecycle = node.lifecycle
        coroutineScope = lifecycle.coroutineScope
        if (node is ParentNode<*>) {
            children = node.children
            initialize()
        } else {
            children = MutableStateFlow(emptyMap())
        }
    }

    private fun initialize() {
        coroutineScope.apply {
            launch { findNewKeys() }
            launch { findPromotedChildren() }
        }
    }

    private suspend fun findNewKeys() {
        children
            .withPrevious()
            .collect { value ->
                val addedKeys = value.current.keys - (value.previous?.keys ?: emptySet())
                if (addedKeys.isEmpty()) return@collect
                val currentNodes = getCreatedNodes(value.current)
                val visitedSet = HashSet<Node>()
                addedKeys.forEach { key ->
                    val node = value.current[key]?.nodeOrNull
                    if (node != null) {
                        notifyWhenChanged(node, currentNodes, visitedSet)
                        visitedSet.add(node)
                    }
                }
            }
    }

    private suspend fun findPromotedChildren() {
        children
            .withPrevious()
            .collect { value ->
                if (value.previous == null) return@collect
                val commonKeys = value.current.keys.intersect(value.previous.keys)
                if (commonKeys.isEmpty()) return@collect
                val nodes = getCreatedNodes(value.current)
                val visitedSet = HashSet<Node>()
                commonKeys.forEach { key ->
                    val current = value.current[key]
                    if (current != value.previous[key] && current is ChildEntry.Eager) {
                        notifyWhenChanged(current.node, nodes, visitedSet)
                        visitedSet.add(current.node)
                    }
                }
            }
    }

    override fun <T : Node> whenChildAttached(
        child: KClass<T>,
        callback: ChildCallback<T>
    ) {
        if (lifecycle.isDestroyed) return
        val info = ChildAwareCallbackInfo.Single(child, callback, lifecycle)
        callbacks += info
        notifyWhenRegistered(info)
        lifecycle.removeWhenDestroyed(info)
    }

    override fun <T1 : Node, T2 : Node> whenChildrenAttached(
        child1: KClass<T1>,
        child2: KClass<T2>,
        callback: ChildrenCallback<T1, T2>
    ) {
        if (lifecycle.isDestroyed) return
        val info = ChildAwareCallbackInfo.Double(child1, child2, callback, lifecycle)
        callbacks += info
        notifyWhenRegistered(info)
        lifecycle.removeWhenDestroyed(info)
    }

    private fun notifyWhenRegistered(callback: ChildAwareCallbackInfo) {
        callback.onRegistered(getCreatedNodes(children.value))
    }

    private fun notifyWhenChanged(child: Node, nodes: List<Node>, ignore: Set<Node>) {
        for (callback in callbacks) {
            when (callback) {
                is ChildAwareCallbackInfo.Double<*, *> -> callback.onNewNodeAppeared(
                    activeNodes = nodes,
                    newNode = child,
                    ignoreNodes = ignore,
                )
                is ChildAwareCallbackInfo.Single<*> -> callback.onNewNodeAppeared(child)
            }
        }
    }

    private fun Lifecycle.removeWhenDestroyed(info: ChildAwareCallbackInfo) {
        addObserver(object : DefaultLifecycleObserver {
            override fun onDestroy(owner: LifecycleOwner) {
                callbacks.remove(info)
            }
        })
    }

    private fun getCreatedNodes(childEntryMap: Map<out RoutingKey<*>, ChildEntry<*>>) =
        childEntryMap.values.mapNotNull { entry ->
            when (entry) {
                is ChildEntry.Eager -> entry.node
                is ChildEntry.Lazy -> null
            }
        }

}

package com.bumble.appyx.core.children

import androidx.lifecycle.Lifecycle

typealias ChildrenCallback<T1, T2> = (commonLifecycle: Lifecycle, child1: T1, child2: T2) -> Unit

typealias ChildCallback<T> = (commonLifecycle: Lifecycle, child: T) -> Unit

package com.bumble.appyx.core.children

import com.bumble.appyx.core.node.Node

val <T> ChildEntry<T>.nodeOrNull: Node?
    get() =
        when (this) {
            is ChildEntry.Eager -> node
            is ChildEntry.Lazy -> null
        }

package com.bumble.appyx.core.children

import com.bumble.appyx.core.routing.RoutingKey

typealias ChildEntryMap<Routing> = Map<RoutingKey<Routing>, ChildEntry<Routing>>

package com.bumble.appyx.core.composable

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

interface ChildRenderer {

    @Composable
    operator fun invoke(modifier: Modifier)

    @Composable
    operator fun invoke()
}

package com.bumble.appyx.core.integration

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectory
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import com.bumble.appyx.core.integrationpoint.IntegrationPoint
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.build

@Stable
fun interface NodeFactory<N : Node> {
    fun create(buildContext: BuildContext): N
}

/**
 * Composable function to host [Node].
 *
 * Aligns lifecycle and manages state restoration.
 */
@Composable
fun <N : Node> NodeHost(
    customisations: NodeCustomisationDirectory = NodeCustomisationDirectoryImpl(),
    integrationPoint: IntegrationPoint,
    factory: NodeFactory<N>
) {
    val node by rememberNode(factory, customisations)
    LaunchedEffect(integrationPoint) {
        integrationPoint.attach(node)
    }
    DisposableEffect(node) {
        onDispose { node.updateLifecycleState(Lifecycle.State.DESTROYED) }
    }
    node.Compose()
    val lifecycle = LocalLifecycleOwner.current.lifecycle
    DisposableEffect(lifecycle) {
        node.updateLifecycleState(lifecycle.currentState)
        val observer = LifecycleEventObserver { source, _ ->
            node.updateLifecycleState(source.lifecycle.currentState)
        }
        lifecycle.addObserver(observer)
        onDispose { lifecycle.removeObserver(observer) }
    }
}

@Composable
internal fun <N : Node> rememberNode(
    factory: NodeFactory<N>,
    customisations: NodeCustomisationDirectory
): State<N> =
    rememberSaveable(
        inputs = arrayOf(),
        stateSaver = mapSaver(
            save = { node -> node.saveInstanceState(this) },
            restore = { state ->
                factory.create(
                    buildContext = BuildContext.root(
                        savedStateMap = state,
                        customisations = customisations
                    ),
                ).build()
            },
        ),
    ) {
        mutableStateOf(
            factory.create(
                buildContext = BuildContext.root(
                    savedStateMap = null,
                    customisations = customisations
                )
            ).build()
        )
    }

package com.bumble.appyx.core.integrationpoint

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityBoundary
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityStarter
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequestBoundary
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester

open class ActivityIntegrationPoint(
    private val activity: AppCompatActivity,
    savedInstanceState: Bundle?,
) : IntegrationPoint(savedInstanceState = savedInstanceState) {
    private val activityBoundary = ActivityBoundary(activity, requestCodeRegistry)
    private val permissionRequestBoundary = PermissionRequestBoundary(activity, requestCodeRegistry)

    override val activityStarter: ActivityStarter
        get() = activityBoundary

    override val permissionRequester: PermissionRequester
        get() = permissionRequestBoundary

    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        activityBoundary.onActivityResult(requestCode, resultCode, data)
    }

    fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        permissionRequestBoundary.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

    override fun handleUpNavigation() {
        if (!activity.onNavigateUp()) {
            activity.onBackPressed()
        }
    }

    override fun onRootFinished() {
        if (!activity.onNavigateUp()) {
            activity.finish()
        }
    }
}

package com.bumble.appyx.core.integrationpoint

import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityBoundary
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityStarter
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequestBoundary
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester

open class FragmentIntegrationPoint(
    private val fragment: Fragment,
    savedInstanceState: Bundle?
) : IntegrationPoint(savedInstanceState = savedInstanceState) {
    private val activityBoundary = ActivityBoundary(fragment, requestCodeRegistry)
    private val permissionRequestBoundary = PermissionRequestBoundary(fragment, requestCodeRegistry)

    override val activityStarter: ActivityStarter
        get() = activityBoundary

    override val permissionRequester: PermissionRequester
        get() = permissionRequestBoundary

    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        activityBoundary.onActivityResult(requestCode, resultCode, data)
    }

    fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        permissionRequestBoundary.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

    override fun handleUpNavigation() {
        val activity = fragment.requireActivity()
        if (!activity.onNavigateUp()) {
            activity.onBackPressed()
        }
    }

    override fun onRootFinished() {
        val activity = fragment.requireActivity()
        if (!activity.onNavigateUp()) {
            activity.finish()
        }
    }
}

package com.bumble.appyx.core.integrationpoint

import android.os.Bundle
import androidx.compose.runtime.Stable
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityStarter
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeRegistry
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.routing.upnavigation.UpNavigationHandler

@Stable
abstract class IntegrationPoint(
    protected val savedInstanceState: Bundle?
) : UpNavigationHandler {

    protected val requestCodeRegistry = RequestCodeRegistry(savedInstanceState)

    abstract val activityStarter: ActivityStarter

    abstract val permissionRequester: PermissionRequester

    private var _root: Node? = null
    private val root: Node
        get() = _root ?: error("Root has not been initialised. Did you forget to call attach?")

    fun attach(root: Node) {
        if (_root != null) error("A root has already been attached to this integration point")
        if (!root.isRoot) error("Trying to attach non-root Node")
        this._root = root
        root.integrationPoint = this
    }

    fun onSaveInstanceState(outState: Bundle) {
        requestCodeRegistry.onSaveInstanceState(outState)
    }

    abstract fun onRootFinished()
}

package com.bumble.appyx.core.integrationpoint

import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityStarter
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester

class IntegrationPointStub : IntegrationPoint(savedInstanceState = null) {
    companion object {
        private const val ERROR = "You're accessing an IntegrationPointStub. " +
            "This means you're using a Node without ever integrating it to a proper IntegrationPoint. " +
            "This is fine during tests with limited scope, but it looks like the code that leads here " +
            "requires interfacing with a valid implementation."
    }

    override val activityStarter: ActivityStarter
        get() = error(ERROR)

    override val permissionRequester: PermissionRequester
        get() = error(ERROR)

    override fun handleUpNavigation() {
        error(ERROR)
    }

    override fun onRootFinished() {
        error(ERROR)
    }
}

package com.bumble.appyx.core.integrationpoint

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

/**
 * Helper class for root [Node] integration.
 *
 * Also offers base functionality to satisfy dependencies of Android-related functionality
 * down the tree via [integrationPoint]:
 * - [ActivityStarter]
 * - [PermissionRequester]
 *
 * Feel free to not extend this and use your own integration point - in this case,
 * don't forget to take a look here what methods needs to be forwarded to the root Node.
 */
abstract class NodeActivity : AppCompatActivity() {

    lateinit var integrationPoint: ActivityIntegrationPoint
        protected set

    protected open fun createIntegrationPoint(savedInstanceState: Bundle?): ActivityIntegrationPoint =
        ActivityIntegrationPoint(
            activity = this,
            savedInstanceState = savedInstanceState
        )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        integrationPoint = createIntegrationPoint(savedInstanceState)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        integrationPoint.onActivityResult(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        integrationPoint.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        integrationPoint.onSaveInstanceState(outState)
    }

}

package com.bumble.appyx.core.lifecycle

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.coroutineScope
import com.bumble.appyx.core.children.ChildEntry
import com.bumble.appyx.core.children.ChildEntryMap
import com.bumble.appyx.core.children.nodeOrNull
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.core.routing.RoutingSource
import com.bumble.appyx.core.routing.RoutingSourceAdapter
import com.bumble.appyx.core.withPrevious
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch

/**
 * Hosts [LifecycleRegistry] to manage the current node lifecycle
 * and updates lifecycle of children nodes when updated.
 */
internal class ChildNodeLifecycleManager<Routing>(
    private val lifecycle: Lifecycle,
    private val routingSource: RoutingSource<Routing, *>,
    private val children: StateFlow<ChildEntryMap<Routing>>,
    private val coroutineScope: CoroutineScope = lifecycle.coroutineScope,
) {

    fun launch() {
        // previously state management was split into multiple jobs
        // but we found execution order issues when the main thread is busy (?)
        // so we merged it into single one
        coroutineScope.launch {
            combine(
                lifecycle.asFlow(),
                routingSource.screenState.keys(),
                children.withPrevious(),
                ::Triple
            )
                .onCompletion {
                    // when scope is closed we need to destroy all existing children
                    // do it manually here as emit() does not work for cancellation case
                    children
                        .value
                        .values
                        .forEach { entry -> entry.setState(Lifecycle.State.DESTROYED) }
                }
                .collect { (parentLifecycleState, screenState, children) ->
                    screenState.onScreen.forEach { key ->
                        val childState = minOf(parentLifecycleState, Lifecycle.State.RESUMED)
                        children.current[key]?.setState(childState)
                    }

                    screenState.offScreen.forEach { key ->
                        val childState = minOf(parentLifecycleState, Lifecycle.State.CREATED)
                        children.current[key]?.setState(childState)
                    }

                    if (children.previous != null) {
                        val removedKeys = children.previous.keys - children.current.keys
                        removedKeys.forEach { key ->
                            val removedChild = children.previous[key]
                            removedChild?.setState(Lifecycle.State.DESTROYED)
                        }
                    }
                }
        }
    }

    private fun <Routing> Flow<RoutingSourceAdapter.ScreenState<Routing, *>>.keys() =
        this
            .map { screenState ->
                ScreenState(
                    onScreen = screenState.onScreen.map { it.key },
                    offScreen = screenState.offScreen.map { it.key },
                )
            }
            .distinctUntilChanged()

    private fun ChildEntry<*>.setState(state: Lifecycle.State) {
        nodeOrNull?.updateLifecycleState(state)
    }

    private data class ScreenState<Routing>(
        val onScreen: List<RoutingKey<Routing>> = emptyList(),
        val offScreen: List<RoutingKey<Routing>> = emptyList(),
    )

}

package com.bumble.appyx.core.lifecycle

import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow

fun LifecycleOwner.asFlow(): Flow<Lifecycle.State> =
    lifecycle.asFlow()

fun Lifecycle.asFlow(): Flow<Lifecycle.State> =
    callbackFlow {
        val observer = LifecycleEventObserver { source, _ ->
            trySend(source.lifecycle.currentState)
        }
        trySend(currentState)
        addObserver(observer)
        awaitClose { removeObserver(observer) }
    }

internal val Lifecycle.isDestroyed: Boolean
    get() = currentState == Lifecycle.State.DESTROYED

fun Lifecycle.subscribe(
    onCreate: () -> Unit = {},
    onStart: () -> Unit = {},
    onResume: () -> Unit = {},
    onPause: () -> Unit = {},
    onStop: () -> Unit = {},
    onDestroy: () -> Unit = {}
) {
    addObserver(
        object : DefaultLifecycleObserver {
            override fun onCreate(owner: LifecycleOwner) {
                onCreate()
            }

            override fun onStart(owner: LifecycleOwner) {
                onStart()
            }

            override fun onResume(owner: LifecycleOwner) {
                onResume()
            }

            override fun onPause(owner: LifecycleOwner) {
                onPause()
            }

            override fun onStop(owner: LifecycleOwner) {
                onStop()
            }

            override fun onDestroy(owner: LifecycleOwner) {
                onDestroy()
            }
        }
    )
}

package com.bumble.appyx.core.lifecycle

import android.util.Log
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner

internal object LifecycleLogger : DefaultLifecycleObserver {

    private const val LOG_TAG = "Lifecycle"

    override fun onCreate(owner: LifecycleOwner) {
        Log.d(LOG_TAG, "${owner.javaClass.simpleName}@${owner.hashCode()} onCreate")
    }

    override fun onStart(owner: LifecycleOwner) {
        Log.d(LOG_TAG, "${owner.javaClass.simpleName}@${owner.hashCode()} onStart")
    }

    override fun onResume(owner: LifecycleOwner) {
        Log.d(LOG_TAG, "${owner.javaClass.simpleName}@${owner.hashCode()} onResume")
    }

    override fun onPause(owner: LifecycleOwner) {
        Log.d(LOG_TAG, "${owner.javaClass.simpleName}@${owner.hashCode()} onPause")
    }

    override fun onStop(owner: LifecycleOwner) {
        Log.d(LOG_TAG, "${owner.javaClass.simpleName}@${owner.hashCode()} onStop")
    }

    override fun onDestroy(owner: LifecycleOwner) {
        Log.d(LOG_TAG, "${owner.javaClass.simpleName}@${owner.hashCode()} onDestroy")
    }

}

package com.bumble.appyx.core.lifecycle

import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry

/**
 * Combines multiple lifecycles and provides a minimum of their states.
 *
 * For example:
 * - RESUMED + STARTED + RESUMED -> STARTED
 * - CREATED + RESUMED + DESTROYED -> DESTROYED
 * - INITIALIZED + DESTROYED -> DESTROYED
 */
internal class MinimumCombinedLifecycle(
    vararg lifecycles: Lifecycle,
) : LifecycleOwner {
    private val registry = LifecycleRegistry(this)
    private val lifecycles = ArrayList<Lifecycle>()

    init {
        /*
        Sort list to avoid unnecessary state jumps.
        If Lifecycle(RESUMED) + Lifecycle(DESTROYED) is passed,
        then we should have the final state in DESTROYED state without additional jumping to RESUMED.
         */
        lifecycles.sortedBy { it.currentState }.forEach { manage(it) }
    }

    override fun getLifecycle(): Lifecycle =
        registry

    fun manage(lifecycle: Lifecycle) {
        lifecycles += lifecycle
        lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onCreate(owner: LifecycleOwner) {
                update()
            }

            override fun onStart(owner: LifecycleOwner) {
                update()
            }

            override fun onResume(owner: LifecycleOwner) {
                update()
            }

            override fun onPause(owner: LifecycleOwner) {
                update()
            }

            override fun onStop(owner: LifecycleOwner) {
                update()
            }

            override fun onDestroy(owner: LifecycleOwner) {
                update()
            }
        })
        update()
    }

    private fun update() {
        lifecycles
            .minByOrNull { it.currentState }
            ?.takeIf { it.currentState != Lifecycle.State.INITIALIZED }
            ?.also { registry.currentState = it.currentState }
    }

}

package com.bumble.appyx.core.lifecycle

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner

interface NodeLifecycle : LifecycleOwner {

    fun updateLifecycleState(state: Lifecycle.State)

}

package com.bumble.appyx.core.lifecycle

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry

internal class NodeLifecycleImpl(owner: LifecycleOwner) : NodeLifecycle {

    private val lifecycleRegistry = LifecycleRegistry(owner)

    override fun getLifecycle(): Lifecycle =
        lifecycleRegistry

    override fun updateLifecycleState(state: Lifecycle.State) {
        lifecycleRegistry.currentState = state
    }

}

package com.bumble.appyx.core.modality

import com.bumble.appyx.core.node.ParentNode

sealed class AncestryInfo {

    object Root : AncestryInfo()

    data class Child(
        val anchor: ParentNode<*>,
    ) : AncestryInfo()

}

package com.bumble.appyx.core.modality

import com.bumble.appyx.utils.customisations.NodeCustomisation
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectory
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import com.bumble.appyx.core.state.SavedStateMap

data class BuildContext(
    val ancestryInfo: AncestryInfo,
    val savedStateMap: SavedStateMap?,
    val customisations: NodeCustomisationDirectory,
) {
    companion object {
        fun root(
            savedStateMap: SavedStateMap?,
            customisations: NodeCustomisationDirectory = NodeCustomisationDirectoryImpl()
        ): BuildContext =
            BuildContext(
                ancestryInfo = AncestryInfo.Root,
                savedStateMap = savedStateMap,
                customisations = customisations
            )
    }

    fun <T : NodeCustomisation> getOrDefault(defaultCustomisation: T) : T =
        customisations.getRecursivelyOrDefault(defaultCustomisation)
}

package com.bumble.appyx.core.node

import androidx.annotation.CallSuper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.bumble.appyx.core.BuildConfig
import com.bumble.appyx.core.integrationpoint.IntegrationPoint
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.lifecycle.LifecycleLogger
import com.bumble.appyx.core.lifecycle.NodeLifecycle
import com.bumble.appyx.core.lifecycle.NodeLifecycleImpl
import com.bumble.appyx.core.modality.AncestryInfo
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.plugin.Destroyable
import com.bumble.appyx.core.plugin.NodeAware
import com.bumble.appyx.core.plugin.NodeLifecycleAware
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.SavesInstanceState
import com.bumble.appyx.core.plugin.UpNavigationHandler
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
import com.bumble.appyx.core.state.SavedStateMap
import com.bumble.appyx.debug.Appyx

abstract class Node(
    buildContext: BuildContext,
    val view: NodeView = EmptyNodeView,
    plugins: List<Plugin> = emptyList()
) : NodeLifecycle, NodeView by view {

    @Suppress("LeakingThis") // Implemented in the same way as in androidx.Fragment
    private val nodeLifecycle = NodeLifecycleImpl(this)

    val plugins: List<Plugin> = plugins + listOfNotNull(this as? Plugin)

    val ancestryInfo: AncestryInfo =
        buildContext.ancestryInfo

    val isRoot: Boolean =
        ancestryInfo == AncestryInfo.Root

    private val parent: ParentNode<*>? =
        when (ancestryInfo) {
            is AncestryInfo.Child -> ancestryInfo.anchor
            is AncestryInfo.Root -> null
        }

    var integrationPoint: IntegrationPoint = IntegrationPointStub()
        get() {
            return if (isRoot) field
            else parent?.integrationPoint ?: error(
                "Non-root Node should have a parent"
            )
        }

    private val lifecycleRegistry = LifecycleRegistry(this)

    private var wasBuilt = false

    init {
        if (BuildConfig.DEBUG) {
            lifecycle.addObserver(LifecycleLogger)
        }
        lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onCreate(owner: LifecycleOwner) {
                if (!wasBuilt) error("onBuilt was not invoked for $this")
            }
        });
    }

    @CallSuper
    open fun onBuilt() {
        require(!wasBuilt) { "onBuilt was already invoked" }
        wasBuilt = true
        updateLifecycleState(Lifecycle.State.CREATED)
        plugins<NodeAware<Node>>().forEach { it.init(this) }
        plugins<NodeLifecycleAware>().forEach { it.onCreate(lifecycle) }
    }

    @Composable
    fun Compose(modifier: Modifier = Modifier) {
        CompositionLocalProvider(
            LocalNode provides this,
            LocalLifecycleOwner provides this,
        ) {
            DerivedSetup()
            View(modifier)
        }
    }

    /** Derived classes can declare functional (non-ui) Composable blocks before [View()] is invoked. */
    @Composable
    protected open fun DerivedSetup() {
    }

    override fun getLifecycle(): Lifecycle =
        nodeLifecycle.lifecycle

    override fun updateLifecycleState(state: Lifecycle.State) {
        if (lifecycle.currentState == state) return
        if (lifecycle.currentState == Lifecycle.State.DESTROYED && state != Lifecycle.State.DESTROYED) {
            Appyx.reportException(
                IllegalStateException("Trying to change lifecycle state of already destroyed node ${this::class.qualifiedName}")
            )
            return
        }
        nodeLifecycle.updateLifecycleState(state)
        if (state == Lifecycle.State.DESTROYED) {
            plugins<Destroyable>().forEach { it.destroy() }
        }
    }

    fun saveInstanceState(scope: SaverScope): SavedStateMap {
        val writer = MutableSavedStateMapImpl(saverScope = scope)
        onSaveInstanceState(writer)
        plugins
            .filterIsInstance<SavesInstanceState>()
            .forEach { it.saveInstanceState(writer) }
        return writer.savedState
    }

    @CallSuper
    protected open fun onSaveInstanceState(state: MutableSavedStateMap) {
        // no-op
    }

    fun finish() {
        parent?.onChildFinished(this) ?: integrationPoint.onRootFinished()
    }

    /**
     * Triggers parents up navigation (back navigation by default).
     *
     * This method is useful for different cases like:
     * - Close button on the screen which leads back to the previous screen.
     * - Blocker screen that intercepts back button clicks but closes itself when condition is met.
     *
     * To properly handle blocker case this method skips the current node plugins (like router),
     * and invokes the parent directly.
     */
    fun navigateUp() {
        require(parent != null || isRoot) {
            "Can't navigate up, neither parent nor integration point is presented"
        }
        if (parent?.performUpNavigation() != true) {
            integrationPoint.handleUpNavigation()
        }
    }

    @CallSuper
    protected open fun performUpNavigation(): Boolean =
        handleUpNavigationByPlugins() || parent?.performUpNavigation() == true

    private fun handleUpNavigationByPlugins(): Boolean =
        plugins<UpNavigationHandler>().any { it.handleUpNavigation() }

}

package com.bumble.appyx.core.routing

import com.bumble.appyx.core.plugin.BackPressHandler
import com.bumble.appyx.core.plugin.Destroyable
import com.bumble.appyx.core.routing.backpresshandlerstrategies.BackPressHandlerStrategy
import com.bumble.appyx.core.routing.backpresshandlerstrategies.DontHandleBackPress
import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver
import com.bumble.appyx.core.routing.onscreen.isOnScreen
import com.bumble.appyx.core.routing.operationstrategies.ExecuteImmediately
import com.bumble.appyx.core.routing.operationstrategies.OperationStrategy
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlin.coroutines.EmptyCoroutineContext

/**
 * Base class for routingSource implementations.
 * Use this one if the base behaviour suffice your requirements, you want to use backPressHandler or operationStrategy,
 * and to reduce boilerplate.
 *
 * If more granular configuration is required, you should inherit RoutingSource interface instead.
 */
abstract class BaseRoutingSource<Routing, State>(
    private val backPressHandler: BackPressHandlerStrategy<Routing, State> = DontHandleBackPress(),
    private val operationStrategy: OperationStrategy<Routing, State> = ExecuteImmediately(),
    private val screenResolver: OnScreenStateResolver<State>,
    protected val scope: CoroutineScope = CoroutineScope(EmptyCoroutineContext + Dispatchers.Unconfined),
    private val finalStates: Set<State>,
    private val key: String = KEY_ROUTING_SOURCE,
    savedStateMap: SavedStateMap?
) : RoutingSource<Routing, State>, Destroyable, BackPressHandler by backPressHandler {

    constructor(
        backPressHandler: BackPressHandlerStrategy<Routing, State> = DontHandleBackPress(),
        operationStrategy: OperationStrategy<Routing, State> = ExecuteImmediately(),
        screenResolver: OnScreenStateResolver<State>,
        scope: CoroutineScope = CoroutineScope(EmptyCoroutineContext + Dispatchers.Unconfined),
        finalState: State?,
        key: String = KEY_ROUTING_SOURCE,
        savedStateMap: SavedStateMap?
    ) : this(
        backPressHandler = backPressHandler,
        operationStrategy = operationStrategy,
        screenResolver = screenResolver,
        scope = scope,
        finalStates = setOfNotNull(finalState),
        key = key,
        savedStateMap = savedStateMap
    )

    // TODO: think about how we can avoid keeping unnecessary object after state initialization
    protected abstract val initialElements: RoutingElements<Routing, State>

    private val state: MutableStateFlow<RoutingElements<Routing, State>> by lazy {
        MutableStateFlow(savedStateMap?.restoreHistory() ?: initialElements)
    }

    override val elements: StateFlow<RoutingElements<Routing, State>> get() = state

    override val screenState: StateFlow<RoutingSourceAdapter.ScreenState<Routing, State>> by lazy {
        state
            .map { elements ->
                RoutingSourceAdapter.ScreenState(
                    onScreen = elements.filter { screenResolver.isOnScreen(it) },
                    offScreen = elements.filterNot { screenResolver.isOnScreen(it) },
                )
            }
            .stateIn(scope, SharingStarted.Eagerly, RoutingSourceAdapter.ScreenState())
    }

    override val canHandleBackPress: StateFlow<Boolean> by lazy(LazyThreadSafetyMode.NONE) {
        backPressHandler.canHandleBackPress
    }

    init {
        backPressHandler.init(this, scope)
        operationStrategy.init(this, scope, ::execute)
    }

    override fun accept(operation: Operation<Routing, State>) {
        operationStrategy.accept(operation)
    }

    protected fun updateState(block: (RoutingElements<Routing, State>) -> RoutingElements<Routing, State>) {
        state.update { currentState ->
            val newState = block(currentState)
            sanitizeOffScreenTransitions(newState)
        }
    }

    private fun execute(operation: Operation<Routing, State>) {
        updateState {
            if (operation.isApplicable(it)) {
                operation(it)
            } else {
                it
            }
        }
    }

    /**
     * Off screen transitions can't finish without [onTransitionFinished] callback from UI.
     * In case if we have any, lets finish them instantly.
     */
    protected fun sanitizeOffScreenTransitions(state: RoutingElements<Routing, State>): RoutingElements<Routing, State> =
        state.mapNotNull {
            if (screenResolver.isOnScreen(it)) {
                it
            } else {
                it.finishTransitionOrRemove()
            }
        }

    override fun onTransitionFinished(keys: Collection<RoutingKey<Routing>>) {
        if (keys.isEmpty()) return
        state.update { list ->
            list.mapNotNull {
                if (it.key in keys) {
                    it.finishTransitionOrRemove()
                } else {
                    it
                }
            }
        }
    }

    protected open fun RoutingElement<Routing, State>.finishTransitionOrRemove(): RoutingElement<Routing, State>? =
        if (targetState.isFinalState) {
            null
        } else {
            onTransitionFinished()
        }

    override fun saveInstanceState(state: MutableSavedStateMap) {
        state[key] =
            elements.value.mapNotNull {
                // Sanitize outputs, removing all transitions
                if (it.targetState.isFinalState) {
                    null
                } else {
                    it.onTransitionFinished()
                }
            }
    }

    override fun destroy() {
        scope.cancel()
    }

    protected val State.isFinalState
        get() = finalStates.contains(this)

    private fun SavedStateMap.restoreHistory() =
        this[key] as? RoutingElements<Routing, State>

    companion object {
        const val KEY_ROUTING_SOURCE = "RoutingSource"
    }

}

package com.bumble.appyx.core.routing

import android.os.Parcelable
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

@Parcelize
@Immutable
class RoutingElement<Routing, State> private constructor(
    val key: @RawValue RoutingKey<Routing>,
    val fromState: @RawValue State,
    val targetState: @RawValue State,
    val operation: @RawValue Operation<Routing, State>,
    val transitionHistory: List<Pair<State, State>>
) : Parcelable {
    constructor(
        key: @RawValue RoutingKey<Routing>,
        fromState: @RawValue State,
        targetState: @RawValue State,
        operation: @RawValue Operation<Routing, State>,
    ) : this(
        key,
        fromState,
        targetState,
        operation,
        if (fromState == targetState) emptyList() else listOf(fromState to targetState)
    )

    fun transitionTo(
        targetState: @RawValue State,
        operation: @RawValue Operation<Routing, State>
    ): RoutingElement<Routing, State> =
        RoutingElement(
            key = key,
            fromState = fromState,
            targetState = targetState,
            operation = operation,
            transitionHistory =
            if (isTransitioning) {
                transitionHistory + listOf(fromState to targetState)
            } else transitionHistory
        )

    fun onTransitionFinished(): RoutingElement<Routing, State> =
        RoutingElement(
            key = key,
            fromState = targetState,
            targetState = targetState,
            operation = operation,
            transitionHistory = emptyList()
        )

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as RoutingElement<*, *>

        if (key != other.key) return false
        if (fromState != other.fromState) return false
        if (targetState != other.targetState) return false
        if (operation != other.operation) return false
        if (transitionHistory != other.transitionHistory) return false

        return true
    }

    override fun hashCode(): Int {
        var result = key.hashCode()
        result = 31 * result + (fromState?.hashCode() ?: 0)
        result = 31 * result + (targetState?.hashCode() ?: 0)
        result = 31 * result + operation.hashCode()
        result = 31 * result + transitionHistory.hashCode()
        return result
    }

    override fun toString(): String {
        return "RoutingElement(key=$key, fromState=$fromState, targetState=$targetState, operation=$operation, transitionHistory=$transitionHistory)"
    }
}

package com.bumble.appyx.core.routing

val RoutingElement<*, *>.isTransitioning: Boolean
    get() = fromState != targetState

package com.bumble.appyx.core.routing

typealias RoutingElements<Routing, State> = List<RoutingElement<Routing, State>>

package com.bumble.appyx.core.routing

import android.os.Parcelable
import androidx.compose.runtime.Immutable
import java.util.*
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

@Parcelize
@Immutable
class RoutingKey<Routing> private constructor(
    val routing: @RawValue Routing,
    val id: String
) : Parcelable {

    constructor(routing: @RawValue Routing) : this(
        routing = routing,
        id = UUID.randomUUID().toString()
    )

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as RoutingKey<*>

        if (routing != other.routing) return false
        if (id != other.id) return false

        return true
    }

    override fun hashCode(): Int {
        var result = routing?.hashCode() ?: 0
        result = 31 * result + id.hashCode()
        return result
    }

}

package com.bumble.appyx.core.routing

import androidx.compose.runtime.Stable
import com.bumble.appyx.core.plugin.BackPressHandler
import com.bumble.appyx.core.plugin.SavesInstanceState
import com.bumble.appyx.core.plugin.UpNavigationHandler
import kotlinx.coroutines.flow.StateFlow

@Stable
interface RoutingSource<Routing, State> : RoutingSourceAdapter<Routing, State>,
    UpNavigationHandler,
    SavesInstanceState,
    BackPressHandler {

    val elements: StateFlow<RoutingElements<Routing, out State>>

    val canHandleBackPress: StateFlow<Boolean>

    fun onTransitionFinished(key: RoutingKey<Routing>) {
        onTransitionFinished(listOf(key))
    }

    fun onTransitionFinished(keys: Collection<RoutingKey<Routing>>)

    fun accept(operation: Operation<Routing, State>) = Unit

    override fun handleUpNavigation(): Boolean =
        canHandleBackPress.value.also { if (it) onBackPressed() }
}

package com.bumble.appyx.core.routing

import androidx.compose.runtime.Stable
import kotlinx.coroutines.flow.StateFlow

@Stable
interface RoutingSourceAdapter<Routing, State> {

    val screenState: StateFlow<ScreenState<Routing, out State>>

    data class ScreenState<Routing, State>(
        val onScreen: RoutingElements<Routing, out State> = emptyList(),
        val offScreen: RoutingElements<Routing, out State> = emptyList(),
    )

}

package com.bumble.appyx.core.state

import androidx.compose.runtime.saveable.SaverScope

interface MutableSavedStateMap : MutableMap<String, Any?> {
    val saverScope: SaverScope
}

package com.bumble.appyx.core.state

import androidx.compose.runtime.saveable.SaverScope

class MutableSavedStateMapImpl(
    private val map: MutableMap<String, Any?> = HashMap(),
    override val saverScope: SaverScope,
) : MutableSavedStateMap, MutableMap<String, Any?> by map {
    private var lock: Boolean = false

    val savedState: SavedStateMap by lazy(LazyThreadSafetyMode.NONE) {
        lock = true
        map
    }

    override fun put(key: String, value: Any?): Any? {
        checkState()
        checkKey(key, value)
        return map.put(key, value)
    }

    override fun putAll(from: Map<out String, Any?>) {
        checkState()
        from.entries.forEach { checkKey(it.key, it.value) }
        return map.putAll(from)
    }

    private fun checkState() {
        check(!lock) { "This MutableSavedStateMap has already dumped its state, it is meaningless to write anything anymore" }
    }

    private fun checkKey(key: String, value: Any?) {
        check(!map.containsKey(key)) {
            "Save instance process has faced key collision at '${key}': " +
                    "existing value is '${map[key]}', " +
                    "new value is '$value'"
        }
    }

}

package com.bumble.appyx.routingsource.backstack

import com.bumble.appyx.core.routing.BaseRoutingSource
import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.core.routing.Operation.Noop
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.core.routing.backpresshandlerstrategies.BackPressHandlerStrategy
import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver
import com.bumble.appyx.core.routing.operationstrategies.ExecuteImmediately
import com.bumble.appyx.core.routing.operationstrategies.OperationStrategy
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.DESTROYED
import com.bumble.appyx.routingsource.backstack.backpresshandler.PopBackPressHandler
import com.bumble.appyx.core.state.SavedStateMap
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.ACTIVE

class BackStack<Routing : Any>(
    initialElement: Routing,
    savedStateMap: SavedStateMap?,
    key: String = KEY_ROUTING_SOURCE,
    backPressHandler: BackPressHandlerStrategy<Routing, TransitionState> = PopBackPressHandler(),
    operationStrategy: OperationStrategy<Routing, TransitionState> = ExecuteImmediately(),
    screenResolver: OnScreenStateResolver<TransitionState> = BackStackOnScreenResolver
) : BaseRoutingSource<Routing, TransitionState>(
    backPressHandler = backPressHandler,
    screenResolver = screenResolver,
    operationStrategy = operationStrategy,
    finalState = DESTROYED,
    savedStateMap = savedStateMap,
    key = key,
) {

    enum class TransitionState {
        CREATED, ACTIVE, STASHED_IN_BACK_STACK, DESTROYED,
    }

    override val initialElements = listOf(
        BackStackElement(
            key = RoutingKey(initialElement),
            fromState = ACTIVE,
            targetState = ACTIVE,
            operation = Noop()
        )
    )

}

package com.bumble.appyx.routingsource.backstack

import com.bumble.appyx.core.routing.RoutingElement
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState

typealias BackStackElement<T> = RoutingElement<T, TransitionState>

package com.bumble.appyx.routingsource.backstack

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState

typealias BackStackElements<T> = RoutingElements<T, TransitionState>

package com.bumble.appyx.routingsource.backstack

import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.ACTIVE

val <T> BackStackElements<T>.active: BackStackElement<T>?
    get() = lastOrNull { it.targetState == ACTIVE }

val <T : Any> BackStack<T>.active: BackStackElement<T>?
    get() = elements.value.active

val <T> BackStackElements<T>.activeRouting: T?
    get() = active?.key?.routing

val <T : Any> BackStack<T>.activeRouting: T?
    get() = elements.value.activeRouting

val <T> BackStackElements<T>.activeIndex: Int
    get() = indexOfLast { it.targetState == ACTIVE }

package com.bumble.appyx.routingsource.backstack

import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState

object BackStackOnScreenResolver : OnScreenStateResolver<TransitionState> {
    override fun isOnScreen(state: TransitionState): Boolean =
        when (state) {
            TransitionState.CREATED,
            TransitionState.STASHED_IN_BACK_STACK,
            TransitionState.DESTROYED -> false
            TransitionState.ACTIVE -> true
        }
}

//package com.bumble.appyx.core.routing.source.backstack
//
//class OnScreenBackStack<T>(
//    initialElement: T,
//) : BackStack<T>(
//    initialElement
//) {
//
//    /**
//     * Never remove anything from screen
//     */
//    override val offScreen: Elements<T>
//        get() = emptyList()
//
//    /**
//     * All elements remain on screen
//     */
//    override val onScreen: Elements<T>
//        get() = elementsObservable
//}

package com.bumble.appyx.routingsource.spotlight

import com.bumble.appyx.core.routing.BaseRoutingSource
import com.bumble.appyx.core.routing.backpresshandlerstrategies.BackPressHandlerStrategy
import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver
import com.bumble.appyx.core.routing.operationstrategies.ExecuteImmediately
import com.bumble.appyx.core.routing.operationstrategies.OperationStrategy
import com.bumble.appyx.core.state.SavedStateMap
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState
import com.bumble.appyx.routingsource.spotlight.backpresshandler.GoToDefault
import com.bumble.appyx.routingsource.spotlight.operation.toSpotlightElements

class Spotlight<Routing : Any>(
    items: List<Routing>,
    initialActiveIndex: Int = 0,
    savedStateMap: SavedStateMap?,
    key: String = KEY_ROUTING_SOURCE,
    backPressHandler: BackPressHandlerStrategy<Routing, TransitionState> = GoToDefault(
        initialActiveIndex
    ),
    operationStrategy: OperationStrategy<Routing, TransitionState> = ExecuteImmediately(),
    screenResolver: OnScreenStateResolver<TransitionState> = SpotlightOnScreenResolver
) : BaseRoutingSource<Routing, TransitionState>(
    backPressHandler = backPressHandler,
    operationStrategy = operationStrategy,
    screenResolver = screenResolver,
    finalState = null,
    savedStateMap = savedStateMap,
    key = key,
) {

    enum class TransitionState {
        INACTIVE_BEFORE, ACTIVE, INACTIVE_AFTER;
    }

    override val initialElements = items.toSpotlightElements(initialActiveIndex)

}

package com.bumble.appyx.routingsource.spotlight

import com.bumble.appyx.core.routing.RoutingElement
import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState

typealias SpotlightElement<T> = RoutingElement<T, TransitionState>

typealias SpotlightElements<T> = RoutingElements<T, TransitionState>

package com.bumble.appyx.routingsource.spotlight

import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.ACTIVE

val <T> SpotlightElements<T>.current: SpotlightElement<T>?
    get() = this.lastOrNull { it.targetState == ACTIVE }

val <T> SpotlightElements<T>.currentIndex: Int
    get() = this.indexOfLast { it.targetState == ACTIVE }

package com.bumble.appyx.routingsource.spotlight

import kotlinx.coroutines.flow.map

fun <T : Any> Spotlight<T>.hasNext() =
    elements.map { value -> value.lastIndex != elements.value.currentIndex }

fun <T : Any> Spotlight<T>.hasPrevious() =
    elements.map { value -> value.currentIndex != 0 }

fun <T : Any> Spotlight<T>.activeIndex() =
    elements.map { value -> value.currentIndex }

fun <T : Any> Spotlight<T>.elementsCount() =
    elements.value.size

package com.bumble.appyx.routingsource.spotlight

import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.INACTIVE_AFTER
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.INACTIVE_BEFORE

object SpotlightOnScreenResolver : OnScreenStateResolver<TransitionState> {
    override fun isOnScreen(state: TransitionState): Boolean =
        when (state) {
            INACTIVE_BEFORE,
            INACTIVE_AFTER -> false
            ACTIVE -> true
        }
}

package com.bumble.appyx.interop.ribs

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper

@Suppress("UNCHECKED_CAST")
internal fun <T : Activity> Context.findActivity(): T? {
    return if (this is Activity) {
        this as T?
    } else {
        val contextWrapper = this as ContextWrapper?
        val baseContext = contextWrapper?.baseContext
        requireNotNull(baseContext)
        baseContext.findActivity()
    }
}

package com.bumble.appyx.interop.ribs

import com.bumble.appyx.core.integrationpoint.IntegrationPoint

interface IntegrationPointAppyxProvider {

    val integrationPointAppyx: IntegrationPoint
}

package com.bumble.appyx.interop.ribs

import android.content.Intent
import android.os.Bundle
import com.badoo.ribs.android.RibActivity
import com.bumble.appyx.core.integrationpoint.ActivityIntegrationPoint

abstract class InteropActivity : RibActivity(), IntegrationPointAppyxProvider {

    override lateinit var integrationPointAppyx: ActivityIntegrationPoint
        protected set

    protected open fun createIntegrationPointV2(savedInstanceState: Bundle?): ActivityIntegrationPoint =
        ActivityIntegrationPoint(
            activity = this,
            savedInstanceState = savedInstanceState
        )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        integrationPointAppyx = createIntegrationPointV2(savedInstanceState)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        integrationPointAppyx.onActivityResult(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        integrationPointAppyx.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        integrationPointAppyx.onSaveInstanceState(outState)
    }

}

package com.bumble.appyx.interop.ribs

import com.badoo.ribs.builder.SimpleBuilder
import com.badoo.ribs.core.modality.BuildParams
import com.bumble.appyx.interop.ribs.InteropNodeImpl.Companion.InteropNodeKey
import com.bumble.appyx.core.integration.NodeFactory
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.build

class InteropBuilder<N : Node>(
    private val nodeFactory: NodeFactory<N>
) : SimpleBuilder<InteropNode<N>>() {

    override fun build(buildParams: BuildParams<Nothing?>): InteropNode<N> {
        val bundle = buildParams.savedInstanceState?.getBundle(InteropNodeKey)
        val stateMap = bundle?.let {
            val keys = bundle.keySet()
            val map = mutableMapOf<String, Any?>()
            keys.forEach {
                map[it] = bundle[it]
            }
            map
        }

        val appyxNode = nodeFactory
            .create(
                buildContext = BuildContext.root(
                    savedStateMap = stateMap,
                    customisations = buildParams.buildContext.customisations
                )
            )
            .build()

        return InteropNodeImpl(buildParams = buildParams, appyxNode = appyxNode)
    }
}

package com.bumble.appyx.interop.ribs

import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.lifecycle.LifecycleEventObserver
import com.badoo.ribs.core.Rib
import com.badoo.ribs.core.modality.BuildParams
import com.bumble.appyx.interop.ribs.InteropView.Dependency
import com.bumble.appyx.interop.ribs.InteropView.Factory
import com.bumble.appyx.core.node.Node

interface InteropNode<N : Node> : Rib {
    val appyxNode: N
}

internal class InteropNodeImpl<N : Node>(
    buildParams: BuildParams<*>,
    override val appyxNode: N
) : com.badoo.ribs.core.Node<InteropView>(
    buildParams = buildParams,
    viewFactory = Factory<N>().invoke(object : Dependency<N> {
        override val appyxNode: N = appyxNode
    })
), InteropNode<N> {

    private val observer = LifecycleEventObserver { source, _ ->
        appyxNode.updateLifecycleState(source.lifecycle.currentState)
    }

    override fun onCreate() {
        super.onCreate()
        appyxNode.updateLifecycleState(lifecycle.currentState)
        lifecycle.addObserver(observer)
    }

    override fun onDestroy(isRecreating: Boolean) {
        super.onDestroy(isRecreating)
        lifecycle.removeObserver(observer)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        val state = appyxNode.saveInstanceState { true }
        outState.putBundle(InteropNodeKey, state.toBundle())
    }

    companion object {
        const val InteropNodeKey = "InteropNodeKey"
    }

    private fun Map<String, Any?>.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray())

}

package com.bumble.appyx.interop.ribs

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import com.badoo.ribs.compose.ComposeRibView
import com.badoo.ribs.compose.ComposeView
import com.badoo.ribs.core.view.ViewFactory
import com.badoo.ribs.core.view.ViewFactoryBuilder
import com.bumble.appyx.core.integrationpoint.IntegrationPoint
import com.bumble.appyx.core.node.Node

internal class InteropView private constructor(
    override val context: Context,
    private val appyxNode: Node,
) : ComposeRibView(context) {

    private val integrationPoint = retrieveIntegrationPoint()

    init {
        appyxNode.integrationPoint = integrationPoint
    }

    override val composable: ComposeView
        get() = @Composable {
            appyxNode.Compose()
        }

    class Factory<N: Node> : ViewFactoryBuilder<Dependency<N>, InteropView> {
        override fun invoke(deps: Dependency<N>): ViewFactory<InteropView> =
            ViewFactory {
                InteropView(
                    context = it.parent.context,
                    appyxNode = deps.appyxNode
                )
            }
    }

    private fun retrieveIntegrationPoint(): IntegrationPoint {
        val activity = context.findActivity<AppCompatActivity>()
        return if (activity is IntegrationPointAppyxProvider) {
            activity.integrationPointAppyx
        } else {
            throw IllegalStateException("Activity where InteropNode is used must implement IntegrationPointAppyxProvider")
        }
    }

    interface Dependency<N : Node> {
        val appyxNode: N
    }
}

package com.bumble.appyx.connectable.rx2

import com.bumble.appyx.core.plugin.NodeLifecycleAware
import com.jakewharton.rxrelay2.Relay

interface Connectable<Input, Output> : NodeLifecycleAware {
    val input: Relay<Input>
    val output: Relay<Output>
}

package com.bumble.appyx.connectable.rx2

import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.lifecycle.subscribe
import com.jakewharton.rxrelay2.PublishRelay
import com.jakewharton.rxrelay2.Relay
import io.reactivex.Observer

class NodeConnector<Input, Output>(
    override val input: Relay<Input> = PublishRelay.create(),
) : Connectable<Input, Output> {

    private val intake: Relay<Output> = PublishRelay.create()
    private val exhaust: Relay<Output> = PublishRelay.create()
    private var isFlushed = false
    private val outputCache = mutableListOf<Output>()

    override val output: Relay<Output> = object : Relay<Output>() {

        override fun subscribeActual(observer: Observer<in Output>?) {
            exhaust.subscribe(observer as Observer<Output>)
        }

        override fun accept(value: Output) {
            intake.accept(value)
        }

        override fun hasObservers() = exhaust.hasObservers()

    }

    override fun onCreate(lifecycle: Lifecycle) {
        lifecycle.subscribe(onCreate = { flushOutputCache() })
    }

    private val cacheSubscription = intake.subscribe {
        synchronized(this) {
            if (!isFlushed) {
                outputCache.add(it)
            } else {
                exhaust.accept(it)
                switchToExhaust()
            }
        }
    }

    private fun flushOutputCache() {
        synchronized(this) {
            if (isFlushed) error("Already flushed")
            isFlushed = true
            outputCache.forEach { exhaust.accept(it) }
            outputCache.clear()
        }
    }

    private fun switchToExhaust() {
        intake.subscribe { exhaust.accept(it) }
        cacheSubscription.dispose()
    }
}

package com.bumble.appyx.routingsource.modal

import com.bumble.appyx.core.routing.BaseRoutingSource
import com.bumble.appyx.core.routing.Operation.Noop
import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.core.routing.backpresshandlerstrategies.BackPressHandlerStrategy
import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver
import com.bumble.appyx.core.routing.operationstrategies.ExecuteImmediately
import com.bumble.appyx.core.routing.operationstrategies.OperationStrategy
import com.bumble.appyx.core.state.SavedStateMap
import com.bumble.appyx.routingsource.modal.Modal.TransitionState
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.CREATED
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.DESTROYED
import com.bumble.appyx.routingsource.modal.backpresshandler.RevertBackPressHandler

class Modal<Routing : Any>(
    initialElement: Routing,
    savedStateMap: SavedStateMap?,
    key: String = KEY_ROUTING_SOURCE,
    backPressHandler: BackPressHandlerStrategy<Routing, TransitionState> = RevertBackPressHandler(),
    operationStrategy: OperationStrategy<Routing, TransitionState> = ExecuteImmediately(),
    screenResolver: OnScreenStateResolver<TransitionState> = ModalOnScreenResolver
) : BaseRoutingSource<Routing, TransitionState>(
    savedStateMap = savedStateMap,
    screenResolver = screenResolver,
    operationStrategy = operationStrategy,
    backPressHandler = backPressHandler,
    key = key,
    finalState = DESTROYED
) {

    enum class TransitionState {
        CREATED, MODAL, FULL_SCREEN, DESTROYED
    }

    override val initialElements: RoutingElements<Routing, TransitionState> = listOf(
        ModalElement(
            key = RoutingKey(initialElement),
            fromState = CREATED,
            targetState = CREATED,
            operation = Noop()
        )
    )
}

package com.bumble.appyx.routingsource.modal

import com.bumble.appyx.routingsource.modal.Modal.TransitionState
import com.bumble.appyx.core.routing.RoutingElement
import com.bumble.appyx.core.routing.RoutingElements

typealias ModalElement<T> = RoutingElement<T, TransitionState>

typealias ModalElements<T> = RoutingElements<T, TransitionState>

package com.bumble.appyx.routingsource.modal

import com.bumble.appyx.routingsource.modal.Modal.TransitionState
import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver

object ModalOnScreenResolver : OnScreenStateResolver<TransitionState> {
    override fun isOnScreen(state: TransitionState): Boolean =
        when (state) {
            TransitionState.MODAL,
            TransitionState.FULL_SCREEN,
            TransitionState.CREATED -> true
            TransitionState.DESTROYED -> false
        }
}

package com.bumble.appyx.routingsource.modal

import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.Dp
import com.bumble.appyx.routingsource.modal.Modal.TransitionState
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor
import com.bumble.appyx.core.routing.transition.TransitionSpec

@Suppress("TransitionPropertiesLabel")
class ModalTransitionHandler<T>(
    private val transitionSpec: TransitionSpec<TransitionState, Float> = { tween(500) }
) : ModifierTransitionHandler<T, TransitionState>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<TransitionState>,
        descriptor: TransitionDescriptor<T, TransitionState>
    ): Modifier = modifier.composed {
        val screenHeight = LocalConfiguration.current.screenHeightDp
        val offset = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    TransitionState.CREATED -> 1f
                    TransitionState.MODAL -> 0.5f
                    TransitionState.FULL_SCREEN -> 0f
                    TransitionState.DESTROYED -> -5f
                }
            })

        val height = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    TransitionState.CREATED -> 0f
                    TransitionState.MODAL -> 0.5f
                    TransitionState.FULL_SCREEN -> 1f
                    TransitionState.DESTROYED -> 1f
                }
            })

        val width = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    TransitionState.CREATED -> 0.8f
                    TransitionState.MODAL -> 0.9f
                    TransitionState.FULL_SCREEN -> 1f
                    TransitionState.DESTROYED -> 1f
                }
            })

        val cornerRadius = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    TransitionState.CREATED -> 20f
                    TransitionState.MODAL -> 20f
                    TransitionState.FULL_SCREEN -> 0f
                    TransitionState.DESTROYED -> 0f
                }
            })

        offset(x = Dp(0f), y = Dp(screenHeight * offset.value))
            .fillMaxWidth(width.value)
            .fillMaxHeight(height.value)
            .background(
                color = Color(0xFFBDC6D1),
                shape = RoundedCornerShape(Dp(cornerRadius.value))
            )
    }

}

package com.bumble.appyx.routingsource.tiles

import com.bumble.appyx.core.routing.BaseRoutingSource
import com.bumble.appyx.core.routing.Operation.Noop
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.core.routing.backpresshandlerstrategies.BackPressHandlerStrategy
import com.bumble.appyx.routingsource.tiles.backPressHandler.DeselectAllTiles
import com.bumble.appyx.routingsource.tiles.Tiles.TransitionState
import com.bumble.appyx.routingsource.tiles.Tiles.TransitionState.CREATED
import com.bumble.appyx.routingsource.tiles.Tiles.TransitionState.STANDARD

class Tiles<T : Any>(
    initialItems: List<T>,
    backPressHandler: BackPressHandlerStrategy<T, TransitionState> = DeselectAllTiles()
) : BaseRoutingSource<T, TransitionState>(
    backPressHandler = backPressHandler,
    screenResolver = TilesOnScreenResolver,
    finalState = null,
    savedStateMap = null,
) {

    enum class TransitionState {
        CREATED, STANDARD, SELECTED, DESTROYED
    }

    override val initialElements = initialItems.map {
        TilesElement(
            key = RoutingKey(it),
            fromState = CREATED,
            targetState = STANDARD,
            operation = Noop()
        )
    }
}

package com.bumble.appyx.routingsource.tiles

import com.bumble.appyx.core.routing.RoutingElement
import com.bumble.appyx.routingsource.tiles.Tiles.TransitionState

typealias TilesElement<T> = RoutingElement<T, TransitionState>

package com.bumble.appyx.routingsource.tiles

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.routingsource.tiles.Tiles.TransitionState

typealias TilesElements<T> = RoutingElements<T, TransitionState>

package com.bumble.appyx.routingsource.tiles

import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver
import com.bumble.appyx.routingsource.tiles.Tiles.TransitionState

internal object TilesOnScreenResolver : OnScreenStateResolver<TransitionState> {
    override fun isOnScreen(state: TransitionState): Boolean =
        when (state) {
            TransitionState.CREATED,
            TransitionState.STANDARD,
            TransitionState.SELECTED -> true
            TransitionState.DESTROYED -> false
        }
}

package com.bumble.appyx.sandbox.off

import androidx.compose.animation.animateColor
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor

@Suppress("TransitionPropertiesLabel")
object ContainerTransitionHandlerColour :
    ModifierTransitionHandler<Any, BackStack.TransitionState>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<BackStack.TransitionState>,
        descriptor: TransitionDescriptor<Any, BackStack.TransitionState>
    ): Modifier = modifier.composed {
        val color = transition.animateColor(
            transitionSpec = { tween(3500) },
            targetValueByState = {
                when (it) {
                    BackStack.TransitionState.CREATED -> Color.Blue
                    BackStack.TransitionState.ACTIVE -> Color.Green
                    BackStack.TransitionState.STASHED_IN_BACK_STACK -> Color.LightGray
                    BackStack.TransitionState.DESTROYED -> Color.Red
                }
            })

        background(color = color.value)
    }

}

package com.bumble.appyx.sandbox.ui

import androidx.compose.ui.graphics.Color

// reds
val md_red_50 = Color(0xFFFFEBEE)
val md_red_100 = Color(0xFFFFCDD2)
val md_red_200 = Color(0xFFEF9A9A)
val md_red_300 = Color(0xFFE57373)
val md_red_400 = Color(0xFFEF5350)
val md_red_500 = Color(0xFFF44336)
val md_red_600 = Color(0xFFE53935)
val md_red_700 = Color(0xFFD32F2F)
val md_red_800 = Color(0xFFC62828)
val md_red_900 = Color(0xFFB71C1C)
val md_red_A100 = Color(0xFFFF8A80)
val md_red_A200 = Color(0xFFFF5252)
val md_red_A400 = Color(0xFFFF1744)
val md_red_A700 = Color(0xFFD50000)

//  pinks
val md_pink_50 = Color(0xFFFCE4EC)
val md_pink_100 = Color(0xFFF8BBD0)
val md_pink_200 = Color(0xFFF48FB1)
val md_pink_300 = Color(0xFFF06292)
val md_pink_400 = Color(0xFFEC407A)
val md_pink_500 = Color(0xFFE91E63)
val md_pink_600 = Color(0xFFD81B60)
val md_pink_700 = Color(0xFFC2185B)
val md_pink_800 = Color(0xFFAD1457)
val md_pink_900 = Color(0xFF880E4F)
val md_pink_A100 = Color(0xFFFF80AB)
val md_pink_A200 = Color(0xFFFF4081)
val md_pink_A400 = Color(0xFFF50057)
val md_pink_A700 = Color(0xFFC51162)

//  purples
val md_purple_50 = Color(0xFFF3E5F5)
val md_purple_100 = Color(0xFFE1BEE7)
val md_purple_200 = Color(0xFFCE93D8)
val md_purple_300 = Color(0xFFBA68C8)
val md_purple_400 = Color(0xFFAB47BC)
val md_purple_500 = Color(0xFF9C27B0)
val md_purple_600 = Color(0xFF8E24AA)
val md_purple_700 = Color(0xFF7B1FA2)
val md_purple_800 = Color(0xFF6A1B9A)
val md_purple_900 = Color(0xFF4A148C)
val md_purple_A100 = Color(0xFFEA80FC)
val md_purple_A200 = Color(0xFFE040FB)
val md_purple_A400 = Color(0xFFD500F9)
val md_purple_A700 = Color(0xFFAA00FF)

//  deep purples
val md_deep_purple_50 = Color(0xFFEDE7F6)
val md_deep_purple_100 = Color(0xFFD1C4E9)
val md_deep_purple_200 = Color(0xFFB39DDB)
val md_deep_purple_300 = Color(0xFF9575CD)
val md_deep_purple_400 = Color(0xFF7E57C2)
val md_deep_purple_500 = Color(0xFF673AB7)
val md_deep_purple_600 = Color(0xFF5E35B1)
val md_deep_purple_700 = Color(0xFF512DA8)
val md_deep_purple_800 = Color(0xFF4527A0)
val md_deep_purple_900 = Color(0xFF311B92)
val md_deep_purple_A100 = Color(0xFFB388FF)
val md_deep_purple_A200 = Color(0xFF7C4DFF)
val md_deep_purple_A400 = Color(0xFF651FFF)
val md_deep_purple_A700 = Color(0xFF6200EA)

//  indigo
val md_indigo_50 = Color(0xFFE8EAF6)
val md_indigo_100 = Color(0xFFC5CAE9)
val md_indigo_200 = Color(0xFF9FA8DA)
val md_indigo_300 = Color(0xFF7986CB)
val md_indigo_400 = Color(0xFF5C6BC0)
val md_indigo_500 = Color(0xFF3F51B5)
val md_indigo_600 = Color(0xFF3949AB)
val md_indigo_700 = Color(0xFF303F9F)
val md_indigo_800 = Color(0xFF283593)
val md_indigo_900 = Color(0xFF1A237E)
val md_indigo_A100 = Color(0xFF8C9EFF)
val md_indigo_A200 = Color(0xFF536DFE)
val md_indigo_A400 = Color(0xFF3D5AFE)
val md_indigo_A700 = Color(0xFF304FFE)

// blue
val md_blue_50 = Color(0xFFE3F2FD)
val md_blue_100 = Color(0xFFBBDEFB)
val md_blue_200 = Color(0xFF90CAF9)
val md_blue_300 = Color(0xFF64B5F6)
val md_blue_400 = Color(0xFF42A5F5)
val md_blue_500 = Color(0xFF2196F3)
val md_blue_600 = Color(0xFF1E88E5)
val md_blue_700 = Color(0xFF1976D2)
val md_blue_800 = Color(0xFF1565C0)
val md_blue_900 = Color(0xFF0D47A1)
val md_blue_A100 = Color(0xFF82B1FF)
val md_blue_A200 = Color(0xFF448AFF)
val md_blue_A400 = Color(0xFF2979FF)
val md_blue_A700 = Color(0xFF2962FF)

//  light blue
val md_light_blue_50 = Color(0xFFE1F5FE)
val md_light_blue_100 = Color(0xFFB3E5FC)
val md_light_blue_200 = Color(0xFF81D4fA)
val md_light_blue_300 = Color(0xFF4fC3F7)
val md_light_blue_400 = Color(0xFF29B6FC)
val md_light_blue_500 = Color(0xFF03A9F4)
val md_light_blue_600 = Color(0xFF039BE5)
val md_light_blue_700 = Color(0xFF0288D1)
val md_light_blue_800 = Color(0xFF0277BD)
val md_light_blue_900 = Color(0xFF01579B)
val md_light_blue_A100 = Color(0xFF80D8FF)
val md_light_blue_A200 = Color(0xFF40C4FF)
val md_light_blue_A400 = Color(0xFF00B0FF)
val md_light_blue_A700 = Color(0xFF0091EA)

//  cyan
val md_cyan_50 = Color(0xFFE0F7FA)
val md_cyan_100 = Color(0xFFB2EBF2)
val md_cyan_200 = Color(0xFF80DEEA)
val md_cyan_300 = Color(0xFF4DD0E1)
val md_cyan_400 = Color(0xFF26C6DA)
val md_cyan_500 = Color(0xFF00BCD4)
val md_cyan_600 = Color(0xFF00ACC1)
val md_cyan_700 = Color(0xFF0097A7)
val md_cyan_800 = Color(0xFF00838F)
val md_cyan_900 = Color(0xFF006064)
val md_cyan_A100 = Color(0xFF84FFFF)
val md_cyan_A200 = Color(0xFF18FFFF)
val md_cyan_A400 = Color(0xFF00E5FF)
val md_cyan_A700 = Color(0xFF00B8D4)

//  teal
val md_teal_50 = Color(0xFFE0F2F1)
val md_teal_100 = Color(0xFFB2DFDB)
val md_teal_200 = Color(0xFF80CBC4)
val md_teal_300 = Color(0xFF4DB6AC)
val md_teal_400 = Color(0xFF26A69A)
val md_teal_500 = Color(0xFF009688)
val md_teal_600 = Color(0xFF00897B)
val md_teal_700 = Color(0xFF00796B)
val md_teal_800 = Color(0xFF00695C)
val md_teal_900 = Color(0xFF004D40)
val md_teal_A100 = Color(0xFFA7FFEB)
val md_teal_A200 = Color(0xFF64FFDA)
val md_teal_A400 = Color(0xFF1DE9B6)
val md_teal_A700 = Color(0xFF00BFA5)

//  green
val md_green_50 = Color(0xFFE8F5E9)
val md_green_100 = Color(0xFFC8E6C9)
val md_green_200 = Color(0xFFA5D6A7)
val md_green_300 = Color(0xFF81C784)
val md_green_400 = Color(0xFF66BB6A)
val md_green_500 = Color(0xFF4CAF50)
val md_green_600 = Color(0xFF43A047)
val md_green_700 = Color(0xFF388E3C)
val md_green_800 = Color(0xFF2E7D32)
val md_green_900 = Color(0xFF1B5E20)
val md_green_A100 = Color(0xFFB9F6CA)
val md_green_A200 = Color(0xFF69F0AE)
val md_green_A400 = Color(0xFF00E676)
val md_green_A700 = Color(0xFF00C853)

// light green
val md_light_green_50 = Color(0xFFF1F8E9)
val md_light_green_100 = Color(0xFFDCEDC8)
val md_light_green_200 = Color(0xFFC5E1A5)
val md_light_green_300 = Color(0xFFAED581)
val md_light_green_400 = Color(0xFF9CCC65)
val md_light_green_500 = Color(0xFF8BC34A)
val md_light_green_600 = Color(0xFF7CB342)
val md_light_green_700 = Color(0xFF689F38)
val md_light_green_800 = Color(0xFF558B2F)
val md_light_green_900 = Color(0xFF33691E)
val md_light_green_A100 = Color(0xFFCCFF90)
val md_light_green_A200 = Color(0xFFB2FF59)
val md_light_green_A400 = Color(0xFF76FF03)
val md_light_green_A700 = Color(0xFF64DD17)

//  lime
val md_lime_50 = Color(0xFFF9FBE7)
val md_lime_100 = Color(0xFFF0F4C3)
val md_lime_200 = Color(0xFFE6EE9C)
val md_lime_300 = Color(0xFFDCE775)
val md_lime_400 = Color(0xFFD4E157)
val md_lime_500 = Color(0xFFCDDC39)
val md_lime_600 = Color(0xFFC0CA33)
val md_lime_700 = Color(0xFFA4B42B)
val md_lime_800 = Color(0xFF9E9D24)
val md_lime_900 = Color(0xFF827717)
val md_lime_A100 = Color(0xFFF4FF81)
val md_lime_A200 = Color(0xFFEEFF41)
val md_lime_A400 = Color(0xFFC6FF00)
val md_lime_A700 = Color(0xFFAEEA00)

// yellow
val md_yellow_50 = Color(0xFFFFFDE7)
val md_yellow_100 = Color(0xFFFFF9C4)
val md_yellow_200 = Color(0xFFFFF590)
val md_yellow_300 = Color(0xFFFFF176)
val md_yellow_400 = Color(0xFFFFEE58)
val md_yellow_500 = Color(0xFFFFEB3B)
val md_yellow_600 = Color(0xFFFDD835)
val md_yellow_700 = Color(0xFFFBC02D)
val md_yellow_800 = Color(0xFFF9A825)
val md_yellow_900 = Color(0xFFF57F17)
val md_yellow_A100 = Color(0xFFFFFF82)
val md_yellow_A200 = Color(0xFFFFFF00)
val md_yellow_A400 = Color(0xFFFFEA00)
val md_yellow_A700 = Color(0xFFFFD600)

// amber
val md_amber_50 = Color(0xFFFFF8E1)
val md_amber_100 = Color(0xFFFFECB3)
val md_amber_200 = Color(0xFFFFE082)
val md_amber_300 = Color(0xFFFFD54F)
val md_amber_400 = Color(0xFFFFCA28)
val md_amber_500 = Color(0xFFFFC107)
val md_amber_600 = Color(0xFFFFB300)
val md_amber_700 = Color(0xFFFFA000)
val md_amber_800 = Color(0xFFFF8F00)
val md_amber_900 = Color(0xFFFF6F00)
val md_amber_A100 = Color(0xFFFFE57F)
val md_amber_A200 = Color(0xFFFFD740)
val md_amber_A400 = Color(0xFFFFC400)
val md_amber_A700 = Color(0xFFFFAB00)

// orange
val md_orange_50 = Color(0xFFFFF3E0)
val md_orange_100 = Color(0xFFFFE0B2)
val md_orange_200 = Color(0xFFFFCC80)
val md_orange_300 = Color(0xFFFFB74D)
val md_orange_400 = Color(0xFFFFA726)
val md_orange_500 = Color(0xFFFF9800)
val md_orange_600 = Color(0xFFFB8C00)
val md_orange_700 = Color(0xFFF57C00)
val md_orange_800 = Color(0xFFEF6C00)
val md_orange_900 = Color(0xFFE65100)
val md_orange_A100 = Color(0xFFFFD180)
val md_orange_A200 = Color(0xFFFFAB40)
val md_orange_A400 = Color(0xFFFF9100)
val md_orange_A700 = Color(0xFFFF6D00)

// deep orange
val md_deep_orange_50 = Color(0xFFFBE9A7)
val md_deep_orange_100 = Color(0xFFFFCCBC)
val md_deep_orange_200 = Color(0xFFFFAB91)
val md_deep_orange_300 = Color(0xFFFF8A65)
val md_deep_orange_400 = Color(0xFFFF7043)
val md_deep_orange_500 = Color(0xFFFF5722)
val md_deep_orange_600 = Color(0xFFF4511E)
val md_deep_orange_700 = Color(0xFFE64A19)
val md_deep_orange_800 = Color(0xFFD84315)
val md_deep_orange_900 = Color(0xFFBF360C)
val md_deep_orange_A100 = Color(0xFFFF9E80)
val md_deep_orange_A200 = Color(0xFFFF6E40)
val md_deep_orange_A400 = Color(0xFFFF3D00)
val md_deep_orange_A700 = Color(0xFFDD2600)

// brown
val md_brown_50 = Color(0xFFEFEBE9)
val md_brown_100 = Color(0xFFD7CCC8)
val md_brown_200 = Color(0xFFBCAAA4)
val md_brown_300 = Color(0xFFA1887F)
val md_brown_400 = Color(0xFF8D6E63)
val md_brown_500 = Color(0xFF795548)
val md_brown_600 = Color(0xFF6D4C41)
val md_brown_700 = Color(0xFF5D4037)
val md_brown_800 = Color(0xFF4E342E)
val md_brown_900 = Color(0xFF3E2723)

// grey
val md_grey_50 = Color(0xFFFAFAFA)
val md_grey_100 = Color(0xFFF5F5F5)
val md_grey_200 = Color(0xFFEEEEEE)
val md_grey_300 = Color(0xFFE0E0E0)
val md_grey_400 = Color(0xFFBDBDBD)
val md_grey_500 = Color(0xFF9E9E9E)
val md_grey_600 = Color(0xFF757575)
val md_grey_700 = Color(0xFF616161)
val md_grey_800 = Color(0xFF424242)
val md_grey_900 = Color(0xFF212121)
val md_black_1000 = Color(0xFF000000)
val md_white_1000 = Color(0xFFffffff)

// blue grey
val md_blue_grey_50 = Color(0xFFECEFF1)
val md_blue_grey_100 = Color(0xFFCFD8DC)
val md_blue_grey_200 = Color(0xFFB0BBC5)
val md_blue_grey_300 = Color(0xFF90A4AE)
val md_blue_grey_400 = Color(0xFF78909C)
val md_blue_grey_500 = Color(0xFF607D8B)
val md_blue_grey_600 = Color(0xFF546E7A)
val md_blue_grey_700 = Color(0xFF455A64)
val md_blue_grey_800 = Color(0xFF37474F)
val md_blue_grey_900 = Color(0xFF263238)

package com.bumble.appyx.app.composable.graph

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier

interface GraphNode {

    val isActive: MutableState<Boolean>

    fun children(): List<GraphNode>

    @Composable
    fun View(modifier: Modifier)
}

package com.bumble.appyx.app.node.child

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.coroutineScope
import com.bumble.appyx.app.ui.atomic_tangerine
import com.bumble.appyx.app.ui.manatee
import com.bumble.appyx.app.ui.md_amber_500
import com.bumble.appyx.app.ui.md_blue_500
import com.bumble.appyx.app.ui.md_blue_grey_500
import com.bumble.appyx.app.ui.md_cyan_500
import com.bumble.appyx.app.ui.md_grey_500
import com.bumble.appyx.app.ui.md_indigo_500
import com.bumble.appyx.app.ui.md_light_blue_500
import com.bumble.appyx.app.ui.md_light_green_500
import com.bumble.appyx.app.ui.md_lime_500
import com.bumble.appyx.app.ui.md_pink_500
import com.bumble.appyx.app.ui.md_teal_500
import com.bumble.appyx.app.ui.silver_sand
import com.bumble.appyx.app.ui.sizzling_red
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.modality.BuildContext.Companion.root
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.state.MutableSavedStateMap
import kotlinx.coroutines.delay
import kotlin.random.Random

class GenericChildNode(
    buildContext: BuildContext,
    counterStartValue: Int
) : Node(
    buildContext = buildContext
) {

    companion object {
        private const val KEY_ID = "Id"
        private const val KEY_COUNTER = "Counter"
        private const val KEY_COLOR_INDEX = "ColorIndex"
    }

    private val colors = listOf(
        manatee,
        sizzling_red,
        atomic_tangerine,
        silver_sand,
        md_pink_500,
        md_indigo_500,
        md_blue_500,
        md_light_blue_500,
        md_cyan_500,
        md_teal_500,
        md_light_green_500,
        md_lime_500,
        md_amber_500,
        md_grey_500,
        md_blue_grey_500
    )

    private val id = Random.nextInt(10000)
    private var counter by mutableStateOf(
        buildContext.savedStateMap?.get(KEY_COUNTER) as? Int ?: counterStartValue
    )
    private var colorIndex by mutableStateOf(
        buildContext.savedStateMap?.get(KEY_COLOR_INDEX) as? Int ?: Random.nextInt(colors.size)
    )

    init {
        lifecycle.coroutineScope.launchWhenCreated {
            while (true) {
                counter++
                delay(1000)
            }
        }
    }

    override fun onSaveInstanceState(state: MutableSavedStateMap) {
        super.onSaveInstanceState(state)
        state[KEY_ID] = id
        state[KEY_COUNTER] = counter
        state[KEY_COLOR_INDEX] = colorIndex
    }

    @Composable
    override fun View(modifier: Modifier) {
        val color by derivedStateOf { colors[colorIndex] }

        Box(
            modifier = modifier
                .fillMaxSize()
                .background(
                    color = color,
                    shape = RoundedCornerShape(6.dp)
                )
                .clickable {
                    colorIndex = Random.nextInt(colors.size)
                }
        ) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(6.dp)
            ) {
                Text(
                    text = "Child ($id)",
                    style = MaterialTheme.typography.caption
                )
                Text(
                    text = "$counter",
                    modifier = Modifier.align(Alignment.Center),
                    fontSize = 40.sp
                )
            }
        }
    }
}

@Preview
@Composable
fun GenericChildNodePreview() {
    Box(Modifier.size(200.dp)) {
        NodeHost(integrationPoint = IntegrationPointStub()) {
            GenericChildNode(root(null), 100)
        }
    }
}

package com.bumble.appyx.app.node.onboarding

import android.os.Parcelable
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toUpperCase
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import com.bumble.appyx.app.composable.SpotlightDotsIndicator
import com.bumble.appyx.app.node.onboarding.OnboardingContainerNode.Routing
import com.bumble.appyx.app.node.onboarding.OnboardingContainerNode.Routing.RoutingSource
import com.bumble.appyx.app.node.onboarding.screen.ApplicationTree
import com.bumble.appyx.app.node.onboarding.screen.IntroScreen
import com.bumble.appyx.app.node.onboarding.screen.RoutingSourceTeaser
import com.bumble.appyx.app.node.onboarding.screen.StatefulNode1
import com.bumble.appyx.app.node.onboarding.screen.StatefulNode2
import com.bumble.appyx.app.ui.AppyxSampleAppTheme
import com.bumble.appyx.app.ui.appyx_dark
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.modality.BuildContext.Companion.root
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.backpresshandler.GoToPrevious
import com.bumble.appyx.routingsource.spotlight.hasNext
import com.bumble.appyx.routingsource.spotlight.hasPrevious
import com.bumble.appyx.routingsource.spotlight.operation.next
import com.bumble.appyx.routingsource.spotlight.operation.previous
import com.bumble.appyx.routingsource.spotlight.transitionhandler.rememberSpotlightSlider
import kotlinx.parcelize.Parcelize

@ExperimentalUnitApi
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
class OnboardingContainerNode(
    buildContext: BuildContext,
    private val spotlight: Spotlight<Routing> = Spotlight(
        items = listOf(
            Routing.IntroScreen,
            Routing.ApplicationTree,
            Routing.StatefulNode1,
            Routing.StatefulNode2,
            RoutingSource,
        ),
        backPressHandler = GoToPrevious(),
        savedStateMap = buildContext.savedStateMap,
    ),
) : ParentNode<Routing>(
    routingSource = spotlight,
    buildContext = buildContext
) {

    sealed class Routing : Parcelable {
        @Parcelize
        object IntroScreen : Routing()

        @Parcelize
        object ApplicationTree : Routing()

        @Parcelize
        object StatefulNode1 : Routing()

        @Parcelize
        object StatefulNode2 : Routing()

        @Parcelize
        object RoutingSource : Routing()
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            Routing.IntroScreen -> IntroScreen(buildContext)
            Routing.ApplicationTree -> ApplicationTree(buildContext)
            Routing.StatefulNode1 -> StatefulNode1(buildContext)
            Routing.StatefulNode2 -> StatefulNode2(buildContext)
            Routing.RoutingSource -> RoutingSourceTeaser(buildContext)
        }

    @Composable
    override fun View(modifier: Modifier) {
        val hasPrevious = spotlight.hasPrevious().collectAsState(initial = false)
        val hasNext = spotlight.hasNext().collectAsState(initial = false)
        val previousVisibility = animateFloatAsState(
            targetValue = if (hasPrevious.value) 1f else 0f
        )
        Column(
            modifier = modifier
                .fillMaxSize()
        ) {
            Children(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth(),
                transitionHandler = rememberSpotlightSlider(clipToBounds = true),
                routingSource = spotlight
            )
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(bottom = 12.dp),
                contentAlignment = Alignment.Center
            ) {
                SpotlightDotsIndicator(
                    spotlight = spotlight
                )
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .wrapContentHeight()
                        .padding(
                            start = 24.dp,
                            end = 24.dp,
                        ),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {
                    if (hasNext.value) {
                        TextButton(
                            modifier = Modifier.alpha(previousVisibility.value),
                            enabled = hasPrevious.value,
                            onClick = { spotlight.previous() }
                        ) {
                            Text(
                                text = "Previous".toUpperCase(Locale.current),
                                fontWeight = FontWeight.Bold
                            )
                        }
                        TextButton(
                            onClick = { spotlight.next() }
                        ) {
                            Text(
                                text = "Next".toUpperCase(Locale.current),
                                fontWeight = FontWeight.Bold
                            )
                        }
                    } else {
                        Spacer(Modifier)
                        Button(
                            modifier = Modifier.fillMaxWidth(),
                            onClick = { finish() }
                        ) {
                            Text(
                                text = "Check it out!",
                                color = appyx_dark,
                            )
                        }
                        Spacer(Modifier)
                    }
                }
            }
        }
    }
}

@Preview
@Composable
@ExperimentalUnitApi
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
fun OnboardingContainerNodePreview() {
    AppyxSampleAppTheme(darkTheme = false) {
        PreviewContent()
    }
}

@Preview
@Composable
@ExperimentalUnitApi
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
fun OnboardingContainerNodePreviewDark() {
    AppyxSampleAppTheme(darkTheme = true) {
        PreviewContent()
    }
}

@Composable
@ExperimentalUnitApi
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
private fun PreviewContent() {
    Surface(color = MaterialTheme.colors.background) {
        Box(Modifier.fillMaxSize()) {
            NodeHost(integrationPoint = IntegrationPointStub()) {
                OnboardingContainerNode(root(null))
            }
        }
    }
}

package com.bumble.appyx.core.clienthelper.interactor

import com.bumble.appyx.core.children.ChildAware
import com.bumble.appyx.core.children.ChildAwareImpl
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.NodeAware
import com.bumble.appyx.core.plugin.NodeLifecycleAware
import com.bumble.appyx.core.plugin.SavesInstanceState

abstract class Interactor<N: Node>(
    private val childAwareImpl: ChildAware<N> = ChildAwareImpl()
) : NodeAware<N>,
    NodeLifecycleAware,
    ChildAware<N> by childAwareImpl,
    SavesInstanceState

package com.bumble.appyx.core.integrationpoint.activitystarter

import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityStarter.ActivityResultEvent
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeBasedEventStreamImpl
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeClient
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeRegistry

class ActivityBoundary(
    private val activityStarterHost: ActivityStarterHost,
    requestCodeRegistry: RequestCodeRegistry
) : RequestCodeBasedEventStreamImpl<ActivityResultEvent>(requestCodeRegistry),
    ActivityStarter,
    ActivityResultHandler {

    constructor(
        activity: Activity,
        requestCodeRegistry: RequestCodeRegistry
    ) : this(
        ActivityStarterHost.ActivityHost(activity),
        requestCodeRegistry
    )

    constructor(
        fragment: Fragment,
        requestCodeRegistry: RequestCodeRegistry
    ) : this(
        ActivityStarterHost.FragmentHost(fragment),
        requestCodeRegistry
    )

    override fun startActivity(createIntent: Context.() -> Intent) {
        activityStarterHost.startActivity(activityStarterHost.context.createIntent())
    }

    override fun startActivityForResult(
        client: RequestCodeClient,
        requestCode: Int,
        createIntent: Context.() -> Intent
    ) {
        activityStarterHost.startActivityForResult(
            activityStarterHost.context.createIntent(),
            client.forgeExternalRequestCode(requestCode)
        )
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        publish(
            requestCode,
            ActivityResultEvent(
                requestCode = requestCode.toInternalRequestCode(),
                resultCode = resultCode,
                data = data
            )
        )
    }
}

package com.bumble.appyx.core.integrationpoint.activitystarter

import android.content.Intent

interface ActivityResultHandler {

    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
}

package com.bumble.appyx.core.integrationpoint.activitystarter

import android.content.Context
import android.content.Intent
import com.bumble.appyx.core.integrationpoint.activitystarter.ActivityStarter.ActivityResultEvent
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeBasedEventStream
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeBasedEventStream.RequestCodeBasedEvent
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeClient

/**
 * Start Activities.
 *
 * A leaner dependency than passing an entire Activity or Context.
 *
 * @see [com.badoo.ribs.android.requestcode.RequestCodeRegistry] for requirements of The requestcode
 */
interface ActivityStarter : RequestCodeBasedEventStream<ActivityResultEvent> {

    fun startActivity(createIntent: Context.() -> Intent)

    fun startActivityForResult(client: RequestCodeClient, requestCode: Int, createIntent: Context.() -> Intent)

    data class ActivityResultEvent(
        override val requestCode: Int,
        val resultCode: Int,
        val data: Intent?
    ) : RequestCodeBasedEvent
}

package com.bumble.appyx.core.integrationpoint.activitystarter

import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment

interface ActivityStarterHost {

    val context: Context

    fun startActivity(intent: Intent)

    fun startActivityForResult(intent: Intent, requestCode: Int)

    class ActivityHost(private val activity: Activity) : ActivityStarterHost {
        override val context: Context
            get() = activity

        override fun startActivity(intent: Intent) {
            activity.startActivity(intent)
        }

        override fun startActivityForResult(intent: Intent, requestCode: Int) {
            activity.startActivityForResult(intent, requestCode)
        }
    }

    class FragmentHost(private val fragment: Fragment) : ActivityStarterHost {
        override val context: Context
            get() = fragment.requireContext()

        override fun startActivity(intent: Intent) {
            fragment.startActivity(intent)
        }

        override fun startActivityForResult(intent: Intent, requestCode: Int) {
            fragment.startActivityForResult(intent, requestCode)
        }
    }

}

package com.bumble.appyx.core.integrationpoint.activitystarter

interface CanProvideActivityStarter {
    val activityStarter: ActivityStarter
}

package com.bumble.appyx.core.integrationpoint.permissionrequester

import android.app.Activity
import android.content.pm.PackageManager
import androidx.fragment.app.Fragment
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester.CheckPermissionsResult
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester.RequestPermissionsEvent.Cancelled
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester.RequestPermissionsEvent.RequestPermissionsResult
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeBasedEventStreamImpl
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeClient
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeRegistry

class PermissionRequestBoundary(
    private val permissionRequesterHost: PermissionRequesterHost,
    requestCodeRegistry: RequestCodeRegistry
) : RequestCodeBasedEventStreamImpl<PermissionRequester.RequestPermissionsEvent>(requestCodeRegistry),
    PermissionRequester,
    PermissionRequestResultHandler {

    constructor(
        activity: Activity,
        requestCodeRegistry: RequestCodeRegistry
    ) : this(
        PermissionRequesterHost.ActivityHost(activity),
        requestCodeRegistry
    )

    constructor(
        fragment: Fragment,
        requestCodeRegistry: RequestCodeRegistry
    ) : this(
        PermissionRequesterHost.FragmentHost(fragment),
        requestCodeRegistry
    )

    override fun checkPermissions(
        client: RequestCodeClient,
        permissions: Array<String>
    ): CheckPermissionsResult {
        val granted = mutableListOf<String>()
        val shouldShowRationale = mutableListOf<String>()
        val canAsk = mutableListOf<String>()

        permissions.forEach { permission ->
            val list = when {
                permissionRequesterHost.isGranted(permission) -> granted
                permissionRequesterHost.shouldShowRationale(permission) -> shouldShowRationale
                else -> canAsk
            }

            list += permission
        }

        return CheckPermissionsResult(
            granted, canAsk, shouldShowRationale
        )
    }

    override fun requestPermissions(
        client: RequestCodeClient,
        requestCode: Int,
        permissions: Array<String>
    ) {
        permissionRequesterHost.requestPermissions(
            client.forgeExternalRequestCode(requestCode),
            permissions
        )
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (grantResults.isEmpty()) {
            onPermissionRequestCancelled(requestCode)

        } else {
            onPermissionRequestFinished(requestCode, permissions, grantResults)
        }
    }

    private fun onPermissionRequestCancelled(externalRequestCode: Int) {
        publish(
            externalRequestCode,
            Cancelled(
                requestCode = externalRequestCode.toInternalRequestCode()
            )
        )
    }

    private fun onPermissionRequestFinished(
        externalRequestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        val (granted, denied) = sortResults(permissions, grantResults)

        publish(
            externalRequestCode,
            RequestPermissionsResult(
                requestCode = externalRequestCode.toInternalRequestCode(),
                granted = granted,
                denied = denied
            )
        )
    }

    private fun sortResults(
        permissions: Array<out String>,
        grantResults: IntArray
    ): Pair<MutableList<String>, MutableList<String>> {
        val granted = mutableListOf<String>()
        val denied = mutableListOf<String>()

        permissions.forEachIndexed { index, permission ->
            if (grantResults[index] == PackageManager.PERMISSION_GRANTED) {
                granted.add(permission)
            } else {
                denied.add(permission)
            }
        }

        return granted to denied
    }
}

package com.bumble.appyx.core.integrationpoint.permissionrequester

interface PermissionRequestResultHandler {

    fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    )
}

package com.bumble.appyx.core.integrationpoint.permissionrequester

import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeBasedEventStream
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeBasedEventStream.RequestCodeBasedEvent
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeClient

interface PermissionRequester :
    RequestCodeBasedEventStream<PermissionRequester.RequestPermissionsEvent> {

    fun checkPermissions(client: RequestCodeClient, permissions: Array<String>) : CheckPermissionsResult

    fun requestPermissions(client: RequestCodeClient, requestCode: Int, permissions: Array<String>)

    data class CheckPermissionsResult(
        val granted: List<String>,
        val notGranted: List<String>,
        val shouldShowRationale: List<String>
    ) {
        val allGranted: Boolean =
            notGranted.isEmpty() && shouldShowRationale.isEmpty()
    }

    sealed class RequestPermissionsEvent : RequestCodeBasedEvent {
        data class Cancelled(
            override val requestCode: Int
        ) : RequestPermissionsEvent()

        data class RequestPermissionsResult(
            override val requestCode: Int,
            val granted: List<String>,
            val denied: List<String>
        ) : RequestPermissionsEvent()
    }
}

package com.bumble.appyx.core.integrationpoint.permissionrequester

import android.app.Activity
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment

interface PermissionRequesterHost {

    fun isGranted(permission: String): Boolean

    fun shouldShowRationale(permission: String): Boolean

    fun requestPermissions(requestCode: Int, permissions: Array<String>)

    class ActivityHost(private val activity: Activity) : PermissionRequesterHost {

        override fun isGranted(permission: String): Boolean =
            ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED

        override fun shouldShowRationale(permission: String): Boolean =
            ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)

        override fun requestPermissions(requestCode: Int, permissions: Array<String>) {
            ActivityCompat.requestPermissions(activity, permissions, requestCode)
        }

    }

    class FragmentHost(private val fragment: Fragment) : PermissionRequesterHost {

        override fun isGranted(permission: String): Boolean =
            ContextCompat.checkSelfPermission(fragment.requireContext(), permission) == PackageManager.PERMISSION_GRANTED

        override fun shouldShowRationale(permission: String): Boolean =
            fragment.shouldShowRequestPermissionRationale(permission)

        override fun requestPermissions(requestCode: Int, permissions: Array<String>) {
            fragment.requestPermissions(permissions, requestCode)
        }

    }

}

package com.bumble.appyx.core.integrationpoint.requestcode

import com.bumble.appyx.core.minimal.reactive.Source

interface RequestCodeBasedEventStream<T : RequestCodeBasedEventStream.RequestCodeBasedEvent> {

    fun events(client: RequestCodeClient): Source<T>

    interface RequestCodeBasedEvent {
        val requestCode: Int
    }
}

package com.bumble.appyx.core.integrationpoint.requestcode

import android.util.Log
import com.bumble.appyx.core.minimal.reactive.Relay
import com.bumble.appyx.core.minimal.reactive.Source
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeBasedEventStream.*

abstract class RequestCodeBasedEventStreamImpl<T : RequestCodeBasedEvent>(
    private val requestCodeRegistry: RequestCodeRegistry
) : RequestCodeBasedEventStream<T> {
    private val events = HashMap<Int, Relay<T>>()

    override fun events(client: RequestCodeClient): Source<T> {
        val id = requestCodeRegistry.generateGroupId(client.requestCodeClientId)
        ensureSubject(id)

        return events.getValue(id)
    }

    private fun ensureSubject(id: Int, onSubjectDidNotExist: (() -> Unit)? = null) {
        var subjectJustCreated = false

        if (!events.containsKey(id)) {
            events[id] = Relay()
            subjectJustCreated = true
        }

        if (subjectJustCreated) {
            onSubjectDidNotExist?.invoke()
        }
    }

    protected fun publish(externalRequestCode: Int, event: T) {
        val id = requestCodeRegistry.resolveGroupId(externalRequestCode)
        val internalRequestCode = externalRequestCode.toInternalRequestCode()

        ensureSubject(id) {
            Log.e( "RIBs", "There's no one listening for request code event! " +
                    "requestCode: $externalRequestCode, " +
                    "resolved group: $id, " +
                    "resolved code: $internalRequestCode, " +
                    "event: $event")
        }

        events.getValue(id).emit(event)
    }

    protected fun Int.toInternalRequestCode() =
        requestCodeRegistry.resolveRequestCode(this)

    protected fun RequestCodeClient.forgeExternalRequestCode(internalRequestCode: Int) =
        requestCodeRegistry.generateRequestCode(this.requestCodeClientId, internalRequestCode)
}

package com.bumble.appyx.core.integrationpoint.requestcode

interface RequestCodeClient {

    val requestCodeClientId: String
}

package com.bumble.appyx.core.integrationpoint.requestcode

import java.lang.RuntimeException

class RequestCodeDoesntFitInMask(override val message: String?) : RuntimeException(message)

package com.bumble.appyx.core.integrationpoint.requestcode

import android.os.Bundle
import java.util.*
import kotlin.math.pow

/**
 * Provides request code generation and lookup.
 *
 * Clients can use this to create a request code unique to the RequestCodeRegistry
 * instance (i.e. the whole app if the same instance is reused) while providing only:
 *  - locally unique request code
 *  - globally unique string identifier
 *
 * The idea is that you shouldn't have to come up with "random" numbers for request codes in your
 * components. Instead, use sensible numbers starting from 1, only unique locally to your component,
 * and let this class do the rest to ensure global uniqueness.
 */
class RequestCodeRegistry constructor(
    initialState: Bundle?,
    private val nbLowerBitsForIds: Int = 4
) {
    internal val requestCodes: HashMap<Int, String> =
        (initialState?.getSerializable(KEY_REQUEST_CODE_REGISTRY) as? HashMap<Int, String>) ?: hashMapOf()

    private val lowerBitsShift: Int = nbLowerBitsForIds - 0
    private val maskLowerBits = (1 shl lowerBitsShift) - 1
    private val maskHigherBits = 0x0000FFFF - maskLowerBits

    init {
        if (nbLowerBitsForIds < 1) throw IllegalArgumentException("nbLowerBitsForIds can't be less than 1")
        if (nbLowerBitsForIds > 4) throw IllegalArgumentException("nbLowerBitsForIds can't be larger than 4")
    }

    fun generateGroupId(groupName: String): Int {
        var code = generateInitialCode(groupName)

        while (codeCollisionWithAnotherGroup(code, groupName)) {
            code += (1 shl lowerBitsShift) and 0x0000FFFF
        }

        requestCodes[code] = groupName

        return code
    }

    internal fun generateInitialCode(groupName: String) =
        (groupName.hashCode() shl lowerBitsShift) and 0x0000FFFF

    private fun codeCollisionWithAnotherGroup(code: Int, groupName: String) =
        requestCodes.containsKey(code) && requestCodes[code] != groupName

    fun generateRequestCode(groupName: String, code: Int): Int {
        ensureCodeIsCorrect(code)
        return generateGroupId(groupName) + (code and maskLowerBits)
    }

    private fun ensureCodeIsCorrect(code: Int) {
        if (code < 1 || code != code and maskLowerBits) {
            throw RequestCodeDoesntFitInMask(
                "Requestcode '$code' does not fit requirements. Allowed min: 1, max: ${2.0.pow(nbLowerBitsForIds).toInt() - 1}"
            )
        }
    }

    fun resolveGroupId(code: Int): Int =
        code and maskHigherBits

    fun resolveRequestCode(code: Int): Int =
        code and maskLowerBits

    fun onSaveInstanceState(outState: Bundle) {
        outState.putSerializable(KEY_REQUEST_CODE_REGISTRY, HashMap(requestCodes))
    }

    companion object {
        internal const val KEY_REQUEST_CODE_REGISTRY = "requestCodeRegistry"
    }
}

package com.bumble.appyx.core.minimal.reactive

interface Cancellable {
    fun cancel()

    companion object {
        fun cancellableOf(f: () -> Unit): Cancellable =
            object : Cancellable {
                override fun cancel() {
                    f()
                }
            }
    }

    object Empty : Cancellable {
        override fun cancel() { }
    }
}

package com.bumble.appyx.core.minimal.reactive

interface Emitter<in T> {
    fun emit(value: T)
}

package com.bumble.appyx.core.minimal.reactive

import com.bumble.appyx.core.minimal.reactive.Cancellable.Companion.cancellableOf

open class Relay<T> : Source<T>, Emitter<T> {
    // keep list read-only to avoid modification during iteration
    private var listeners: List<(T) -> Unit> = emptyList()

    fun accept(value: T) {
        emit(value)
    }

    override fun emit(value: T) {
        listeners.forEach { it.invoke(value) }
    }

    override fun observe(callback: (T) -> Unit): Cancellable {
        listeners = listeners + callback
        return cancellableOf {
            listeners = listeners - callback
        }
    }
}

package com.bumble.appyx.core.minimal.reactive

interface Source<out T> {
    fun observe(callback: (T) -> Unit): Cancellable
}

package com.bumble.appyx.core.routing.backpresshandlerstrategies

import com.bumble.appyx.core.plugin.BackPressHandler
import com.bumble.appyx.core.routing.BaseRoutingSource
import com.bumble.appyx.core.routing.RoutingSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow

interface BackPressHandlerStrategy<Routing, State>
    : BackPressHandler {

    fun init(routingSource: BaseRoutingSource<Routing, State>, scope: CoroutineScope)

    val canHandleBackPress: StateFlow<Boolean>

}

package com.bumble.appyx.core.routing.backpresshandlerstrategies

import com.bumble.appyx.core.routing.BaseRoutingSource
import com.bumble.appyx.core.routing.RoutingSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn

abstract class BaseBackPressHandlerStrategy<Routing, TransitionState>
    : BackPressHandlerStrategy<Routing, TransitionState> {

    protected lateinit var scope: CoroutineScope
    protected lateinit var routingSource: BaseRoutingSource<Routing, TransitionState>

    override fun init(
        routingSource: BaseRoutingSource<Routing, TransitionState>,
        scope: CoroutineScope
    ) {
        this.scope = scope
        this.routingSource = routingSource
    }

    protected abstract val canHandleBackPressFlow: Flow<Boolean>

    override val canHandleBackPress: StateFlow<Boolean> by lazy(LazyThreadSafetyMode.NONE) {
        canHandleBackPressFlow.stateIn(scope, SharingStarted.Eagerly, false)
    }
}

package com.bumble.appyx.core.routing.backpresshandlerstrategies

import com.bumble.appyx.core.routing.RoutingSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

class DontHandleBackPress<Routing, TransitionState> :
    BaseBackPressHandlerStrategy<Routing, TransitionState>() {

    override val canHandleBackPressFlow: Flow<Boolean> =
        flowOf(false)

    override fun onBackPressed() {
        // Noop
    }
}

package com.bumble.appyx.core.routing.onscreen

class AlwaysOnScreenResolver<State> : OnScreenStateResolver<State> {

    override fun isOnScreen(state: State): Boolean = true
}

package com.bumble.appyx.core.routing.onscreen

interface OnScreenStateResolver<State> {

    fun isOnScreen(state: State): Boolean
}

package com.bumble.appyx.core.routing.onscreen

import com.bumble.appyx.core.routing.RoutingElement

fun <State> OnScreenStateResolver<State>.isOnScreen(element: RoutingElement<*, out State>): Boolean =
    if (element.transitionHistory.isEmpty()) {
        isOnScreen(element.fromState) || isOnScreen(element.targetState)
    } else {
        element.transitionHistory.any { (fromState, targetState) ->
            isOnScreen(fromState) || isOnScreen(targetState)
        }
    }

package com.bumble.appyx.core.routing.operationstrategies

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.core.routing.RoutingSource
import kotlinx.coroutines.CoroutineScope

abstract class BaseOperationStrategy<Routing, State> : OperationStrategy<Routing, State> {

    protected lateinit var scope: CoroutineScope
    protected lateinit var routingSource: RoutingSource<Routing, State>
    protected lateinit var executeOperation: (operation: Operation<Routing, State>) -> Unit

    override fun init(
        routingSource: RoutingSource<Routing, State>,
        scope: CoroutineScope,
        executeOperation: (operation: Operation<Routing, State>) -> Unit
    ) {
        this.scope = scope
        this.routingSource = routingSource
        this.executeOperation = executeOperation
    }
}

package com.bumble.appyx.core.routing.operationstrategies

import com.bumble.appyx.core.routing.Operation

class ExecuteImmediately<Routing, State> : BaseOperationStrategy<Routing, State>() {

    override fun accept(operation: Operation<Routing, State>) {
        executeOperation(operation)
    }
}

package com.bumble.appyx.core.routing.operationstrategies

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.core.routing.isTransitioning

class FinishTransitionsOnNewOperation<Routing, State> : BaseOperationStrategy<Routing, State>() {

    override fun accept(operation: Operation<Routing, State>) {
        finishUnfinishedTransitions()
        executeOperation(operation)
    }

    private fun finishUnfinishedTransitions() {
        routingSource
            .elements
            .value
            .mapNotNull { routingElement ->
                if (routingElement.isTransitioning) routingElement.key else null
            }
            .also { if (it.isNotEmpty()) routingSource.onTransitionFinished(it) }
    }
}

package com.bumble.appyx.core.routing.operationstrategies

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.core.routing.isTransitioning

class IgnoreIfThereAreUnfinishedTransitions<Routing, State> : BaseOperationStrategy<Routing, State>() {

    override fun accept(operation: Operation<Routing, State>) {
        if (hasNoUnfinishedTransitions()) {
            executeOperation(operation)
        }
    }

    private fun hasNoUnfinishedTransitions(): Boolean =
        routingSource.elements.value
            .none { it.isTransitioning }
}

package com.bumble.appyx.core.routing.operationstrategies

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.core.routing.RoutingSource
import kotlinx.coroutines.CoroutineScope

interface OperationStrategy<Routing, State> {

    fun init(
        routingSource: RoutingSource<Routing, State>,
        scope: CoroutineScope,
        executeOperation: (operation: Operation<Routing, State>) -> Unit
    )

    fun accept(operation: Operation<Routing, State>)
}

package com.bumble.appyx.core.routing.operationstrategies

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.isTransitioning
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.util.*

class QueueOperations<Routing, State> : BaseOperationStrategy<Routing, State>() {

    private val operationQueue = LinkedList<Operation<Routing, State>>()
    private lateinit var collectJob: Job

    override fun accept(operation: Operation<Routing, State>) {
        collectElementsIfRequired()

        if (hasUnfinishedOperation()) {
            operationQueue.addFirst(operation)
        } else {
            executeOperation(operation)
        }
    }

    private fun collectElementsIfRequired() {
        if (::collectJob.isInitialized.not()) {
            collectJob = scope.launch {
                routingSource
                    .elements
                    .collect(::addToQueueIfTransitionInProgress)
            }
        }
    }

    private fun addToQueueIfTransitionInProgress(transitionList: RoutingElements<Routing, out State>) {
        if (transitionList.none { it.isTransitioning } && operationQueue.isNotEmpty()) {
            val operation = operationQueue.removeLast()
            executeOperation(operation)
        }
    }

    private fun hasUnfinishedOperation(): Boolean {
        return routingSource.elements.value.any { it.targetState != it.fromState }
    }
}

package com.bumble.appyx.core.routing.transition

import androidx.compose.animation.core.Transition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier

class CombinedHandler<T, S>(
    private val handlers: List<ModifierTransitionHandler<T, S>>
) : ModifierTransitionHandler<T, S>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<S>,
        descriptor: TransitionDescriptor<T, S>
    ): Modifier =
        handlers
            .map { it.createModifier(Modifier, transition, descriptor = descriptor) }
            .fold(modifier) { acc: Modifier, modifier: Modifier ->
                acc.then(modifier)
            }
}

@Composable
fun <T, S> rememberCombinedHandler(handlers: List<ModifierTransitionHandler<T, S>>): ModifierTransitionHandler<T, S> =
    remember { CombinedHandler(handlers = handlers) }

package com.bumble.appyx.core.routing.transition

import androidx.compose.animation.core.Transition
import androidx.compose.ui.Modifier

@Suppress("TransitionPropertiesLabel")
class JumpToEndTransitionHandler<T, S> : ModifierTransitionHandler<T, S>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<S>,
        descriptor: TransitionDescriptor<T, S>
    ): Modifier = modifier
}

package com.bumble.appyx.core.routing.transition

import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.updateTransition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import com.bumble.appyx.core.composable.ChildTransitionScope
import com.bumble.appyx.core.composable.ChildTransitionScopeImpl

abstract class ModifierTransitionHandler<T, S>(open val clipToBounds: Boolean = false) :
    TransitionHandler<T, S> {

    private val clipToBoundsModifier: Modifier by lazy(LazyThreadSafetyMode.NONE) {
        if (clipToBounds) {
            Modifier.clipToBounds()
        } else {
            Modifier
        }
    }

    @Composable
    override fun handle(
        descriptor: TransitionDescriptor<T, S>,
        onTransitionFinished: (S) -> Unit
    ): ChildTransitionScope<S> {
        val transitionState = remember { MutableTransitionState(descriptor.fromState) }
        transitionState.targetState = descriptor.toState
        val transition: Transition<S> = updateTransition(transitionState)

        with(transition) {
            LaunchedEffect(currentState, targetState) {
                if (currentState == targetState) {
                    onTransitionFinished(targetState)
                }
            }
        }
        return rememberTransitionScope(transition, descriptor)
    }

    abstract fun createModifier(
        modifier: Modifier,
        transition: Transition<S>,
        descriptor: TransitionDescriptor<T, S>
    ): Modifier

    @Composable
    private fun rememberTransitionScope(
        transition: Transition<S>,
        descriptor: TransitionDescriptor<T, S>
    ) = remember(transition, descriptor) {
        ChildTransitionScopeImpl(
            transition = transition,
            transitionModifier = clipToBoundsModifier
                .then(
                    createModifier(
                        clipToBoundsModifier,
                        transition = transition,
                        descriptor = descriptor
                    )
                )
        )
    }
}

package com.bumble.appyx.core.routing.transition

import androidx.compose.runtime.Immutable
import androidx.compose.ui.unit.Dp

@Immutable
data class TransitionBounds(val width: Dp, val height: Dp)

package com.bumble.appyx.core.routing.transition

import androidx.compose.runtime.Immutable
import com.bumble.appyx.core.routing.Operation

@Immutable
data class TransitionDescriptor<T, out S>(
    val params: TransitionParams,
    val operation: Operation<T, out S>,
    val element: T,
    val fromState: S,
    val toState: S
)

package com.bumble.appyx.core.routing.transition

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import com.bumble.appyx.core.composable.ChildTransitionScope

@Stable
interface TransitionHandler<T, S> {

    @Composable
    fun handle(
        descriptor: TransitionDescriptor<T, S>,
        onTransitionFinished: (S) -> Unit
    ): ChildTransitionScope<S>
}

package com.bumble.appyx.core.routing.transition

import androidx.compose.runtime.Immutable

@Immutable
data class TransitionParams(val bounds: TransitionBounds)

package com.bumble.appyx.core.routing.transition

import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Transition
import androidx.compose.runtime.Composable

typealias TransitionSpec<S, T> = @Composable Transition.Segment<S>.() -> FiniteAnimationSpec<T>

package com.bumble.appyx.core.routing.upnavigation

fun interface UpNavigationHandler {
    fun handleUpNavigation()
}

package com.bumble.appyx.routingsource.backstack.backpresshandler

import com.bumble.appyx.core.routing.backpresshandlerstrategies.BaseBackPressHandlerStrategy
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.BackStackElements
import com.bumble.appyx.routingsource.backstack.operation.Pop
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class PopBackPressHandler<Routing : Any> :
    BaseBackPressHandlerStrategy<Routing, BackStack.TransitionState>() {

    override val canHandleBackPressFlow: Flow<Boolean> by lazy {
        routingSource.elements.map(::areThereStashedElements)
    }

    private fun areThereStashedElements(elements: BackStackElements<Routing>) =
        elements.any { it.targetState == BackStack.TransitionState.STASHED_IN_BACK_STACK }

    override fun onBackPressed() {
        routingSource.accept(Pop())
    }
}

package com.bumble.appyx.routingsource.backstack.operation

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.routingsource.backstack.BackStack

sealed interface BackStackOperation<T> : Operation<T, BackStack.TransitionState>

package com.bumble.appyx.routingsource.backstack.operation

import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.BackStackElement
import com.bumble.appyx.routingsource.backstack.BackStackElements
import com.bumble.appyx.routingsource.backstack.active
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.CREATED
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

/**
 * Operation:
 *
 * [A, B, C] + NewRoot(D) = [ D ]
 */
@Parcelize
data class NewRoot<T : Any>(
    private val element: @RawValue T
) : BackStackOperation<T> {

    override fun isApplicable(elements: BackStackElements<T>): Boolean = true

    override fun invoke(
        elements: BackStackElements<T>,
    ): BackStackElements<T> {

        val current = elements.active
        requireNotNull(current) { "No previous elements, state=$elements" }

        return if (current.key.routing == element) {
            listOf(current)
        } else {
            listOf(
                current.transitionTo(
                    targetState = BackStack.TransitionState.DESTROYED,
                    operation = this
                ),
                BackStackElement(
                    key = RoutingKey(element),
                    fromState = CREATED,
                    targetState = ACTIVE,
                    operation = this
                )
            )
        }
    }
}

fun <T : Any> BackStack<T>.newRoot(element: T) {
    accept(NewRoot(element))
}

package com.bumble.appyx.routingsource.backstack.operation

import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.BackStackElements
import com.bumble.appyx.routingsource.backstack.activeIndex
import kotlinx.parcelize.Parcelize

/**
 * Operation:
 *
 * [A, B, C] + Pop = [A, B]
 */
@Parcelize
class Pop<T : Any> : BackStackOperation<T> {

    override fun isApplicable(elements: BackStackElements<T>): Boolean =
        elements.any { it.targetState == BackStack.TransitionState.ACTIVE } &&
                elements.any { it.targetState == BackStack.TransitionState.STASHED_IN_BACK_STACK }

    override fun invoke(
        elements: BackStackElements<T>
    ): BackStackElements<T> {

        val destroyIndex = elements.activeIndex
        val unStashIndex =
            elements.indexOfLast { it.targetState == BackStack.TransitionState.STASHED_IN_BACK_STACK }
        require(destroyIndex != -1) { "Nothing to destroy, state=$elements" }
        require(unStashIndex != -1) { "Nothing to remove from stash, state=$elements" }
        return elements.mapIndexed { index, element ->
            when (index) {
                destroyIndex -> element.transitionTo(
                    targetState = BackStack.TransitionState.DESTROYED,
                    operation = this
                )
                unStashIndex -> element.transitionTo(
                    targetState = BackStack.TransitionState.ACTIVE,
                    operation = this
                )
                else -> element
            }
        }
    }

    override fun equals(other: Any?): Boolean = this.javaClass == other?.javaClass

    override fun hashCode(): Int = this.javaClass.hashCode()
}

fun <T : Any> BackStack<T>.pop() {
    accept(Pop())
}

package com.bumble.appyx.routingsource.backstack.operation

import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.BackStackElement
import com.bumble.appyx.routingsource.backstack.BackStackElements
import com.bumble.appyx.routingsource.backstack.activeRouting
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.CREATED
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

/**
 * Operation:
 *
 * [A, B, C] + Push(D) = [A, B, C, D]
 */
@Parcelize
data class Push<T : Any>(
    private val element: @RawValue T
) : BackStackOperation<T> {

    override fun isApplicable(elements: BackStackElements<T>): Boolean =
        element != elements.activeRouting

    override fun invoke(elements: BackStackElements<T>): BackStackElements<T> {
        return elements.map {
            if (it.targetState == BackStack.TransitionState.ACTIVE) {
                it.transitionTo(
                    targetState = BackStack.TransitionState.STASHED_IN_BACK_STACK,
                    operation = this
                )
            } else {
                it
            }
        } + BackStackElement(
            key = RoutingKey(element),
            fromState = CREATED,
            targetState = ACTIVE,
            operation = this
        )
    }
}

fun <T : Any> BackStack<T>.push(element: T) {
    accept(Push(element))
}

package com.bumble.appyx.routingsource.backstack.operation

import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.BackStackElements
import com.bumble.appyx.routingsource.backstack.activeIndex
import kotlinx.parcelize.Parcelize

/**
 * Operation:
 *
 * [A, B, C] + Remove(id of B) = [A, C]
 */
@Parcelize
data class Remove<T : Any>(
    private val key: RoutingKey<T>
) : BackStackOperation<T> {

    override fun isApplicable(elements: BackStackElements<T>) =
        elements.hasContentWithKey()

    override fun invoke(
        elements: BackStackElements<T>
    ): BackStackElements<T> =
        when {
            elements.hasContentWithKey() -> updateContent(elements)
            else -> elements
        }

    private fun BackStackElements<T>.hasContentWithKey() =
        find { it.key == key && it.targetState != BackStack.TransitionState.DESTROYED } != null

    private fun updateContent(elements: BackStackElements<T>): BackStackElements<T> {
        val toRemove =
            elements.find { it.key == key && it.targetState != BackStack.TransitionState.DESTROYED }

        requireNotNull(toRemove)
        val toRemoveIndex = elements.indexOf(toRemove)

        return if (toRemoveIndex == elements.activeIndex) {
            val unStashIndex =
                elements.indexOfLast { it.targetState == BackStack.TransitionState.STASHED_IN_BACK_STACK }
            require(unStashIndex != -1) { "Nothing to remove from stash, state=$elements" }

            elements.mapIndexed { index, element ->
                when (index) {
                    toRemoveIndex -> element.transitionTo(
                        targetState = BackStack.TransitionState.DESTROYED,
                        operation = this
                    )
                    unStashIndex -> element.transitionTo(
                        targetState = BackStack.TransitionState.ACTIVE,
                        operation = this
                    )
                    else -> element
                }
            }
        } else {
            elements.minus(toRemove)
        }
    }
}

fun <T : Any> BackStack<T>.remove(key: RoutingKey<T>) {
    accept(Remove(key))
}

package com.bumble.appyx.routingsource.backstack.operation

import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.BackStackElement
import com.bumble.appyx.routingsource.backstack.BackStackElements
import com.bumble.appyx.routingsource.backstack.activeIndex
import com.bumble.appyx.routingsource.backstack.activeRouting
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.CREATED
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

/**
 * Operation:
 *
 * [A, B, C] + Replace(D) = [A, B, D]
 */
@Parcelize
data class Replace<T : Any>(
    private val element: @RawValue T
) : BackStackOperation<T> {

    override fun isApplicable(elements: BackStackElements<T>): Boolean =
        element != elements.activeRouting

    override fun invoke(
        elements: BackStackElements<T>
    ): BackStackElements<T> {
        require(elements.any { it.targetState == BackStack.TransitionState.ACTIVE }) { "No element to be replaced, state=$elements" }

        return elements.mapIndexed { index, element ->
            if (index == elements.activeIndex) {
                element.transitionTo(
                    targetState = BackStack.TransitionState.DESTROYED,
                    operation = this
                )
            } else {
                element
            }
        } + BackStackElement(
            key = RoutingKey(element),
            fromState = CREATED,
            targetState = ACTIVE,
            operation = this,
        )
    }
}

fun <T : Any> BackStack<T>.replace(element: T) {
    accept(Replace(element))
}

package com.bumble.appyx.routingsource.backstack.operation

import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.BackStackElement
import com.bumble.appyx.routingsource.backstack.BackStackElements
import com.bumble.appyx.routingsource.backstack.active
import com.bumble.appyx.routingsource.backstack.activeRouting
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.backstack.BackStack.TransitionState.CREATED
import com.bumble.appyx.routingsource.backstack.operation.SingleTop.Companion
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

/**
 * Operation:
 *
 * [A, B, C, D] + SingleTop(B) = [A, B]          // of same type and equals, acts as n * Pop
 * [A, B, C, D] + SingleTop(B') = [A, B']        // of same type but not equals, acts as n * Pop + Replace
 * [A, B, C, D] + SingleTop(E) = [A, B, C, D, E] // not found, acts as Push
 */
sealed class SingleTop<T : Any> : BackStackOperation<T> {

    @Parcelize
    class SingleTopReactivateBackStackOperation<T : Any>(
        private val element: @RawValue T,
        private val position: Int
    ) : SingleTop<T>() {

        override fun isApplicable(elements: BackStackElements<T>): Boolean =
            element != elements.activeRouting

        override fun invoke(
            elements: BackStackElements<T>
        ): BackStackElements<T> {
            val current = elements.active
            requireNotNull(current)

            val newElements = elements.dropLast(elements.size - position - 1)

            return newElements.mapIndexed { index, element ->
                if (index == newElements.lastIndex) {
                    element.transitionTo(
                        targetState = BackStack.TransitionState.ACTIVE,
                        operation = this
                    )
                } else {
                    element
                }
            } + current.transitionTo(
                targetState = BackStack.TransitionState.DESTROYED,
                operation = this
            )
        }

        override fun equals(other: Any?): Boolean = this.javaClass == other?.javaClass

        override fun hashCode(): Int = this.javaClass.hashCode()
    }

    @Parcelize
    class SingleTopReplaceBackStackOperation<T : Any>(
        private val element: @RawValue T,
        private val position: Int,
    ) : SingleTop<T>() {

        override fun isApplicable(elements: BackStackElements<T>): Boolean = true

        override fun invoke(
            elements: BackStackElements<T>
        ): BackStackElements<T> {
            val current = elements.active
            requireNotNull(current)

            val newElements = elements.dropLast(elements.size - position)

            return newElements + current.transitionTo(
                targetState = BackStack.TransitionState.DESTROYED,
                operation = this
            ) + BackStackElement(
                key = RoutingKey(element),
                fromState = CREATED,
                targetState = ACTIVE,
                operation = this,
            )
        }

        override fun equals(other: Any?): Boolean = this.javaClass == other?.javaClass

        override fun hashCode(): Int = this.javaClass.hashCode()
    }

    companion object {

        fun <T : Any> init(
            element: T,
            elements: BackStackElements<T>
        ): BackStackOperation<T> {
            val targetClass = element.javaClass
            val lastIndexOfSameClass =
                elements.indexOfLast { targetClass.isInstance(it.key.routing) }

            return if (lastIndexOfSameClass == -1) {
                Push(element)
            } else {
                if (elements[lastIndexOfSameClass].key.routing == element) {
                    SingleTopReactivateBackStackOperation(
                        element,
                        lastIndexOfSameClass
                    )
                } else {
                    SingleTopReplaceBackStackOperation(
                        element,
                        lastIndexOfSameClass
                    )
                }
            }
        }
    }
}

fun <T : Any> BackStack<T>.singleTop(element: T) {
    accept(SingleTop.init(element, elements.value))
}

package com.bumble.appyx.routingsource.backstack.transitionhandler

import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor
import com.bumble.appyx.core.routing.transition.TransitionSpec

@Suppress("TransitionPropertiesLabel")
class BackStackFader<T>(
    private val transitionSpec: TransitionSpec<BackStack.TransitionState, Float> = { spring() }
) : ModifierTransitionHandler<T, BackStack.TransitionState>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<BackStack.TransitionState>,
        descriptor: TransitionDescriptor<T, BackStack.TransitionState>
    ): Modifier = modifier.composed {
        val alpha = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    BackStack.TransitionState.ACTIVE -> 1f
                    else -> 0f
                }
            })

        alpha(alpha.value)
    }
}

@Composable
fun <T> rememberBackstackFader(
    transitionSpec: TransitionSpec<BackStack.TransitionState, Float> = { spring() }
): ModifierTransitionHandler<T, BackStack.TransitionState> = remember {
    BackStackFader(transitionSpec = transitionSpec)
}

package com.bumble.appyx.routingsource.backstack.transitionhandler

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateOffset
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.operation.BackStackOperation
import com.bumble.appyx.routingsource.backstack.operation.NewRoot
import com.bumble.appyx.routingsource.backstack.operation.Pop
import com.bumble.appyx.routingsource.backstack.operation.Push
import com.bumble.appyx.routingsource.backstack.operation.Remove
import com.bumble.appyx.routingsource.backstack.operation.Replace
import com.bumble.appyx.routingsource.backstack.operation.SingleTop.SingleTopReactivateBackStackOperation
import com.bumble.appyx.routingsource.backstack.operation.SingleTop.SingleTopReplaceBackStackOperation
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor
import com.bumble.appyx.core.routing.transition.TransitionSpec
import kotlin.math.roundToInt

@Suppress("TransitionPropertiesLabel")
class BackStackSlider<T>(
    private val transitionSpec: TransitionSpec<BackStack.TransitionState, Offset> = {
        spring(stiffness = Spring.StiffnessVeryLow)
    },
    override val clipToBounds: Boolean = false
) : ModifierTransitionHandler<T, BackStack.TransitionState>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<BackStack.TransitionState>,
        descriptor: TransitionDescriptor<T, BackStack.TransitionState>
    ): Modifier = modifier.composed {
        val offset = transition.animateOffset(
            transitionSpec = transitionSpec,
            targetValueByState = {
                val width = descriptor.params.bounds.width.value
                when (it) {
                    BackStack.TransitionState.CREATED -> toOutsideRight(width)
                    BackStack.TransitionState.ACTIVE -> toCenter()
                    BackStack.TransitionState.STASHED_IN_BACK_STACK -> toOutsideLeft(width)
                    BackStack.TransitionState.DESTROYED -> {
                        when (val operation = descriptor.operation as? BackStackOperation) {
                            is Push,
                            is Pop,
                            is Remove,
                            is SingleTopReactivateBackStackOperation -> toOutsideRight(width)
                            is Replace,
                            is NewRoot,
                            is SingleTopReplaceBackStackOperation -> toOutsideLeft(width)
                            null -> error("Unexpected operation: $operation")
                        }
                    }
                }
            })

        offset {
            IntOffset(
                x = (offset.value.x * this.density).roundToInt(),
                y = (offset.value.y * this.density).roundToInt()
            )
        }
    }

    private fun toOutsideRight(width: Float) = Offset(1.0f * width, 0f)

    private fun toOutsideLeft(width: Float) = Offset(-1.0f * width, 0f)

    private fun toCenter() = Offset(0f, 0f)
}

@Composable
fun <T> rememberBackstackSlider(
    transitionSpec: TransitionSpec<BackStack.TransitionState, Offset> = { spring(stiffness = Spring.StiffnessVeryLow) },
    clipToBounds: Boolean = false
): ModifierTransitionHandler<T, BackStack.TransitionState> = remember {
    BackStackSlider(transitionSpec = transitionSpec, clipToBounds = clipToBounds)
}

package com.bumble.appyx.routingsource.spotlight.backpresshandler

import com.bumble.appyx.core.routing.backpresshandlerstrategies.BaseBackPressHandlerStrategy
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.spotlight.SpotlightElements
import com.bumble.appyx.routingsource.spotlight.operation.Activate
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class GoToDefault<Routing : Any>(
    private val defaultElementIndex: Int = 0
) : BaseBackPressHandlerStrategy<Routing, Spotlight.TransitionState>() {

    override val canHandleBackPressFlow: Flow<Boolean> by lazy {
        routingSource.elements.map(::defaultElementIsNotActive)
    }

    override fun onBackPressed() {
        routingSource.accept(Activate(defaultElementIndex))
    }

    private fun defaultElementIsNotActive(elements: SpotlightElements<Routing>) =
        elements.getOrNull(defaultElementIndex)?.targetState != ACTIVE
}

package com.bumble.appyx.routingsource.spotlight.backpresshandler

import com.bumble.appyx.core.routing.backpresshandlerstrategies.BaseBackPressHandlerStrategy
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.SpotlightElements
import com.bumble.appyx.routingsource.spotlight.operation.Previous
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class GoToPrevious<Routing : Any> :
    BaseBackPressHandlerStrategy<Routing, Spotlight.TransitionState>() {

    override val canHandleBackPressFlow: Flow<Boolean> by lazy {
        routingSource.elements.map(::areTherePreviousElements)
    }

    private fun areTherePreviousElements(elements: SpotlightElements<Routing>) =
        elements.any { it.targetState == Spotlight.TransitionState.INACTIVE_BEFORE }

    override fun onBackPressed() {
        routingSource.accept(Previous())
    }
}

package com.bumble.appyx.routingsource.spotlight.backpresshandler

import com.bumble.appyx.core.routing.backpresshandlerstrategies.BaseBackPressHandlerStrategy
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.spotlight.SpotlightElements
import com.bumble.appyx.routingsource.spotlight.operation.Activate
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class UndoHistory<Routing : Any>(
    private val historyLimit: Int = 10
) : BaseBackPressHandlerStrategy<Routing, Spotlight.TransitionState>() {

    private val history = ArrayDeque<Int>()

    override val canHandleBackPressFlow: Flow<Boolean> by lazy {
        routingSource.elements.map { elements ->
            elements.addToHistory()
            historyHasElements()
        }
    }

    override fun onBackPressed() {
        history.removeLast()
        routingSource.accept(Activate(history.last()))
    }

    private fun historyHasElements() =
        history.size > 1

    private fun SpotlightElements<Routing>.addToHistory() {
        val newIndex = indexOfFirst { it.targetState == ACTIVE }
        if (newIndex != history.lastOrNull()) {
            history.addLast(newIndex)
            adjustToHistoryLimit()
        }
    }

    private fun adjustToHistoryLimit() {
        if (history.size > historyLimit) history.removeFirst()
    }
}

package com.bumble.appyx.routingsource.spotlight.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState
import com.bumble.appyx.routingsource.spotlight.currentIndex
import kotlinx.parcelize.Parcelize

@Parcelize
class Activate<T : Any>(
    private val index: Int
) : SpotlightOperation<T> {

    override fun isApplicable(elements: RoutingElements<T, TransitionState>) =
        index != elements.currentIndex && index <= elements.lastIndex && index >= 0

    override fun invoke(elements: RoutingElements<T, TransitionState>): RoutingElements<T, TransitionState> {

        val toActivateIndex = this.index
        return elements.mapIndexed { index, element ->
            when {
                index < toActivateIndex -> {
                    element.transitionTo(
                        targetState = TransitionState.INACTIVE_BEFORE,
                        operation = this
                    )
                }
                index == toActivateIndex -> {
                    element.transitionTo(
                        targetState = TransitionState.ACTIVE,
                        operation = this
                    )
                }
                else -> {
                    element.transitionTo(
                        targetState = TransitionState.INACTIVE_AFTER,
                        operation = this
                    )
                }
            }
        }
    }
}

fun <T : Any> Spotlight<T>.activate(index: Int) {
    accept(Activate(index))
}

package com.bumble.appyx.routingsource.spotlight.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.INACTIVE_AFTER
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.INACTIVE_BEFORE
import kotlinx.parcelize.Parcelize

@Parcelize
class Next<T : Any> : SpotlightOperation<T> {

    override fun isApplicable(elements: RoutingElements<T, TransitionState>) =
        elements.any { it.fromState == INACTIVE_AFTER }

    override fun invoke(elements: RoutingElements<T, TransitionState>): RoutingElements<T, TransitionState> {
        val nextKey =
            elements.first { it.targetState == INACTIVE_AFTER }.key

        return elements.map {
            when {
                it.targetState == ACTIVE -> {
                    it.transitionTo(
                        targetState = INACTIVE_BEFORE,
                        operation = this
                    )
                }
                it.key == nextKey -> {
                    it.transitionTo(
                        targetState = ACTIVE,
                        operation = this
                    )
                }
                else -> {
                    it
                }
            }
        }
    }
}

fun <T : Any> Spotlight<T>.next() {
    accept(Next())
}

package com.bumble.appyx.routingsource.spotlight.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.INACTIVE_AFTER
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.INACTIVE_BEFORE
import kotlinx.parcelize.Parcelize

@Parcelize
class Previous<T : Any> : SpotlightOperation<T> {

    override fun isApplicable(elements: RoutingElements<T, Spotlight.TransitionState>) =
        elements.any { it.fromState == INACTIVE_BEFORE }

    override fun invoke(elements: RoutingElements<T, Spotlight.TransitionState>): RoutingElements<T, Spotlight.TransitionState> {
        val previousKey =
            elements.last { it.targetState == INACTIVE_BEFORE }.key

        return elements.map {
            when {
                it.targetState == ACTIVE -> {
                    it.transitionTo(
                        targetState = INACTIVE_AFTER,
                        operation = this
                    )
                }
                it.key == previousKey -> {
                    it.transitionTo(
                        targetState = ACTIVE,
                        operation = this
                    )
                }
                else -> {
                    it
                }
            }
        }
    }
}

fun <T : Any> Spotlight<T>.previous() {
    accept(Previous())
}

package com.bumble.appyx.routingsource.spotlight.operation

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.routingsource.spotlight.Spotlight

typealias SpotlightOperation<T> = Operation<T, Spotlight.TransitionState>

package com.bumble.appyx.routingsource.spotlight.operation

import com.bumble.appyx.core.routing.Operation.Noop
import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.ACTIVE
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.INACTIVE_AFTER
import com.bumble.appyx.routingsource.spotlight.Spotlight.TransitionState.INACTIVE_BEFORE
import com.bumble.appyx.routingsource.spotlight.SpotlightElement
import com.bumble.appyx.routingsource.spotlight.SpotlightElements
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

@Parcelize
class UpdateElements<T : Any>(
    private val elements: @RawValue List<T>,
    private val initialActiveIndex: Int? = null,
) : SpotlightOperation<T> {

    override fun isApplicable(elements: RoutingElements<T, TransitionState>) = true

    override fun invoke(elements: RoutingElements<T, TransitionState>): RoutingElements<T, TransitionState> {
        if (initialActiveIndex != null) {
            require(initialActiveIndex in this.elements.indices) {
                "Initial active index $initialActiveIndex is out of bounds of provided list of items: ${this.elements.indices}"
            }
        }
        return if (initialActiveIndex == null) {
            val currentActiveElement = elements.find { it.targetState == ACTIVE }

            // if current routing exists in the new list of items and initialActiveIndex is null
            // then keep existing routing active
            if (this.elements.contains(currentActiveElement?.key?.routing)) {
                this.elements.toSpotlightElements(elements.indexOf(currentActiveElement))
            } else {
                // if current routing does not exist in the new list of items and initialActiveIndex is null
                // then set 0 as active index
                this.elements.toSpotlightElements(0)
            }
        } else {
            this.elements.toSpotlightElements(initialActiveIndex)
        }
    }
}

fun <T : Any> Spotlight<T>.updateElements(
    items: List<T>,
    initialActiveItem: Int? = null
) {
    accept(UpdateElements(items, initialActiveItem))
}

internal fun <T> List<T>.toSpotlightElements(activeIndex: Int): SpotlightElements<T> =
    mapIndexed { index, item ->
        val state = when {
            index < activeIndex -> INACTIVE_BEFORE
            index == activeIndex -> ACTIVE
            else -> INACTIVE_AFTER
        }
        SpotlightElement(
            key = RoutingKey(item),
            fromState = state,
            targetState = state,
            operation = Noop()
        )
    }

package com.bumble.appyx.routingsource.spotlight.transitionhandler

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateOffset
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Dp
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor
import com.bumble.appyx.core.routing.transition.TransitionSpec
import com.bumble.appyx.routingsource.spotlight.Spotlight

@Suppress("TransitionPropertiesLabel")
class SpotlightSlider<T>(
    private val transitionSpec: TransitionSpec<Spotlight.TransitionState, Offset> = {
        spring(stiffness = Spring.StiffnessVeryLow)
    },
    override val clipToBounds: Boolean = false
) : ModifierTransitionHandler<T, Spotlight.TransitionState>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<Spotlight.TransitionState>,
        descriptor: TransitionDescriptor<T, Spotlight.TransitionState>
    ): Modifier = modifier.composed {
        val offset = transition.animateOffset(
            transitionSpec = transitionSpec,
            targetValueByState = {
                val width = descriptor.params.bounds.width.value
                when (it) {
                    Spotlight.TransitionState.INACTIVE_BEFORE -> toOutsideLeft(width)
                    Spotlight.TransitionState.ACTIVE -> toCenter()
                    Spotlight.TransitionState.INACTIVE_AFTER -> toOutsideRight(width)
                }
            },
        )

        offset(Dp(offset.value.x), Dp(offset.value.y))
    }

    private fun toOutsideRight(width: Float) = Offset(1.0f * width, 0f)

    private fun toOutsideLeft(width: Float) = Offset(-1.0f * width, 0f)

    private fun toCenter() = Offset(0f, 0f)
}

@Composable
fun <T> rememberSpotlightSlider(
    transitionSpec: TransitionSpec<Spotlight.TransitionState, Offset> = { spring(stiffness = Spring.StiffnessVeryLow) },
    clipToBounds: Boolean = false
): ModifierTransitionHandler<T, Spotlight.TransitionState> = remember {
    SpotlightSlider(transitionSpec = transitionSpec, clipToBounds = clipToBounds)
}

package com.bumble.appyx.routingsource.spotlight.transitionhandler

import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor
import com.bumble.appyx.core.routing.transition.TransitionSpec
import com.bumble.appyx.routingsource.spotlight.Spotlight

@Suppress("TransitionPropertiesLabel")

class SpotlightFader<T>(
    private val transitionSpec: TransitionSpec<Spotlight.TransitionState, Float> = { spring() }
) : ModifierTransitionHandler<T, Spotlight.TransitionState>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<Spotlight.TransitionState>,
        descriptor: TransitionDescriptor<T, Spotlight.TransitionState>
    ): Modifier = modifier.composed {
        val alpha = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    Spotlight.TransitionState.ACTIVE -> 1f
                    else -> 0f
                }
            })

        alpha(alpha.value)
    }
}

@Composable
fun <T> rememberSpotlightFader(
    transitionSpec: TransitionSpec<Spotlight.TransitionState, Float> = { spring() }
): ModifierTransitionHandler<T, Spotlight.TransitionState> = remember {
    SpotlightFader(transitionSpec = transitionSpec)
}

package com.bumble.appyx.routingsource.modal.backpresshandler

import com.bumble.appyx.core.routing.backpresshandlerstrategies.BaseBackPressHandlerStrategy
import com.bumble.appyx.routingsource.modal.Modal.TransitionState
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.FULL_SCREEN
import com.bumble.appyx.routingsource.modal.ModalElements
import com.bumble.appyx.routingsource.modal.operation.Revert
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class RevertBackPressHandler<Routing : Any> :
    BaseBackPressHandlerStrategy<Routing, TransitionState>() {

    override val canHandleBackPressFlow: Flow<Boolean> by lazy {
        routingSource.elements.map(::areThereFullScreenElements)
    }

    private fun areThereFullScreenElements(elements: ModalElements<Routing>) =
        elements.any { it.targetState == FULL_SCREEN }

    override fun onBackPressed() {
        routingSource.accept(Revert())
    }
}

package com.bumble.appyx.routingsource.modal.operation

import com.bumble.appyx.routingsource.modal.Modal
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.CREATED
import com.bumble.appyx.routingsource.modal.ModalElement
import com.bumble.appyx.routingsource.modal.ModalElements
import com.bumble.appyx.core.routing.RoutingKey
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

@Parcelize
data class Add<T : Any>(
    private val element: @RawValue T
) : ModalOperation<T> {

    override fun isApplicable(elements: ModalElements<T>) = true

    override fun invoke(elements: ModalElements<T>): ModalElements<T> {
        return elements + ModalElement(
            key = RoutingKey(element),
            fromState = CREATED,
            targetState = CREATED,
            operation = this
        )
    }
}

fun <T : Any> Modal<T>.add(element: T) {
    accept(Add(element))
}

package com.bumble.appyx.routingsource.modal.operation

import com.bumble.appyx.routingsource.modal.Modal
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.DESTROYED
import com.bumble.appyx.routingsource.modal.ModalElements
import com.bumble.appyx.core.routing.RoutingKey
import kotlinx.parcelize.Parcelize

@Parcelize
data class Destroy<T : Any>(
    private val key: RoutingKey<T>
) : ModalOperation<T> {

    override fun isApplicable(elements: ModalElements<T>) = true

    override fun invoke(elements: ModalElements<T>): ModalElements<T> {
        return elements.map {
            if (it.key == key) {
                it.transitionTo(
                    targetState = DESTROYED,
                    operation = this
                )
            } else {
                it
            }
        }
    }
}

fun <T : Any> Modal<T>.destroy(key: RoutingKey<T>) {
    accept(Destroy(key))
}

package com.bumble.appyx.routingsource.modal.operation

import com.bumble.appyx.routingsource.modal.Modal
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.CREATED
import com.bumble.appyx.routingsource.modal.ModalElements
import com.bumble.appyx.core.routing.RoutingKey
import kotlinx.parcelize.Parcelize

@Parcelize
data class Dismiss<T : Any>(
    private val key: RoutingKey<T>
) : ModalOperation<T> {

    override fun isApplicable(elements: ModalElements<T>) = true

    override fun invoke(elements: ModalElements<T>): ModalElements<T> {
        return elements.map {
            if (it.key == key) {
                it.transitionTo(
                    targetState = CREATED,
                    operation = this
                )
            } else {
                it
            }
        }
    }
}

fun <T : Any> Modal<T>.dismiss(key: RoutingKey<T>) {
    accept(Dismiss(key))
}

package com.bumble.appyx.routingsource.modal.operation

import com.bumble.appyx.routingsource.modal.Modal
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.FULL_SCREEN
import com.bumble.appyx.routingsource.modal.ModalElements
import com.bumble.appyx.core.routing.RoutingKey
import kotlinx.parcelize.Parcelize

@Parcelize
data class FullScreen<T : Any>(
    private val key: RoutingKey<T>
) : ModalOperation<T> {

    override fun isApplicable(elements: ModalElements<T>) = true

    override fun invoke(elements: ModalElements<T>): ModalElements<T> {
        return elements.map {
            if (it.key == key) {
                it.transitionTo(
                    targetState = FULL_SCREEN,
                    operation = this
                )
            } else {
                it
            }
        }
    }
}

fun <T : Any> Modal<T>.fullScreen(key: RoutingKey<T>) {
    accept(FullScreen(key))
}

package com.bumble.appyx.routingsource.modal.operation

import com.bumble.appyx.routingsource.modal.Modal.TransitionState
import com.bumble.appyx.core.routing.Operation

sealed interface ModalOperation<T> : Operation<T, TransitionState>

package com.bumble.appyx.routingsource.modal.operation

import com.bumble.appyx.routingsource.modal.Modal
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.FULL_SCREEN
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.MODAL
import com.bumble.appyx.routingsource.modal.ModalElements
import kotlinx.parcelize.Parcelize

@Parcelize
class Revert<T : Any> : ModalOperation<T> {

    override fun isApplicable(elements: ModalElements<T>): Boolean =
        true

    override fun invoke(elements: ModalElements<T>): ModalElements<T> {
        return elements.map {
            when (it.targetState) {
                FULL_SCREEN -> {
                    it.transitionTo(
                        targetState = MODAL,
                        operation = this
                    )
                }
                MODAL -> {
                    it.transitionTo(
                        targetState = FULL_SCREEN,
                        operation = this
                    )
                }
                else -> {
                    it
                }
            }
        }
    }
}

fun <T : Any> Modal<T>.revert() {
    accept(Revert())
}

package com.bumble.appyx.routingsource.modal.operation

import com.bumble.appyx.routingsource.modal.Modal
import com.bumble.appyx.routingsource.modal.Modal.TransitionState.MODAL
import com.bumble.appyx.routingsource.modal.ModalElements
import com.bumble.appyx.core.routing.RoutingKey
import kotlinx.parcelize.Parcelize

@Parcelize
data class Show<T : Any>(
    private val key: RoutingKey<T>
) : ModalOperation<T> {

    override fun isApplicable(elements: ModalElements<T>) = true

    override fun invoke(elements: ModalElements<T>): ModalElements<T> {
        return elements.map {
            if (it.key == key) {
                it.transitionTo(
                    targetState = MODAL,
                    operation = this
                )
            } else {
                it
            }
        }
    }
}

fun <T : Any> Modal<T>.show(key: RoutingKey<T>) {
    accept(Show(key))
}

package com.bumble.appyx.routingsource.promoter.routingsource

import com.bumble.appyx.routingsource.promoter.routingsource.Promoter.TransitionState
import com.bumble.appyx.routingsource.promoter.routingsource.Promoter.TransitionState.CREATED
import com.bumble.appyx.routingsource.promoter.routingsource.Promoter.TransitionState.DESTROYED
import com.bumble.appyx.routingsource.promoter.routingsource.Promoter.TransitionState.STAGE1
import com.bumble.appyx.core.routing.BaseRoutingSource
import com.bumble.appyx.core.routing.Operation.Noop
import com.bumble.appyx.core.routing.RoutingKey

class Promoter<T : Any>(
    initialItems: List<T> = listOf(),
) : BaseRoutingSource<T, TransitionState>(
    screenResolver = PromoterOnScreenResolver,
    finalState = DESTROYED,
    savedStateMap = null
) {

    enum class TransitionState {
        CREATED, STAGE1, STAGE2, STAGE3, STAGE4, SELECTED, DESTROYED;

        fun next(): TransitionState =
            when (this) {
                CREATED -> STAGE1
                STAGE1 -> STAGE2
                STAGE2 -> STAGE3
                STAGE3 -> STAGE4
                STAGE4 -> SELECTED
                SELECTED -> DESTROYED
                DESTROYED -> DESTROYED
            }
    }

    override val initialElements = initialItems.map {
        PromoterElement(
            key = RoutingKey(it),
            fromState = CREATED,
            targetState = STAGE1,
            operation = Noop()
        )
    }
}

package com.bumble.appyx.routingsource.promoter.routingsource

import com.bumble.appyx.routingsource.promoter.routingsource.Promoter.TransitionState
import com.bumble.appyx.core.routing.RoutingElement
import com.bumble.appyx.core.routing.RoutingElements

typealias PromoterElement<T> = RoutingElement<T, TransitionState>

typealias PromoterElements<T> = RoutingElements<T, TransitionState>

package com.bumble.appyx.routingsource.promoter.routingsource

import com.bumble.appyx.routingsource.promoter.routingsource.Promoter.TransitionState
import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver

internal object PromoterOnScreenResolver : OnScreenStateResolver<TransitionState> {
    override fun isOnScreen(state: TransitionState): Boolean =
        when (state) {
            TransitionState.DESTROYED -> false
            else -> true
        }
}

package com.bumble.appyx.routingsource.promoter.transitionhandler

import android.annotation.SuppressLint
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.bumble.appyx.routingsource.promoter.routingsource.Promoter
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor
import com.bumble.appyx.core.routing.transition.TransitionSpec
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sin

@Suppress("TransitionPropertiesLabel")
class PromoterTransitionHandler<T>(
    private val childSize: Dp,
    private val transitionSpec: TransitionSpec<Promoter.TransitionState, Float> = { tween(500) }
) : ModifierTransitionHandler<T, Promoter.TransitionState>() {

    data class TargetStateValues(
        val dpOffset: DpOffset,
        val scale: Float,
        val angleDegrees: Float,
        val effectiveRadiusRatio: Float,
        val rotationY: Float,
        val rotationZ: Float,
    )

    private val created = TargetStateValues(
        dpOffset = DpOffset(0.dp, 0.dp),
        scale = 0f,
        angleDegrees = 0f,
        effectiveRadiusRatio = 1f,
        rotationY = 0f,
        rotationZ = 0f,
    )

    private val stage1 = created.copy(
        scale = 0.25f
    )

    private val stage2 = stage1.copy(
        scale = 0.45f,
        angleDegrees = 90f,
    )

    private val stage3 = stage2.copy(
        scale = 0.65f,
        angleDegrees = 180f,
    )

    private val stage4 = stage3.copy(
        scale = 0.85f,
        angleDegrees = 270f,
    )

    private val selected = stage4.copy(
        scale = 1f,
        effectiveRadiusRatio = 0f,
        rotationY = 360f,
    )

    private val destroyed = selected.copy(
        dpOffset = DpOffset(500.dp, (-200).dp),
        scale = 0f,
        rotationZ = 540f,
    )

    @SuppressLint("UnusedTransitionTargetStateParameter")
    override fun createModifier(
        modifier: Modifier,
        transition: Transition<Promoter.TransitionState>,
        descriptor: TransitionDescriptor<T, Promoter.TransitionState>
    ): Modifier = modifier.composed {

        val halfWidthDp = (descriptor.params.bounds.width.value - childSize.value) / 2
        val halfHeightDp = (descriptor.params.bounds.height.value - childSize.value) / 2
        val radiusDp = min(halfWidthDp, halfHeightDp) // * 0.75f

        val angleDegrees = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = { it.target().angleDegrees }
        )

        val effectiveRadiusDp = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = { it.target().effectiveRadiusRatio * radiusDp }
        )

        val arcOffsetDp = derivedStateOf {
            val angleRadians = Math.toRadians(angleDegrees.value.toDouble() - 90)
            val x = effectiveRadiusDp.value * cos(angleRadians)
            val y = effectiveRadiusDp.value * sin(angleRadians)
            Offset(x.toFloat(), y.toFloat())
        }

        val rotationY = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = { it.target().rotationY })

        val rotationZ = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = { it.target().rotationZ })

        val scale = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = { it.target().scale })

        val dpOffsetX = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = { it.target().dpOffset.x.value })

        val dpOffsetY = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = { it.target().dpOffset.y.value })

        return@composed this
            .offset {
                IntOffset(
                    x = (this.density * (halfWidthDp + dpOffsetX.value + arcOffsetDp.value.x)).roundToInt(),
                    y = (this.density * (halfHeightDp + dpOffsetY.value + arcOffsetDp.value.y)).roundToInt()
                )
            }
            .graphicsLayer(
                rotationY = rotationY.value,
                rotationZ = rotationZ.value
            )
            .scale(scale.value)
    }

    @Composable
    private fun Promoter.TransitionState.target() =
        when (this) {
            Promoter.TransitionState.CREATED -> created
            Promoter.TransitionState.STAGE1 -> stage1
            Promoter.TransitionState.STAGE2 -> stage2
            Promoter.TransitionState.STAGE3 -> stage3
            Promoter.TransitionState.STAGE4 -> stage4
            Promoter.TransitionState.SELECTED -> selected
            Promoter.TransitionState.DESTROYED -> destroyed
        }
}

@Composable
fun <T> rememberPromoterTransitionHandler(
    childSize: Dp,
    transitionSpec: TransitionSpec<Promoter.TransitionState, Float> = { tween(500) }
): ModifierTransitionHandler<T, Promoter.TransitionState> = remember {
    PromoterTransitionHandler(childSize, transitionSpec)
}

package com.bumble.appyx.routingsource.tiles.backPressHandler

import com.bumble.appyx.core.routing.backpresshandlerstrategies.BaseBackPressHandlerStrategy
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.TilesElements
import com.bumble.appyx.routingsource.tiles.operation.DeselectAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class DeselectAllTiles<Routing : Any> :
    BaseBackPressHandlerStrategy<Routing, Tiles.TransitionState>() {

    override val canHandleBackPressFlow: Flow<Boolean> by lazy {
        routingSource.elements.map(::areThereSelectedTiles)
    }

    private fun areThereSelectedTiles(elements: TilesElements<Routing>) =
        elements.any { it.targetState == Tiles.TransitionState.SELECTED }

    override fun onBackPressed() {
        routingSource.accept(DeselectAll())
    }
}

package com.bumble.appyx.routingsource.tiles.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.TilesElement
import com.bumble.appyx.routingsource.tiles.TilesElements
import com.bumble.appyx.routingsource.tiles.Tiles.TransitionState.CREATED
import com.bumble.appyx.routingsource.tiles.Tiles.TransitionState.STANDARD
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

@Parcelize
data class Add<T : Any>(
    private val element: @RawValue T
) : TilesOperation<T> {

    override fun isApplicable(elements: TilesElements<T>): Boolean = true

    override fun invoke(
        elements: TilesElements<T>,
    ): RoutingElements<T, Tiles.TransitionState> =
        elements + TilesElement(
            key = RoutingKey(element),
            fromState = CREATED,
            targetState = STANDARD,
            operation = this
        )
}

fun <T : Any> Tiles<T>.add(element: T) {
    accept(Add(element))
}

package com.bumble.appyx.routingsource.tiles.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.TilesElements
import kotlinx.parcelize.Parcelize

@Parcelize
data class Deselect<T : Any>(
    private val key: RoutingKey<T>
) : TilesOperation<T> {

    override fun isApplicable(elements: TilesElements<T>): Boolean = true

    override fun invoke(
        elements: TilesElements<T>
    ): RoutingElements<T, Tiles.TransitionState> =
        elements.map {
            if (it.key == key && it.targetState == Tiles.TransitionState.SELECTED) {
                it.transitionTo(
                    targetState = Tiles.TransitionState.STANDARD,
                    operation = this
                )
            } else {
                it
            }
        }
}

fun <T : Any> Tiles<T>.deselect(key: RoutingKey<T>) {
    accept(Deselect(key))
}

package com.bumble.appyx.routingsource.tiles.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.TilesElements
import kotlinx.parcelize.Parcelize

@Parcelize
class DeselectAll<T : Any> : TilesOperation<T> {

    override fun isApplicable(elements: TilesElements<T>): Boolean = true

    override fun invoke(
        elements: TilesElements<T>
    ): RoutingElements<T, Tiles.TransitionState> =
        elements.map {
            if (it.targetState == Tiles.TransitionState.SELECTED) {
                it.transitionTo(
                    targetState = Tiles.TransitionState.STANDARD,
                    operation = this
                )
            } else {
                it
            }
        }

    override fun equals(other: Any?): Boolean = this.javaClass == other?.javaClass

    override fun hashCode(): Int = this.javaClass.hashCode()
}

fun <T : Any> Tiles<T>.deselectAll() {
    accept(DeselectAll())
}

package com.bumble.appyx.routingsource.tiles.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.TilesElements
import kotlinx.parcelize.Parcelize

@Parcelize
data class Destroy<T : Any>(
    private val key: RoutingKey<T>
) : TilesOperation<T> {

    override fun isApplicable(elements: TilesElements<T>): Boolean = true

    override fun invoke(
        elements: TilesElements<T>
    ): RoutingElements<T, Tiles.TransitionState> =
        elements.map {
            if (it.key == key) {
                it.transitionTo(
                    targetState = Tiles.TransitionState.DESTROYED,
                    operation = this
                )
            } else {
                it
            }
        }
}

fun <T : Any> Tiles<T>.destroy(key: RoutingKey<T>) {
    accept(Destroy(key))
}

package com.bumble.appyx.routingsource.tiles.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.TilesElements
import kotlinx.parcelize.Parcelize

@Parcelize
class RemoveSelected<T : Any> : TilesOperation<T> {

    override fun isApplicable(elements: TilesElements<T>): Boolean = true

    override fun invoke(
        elements: TilesElements<T>
    ): RoutingElements<T, Tiles.TransitionState> =
        elements.map {
            if (it.targetState == Tiles.TransitionState.SELECTED) {
                it.transitionTo(
                    targetState = Tiles.TransitionState.DESTROYED,
                    operation = this
                )
            } else {
                it
            }
        }

    override fun equals(other: Any?): Boolean = this.javaClass == other?.javaClass

    override fun hashCode(): Int = this.javaClass.hashCode()
}

fun <T : Any> Tiles<T>.removeSelected() {
    accept(RemoveSelected())
}

package com.bumble.appyx.routingsource.tiles.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.TilesElements
import kotlinx.parcelize.Parcelize

@Parcelize
data class Select<T : Any>(
    private val key: RoutingKey<T>
) : TilesOperation<T> {

    override fun isApplicable(elements: TilesElements<T>): Boolean = true

    override fun invoke(
        elements: TilesElements<T>
    ): RoutingElements<T, Tiles.TransitionState> =
        elements.map {
            if (it.key == key && it.targetState == Tiles.TransitionState.STANDARD) {
                it.transitionTo(
                    targetState = Tiles.TransitionState.SELECTED,
                    operation = this
                )
            } else {
                it
            }
        }
}

fun <T : Any> Tiles<T>.select(key: RoutingKey<T>) {
    accept(Select(key))
}

package com.bumble.appyx.routingsource.tiles.operation

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.routingsource.tiles.Tiles

sealed interface TilesOperation<T> : Operation<T, Tiles.TransitionState>

package com.bumble.appyx.routingsource.tiles.operation

import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.TilesElements
import kotlinx.parcelize.Parcelize

@Parcelize
data class ToggleSelection<T : Any>(
    private val key: RoutingKey<T>
) : TilesOperation<T> {

    override fun isApplicable(elements: TilesElements<T>): Boolean = true

    override fun invoke(
        elements: TilesElements<T>
    ): RoutingElements<T, Tiles.TransitionState> =
        elements.map {
            if (it.key == key) {
                when (it.targetState) {
                    Tiles.TransitionState.SELECTED -> it.transitionTo(
                        targetState = Tiles.TransitionState.STANDARD,
                        operation = this
                    )
                    Tiles.TransitionState.STANDARD -> it.transitionTo(
                        targetState = Tiles.TransitionState.SELECTED,
                        operation = this
                    )
                    else -> it
                }
            } else {
                it
            }
        }
}

fun <T : Any> Tiles<T>.toggleSelection(key: RoutingKey<T>) {
    accept(ToggleSelection(key))
}

package com.bumble.appyx.routingsource.tiles.transitionhandler

import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.unit.IntOffset
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor
import com.bumble.appyx.core.routing.transition.TransitionSpec
import kotlin.math.roundToInt

@Suppress("TransitionPropertiesLabel")
class TilesTransitionHandler<T>(
    private val transitionSpec: TransitionSpec<Tiles.TransitionState, Float> = { spring() }
) : ModifierTransitionHandler<T, Tiles.TransitionState>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<Tiles.TransitionState>,
        descriptor: TransitionDescriptor<T, Tiles.TransitionState>
    ): Modifier = modifier.composed {
        val scale = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    Tiles.TransitionState.CREATED -> 0f
                    Tiles.TransitionState.STANDARD -> 0.75f
                    Tiles.TransitionState.SELECTED -> 1.0f
                    Tiles.TransitionState.DESTROYED -> 0f
                }
            })

        val destroyProgress = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    Tiles.TransitionState.DESTROYED -> 1f
                    else -> 0f
                }
            })

        offset {
            IntOffset(
                x = (1000f * destroyProgress.value * this.density).roundToInt(),
                y = (-200 * destroyProgress.value * this.density).roundToInt()
            )
        }
            .rotate(720 * destroyProgress.value)
            .scale(scale.value)
    }
}

@Composable
fun <T> rememberTilesTransitionHandler(
    transitionSpec: TransitionSpec<Tiles.TransitionState, Float> = { spring() }
): ModifierTransitionHandler<T, Tiles.TransitionState> = remember {
    TilesTransitionHandler(transitionSpec)
}

package com.bumble.appyx.sandbox.client.backstack

import android.os.Parcelable
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight.Companion.Bold
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.routing.backpresshandlerstrategies.DontHandleBackPress
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.BackStackElements
import com.bumble.appyx.routingsource.backstack.operation.newRoot
import com.bumble.appyx.routingsource.backstack.operation.pop
import com.bumble.appyx.routingsource.backstack.operation.push
import com.bumble.appyx.routingsource.backstack.operation.remove
import com.bumble.appyx.routingsource.backstack.operation.replace
import com.bumble.appyx.routingsource.backstack.operation.singleTop
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Operation.NEW_ROOT
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Operation.POP
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Operation.PUSH
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Operation.REMOVE
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Operation.REPLACE
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Operation.SINGLE_TOP
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Operation.values
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Routing
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Routing.ChildA
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Routing.ChildB
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Routing.ChildC
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode.Routing.ChildD
import com.bumble.appyx.sandbox.client.child.ChildNode
import com.google.accompanist.flowlayout.FlowRow
import kotlinx.parcelize.Parcelize
import kotlin.random.Random

class BackStackExampleNode(
    buildContext: BuildContext,
    private val backStack: BackStack<Routing> = BackStack(
        initialElement = ChildA(value = DEFAULT_VALUE),
        savedStateMap = buildContext.savedStateMap,
        backPressHandler = DontHandleBackPress(),
    )
) : ParentNode<Routing>(
    routingSource = backStack,
    buildContext = buildContext,
) {

    sealed class Routing(
        val name: String
    ) : Parcelable {

        abstract val value: String

        @Parcelize
        data class ChildA(
            override val value: String
        ) : Routing("A")

        @Parcelize
        data class ChildB(
            override val value: String
        ) : Routing("B")

        @Parcelize
        data class ChildC(
            override val value: String
        ) : Routing("C")

        @Parcelize
        data class ChildD(
            override val value: String
        ) : Routing("D")
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is ChildA -> ChildNode(routing.name, buildContext)
            is ChildB -> ChildNode(routing.name, buildContext)
            is ChildC -> ChildNode(routing.name, buildContext)
            is ChildD -> ChildNode(routing.name, buildContext)
        }

    @Composable
    override fun View(modifier: Modifier) {
        val backStackState = backStack.elements.collectAsState()
        val selectedChildRadioButton = rememberSaveable { mutableStateOf("") }
        val defaultOrRandomRadioButton = rememberSaveable { mutableStateOf(DEFAULT_LABEL) }
        val isRadioButtonNeeded = rememberSaveable { mutableStateOf(false) }
        val selectedId = rememberSaveable { mutableStateOf("") }
        val isIdNeeded = rememberSaveable { mutableStateOf(false) }
        val selectedOperation = rememberSaveable { mutableStateOf<Operation?>(null) }
        val areThereMissingParams = rememberSaveable { mutableStateOf(true) }
        val skipChildRenderingByRouting = rememberSaveable { mutableStateOf(NONE_VALUE) }

        Column(
            modifier = modifier
                .fillMaxSize()
                .verticalScroll(rememberScrollState())
        ) {
            Text("Back stack example placeholder")
            Column(
                Modifier.padding(24.dp),
                horizontalAlignment = CenterHorizontally
            ) {
                Children(
                    modifier = Modifier
                        .padding(top = 12.dp, bottom = 12.dp)
                        .fillMaxWidth(),
                    routingSource = backStack,
                    transitionHandler = rememberBackStackExampleTransitionHandler()
                ) {
                    children<Routing> { child, descriptor ->
                        if (!descriptor.element.isFiltered(skipChildRenderingByRouting.value)) {
                            child()
                        }
                    }
                }
                Spacer(modifier = Modifier.size(8.dp))
                Column(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(text = "Child: ", fontWeight = Bold)
                    Row {
                        listOf("A", "B", "C", "D").forEach {
                            Row {
                                RadioButton(
                                    selected = it == selectedChildRadioButton.value,
                                    enabled = isRadioButtonNeeded.value,
                                    onClick = { selectedChildRadioButton.value = it }
                                )
                                Text(text = it)
                                Spacer(modifier = Modifier.size(36.dp))
                            }
                        }
                    }
                    Row {
                        RadioButton(
                            selected = defaultOrRandomRadioButton.value == DEFAULT_LABEL,
                            enabled = isRadioButtonNeeded.value,
                            onClick = { defaultOrRandomRadioButton.value = DEFAULT_LABEL }
                        )
                        Text(text = DEFAULT_LABEL)
                        Spacer(modifier = Modifier.size(36.dp))
                        RadioButton(
                            selected = defaultOrRandomRadioButton.value == RANDOM_LABEL,
                            enabled = isRadioButtonNeeded.value,
                            onClick = { defaultOrRandomRadioButton.value = RANDOM_LABEL }
                        )
                        Text(text = RANDOM_LABEL)
                    }
                }
                Spacer(modifier = Modifier.size(8.dp))
                Column(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(text = "Id: ", fontWeight = Bold)
                    val expanded = rememberSaveable { mutableStateOf(false) }
                    Text(
                        text = if (selectedId.value.isNotEmpty()) selectedId.value else "Select an id",
                        modifier = if (isIdNeeded.value) {
                            Modifier
                                .fillMaxWidth()
                                .background(Color.White)
                                .clickable { expanded.value = true }
                        } else {
                            Modifier
                                .fillMaxWidth()
                                .background(Color.LightGray)
                        }
                    )
                    DropdownMenu(
                        expanded = expanded.value,
                        onDismissRequest = { expanded.value = false }
                    ) {
                        backStackState.value.forEach { element ->
                            DropdownMenuItem(onClick = {
                                selectedId.value = element.key.id
                                expanded.value = false
                            }) {
                                Text(text = element.key.id)
                            }
                        }
                    }
                }
                Spacer(modifier = Modifier.size(8.dp))
                Column(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(text = "Operation: ", fontWeight = Bold)
                    FlowRow {
                        values().forEach { operation ->
                            val selected = selectedOperation.value == operation
                            Button(
                                onClick = {
                                    selectedOperation.value = operation
                                    selectedChildRadioButton.value = ""
                                    defaultOrRandomRadioButton.value = DEFAULT_LABEL
                                    selectedId.value = ""
                                    isRadioButtonNeeded.value = operation.radioButtonNeeded
                                    isIdNeeded.value = operation.idNeeded
                                    areThereMissingParams.value = true
                                },
                                modifier = Modifier.padding(4.dp),
                                colors = ButtonDefaults.buttonColors(
                                    backgroundColor = if (selected) {
                                        MaterialTheme.colors.primary
                                    } else {
                                        MaterialTheme.colors.primaryVariant
                                    },
                                    contentColor = MaterialTheme.colors.onPrimary
                                )
                            ) {
                                Text(text = operation.label)
                            }
                        }
                    }
                }
                Spacer(modifier = Modifier.size(8.dp))
                Column(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(text = "Missing params: ", fontWeight = Bold)
                    val textBuilder = mutableListOf<String>()
                    if (selectedOperation.value?.radioButtonNeeded == true && selectedChildRadioButton.value.isEmpty()) {
                        textBuilder.add("Child")
                        areThereMissingParams.value = true
                    }
                    if (selectedOperation.value?.idNeeded == true && selectedId.value.isEmpty()) {
                        textBuilder.add("Id")
                        areThereMissingParams.value = true
                    }
                    if (textBuilder.isEmpty()) {
                        textBuilder.add("None")
                        areThereMissingParams.value = false
                    }
                    Text(text = textBuilder.joinToString(", "))
                }
                Button(
                    enabled = selectedOperation.value != null && !areThereMissingParams.value,
                    onClick = {
                        when (selectedOperation.value) {
                            PUSH -> {
                                backStack.push(selectedChildRadioButton.value.toChild(random = defaultOrRandomRadioButton.value.random))
                            }
                            POP -> {
                                backStack.pop()
                            }
                            REPLACE -> {
                                backStack.replace(selectedChildRadioButton.value.toChild(random = defaultOrRandomRadioButton.value.random))
                            }
                            REMOVE -> {
                                backStack.remove(backStackState.value.first { it.key.id == selectedId.value }.key)
                                selectedId.value = ""
                            }
                            NEW_ROOT -> {
                                backStack.newRoot(selectedChildRadioButton.value.toChild(random = defaultOrRandomRadioButton.value.random))
                            }
                            SINGLE_TOP -> {
                                backStack.singleTop(
                                    selectedChildRadioButton.value.toChild(
                                        random = defaultOrRandomRadioButton.value.random
                                    )
                                )
                            }
                            null -> Unit
                        }
                    }
                ) {
                    Text(text = "Perform")
                }
                Spacer(modifier = Modifier.size(8.dp))
                Column(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(text = "BackStack:", fontWeight = Bold)
                    Text(text = "${backStackState.value.toStateString()}")
                }
                Spacer(modifier = Modifier.size(16.dp))
                Column(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(text = "Skip rendering of:", fontWeight = Bold)
                    Spacer(modifier = Modifier.size(8.dp))
                    Row {
                        listOf(NONE_VALUE, "A", "B", "C", "D").forEach {
                            Row {
                                RadioButton(
                                    selected = it == skipChildRenderingByRouting.value,
                                    onClick = { skipChildRenderingByRouting.value = it }
                                )
                                Text(text = it)
                                Spacer(modifier = Modifier.size(36.dp))
                            }
                        }
                    }
                }
            }
        }
    }

    private fun BackStackElements<Routing>.toStateString() = map { element ->
        val key = element.key
        val name = key.routing.name
        val value = key.routing.value
        val id = key.id
        "$name(Value: $value. Id: $id)"
    }

    private fun String.toChild(random: Boolean): Routing {
        val value = if (random) Random.nextInt(1000).toString() else DEFAULT_VALUE
        return when (this) {
            "A" -> ChildA(value = value)
            "B" -> ChildB(value = value)
            "C" -> ChildC(value = value)
            "D" -> ChildD(value = value)
            else -> throw IllegalArgumentException("Could not find the corresponding child!")
        }
    }

    private fun Routing.isFiltered(filter: String) = when (filter) {
        "A" -> this is ChildA
        "B" -> this is ChildB
        "C" -> this is ChildC
        "D" -> this is ChildD
        else -> false
    }

    private val String.random
        get() = this == RANDOM_LABEL

    enum class Operation(
        val label: String,
        val radioButtonNeeded: Boolean,
        val idNeeded: Boolean
    ) {
        PUSH("Push", true, false),
        POP("Pop", false, false),
        REPLACE("Replace", true, false),
        REMOVE("Remove", false, true),
        NEW_ROOT("New Root", true, false),
        SINGLE_TOP("Single Top", true, false)
    }

    companion object {
        private const val DEFAULT_LABEL = "Default"
        private const val RANDOM_LABEL = "Random"
        private const val DEFAULT_VALUE = "DEFAULT"
        private const val NONE_VALUE = "NONE"
    }
}

package com.bumble.appyx.sandbox.client.backstack

import androidx.compose.animation.core.Transition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.operation.Replace
import com.bumble.appyx.routingsource.backstack.transitionhandler.BackStackFader
import com.bumble.appyx.routingsource.backstack.transitionhandler.BackStackSlider
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor

class BackStackExampleTransitionHandler<T> :
    ModifierTransitionHandler<T, BackStack.TransitionState>(clipToBounds = true) {

    private val slider = BackStackSlider<T>(clipToBounds = clipToBounds)
    private val fader = BackStackFader<T>()

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<BackStack.TransitionState>,
        descriptor: TransitionDescriptor<T, BackStack.TransitionState>
    ): Modifier =
        when (descriptor.operation) {
            is Replace -> fader.createModifier(modifier, transition, descriptor)
            else -> slider.createModifier(modifier, transition, descriptor)
        }
}

@Composable
fun <T> rememberBackStackExampleTransitionHandler(
): ModifierTransitionHandler<T, BackStack.TransitionState> = remember {
    BackStackExampleTransitionHandler()
}

package com.bumble.appyx.sandbox.client.blocker

import android.os.Parcelable
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.operation.push
import com.bumble.appyx.sandbox.client.child.ChildNode
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.parcelize.Parcelize
import java.util.UUID

class BlockerExampleNode(
    buildContext: BuildContext,
    private val backStack: BackStack<Child> = BackStack(
        savedStateMap = buildContext.savedStateMap,
        initialElement = Child(UUID.randomUUID().toString()),
    )
) : ParentNode<BlockerExampleNode.Child>(
    buildContext = buildContext,
    routingSource = backStack,
) {

    @Parcelize
    data class Child(val id: String) : Parcelable

    private val allowNavigateUpFromChildren = MutableStateFlow(true)

    override fun resolve(routing: Child, buildContext: BuildContext): Node =
        ChildNode(routing.id, buildContext)

    @Composable
    override fun View(modifier: Modifier) {
        Column(modifier = Modifier.fillMaxWidth()) {

            val context = LocalContext.current
            var lastToastClickTime by remember { mutableStateOf(0L) }
            BackHandler {
                val time = System.currentTimeMillis()
                if (time - lastToastClickTime > 5000L) {
                    Toast.makeText(
                        context,
                        "It is a blocker, you can't go back by yourself, use 'go up'",
                        Toast.LENGTH_SHORT,
                    ).show()
                    lastToastClickTime = time
                }
            }

            Button(onClick = { backStack.push(Child(UUID.randomUUID().toString())) }) {
                Text(text = "Push child")
            }

            Row(
                verticalAlignment = Alignment.CenterVertically,
                modifier = Modifier.fillMaxWidth(),
            ) {
                Text(text = "Allow navigate up events from children")
                val checked by allowNavigateUpFromChildren.collectAsState()
                Checkbox(
                    checked = checked,
                    onCheckedChange = { allowNavigateUpFromChildren.value = it },
                )
            }

            Children(
                modifier = Modifier
                    .padding(top = 12.dp, bottom = 12.dp)
                    .fillMaxWidth(),
                routingSource = backStack,
            )
        }
    }

    override fun performUpNavigation(): Boolean =
        if (allowNavigateUpFromChildren.value) {
            super.performUpNavigation()
        } else {
            true
        }

}

package com.bumble.appyx.sandbox.client.child

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.sandbox.ui.atomic_tangerine
import com.bumble.appyx.sandbox.ui.manatee
import com.bumble.appyx.sandbox.ui.md_amber_500
import com.bumble.appyx.sandbox.ui.md_blue_500
import com.bumble.appyx.sandbox.ui.md_blue_grey_500
import com.bumble.appyx.sandbox.ui.md_cyan_500
import com.bumble.appyx.sandbox.ui.md_grey_500
import com.bumble.appyx.sandbox.ui.md_indigo_500
import com.bumble.appyx.sandbox.ui.md_light_blue_500
import com.bumble.appyx.sandbox.ui.md_light_green_500
import com.bumble.appyx.sandbox.ui.md_lime_500
import com.bumble.appyx.sandbox.ui.md_pink_500
import com.bumble.appyx.sandbox.ui.md_teal_500
import com.bumble.appyx.sandbox.ui.silver_sand
import com.bumble.appyx.sandbox.ui.sizzling_red
import kotlin.random.Random

class ChildNode(
    private val name: String,
    buildContext: BuildContext
) : Node(
    buildContext = buildContext,
) {

    private val colors = listOf(
        manatee,
        sizzling_red,
        atomic_tangerine,
        silver_sand,
        md_pink_500,
        md_indigo_500,
        md_blue_500,
        md_light_blue_500,
        md_cyan_500,
        md_teal_500,
        md_light_green_500,
        md_lime_500,
        md_amber_500,
        md_grey_500,
        md_blue_grey_500
    )

    private val colorIndex =
        buildContext.savedStateMap?.get(KEY_COLOR_INDEX) as? Int ?: Random.nextInt(colors.size)
    private val color = colors[colorIndex]

    override fun onSaveInstanceState(state: MutableSavedStateMap) {
        super.onSaveInstanceState(state)
        state[KEY_COLOR_INDEX] = colorIndex
    }

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
                .background(
                    color = color,
                    shape = RoundedCornerShape(6.dp)
                )
        ) {
            Column(
                modifier = Modifier.padding(24.dp)
            ) {
                Text("Child ($name).")
                Row {
                    // Local UI state should be saved too (both in backstack and onSaveInstanceState)
                    var counter by rememberSaveable { mutableStateOf(0) }
                    Text(text = "Counter $counter", modifier = Modifier.align(CenterVertically))
                    Spacer(modifier = Modifier.width(16.dp))
                    Button(onClick = { counter++ }) {
                        Text("Increment")
                    }
                }
                Row {
                    Button(onClick = { navigateUp() }) {
                        Text("Go up")
                    }
                }
            }
        }
    }

    companion object {
        private const val KEY_COLOR_INDEX = "ColorIndex"
    }
}

@Preview
@Composable
fun ChildPreview() {
    ChildNode("1", BuildContext.root(null)).Compose()
}

package com.bumble.appyx.sandbox.client.combined

import android.os.Parcelable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.operation.push
import com.bumble.appyx.routingsource.backstack.transitionhandler.rememberBackstackFader
import com.bumble.appyx.core.routing.source.combined.plus
import com.bumble.appyx.sandbox.client.child.ChildNode
import com.bumble.appyx.sandbox.client.combined.CombinedRoutingSourceNode.Routing.Configuration.Child
import java.util.*
import kotlinx.parcelize.Parcelize

class CombinedRoutingSourceNode(
    buildContext: BuildContext,
    private val backStack1: BackStack<Routing> = BackStack(
        initialElement = Child(UUID.randomUUID().toString()),
        savedStateMap = buildContext.savedStateMap,
        key = "BackStack1",
    ),
    private val backStack2: BackStack<Routing> = BackStack(
        initialElement = Child(UUID.randomUUID().toString()),
        savedStateMap = buildContext.savedStateMap,
        key = "BackStack2",
    ),
) : ParentNode<CombinedRoutingSourceNode.Routing>(
    buildContext = buildContext,
    routingSource = backStack1 + backStack2,
) {

    sealed class Routing : Parcelable {
        sealed class Permanent : Routing() {
            @Parcelize
            object Child1 : Permanent()
        }

        sealed class Configuration : Routing() {
            @Parcelize
            data class Child(val id: String) : Configuration()
        }
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is Routing.Configuration.Child -> ChildNode(routing.id, buildContext)
            is Routing.Permanent.Child1 -> ChildNode("Permanent", buildContext)
        }

    @Composable
    override fun View(modifier: Modifier) {
        val scrollState = rememberScrollState()
        Column(
            modifier = modifier
                .fillMaxSize()
                .verticalScroll(scrollState)
                .padding(16.dp)
        ) {

            Permanent()

            Spacer(modifier = Modifier.height(16.dp))

            BackStack("BackStack1", backStack1)

            Spacer(modifier = Modifier.height(16.dp))

            BackStack("BackStack2", backStack2)

        }
    }

    @Composable
    private fun Permanent() {
        var visibility by remember { mutableStateOf(true) }
        Text(text = "Permanent")
        Button(onClick = { visibility = !visibility }) {
            Text(text = "Trigger visibility")
        }
        PermanentChild(Routing.Permanent.Child1) { child ->
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(200.dp)
            ) {
                AnimatedVisibility(visible = visibility) {
                    child()
                }
            }
        }
    }

    @Composable
    private fun BackStack(
        name: String,
        backStack: BackStack<Routing>,
    ) {
        Text(text = name)
        Children(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp),
            routingSource = backStack,
            transitionHandler = rememberBackstackFader(transitionSpec = { tween(300) }),
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(
            onClick = {
                backStack.push(
                    Routing.Configuration.Child(
                        UUID.randomUUID().toString()
                    )
                )
            },
            modifier = Modifier.fillMaxWidth(),
        ) {
            Text(text = "Add", modifier = Modifier.padding(horizontal = 16.dp))
        }
    }

}

package com.bumble.appyx.sandbox.client.container

import android.content.Intent
import android.os.Parcelable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.bumble.appyx.utils.customisations.NodeCustomisation
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.operation.push
import com.bumble.appyx.routingsource.backstack.transitionhandler.rememberBackstackFader
import com.bumble.appyx.routingsource.backstack.transitionhandler.rememberBackstackSlider
import com.bumble.appyx.core.routing.transition.rememberCombinedHandler
import com.bumble.appyx.sandbox.client.backstack.BackStackExampleNode
import com.bumble.appyx.sandbox.client.blocker.BlockerExampleNode
import com.bumble.appyx.sandbox.client.combined.CombinedRoutingSourceNode
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.BackStackExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.BlockerExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.CombinedRoutingSource
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.Customisations
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.InteractorExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.LazyExamples
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.ModalExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.MviCoreExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.Picker
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.RequestPermissionsExamples
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.RoutingSourcesExamples
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.SpotlightExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.Routing.TilesExample
import com.bumble.appyx.sandbox.client.customisations.createViewCustomisationsActivityIntent
import com.bumble.appyx.sandbox.client.integrationpoint.IntegrationPointExampleNode
import com.bumble.appyx.sandbox.client.interactorusage.InteractorNodeBuilder
import com.bumble.appyx.sandbox.client.interop.InteropExampleActivity
import com.bumble.appyx.sandbox.client.list.LazyListContainerNode
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleBuilder
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode
import com.bumble.appyx.sandbox.client.tiles.TilesExampleNode
import com.bumble.appyx.sandbox.client.modal.ModalExampleNode
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize

class ContainerNode(
    buildContext: BuildContext,
    private val backStack: BackStack<Routing> = BackStack(
        initialElement = Picker,
        savedStateMap = buildContext.savedStateMap,
    )
) : ParentNode<Routing>(
    routingSource = backStack,
    buildContext = buildContext,
) {

    class Customisation(val name: String? = null) : NodeCustomisation

    private val label: String? = buildContext.getOrDefault(Customisation()).name

    sealed class Routing : Parcelable {
        @Parcelize
        object Picker : Routing()

        @Parcelize
        object BackStackExample : Routing()

        @Parcelize
        object TilesExample : Routing()

        @Parcelize
        object ModalExample : Routing()

        @Parcelize
        object CombinedRoutingSource : Routing()

        @Parcelize
        object LazyExamples : Routing()

        @Parcelize
        object RequestPermissionsExamples : Routing()

        @Parcelize
        object RoutingSourcesExamples : Routing()

        @Parcelize
        object SpotlightExample : Routing()

        @Parcelize
        object InteractorExample : Routing()

        @Parcelize
        object MviCoreExample : Routing()

        @Parcelize
        object BlockerExample : Routing()

        @Parcelize
        object Customisations : Routing()
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is Picker -> node(buildContext) { modifier -> ExamplesList(modifier) }
            is RoutingSourcesExamples -> node(buildContext) { modifier -> RoutingSources(modifier) }
            is BackStackExample -> BackStackExampleNode(buildContext)
            is ModalExample -> ModalExampleNode(buildContext)
            is TilesExample -> TilesExampleNode(buildContext)
            is CombinedRoutingSource -> CombinedRoutingSourceNode(buildContext)
            is LazyExamples -> LazyListContainerNode(buildContext)
            is SpotlightExample -> SpotlightExampleNode(buildContext)
            is InteractorExample -> InteractorNodeBuilder().build(buildContext)
            is RequestPermissionsExamples -> IntegrationPointExampleNode(buildContext)
            is MviCoreExample -> MviCoreExampleBuilder().build(
                buildContext,
                "MVICore initial state"
            )
            is BlockerExample -> BlockerExampleNode(buildContext)
            is Customisations -> node(buildContext) { modifier -> Customisations(modifier) }
        }

    @Composable
    override fun View(modifier: Modifier) {
        Children(
            modifier = modifier.fillMaxSize(),
            routingSource = backStack,
            transitionHandler = rememberCombinedHandler(
                handlers = listOf(rememberBackstackSlider(), rememberBackstackFader())
            )
        ) {
            children<Routing> { child, descriptor ->
                val color = when (descriptor.element) {
                    is BackStackExample -> Color.LightGray
                    else -> Color.White
                }
                child(modifier = Modifier.background(color))
            }
        }
    }

    @Composable
    fun ExamplesList(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
                .padding(24.dp),
            contentAlignment = Alignment.Center
        ) {
            Column(
                verticalArrangement = Arrangement.spacedBy(24.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                label?.let {
                    Text(it, textAlign = TextAlign.Center)
                }
                TextButton("Customisations Example") { backStack.push(Customisations) }
                TextButton("MVICore Example") { backStack.push(MviCoreExample) }
                TextButton("Launch interop example") {
                    integrationPoint.activityStarter.startActivity {
                        Intent(this, InteropExampleActivity::class.java)
                    }
                }
                TextButton("Routing Sources Examples") { backStack.push(RoutingSourcesExamples) }

                val scope = rememberCoroutineScope()
                TextButton("Trigger double navigation in 3 seconds") {
                    scope.launch {
                        delay(3_000)
                        backStack.push(BackStackExample)
                        backStack.push(TilesExample)
                    }
                }
                TextButton("LazyList") { backStack.push(LazyExamples) }
                TextButton("Blocker") { backStack.push(BlockerExample) }
            }
        }
    }

    @Composable
    fun RoutingSources(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
                .padding(24.dp),
            contentAlignment = Alignment.Center
        ) {
            Column(
                verticalArrangement = Arrangement.spacedBy(24.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                TextButton("Backstack example") { backStack.push(BackStackExample) }
                TextButton("Tiles example") { backStack.push(TilesExample) }
                TextButton("Modal example") { backStack.push(ModalExample) }
                TextButton("Combined routing source") { backStack.push(CombinedRoutingSource) }
                TextButton("Node with interactor") { backStack.push(Routing.InteractorExample) }
                TextButton("Spotlight Example") { backStack.push(SpotlightExample) }
                TextButton("Request permissions / start activities example") {
                    backStack.push(RequestPermissionsExamples)
                }
            }
        }
    }

    @Composable
    fun Customisations(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
                .padding(24.dp),
            contentAlignment = Alignment.Center
        ) {
            Column(
                verticalArrangement = Arrangement.spacedBy(24.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                TextButton("Push default node") {
                    integrationPoint.activityStarter.startActivity {
                        createViewCustomisationsActivityIntent(
                            context = this,
                            hasCustomisedView = false
                        )
                    }
                }
                TextButton("Push node with customised view") {
                    integrationPoint.activityStarter.startActivity {
                        createViewCustomisationsActivityIntent(
                            context = this,
                            hasCustomisedView = true
                        )
                    }
                }
            }
        }
    }

    @Composable
    private fun TextButton(text: String, onClick: () -> Unit) {
        Button(onClick = onClick) {
            Text(textAlign = TextAlign.Center, text = text)
        }
    }

}

package com.bumble.appyx.sandbox.client.customisations

import com.bumble.appyx.core.builder.SimpleBuilder
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node

class ViewCustomisationExampleBuilder : SimpleBuilder() {
    override fun build(buildContext: BuildContext): Node {
        val viewFactory =
            buildContext.getOrDefault(ViewCustomisationExampleNode.Customisations()).viewFactory

        return ViewCustomisationExampleNode(
            buildContext = buildContext,
            view = viewFactory.invoke()
        )
    }
}

package com.bumble.appyx.sandbox.client.customisations

import com.bumble.appyx.utils.customisations.NodeCustomisation
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.NodeView
import com.bumble.appyx.core.node.ViewFactory

class ViewCustomisationExampleNode(
    buildContext: BuildContext,
    view: NodeView,
) : Node(
    view = view,
    buildContext = buildContext
) {

    class Customisations(
        val viewFactory: ViewFactory<NodeView> = ViewFactory {
            ViewCustomisationExampleDefaultView()
        }
    ) : NodeCustomisation
}

package com.bumble.appyx.sandbox.client.customisations

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.bumble.appyx.core.node.NodeView

class ViewCustomisationExampleDefaultView : NodeView {

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.Gray)
        ) {
            Text(text = "Default View")
        }
    }
}

class ViewCustomisationExampleCustomisedView : NodeView {

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.LightGray)
        ) {
            Text(text = "Customised View")
        }
    }
}

package com.bumble.appyx.sandbox.client.customisations

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.sandbox.ui.AppyxSandboxTheme

class ViewCustomisationsActivity : NodeActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val hasCustomisedView: Boolean = intent.getBooleanExtra(HAS_CUSTOMISED_VIEW, false)
        setContent {
            AppyxSandboxTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Column {
                        NodeHost(
                            integrationPoint = integrationPoint,
                            customisations = getCustomisations(hasCustomisedView)
                        ) {
                            ViewCustomisationExampleBuilder().build(buildContext = it)
                        }
                    }
                }
            }
        }
    }

    private fun getCustomisations(hasCustomisedView: Boolean): NodeCustomisationDirectoryImpl {
        val nodeCustomisationDirectory = NodeCustomisationDirectoryImpl()
        if (hasCustomisedView) {
            nodeCustomisationDirectory.apply {
                put(ViewCustomisationExampleNode.Customisations(viewFactory = {
                    ViewCustomisationExampleCustomisedView()
                }))
            }
        }

        return nodeCustomisationDirectory
    }
}

fun createViewCustomisationsActivityIntent(context: Context, hasCustomisedView: Boolean = false) =
    Intent(context, ViewCustomisationsActivity::class.java).apply {
        putExtra(HAS_CUSTOMISED_VIEW, hasCustomisedView)
    }

private const val HAS_CUSTOMISED_VIEW = "has_customised_view"

package com.bumble.appyx.sandbox.client.integrationpoint

import android.Manifest
import android.app.Activity
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.integrationpoint.permissionrequester.PermissionRequester
import com.bumble.appyx.core.integrationpoint.requestcode.RequestCodeClient
import com.bumble.appyx.core.minimal.reactive.Cancellable
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.sandbox.client.integrationpoint.StartActivityExample.Companion.StringExtraKey
import java.util.*

class IntegrationPointExampleNode(buildContext: BuildContext) : Node(buildContext = buildContext),
    RequestCodeClient {

    private val permissionRequester = integrationPoint.permissionRequester
    private val activityStarter = integrationPoint.activityStarter
    private var permissionsResultCancellable: Cancellable? = null
    private var activityResultsCancellable: Cancellable? = null
    private var permissionsResultState by mutableStateOf("Press request permissions to check permissions state")
    private var activityResultState by mutableStateOf("Launch activity for result to check result state ")

    @Composable
    override fun View(modifier: Modifier) {

        DisposableEffect(key1 = Any()) {
            observeCameraPermissions()
            observeActivityResult()
            onDispose {
                permissionsResultCancellable?.cancel()
                activityResultsCancellable?.cancel()
            }
        }

        Box(
            modifier = modifier
                .fillMaxSize()
                .background(color = Color.Gray)
        ) {

            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier
                    .align(Alignment.Center)
                    .wrapContentSize()
            ) {
                Text(
                    modifier = Modifier.padding(16.dp),
                    text = permissionsResultState
                )
                Spacer(modifier = Modifier.height(8.dp))
                Button(onClick = { requestCameraPermissions() }) {
                    Text("Request permissions")
                }
                Spacer(modifier = Modifier.height(8.dp))
                Button(onClick = { launchActivity() }) {
                    Text("Launch activity")
                }
                Spacer(modifier = Modifier.height(8.dp))
                Button(onClick = { launchActivityForResult() }) {
                    Text("Launch activity for result")
                }
                Spacer(modifier = Modifier.height(8.dp))
                Text(
                    modifier = Modifier.padding(16.dp),
                    text = activityResultState
                )
            }
        }
    }

    private fun launchActivity() {
        integrationPoint.activityStarter.startActivity {
            Intent(this, StartActivityExample::class.java)
        }
    }

    private fun launchActivityForResult() {
        integrationPoint.activityStarter.startActivityForResult(this, StartActivityForResultCode) {
            Intent(this, StartActivityExample::class.java)
        }
    }

    private fun requestCameraPermissions() {
        permissionRequester.requestPermissions(
            client = this,
            requestCode = RequestCameraCode,
            permissions = arrayOf(Manifest.permission.CAMERA)
        )
    }

    private fun observeCameraPermissions() {
        permissionsResultCancellable = permissionRequester
            .events(this)
            .observe { event ->
                if (event.requestCode == RequestCameraCode) {
                    when (event) {
                        is PermissionRequester.RequestPermissionsEvent.RequestPermissionsResult -> {
                            permissionsResultState = "Permission event: $event"
                        }
                        is PermissionRequester.RequestPermissionsEvent.Cancelled -> {
                            permissionsResultState = "Permission request cancelled"
                        }
                        else -> Unit
                    }
                }
            }
    }

    private fun observeActivityResult() {
        activityResultsCancellable = activityStarter
            .events(this)
            .observe { event ->
                if (event.requestCode == StartActivityForResultCode && event.resultCode == Activity.RESULT_OK && event.data != null) {
                    val result = event.data?.getStringExtra(StringExtraKey) ?: ""
                    activityResultState = "Activity result: $result"
                }
            }
    }

    companion object {
        private const val RequestCameraCode = 1
        private const val StartActivityForResultCode = 2
    }

    override val requestCodeClientId: String = UUID.randomUUID().toString()
}

@Preview
@Composable
fun IntegrationPointExampleNodePreview() {
    IntegrationPointExampleNode(BuildContext.root(null)).Compose()
}

package com.bumble.appyx.sandbox.client.integrationpoint

import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import java.util.*

class StartActivityExample : AppCompatActivity() {

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

            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .background(color = Color.Gray)
            ) {
                Button(onClick = {
                    val uuid = UUID.randomUUID().toString()
                    setResult(RESULT_OK, Intent().apply {
                        putExtra(StringExtraKey, uuid)
                    })
                    finish()
                }) {
                    Text("Return random result")
                }
            }

        }
    }

    companion object {
        const val StringExtraKey = "StringExtraKey"
    }
}

package com.bumble.appyx.sandbox.client.interactorusage

import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.children.whenChildAttached
import com.bumble.appyx.core.children.whenChildrenAttached
import com.bumble.appyx.core.clienthelper.interactor.Interactor
import com.bumble.appyx.core.lifecycle.subscribe

class InteractorExample : Interactor<InteractorExampleNode>() {

    override fun onCreate(lifecycle: Lifecycle) {
        lifecycle.subscribe(onCreate = {
            whenChildAttached { _: Lifecycle, _: Child2Node ->
                node.child2InfoState = "Child2 has been attached"
            }
            whenChildrenAttached { _: Lifecycle, _: Child2Node, _: Child3Node ->
                node.child2And3InfoState = "Child2 and Child3 have been attached"
            }
        })
    }
}

package com.bumble.appyx.sandbox.client.interactorusage

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node

class Child2Node(buildContext: BuildContext) : Node(buildContext) {

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
                .background(color = Color.LightGray)
        )
    }
}

class Child3Node(buildContext: BuildContext) : Node(buildContext) {

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
                .background(color = Color.DarkGray)
        )
    }
}

package com.bumble.appyx.sandbox.client.interactorusage

import android.os.Parcelable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.clienthelper.interactor.Interactor
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.UpNavigationHandler
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.operation.pop
import com.bumble.appyx.routingsource.backstack.operation.push
import com.bumble.appyx.routingsource.backstack.transitionhandler.rememberBackstackFader
import com.bumble.appyx.routingsource.backstack.transitionhandler.rememberBackstackSlider
import com.bumble.appyx.core.routing.transition.rememberCombinedHandler
import com.bumble.appyx.sandbox.client.interactorusage.InteractorExampleNode.Routing
import com.bumble.appyx.sandbox.client.interactorusage.InteractorExampleNode.Routing.Child1
import kotlinx.parcelize.Parcelize

class InteractorExampleNode(
    interactor: Interactor<InteractorExampleNode>,
    buildContext: BuildContext,
    private val backStack: BackStack<Routing> = BackStack(
        initialElement = Child1,
        savedStateMap = buildContext.savedStateMap,
    )
) : ParentNode<Routing>(
    routingSource = backStack,
    buildContext = buildContext,
    plugins = listOf(interactor)
), UpNavigationHandler {

    var child2InfoState by mutableStateOf("Here will appear child2 info")
    var child2And3InfoState by mutableStateOf("Here will appear child2 and child3 combined info")

    sealed class Routing : Parcelable {
        @Parcelize
        object Child1 : Routing()

        @Parcelize
        object Child2 : Routing()

        @Parcelize
        object Child3 : Routing()
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is Routing.Child1 -> node(buildContext) { modifier ->
                Box(
                    modifier = modifier
                        .fillMaxSize()
                        .background(color = Color.LightGray)
                )
            }
            is Routing.Child2 -> Child2Node(buildContext)
            is Routing.Child3 -> Child3Node(buildContext)
        }

    @Composable
    override fun View(modifier: Modifier) {
        Column(modifier = modifier.fillMaxSize()) {
            Children(
                modifier = Modifier
                    .padding(16.dp)
                    .fillMaxWidth()
                    .requiredHeight(250.dp),
                routingSource = backStack,
                transitionHandler = rememberCombinedHandler(
                    handlers = listOf(rememberBackstackSlider(), rememberBackstackFader())
                )
            )
            Spacer(modifier = Modifier.requiredHeight(4.dp))
            Button(
                onClick = { backStack.push(Routing.Child2) },
                modifier = Modifier.padding(4.dp),
            ) {
                Text(text = "Push Child2")
            }

            Spacer(modifier = Modifier.requiredHeight(4.dp))
            Button(
                onClick = { backStack.push(Routing.Child3) },
                modifier = Modifier.padding(4.dp),
            ) {
                Text(text = "Push Child3")
            }

            Spacer(modifier = Modifier.requiredHeight(4.dp))

            Button(
                onClick = { backStack.pop() },
                modifier = Modifier.padding(4.dp),
            ) {
                Text(text = "Pop")
            }

            Spacer(modifier = Modifier.requiredHeight(8.dp))
            Text(text = "Child2 info :")
            Text(text = child2InfoState)

            Spacer(modifier = Modifier.requiredHeight(8.dp))
            Text(text = "Child2 and Child3 combined info :")
            Text(text = child2And3InfoState)
        }
    }
}

package com.bumble.appyx.sandbox.client.interactorusage

import com.bumble.appyx.core.builder.SimpleBuilder
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node

class InteractorNodeBuilder : SimpleBuilder() {
    override fun build(buildContext: BuildContext): Node {
        val interactor = InteractorExample()
        return InteractorExampleNode(buildContext = buildContext, interactor = interactor)
    }
}

package com.bumble.appyx.sandbox.client.interop

import android.os.Bundle
import android.view.ViewGroup
import android.widget.FrameLayout
import com.badoo.ribs.core.Rib
import com.badoo.ribs.core.customisation.RibCustomisationDirectory
import com.badoo.ribs.core.customisation.RibCustomisationDirectoryImpl
import com.badoo.ribs.core.modality.BuildContext
import com.bumble.appyx.interop.ribs.InteropActivity
import com.bumble.appyx.sandbox.client.container.ContainerNode
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentBuilder

class InteropExampleActivity : InteropActivity() {

    private lateinit var rootView: FrameLayout
    override val rootViewGroup: ViewGroup get() = rootView

    override fun createRib(savedInstanceState: Bundle?): Rib {
        return RibsParentBuilder().build(
            buildContext = BuildContext.root(
                savedInstanceState,
                customisations = ribCustomisations()
            )
        )
    }

    private fun ribCustomisations(): RibCustomisationDirectory =
        RibCustomisationDirectoryImpl().apply {
            put(
                ContainerNode.Customisation(name = "Interop ContainerNode customisation")
            )
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        rootView = FrameLayout(this)
        setContentView(rootView)
        super.onCreate(savedInstanceState)
    }
}

package com.bumble.appyx.sandbox.client.list

import android.os.Parcelable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells.Fixed
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.MaterialTheme
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.composable.Child
import com.bumble.appyx.core.composable.visibleChildrenAsState
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.routing.RoutingElement
import com.bumble.appyx.core.routing.source.permanent.PermanentRoutingSource
import com.bumble.appyx.sandbox.client.child.ChildNode
import com.bumble.appyx.sandbox.client.list.LazyListContainerNode.ListMode.Column
import com.bumble.appyx.sandbox.client.list.LazyListContainerNode.ListMode.Grid
import com.bumble.appyx.sandbox.client.list.LazyListContainerNode.ListMode.Row
import com.bumble.appyx.sandbox.client.list.LazyListContainerNode.ListMode.values
import com.bumble.appyx.sandbox.client.list.LazyListContainerNode.Routing
import kotlinx.parcelize.Parcelize

class LazyListContainerNode constructor(
    buildContext: BuildContext,
    routingSource: PermanentRoutingSource<Routing> = PermanentRoutingSource(
        routings = buildSet<Routing> {
            repeat(100) {
                add(Routing(it.toString()))
            }
        },
        savedStateMap = buildContext.savedStateMap
    )
) : ParentNode<Routing>(routingSource, buildContext) {
    @Parcelize
    data class Routing(val name: String) : Parcelable

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        ChildNode(routing.name, buildContext)

    enum class ListMode {
        Column, Row, Grid
    }

    @Composable
    override fun View(modifier: Modifier) {
        var selectedMode by remember { mutableStateOf(Column) }

        Column(modifier = Modifier) {
            Column {
                values().forEach { mode ->
                    RadioItem(mode, mode == selectedMode) { selectedMode = mode }
                }
            }

            val children by routingSource.visibleChildrenAsState()
            when (selectedMode) {
                Column -> ColumnExample(children)
                Row -> RowExample(children)
                Grid -> GridExample(children)
            }
        }
    }

    @Composable
    private fun ColumnExample(elements: List<RoutingElement<Routing, out Any?>>) {
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(horizontal = 16.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)

        ) {
            items(elements, key = { element -> element.key.id }) { element ->
                Child(routingElement = element)
            }
        }
    }

    @Composable
    private fun RowExample(elements: List<RoutingElement<Routing, out Any?>>) {
        LazyRow(
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(vertical = 16.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            items(elements, key = { element -> element.key.id }) { element ->
                Child(routingElement = element)
            }
        }
    }

    @Composable
    private fun GridExample(elements: List<RoutingElement<Routing, out Any?>>) {
        LazyVerticalGrid(
            columns = Fixed(2),
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(horizontal = 16.dp),
        ) {
            items(elements) { element ->
                Child(routingElement = element)
            }
        }
    }

    @Composable
    private fun RadioItem(
        mode: LazyListContainerNode.ListMode,
        isSelected: Boolean,
        onClick: () -> Unit,
    ) {
        Row(
            Modifier
                .fillMaxWidth()
                .selectable(
                    selected = isSelected,
                    onClick = onClick
                )
                .padding(horizontal = 16.dp)
        ) {
            RadioButton(
                selected = isSelected,
                onClick = onClick
            )
            Text(
                text = mode.name,
                style = MaterialTheme.typography.body1,
                modifier = Modifier.padding(start = 16.dp)
            )
        }
    }
}

package com.bumble.appyx.sandbox.client.modal

import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.routingsource.modal.Modal
import com.bumble.appyx.routingsource.modal.ModalTransitionHandler
import com.bumble.appyx.routingsource.modal.operation.fullScreen
import com.bumble.appyx.routingsource.modal.operation.show
import com.bumble.appyx.sandbox.client.child.ChildNode
import com.bumble.appyx.sandbox.client.modal.ModalExampleNode.Routing
import com.bumble.appyx.sandbox.client.modal.ModalExampleNode.Routing.Child
import kotlinx.parcelize.Parcelize

class ModalExampleNode(
    buildContext: BuildContext,
    private val modal: Modal<Routing> = Modal(
        savedStateMap = buildContext.savedStateMap,
        initialElement = Child("first")
    )
) : ParentNode<Routing>(
    routingSource = modal,
    buildContext = buildContext,
) {

    sealed class Routing(val name: String) : Parcelable {

        abstract val value: String

        @Parcelize
        data class Child(override val value: String) : Routing(value)
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is Child -> ChildNode("", buildContext)
        }

    @Composable
    override fun View(modifier: Modifier) {
        Text("Modal placeholder")
        Box(Modifier.fillMaxSize()) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(24.dp),
            ) {
                var fullScreenEnabled by remember { mutableStateOf(false) }
                val elements by modal.elements.collectAsState()

                Button(
                    enabled = !fullScreenEnabled,
                    onClick = {
                        fullScreenEnabled = true
                        modal.show(elements.last().key)
                    }

                ) {
                    Text("Show modal")
                }

                Button(
                    enabled = fullScreenEnabled,
                    onClick = {
                        modal.fullScreen(elements.last().key)
                    }
                ) {
                    Text("Make it fullscreen")
                }
            }

            Children(
                modifier = Modifier.fillMaxSize(),
                routingSource = modal,
                transitionHandler = ModalTransitionHandler()
            )
        }
    }

}

@Preview
@Composable
fun ModalPreview() {
    ModalExampleNode(BuildContext.root(null)).Compose()
}

package com.bumble.appyx.sandbox.client.mvicoreexample

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.bumble.appyx.connectable.rx2.Connectable
import com.bumble.appyx.connectable.rx2.NodeConnector
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreChildNode1.Input
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreChildNode1.Output
import kotlin.random.Random.Default.nextInt

class MviCoreChildNode1(
    buildContext: BuildContext,
    private val connector: NodeConnector<Input, Output> = NodeConnector()
) : Node(buildContext),
    Connectable<Input, Output> by connector {

    sealed class Input

    sealed class Output {
        data class Result(val data: String) : Output()
    }

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
                .background(color = Color.LightGray)
        ) {
            Button(
                onClick = { connector.output.accept(Output.Result("Child1 output ${nextInt()}")) },
                modifier = Modifier.align(Alignment.Center)
            ) {
                Text(text = "Generate and send output")
            }
        }
    }
}

package com.bumble.appyx.sandbox.client.mvicoreexample

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.bumble.appyx.connectable.rx2.Connectable
import com.bumble.appyx.connectable.rx2.NodeConnector
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreChildNode2.Input
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreChildNode2.Output
import kotlin.random.Random

class MviCoreChildNode2(
    buildContext: BuildContext,
    private val connector: NodeConnector<Input, Output> = NodeConnector()
) : Node(buildContext), Connectable<Input, Output> by connector {

    sealed class Input

    sealed class Output {
        data class Result(val data: String) : Output()
    }

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
                .background(color = Color.Black)
        ) {
            Button(
                onClick = { connector.output.accept(Output.Result("Child2 output ${Random.nextInt()}")) },
                modifier = Modifier.align(Alignment.Center)
            ) {
                Text(text = "Generate and send output")
            }
        }
    }
}

package com.bumble.appyx.sandbox.client.mvicoreexample

import com.bumble.appyx.core.builder.Builder
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleNode.Routing
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleNode.Routing.Child1
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature

class MviCoreExampleBuilder : Builder<String>() {

    override fun build(buildContext: BuildContext, payload: String): Node {
        val feature = MviCoreExampleFeature(payload)

        val backStack = BackStack<Routing>(
            initialElement = Child1,
            savedStateMap = buildContext.savedStateMap,
        )
        val view = MviCoreExampleView(backStack = backStack)
        val interactor = MviCoreExampleInteractor(
            feature = feature,
            backStack = backStack,
            view = view
        )

        return MviCoreExampleNode(
            buildContext = buildContext,
            backStack = backStack,
            view = view,
            plugins = listOf(interactor)
        )
    }
}

package com.bumble.appyx.sandbox.client.mvicoreexample

import androidx.lifecycle.Lifecycle
import com.badoo.binder.using
import com.badoo.mvicore.android.lifecycle.createDestroy
import com.badoo.mvicore.android.lifecycle.startStop
import com.bumble.appyx.core.children.whenChildAttached
import com.bumble.appyx.core.clienthelper.interactor.Interactor
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.activeRouting
import com.bumble.appyx.routingsource.backstack.operation.newRoot
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleNode.Routing
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleNode.Routing.Child1
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleNode.Routing.Child2
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleView.Event
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.EventsToWish
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.OutputChild1ToWish
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.OutputChild2ToWish
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.StateToViewModel
import io.reactivex.functions.Consumer

class MviCoreExampleInteractor(
    private val view: MviCoreExampleView,
    private val feature: MviCoreExampleFeature,
    private val backStack: BackStack<Routing>,
) : Interactor<MviCoreExampleNode>() {

    private val backStackUpdater = Consumer<Event> { event ->
        when (event) {
            is Event.SwitchChildClicked -> {
                if (backStack.activeRouting == Child1) {
                    backStack.newRoot(Child2)
                } else {
                    backStack.newRoot(Child1)
                }
            }
            else -> Unit
        }
    }

    override fun onCreate(lifecycle: Lifecycle) {
        lifecycle.startStop {
            bind(feature to view using StateToViewModel)
            bind(view to feature using EventsToWish)
            bind(view to backStackUpdater)
        }
        whenChildAttached { _: Lifecycle, child: Node ->
            child.lifecycle.createDestroy {
                when (child) {
                    is MviCoreChildNode1 -> bind(child.output to feature using OutputChild1ToWish)
                    is MviCoreChildNode2 -> bind(child.output to feature using OutputChild2ToWish)
                }
            }
        }
    }
}

package com.bumble.appyx.sandbox.client.mvicoreexample

import android.os.Parcelable
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleNode.Routing
import kotlinx.parcelize.Parcelize

class MviCoreExampleNode(
    view: MviCoreExampleView,
    buildContext: BuildContext,
    plugins: List<Plugin>,
    internal val backStack: BackStack<Routing>,
) : ParentNode<Routing>(
    view = view,
    routingSource = backStack,
    buildContext = buildContext,
    plugins = plugins
) {

    sealed class Routing : Parcelable {
        @Parcelize
        object Child1 : Routing()

        @Parcelize
        object Child2 : Routing()
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is Routing.Child1 -> MviCoreChildNode1(buildContext)
            is Routing.Child2 -> MviCoreChildNode2(buildContext)
        }
}

package com.bumble.appyx.sandbox.client.mvicoreexample

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.ParentNodeView
import com.bumble.appyx.core.routing.RoutingSource
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.transitionhandler.rememberBackstackSlider
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleNode.Routing
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleView.Event
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleView.Event.LoadDataClicked
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleView.Event.SwitchChildClicked
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.ViewModel
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.ViewModel.InitialState
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.ViewModel.Loaded
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.ViewModel.Loading
import com.jakewharton.rxrelay2.PublishRelay
import io.reactivex.ObservableSource
import io.reactivex.functions.Consumer

class MviCoreExampleView(
    private val title: String = "Title",
    private val backStack: RoutingSource<Routing, BackStack.TransitionState>,
    private val events: PublishRelay<Event> = PublishRelay.create()
) : ParentNodeView<Routing>(), ObservableSource<Event> by events, Consumer<ViewModel> {

    sealed class Event {
        object LoadDataClicked : Event()
        object SwitchChildClicked : Event()
    }

    private var vm by mutableStateOf<ViewModel?>(null)

    override fun accept(vm: ViewModel) {
        this.vm = vm
    }

    @Composable
    override fun ParentNode<Routing>.NodeView(modifier: Modifier) {
        val viewModel = vm ?: return
        Column(
            modifier = modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center
        ) {
            Text(
                text = title,
                fontSize = 18.sp,
                modifier = Modifier
                    .testTag(TitleTag)
            )
            Children(
                transitionHandler = rememberBackstackSlider(),
                modifier = modifier
                    .fillMaxWidth()
                    .requiredHeight(200.dp),
                routingSource = backStack
            )
            Button(
                modifier = Modifier
                    .align(Alignment.CenterHorizontally)
                    .padding(8.dp),
                onClick = { events.accept(SwitchChildClicked) }
            ) {
                Text(text = "Switch between children")
            }
            when (viewModel) {
                is Loading -> Box(modifier = modifier.fillMaxSize()) {
                    CircularProgressIndicator(
                        modifier = Modifier
                            .align(Alignment.Center)
                            .testTag(LoadingTestTag)
                    )
                }
                is InitialState ->
                    Box(modifier = modifier.fillMaxSize()) {
                        Column(modifier = modifier.align(Alignment.Center)) {
                            Text(
                                modifier = Modifier.testTag(InitialStateTextTag),
                                color = Color.Black, text = viewModel.stateName
                            )
                            Spacer(modifier = Modifier.requiredHeight(8.dp))
                            Button(
                                modifier = Modifier.testTag(InitialStateButtonTag),
                                onClick = { events.accept(LoadDataClicked) }
                            ) {
                                Text(
                                    modifier = Modifier.testTag(InitialStateButtonTextTag),
                                    text = "Load data"
                                )
                            }
                        }
                    }
                is Loaded ->
                    Box(modifier = modifier.fillMaxSize()) {
                        Text(
                            modifier = Modifier.align(Alignment.Center),
                            color = Color.Black,
                            text = viewModel.stateName
                        )
                    }
            }
        }
    }

    companion object {
        const val TitleTag = "Title"
        const val LoadingTestTag = "Loading"
        const val InitialStateTextTag = "InitialStateText"
        const val InitialStateButtonTag = "InitialStateButton"
        const val InitialStateButtonTextTag = "InitialStateButton"
    }
}

package com.bumble.appyx.sandbox.client.spotlight

import android.os.Parcelable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.coroutineScope
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.routingsource.spotlight.Spotlight
import com.bumble.appyx.routingsource.spotlight.backpresshandler.GoToPrevious
import com.bumble.appyx.routingsource.spotlight.elementsCount
import com.bumble.appyx.routingsource.spotlight.hasNext
import com.bumble.appyx.routingsource.spotlight.hasPrevious
import com.bumble.appyx.routingsource.spotlight.operation.next
import com.bumble.appyx.routingsource.spotlight.operation.activate
import com.bumble.appyx.routingsource.spotlight.operation.previous
import com.bumble.appyx.routingsource.spotlight.operation.updateElements
import com.bumble.appyx.routingsource.spotlight.transitionhandler.rememberSpotlightSlider
import com.bumble.appyx.sandbox.client.child.ChildNode
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Item.C1
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Item.C2
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Item.C3
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Routing.Child1
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Routing.Child2
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Routing.Child3
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.State.Loaded
import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.State.Loading
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize

class SpotlightExampleNode(
    buildContext: BuildContext,
    private val spotlight: Spotlight<Routing> = Spotlight(
        items = emptyList(),
        savedStateMap = buildContext.savedStateMap,
        backPressHandler = GoToPrevious(),
    )
) : ParentNode<SpotlightExampleNode.Routing>(
    buildContext = buildContext,
    routingSource = spotlight
) {

    private val screenState = mutableStateOf<State?>(null)

    sealed class State {
        object Loading : State()
        object Loaded : State()
    }

    init {
        // simulate loading tabs
        if (spotlight.elementsCount() == 0) {
            screenState.value = Loading
            lifecycle.coroutineScope.launch {
                delay(2000)
                spotlight.updateElements(items = Item.getItemList())
                screenState.value = Loaded
            }
        } else {
            screenState.value = Loaded
        }
    }

    sealed class Routing : Parcelable {

        @Parcelize
        object Child1 : Routing()

        @Parcelize
        object Child2 : Routing()

        @Parcelize
        object Child3 : Routing()
    }

    @Parcelize
    private enum class Item(val routing: Routing) : Parcelable {
        C1(Child1),
        C2(Child2),
        C3(Child3);

        companion object {
            fun getItemList() = values().map { it.routing }
        }
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            Child1 -> ChildNode(name = "Child1", buildContext = buildContext)
            Child2 -> ChildNode(name = "Child2", buildContext = buildContext)
            Child3 -> ChildNode(name = "Child3", buildContext = buildContext)
        }

    @Composable
    override fun View(modifier: Modifier) {
        val state by screenState

        Box(
            modifier = modifier
                .fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            when (state) {
                is Loading -> CircularProgressIndicator()
                is Loaded -> LoadedState(modifier = modifier)
                else -> Unit
            }
        }
    }

    @Composable
    private fun LoadedState(modifier: Modifier) {
        val hasPrevious = spotlight.hasPrevious().collectAsState(initial = false)
        val hasNext = spotlight.hasNext().collectAsState(initial = false)
        Column(
            verticalArrangement = Arrangement.spacedBy(24.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = modifier,
        ) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(24.dp),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                TextButton(
                    text = "Previous",
                    enabled = hasPrevious.value
                ) {
                    spotlight.previous()
                }
                TextButton(
                    text = "Next",
                    enabled = hasNext.value
                ) {
                    spotlight.next()
                }
            }
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(24.dp),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                TextButton(
                    text = "C1",
                    enabled = true
                ) {
                    spotlight.activate(C1)
                }
                TextButton(
                    text = "C2",
                    enabled = true
                ) {
                    spotlight.activate(C2)
                }
                TextButton(
                    text = "C3",
                    enabled = true
                ) {
                    spotlight.activate(C3)
                }
            }

            Children(
                modifier = Modifier
                    .padding(top = 12.dp, bottom = 12.dp)
                    .fillMaxWidth(),
                transitionHandler = rememberSpotlightSlider(clipToBounds = true),
                routingSource = spotlight
            )

        }
    }

    @Composable
    private fun TextButton(text: String, enabled: Boolean = true, onClick: () -> Unit) {
        Button(onClick = onClick, enabled = enabled, modifier = Modifier.padding(4.dp)) {
            Text(text = text)
        }
    }

    private fun Spotlight<*>.activate(item: Item) {
        activate(item.ordinal)
    }
}

package com.bumble.appyx.sandbox.client.tiles

import android.util.Log
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.bumble.appyx.routingsource.tiles.Tiles
import com.bumble.appyx.routingsource.tiles.operation.removeSelected
import com.bumble.appyx.routingsource.tiles.operation.toggleSelection
import com.bumble.appyx.routingsource.tiles.transitionhandler.rememberTilesTransitionHandler
import com.bumble.appyx.core.composable.Child
import com.bumble.appyx.core.composable.visibleChildrenAsState
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.sandbox.client.child.ChildNode
import com.bumble.appyx.sandbox.client.tiles.TilesExampleNode.Routing
import com.bumble.appyx.sandbox.client.tiles.TilesExampleNode.Routing.Child1
import com.bumble.appyx.sandbox.client.tiles.TilesExampleNode.Routing.Child2
import com.bumble.appyx.sandbox.client.tiles.TilesExampleNode.Routing.Child3
import com.bumble.appyx.sandbox.client.tiles.TilesExampleNode.Routing.Child4

class TilesExampleNode(
    buildContext: BuildContext,
    private val tiles: Tiles<Routing> = Tiles(
        initialItems = listOf(
            Child1, Child2, Child3, Child4
        )
    ),
) : ParentNode<Routing>(
    routingSource = tiles,
    buildContext = buildContext,
) {

    enum class Routing {
        Child1, Child2, Child3, Child4,
    }

    override fun onBuilt() {
        super.onBuilt()
        whenChildrenAttached { commonLifecycle: Lifecycle, child1: ChildNode, child2: ChildNode ->
            commonLifecycle.addObserver(object : DefaultLifecycleObserver {
                override fun onCreate(owner: LifecycleOwner) {
                    Log.d("TilesExampleNode", "Children $child1 and $child2 were connected")
                }

                override fun onDestroy(owner: LifecycleOwner) {
                    Log.d("TilesExampleNode", "Children $child1 and $child2 were disconnected")
                }
            })
        }
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            Child1 -> ChildNode("1", buildContext)
            Child2 -> ChildNode("2", buildContext)
            Child3 -> ChildNode("3", buildContext)
            Child4 -> ChildNode("4", buildContext)
        }

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            modifier = modifier
                .fillMaxSize()
        ) {
            Button(
                onClick = { tiles.removeSelected() },
                modifier = Modifier.align(Alignment.TopCenter)
            ) {
                Text(text = "Remove selected")
            }

            val elements by tiles.visibleChildrenAsState()
            LazyVerticalGrid(
                columns = GridCells.Fixed(2),
                modifier = Modifier
                    .padding(top = 60.dp)
                    .fillMaxSize(),
                contentPadding = PaddingValues(horizontal = 16.dp),
            ) {

                items(elements) { element ->
                    Child(
                        routingElement = element,
                        transitionHandler = rememberTilesTransitionHandler()
                    ) { child, _ ->
                        Box(modifier = Modifier
                            .fillMaxWidth()
                            .height(150.dp)
                            .clickable {
                                tiles.toggleSelection(element.key)
                            }
                        ) {
                            child()
                        }
                    }
                }
            }

        }
    }
}

package com.bumble.appyx.testing.ui.rules

import com.bumble.appyx.testing.ui.utils.DummyParentNode
import com.bumble.appyx.core.node.ParentNodeView
import com.bumble.appyx.core.node.ViewFactory

open class AppyxParentViewTestRule<Routing : Any, View : ParentNodeView<Routing>>(
    launchActivity: Boolean = true,
    viewFactory: ViewFactory<View>
) : AppyxViewTestRule<View>(viewFactory, launchActivity) {

    override fun beforeActivityLaunched() {
        super.beforeActivityLaunched()
        runOnUiThread {
            val testNode = DummyParentNode<Routing>()
            view.init(testNode)
        }
    }
}

package com.bumble.appyx.testing.ui.rules

import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable

class AppyxViewActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val composableView = composableView
        setContent {
            requireNotNull(composableView) { "AppyxViewActivity View has not been setup" }
            composableView()
        }
    }

    companion object {
        var composableView: (@Composable () -> Unit)? = null
    }

}

package com.bumble.appyx.testing.ui.rules

import androidx.annotation.CallSuper
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.test.rule.ActivityTestRule
import com.bumble.appyx.core.node.NodeView
import com.bumble.appyx.core.node.ViewFactory
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

open class AppyxViewTestRule<View : NodeView>(
    viewFactory: ViewFactory<View>,
    private val launchActivity: Boolean = true,
    private val composeTestRule: ComposeTestRule = createEmptyComposeRule()
) : ActivityTestRule<AppyxViewActivity>(
    /* activityClass = */ AppyxViewActivity::class.java,
    /* initialTouchMode = */ true,
    /* launchActivity = */ launchActivity
), ComposeTestRule by composeTestRule {

    val view by lazy { viewFactory.invoke() }

    override fun apply(base: Statement, description: Description): Statement {
        val parent =
            TestRule { parentBase, parentDescription -> super.apply(parentBase, parentDescription) }

        return RunRules(base, listOf(composeTestRule, parent), description, ::before, ::after)
    }

    fun start() {
        require(!launchActivity) {
            "Activity will be launched automatically, launchActivity parameter was passed into constructor"
        }
        launchActivity(null)
    }

    override fun beforeActivityLaunched() {
        AppyxViewActivity.composableView =  {
            view.View(modifier = Modifier)
        }
    }

    override fun afterActivityLaunched() {
        AppyxViewActivity.composableView = null
    }

    @CallSuper
    open fun before() {

    }

    @CallSuper
    open fun after() {

    }

}

package com.bumble.appyx.testing.ui.rules

import org.junit.rules.TestRule
import kotlin.Throws
import org.junit.runner.Description
import org.junit.runners.model.Statement

internal class RunRules(
    base: Statement,
    rules: Iterable<TestRule>,
    description: Description,
    private val before: () -> Unit = {},
    private val after: () -> Unit= {}
) : Statement() {

    private val statement: Statement = applyAll(base, rules, description)

    @Throws(Throwable::class)
    override fun evaluate() {
        try {
            before.invoke()
            statement.evaluate()
        } finally {
            after.invoke()
        }
    }

    private fun applyAll(
        result: Statement,
        rules: Iterable<TestRule>,
        description: Description
    ): Statement {
        var result = result
        for (each in rules) {
            result = each.apply(result, description)
        }
        return result
    }
}

package com.bumble.appyx.testing.ui.utils

import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node

class DummyParentNode<Routing : Any> : ParentNode<Routing>(
    routingSource = DummyRoutingSource<Routing, Unit>(),
    buildContext = BuildContext.root(savedStateMap = null)
) {
    override fun resolve(routing: Routing, buildContext: BuildContext) = node(buildContext) { }
}

package com.bumble.appyx.testing.ui.utils

import com.bumble.appyx.core.routing.BaseRoutingSource
import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.onscreen.OnScreenStateResolver

class DummyRoutingSource<Routing : Any, State> : BaseRoutingSource<Routing, State>(
    savedStateMap = null,
    finalState = null,
    screenResolver = object : OnScreenStateResolver<State> {
        override fun isOnScreen(state: State) = true
    }
) {
    override val initialElements: RoutingElements<Routing, State>
        get() = emptyList()

}

package com.bumble.appyx.app.composable.graph.nodeimpl

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import com.bumble.appyx.app.composable.graph.GraphNode
import com.bumble.appyx.app.node.onboarding.screen.ApplicationTree
import com.bumble.appyx.app.ui.md_amber_300
import com.bumble.appyx.app.ui.md_blue_300
import com.bumble.appyx.app.ui.md_blue_grey_300
import com.bumble.appyx.app.ui.md_cyan_300
import com.bumble.appyx.app.ui.md_grey_300
import com.bumble.appyx.app.ui.md_indigo_300
import com.bumble.appyx.app.ui.md_light_blue_300
import com.bumble.appyx.app.ui.md_light_green_300
import com.bumble.appyx.app.ui.md_lime_300
import com.bumble.appyx.app.ui.md_pink_300
import com.bumble.appyx.app.ui.md_teal_300
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.modality.BuildContext.Companion.root
import kotlin.random.Random

private val colors = listOf(
    md_pink_300,
    md_indigo_300,
    md_blue_300,
    md_light_blue_300,
    md_cyan_300,
    md_teal_300,
    md_light_green_300,
    md_lime_300,
    md_amber_300,
    md_grey_300,
    md_blue_grey_300
)

class SimpleGraphNode(
    private val label: String,
    private val color: Color = colors[Random.nextInt(colors.size)],
    private val children: List<GraphNode> = emptyList()
) : GraphNode {

    override val isActive: MutableState<Boolean> =
        mutableStateOf(false)

    override fun children(): List<GraphNode> =
        children

    @Composable
    override fun View(modifier: Modifier) {
        val cornerRadius = 8.dp
        val horizontalPadding = 4.dp
        val internalPadding = 6.dp
        val alpha by animateFloatAsState(targetValue = if (isActive.value) 1f else 0.25f)

        Box(
            modifier = modifier
                .wrapContentSize()
                .padding(start = horizontalPadding, end = horizontalPadding)
                .background(
                    color = MaterialTheme.colors.surface,
                    shape = RoundedCornerShape(cornerRadius)
                )
                .alpha(alpha)
                .background(
                    color = color,
                    shape = RoundedCornerShape(cornerRadius)
                )
                .padding(internalPadding)
        ) {
            Text(
                text = label,
                modifier = Modifier.align(Alignment.Center),
                color = Color.Black,
                style = MaterialTheme.typography.caption
            )
        }
    }
}

@Preview
@Composable
@ExperimentalUnitApi
@ExperimentalComposeUiApi
fun SimpleGraphNodePreview() {
    Box(Modifier.fillMaxSize()) {
        NodeHost(integrationPoint = IntegrationPointStub()) {
            ApplicationTree(
                buildContext = root(null),
            )
        }
    }
}

package com.bumble.appyx.app.composable.graph.nodeimpl

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bumble.appyx.app.composable.graph.GraphNode
import com.bumble.appyx.app.composable.graph.Tree
import com.bumble.appyx.app.ui.atomic_tangerine
import com.bumble.appyx.app.ui.manatee
import com.bumble.appyx.app.ui.silver_sand
import com.bumble.appyx.app.ui.sizzling_red
import com.bumble.appyx.app.ui.md_amber_500
import com.bumble.appyx.app.ui.md_blue_500
import com.bumble.appyx.app.ui.md_blue_grey_500
import com.bumble.appyx.app.ui.md_cyan_500
import com.bumble.appyx.app.ui.md_grey_500
import com.bumble.appyx.app.ui.md_indigo_500
import com.bumble.appyx.app.ui.md_light_blue_500
import com.bumble.appyx.app.ui.md_light_green_500
import com.bumble.appyx.app.ui.md_lime_500
import com.bumble.appyx.app.ui.md_pink_500
import com.bumble.appyx.app.ui.md_teal_500
import kotlin.random.Random

class TestImpl(
    private val id: Int,
    private val childLevels: Int,
) : GraphNode {

    override val isActive: MutableState<Boolean> =
        mutableStateOf(false)

    private val colors = listOf(
        manatee,
        sizzling_red,
        atomic_tangerine,
        silver_sand,
        md_pink_500,
        md_indigo_500,
        md_blue_500,
        md_light_blue_500,
        md_cyan_500,
        md_teal_500,
        md_light_green_500,
        md_lime_500,
        md_amber_500,
        md_grey_500,
        md_blue_grey_500
    )

    private val color = colors[Random.nextInt(colors.size)]

    override fun children(): List<GraphNode> =
        if (childLevels == 0) emptyList() else listOf(
            TestImpl(1, childLevels - 1),
            TestImpl(2, childLevels - 1),
        )

    @Composable
    override fun View(modifier: Modifier) {
        Box(
            modifier = modifier
                .size(30.dp)
                .padding(top = 2.dp, start = 4.dp, end = 4.dp, bottom = 2.dp)
                .background(
                    color = color,
                    shape = RoundedCornerShape(6.dp)
                )
                .padding(4.dp)
        ) {
            androidx.compose.material.Text(
                text = "$id",
                modifier = Modifier.align(Alignment.Center),
                style = MaterialTheme.typography.caption
            )
        }
    }
}

@Preview
@Composable
@ExperimentalComposeUiApi
fun TestImplPreview() {
    Tree(TestImpl(1, 2))
}

package com.bumble.appyx.app.node.onboarding.screen

import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.ExperimentalUnitApi
import com.bumble.appyx.app.composable.Page
import com.bumble.appyx.app.composable.graph.GraphNode
import com.bumble.appyx.app.composable.graph.Tree
import com.bumble.appyx.app.composable.graph.nodeimpl.SimpleGraphNode
import com.bumble.appyx.app.ui.AppyxSampleAppTheme
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.modality.BuildContext.Companion.root
import com.bumble.appyx.core.node.Node
import kotlinx.coroutines.delay

@ExperimentalUnitApi
@ExperimentalComposeUiApi
class ApplicationTree(
    buildContext: BuildContext
) : Node(
    buildContext = buildContext,
) {
    private val o1 = SimpleGraphNode(label = "O1")
    private val o2 = SimpleGraphNode(label = "O2")
    private val o3 = SimpleGraphNode(label = "O3")
    private val onboarding = SimpleGraphNode(
        label = "Onboarding",
        children = listOf(
            o1,
            o2,
            o3,
        )
    )

    private val people = SimpleGraphNode(label = "People")
    private val chat = SimpleGraphNode(label = "Chat")
    private val messages = SimpleGraphNode(
        label = "Messages",
        children = listOf(
            people,
            chat,
        )
    )

    private val settings = SimpleGraphNode(label = "Settings")
    private val profile = SimpleGraphNode(label = "Profile")
    private val main = SimpleGraphNode(
        label = "Main",
        children = listOf(
            messages,
            profile,
            settings
        )
    )
    private val root: GraphNode = SimpleGraphNode(
        label = "Root",
        children = listOf(
            onboarding,
            main,
        )
    )

    @Composable
    override fun View(modifier: Modifier) {
        Page(
            modifier = modifier,
            title = "Nodes",
            body = "The app is organised into a tree of Nodes." +
                "\n\nNodes have @Composable UI, each have their own lifecycle on and off the screen, and can choose which of their children to delegate the control flow to."
        ) {
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .horizontalScroll(rememberScrollState()),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Tree(graphNode = root)
            }
        }

        LaunchedEffect(Unit) {
            val startDelay: Long = 500
            val intervalDelay: Long = 1200

            while (true) {
                root.isActive.value = false
                onboarding.isActive.value = false
                o1.isActive.value = false
                o2.isActive.value = false
                o3.isActive.value = false
                main.isActive.value = false
                settings.isActive.value = false
                profile.isActive.value = false
                messages.isActive.value = false
                people.isActive.value = false
                chat.isActive.value = false

                delay(startDelay)
                root.isActive.value = true
                onboarding.isActive.value = true
                o1.isActive.value = true
                delay(intervalDelay)
                o1.isActive.value = false
                o2.isActive.value = true
                delay(intervalDelay)
                o2.isActive.value = false
                o3.isActive.value = true

                delay(intervalDelay)
                o3.isActive.value = false
                onboarding.isActive.value = false
                main.isActive.value = true
                profile.isActive.value = true

                delay(intervalDelay)
                profile.isActive.value = false
                messages.isActive.value = true
                people.isActive.value = true

                delay(intervalDelay)
                people.isActive.value = false
                chat.isActive.value = true

                delay(intervalDelay)
                messages.isActive.value = false
                chat.isActive.value = false
                settings.isActive.value = true

                delay(intervalDelay * 2)
            }
        }
    }
}

@Preview
@Composable
@ExperimentalUnitApi
@ExperimentalComposeUiApi
fun ApplicationTreePreview() {
    AppyxSampleAppTheme(darkTheme = false) {
        PreviewContent()
    }
}

@Preview
@Composable
@ExperimentalUnitApi
@ExperimentalComposeUiApi
fun ApplicationTreePreviewDark() {
    AppyxSampleAppTheme(darkTheme = true) {
        PreviewContent()
    }
}

@Composable
@ExperimentalUnitApi
@ExperimentalComposeUiApi
private fun PreviewContent() {
    Surface(color = MaterialTheme.colors.background) {
        Box(Modifier.fillMaxSize()) {
            NodeHost(integrationPoint = IntegrationPointStub()) {
                ApplicationTree(
                    root(null),
                )
            }
        }
    }
}

package com.bumble.appyx.app.node.onboarding.screen

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.ExperimentalUnitApi
import com.bumble.appyx.R
import com.bumble.appyx.app.composable.Page
import com.bumble.appyx.app.ui.AppyxSampleAppTheme
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.modality.BuildContext.Companion.root
import com.bumble.appyx.core.node.Node

@ExperimentalUnitApi
class IntroScreen(
    buildContext: BuildContext,
) : Node(
    buildContext = buildContext
) {

    @Composable
    override fun View(modifier: Modifier) {
        Page(
            modifier = modifier,
            title = "Hi there!",
            body = "Appyx is an Android application framework built with love on top of Jetpack Compose."
        ) {
            val image: Painter = painterResource(id = R.drawable.appyx)
            Image(
                painter = image,
                contentDescription = "logo",
                modifier = Modifier
                    .fillMaxSize(0.65f)
                    .align(Alignment.Center)
            )
        }
    }
}

@Preview
@Composable
@ExperimentalUnitApi
fun IntroScreenPreview() {
    AppyxSampleAppTheme(darkTheme = false) {
        PreviewContent()
    }
}

@Preview
@Composable
@ExperimentalUnitApi
fun IntroScreenPreviewDark() {
    AppyxSampleAppTheme(darkTheme = true) {
        PreviewContent()
    }
}

@Composable
@ExperimentalUnitApi
private fun PreviewContent() {
    Surface(color = MaterialTheme.colors.background) {
        Box(Modifier.fillMaxSize()) {
            NodeHost(integrationPoint = IntegrationPointStub()) {
                IntroScreen(
                    root(null)
                )
            }
        }
    }
}

package com.bumble.appyx.app.node.onboarding.screen

import android.os.Parcelable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.ExperimentalUnitApi
import com.bumble.appyx.app.composable.Page
import com.bumble.appyx.app.node.onboarding.screen.RoutingSourceTeaser.Routing
import com.bumble.appyx.app.node.onboarding.screen.RoutingSourceTeaser.Routing.BackStackTeaser
import com.bumble.appyx.app.node.onboarding.screen.RoutingSourceTeaser.Routing.RandomOtherTeaser
import com.bumble.appyx.app.node.teaser.backstack.BackstackTeaserNode
import com.bumble.appyx.app.ui.AppyxSampleAppTheme
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.modality.BuildContext.Companion.root
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.activeRouting
import com.bumble.appyx.routingsource.backstack.operation.replace
import com.bumble.appyx.routingsource.backstack.transitionhandler.rememberBackstackFader
import com.bumble.appyx.app.node.teaser.promoter.PromoterTeaserNode
import kotlinx.parcelize.Parcelize

@ExperimentalUnitApi
class RoutingSourceTeaser(
    buildContext: BuildContext,
    private val backStack: BackStack<Routing> = BackStack(
        initialElement = BackStackTeaser,
        savedStateMap = buildContext.savedStateMap
    ),
) : ParentNode<Routing>(
    buildContext = buildContext,
    routingSource = backStack
) {

    sealed class Routing : Parcelable {
        @Parcelize
        object BackStackTeaser : Routing()

        @Parcelize
        object RandomOtherTeaser : Routing()
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is BackStackTeaser -> BackstackTeaserNode(buildContext)
            is RandomOtherTeaser -> PromoterTeaserNode(buildContext)
        }

    override fun onChildFinished(child: Node) {
        switchToNextExample()
    }

    private fun switchToNextExample() {
        val next = when (backStack.activeRouting) {
            is BackStackTeaser -> RandomOtherTeaser
            is RandomOtherTeaser -> BackStackTeaser
            null -> null
        }
        if (next != null) backStack.replace(next)
    }

    @Composable
    override fun View(modifier: Modifier) {
        Page(
            modifier = modifier,
            title = "Routing sources",
            body = "From simple switches to flows to back stacks to complex UI interactions, " +
                "Routing sources are a powerful concept to drive your application tree with."
        ) {
            Children(
                modifier = Modifier.fillMaxSize(),
                routingSource = backStack,
                transitionHandler = rememberBackstackFader { tween(1000) }
            )
        }
    }
}

@Preview
@Composable
@ExperimentalUnitApi
fun RoutingSourceTeaserPreview() {
    AppyxSampleAppTheme(darkTheme = false) {
        PreviewContent()
    }
}

@Preview
@Composable
@ExperimentalUnitApi
fun RoutingSourceTeaserPreviewDark() {
    AppyxSampleAppTheme(darkTheme = true) {
        PreviewContent()
    }
}

@Composable
@ExperimentalUnitApi
private fun PreviewContent() {
    Surface(color = MaterialTheme.colors.background) {
        Box(Modifier.fillMaxSize()) {
            NodeHost(integrationPoint = IntegrationPointStub()) {
                RoutingSourceTeaser(
                    root(null)
                )
            }
        }
    }
}

package com.bumble.appyx.app.node.onboarding.screen

import android.os.Parcelable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.scaleIn
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import com.bumble.appyx.app.composable.Page
import com.bumble.appyx.app.node.child.GenericChildNode
import com.bumble.appyx.app.node.onboarding.screen.StatefulNode1.Routing
import com.bumble.appyx.app.ui.AppyxSampleAppTheme
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.modality.BuildContext.Companion.root
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.routing.source.permanent.PermanentRoutingSource
import kotlinx.coroutines.delay
import kotlinx.parcelize.Parcelize

@ExperimentalUnitApi
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
class StatefulNode1(
    buildContext: BuildContext
) : ParentNode<Routing>(
    buildContext = buildContext,
    routingSource = PermanentRoutingSource(
        savedStateMap = buildContext.savedStateMap
    )
) {
    sealed class Routing : Parcelable {
        @Parcelize
        data class Child(val counterStartValue: Int) : Routing()
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is Routing.Child -> GenericChildNode(buildContext, routing.counterStartValue)
        }

    @Composable
    override fun View(modifier: Modifier) {
        Page(
            modifier = modifier,
            title = "Stateful",
            body = "Each Node on this screen has some state:" +
                "\n\n1. The counter represents data from a background process (e.g. server)." +
                "\n2. You can also tap them to change their colour. Try it!"
        ) {
            Column(Modifier.fillMaxSize()) {
                Row(
                    modifier = Modifier
                        .weight(0.5f)
                        .padding(bottom = 8.dp)
                ) {
                    ChildInABox(
                        routing = Routing.Child(BASE_COUNTER),
                        showWithDelay = BASE_DELAY,
                        modifier = Modifier
                            .weight(0.5f)
                            .padding(end = 8.dp)
                    )
                    ChildInABox(
                        routing = Routing.Child(BASE_COUNTER * 2),
                        showWithDelay = BASE_DELAY * 2,
                        modifier = Modifier
                            .weight(0.5f)
                    )
                }
                Row(
                    modifier = Modifier
                        .weight(0.5f)
                ) {
                    ChildInABox(
                        routing = Routing.Child(BASE_COUNTER * 3),
                        showWithDelay = BASE_DELAY * 3,
                        modifier = Modifier
                            .weight(0.5f)
                            .padding(end = 8.dp)
                    )
                    ChildInABox(
                        routing = Routing.Child(BASE_COUNTER * 4),
                        showWithDelay = BASE_DELAY * 4,
                        modifier = Modifier
                            .weight(0.5f)
                    )
                }
            }
        }
    }

    @Composable
    private fun ChildInABox(routing: Routing, showWithDelay: Long, modifier: Modifier) {
        PermanentChild(routing) { child ->
            Box(modifier) {
                var visible by remember { mutableStateOf(false) }
                AnimatedVisibility(
                    visible = visible,
                    enter = scaleIn()
                ) {
                    child()
                }

                LaunchedEffect(Unit) {
                    delay(showWithDelay)
                    visible = true
                }
            }
        }
    }

    companion object {
        private const val BASE_COUNTER = 100
        private const val BASE_DELAY = 200L
    }
}

@Preview
@Composable
@ExperimentalUnitApi
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
fun StatefulNode1Preview() {
    AppyxSampleAppTheme(darkTheme = false) {
        PreviewContent()
    }
}

@Preview
@Composable
@ExperimentalUnitApi
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
fun StatefulNode1PreviewDark() {
    AppyxSampleAppTheme(darkTheme = true) {
        PreviewContent()
    }
}

@Composable
@ExperimentalUnitApi
@ExperimentalAnimationApi
@ExperimentalComposeUiApi
private fun PreviewContent() {
    Surface(color = MaterialTheme.colors.background) {
        Box(Modifier.fillMaxSize()) {
            NodeHost(integrationPoint = IntegrationPointStub()) {
                StatefulNode1(
                    root(null)
                )
            }
        }
    }
}

package com.bumble.appyx.app.node.onboarding.screen

import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import com.bumble.appyx.app.composable.Page
import com.bumble.appyx.app.composable.dashedBorder
import com.bumble.appyx.app.ui.AppyxSampleAppTheme
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.IntegrationPointStub
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.modality.BuildContext.Companion.root
import com.bumble.appyx.core.node.Node
import kotlinx.coroutines.delay

@ExperimentalUnitApi
class StatefulNode2(
    buildContext: BuildContext,
) : Node(
    buildContext = buildContext
) {

    @Composable
    override fun View(modifier: Modifier) {
        Page(
            modifier = modifier,
            title = "Nodes are alive",
            body = "– even when they're not visible. " +
                "\n\nTry going back to the previous screen! " +
                "You should see that the counters kept on working in the background, and changes you made to colours are persisted."
        ) {
            val shape = RoundedCornerShape(16.dp)

            Column(
                modifier = modifier
                    .size(200.dp)
                    .align(Alignment.Center)
                    .background(
                        color = if (isSystemInDarkTheme()) Color.DarkGray else Color.LightGray,
                        shape = shape
                    )
                    .dashedBorder(
                        width = 4.dp,
                        color = if (isSystemInDarkTheme()) Color.LightGray else Color.DarkGray,
                        shape = shape,
                        on = 8.dp,
                        off = 6.dp
                    )
                    .padding(16.dp)

            ) {
                var counter by remember { mutableStateOf(105) }
                LaunchedEffect(Unit) {
                        while (true) {
                            delay(1000)
                            counter++
                        }
                }

                Text(
                    text = "Child (3456)",
                    style = MaterialTheme.typography.caption,
                    modifier = Modifier.padding(bottom = 8.dp)
                )
                Text(
                    text = "id = 3456",
                    style = MaterialTheme.typography.caption
                )
                Text(
                    text = "counter = $counter",
                    style = MaterialTheme.typography.caption
                )
                Text(
                    text = "color = #03A9F4",
                    style = MaterialTheme.typography.caption
                )
            }
        }
    }
}

@Preview
@Composable
@ExperimentalUnitApi
fun StatefulNode2Preview() {
    AppyxSampleAppTheme(darkTheme = false) {
        PreviewContent()
    }
}

@Preview
@Composable
@ExperimentalUnitApi
fun StatefulNode2PreviewDark() {
    AppyxSampleAppTheme(darkTheme = true) {
        PreviewContent()
    }
}

@Composable
@ExperimentalUnitApi
private fun PreviewContent() {
    Surface(color = MaterialTheme.colors.background) {
        Box(Modifier.fillMaxSize()) {
            NodeHost(integrationPoint = IntegrationPointStub()) {
                StatefulNode2(
                    root(null)
                )
            }
        }
    }
}

package com.bumble.appyx.app.node.teaser.backstack

import android.os.Parcelable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.lifecycle.coroutineScope
import com.bumble.appyx.app.node.child.GenericChildNode
import com.bumble.appyx.app.node.teaser.backstack.BackstackTeaserNode.Routing
import com.bumble.appyx.app.node.teaser.backstack.BackstackTeaserNode.Routing.Child
import com.bumble.appyx.app.node.teaser.backstack.transitionhandler.rememberCustomHandler
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.routingsource.backstack.operation.pop
import com.bumble.appyx.routingsource.backstack.operation.push
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import kotlin.random.Random

@ExperimentalUnitApi
class BackstackTeaserNode(
    buildContext: BuildContext,
    private val backStack: BackStack<Routing> = BackStack(
        initialElement = Child(100),
        savedStateMap = buildContext.savedStateMap
    ),
) : ParentNode<Routing>(
    buildContext = buildContext,
    routingSource = backStack
) {

    init {
        lifecycle.coroutineScope.launch {
            delay(1000)
            repeat(4) {
                backStack.push(Routing.Child((it + 2) * 100))
                delay(400)
            }
            delay(500)
            repeat(4) {
                backStack.pop()
                delay(150)
            }
            delay(1000)
            finish()
        }
    }

    sealed class Routing : Parcelable {
        @Parcelize
        data class Child(val int: Int = Random.nextInt(1000)) : Routing()
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is Routing.Child -> GenericChildNode(buildContext, routing.int)
        }

    @Composable
    override fun View(modifier: Modifier) {
        Children(
            modifier = Modifier.fillMaxSize(),
            routingSource = backStack,
            transitionHandler = rememberCustomHandler()
        )
    }
}

package com.bumble.appyx.app.node.teaser.promoter

import android.os.Parcelable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import androidx.lifecycle.coroutineScope
import com.bumble.appyx.app.node.child.GenericChildNode
import com.bumble.appyx.routingsource.promoter.routingsource.Promoter
import com.bumble.appyx.routingsource.promoter.routingsource.operation.addFirst
import com.bumble.appyx.routingsource.promoter.routingsource.operation.promoteAll
import com.bumble.appyx.routingsource.promoter.transitionhandler.rememberPromoterTransitionHandler
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.app.node.teaser.promoter.PromoterTeaserNode.Routing
import kotlin.random.Random
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize

@ExperimentalUnitApi
class PromoterTeaserNode(
    buildContext: BuildContext,
    private val promoter: Promoter<Routing> = Promoter(),
) : ParentNode<Routing>(
    buildContext = buildContext,
    routingSource = promoter
) {

    init {
        lifecycle.coroutineScope.launch {
            repeat(4) {
                promoter.addFirst(Routing.Child((it + 1) * 100))
                promoter.promoteAll()
            }
            delay(500)
            repeat(4) {
                delay(1500)
                promoter.addFirst(Routing.Child((it + 5) * 100))
                promoter.promoteAll()
            }
            finish()
        }
    }

    sealed class Routing : Parcelable {
        @Parcelize
        data class Child(val int: Int = Random.nextInt(1000)) : Routing()
    }

    override fun resolve(routing: Routing, buildContext: BuildContext): Node =
        when (routing) {
            is Routing.Child -> GenericChildNode(buildContext, routing.int)
        }

    @Composable
    override fun View(modifier: Modifier) {
        val childSize = remember { 100.dp }
        Children(
            modifier = Modifier.fillMaxSize(),
            routingSource = promoter,
            transitionHandler = rememberPromoterTransitionHandler(childSize) { spring(stiffness = Spring.StiffnessVeryLow / 4) }
        ) {
            children<Routing> { child ->
                child(Modifier.size(childSize))
            }
        }
    }
}

package com.bumble.appyx.core.routing.source.combined

import com.bumble.appyx.core.plugin.Destroyable
import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.core.routing.RoutingSource
import com.bumble.appyx.core.routing.RoutingSourceAdapter
import com.bumble.appyx.core.state.MutableSavedStateMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlin.coroutines.EmptyCoroutineContext

class CombinedRoutingSource<Routing>(
    val sources: List<RoutingSource<Routing, *>>,
) : RoutingSource<Routing, Any?>, Destroyable {

    constructor(vararg sources: RoutingSource<Routing, *>) : this(sources.toList())

    private val scope = CoroutineScope(EmptyCoroutineContext + Dispatchers.Unconfined)

    override val elements: StateFlow<RoutingElements<Routing, *>> =
        combine(sources.map { it.elements }) { arr -> arr.reduce { acc, list -> acc + list } }
            .stateIn(scope, SharingStarted.Eagerly, emptyList())

    override val screenState: StateFlow<RoutingSourceAdapter.ScreenState<Routing, *>> =
        combine(sources.map { it.screenState }) { arr ->
            RoutingSourceAdapter.ScreenState(
                onScreen = arr.flatMap { it.onScreen },
                offScreen = arr.flatMap { it.offScreen },
            )
        }
            .stateIn(scope, SharingStarted.Eagerly, RoutingSourceAdapter.ScreenState())

    override val canHandleBackPress: StateFlow<Boolean> =
        combine(sources.map { it.canHandleBackPress }) { arr -> arr.any { it } }
            .stateIn(scope, SharingStarted.Eagerly, false)

    override fun onBackPressed() {
        sources.firstOrNull { it.canHandleBackPress.value }?.onBackPressed()
    }

    override fun onTransitionFinished(key: RoutingKey<Routing>) {
        sources.forEach { it.onTransitionFinished(key) }
    }

    override fun onTransitionFinished(keys: Collection<RoutingKey<Routing>>) {
        sources.forEach { it.onTransitionFinished(keys) }
    }

    override fun saveInstanceState(state: MutableSavedStateMap) {
        sources.forEach { it.saveInstanceState(state) }
    }

    override fun destroy() {
        scope.cancel()
        sources.filterIsInstance<Destroyable>().forEach { it.destroy() }
    }

}

package com.bumble.appyx.core.routing.source.combined

import com.bumble.appyx.core.routing.RoutingSource

operator fun <Routing> RoutingSource<Routing, *>.plus(
    other: RoutingSource<Routing, *>,
): CombinedRoutingSource<Routing> {
    val currentSources = if (this is CombinedRoutingSource<Routing>) sources else listOf(this)
    val otherSources = if (other is CombinedRoutingSource<Routing>) other.sources else listOf(other)
    return CombinedRoutingSource(currentSources + otherSources)
}

package com.bumble.appyx.core.routing.source.permanent

import com.bumble.appyx.core.routing.RoutingElement

typealias PermanentElement<T> = RoutingElement<T, Int>

package com.bumble.appyx.core.routing.source.permanent

import com.bumble.appyx.core.routing.RoutingElements

typealias PermanentElements<T> = RoutingElements<T, Int>

package com.bumble.appyx.core.routing.source.permanent

import com.bumble.appyx.core.routing.Operation
import com.bumble.appyx.core.routing.RoutingElement
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.core.routing.RoutingSource
import com.bumble.appyx.core.routing.RoutingSourceAdapter
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlin.coroutines.EmptyCoroutineContext

class PermanentRoutingSource<Routing : Any>(
    routings: Set<Routing> = emptySet(),
    savedStateMap: SavedStateMap?,
    private val key: String = requireNotNull(PermanentRoutingSource::class.qualifiedName),
) : RoutingSource<Routing, Int> {
    private val scope: CoroutineScope =
        CoroutineScope(EmptyCoroutineContext + Dispatchers.Unconfined)

    constructor(
        vararg routings: Routing,
        savedStateMap: SavedStateMap?,
        key: String = requireNotNull(PermanentRoutingSource::class.qualifiedName),
    ) : this(
        routings = routings.toSet(),
        savedStateMap = savedStateMap,
        key = key
    )

    private val state = MutableStateFlow(
        savedStateMap.restore() ?: routings.map { key ->
            PermanentElement(
                key = RoutingKey(routing = key),
                fromState = 0,
                targetState = 0,
                operation = Operation.Noop()
            )
        }
    )

    override val elements: StateFlow<PermanentElements<Routing>>
        get() = state

    override val screenState: StateFlow<RoutingSourceAdapter.ScreenState<Routing, Int>>
        get() = state
            .map { RoutingSourceAdapter.ScreenState(onScreen = it) }
            .stateIn(
                scope = scope,
                started = SharingStarted.Lazily,
                initialValue = RoutingSourceAdapter.ScreenState(onScreen = state.value)
            )

    override val canHandleBackPress: StateFlow<Boolean> =
        MutableStateFlow(false)

    override fun onBackPressed() {
        // no-op
    }

    override fun onTransitionFinished(keys: Collection<RoutingKey<Routing>>) {
        // no-op
    }

    override fun accept(operation: Operation<Routing, Int>) {
        if (operation.isApplicable(state.value)) {
            state.update { operation(it) }
        }
    }

    override fun saveInstanceState(state: MutableSavedStateMap) {
        state[key] = this.state.value
    }

    private fun SavedStateMap?.restore(): List<RoutingElement<Routing, Int>>? =
        (this?.get(key) as? PermanentElements<Routing>)

}

package com.bumble.appyx.routingsource.promoter.routingsource.operation

import com.bumble.appyx.routingsource.promoter.routingsource.Promoter
import com.bumble.appyx.routingsource.promoter.routingsource.Promoter.TransitionState.CREATED
import com.bumble.appyx.routingsource.promoter.routingsource.PromoterElement
import com.bumble.appyx.routingsource.promoter.routingsource.PromoterElements
import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue

@Parcelize
data class AddFirst<T : Any>(
    private val element: @RawValue T
) : PromoterOperation<T> {

    override fun isApplicable(elements: PromoterElements<T>): Boolean =
        true

    override fun invoke(
        elements: PromoterElements<T>,
    ): RoutingElements<T, Promoter.TransitionState> {
        val new = PromoterElement(
            key = RoutingKey(element),
            fromState = CREATED,
            targetState = CREATED,
            operation = this
        )

        return listOf(new) + elements
    }
}

fun <T : Any> Promoter<T>.addFirst(element: T) {
    accept(AddFirst(element))
}

package com.bumble.appyx.routingsource.promoter.routingsource.operation

import com.bumble.appyx.routingsource.promoter.routingsource.Promoter
import com.bumble.appyx.routingsource.promoter.routingsource.PromoterElements
import com.bumble.appyx.core.routing.RoutingElements
import kotlinx.parcelize.Parcelize

@Parcelize
class PromoteAll<T : Any> : PromoterOperation<T> {

    override fun isApplicable(elements: PromoterElements<T>): Boolean =
        true

    override fun invoke(
        elements: PromoterElements<T>,
    ): RoutingElements<T, Promoter.TransitionState> =
        elements.map {
            it.transitionTo(
                targetState = it.targetState.next(),
                operation = this
            )
        }
}

fun <T : Any> Promoter<T>.promoteAll() {
    accept(PromoteAll())
}

package com.bumble.appyx.routingsource.promoter.routingsource.operation

import com.bumble.appyx.routingsource.promoter.routingsource.Promoter
import com.bumble.appyx.core.routing.Operation

sealed interface PromoterOperation<T> : Operation<T, Promoter.TransitionState>

package com.bumble.appyx.sandbox.client.interop.child

import com.badoo.ribs.core.Node
import com.badoo.ribs.core.modality.BuildParams
import com.badoo.ribs.core.view.RibView

class RibsChildNode internal constructor(buildParams: BuildParams<*>) : Node<RibView>(
    buildParams = buildParams,
    viewFactory = RibsChildViewImpl.Factory().invoke(null),
    plugins = emptyList()
)

package com.bumble.appyx.sandbox.client.interop.child

import android.view.ViewGroup
import com.badoo.ribs.core.customisation.inflate
import com.badoo.ribs.core.view.AndroidRibView
import com.badoo.ribs.core.view.RibView
import com.badoo.ribs.core.view.ViewFactory
import com.badoo.ribs.core.view.ViewFactoryBuilder
import com.bumble.appyx.R.layout

interface RibsChildView : RibView {

    interface Factory : ViewFactoryBuilder<Nothing?, RibsChildView>
}

class RibsChildViewImpl private constructor(
    override val androidView: ViewGroup
) : AndroidRibView(), RibsChildView {

    class Factory : RibsChildView.Factory {
        override fun invoke(deps: Nothing?): ViewFactory<RibsChildView> =
            ViewFactory {
                RibsChildViewImpl(androidView = it.inflate(layout.rib_child))
            }
    }
}

package com.bumble.appyx.sandbox.client.interop.parent

import com.badoo.ribs.builder.SimpleBuilder
import com.badoo.ribs.core.modality.BuildParams
import com.badoo.ribs.routing.source.backstack.BackStack
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentRib.Customisation
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentRouter.Configuration
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentRouter.Configuration.InteropNode

class RibsParentBuilder : SimpleBuilder<RibsParentRib>() {

    override fun build(buildParams: BuildParams<Nothing?>): RibsParentRib {
        val customisation = buildParams.getOrDefault(Customisation())
        val backStack = BackStack<Configuration>(
            buildParams = buildParams,
            initialConfiguration = InteropNode
        )
        val router = RibsParentRouter(buildParams = buildParams, backStack = backStack)
        val interactor = RibsParentInteractor(buildParams = buildParams, backStack = backStack)
        return node(
            buildParams = buildParams,
            customisation = customisation,
            router = router,
            interactor = interactor
        )
    }

    private fun node(
        buildParams: BuildParams<*>,
        customisation: Customisation,
        router: RibsParentRouter,
        interactor: RibsParentInteractor,
    ) = RibsParentNode(
        buildParams = buildParams,
        viewFactory = customisation.viewFactory(null),
        plugins = listOf(router, interactor)
    )
}

package com.bumble.appyx.sandbox.client.interop.parent

import androidx.lifecycle.Lifecycle
import com.badoo.mvicore.android.lifecycle.createDestroy
import com.badoo.ribs.clienthelper.interactor.Interactor
import com.badoo.ribs.core.modality.BuildParams
import com.badoo.ribs.routing.source.backstack.BackStack
import com.badoo.ribs.routing.source.backstack.operation.push
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentRouter.Configuration
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentView.Event
import io.reactivex.functions.Consumer

internal class RibsParentInteractor(
    private val backStack: BackStack<Configuration>,
    buildParams: BuildParams<*>
) : Interactor<RibsParentRib, RibsParentView>(
    buildParams = buildParams
) {

    override fun onViewCreated(view: RibsParentView, viewLifecycle: Lifecycle) {
        viewLifecycle.createDestroy {
            bind(view to Consumer<Event> {
                val current = backStack.activeConfiguration
                if (current is Configuration.InteropNode) {
                    backStack.push(Configuration.RibsNode)
                } else {
                    backStack.push(Configuration.InteropNode)
                }
            })
        }
    }
}

package com.bumble.appyx.sandbox.client.interop.parent

import com.badoo.ribs.core.Node
import com.badoo.ribs.core.Rib
import com.badoo.ribs.core.customisation.RibCustomisation
import com.badoo.ribs.core.modality.BuildParams
import com.badoo.ribs.core.plugin.Plugin
import com.badoo.ribs.core.view.ViewFactory
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentViewImpl.Factory

interface RibsParentRib : Rib {

    class Customisation(
        val viewFactory: Factory = Factory(),
        val name: String = "defaultName"
    ) : RibCustomisation
}

class RibsParentNode internal constructor(
    buildParams: BuildParams<*>,
    viewFactory: ViewFactory<RibsParentView>?,
    plugins: List<Plugin> = emptyList(),
) : Node<RibsParentView>(
    buildParams = buildParams,
    viewFactory = viewFactory,
    plugins = plugins
), RibsParentRib

package com.bumble.appyx.sandbox.client.interop.parent

import android.os.Parcelable
import com.badoo.ribs.core.modality.BuildParams
import com.badoo.ribs.routing.Routing
import com.badoo.ribs.routing.resolution.ChildResolution.Companion.child
import com.badoo.ribs.routing.resolution.Resolution
import com.badoo.ribs.routing.router.Router
import com.badoo.ribs.routing.source.backstack.BackStack
import com.bumble.appyx.interop.ribs.InteropBuilder
import com.bumble.appyx.sandbox.client.container.ContainerNode
import com.bumble.appyx.sandbox.client.interop.child.RibsChildNode
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentRouter.Configuration
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentRouter.Configuration.RibsNode
import kotlinx.android.parcel.Parcelize

internal class RibsParentRouter(
    backStack: BackStack<Configuration>,
    private val buildParams: BuildParams<*>,
) : Router<Configuration>(
    buildParams = buildParams,
    routingSource = backStack
) {

    sealed class Configuration : Parcelable {
        @Parcelize
        object InteropNode : Configuration()

        @Parcelize
        object RibsNode : Configuration()
    }

    override fun resolve(routing: Routing<Configuration>): Resolution =
        when (routing.configuration) {
            is Configuration.InteropNode ->
                child {
                    InteropBuilder(
                        nodeFactory = { buildContext ->
                            ContainerNode(buildContext)
                        }
                    ).build(it)
                }
            is RibsNode -> {
                child {
                    RibsChildNode(
                        buildParams = BuildParams(
                            payload = buildParams.payload,
                            buildContext = it
                        )
                    )
                }
            }
        }
}

package com.bumble.appyx.sandbox.client.interop.parent

import android.view.ViewGroup
import android.widget.Button
import android.widget.FrameLayout
import com.badoo.ribs.core.Node
import com.badoo.ribs.core.customisation.inflate
import com.badoo.ribs.core.view.AndroidRibView
import com.badoo.ribs.core.view.RibView
import com.badoo.ribs.core.view.ViewFactory
import com.badoo.ribs.core.view.ViewFactoryBuilder
import com.bumble.appyx.R
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentView.Event
import com.bumble.appyx.sandbox.client.interop.parent.RibsParentView.Event.SwitchClicked
import com.jakewharton.rxrelay2.PublishRelay
import io.reactivex.ObservableSource

interface RibsParentView : RibView, ObservableSource<Event> {

    interface Factory : ViewFactoryBuilder<Nothing?, RibsParentView>

    sealed class Event {
        object SwitchClicked : Event()
    }
}

class RibsParentViewImpl private constructor(
    private val events: PublishRelay<Event> = PublishRelay.create(),
    override val androidView: ViewGroup
) : AndroidRibView(), RibsParentView, ObservableSource<Event> by events {

    private val container = androidView.findViewById<FrameLayout>(R.id.child)
    private val switch = androidView.findViewById<Button>(R.id.switchButton)

    init {
        switch.setOnClickListener {
            events.accept(SwitchClicked)
        }
    }

    class Factory : RibsParentView.Factory {
        override fun invoke(deps: Nothing?): ViewFactory<RibsParentView> =
            ViewFactory {
                val view = it.inflate<ViewGroup>(R.layout.rib_root)
                RibsParentViewImpl(androidView = view)
            }
    }

    override fun getParentViewForSubtree(subtreeOf: Node<*>): ViewGroup = container

}

package com.bumble.appyx.sandbox.client.mvicoreexample.feature

import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleView.Event
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Wish

internal object EventsToWish : (Event) -> Wish? {
    override fun invoke(event: Event): Wish? =
        when (event) {
            is Event.LoadDataClicked -> Wish.LoadData
            else -> null
        }
}

package com.bumble.appyx.sandbox.client.mvicoreexample.feature

import com.badoo.mvicore.element.Actor
import com.badoo.mvicore.element.NewsPublisher
import com.badoo.mvicore.element.Reducer
import com.badoo.mvicore.feature.ActorReducerFeature
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Effect
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.News
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.State
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Wish
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Wish.ChangeState
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Wish.ChildInput
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Wish.Finish
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Wish.LoadData
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit

class MviCoreExampleFeature(initialStateName: String) :
    ActorReducerFeature<Wish, Effect, State, News>(
        initialState = State.InitialState(initialStateName),
        actor = ActorImpl,
        reducer = ReducerImpl,
        newsPublisher = NewsPublisherImpl
    ) {

    sealed class State {
        data class InitialState(val stateName: String) : State()
        object Loading : State()
        data class Loaded(val stateName: String) : State()
        object Finished : State()
    }

    sealed class Wish {
        data class ChildInput(val data: String) : Wish()
        object LoadData : Wish()
        object Finish : Wish()
        data class ChangeState(val stateName: String) : Wish()
    }

    sealed class Effect {
        data class ChangeState(val stateName: String) : Effect()
        data class ChildInput(val data: String) : Effect()
        object Loading : Effect()
        object Finished : Effect()
        object DataLoaded : Effect()
    }

    sealed class News {
        object Finished : News()
        data class StateUpdated(val message: String) : News()
    }

    private object ActorImpl : Actor<State, Wish, Effect> {
        override fun invoke(state: State, wish: Wish): Observable<Effect> =
            when (wish) {
                is ChangeState -> Effect.ChangeState(wish.stateName).toObservable()
                is Finish -> Effect.Finished.toObservable()
                is ChildInput -> Effect.ChildInput(wish.data).toObservable()
                is LoadData -> Observable.timer(2, TimeUnit.SECONDS)
                    .map<Effect> { Effect.DataLoaded }
                    .startWith(Effect.Loading)
                    .observeOn(AndroidSchedulers.mainThread())
            }
    }

    private object ReducerImpl : Reducer<State, Effect> {
        override fun invoke(state: State, effect: Effect): State =
            when (effect) {
                is Effect.ChangeState -> State.Loaded(effect.stateName)
                is Effect.Finished -> State.Finished
                is Effect.ChildInput -> State.Loaded(effect.data)
                is Effect.DataLoaded -> State.Loaded("Loaded")
                is Effect.Loading -> State.Loading
            }
    }

    private object NewsPublisherImpl : NewsPublisher<Wish, Effect, State, News> {
        override fun invoke(action: Wish, effect: Effect, state: State): News? =
            when (effect) {
                is Effect.ChangeState -> News.StateUpdated(effect.stateName)
                is Effect.Finished -> News.Finished
                else -> null
            }
    }
}

fun <T> T?.toObservable(): Observable<T> =
    if (this == null) Observable.empty() else Observable.just(this)

package com.bumble.appyx.sandbox.client.mvicoreexample.feature

import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreChildNode1.Output
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Wish

internal object OutputChild1ToWish : (Output) -> Wish {
    override fun invoke(output: Output): Wish =
        when (output) {
            is Output.Result -> Wish.ChildInput(output.data)
        }
}

package com.bumble.appyx.sandbox.client.mvicoreexample.feature

import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreChildNode2.Output
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.Wish

internal object OutputChild2ToWish : (Output) -> Wish {

    override fun invoke(output: Output): Wish =
        when (output) {
            is Output.Result -> Wish.ChildInput(output.data)
        }
}

package com.bumble.appyx.sandbox.client.mvicoreexample.feature

import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.State
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.State.Finished
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.State.InitialState
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.State.Loaded
import com.bumble.appyx.sandbox.client.mvicoreexample.feature.MviCoreExampleFeature.State.Loading

internal object StateToViewModel : (State) -> ViewModel? {

    override fun invoke(state: State): ViewModel? =
        when (state) {
            is Loading -> ViewModel.Loading
            is InitialState -> ViewModel.InitialState(state.stateName)
            is Loaded -> ViewModel.Loaded(state.stateName)
            is Finished -> null
        }
}

package com.bumble.appyx.sandbox.client.mvicoreexample.feature

sealed class ViewModel {
    data class InitialState(val stateName: String) : ViewModel()
    data class Loaded(val stateName: String) : ViewModel()
    object Loading : ViewModel()
}

package com.bumble.appyx.app.node.teaser.backstack.transitionhandler

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateOffset
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import com.bumble.appyx.routingsource.backstack.BackStack
import com.bumble.appyx.core.routing.transition.ModifierTransitionHandler
import com.bumble.appyx.core.routing.transition.TransitionDescriptor
import kotlin.math.roundToInt

@Suppress("TransitionPropertiesLabel")
class CustomBackStackTransitionHandler<T> : ModifierTransitionHandler<T, BackStack.TransitionState>() {

    override fun createModifier(
        modifier: Modifier,
        transition: Transition<BackStack.TransitionState>,
        descriptor: TransitionDescriptor<T, BackStack.TransitionState>
    ): Modifier = modifier.composed {
        val alpha = transition.animateFloat(
            transitionSpec = { spring(stiffness = Spring.StiffnessVeryLow) },
            targetValueByState = {
                when (it) {
                    BackStack.TransitionState.STASHED_IN_BACK_STACK,
                    BackStack.TransitionState.DESTROYED -> 0f
                    else -> 1f
                }
            })

        val scale = transition.animateFloat(
            transitionSpec = { spring(stiffness = Spring.StiffnessVeryLow) },
            targetValueByState = {
                when (it) {
                    BackStack.TransitionState.DESTROYED -> 10f
                    else -> 1f
                }
            })

        val offset = transition.animateOffset(
            transitionSpec = { spring(stiffness = Spring.StiffnessVeryLow) },
            targetValueByState = {
                val width = descriptor.params.bounds.width.value
                when (it) {
                    BackStack.TransitionState.CREATED -> toOutsideRight(width)
                    else -> toCenter()
                }
            })

        return@composed this
            .alpha(alpha.value)
            .scale(scale.value)
            .offset {
                IntOffset(
                    x = (offset.value.x * this.density).roundToInt(),
                    y = (offset.value.y * this.density).roundToInt()
                )
            }
    }

    private fun toOutsideRight(width: Float) = Offset(1.0f * width, 0f)

    private fun toOutsideLeft(width: Float) = Offset(-1.0f * width, 0f)

    private fun toCenter() = Offset(0f, 0f)
}

@Composable
fun <T> rememberCustomHandler(): ModifierTransitionHandler<T, BackStack.TransitionState> = remember {
    CustomBackStackTransitionHandler()
}

package com.bumble.appyx.core.routing.source.permanent.operation

import com.bumble.appyx.core.routing.RoutingElement
import com.bumble.appyx.core.routing.RoutingElements
import com.bumble.appyx.core.routing.RoutingKey
import com.bumble.appyx.core.routing.source.permanent.PermanentRoutingSource
import kotlinx.parcelize.Parcelize

@Parcelize
data class Add<T : Any>(
    private val key: RoutingKey<T>
) : PermanentOperation<T> {

    override fun isApplicable(elements: RoutingElements<T, Int>): Boolean =
        !elements.any { it.key == key }

    override fun invoke(
        elements: RoutingElements<T, Int>
    ): RoutingElements<T, Int> =
        if (elements.any { it.key == key }) {
            elements
        } else {
            elements + RoutingElement(
                key = key,
                fromState = 0,
                targetState = 0,
                operation = this,
            )
        }
}

fun <T : Any> PermanentRoutingSource<T>.add(routingKey: RoutingKey<T>) {
    accept(Add(routingKey))
}

package com.bumble.appyx.core.routing.source.permanent.operation

import com.bumble.appyx.core.routing.Operation

sealed interface PermanentOperation<T> : Operation<T, Int>