package de.arxsilex.maptools.converter.desktop

import androidx.compose.ui.geometry.Offset
import kotlin.math.roundToInt
import kotlin.math.sqrt

internal data class LabelPlacementCandidate(
    val labelIndex: Int,
    val position: Offset,
    val importance: Int,
)

/**
 * Orders visible labels in spatial rounds instead of consuming the map index from top to bottom.
 * The first round contributes the most important label from every screen cell; later rounds add
 * detail. The renderer can therefore stop at its budget without leaving one side of the map empty.
 */
internal fun spatiallyBalancedLabelCandidates(
    candidates: List<LabelPlacementCandidate>,
    canvasWidth: Float,
    canvasHeight: Float,
    firstRoundBudget: Int,
): List<LabelPlacementCandidate> {
    if (candidates.size < 2 || firstRoundBudget < 2) return candidates
    val width = canvasWidth.coerceAtLeast(1f)
    val height = canvasHeight.coerceAtLeast(1f)
    val budget = firstRoundBudget.coerceAtLeast(2)
    val aspect = width / height
    val columns = sqrt(budget * aspect).roundToInt().coerceIn(1, budget)
    val rows = (budget / columns).coerceAtLeast(1)
    val cells = Array(columns * rows) { mutableListOf<LabelPlacementCandidate>() }

    candidates.forEach { candidate ->
        val column = (candidate.position.x / width * columns).toInt().coerceIn(0, columns - 1)
        val row = (candidate.position.y / height * rows).toInt().coerceIn(0, rows - 1)
        cells[row * columns + column] += candidate
    }
    cells.forEachIndexed { index, cell ->
        val centerX = (index % columns + 0.5f) * width / columns
        val centerY = (index / columns + 0.5f) * height / rows
        cell.sortWith(
            compareByDescending<LabelPlacementCandidate> { it.importance }
                .thenBy {
                    val delta = it.position - Offset(centerX, centerY)
                    delta.x * delta.x + delta.y * delta.y
                }
                .thenBy { it.labelIndex },
        )
    }

    val ordered = ArrayList<LabelPlacementCandidate>(candidates.size)
    var depth = 0
    while (ordered.size < candidates.size) {
        var added = false
        cells.forEach { cell ->
            cell.getOrNull(depth)?.let {
                ordered += it
                added = true
            }
        }
        if (!added) break
        depth++
    }
    return ordered
}

/** The settled map is complete; sampling is only allowed for active pointer interaction. */
internal fun previewSamplingStep(visibleCount: Int, targetCount: Int, interactive: Boolean): Int {
    if (!interactive) return 1
    return ((visibleCount + targetCount - 1) / targetCount).coerceAtLeast(1)
}
