package de.tsschulz.tt_tagebuch.app.ui

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.window.Dialog
import androidx.compose.material.Surface
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextButton
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.runtime.LaunchedEffect
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.LinearEasing
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import android.util.Log
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.Alignment
import de.tsschulz.tt_tagebuch.shared.api.models.PredefinedActivityDto
import de.tsschulz.tt_tagebuch.shared.api.models.PredefinedActivityUpsertBody
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray

internal data class DiaryCourtStroke(
    val side: String = "VH",
    val type: String = "US",
    val target: String = "",
)

internal data class DiaryCourtDrawingData(
    val start: String = "AS2",
    val stroke: String = "VH",
    val spin: String = "",
    val target: String = "",
    val additionalStrokes: List<DiaryCourtStroke> = emptyList(),
) {
    val valid: Boolean get() = start.isNotBlank() && stroke.isNotBlank() && spin.isNotBlank() && target.isNotBlank()

    fun fullCode(): String = buildString {
        append("$start $stroke $spin")
        if (target.isNotBlank()) append(" -> $target")
        additionalStrokes.forEach { extra ->
            append(" / ${extra.side} ${extra.type} -> ${extra.target}")
        }
    }.trim()

    fun toJson(): String = buildJsonObject {
        put("selectedStartPosition", start)
        put("selectedCirclePosition", when (start) {
            "AS1" -> "top"
            "AS3" -> "bottom"
            else -> "middle"
        })
        put("strokeType", stroke)
        put("spinType", spin)
        put("targetPosition", target)
        put("exerciseCounter", 1 + additionalStrokes.size)
        put("code", fullCode())
        put("renderCode", fullCode())
        putJsonArray("additionalStrokes") {
            additionalStrokes.forEachIndexed { index, extra ->
                add(buildJsonObject {
                    put("side", extra.side)
                    put("type", extra.type)
                    put("targetPosition", extra.target)
                    put("counter", index + 2)
                })
            }
        }
    }.toString()

    companion object {
        fun fromJson(raw: String?): DiaryCourtDrawingData {
            if (raw.isNullOrBlank()) return DiaryCourtDrawingData()
            val obj = runCatching { Json.parseToJsonElement(raw) as? JsonObject }.getOrNull() ?: return DiaryCourtDrawingData()
            fun string(key: String, fallback: String) = obj[key]?.jsonPrimitive?.contentOrNull ?: fallback
            val extras = (obj["additionalStrokes"] as? JsonArray).orEmpty().mapNotNull { rawStroke ->
                val strokeObj = rawStroke as? JsonObject ?: return@mapNotNull null
                val target = strokeObj["targetPosition"]?.jsonPrimitive?.contentOrNull.orEmpty()
                if (target.isBlank()) return@mapNotNull null
                DiaryCourtStroke(
                    side = strokeObj["side"]?.jsonPrimitive?.contentOrNull ?: "VH",
                    type = strokeObj["type"]?.jsonPrimitive?.contentOrNull ?: "US",
                    target = target,
                )
            }.take(4)
            return DiaryCourtDrawingData(
                start = string("selectedStartPosition", "AS2"),
                stroke = string("strokeType", "VH"),
                spin = string("spinType", ""),
                target = string("targetPosition", ""),
                additionalStrokes = extras,
            )
        }
    }
}

@Composable
internal fun DiaryCourtDrawingDialog(
    initial: PredefinedActivityDto?,
    onDismiss: () -> Unit,
    onSave: (PredefinedActivityUpsertBody) -> Unit,
) {
    // remember state
    var name by remember(initial?.id) { mutableStateOf(initial?.name.orEmpty()) }
    var code by remember(initial?.id) { mutableStateOf(initial?.code.orEmpty()) }
    var duration by remember(initial?.id) { mutableStateOf(initial?.duration?.toString().orEmpty()) }
    var durationText by remember(initial?.id) { mutableStateOf(initial?.durationText.orEmpty()) }
    var drawing by remember(initial?.id) { mutableStateOf(DiaryCourtDrawingData.fromJson(initial?.drawingData)) }
    var nextSide by remember(initial?.id) { mutableStateOf("VH") }
    var nextType by remember(initial?.id) { mutableStateOf("US") }
    var nextTarget by remember(initial?.id) { mutableStateOf("") }

    val cfg = LocalConfiguration.current
    val maxHeight = (cfg.screenHeightDp.dp * 0.86f)
    val dialogWidth = (cfg.screenWidthDp.dp * 0.6f).coerceAtLeast(320.dp)
    val safeMaxHeight = maxHeight.coerceAtLeast(240.dp)

    Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) {
        Surface(modifier = Modifier.widthIn(min = dialogWidth, max = dialogWidth).padding(12.dp)) {
            Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
                Text("Uebungszeichnung", style = MaterialTheme.typography.h6)

                OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Name") }, singleLine = true)
                OutlinedTextField(value = code, onValueChange = { code = it }, label = { Text("Kuerzel") }, singleLine = true)

                Box(modifier = Modifier.fillMaxWidth().heightIn(min = safeMaxHeight * 2f).onGloballyPositioned { coords ->
                    Log.d(TAG, "Dialog Box size=${coords.size}")
                }) {
                    Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
                        DrawingChoiceRow("Start", listOf("AS1", "AS2", "AS3"), drawing.start) { drawing = drawing.copy(start = it) }
                        DrawingChoiceRow("Schlagseite", listOf("VH", "RH"), drawing.stroke) { drawing = drawing.copy(stroke = it) }
                        DrawingChoiceRow("Spin", listOf("US", "OS", "SS", "SUS"), drawing.spin) { drawing = drawing.copy(spin = it) }
                        DrawingChoiceRow("Ziel", (1..9).map(Int::toString), drawing.target) { drawing = drawing.copy(target = it) }
                                    var playKey by remember { mutableStateOf(0) }
                                    Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                                        OutlinedButton(onClick = { playKey += 1 }) { Text("Animation starten") }
                                    }
                                    DiaryCourtDrawingPreview(drawing, playKey = playKey)
                    }
                }

                if (drawing.additionalStrokes.isNotEmpty()) {
                    Text("Folgeschlaege", style = MaterialTheme.typography.caption, fontWeight = FontWeight.SemiBold)
                    drawing.additionalStrokes.forEachIndexed { index, extra ->
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                            Text("${index + 2}. ${extra.side} ${extra.type} -> ${extra.target}")
                            TextButton(onClick = { drawing = drawing.copy(additionalStrokes = drawing.additionalStrokes.filterIndexed { i, _ -> i != index }) }) { Text("Entfernen") }
                        }
                    }
                }

                if (drawing.additionalStrokes.size < 4) {
                    Text("Folgeschlag hinzufuegen", style = MaterialTheme.typography.caption, fontWeight = FontWeight.SemiBold)
                    DrawingChoiceRow("Seite", listOf("VH", "RH"), nextSide) { nextSide = it }
                    DrawingChoiceRow("Schlag", listOf("US", "OS", "SS", "SUS"), nextType) { nextType = it }
                    DrawingChoiceRow("Ziel", (1..9).map(Int::toString), nextTarget) { nextTarget = it }
                    OutlinedButton(enabled = nextTarget.isNotBlank(), onClick = {
                        drawing = drawing.copy(additionalStrokes = drawing.additionalStrokes + DiaryCourtStroke(nextSide, nextType, nextTarget))
                        nextTarget = ""
                    }) { Text("Folgeschlag uebernehmen") }
                }

                Text(drawing.fullCode(), style = MaterialTheme.typography.caption)

                Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                    OutlinedTextField(
                        value = durationText,
                        onValueChange = { durationText = it },
                        label = { Text("Dauer (Text)") },
                        modifier = Modifier.fillMaxWidth(),
                        singleLine = true,
                    )
                    OutlinedTextField(
                        value = duration,
                        onValueChange = { duration = it.filter(Char::isDigit) },
                        label = { Text("Min.") },
                        modifier = Modifier.fillMaxWidth(0.4f),
                        singleLine = true,
                    )
                }

                Row(modifier = Modifier.fillMaxWidth().padding(top = 8.dp), horizontalArrangement = Arrangement.End) {
                    TextButton(enabled = name.isNotBlank() && drawing.valid, onClick = {
                        onSave(
                            PredefinedActivityUpsertBody(
                                name = name.trim(),
                                code = code.trim().ifBlank { null },
                                duration = duration.toIntOrNull(),
                                durationText = durationText.trim().ifBlank { null },
                                drawingData = drawing.toJson(),
                                excludeFromStats = initial?.excludeFromStats ?: false,
                            ),
                        )
                    }) { Text("Speichern") }
                    TextButton(onClick = onDismiss) { Text("Abbrechen") }
                }
            }
        }
    }
}

@Composable
internal fun DiaryCourtDrawingViewerDialog(
    title: String,
    data: DiaryCourtDrawingData,
    onDismiss: () -> Unit,
) {
    val cfg = LocalConfiguration.current
    val maxHeight = (cfg.screenHeightDp.dp * 0.86f)
    val dialogWidth = (cfg.screenWidthDp.dp * 0.6f).coerceAtLeast(320.dp)
    val safeMaxHeight = maxHeight.coerceAtLeast(240.dp)

    Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) {
        Surface(modifier = Modifier.widthIn(min = dialogWidth, max = dialogWidth).padding(12.dp)) {
            Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
                Text(title, style = MaterialTheme.typography.h6)
                var playKey by remember { mutableStateOf(0) }
                Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                    OutlinedButton(onClick = { playKey += 1 }) { Text("Animation starten") }
                }
                DiaryCourtDrawingPreview(data, playKey = playKey)
                Text(data.fullCode(), style = MaterialTheme.typography.body2)
                Row(modifier = Modifier.fillMaxWidth().padding(top = 8.dp), horizontalArrangement = Arrangement.End) {
                    TextButton(onClick = onDismiss) { Text("Schliessen") }
                }
            }
        }
    }
}

@Composable
private fun DrawingChoiceRow(label: String, values: List<String>, selected: String, select: (String) -> Unit) {
    Column {
        Text(label, style = MaterialTheme.typography.caption, fontWeight = FontWeight.SemiBold)
        Row(
            modifier = Modifier.horizontalScroll(rememberScrollState()),
            horizontalArrangement = Arrangement.spacedBy(4.dp),
        ) {
            values.forEach { value ->
                OutlinedButton(onClick = { select(value) }) {
                    Text(if (selected == value) "[$value]" else value)
                }
            }
        }
    }
}

@Composable
internal fun DiaryCourtDrawingPreview(data: DiaryCourtDrawingData, modifier: Modifier = Modifier, playKey: Int = 0) {
    val cfg = LocalConfiguration.current
    val dialogWidthDp = (cfg.screenWidthDp.dp * 0.6f).coerceAtLeast(320.dp)
    val canvasPadding = 10.dp
    val canvasWidth = dialogWidthDp - (canvasPadding * 2)
    val aspect = 0.45f
    val canvasHeight = (canvasWidth * aspect)

    // show static final view on open (no animation) — animation runs only when playKey>0
    val animationProgress = remember { Animatable(1f) }
    LaunchedEffect(playKey) {
        if (playKey > 0 && data.target.isNotBlank()) {
            animationProgress.snapTo(0f)
            // much slower overall but with constant speed (linear easing)
            animationProgress.animateTo(1f, animationSpec = tween(durationMillis = 4800, easing = LinearEasing))
        }
    }

    Canvas(
        modifier = modifier
            .width(canvasWidth)
            .height(canvasHeight)
            .onGloballyPositioned { coords -> Log.d(TAG, "Canvas px size=${coords.size}") }
            .background(Color(0xFFF0F4F1))
            .padding(canvasPadding),
    ) {
        if (size.width <= 0f || size.height <= 0f) return@Canvas
        val left = size.width * 0.15f
        val top = size.height * 0.10f
        val right = size.width * 0.92f
        val bottom = size.height * 0.90f
        val midX = (left + right) / 2f
        val midY = (top + bottom) / 2f
        drawRect(Color(0xFF315F43), topLeft = Offset(left, top), size = Size(right - left, bottom - top))
        drawRect(Color.White, topLeft = Offset(left, top), size = Size(right - left, bottom - top), style = Stroke(width = 8f))
        drawLine(Color.White, Offset(midX, top - 5f), Offset(midX, bottom + 5f), strokeWidth = 8f)
        drawLine(Color(0xFFD9E4DD), Offset(left, midY), Offset(right, midY), strokeWidth = 4f)

        // original mappings (left/right matrices)
        // left-of-pipe matrix:
        // 3 6 9
        // 2 5 8
        // 1 4 7
        val originalLeft = mapOf(
            1 to (0 to 2), 2 to (0 to 1), 3 to (0 to 0),
            4 to (1 to 2), 5 to (1 to 1), 6 to (1 to 0),
            7 to (2 to 2), 8 to (2 to 1), 9 to (2 to 0),
        )
        // right-of-pipe matrix:
        // 7 4 1
        // 8 5 2
        // 9 6 3
        val originalRight = mapOf(
            1 to (2 to 0), 2 to (2 to 1), 3 to (2 to 2),
            4 to (1 to 0), 5 to (1 to 1), 6 to (1 to 2),
            7 to (0 to 0), 8 to (0 to 1), 9 to (0 to 2),
        )

        // Flip the matrices as requested: swap which map is used for onRight
        val mapOnRightTrue = originalRight
        val mapOnRightFalse = originalLeft

        fun targetPoint(target: String, onRight: Boolean): Offset {
            val number = target.toIntOrNull() ?: 5
            val (colIndex, rowIndex) = if (onRight) {
                mapOnRightTrue[number] ?: (1 to 1)
            } else {
                mapOnRightFalse[number] ?: (1 to 1)
            }

            val halfLeft = if (onRight) midX else left
            val halfRight = if (onRight) right else midX

            return Offset(
                halfLeft + (halfRight - halfLeft) * (0.20f + colIndex * 0.30f),
                top + (bottom - top) * (0.18f + rowIndex * 0.32f),
            )
        }

        val startY = when (data.start) {
            "AS1" -> top + (bottom - top) * 0.18f
            "AS3" -> bottom - (bottom - top) * 0.18f
            else -> midY
        }
        // circle center left of the table
        val circleCenter = Offset(left - 14f, startY)
        drawCircle(Color(0xFFC84C32), radius = 8f, center = circleCenter)
        // compute arrow start point similar to web: offset horizontally and vertically depending on stroke side
        val isVH = (data.stroke == "VH")
        val start = Offset(
            circleCenter.x + (if (isVH) 13f else 18f),
            circleCenter.y + (if (isVH) 8f else 0f)
        )
        if (data.target.isNotBlank()) {
            // prepare segments and debug info (target number, side, row/col)
            val segments = mutableListOf<Pair<Offset, Offset>>()
            val segmentInfo = mutableListOf<String>()
            fun computeRowCol(numberStr: String, onRight: Boolean): Pair<Int, Int> {
                val number = numberStr.toIntOrNull() ?: 5
                val (col, row) = if (onRight) {
                    // use same flipped maps as targetPoint
                    (mapOnRightTrue[number] ?: (1 to 1))
                } else {
                    (mapOnRightFalse[number] ?: (1 to 1))
                }
                return row to col
            }

            var prev = start
            val mainNum = data.target
            var t = targetPoint(mainNum, true)
            segments.add(prev to t)
            val (mainRow, mainCol) = computeRowCol(mainNum, true)
            segmentInfo.add("main num=$mainNum onRight=true row=$mainRow col=$mainCol")
            prev = t
            data.additionalStrokes.forEachIndexed { index, extra ->
                val onRight = index % 2 != 0
                t = targetPoint(extra.target, onRight)
                segments.add(prev to t)
                val (r, c) = computeRowCol(extra.target, onRight)
                segmentInfo.add("extra#${index + 1} num=${extra.target} onRight=$onRight row=$r col=$c")
                prev = t
            }

            val lengths = segments.map { (s, e) ->
                val dx = (e.x - s.x)
                val dy = (e.y - s.y)
                kotlin.math.sqrt(dx * dx + dy * dy)
            }
            val totalLen = lengths.sum().coerceAtLeast(1f)
            val drawUpTo = animationProgress.value * totalLen

            var drawn = 0f
            // debug log coordinates
            Log.d(TAG, "Segments count=${segments.size}")
            for (i in segments.indices) {
                val (s, e) = segments[i]
                val len = lengths[i].coerceAtLeast(1f)
                val color = when (i) {
                    0 -> Color(0xFFC84C32)
                    else -> {
                        val extraColors = listOf(Color(0xFF1F6A8A), Color(0xFF4F7F32), Color(0xFF8A5A1F))
                        extraColors[(i - 1) % extraColors.size]
                    }
                }
                if (drawn + len <= drawUpTo) {
                    // if segment goes from right->left, shift up by half the stroke width
                    val strokeW = 10f
                    val shiftY = strokeW / 2f
                    val sAdj = if (e.x < s.x) Offset(s.x, s.y - shiftY) else s
                    val eAdj = if (e.x < s.x) Offset(e.x, e.y - shiftY) else e
                    drawCourtArrow(sAdj, eAdj, color)
                    drawCircle(Color(0xFF90AF55), radius = if (i == 0) 9f else 7f, center = eAdj)
                    drawn += len
                    val info = segmentInfo.getOrNull(i) ?: "n/a"
                    Log.d(TAG, "Segment $i full: s=${s.x.toInt()},${s.y.toInt()} e=${e.x.toInt()},${e.y.toInt()} len=${len.toInt()} info=$info")
                } else if (drawn < drawUpTo) {
                    val remain = (drawUpTo - drawn) / len
                    val mid = Offset(s.x + (e.x - s.x) * remain, s.y + (e.y - s.y) * remain)
                    val path = Path().apply {
                        val strokeW = 10f
                        val shiftY = strokeW / 2f
                        val sAdj = if (e.x < s.x) Offset(s.x, s.y - shiftY) else s
                        val midAdj = if (e.x < s.x) Offset(mid.x, mid.y - shiftY) else mid
                        moveTo(sAdj.x, sAdj.y)
                        lineTo(midAdj.x, midAdj.y)
                    }
                    drawPath(path, color, style = Stroke(width = 10f, cap = StrokeCap.Round))
                    val midForCircle = if (e.x < s.x) Offset(mid.x, mid.y - (10f / 2f)) else mid
                    drawCircle(Color(0xFF90AF55), radius = 6f, center = midForCircle)
                    val info = segmentInfo.getOrNull(i) ?: "n/a"
                    Log.d(TAG, "Segment $i partial: s=${s.x.toInt()},${s.y.toInt()} mid=${mid.x.toInt()},${mid.y.toInt()} len=${len.toInt()} progress=${animationProgress.value} info=$info")
                    break
                } else {
                    break
                }
            }
        }
    }
}

private fun DrawScope.drawCourtArrow(start: Offset, target: Offset, color: Color) {
    val strokeW = 10f
    val shiftY = strokeW / 2f
    // if drawing right->left, shift both points up by half the stroke width
    val s = if (target.x < start.x) Offset(start.x, start.y - shiftY) else start
    val t = if (target.x < start.x) Offset(target.x, target.y - shiftY) else target
    val path = Path().apply {
        moveTo(s.x, s.y)
        lineTo(t.x, t.y)
    }
    drawPath(path, color, style = Stroke(width = strokeW, cap = StrokeCap.Round))
    val vector = t - s
    val len = kotlin.math.sqrt(vector.x * vector.x + vector.y * vector.y).coerceAtLeast(1f)
    val unit = Offset(vector.x / len, vector.y / len)
    val perpendicular = Offset(-unit.y, unit.x)
    val head = Path().apply {
        moveTo(t.x, t.y)
        lineTo(t.x - unit.x * 16f + perpendicular.x * 7f, t.y - unit.y * 16f + perpendicular.y * 7f)
        lineTo(t.x - unit.x * 16f - perpendicular.x * 7f, t.y - unit.y * 16f - perpendicular.y * 7f)
        close()
    }

    drawPath(head, color)
}

private const val TAG = "DiaryCourtDrawing"
