package theorycrafter.ui.widgets

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.*
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.input.pointer.isAltPressed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.*
import compose.utils.withEnabledAlpha
import theorycrafter.ui.TheorycrafterTheme
import theorycrafter.utils.thenIf
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
import kotlin.math.sign


/**
 * The slider for changing the mutated attribute value.
 */
@Composable
fun AttributeMutationSlider(
    baseValue: Double,
    highIsGood: Boolean,
    drawValueBackground: Boolean = true,
    value: Double,
    onValueChange: (Double) -> Unit,
    modifier: Modifier,
    enabled: Boolean = true,
    valueRange: ClosedRange<Double>,
    extraTickValue: Double? = null
) {
    var size by remember { mutableStateOf(IntSize.Zero) }
    val isDraggingState = remember { mutableStateOf(false) }

    val animatedValue = if (isDraggingState.value)
        value
    else key (valueRange) {  // key to prevent an animation when the attribute mutation (and thus the range) changes
        animateFloatAsState(value.toFloat()).value.toDouble()
    }

    val density = LocalDensity.current
    fun Double.scaleToDp() = with(density) { (this@scaleToDp * (size.width / valueRange.size)).roundToInt().toDp() }
    fun Double.valueToDp(): Dp {
        val unscaled = if (highIsGood)
            (this@valueToDp - valueRange.start)
        else
            (valueRange.endInclusive - this@valueToDp)
        return unscaled.scaleToDp()
    }
    fun Float.pixelToValue(): Double {
        val coef = (valueRange.size / size.width)
        return if (highIsGood)
            valueRange.start + this * coef
        else
            valueRange.endInclusive - this * coef
    }

    val baseValueDp: Dp? = if (baseValue in valueRange) baseValue.valueToDp() else null
    val extraTickValueDp: Dp? =
        if ((extraTickValue != null) && (extraTickValue in valueRange))
            extraTickValue.valueToDp()
        else
            null

    Box(
        modifier = modifier
            .onPlaced {
                size = it.size
            }
            .thenIf(enabled) {
                pointerInput(valueRange, value, onValueChange) {
                    detectTapGestures(
                        onPress = {
                            val pressX = it.x
                            val pressedValue = pressX.pixelToValue()
                            val deltaValue = valueRange.size / 20
                            val newValue = value + deltaValue * (pressedValue - value).sign
                            onValueChange(newValue.coerceIn(valueRange))
                        }
                    )
                }
            }
    ) {
        if (size == IntSize.Zero)
            return

        // The ruler
        val rulerColor = LocalContentColor.current.copy(alpha = 0.3f)
            .compositeOver(TheorycrafterTheme.colors.base().background)
        Box(Modifier
            .fillMaxWidth()
            .height(RulerThickness)
            .align(Alignment.Center)
            .background(rulerColor)
        )
        Box(Modifier
            .align(Alignment.CenterStart)
            .height(RulerEdgeHeight)
            .width(RulerThickness)
            .background(rulerColor)
        )
        Box(Modifier
            .align(Alignment.CenterEnd)
            .height(RulerEdgeHeight)
            .width(RulerThickness)
            .background(rulerColor)
        )
        if (extraTickValueDp != null) {
            Box(Modifier
                .align(Alignment.CenterStart)
                .offset(x = extraTickValueDp - RulerThickness/2)
                .height(RulerEdgeHeight)
                .width(RulerThickness)
                .background(rulerColor)
            )
        }

        // The color bar
        val sizeDp = with(density) { DpSize(size.width.toDp(), size.height.toDp()) }
        val coercedBaseValue = baseValue.coerceIn(valueRange)
        val colorBarX = min(coercedBaseValue.valueToDp(), animatedValue.valueToDp())
        val colorBarLengthDp = (animatedValue - coercedBaseValue).absoluteValue.scaleToDp()
        Box(
            modifier = Modifier
                .height(ColorBarThicknessDp)
                .offset(
                    x = colorBarX,
                    y = (sizeDp.height - ColorBarThicknessDp) / 2
                )
                .width(colorBarLengthDp)
                .thenIf(drawValueBackground) {
                    background(
                        (
                                if ((animatedValue > baseValue) == highIsGood)
                                    TheorycrafterTheme.colors.mutationColorGood
                                else
                                    TheorycrafterTheme.colors.mutationColorBad
                        ).withEnabledAlpha(enabled)
                    )
                }
        )

        val windowInfo = LocalWindowInfo.current
        val tickValues = remember(baseValueDp, extraTickValueDp) {
            listOfNotNull(baseValue, extraTickValue)
        }
        SliderThumb(
            trackWidth = size.width,
            value = if (highIsGood) animatedValue else -animatedValue,
            onValueChange = { sliderValue ->
                val newValue = if (highIsGood) sliderValue else -sliderValue
                val nearestTick = tickValues.minByOrNull { (it - newValue).absoluteValue }
                val adjustedValue = when {
                    windowInfo.keyboardModifiers.isAltPressed -> newValue
                    nearestTick == null -> newValue
                    (newValue - nearestTick).absoluteValue.scaleToDp() < TickAttractionDistance -> nearestTick
                    else -> newValue
                }
                onValueChange(adjustedValue)
            },
            valueRange = if (highIsGood) valueRange else -valueRange.endInclusive .. -valueRange.start,
            isDraggingState = isDraggingState,
            enabled = enabled
        )
    }
}


/**
 * The thumb, by which the slider can be dragged.
 */
@Composable
private fun BoxScope.SliderThumb(
    trackWidth: Int,
    value: Double,
    onValueChange: (Double) -> Unit,
    isDraggingState: MutableState<Boolean>,
    valueRange: ClosedRange<Double>,
    enabled: Boolean
) {
    val density = LocalDensity.current
    val thumbRangeSizePx = trackWidth - with(density) { ThumbWidth.toPx() }.toDouble()
    val valueRangeSize = valueRange.size
    val thumbOffsetPx = (value - valueRange.start) * (thumbRangeSizePx / valueRangeSize)

    fun Double.rawOffsetToValue(): Double {
        val unbounded = valueRange.start + this * (valueRangeSize / thumbRangeSizePx)
        return unbounded.coerceIn(valueRange)
    }

    var rawOffsetDuringDragPx by remember { mutableDoubleStateOf(0.0) }
    val draggableState = rememberDraggableState { delta ->
        rawOffsetDuringDragPx += delta
        val newValue =  rawOffsetDuringDragPx.rawOffsetToValue()
        if (newValue != value)
            onValueChange(newValue)
    }
    val draggableWidth = ThumbWidth + 2*ThumbExtraDraggableArea

    Box(
        modifier = Modifier
            .size(width = draggableWidth, height = ThumbHeight)
            .align(Alignment.CenterStart)
            .offset(
                x = with(density) {
                    (thumbOffsetPx - ThumbExtraDraggableArea.toPx()).roundToInt().toDp()
                }
            )
            .draggable(
                state = draggableState,
                orientation = Orientation.Horizontal,
                enabled = enabled,
                startDragImmediately = true,
                onDragStarted = {
                    isDraggingState.value = true
                    rawOffsetDuringDragPx = thumbOffsetPx + it.x - with(density) { draggableWidth.toPx() / 2.0 }
                    onValueChange(rawOffsetDuringDragPx.rawOffsetToValue())
                },
                onDragStopped = {
                    isDraggingState.value = false
                }
            )
            .padding(horizontal = ThumbExtraDraggableArea)
            .background(
                // composite because we can't have translucency here, or the track (and ticks) will show through
                color = TheorycrafterTheme.colors.primary(enabled = enabled)
                    .compositeOver(TheorycrafterTheme.colors.base().background),
                shape = MaterialTheme.shapes.small
            )
    )
}



/**
 * Returns the size of the given range.
 */
private val ClosedRange<Double>.size
    get() = endInclusive - start


/**
 * The thickness of lines in the slider ruler.
 */
private val RulerThickness = 2.dp


/**
 * The height of the edges of the slider ruler.
 */
private val RulerEdgeHeight = 8.dp


/**
 * The thickness of the red/green color bar.
 */
private val ColorBarThicknessDp = 4.dp


/**
 * The distance at which the thumb jumps to a tick.
 */
private val TickAttractionDistance = 4.dp


/**
 * The width of the slider thumb.
 */
val ThumbWidth = 6.dp


/**
 * The height of the slide thumb.
 */
val ThumbHeight = 20.dp


/**
 * Extra draggable area around the thumb.
 */
val ThumbExtraDraggableArea = 8.dp  // For extra draggable area around the thumb
