package compose.widgets

import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
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.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex


/**
 * The coefficient that slows down the appearance of the shadow.
 */
private const val ShadowScrollCoefficient = 3


/**
 * Converts a scroll value to a shadow elevation.
 */
private fun scrollOffsetToShadowElevation(scrollOffset: Int, density: Density): Dp {
    return with(density) {
        (scrollOffset / ShadowScrollCoefficient).toDp().coerceIn(0.dp .. MaximumElevationDp.dp)
    }
}


/**
 * Draws a shadow at the top and/or bottom of the containing box, to indicate scrolling.
 */
@Composable
fun BoxScope.ScrollShadow(
    scrollState: ScrollState,
    top: Boolean = false,
    bottom: Boolean = false
) {
    if (top)
        TopScrollShadow(scrollState)
    if (bottom)
        BottomScrollShadow(scrollState)
}


/**
 * Draws a shadow at the top and/or bottom of the containing box, to indicate scrolling.
 */
@Composable
fun BoxScope.ScrollShadow(
    listState: LazyListState,
    top: Boolean = false,
    bottom: Boolean = false
) {
    if (top)
        TopScrollShadow(listState)
    if (bottom)
        BottomScrollShadow(listState)
}


/**
 * Draws a shadow at the top of the containing box to indicate scrolling.
 */
@Composable
fun BoxScope.TopScrollShadow(scrollState: ScrollState) {
    val density = LocalDensity.current
    val elevation by remember(scrollState, density) {
        derivedStateOf {
            scrollOffsetToShadowElevation(scrollState.value, density)
        }
    }

    TopScrollShadow(elevation)
}


/**
 * Draws a shadow at the bottom of the containing box to indicate scrolling.
 */
@Composable
fun BoxScope.BottomScrollShadow(scrollState: ScrollState) {
    val density = LocalDensity.current
    val elevation by remember(scrollState, density) {
        derivedStateOf {
            scrollOffsetToShadowElevation(scrollState.maxValue - scrollState.value, density)
        }
    }

    BottomScrollShadow(elevation)
}


/**
 * Draws a shadow at the top of the containing box to indicate scrolling.
 */
@Composable
fun BoxScope.TopScrollShadow(listState: LazyListState) {
    val density = LocalDensity.current
    val elevation by remember(listState, density) {
        derivedStateOf {
            // This is wrong if the first item is smaller than the
            // smallest scroll that causes the maximum shadow
            if (listState.firstVisibleItemIndex >= 1)
                MaximumElevationDp.dp
            else
                scrollOffsetToShadowElevation(listState.firstVisibleItemScrollOffset, density)
        }
    }

    TopScrollShadow(elevation)
}


/**
 * Draws a shadow at the bottom of the containing box to indicate scrolling.
 */
@Composable
fun BoxScope.BottomScrollShadow(listState: LazyListState) {
    val density = LocalDensity.current
    val elevation by remember(listState, density) {
        derivedStateOf {
            // This is wrong if the first item is smaller than the
            // smallest scroll that causes the maximum shadow
            with(listState.layoutInfo) {
                val lastVisibleItemInfo = visibleItemsInfo.lastOrNull() ?: return@derivedStateOf 0.dp
                if (lastVisibleItemInfo.index < totalItemsCount - 1)
                    MaximumElevationDp.dp
                else {
                    scrollOffsetToShadowElevation(
                        scrollOffset = lastVisibleItemInfo.offset + lastVisibleItemInfo.size - viewportSize.height,
                        density = density
                    )
                }
            }
        }
    }

    BottomScrollShadow(elevation)
}


/**
 * Draws a shadow at the top of the containing box, with the given elevation.
 */
@Composable
fun BoxScope.TopScrollShadow(elevation: Dp) {
    Box(
        Modifier
            .matchParentSize()
            .zIndex(Float.MAX_VALUE)
    ) {
        Box(
            Modifier
                .fillMaxWidth()
                .height(1.dp)
                .offset(y = (-1).dp)
                .clip(OpenBottomShape)
                .shadow(elevation)
        )
    }
}


/**
 * Draws a shadow at the bottom of the containing box, with the given elevation.
 */
@Composable
fun BoxScope.BottomScrollShadow(elevation: Dp) {
    Box(
        Modifier
            .matchParentSize()
            .zIndex(5f)
    ) {
        Box(
            Modifier
                .fillMaxWidth()
                .align(Alignment.BottomStart)
                .height(1.dp)
                .offset(y = 1.dp)
                .clip(OpenTopShape)
                .graphicsLayer {
                    // The shadow is smaller at the top, so instead of displaying the top shadow, we display
                    // the bottom shadow by rottating it 180 degrees.
                    shadowElevation = elevation.toPx()
                    shape = shape
                    clip = clip
                    ambientShadowColor = Color.Black
                    spotShadowColor = Color.Black
                    rotationZ = 180f
                }
        )
    }
}



/**
 * The maximum elevation of the shadow, in [Dp].
 */
private const val MaximumElevationDp = 3


/**
 * A shape that clips the box such that only the shadow at the bottom is drawn.
 */
private val OpenBottomShape = GenericShape { size, _ ->
    moveTo(size.width, size.height)
    lineTo(size.width, Float.MAX_VALUE)
    lineTo(0f, Float.MAX_VALUE)
    lineTo(0f, size.height)
}


/**
 * A shape that clips the box such that only the shadow at the top is drawn.
 */
private val OpenTopShape = GenericShape { size, _ ->
    moveTo(0f, 0f)
    lineTo(0f, -Float.MAX_VALUE)
    lineTo(size.width, -Float.MAX_VALUE)
    lineTo(size.width, 0f)
}
