package space.kscience.maps.features

import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.DrawScopeMarker
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.DpRect
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.sample
import space.kscience.attributes.Attributes
import space.kscience.attributes.plus
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

/**
 * An extension of [DrawScope] to include map-specific features
 */
@DrawScopeMarker
public abstract class FeatureDrawScope<T : Any>(
    public val state: CanvasState<T>,
) : DrawScope {
    public fun Offset.toCoordinates(): T = with(state) {
        toCoordinates(this@toCoordinates, this@FeatureDrawScope)
    }

    public open fun T.toOffset(): Offset = with(state) {
        toOffset(this@toOffset, this@FeatureDrawScope)
    }

    public fun Rectangle<T>.toDpRect(): DpRect = with(state) { toDpRect() }

    public abstract fun painterFor(feature: PainterFeature<T>): Painter

    public abstract fun drawText(text: String, position: Offset, attributes: Attributes)
}

/**
 * Default implementation of FeatureDrawScope to be used in Compose (both schemes and Maps)
 */
@DrawScopeMarker
public class ComposeFeatureDrawScope<T : Any>(
    drawScope: DrawScope,
    state: CanvasState<T>,
    private val painterCache: Map<PainterFeature<T>, Painter>,
    private val textMeasurer: TextMeasurer?,
) : FeatureDrawScope<T>(state), DrawScope by drawScope {
    override fun drawText(text: String, position: Offset, attributes: Attributes) {
        try {
            //TODO don't draw text that is not on screen
            drawText(textMeasurer ?: error("Text measurer not defined"), text, position)
        } catch (ex: Exception) {
            logger.error(ex) { "Failed to measure text" }
        }
    }

    override fun painterFor(feature: PainterFeature<T>): Painter =
        painterCache[feature] ?: error("Can't resolve painter for $feature")

    public companion object {
        private val logger = KotlinLogging.logger("ComposeFeatureDrawScope")
    }
}


/**
 * Create a canvas with extended functionality (e.g., drawing text)
 */
@OptIn(FlowPreview::class)
@Composable
public fun <T : Any> FeatureCanvas(
    state: CanvasState<T>,
    featureFlow: StateFlow<Map<String, Feature<T>>>,
    modifier: Modifier = Modifier,
    sampleDuration: Duration = 20.milliseconds,
    draw: FeatureDrawScope<T>.() -> Unit = {},
) {
    val textMeasurer = rememberTextMeasurer(0)

    val features by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value)

    val painterCache = features.values
        .filterIsInstance<PainterFeature<T>>()
        .associateWith { it.getPainter() }


    Canvas(modifier) {
        if (state.canvasSize != size.toDpSize()) {
            state.canvasSize = size.toDpSize()
        }
        clipRect {
            ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply {

                val attributesCache = mutableMapOf<List<String>, Attributes>()

                fun computeGroupAttributes(path: List<String>): Attributes = attributesCache.getOrPut(path) {
                    if (path.isEmpty()) return Attributes.EMPTY
                    else if (path.size == 1) {
                        features[path.first()]?.attributes ?: Attributes.EMPTY
                    } else {
                        computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes
                            ?: Attributes.EMPTY)
                    }
                }

                features.entries.sortedBy { it.value.z }
                    .filter { state.viewPoint.zoom in it.value.zoomRange }
                    .forEach { (id, feature) ->
                        val path = id.split("/")
                        drawFeature(feature, computeGroupAttributes(path.dropLast(1)))
                    }
            }
        }
        state.selectRect?.let { dpRect ->
            val rect = dpRect.toRect()
            drawRect(
                color = Color.Blue,
                topLeft = rect.topLeft,
                size = rect.size,
                alpha = 0.5f,
                style = Stroke(
                    width = 2f,
                    pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
                )
            )
        }
    }
}