package de.arxsilex.maptools.converter.desktop

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.ScrollbarStyle
import androidx.compose.foundation.focusable
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.input.pointer.isTertiaryPressed
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.arxsilex.maptools.converter.core.InputFileInfo
import de.arxsilex.maptools.converter.core.GeoBounds
import de.arxsilex.maptools.converter.core.CircleSelection
import de.arxsilex.maptools.converter.core.GeoRegionMetrics
import de.arxsilex.maptools.converter.core.GeoRectangleRegion
import de.arxsilex.maptools.converter.core.MapSelectionShape
import de.arxsilex.maptools.converter.core.NormalizedMapArea
import de.arxsilex.maptools.converter.core.RectangleSelection
import de.arxsilex.maptools.converter.core.SelectionCorner
import de.arxsilex.maptools.converter.core.greatCircleKilometers
import de.arxsilex.maptools.converter.core.metrics
import de.arxsilex.maptools.converter.core.toGeoRegions
import de.arxsilex.maptools.converter.core.unionAreaSquareKilometers
import de.arxsilex.maptools.converter.core.elevation.ElevationPreviewRaster
import de.arxsilex.maptools.converter.core.preview.PreviewDataset
import de.arxsilex.maptools.converter.core.preview.PreviewFeatureClass
import de.arxsilex.maptools.converter.core.preview.PreviewLabelClass
import de.arxsilex.maptools.converter.core.preview.PreviewLabel
import kotlin.math.PI
import kotlin.math.ceil
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.roundToInt
import kotlin.math.sqrt
import java.util.Locale
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.skia.ColorAlphaType
import org.jetbrains.skia.ColorType
import org.jetbrains.skia.Image
import org.jetbrains.skia.ImageInfo

@Composable
internal fun AreaSelectionStep(
    inputFiles: List<InputFileInfo>,
    sourceBounds: GeoBounds,
    previewState: PreviewUiState,
    selectedAreas: List<MapSelectionShape>,
    onAreasChange: (List<MapSelectionShape>) -> Unit,
    detailStatus: String?,
    detailSources: List<PreviewDetailSource>,
    onDetailRequest: (GeoBounds, Float) -> Unit,
    onBack: () -> Unit,
    onNext: () -> Unit,
) {
    val strings = LocalAppStrings.current
    val sourceKey = inputFiles.joinToString("|") { it.path }
    val mapProjection = remember(sourceBounds) { MapCanvasProjection(sourceBounds) }
    var interactionMode by remember(sourceKey) { mutableStateOf(MapInteractionMode.EDIT) }
    var viewport by remember(sourceKey) { mutableStateOf(MapViewport.Full) }
    var canvasAspect by remember(sourceKey) { mutableStateOf(1f) }
    var visibleLayers by remember(sourceKey) { mutableStateOf(PreviewLayerVisibility()) }
    var activeAreaIndex by remember(sourceKey) { mutableIntStateOf(-1) }
    var selectionHistory by remember(sourceKey) { mutableStateOf(AreaSelectionHistory()) }
    var searchQuery by remember(sourceKey) { mutableStateOf("") }
    var databaseSearchResults by remember(sourceKey) { mutableStateOf<List<PreviewLabel>>(emptyList()) }
    var markedLabel by remember(sourceKey) { mutableStateOf<PreviewLabel?>(null) }
    val readyPreview = previewState as? PreviewUiState.Ready
    val readyDataset = readyPreview?.dataset
    val sourceSizeBytes = inputFiles.sumOf(InputFileInfo::sizeBytes)
    val geographicRegions = remember(sourceBounds, selectedAreas) { selectedAreas.toGeoRegions(sourceBounds) }
    val areaMetrics = remember(geographicRegions) { geographicRegions.map { it.metrics() } }
    val totalAreaSquareKilometers = remember(sourceBounds, selectedAreas) {
        geographicRegions.unionAreaSquareKilometers(sourceBounds)
    }
    val selectionEstimates = remember(readyPreview, selectedAreas, sourceSizeBytes) {
        readyPreview?.let { ready ->
            PreviewSelectionEstimator.estimate(
                dataset = ready.dataset,
                renderIndex = ready.renderIndex,
                shapes = selectedAreas,
                sourceBounds = sourceBounds,
                sourceSizeBytes = sourceSizeBytes,
            )
        }
    }
    LaunchedEffect(searchQuery, detailSources) {
        databaseSearchResults = emptyList()
        if (searchQuery.trim().length >= 2 && detailSources.any { it.poiPath != null }) {
            delay(180)
            databaseSearchResults = runCatching {
                withContext(Dispatchers.IO) { PreviewSearch.findPoiDatabases(detailSources, searchQuery) }
            }.getOrDefault(emptyList())
        }
    }
    val searchResults = remember(readyDataset, searchQuery, databaseSearchResults) {
        (readyDataset?.let { PreviewSearch.find(it, searchQuery, 24) }.orEmpty() + databaseSearchResults)
            .distinctBy { listOf(it.text, it.latitude, it.longitude) }
            .take(24)
    }
    LaunchedEffect(selectedAreas.size) {
        activeAreaIndex = when {
            selectedAreas.isEmpty() -> -1
            activeAreaIndex !in selectedAreas.indices -> selectedAreas.lastIndex
            else -> activeAreaIndex
        }
    }
    LaunchedEffect(viewport, canvasAspect) {
        val zoom = 1f / viewport.width
        if (zoom >= 7f) {
            delay(260)
            val visibleArea = MapCanvasTransform(mapProjection, viewport, canvasAspect * 1_000f, 1_000f)
                .visibleNormalizedArea()
            onDetailRequest(sourceBounds.subset(visibleArea), zoom)
        }
    }

    fun replaceAreas(next: List<MapSelectionShape>) {
        selectionHistory = selectionHistory.record(selectedAreas, next)
        onAreasChange(next)
    }

    fun undoSelection() {
        selectionHistory.undo(selectedAreas)?.let { step ->
            selectionHistory = step.history
            onAreasChange(step.areas)
        }
    }

    fun redoSelection() {
        selectionHistory.redo(selectedAreas)?.let { step ->
            selectionHistory = step.history
            onAreasChange(step.areas)
        }
    }
    Column(
        modifier = Modifier.fillMaxSize().padding(32.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp),
    ) {
        ScreenTitle(
            title = strings["step.area"],
            description = strings["area.description"],
        )
        Row(
            modifier = Modifier.fillMaxWidth().weight(1f),
            horizontalArrangement = Arrangement.spacedBy(20.dp),
        ) {
            AreaDetails(
                inputFiles = inputFiles,
                sourceBounds = sourceBounds,
                selectedAreas = selectedAreas,
                areaMetrics = areaMetrics,
                totalAreaSquareKilometers = totalAreaSquareKilometers,
                selectionEstimates = selectionEstimates,
                activeAreaIndex = activeAreaIndex,
                onActiveAreaChange = { activeAreaIndex = it },
                onShowArea = { index ->
                    selectedAreas.getOrNull(index)?.let { area ->
                        activeAreaIndex = index
                        viewport = MapViewport.focusedOn(area.normalizedEnvelope(sourceBounds))
                    }
                },
                viewport = viewport,
                mapProjection = mapProjection,
                canvasAspect = canvasAspect,
                visibleLayers = visibleLayers,
                onVisibleLayersChange = { visibleLayers = it },
                onAreaChange = { area ->
                    if (activeAreaIndex in selectedAreas.indices) {
                        replaceAreas(selectedAreas.toMutableList().apply { this[activeAreaIndex] = area })
                    }
                },
                searchQuery = searchQuery,
                onSearchQueryChange = { searchQuery = it },
                searchResults = searchResults,
                onSearchResultSelected = { label ->
                    markedLabel = label
                    val x = ((label.longitude - sourceBounds.west) / (sourceBounds.east - sourceBounds.west)).toFloat()
                    val y = ((sourceBounds.north - label.latitude) / (sourceBounds.north - sourceBounds.south)).toFloat()
                    viewport = MapViewport.centeredAt(x, y, 0.025f)
                },
                markedLabel = markedLabel,
                onAddCircleAtMarkedLabel = { radiusKilometers ->
                    markedLabel?.let { label ->
                        val centerX = ((label.longitude - sourceBounds.west) /
                            (sourceBounds.east - sourceBounds.west)).toFloat().coerceIn(0f, 1f)
                        val centerY = ((sourceBounds.north - label.latitude) /
                            (sourceBounds.north - sourceBounds.south)).toFloat().coerceIn(0f, 1f)
                        replaceAreas(selectedAreas + CircleSelection(centerX, centerY, radiusKilometers))
                        activeAreaIndex = selectedAreas.size
                    }
                },
                detailStatus = detailStatus,
                modifier = Modifier.width(400.dp).fillMaxHeight(),
            )
            Column(
                modifier = Modifier.weight(1f).fillMaxHeight(),
                verticalArrangement = Arrangement.spacedBy(6.dp),
            ) {
                SelectionCanvas(
                    sourceBounds = sourceBounds,
                    mapProjection = mapProjection,
                    selectedAreas = selectedAreas,
                    activeAreaIndex = activeAreaIndex,
                    onActiveAreaChange = { activeAreaIndex = it },
                    previewState = previewState,
                    interactionMode = interactionMode,
                    onAreaAdded = { interactionMode = MapInteractionMode.EDIT },
                    onSelectionCommitted = { before, after ->
                        selectionHistory = selectionHistory.record(before, after)
                    },
                    onDeleteActiveArea = {
                        if (activeAreaIndex in selectedAreas.indices) {
                            replaceAreas(selectedAreas.filterIndexed { index, _ -> index != activeAreaIndex })
                        }
                    },
                    onUndo = ::undoSelection,
                    onRedo = ::redoSelection,
                    onCancelTool = { interactionMode = MapInteractionMode.EDIT },
                    viewport = viewport,
                    onViewportChange = { viewport = it },
                    visibleLayers = visibleLayers,
                    onAreasChange = onAreasChange,
                    markedLabel = markedLabel,
                    onCanvasAspectChange = { canvasAspect = it },
                    modifier = Modifier.fillMaxWidth().weight(1f),
                )
                MapControlsRow(
                    interactionMode = interactionMode,
                    onInteractionModeChange = { interactionMode = it },
                    onResetViewport = { viewport = MapViewport.Full },
                    selectedAreaCount = selectedAreas.size,
                    canDeleteArea = activeAreaIndex in selectedAreas.indices,
                    canUndo = selectionHistory.canUndo,
                    canRedo = selectionHistory.canRedo,
                    onDeleteArea = {
                        if (activeAreaIndex in selectedAreas.indices) {
                            replaceAreas(selectedAreas.filterIndexed { index, _ -> index != activeAreaIndex })
                            activeAreaIndex = (activeAreaIndex - 1).coerceAtLeast(0)
                        }
                    },
                    onSelectFullFile = {
                        replaceAreas(listOf(RectangleSelection(NormalizedMapArea.Full)))
                        activeAreaIndex = 0
                    },
                    onUndo = ::undoSelection,
                    onRedo = ::redoSelection,
                )
            }
        }
        WorkflowFooter(
            status = selectedAreas.takeIf { it.isNotEmpty() }?.let {
                val sourceArea = listOf(GeoRectangleRegion(sourceBounds)).unionAreaSquareKilometers()
                val fraction = (totalAreaSquareKilometers / sourceArea).toFloat().coerceIn(0f, 1f)
                strings(
                    if (it.size == 1) "area.footer_one" else "area.footer_many",
                    it.size,
                    formatPercent(fraction),
                )
            } ?: strings["area.none_selected"],
            onBack = onBack,
            onNext = onNext,
            nextLabel = strings["area.next_content"],
            nextEnabled = selectedAreas.isNotEmpty(),
        )
    }
}

@Composable
@OptIn(ExperimentalComposeUiApi::class)
private fun SelectionCanvas(
    sourceBounds: GeoBounds,
    mapProjection: MapCanvasProjection,
    selectedAreas: List<MapSelectionShape>,
    activeAreaIndex: Int,
    onActiveAreaChange: (Int) -> Unit,
    previewState: PreviewUiState,
    interactionMode: MapInteractionMode,
    onAreaAdded: () -> Unit,
    onSelectionCommitted: (List<MapSelectionShape>, List<MapSelectionShape>) -> Unit,
    onDeleteActiveArea: () -> Unit,
    onUndo: () -> Unit,
    onRedo: () -> Unit,
    onCancelTool: () -> Unit,
    viewport: MapViewport,
    onViewportChange: (MapViewport) -> Unit,
    visibleLayers: PreviewLayerVisibility,
    onAreasChange: (List<MapSelectionShape>) -> Unit,
    markedLabel: PreviewLabel?,
    onCanvasAspectChange: (Float) -> Unit,
    modifier: Modifier,
) {
    val strings = LocalAppStrings.current
    var canvasSize by remember { mutableStateOf(IntSize.Zero) }
    var dragStart by remember { mutableStateOf<Offset?>(null) }
    var dragPansMap by remember { mutableStateOf(false) }
    var selectionDragMode by remember { mutableStateOf<SelectionDragMode?>(null) }
    var dragOriginArea by remember { mutableStateOf<MapSelectionShape?>(null) }
    var dragAreaIndex by remember { mutableIntStateOf(-1) }
    var panPreviewOffset by remember { mutableStateOf(Offset.Zero) }
    var middlePanActive by remember { mutableStateOf(false) }
    var middlePanPosition by remember { mutableStateOf<Offset?>(null) }
    var primaryPressPosition by remember { mutableStateOf<Offset?>(null) }
    var primaryPressMoved by remember { mutableStateOf(false) }
    var areaAddedDuringDrag by remember { mutableStateOf(false) }
    var areasBeforeDrag by remember { mutableStateOf<List<MapSelectionShape>?>(null) }
    var areasDuringDrag by remember { mutableStateOf<List<MapSelectionShape>?>(null) }
    var zoomGestureGeneration by remember { mutableIntStateOf(0) }
    var zooming by remember { mutableStateOf(false) }
    val focusRequester = remember { FocusRequester() }
    val textMeasurer = rememberTextMeasurer()
    val currentViewport by rememberUpdatedState(viewport)
    val currentAreas by rememberUpdatedState(selectedAreas)
    val currentActiveIndex by rememberUpdatedState(activeAreaIndex)
    val currentSelection by rememberUpdatedState(selectedAreas.getOrNull(activeAreaIndex))
    val panPreviewOffsetProvider = remember { { panPreviewOffset } }

    LaunchedEffect(zoomGestureGeneration) {
        if (zoomGestureGeneration == 0) return@LaunchedEffect
        delay(140)
        zooming = false
    }

    fun commitPanPreview() {
        if (panPreviewOffset == Offset.Zero) return
        val width = canvasSize.width.coerceAtLeast(1).toFloat()
        val height = canvasSize.height.coerceAtLeast(1).toFloat()
        val offset = panPreviewOffset
        panPreviewOffset = Offset.Zero
        val delta = MapCanvasTransform(mapProjection, currentViewport, width, height)
            .panDeltaForScreenTranslation(offset)
        onViewportChange(currentViewport.pan(delta.x, delta.y))
    }

    fun areaHitAt(offset: Offset): Triple<Int, Rect, SelectionCorner?>? {
        val width = canvasSize.width.coerceAtLeast(1).toFloat()
        val height = canvasSize.height.coerceAtLeast(1).toFloat()
        val transform = MapCanvasTransform(mapProjection, currentViewport, width, height)
        return currentAreas.indices.reversed().firstNotNullOfOrNull { index ->
            val shape = currentAreas[index]
            val circleGeometry = (shape as? CircleSelection)?.let {
                circleScreenGeometry(it, sourceBounds, transform)
            }
            val rect = circleGeometry?.bounds
                ?: transform.normalizedAreaToScreenRect(shape.normalizedEnvelope(sourceBounds))
            val corner = rect.cornerNear(offset, SELECTION_HANDLE_HIT_RADIUS_PX)
            val contains = when (shape) {
                is RectangleSelection -> rect.contains(offset)
                is CircleSelection -> requireNotNull(circleGeometry).contains(offset)
            }
            if (corner != null || contains) Triple(index, rect, corner) else null
        }
    }

    Box(
        modifier = modifier
            .clip(RoundedCornerShape(18.dp))
            .background(Color(0xFFF4F2ED))
            .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(18.dp))
            .focusRequester(focusRequester)
            .focusable()
            .onPreviewKeyEvent { event ->
                if (event.type != KeyEventType.KeyDown) {
                    false
                } else {
                    when {
                        event.key == Key.Escape -> {
                            onCancelTool()
                            true
                        }
                        event.key == Key.Delete -> {
                            onDeleteActiveArea()
                            true
                        }
                        event.isCtrlPressed && event.key == Key.Z && event.isShiftPressed -> {
                            onRedo()
                            true
                        }
                        event.isCtrlPressed && event.key == Key.Z -> {
                            onUndo()
                            true
                        }
                        event.isCtrlPressed && event.key == Key.Y -> {
                            onRedo()
                            true
                        }
                        else -> false
                    }
                }
            }
            .onPointerEvent(PointerEventType.Scroll) { event ->
                val change = event.changes.firstOrNull() ?: return@onPointerEvent
                val width = canvasSize.width.coerceAtLeast(1).toFloat()
                val height = canvasSize.height.coerceAtLeast(1).toFloat()
                val factor = if (change.scrollDelta.y > 0f) 1.28f else 0.78f
                zooming = true
                zoomGestureGeneration++
                onViewportChange(
                    currentViewport.zoomAt(change.position.x / width, change.position.y / height, factor),
                )
                change.consume()
            },
    ) {
        PreviewMapCanvas(
            previewState = previewState,
            mapProjection = mapProjection,
            viewport = viewport,
            visibleLayers = visibleLayers,
            textMeasurer = textMeasurer,
            interactive = (dragStart != null && !dragPansMap) || zooming,
            panPreviewOffsetProvider = panPreviewOffsetProvider,
            modifier = Modifier.fillMaxSize(),
        )
        Canvas(
            modifier = Modifier.fillMaxSize()
                .onSizeChanged {
                    canvasSize = it
                    if (it.height > 0) onCanvasAspectChange(it.width.toFloat() / it.height.toFloat())
                }
                .graphicsLayer {
                    translationX = panPreviewOffset.x
                    translationY = panPreviewOffset.y
                }
                .onPointerEvent(PointerEventType.Press, PointerEventPass.Initial) { event ->
                    focusRequester.requestFocus()
                    val change = event.changes.firstOrNull() ?: return@onPointerEvent
                    when {
                        event.buttons.isTertiaryPressed -> {
                            middlePanActive = true
                            middlePanPosition = change.position
                            panPreviewOffset = Offset.Zero
                            dragStart = change.position
                            dragPansMap = true
                            change.consume()
                        }
                        event.buttons.isPrimaryPressed -> {
                            primaryPressPosition = change.position
                            primaryPressMoved = false
                        }
                    }
                }
                .onPointerEvent(PointerEventType.Move, PointerEventPass.Initial) { event ->
                    val change = event.changes.firstOrNull() ?: return@onPointerEvent
                    if (middlePanActive) {
                        middlePanPosition?.let { previous ->
                            panPreviewOffset += change.position - previous
                        }
                        middlePanPosition = change.position
                        change.consume()
                    } else {
                        primaryPressPosition?.let { start ->
                            if ((change.position - start).getDistance() > CLICK_MOVE_TOLERANCE_PX) {
                                primaryPressMoved = true
                            }
                        }
                    }
                }
                .onPointerEvent(PointerEventType.Release, PointerEventPass.Initial) { event ->
                    if (middlePanActive) {
                        commitPanPreview()
                        middlePanActive = false
                        middlePanPosition = null
                        dragStart = null
                        dragPansMap = false
                        event.changes.firstOrNull()?.consume()
                    } else {
                        val clickPosition = primaryPressPosition
                        if (!primaryPressMoved && clickPosition != null) {
                            when (interactionMode) {
                                MapInteractionMode.EDIT -> onActiveAreaChange(areaHitAt(clickPosition)?.first ?: -1)
                                MapInteractionMode.ADD_CIRCLE -> {
                                    val width = canvasSize.width.coerceAtLeast(1).toFloat()
                                    val height = canvasSize.height.coerceAtLeast(1).toFloat()
                                    val world = MapCanvasTransform(mapProjection, currentViewport, width, height)
                                        .screenToNormalized(clickPosition)
                                    val next = currentAreas + CircleSelection(world.x, world.y, DEFAULT_CIRCLE_RADIUS_KM)
                                    onSelectionCommitted(currentAreas, next)
                                    onAreasChange(next)
                                    onActiveAreaChange(currentAreas.size)
                                    onAreaAdded()
                                }
                                else -> Unit
                            }
                        }
                    }
                    primaryPressPosition = null
                    primaryPressMoved = false
                }
                .pointerInput(interactionMode) {
                    detectDragGestures(
                        onDragStart = { offset ->
                            if (middlePanActive) return@detectDragGestures
                            dragStart = offset
                            dragPansMap = interactionMode == MapInteractionMode.PAN
                            panPreviewOffset = Offset.Zero
                            areaAddedDuringDrag = false
                            areasBeforeDrag = currentAreas
                            areasDuringDrag = currentAreas
                            if (!dragPansMap) {
                                val width = canvasSize.width.coerceAtLeast(1).toFloat()
                                val height = canvasSize.height.coerceAtLeast(1).toFloat()
                                when (interactionMode) {
                                    MapInteractionMode.EDIT -> {
                                        val hit = areaHitAt(offset)
                                        if (hit != null) {
                                            dragAreaIndex = hit.first
                                            onActiveAreaChange(hit.first)
                                            if (hit.first == currentActiveIndex) {
                                                dragOriginArea = currentAreas[hit.first]
                                                selectionDragMode = hit.third?.let(SelectionDragMode::Resize)
                                                    ?: SelectionDragMode.Move
                                            }
                                        }
                                    }
                                    MapInteractionMode.ADD_RECTANGLE -> {
                                        val world = MapCanvasTransform(mapProjection, currentViewport, width, height)
                                            .screenToNormalized(offset)
                                        val newArea = RectangleSelection(
                                            NormalizedMapArea.fromDrag(world.x, world.y, world.x, world.y),
                                        )
                                        selectionDragMode = SelectionDragMode.NewRectangle
                                        dragAreaIndex = currentAreas.size
                                        areaAddedDuringDrag = true
                                        val updatedAreas = currentAreas + newArea
                                        areasDuringDrag = updatedAreas
                                        onAreasChange(updatedAreas)
                                        onActiveAreaChange(currentAreas.size)
                                    }
                                    MapInteractionMode.ADD_CIRCLE -> {
                                        val world = MapCanvasTransform(mapProjection, currentViewport, width, height)
                                            .screenToNormalized(offset)
                                        val newArea = CircleSelection(world.x, world.y, MIN_CIRCLE_RADIUS_KM)
                                        selectionDragMode = SelectionDragMode.NewCircle
                                        dragAreaIndex = currentAreas.size
                                        areaAddedDuringDrag = true
                                        val updatedAreas = currentAreas + newArea
                                        areasDuringDrag = updatedAreas
                                        onAreasChange(updatedAreas)
                                        onActiveAreaChange(currentAreas.size)
                                    }
                                    MapInteractionMode.PAN -> Unit
                                }
                            }
                        },
                        onDragEnd = {
                            if (dragPansMap) commitPanPreview()
                            val completedAreaAddition = areaAddedDuringDrag
                            areasBeforeDrag?.let { before ->
                                onSelectionCommitted(before, areasDuringDrag ?: currentAreas)
                            }
                            dragStart = null
                            dragPansMap = false
                            selectionDragMode = null
                            dragOriginArea = null
                            dragAreaIndex = -1
                            areaAddedDuringDrag = false
                            areasBeforeDrag = null
                            areasDuringDrag = null
                            if (completedAreaAddition) onAreaAdded()
                        },
                        onDragCancel = {
                            panPreviewOffset = Offset.Zero
                            areasBeforeDrag?.let(onAreasChange)
                            dragStart = null
                            dragPansMap = false
                            selectionDragMode = null
                            dragOriginArea = null
                            dragAreaIndex = -1
                            areaAddedDuringDrag = false
                            areasBeforeDrag = null
                            areasDuringDrag = null
                        },
                    ) drag@{ change, dragAmount ->
                        if (middlePanActive) {
                            change.consume()
                            return@drag
                        }
                        val start = dragStart ?: return@drag
                        val width = canvasSize.width.coerceAtLeast(1).toFloat()
                        val height = canvasSize.height.coerceAtLeast(1).toFloat()
                        if (!dragPansMap) {
                            val mode = selectionDragMode
                            if (mode == null) {
                                change.consume()
                                return@drag
                            }
                            val transform = MapCanvasTransform(mapProjection, currentViewport, width, height)
                            val startWorld = transform.screenToNormalized(start)
                            val unclippedStartWorld = transform.screenToNormalized(start, clampToSource = false)
                            val endWorld = transform.screenToNormalized(change.position)
                            val unclippedEndWorld = transform.screenToNormalized(change.position, clampToSource = false)
                            val origin = dragOriginArea ?: currentSelection
                            val updated: MapSelectionShape = when (mode) {
                                SelectionDragMode.NewRectangle -> RectangleSelection(
                                    NormalizedMapArea.fromDrag(startWorld.x, startWorld.y, endWorld.x, endWorld.y),
                                )
                                SelectionDragMode.NewCircle -> CircleSelection(
                                    centerX = startWorld.x,
                                    centerY = startWorld.y,
                                    radiusKilometers = radiusBetween(sourceBounds, startWorld, endWorld),
                                )
                                SelectionDragMode.Move -> when (val shape = requireNotNull(origin)) {
                                    is RectangleSelection -> shape.copy(
                                        area = shape.area.translated(endWorld.x - startWorld.x, endWorld.y - startWorld.y),
                                    )
                                    is CircleSelection -> shape.translated(
                                        endWorld.x - startWorld.x,
                                        endWorld.y - startWorld.y,
                                    )
                                }
                                is SelectionDragMode.Resize -> when (val shape = requireNotNull(origin)) {
                                    is RectangleSelection -> shape.copy(
                                        area = shape.area.resizedFromCorner(mode.corner, endWorld.x, endWorld.y),
                                    )
                                    is CircleSelection -> shape.copy(
                                        radiusKilometers = resizedCircleRadius(
                                            sourceBounds,
                                            shape,
                                            unclippedStartWorld,
                                            unclippedEndWorld,
                                        ),
                                    )
                                }
                            }
                            val targetIndex = dragAreaIndex.takeIf { it >= 0 } ?: currentActiveIndex
                            if (targetIndex in currentAreas.indices) {
                                val updatedAreas = currentAreas.toMutableList().apply { this[targetIndex] = updated }
                                areasDuringDrag = updatedAreas
                                onAreasChange(updatedAreas)
                            } else if (targetIndex == currentAreas.size) {
                                val updatedAreas = currentAreas + updated
                                areasDuringDrag = updatedAreas
                                onAreasChange(updatedAreas)
                            }
                        } else {
                            // Keep the already rendered map in a retained graphics layer while dragging.
                            // The viewport is committed only once on release, avoiding expensive geometry
                            // reconstruction for every raw mouse event.
                            panPreviewOffset += dragAmount
                        }
                        change.consume()
                    }
                },
        ) {
            val transform = MapCanvasTransform(mapProjection, viewport, size.width, size.height)
            selectedAreas.forEachIndexed { index, shape ->
                val circleGeometry = (shape as? CircleSelection)?.let {
                    circleScreenGeometry(it, sourceBounds, transform)
                }
                val screenRect = circleGeometry?.bounds
                    ?: transform.normalizedAreaToScreenRect(shape.normalizedEnvelope(sourceBounds))
                val topLeft = screenRect.topLeft
                val selectionSize = screenRect.size
                val active = index == activeAreaIndex
                val visualStyle = selectionVisualStyle(active)
                if (circleGeometry != null) {
                    val path = Path().apply {
                        moveTo(circleGeometry.points.first().x, circleGeometry.points.first().y)
                        circleGeometry.points.drop(1).forEach { lineTo(it.x, it.y) }
                        close()
                    }
                    drawPath(path, visualStyle.fillColor)
                    drawPath(
                        path,
                        visualStyle.outlineColor,
                        style = Stroke(visualStyle.outlineWidthDp.dp.toPx()),
                    )
                } else {
                    drawRect(visualStyle.fillColor, topLeft, selectionSize)
                    drawRect(
                        visualStyle.outlineColor,
                        topLeft,
                        selectionSize,
                        style = Stroke(visualStyle.outlineWidthDp.dp.toPx()),
                    )
                }
                if (
                    topLeft.x + selectionSize.width >= 0f && topLeft.x <= size.width &&
                    topLeft.y + selectionSize.height >= 0f && topLeft.y <= size.height
                ) {
                    val badgeCenter = Offset(
                        (topLeft.x + 13.dp.toPx()).coerceIn(13.dp.toPx(), size.width - 13.dp.toPx()),
                        (topLeft.y + 13.dp.toPx()).coerceIn(13.dp.toPx(), size.height - 13.dp.toPx()),
                    )
                    val badgeLayout = textMeasurer.measure(
                        AnnotatedString((index + 1).toString()),
                        TextStyle(
                            color = Color.White,
                            fontSize = 10.sp,
                            fontWeight = FontWeight.Bold,
                        ),
                    )
                    drawCircle(if (active) Color(0xFF24424A) else Color(0xFF557078), 11.dp.toPx(), badgeCenter)
                    drawText(
                        badgeLayout,
                        topLeft = Offset(
                            badgeCenter.x - badgeLayout.size.width / 2f,
                            badgeCenter.y - badgeLayout.size.height / 2f,
                        ),
                    )
                }
                if (active) listOf(
                    topLeft,
                    Offset(topLeft.x + selectionSize.width, topLeft.y),
                    Offset(topLeft.x, topLeft.y + selectionSize.height),
                    Offset(topLeft.x + selectionSize.width, topLeft.y + selectionSize.height),
                ).forEach { point ->
                    drawCircle(Color(0xFF24424A), 7.dp.toPx(), point)
                    drawCircle(visualStyle.outlineColor, 5.dp.toPx(), point)
                }
            }
            markedLabel?.let { label ->
                if (previewState is PreviewUiState.Ready) {
                    val position = transform.geoToScreen(label.longitude, label.latitude)
                    val x = position.x
                    val y = position.y
                    if (x in 0f..size.width && y in 0f..size.height) {
                        drawCircle(Color.White, 12.dp.toPx(), Offset(x, y))
                        drawCircle(Color(0xFFD33F2F), 8.dp.toPx(), Offset(x, y))
                        drawCircle(Color.White, 3.dp.toPx(), Offset(x, y))
                    }
                }
            }
        }

        (previewState as? PreviewUiState.Ready)?.elevationPreview?.let { elevationPreview ->
            ElevationLegend(
                preview = elevationPreview,
                modifier = Modifier.align(Alignment.BottomEnd).padding(14.dp),
            )
        }

        Surface(
            modifier = Modifier.align(Alignment.TopStart).padding(14.dp),
            color = Color.White.copy(alpha = 0.92f),
            shape = RoundedCornerShape(10.dp),
        ) {
            Text(
                when (previewState) {
                    PreviewUiState.Unavailable -> strings["preview.local"]
                    is PreviewUiState.Loading -> buildString {
                        append(strings[if (previewState.elevation) "preview.elevation" else "preview.map_preview"])
                        if (previewState.sourceCount > 1) {
                            append(" ${previewState.sourceIndex}/${previewState.sourceCount}")
                        }
                        append(" ${strings["preview.loading"]}")
                        previewState.progress?.takeIf { it.total > 0 }?.let { progress ->
                            append(" · ${progress.completed * 100 / progress.total} %")
                        }
                    }
                    is PreviewUiState.Ready -> buildString {
                        if (previewState.warnings.isNotEmpty()) {
                            append("${strings["preview.incomplete"]} · ")
                            append(previewState.warnings.first())
                        } else {
                            if (previewState.mapSourceCount == 0 && previewState.elevationPreview != null) {
                                append(strings["preview.elevation"])
                            } else {
                                append(strings[if (previewState.loadedFromCache) "preview.cached" else "preview.real"])
                                append(" · ")
                                append(strings[PreviewLayerPolicy.detailHintKey(1f / viewport.width)])
                                if (previewState.elevationPreview != null) {
                                    append(" · ")
                                    append(strings["preview.elevation_overlay"])
                                }
                            }
                        }
                    }
                    is PreviewUiState.Failed ->
                        strings("preview.error", previewState.message)
                },
                modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
                color = Color(0xFF26383D),
                maxLines = 3,
                overflow = TextOverflow.Ellipsis,
            )
        }

    }
}

@Composable
private fun PreviewMapCanvas(
    previewState: PreviewUiState,
    mapProjection: MapCanvasProjection,
    viewport: MapViewport,
    visibleLayers: PreviewLayerVisibility,
    textMeasurer: TextMeasurer,
    interactive: Boolean,
    panPreviewOffsetProvider: () -> Offset,
    modifier: Modifier,
) {
    val gridColor = Color(0xFFB8C4C8).copy(alpha = 0.28f)
    val secondaryRouteColor = MaterialTheme.colorScheme.secondary
    val primaryRouteColor = MaterialTheme.colorScheme.tertiary
    val readyPreview = previewState as? PreviewUiState.Ready
    val preview = readyPreview?.dataset
    val renderIndex = readyPreview?.renderIndex
    val elevationPreview = readyPreview?.elevationPreview
    val elevationImage = remember(elevationPreview) {
        elevationPreview?.let(::createElevationPreviewImage)
    }
    Canvas(
        modifier = modifier.graphicsLayer {
            val offset = panPreviewOffsetProvider()
            translationX = offset.x
            translationY = offset.y
        },
    ) {
        repeat(9) { index ->
            val x = size.width * index / 8f
            drawLine(gridColor, Offset(x, 0f), Offset(x, size.height), 1.dp.toPx())
        }
        repeat(7) { index ->
            val y = size.height * index / 6f
            drawLine(gridColor, Offset(0f, y), Offset(size.width, y), 1.dp.toPx())
        }

        if (elevationPreview != null && elevationImage != null) {
            val transform = MapCanvasTransform(mapProjection, viewport, size.width, size.height)
            val topLeft = transform.geoToScreen(elevationPreview.bounds.west, elevationPreview.bounds.north)
            val bottomRight = transform.geoToScreen(elevationPreview.bounds.east, elevationPreview.bounds.south)
            val left = floor(minOf(topLeft.x, bottomRight.x)).toInt()
            val top = floor(minOf(topLeft.y, bottomRight.y)).toInt()
            val right = ceil(maxOf(topLeft.x, bottomRight.x)).toInt()
            val bottom = ceil(maxOf(topLeft.y, bottomRight.y)).toInt()
            if (right > left && bottom > top) {
                drawImage(
                    image = elevationImage,
                    dstOffset = IntOffset(left, top),
                    dstSize = IntSize(right - left, bottom - top),
                    alpha = if (readyPreview.mapSourceCount > 0) 0.82f else 1f,
                    filterQuality = FilterQuality.Medium,
                )
            }
        }

        if (preview == null && elevationPreview == null) {
            val river = Path().apply {
                moveTo(size.width * 0.04f, size.height * 0.23f)
                cubicTo(
                    size.width * 0.27f,
                    size.height * 0.07f,
                    size.width * 0.44f,
                    size.height * 0.55f,
                    size.width * 0.67f,
                    size.height * 0.40f,
                )
                cubicTo(
                    size.width * 0.81f,
                    size.height * 0.31f,
                    size.width * 0.87f,
                    size.height * 0.71f,
                    size.width * 0.98f,
                    size.height * 0.64f,
                )
            }
            drawPath(river, Petrol300.copy(alpha = 0.6f), style = Stroke(6.dp.toPx(), cap = StrokeCap.Round))

            val route = Path().apply {
                moveTo(size.width * 0.08f, size.height * 0.82f)
                lineTo(size.width * 0.26f, size.height * 0.57f)
                lineTo(size.width * 0.48f, size.height * 0.69f)
                lineTo(size.width * 0.72f, size.height * 0.28f)
                lineTo(size.width * 0.94f, size.height * 0.18f)
            }
            drawPath(
                route,
                secondaryRouteColor.copy(alpha = 0.72f),
                style = Stroke(5.dp.toPx(), cap = StrokeCap.Round),
            )
            drawPath(route, primaryRouteColor, style = Stroke(2.dp.toPx(), cap = StrokeCap.Round))
        } else if (preview != null && readyPreview.mapSourceCount > 0) {
            drawPreview(
                preview = preview,
                renderIndex = requireNotNull(renderIndex),
                mapProjection = mapProjection,
                viewport = viewport,
                visibleLayers = visibleLayers,
                textMeasurer = textMeasurer,
                interactive = interactive,
            )
        }
    }
}

private fun createElevationPreviewImage(preview: ElevationPreviewRaster) = Image.makeRaster(
    ImageInfo(
        preview.width,
        preview.height,
        ColorType.RGBA_8888,
        ColorAlphaType.UNPREMUL,
    ),
    ElevationPreviewRenderer.rgbaPixels(preview),
    preview.width * 4,
).toComposeImageBitmap()

@Composable
private fun ElevationLegend(
    preview: ElevationPreviewRaster,
    modifier: Modifier = Modifier,
) {
    val strings = LocalAppStrings.current
    val measurements = LocalMeasurements.current
    Surface(
        modifier = modifier.width(220.dp),
        color = Color.White.copy(alpha = 0.9f),
        contentColor = Color(0xFF26383D),
        shape = RoundedCornerShape(10.dp),
    ) {
        Column(
            modifier = Modifier.padding(horizontal = 12.dp, vertical = 9.dp),
            verticalArrangement = Arrangement.spacedBy(5.dp),
        ) {
            Text(strings["content.elevation_data"], fontWeight = FontWeight.SemiBold, fontSize = 12.sp)
            Box(
                Modifier.fillMaxWidth().height(9.dp).clip(RoundedCornerShape(5.dp)).background(
                    Brush.horizontalGradient(
                        ElevationPreviewRenderer.legendColorStops(preview.minimumElevationMeters).map(::Color),
                    ),
                ),
            )
            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                Text(measurements.formatElevation(preview.minimumElevationMeters.toDouble()), fontSize = 11.sp)
                Text(measurements.formatElevation(preview.maximumElevationMeters.toDouble()), fontSize = 11.sp)
            }
        }
    }
}

@Composable
private fun MapControlsRow(
    interactionMode: MapInteractionMode,
    onInteractionModeChange: (MapInteractionMode) -> Unit,
    onResetViewport: () -> Unit,
    selectedAreaCount: Int,
    canDeleteArea: Boolean,
    canUndo: Boolean,
    canRedo: Boolean,
    onDeleteArea: () -> Unit,
    onSelectFullFile: () -> Unit,
    onUndo: () -> Unit,
    onRedo: () -> Unit,
) {
    val strings = LocalAppStrings.current
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colorScheme.surface,
        contentColor = MaterialTheme.colorScheme.onSurface,
        shape = RoundedCornerShape(10.dp),
        border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
    ) {
        Row(
            modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            MapToolbarIconButton(
                label = strings["tooltip.edit"],
                glyph = MapToolbarGlyph.EDIT,
                selected = interactionMode == MapInteractionMode.EDIT,
            ) {
                onInteractionModeChange(MapInteractionMode.EDIT)
            }
            MapToolbarIconButton(
                label = strings["tooltip.add_rectangle"],
                glyph = MapToolbarGlyph.ADD_RECTANGLE,
                selected = interactionMode == MapInteractionMode.ADD_RECTANGLE,
            ) {
                onInteractionModeChange(MapInteractionMode.ADD_RECTANGLE)
            }
            MapToolbarIconButton(
                label = strings["tooltip.add_circle"],
                glyph = MapToolbarGlyph.ADD_CIRCLE,
                selected = interactionMode == MapInteractionMode.ADD_CIRCLE,
            ) {
                onInteractionModeChange(MapInteractionMode.ADD_CIRCLE)
            }
            MapToolbarIconButton(
                label = strings["tooltip.pan"],
                glyph = MapToolbarGlyph.PAN,
                selected = interactionMode == MapInteractionMode.PAN,
            ) {
                onInteractionModeChange(MapInteractionMode.PAN)
            }
            MapToolbarIconButton(strings["tooltip.select_all"], MapToolbarGlyph.SELECT_ALL, onClick = onSelectFullFile)
            MapToolbarIconButton(strings["tooltip.fit"], MapToolbarGlyph.FIT, onClick = onResetViewport)
            MapToolbarIconButton(
                label = strings["tooltip.delete"],
                glyph = MapToolbarGlyph.DELETE,
                enabled = canDeleteArea,
                destructive = true,
                onClick = onDeleteArea,
            )
            MapToolbarIconButton(strings["tooltip.undo"], MapToolbarGlyph.UNDO, enabled = canUndo, onClick = onUndo)
            MapToolbarIconButton(strings["tooltip.redo"], MapToolbarGlyph.REDO, enabled = canRedo, onClick = onRedo)
            Text(
                strings(
                    if (selectedAreaCount == 1) "area.toolbar_one" else "area.toolbar_many",
                    selectedAreaCount,
                ),
                modifier = Modifier.weight(1f),
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                style = MaterialTheme.typography.bodySmall,
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
            )
        }
    }
}

private enum class MapToolbarGlyph { EDIT, ADD_RECTANGLE, ADD_CIRCLE, PAN, SELECT_ALL, FIT, DELETE, UNDO, REDO }

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun MapToolbarIconButton(
    label: String,
    glyph: MapToolbarGlyph,
    selected: Boolean = false,
    enabled: Boolean = true,
    destructive: Boolean = false,
    onClick: () -> Unit,
) {
    TooltipBox(
        positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
        tooltip = { PlainTooltip { Text(label) } },
        state = rememberTooltipState(),
    ) {
        Surface(
            modifier = Modifier.size(38.dp),
            color = if (selected) Color(0xFFB9D5DD) else Color.Transparent,
            contentColor = when {
                !enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f)
                destructive -> MaterialTheme.colorScheme.error
                else -> Color(0xFF24454D)
            },
            shape = RoundedCornerShape(9.dp),
            border = BorderStroke(1.dp, if (selected) Color(0xFF6F969F) else Color(0xFF8EABB2)),
        ) {
            IconButton(onClick = onClick, enabled = enabled) {
                val iconColor = MaterialTheme.colorScheme.onSurface
                Canvas(
                    modifier = Modifier.size(22.dp).semantics { contentDescription = label },
                ) {
                    drawToolbarGlyph(glyph, if (destructive) Color(0xFFB3261E) else iconColor)
                }
            }
        }
    }
}

private fun DrawScope.drawToolbarGlyph(glyph: MapToolbarGlyph, color: Color) {
    val strokeWidth = size.minDimension * 0.09f
    val stroke = Stroke(width = strokeWidth, cap = StrokeCap.Round)
    fun point(x: Float, y: Float) = Offset(size.width * x, size.height * y)
    when (glyph) {
        MapToolbarGlyph.EDIT -> {
            val arrow = Path().apply {
                moveTo(size.width * 0.18f, size.height * 0.10f)
                lineTo(size.width * 0.82f, size.height * 0.55f)
                lineTo(size.width * 0.55f, size.height * 0.61f)
                lineTo(size.width * 0.43f, size.height * 0.89f)
                close()
            }
            drawPath(arrow, color)
        }
        MapToolbarGlyph.ADD_RECTANGLE -> {
            drawRect(color, point(0.08f, 0.17f), Size(size.width * 0.62f, size.height * 0.66f), style = stroke)
            drawLine(color, point(0.63f, 0.72f), point(0.95f, 0.72f), strokeWidth, StrokeCap.Round)
            drawLine(color, point(0.79f, 0.56f), point(0.79f, 0.88f), strokeWidth, StrokeCap.Round)
        }
        MapToolbarGlyph.ADD_CIRCLE -> {
            drawCircle(color, size.minDimension * 0.31f, point(0.40f, 0.50f), style = stroke)
            drawLine(color, point(0.63f, 0.72f), point(0.95f, 0.72f), strokeWidth, StrokeCap.Round)
            drawLine(color, point(0.79f, 0.56f), point(0.79f, 0.88f), strokeWidth, StrokeCap.Round)
        }
        MapToolbarGlyph.PAN -> {
            drawLine(color, point(0.50f, 0.08f), point(0.50f, 0.92f), strokeWidth, StrokeCap.Round)
            drawLine(color, point(0.08f, 0.50f), point(0.92f, 0.50f), strokeWidth, StrokeCap.Round)
            listOf(
                point(0.50f, 0.08f) to listOf(point(0.38f, 0.22f), point(0.62f, 0.22f)),
                point(0.50f, 0.92f) to listOf(point(0.38f, 0.78f), point(0.62f, 0.78f)),
                point(0.08f, 0.50f) to listOf(point(0.22f, 0.38f), point(0.22f, 0.62f)),
                point(0.92f, 0.50f) to listOf(point(0.78f, 0.38f), point(0.78f, 0.62f)),
            ).forEach { (tip, ends) -> ends.forEach { end -> drawLine(color, tip, end, strokeWidth, StrokeCap.Round) } }
        }
        MapToolbarGlyph.SELECT_ALL -> {
            drawRect(color, point(0.12f, 0.12f), Size(size.width * 0.76f, size.height * 0.76f), style = stroke)
            drawRect(color.copy(alpha = 0.22f), point(0.22f, 0.22f), Size(size.width * 0.56f, size.height * 0.56f))
        }
        MapToolbarGlyph.FIT -> {
            listOf(
                point(0.10f, 0.34f) to point(0.10f, 0.10f), point(0.10f, 0.10f) to point(0.34f, 0.10f),
                point(0.66f, 0.10f) to point(0.90f, 0.10f), point(0.90f, 0.10f) to point(0.90f, 0.34f),
                point(0.10f, 0.66f) to point(0.10f, 0.90f), point(0.10f, 0.90f) to point(0.34f, 0.90f),
                point(0.66f, 0.90f) to point(0.90f, 0.90f), point(0.90f, 0.90f) to point(0.90f, 0.66f),
            ).forEach { (start, end) -> drawLine(color, start, end, strokeWidth, StrokeCap.Round) }
        }
        MapToolbarGlyph.DELETE -> {
            drawLine(color, point(0.22f, 0.28f), point(0.78f, 0.28f), strokeWidth, StrokeCap.Round)
            drawLine(color, point(0.38f, 0.14f), point(0.62f, 0.14f), strokeWidth, StrokeCap.Round)
            drawRect(color, point(0.28f, 0.34f), Size(size.width * 0.44f, size.height * 0.54f), style = stroke)
            drawLine(color, point(0.43f, 0.44f), point(0.43f, 0.76f), strokeWidth * 0.75f, StrokeCap.Round)
            drawLine(color, point(0.57f, 0.44f), point(0.57f, 0.76f), strokeWidth * 0.75f, StrokeCap.Round)
        }
        MapToolbarGlyph.UNDO, MapToolbarGlyph.REDO -> {
            val mirrored = glyph == MapToolbarGlyph.REDO
            fun mirrorX(value: Float): Float = if (mirrored) 1f - value else value
            val path = Path().apply {
                moveTo(size.width * mirrorX(0.22f), size.height * 0.48f)
                cubicTo(
                    size.width * mirrorX(0.40f), size.height * 0.24f,
                    size.width * mirrorX(0.75f), size.height * 0.26f,
                    size.width * mirrorX(0.79f), size.height * 0.70f,
                )
            }
            drawPath(path, color, style = stroke)
            drawLine(
                color,
                point(mirrorX(0.22f), 0.48f),
                point(mirrorX(0.39f), 0.29f),
                strokeWidth,
                StrokeCap.Round,
            )
            drawLine(
                color,
                point(mirrorX(0.22f), 0.48f),
                point(mirrorX(0.40f), 0.61f),
                strokeWidth,
                StrokeCap.Round,
            )
        }
    }
}

private fun DrawScope.drawPreview(
    preview: PreviewDataset,
    renderIndex: PreviewRenderIndex,
    mapProjection: MapCanvasProjection,
    viewport: MapViewport,
    visibleLayers: PreviewLayerVisibility,
    textMeasurer: TextMeasurer,
    interactive: Boolean,
) {
    val transform = MapCanvasTransform(mapProjection, viewport, size.width, size.height)
    val visibleArea = transform.visibleNormalizedArea()
    fun point(longitude: Double, latitude: Double): Offset = transform.geoToScreen(longitude, latitude)
    val zoom = 1f / viewport.width
    // During a drag the map is redrawn for every pointer update. A smaller geometry budget keeps
    // the interaction responsive; full detail is restored immediately when the button is released.
    val targetLineCount = if (interactive) {
        (2_500f * sqrt(zoom)).toInt().coerceIn(2_500, 10_000)
    } else {
        (20_000f * sqrt(zoom)).toInt().coerceIn(20_000, 100_000)
    }
    val targetAreaCount = if (interactive) {
        (1_500f * sqrt(zoom)).toInt().coerceIn(1_500, 7_000)
    } else {
        (12_000f * sqrt(zoom)).toInt().coerceIn(12_000, 60_000)
    }
    fun isVisible(featureClass: PreviewFeatureClass): Boolean =
        PreviewLayerPolicy.isFeatureVisible(featureClass, visibleLayers, zoom, interactive)
    val minimumExtentPixels = when {
        interactive -> 2.2f
        zoom < 1.8f -> 1.2f
        zoom < 3.5f -> 0.8f
        else -> 0.35f
    }
    val minimumVertexDistanceSquared = when {
        interactive -> 2.25f * 2.25f
        zoom < 3.5f -> 1.1f * 1.1f
        else -> 0.45f * 0.45f
    }
    fun isRenderable(index: Int): Boolean = renderIndex.hasMinimumScreenExtent(
        index,
        visibleArea.widthFraction,
        visibleArea.heightFraction,
        size.width,
        size.height,
        minimumExtentPixels,
    )
    fun path(line: de.arxsilex.maptools.converter.core.preview.PreviewPolyline): Path = Path().apply {
        val first = point(line.coordinates[0], line.coordinates[1])
        moveTo(first.x, first.y)
        var previous = first
        var coordinateIndex = 2
        while (coordinateIndex < line.coordinates.size - 2) {
            val position = point(line.coordinates[coordinateIndex], line.coordinates[coordinateIndex + 1])
            val delta = position - previous
            if (delta.x * delta.x + delta.y * delta.y >= minimumVertexDistanceSquared) {
                lineTo(position.x, position.y)
                previous = position
            }
            coordinateIndex += 2
        }
        val lastIndex = line.coordinates.size - 2
        val last = point(line.coordinates[lastIndex], line.coordinates[lastIndex + 1])
        lineTo(last.x, last.y)
    }

    val areaCandidates = renderIndex.queryAreaIndices(
        visibleArea.left,
        visibleArea.top,
        visibleArea.right,
        visibleArea.bottom,
    )
    val visibleAreaCount = areaCandidates.count { index ->
        val line = preview.polylines[index]
        isVisible(line.featureClass) && isRenderable(index)
    }
    val areaStep = previewSamplingStep(visibleAreaCount, targetAreaCount, interactive)
    var visibleAreaOrdinal = 0
    for (index in areaCandidates) {
        val line = preview.polylines[index]
        if (!isVisible(line.featureClass) || !isRenderable(index)) continue
        if (visibleAreaOrdinal++ % areaStep != 0) continue
        val areaPath = path(line)
        val fill = when (line.featureClass) {
            PreviewFeatureClass.LAND -> Color(0xFFCFE8C2).copy(alpha = 0.72f)
            PreviewFeatureClass.URBAN_AREA -> Color(0xFFE6DED2).copy(alpha = 0.72f)
            PreviewFeatureClass.WATER_AREA -> Color(0xFFA8D8EA).copy(alpha = 0.88f)
            PreviewFeatureClass.BUILDING -> Color(0xFFD7CBC3).copy(alpha = 0.82f)
            else -> Color.Transparent
        }
        val last = line.coordinates.lastIndex - 1
        val closed = kotlin.math.abs(line.coordinates[0] - line.coordinates[last]) < 0.000_000_1 &&
            kotlin.math.abs(line.coordinates[1] - line.coordinates[last + 1]) < 0.000_000_1
        if (closed) {
            drawPath(areaPath, fill)
        } else {
            drawPath(areaPath, fill.copy(alpha = 0.72f), style = Stroke(1.dp.toPx()))
        }
        if (line.featureClass == PreviewFeatureClass.BUILDING && closed) {
            drawPath(areaPath, Color(0xFFB4A49B), style = Stroke(0.6.dp.toPx()))
        }
    }

    val lineCandidates = renderIndex.queryLineIndices(
        visibleArea.left,
        visibleArea.top,
        visibleArea.right,
        visibleArea.bottom,
    )
    fun isLineRenderable(index: Int): Boolean = !interactive || isRenderable(index)
    val visibleLineCount = lineCandidates.count { index ->
        val line = preview.polylines[index]
        isVisible(line.featureClass) && isLineRenderable(index)
    }
    val lineStep = previewSamplingStep(visibleLineCount, targetLineCount, interactive)
    var visibleLineOrdinal = 0
    for (index in lineCandidates) {
        val line = preview.polylines[index]
        if (!isVisible(line.featureClass) || !isLineRenderable(index)) {
            continue
        }
        val ordinal = visibleLineOrdinal++
        if (ordinal % lineStep != 0) {
            continue
        }
        val linePath = path(line)
        when (line.featureClass) {
            PreviewFeatureClass.MAJOR_ROAD -> {
                val casingWidth = if (zoom < 1.7f) 2.1f else 3.4f
                val fillWidth = if (zoom < 1.7f) 1.25f else 2.2f
                drawPath(linePath, Color(0xFFC98F78), style = Stroke(casingWidth.dp.toPx(), cap = StrokeCap.Round))
                drawPath(linePath, Color(0xFFFFD9C3), style = Stroke(fillWidth.dp.toPx(), cap = StrokeCap.Round))
            }
            PreviewFeatureClass.MINOR_ROAD -> {
                drawPath(linePath, Color(0xFFB8B8B3), style = Stroke(2.4.dp.toPx(), cap = StrokeCap.Round))
                drawPath(linePath, Color.White, style = Stroke(1.5.dp.toPx(), cap = StrokeCap.Round))
            }
            PreviewFeatureClass.LOCAL_ROAD -> {
                drawPath(linePath, Color(0xFFC8C8C3), style = Stroke(1.8.dp.toPx(), cap = StrokeCap.Round))
                drawPath(linePath, Color.White.copy(alpha = 0.92f), style = Stroke(1.05.dp.toPx(), cap = StrokeCap.Round))
            }
            PreviewFeatureClass.PATH -> drawPath(
                linePath,
                Color(0xFF9B795F),
                style = Stroke(1.dp.toPx(), pathEffect = PathEffect.dashPathEffect(floatArrayOf(5f, 4f))),
            )
            PreviewFeatureClass.RAILWAY -> drawPath(
                linePath,
                Color(0xFF6F7375),
                style = Stroke(1.2.dp.toPx(), pathEffect = PathEffect.dashPathEffect(floatArrayOf(7f, 3f))),
            )
            PreviewFeatureClass.WATERWAY -> drawPath(
                linePath,
                Color(0xFF5BA8C7),
                style = Stroke(1.8.dp.toPx(), cap = StrokeCap.Round),
            )
            else -> Unit
        }
    }
    if (PreviewLayerPolicy.isFeatureVisible(PreviewFeatureClass.POI, visibleLayers, zoom, interactive)) {
        val pointCandidates = renderIndex.queryPointIndices(
            visibleArea.left,
            visibleArea.top,
            visibleArea.right,
            visibleArea.bottom,
        )
        val poiBudget = (1_500f * sqrt(zoom)).toInt().coerceIn(1_500, 12_000)
        val poiStep = previewSamplingStep(pointCandidates.size, poiBudget, interactive)
        pointCandidates.forEachIndexed { ordinal, pointIndex ->
            if (ordinal % poiStep != 0) return@forEachIndexed
            val previewPoint = preview.points[pointIndex]
            val position = point(previewPoint.longitude, previewPoint.latitude)
            if (position.x !in 0f..size.width || position.y !in 0f..size.height) return@forEachIndexed
            drawCircle(Color.White, 2.5.dp.toPx(), position)
            drawCircle(Color(0xFFD15B47), 1.6.dp.toPx(), position)
        }
    }
    if (!interactive) {
        drawPreviewLabels(preview, renderIndex, viewport, visibleArea, visibleLayers, textMeasurer, ::point)
    }
}

private fun DrawScope.drawPreviewLabels(
    preview: PreviewDataset,
    renderIndex: PreviewRenderIndex,
    viewport: MapViewport,
    visibleArea: NormalizedMapArea,
    visibleLayers: PreviewLayerVisibility,
    textMeasurer: TextMeasurer,
    point: (Double, Double) -> Offset,
) {
    val zoom = 1f / viewport.width
    val minimumImportance = PreviewLayerPolicy.minimumLabelImportance(zoom)
    val canvasWidth = size.width
    val canvasHeight = size.height
    val canvasBaseLimit = (canvasWidth * canvasHeight / 22_000f).toInt().coerceAtLeast(55)
    val limit = (canvasBaseLimit * sqrt(zoom)).toInt().coerceIn(55, 420)
    val occupied = mutableListOf<Rect>()
    var drawn = 0
    val candidates = buildList {
        for (labelIndex in renderIndex.queryLabelIndices(
            visibleArea.left,
            visibleArea.top,
            visibleArea.right,
            visibleArea.bottom,
        )) {
            val label = preview.labels[labelIndex]
            if (label.importance < minimumImportance ||
                !PreviewLayerPolicy.isLabelVisible(label, visibleLayers, zoom)
            ) continue
            val position = point(label.longitude, label.latitude)
            if (position.x !in -80f..(canvasWidth + 80f) ||
                position.y !in -30f..(canvasHeight + 30f)
            ) continue
            add(LabelPlacementCandidate(labelIndex, position, label.importance))
        }
    }
    for (candidate in spatiallyBalancedLabelCandidates(candidates, canvasWidth, canvasHeight, limit)) {
        if (drawn >= limit) break
        val label = preview.labels[candidate.labelIndex]
        val position = candidate.position
        val fontSize = when (label.labelClass) {
            PreviewLabelClass.CITY -> 15.sp
            PreviewLabelClass.TOWN -> 13.sp
            PreviewLabelClass.VILLAGE -> 11.sp
            else -> 10.sp
        }
        val textColor = when (label.labelClass) {
            PreviewLabelClass.WATER -> Color(0xFF276B89)
            PreviewLabelClass.AREA -> Color(0xFF477246)
            PreviewLabelClass.POI -> Color(0xFF8C4338)
            else -> Color(0xFF263438)
        }
        val layout = textMeasurer.measure(
            AnnotatedString(label.text),
            TextStyle(
                color = textColor,
                fontSize = fontSize,
                fontWeight = if (label.importance >= 78) FontWeight.SemiBold else FontWeight.Normal,
            ),
        )
        val left = position.x - layout.size.width / 2f
        val top = position.y - layout.size.height - 3.dp.toPx()
        val box = Rect(
            left - 3.dp.toPx(),
            top - 1.dp.toPx(),
            left + layout.size.width + 3.dp.toPx(),
            top + layout.size.height + 1.dp.toPx(),
        )
        if (box.left < 0f || box.top < 0f || box.right > canvasWidth || box.bottom > canvasHeight) continue
        if (occupied.any { it.overlaps(box) }) continue
        drawRoundRect(
            Color.White.copy(alpha = 0.78f),
            topLeft = Offset(box.left, box.top),
            size = Size(box.width, box.height),
            cornerRadius = androidx.compose.ui.geometry.CornerRadius(3.dp.toPx()),
        )
        drawText(layout, topLeft = Offset(left, top))
        occupied += box
        drawn++
    }
}

@Composable
private fun AreaDetails(
    inputFiles: List<InputFileInfo>,
    sourceBounds: GeoBounds,
    selectedAreas: List<MapSelectionShape>,
    areaMetrics: List<GeoRegionMetrics>,
    totalAreaSquareKilometers: Double,
    selectionEstimates: SelectionEstimateSummary?,
    activeAreaIndex: Int,
    onActiveAreaChange: (Int) -> Unit,
    onShowArea: (Int) -> Unit,
    viewport: MapViewport,
    mapProjection: MapCanvasProjection,
    canvasAspect: Float,
    visibleLayers: PreviewLayerVisibility,
    onVisibleLayersChange: (PreviewLayerVisibility) -> Unit,
    onAreaChange: (MapSelectionShape) -> Unit,
    searchQuery: String,
    onSearchQueryChange: (String) -> Unit,
    searchResults: List<PreviewLabel>,
    onSearchResultSelected: (PreviewLabel) -> Unit,
    markedLabel: PreviewLabel?,
    onAddCircleAtMarkedLabel: (Double) -> Unit,
    detailStatus: String?,
    modifier: Modifier,
) {
    val strings = LocalAppStrings.current
    val measurements = LocalMeasurements.current
    val selectedArea = selectedAreas.getOrNull(activeAreaIndex)
    val selectedRegion = selectedArea?.toGeoRegion(sourceBounds)
    val selectedBounds = selectedRegion?.bounds
    val selectedMetrics = areaMetrics.getOrNull(activeAreaIndex)
    val selectedEstimate = selectionEstimates?.areas?.getOrNull(activeAreaIndex)
    var westText by remember(sourceBounds) { mutableStateOf("") }
    var southText by remember(sourceBounds) { mutableStateOf("") }
    var eastText by remember(sourceBounds) { mutableStateOf("") }
    var northText by remember(sourceBounds) { mutableStateOf("") }
    var radiusText by remember(sourceBounds, measurements.system) {
        mutableStateOf(measurements.formatEditableRadius(DEFAULT_CIRCLE_RADIUS_KM))
    }
    var markedRadiusText by remember(sourceBounds, measurements.system) {
        mutableStateOf(measurements.formatEditableRadius(DEFAULT_CIRCLE_RADIUS_KM))
    }
    var boundsError by remember(sourceBounds) { mutableStateOf<String?>(null) }
    val detailsScrollState = rememberScrollState()

    LaunchedEffect(selectedBounds) {
        westText = selectedBounds?.west?.let(::formatEditableCoordinate).orEmpty()
        southText = selectedBounds?.south?.let(::formatEditableCoordinate).orEmpty()
        eastText = selectedBounds?.east?.let(::formatEditableCoordinate).orEmpty()
        northText = selectedBounds?.north?.let(::formatEditableCoordinate).orEmpty()
        boundsError = null
    }
    LaunchedEffect(selectedArea, measurements.system) {
        if (selectedArea is CircleSelection) radiusText = measurements.formatEditableRadius(selectedArea.radiusKilometers)
    }

    Box(modifier = modifier) {
    CardContainer(
        modifier = Modifier.fillMaxSize().padding(end = 18.dp).verticalScroll(detailsScrollState),
    ) {
        Text(strings["area.selection"], style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
        Spacer(Modifier.height(8.dp))
        Text(
            if (inputFiles.size == 1) inputFiles.single().name else strings("area.combined_sources", inputFiles.size),
            color = MaterialTheme.colorScheme.primary,
        )
        Text(
            formatFileSize(inputFiles.sumOf(InputFileInfo::sizeBytes)),
            color = MaterialTheme.colorScheme.onSurfaceVariant,
        )
        Spacer(Modifier.height(16.dp))
        Text(strings["area.selection_areas"], fontWeight = FontWeight.SemiBold)
        Spacer(Modifier.height(6.dp))
        if (selectedAreas.isEmpty()) {
            Text(
                strings["area.none_added"],
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                style = MaterialTheme.typography.bodySmall,
            )
        } else {
            selectedAreas.forEachIndexed { index, area ->
                val metrics = areaMetrics[index]
                val estimate = selectionEstimates?.areas?.getOrNull(index)
                Surface(
                    onClick = { onActiveAreaChange(index); onShowArea(index) },
                    modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
                    color = if (index == activeAreaIndex) {
                        MaterialTheme.colorScheme.secondaryContainer
                    } else {
                        MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.45f)
                    },
                    shape = RoundedCornerShape(8.dp),
                    border = BorderStroke(
                        1.dp,
                        if (index == activeAreaIndex) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant,
                    ),
                ) {
                    Row(
                        modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
                        verticalAlignment = Alignment.CenterVertically,
                    ) {
                        Text(
                            "${index + 1}",
                            modifier = Modifier.width(24.dp),
                            fontWeight = FontWeight.Bold,
                            color = MaterialTheme.colorScheme.primary,
                        )
                        Column(modifier = Modifier.weight(1f)) {
                            Text(
                                strings[if (area is CircleSelection) "area.circle" else "area.rectangle"],
                                style = MaterialTheme.typography.bodyMedium,
                                fontWeight = FontWeight.Medium,
                            )
                            Text(
                                "${measurements.formatDistance(metrics.widthKilometers)} × " +
                                    "${measurements.formatDistance(metrics.heightKilometers)} · " +
                                    measurements.formatArea(metrics.areaSquareKilometers),
                                color = MaterialTheme.colorScheme.onSurfaceVariant,
                                style = MaterialTheme.typography.bodySmall,
                            )
                            Text(
                                estimate?.let {
                                    strings("area.objects_approx", measurements.formatCount(it.estimatedSourceObjectCount)) + " · " +
                                        formatSizeRange(it)
                                } ?: strings["area.estimate_loading"],
                                color = MaterialTheme.colorScheme.onSurfaceVariant,
                                style = MaterialTheme.typography.labelSmall,
                                maxLines = 1,
                                overflow = TextOverflow.Ellipsis,
                            )
                        }
                        Text(strings["area.show"], style = MaterialTheme.typography.labelSmall)
                    }
                }
            }
        }
        Spacer(Modifier.height(8.dp))
        Surface(
            modifier = Modifier.fillMaxWidth(),
            color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.55f),
            shape = RoundedCornerShape(8.dp),
        ) {
            Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp)) {
                Text(strings["area.total_selection"], fontWeight = FontWeight.SemiBold)
                DetailRow(strings["area.union_area"], measurements.formatArea(totalAreaSquareKilometers), emphasized = true)
                DetailRow(
                    strings["area.objects_estimated"],
                    selectionEstimates?.total?.let { strings("area.approx", measurements.formatCount(it.estimatedSourceObjectCount)) } ?: "—",
                )
                DetailRow(
                    strings["area.size_estimated"],
                    selectionEstimates?.total?.let(::formatSizeRange) ?: "—",
                )
                Text(
                    strings["area.overlap_hint"],
                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                    style = MaterialTheme.typography.labelSmall,
                )
            }
        }
        Spacer(Modifier.height(16.dp))
        Text(strings["area.search"], fontWeight = FontWeight.SemiBold)
        Spacer(Modifier.height(6.dp))
        OutlinedTextField(
            value = searchQuery,
            onValueChange = onSearchQueryChange,
            modifier = Modifier.fillMaxWidth(),
            label = { Text(strings["area.name"]) },
            singleLine = true,
        )
        searchResults.take(6).forEach { result ->
            OutlinedButton(
                onClick = { onSearchResultSelected(result) },
                modifier = Modifier.fillMaxWidth(),
                contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp),
            ) {
                Text(result.text, maxLines = 1, overflow = TextOverflow.Ellipsis)
            }
        }
        markedLabel?.let { label ->
            Spacer(Modifier.height(6.dp))
            Text(
                strings("area.marked", label.text),
                color = MaterialTheme.colorScheme.primary,
                style = MaterialTheme.typography.bodySmall,
            )
            Row(
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                OutlinedTextField(
                    value = markedRadiusText,
                    onValueChange = { markedRadiusText = it; boundsError = null },
                    modifier = Modifier.weight(1f),
                    label = { Text(strings("area.radius_unit", measurements.radiusUnit)) },
                    singleLine = true,
                )
                OutlinedButton(
                    onClick = {
                        measurements.parseRadius(
                            markedRadiusText,
                            MIN_CIRCLE_RADIUS_KM,
                            MAX_CIRCLE_RADIUS_KM,
                            strings,
                        ).onSuccess(onAddCircleAtMarkedLabel)
                            .onFailure { boundsError = it.message }
                    },
                ) { Text(strings["area.add_radius"]) }
            }
        }
        detailStatus?.let { status ->
            Text(status, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall)
        }
        Spacer(Modifier.height(24.dp))
        Text(strings["area.view"], fontWeight = FontWeight.SemiBold)
        Spacer(Modifier.height(6.dp))
        DetailRow(strings["area.zoom"], "%.1f×".format(1f / viewport.width))
        val visibleArea = MapCanvasTransform(mapProjection, viewport, canvasAspect * 1_000f, 1_000f)
            .visibleNormalizedArea()
        sourceBounds.subset(visibleArea).let { visibleBounds ->
            val centerLatitude = (visibleBounds.south + visibleBounds.north) / 2.0
            val visibleKilometers = (visibleBounds.east - visibleBounds.west) *
                111.32 * cos(centerLatitude * PI / 180.0)
            DetailRow(strings["area.visible_width"], strings("area.approx", measurements.formatDistance(visibleKilometers)))
        }
        Spacer(Modifier.height(16.dp))
        Text(strings["area.preview_layers"], fontWeight = FontWeight.SemiBold)
        PreviewLayerRow(strings["layer.major_roads"], visibleLayers.majorRoads) {
            onVisibleLayersChange(visibleLayers.copy(majorRoads = it))
        }
        PreviewLayerRow(strings["layer.regional_roads"], visibleLayers.regionalRoads) {
            onVisibleLayersChange(visibleLayers.copy(regionalRoads = it))
        }
        PreviewLayerRow(strings["layer.local_roads"], visibleLayers.localRoads) {
            onVisibleLayersChange(visibleLayers.copy(localRoads = it))
        }
        PreviewLayerRow(strings["layer.paths"], visibleLayers.paths) {
            onVisibleLayersChange(visibleLayers.copy(paths = it))
        }
        PreviewLayerRow(strings["layer.railways"], visibleLayers.railways) {
            onVisibleLayersChange(visibleLayers.copy(railways = it))
        }
        PreviewLayerRow(strings["layer.places"], visibleLayers.places) {
            onVisibleLayersChange(visibleLayers.copy(places = it))
        }
        PreviewLayerRow(strings["layer.water"], visibleLayers.water) {
            onVisibleLayersChange(visibleLayers.copy(water = it))
        }
        PreviewLayerRow(strings["layer.landscape"], visibleLayers.landscape) {
            onVisibleLayersChange(visibleLayers.copy(landscape = it))
        }
        PreviewLayerRow(strings["layer.buildings"], visibleLayers.buildings) {
            onVisibleLayersChange(visibleLayers.copy(buildings = it))
        }
        PreviewLayerRow("POIs", visibleLayers.poi) {
            onVisibleLayersChange(visibleLayers.copy(poi = it))
        }
        Text(
                    strings["area.layer_hint"],
            color = MaterialTheme.colorScheme.onSurfaceVariant,
            style = MaterialTheme.typography.bodySmall,
        )
        Spacer(Modifier.height(12.dp))
        Text(strings[if (selectedArea is CircleSelection) "area.circle_area" else "area.bounding_box"], fontWeight = FontWeight.SemiBold)
        Text(
            if (selectedAreas.isEmpty()) strings["area.no_area"] else strings("area.active", selectedAreas.size, activeAreaIndex + 1),
            color = MaterialTheme.colorScheme.onSurfaceVariant,
            style = MaterialTheme.typography.bodySmall,
        )
        if (selectedArea is CircleSelection) {
            Text(
                strings["area.circle_hint"],
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                style = MaterialTheme.typography.bodySmall,
            )
            Spacer(Modifier.height(8.dp))
            selectedMetrics?.let { metrics ->
                DetailRow(strings["area.latitude"], formatCoordinate(metrics.centerLatitude))
                DetailRow(strings["area.longitude"], formatCoordinate(metrics.centerLongitude))
            }
            OutlinedTextField(
                value = radiusText,
                onValueChange = { radiusText = it; boundsError = null },
                modifier = Modifier.fillMaxWidth(),
                label = { Text(strings("area.radius_unit", measurements.radiusUnit)) },
                singleLine = true,
            )
            boundsError?.let { message ->
                Spacer(Modifier.height(6.dp))
                Text(message, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
            }
            Spacer(Modifier.height(8.dp))
            OutlinedButton(
                onClick = {
                    measurements.parseRadius(
                        radiusText,
                        MIN_CIRCLE_RADIUS_KM,
                        MAX_CIRCLE_RADIUS_KM,
                        strings,
                    ).onSuccess { radius ->
                        boundsError = null
                        onAreaChange(selectedArea.copy(radiusKilometers = radius))
                    }.onFailure { error -> boundsError = error.message }
                },
                modifier = Modifier.fillMaxWidth(),
            ) { Text(strings["area.apply_radius"]) }
        } else {
            Text(
                strings["area.coordinates_hint"],
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                style = MaterialTheme.typography.bodySmall,
            )
            Spacer(Modifier.height(8.dp))
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            OutlinedTextField(
                value = westText,
                onValueChange = { westText = it; boundsError = null },
                modifier = Modifier.weight(1f),
                label = { Text(strings["area.west"]) },
                singleLine = true,
            )
            OutlinedTextField(
                value = eastText,
                onValueChange = { eastText = it; boundsError = null },
                modifier = Modifier.weight(1f),
                label = { Text(strings["area.east"]) },
                singleLine = true,
            )
        }
        Spacer(Modifier.height(6.dp))
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            OutlinedTextField(
                value = southText,
                onValueChange = { southText = it; boundsError = null },
                modifier = Modifier.weight(1f),
                label = { Text(strings["area.south"]) },
                singleLine = true,
            )
            OutlinedTextField(
                value = northText,
                onValueChange = { northText = it; boundsError = null },
                modifier = Modifier.weight(1f),
                label = { Text(strings["area.north"]) },
                singleLine = true,
            )
        }
        boundsError?.let { message ->
            Spacer(Modifier.height(6.dp))
            Text(message, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
        }
        Spacer(Modifier.height(8.dp))
        OutlinedButton(
            onClick = {
                runCatching {
                    fun coordinate(value: String, name: String): Double =
                        value.trim().replace(',', '.').toDoubleOrNull()
                            ?: error(strings("error.coordinate", name))
                    sourceBounds.normalizedAreaFor(
                        GeoBounds(
                            west = coordinate(westText, "West"),
                            south = coordinate(southText, "Süd"),
                            east = coordinate(eastText, "Ost"),
                            north = coordinate(northText, "Nord"),
                        ),
                    )
                }.onSuccess { area ->
                    boundsError = null
                    onAreaChange(RectangleSelection(area))
                }.onFailure { error ->
                    boundsError = error.message ?: strings["error.bounding_box"]
                }
            },
            modifier = Modifier.fillMaxWidth(),
        ) {
            Text(strings["area.apply_coordinates"])
        }
        }
        Spacer(Modifier.height(14.dp))
        val normalizedBounds = selectedArea?.normalizedEnvelope(sourceBounds)
        DetailRow(strings["area.left"], normalizedBounds?.left?.let(::formatPercent) ?: "—")
        DetailRow(strings["area.top"], normalizedBounds?.top?.let(::formatPercent) ?: "—")
        DetailRow(strings["area.right"], normalizedBounds?.right?.let(::formatPercent) ?: "—")
        DetailRow(strings["area.bottom"], normalizedBounds?.bottom?.let(::formatPercent) ?: "—")
        DetailRow(
            strings["area.centre"],
            selectedMetrics?.let { "${formatCoordinate(it.centerLatitude)}, ${formatCoordinate(it.centerLongitude)}" } ?: "—",
        )
        DetailRow(strings["area.width"], selectedMetrics?.widthKilometers?.let(measurements::formatDistance) ?: "—")
        DetailRow(strings["area.height"], selectedMetrics?.heightKilometers?.let(measurements::formatDistance) ?: "—")
        if (selectedArea is CircleSelection) {
            DetailRow(strings["area.radius"], measurements.formatDistance(selectedArea.radiusKilometers))
        }
        DetailRow(strings["area.area"], selectedMetrics?.areaSquareKilometers?.let(measurements::formatArea) ?: "—", emphasized = true)
        DetailRow(
            strings["area.objects_estimated"],
            selectedEstimate?.let { strings("area.approx", measurements.formatCount(it.estimatedSourceObjectCount)) } ?: "—",
        )
        DetailRow(strings["area.size_estimated"], selectedEstimate?.let(::formatSizeRange) ?: "—")
        Spacer(Modifier.height(18.dp))
        Text(
            strings["area.geometry_hint"],
            color = MaterialTheme.colorScheme.onSurfaceVariant,
            style = MaterialTheme.typography.bodySmall,
        )
    }
        Box(
            modifier = Modifier.align(Alignment.CenterEnd).padding(vertical = 10.dp)
                .width(10.dp).fillMaxHeight()
                .background(MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(5.dp)),
        )
        VerticalScrollbar(
            adapter = rememberScrollbarAdapter(detailsScrollState),
            modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight().padding(vertical = 10.dp),
            style = ScrollbarStyle(
                minimalHeight = 36.dp,
                thickness = 10.dp,
                shape = RoundedCornerShape(5.dp),
                hoverDurationMillis = 180,
                unhoverColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.62f),
                hoverColor = MaterialTheme.colorScheme.tertiary,
            ),
        )
    }
}

private enum class MapInteractionMode { EDIT, ADD_RECTANGLE, ADD_CIRCLE, PAN }

private fun formatEditableCoordinate(value: Double): String =
    String.format(Locale.US, "%.7f", value).trimEnd('0').trimEnd('.')

private fun formatSizeRange(estimate: SelectionEstimate): String =
    "${formatFileSize(estimate.estimatedSizeLowerBytes)}–${formatFileSize(estimate.estimatedSizeUpperBytes)}"

private sealed interface SelectionDragMode {
    data object NewRectangle : SelectionDragMode
    data object NewCircle : SelectionDragMode
    data object Move : SelectionDragMode
    data class Resize(val corner: SelectionCorner) : SelectionDragMode
}

private fun radiusBetween(sourceBounds: GeoBounds, center: Offset, edge: Offset): Double {
    return distanceBetween(sourceBounds, center, edge)
        .coerceIn(MIN_CIRCLE_RADIUS_KM, MAX_CIRCLE_RADIUS_KM)
}

internal fun resizedCircleRadius(
    sourceBounds: GeoBounds,
    circle: CircleSelection,
    dragStart: Offset,
    dragEnd: Offset,
): Double {
    val center = Offset(circle.centerX, circle.centerY)
    val startDistance = distanceBetween(sourceBounds, center, dragStart)
    if (startDistance <= 1e-9) return circle.radiusKilometers
    val endDistance = distanceBetween(sourceBounds, center, dragEnd)
    return (circle.radiusKilometers * endDistance / startDistance)
        .coerceIn(MIN_CIRCLE_RADIUS_KM, MAX_CIRCLE_RADIUS_KM)
}

private fun distanceBetween(sourceBounds: GeoBounds, center: Offset, edge: Offset): Double {
    fun latitude(point: Offset) = sourceBounds.north - (sourceBounds.north - sourceBounds.south) * point.y
    fun longitude(point: Offset) = sourceBounds.west + (sourceBounds.east - sourceBounds.west) * point.x
    return greatCircleKilometers(latitude(center), longitude(center), latitude(edge), longitude(edge))
}

private const val SELECTION_HANDLE_HIT_RADIUS_PX = 18f
private const val CLICK_MOVE_TOLERANCE_PX = 5f
private const val DEFAULT_CIRCLE_RADIUS_KM = 5.0
private const val MIN_CIRCLE_RADIUS_KM = 0.01
private const val MAX_CIRCLE_RADIUS_KM = 10_000.0

private fun Rect.cornerNear(point: Offset, radius: Float): SelectionCorner? = listOf(
    SelectionCorner.TOP_LEFT to Offset(left, top),
    SelectionCorner.TOP_RIGHT to Offset(right, top),
    SelectionCorner.BOTTOM_LEFT to Offset(left, bottom),
    SelectionCorner.BOTTOM_RIGHT to Offset(right, bottom),
).minByOrNull { (_, corner) -> (corner - point).getDistance() }
    ?.takeIf { (_, corner) -> (corner - point).getDistance() <= radius }
    ?.first

@Composable
private fun PreviewLayerRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Checkbox(checked = checked, onCheckedChange = onCheckedChange)
        Text(label, style = MaterialTheme.typography.bodySmall)
    }
}

internal data class MapViewport(val left: Float, val top: Float, val right: Float, val bottom: Float) {
    val width: Float get() = right - left
    val height: Float get() = bottom - top

    fun zoomAt(relativeX: Float, relativeY: Float, factor: Float): MapViewport {
        val newWidth = (width * factor).coerceIn(MIN_VIEWPORT_SPAN, 1f)
        val newHeight = (height * factor).coerceIn(MIN_VIEWPORT_SPAN, 1f)
        val anchorX = left + relativeX.coerceIn(0f, 1f) * width
        val anchorY = top + relativeY.coerceIn(0f, 1f) * height
        return fromTopLeft(
            anchorX - relativeX * newWidth,
            anchorY - relativeY * newHeight,
            newWidth,
            newHeight,
        )
    }

    fun pan(deltaX: Float, deltaY: Float): MapViewport = fromTopLeft(left + deltaX, top + deltaY, width, height)

    private fun fromTopLeft(newLeft: Float, newTop: Float, newWidth: Float, newHeight: Float): MapViewport {
        val constrainedLeft = newLeft.coerceIn(0f, 1f - newWidth)
        val constrainedTop = newTop.coerceIn(0f, 1f - newHeight)
        return MapViewport(constrainedLeft, constrainedTop, constrainedLeft + newWidth, constrainedTop + newHeight)
    }

    companion object {
        val Full = MapViewport(0f, 0f, 1f, 1f)

        fun centeredAt(x: Float, y: Float, span: Float): MapViewport {
            val size = span.coerceIn(MIN_VIEWPORT_SPAN, 1f)
            val left = (x - size / 2f).coerceIn(0f, 1f - size)
            val top = (y - size / 2f).coerceIn(0f, 1f - size)
            return MapViewport(left, top, left + size, top + size)
        }

        fun focusedOn(area: NormalizedMapArea): MapViewport = centeredAt(
            x = (area.left + area.right) / 2f,
            y = (area.top + area.bottom) / 2f,
            span = maxOf(area.widthFraction, area.heightFraction) * 1.3f,
        )
    }
}

private const val MIN_VIEWPORT_SPAN = 0.004f

@Composable
private fun DetailRow(label: String, value: String, emphasized: Boolean = false) {
    Row(
        modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant)
        Text(
            value,
            color = if (emphasized) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurface,
            fontWeight = if (emphasized) FontWeight.Bold else FontWeight.Normal,
        )
    }
}
