package de.softwareapp_hb.privateqrscanner.data.scanner

import android.graphics.Rect
import android.os.SystemClock
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import de.softwareapp_hb.privateqrscanner.domain.ScanResult
import de.softwareapp_hb.privateqrscanner.util.readableBarcodePayload
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage

data class DetectionPoint(
    val x: Float,
    val y: Float
)

data class DetectionBox(
    val left: Float,
    val top: Float,
    val right: Float,
    val bottom: Float,
    val corners: List<DetectionPoint> = emptyList()
)

private data class TrackedDetection(
    val box: DetectionBox,
    val missCount: Int
)

class MlKitBarcodeAnalyzer(
    private val isAnalysisEnabled: () -> Boolean = { true },
    private val onDetectionStateChanged: (
        hasPotential: Boolean,
        hasReadable: Boolean,
        boxes: List<DetectionBox>,
        sourceWidth: Int,
        sourceHeight: Int
    ) -> Unit = { _, _, _, _, _ -> },
    private val onDetected: (
        result: ScanResult,
        readableBox: DetectionBox?,
        sourceWidth: Int,
        sourceHeight: Int
    ) -> Unit
) : ImageAnalysis.Analyzer, AutoCloseable {
    private companion object {
        const val MATCH_DISTANCE_THRESHOLD = 0.18f
        const val BOX_SMOOTHING_ALPHA = 0.65f
        const val MAX_MISSED_FRAMES = 1
        const val MIN_ANALYSIS_INTERVAL_MS = 30L
        const val MIN_STATE_PUBLISH_INTERVAL_MS = 33L
    }

    private val scanner = BarcodeScanning.getClient(
        BarcodeScannerOptions.Builder()
            .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
            .enableAllPotentialBarcodes()
            .build()
    )
    @Volatile
    private var processing = false
    @Volatile
    private var lastAnalysisStartMs = 0L
    @Volatile
    private var lastStatePublishMs = 0L
    @Volatile
    private var lastHasPotential = false
    @Volatile
    private var lastHasReadable = false
    @Volatile
    private var lastBoxes: List<DetectionBox> = emptyList()
    @Volatile
    private var lastSourceWidth = 0
    @Volatile
    private var lastSourceHeight = 0
    private var trackedDetections: List<TrackedDetection> = emptyList()
    private var trackedSourceWidth = 0
    private var trackedSourceHeight = 0

    override fun analyze(imageProxy: ImageProxy) {
        if (!isAnalysisEnabled()) {
            trackedDetections = emptyList()
            trackedSourceWidth = 0
            trackedSourceHeight = 0
            publishDetectionState(
                hasPotential = false,
                hasReadable = false,
                boxes = emptyList(),
                sourceWidth = 0,
                sourceHeight = 0
            )
            imageProxy.close()
            return
        }
        val nowMs = SystemClock.elapsedRealtime()
        if (nowMs - lastAnalysisStartMs < MIN_ANALYSIS_INTERVAL_MS) {
            imageProxy.close()
            return
        }
        if (processing) {
            imageProxy.close()
            return
        }

        val mediaImage = imageProxy.image ?: run {
            imageProxy.close()
            return
        }
        val rotationDegrees = imageProxy.imageInfo.rotationDegrees
        val sourceWidth = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.height else imageProxy.width
        val sourceHeight = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.width else imageProxy.height
        val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
        processing = true
        lastAnalysisStartMs = nowMs

        scanner.process(image)
            .addOnSuccessListener { barcodes ->
                val readable = barcodes.firstOrNull { it.readableBarcodePayload() != null }
                val boxes = barcodes.mapNotNull { barcode ->
                    val bounds = barcode.boundingBox ?: return@mapNotNull null
                    val normalized = normalizeBoundingBox(
                        rect = bounds,
                        sourceWidth = sourceWidth,
                        sourceHeight = sourceHeight
                    ) ?: return@mapNotNull null
                    val normalizedCorners = barcode.cornerPoints?.map { p ->
                        normalizePoint(
                            x = p.x.toFloat(),
                            y = p.y.toFloat(),
                            sourceWidth = sourceWidth,
                            sourceHeight = sourceHeight
                        )
                    }?.filterNotNull() ?: emptyList()
                    normalized.copy(corners = normalizedCorners)
                }
                val stabilizedBoxes = stabilizeBoxes(boxes, sourceWidth, sourceHeight)
                publishDetectionState(
                    hasPotential = stabilizedBoxes.isNotEmpty(),
                    hasReadable = readable != null,
                    boxes = stabilizedBoxes,
                    sourceWidth = sourceWidth,
                    sourceHeight = sourceHeight
                )
                if (readable != null) {
                    val payload = readable.readableBarcodePayload() ?: return@addOnSuccessListener
                    val readableBox = readable.boundingBox?.let { bounds ->
                        val normalized = normalizeBoundingBox(
                            rect = bounds,
                            sourceWidth = sourceWidth,
                            sourceHeight = sourceHeight
                        )
                        val normalizedCorners = readable.cornerPoints?.map { p ->
                            normalizePoint(
                                x = p.x.toFloat(),
                                y = p.y.toFloat(),
                                sourceWidth = sourceWidth,
                                sourceHeight = sourceHeight
                            )
                        }?.filterNotNull() ?: emptyList()
                        normalized?.copy(corners = normalizedCorners)
                    }
                    val gatedReadableBox = readableBox?.let { rb ->
                        stabilizedBoxes.minByOrNull { distanceBetweenCenters(it, rb) } ?: rb
                    }
                    onDetected(
                        ScanResult(
                            content = payload.content,
                            type = readable.valueType.toHumanType(),
                            isBase64Encoded = payload.isBase64Encoded
                        ),
                        gatedReadableBox,
                        sourceWidth,
                        sourceHeight
                    )
                }
            }
            .addOnFailureListener {
                val sourceW = trackedSourceWidth
                val sourceH = trackedSourceHeight
                val stabilizedBoxes = if (sourceW > 0 && sourceH > 0) {
                    stabilizeBoxes(emptyList(), sourceW, sourceH)
                } else {
                    trackedDetections = emptyList()
                    emptyList()
                }
                publishDetectionState(
                    hasPotential = stabilizedBoxes.isNotEmpty(),
                    hasReadable = false,
                    boxes = stabilizedBoxes,
                    sourceWidth = if (stabilizedBoxes.isNotEmpty()) sourceW else 0,
                    sourceHeight = if (stabilizedBoxes.isNotEmpty()) sourceH else 0
                )
            }
            .addOnCompleteListener {
                processing = false
                imageProxy.close()
            }
    }

    private fun Int.toHumanType(): String = when (this) {
        Barcode.TYPE_CONTACT_INFO -> "Contact"
        Barcode.TYPE_EMAIL -> "Email"
        Barcode.TYPE_ISBN -> "ISBN"
        Barcode.TYPE_PHONE -> "Phone"
        Barcode.TYPE_PRODUCT -> "Product"
        Barcode.TYPE_SMS -> "SMS"
        Barcode.TYPE_TEXT -> "Text"
        Barcode.TYPE_URL -> "URL"
        Barcode.TYPE_WIFI -> "WiFi"
        Barcode.TYPE_GEO -> "Geo"
        Barcode.TYPE_CALENDAR_EVENT -> "Calendar"
        Barcode.TYPE_DRIVER_LICENSE -> "Driver license"
        else -> "Unknown"
    }

    override fun close() {
        scanner.close()
    }

    private fun publishDetectionState(
        hasPotential: Boolean,
        hasReadable: Boolean,
        boxes: List<DetectionBox>,
        sourceWidth: Int,
        sourceHeight: Int
    ) {
        val nowMs = SystemClock.elapsedRealtime()
        if (
            lastHasPotential == hasPotential &&
            lastHasReadable == hasReadable &&
            lastBoxes == boxes &&
            lastSourceWidth == sourceWidth &&
            lastSourceHeight == sourceHeight
        ) return
        val isStateKindUnchanged = lastHasPotential == hasPotential &&
            lastHasReadable == hasReadable &&
            lastSourceWidth == sourceWidth &&
            lastSourceHeight == sourceHeight
        if (isStateKindUnchanged && nowMs - lastStatePublishMs < MIN_STATE_PUBLISH_INTERVAL_MS) {
            return
        }
        lastHasPotential = hasPotential
        lastHasReadable = hasReadable
        lastBoxes = boxes
        lastSourceWidth = sourceWidth
        lastSourceHeight = sourceHeight
        lastStatePublishMs = nowMs
        onDetectionStateChanged(hasPotential, hasReadable, boxes, sourceWidth, sourceHeight)
    }

    private fun normalizeBoundingBox(
        rect: Rect,
        sourceWidth: Int,
        sourceHeight: Int
    ): DetectionBox? {
        if (sourceWidth <= 0 || sourceHeight <= 0) return null
        val outWidth = sourceWidth.toFloat()
        val outHeight = sourceHeight.toFloat()
        return DetectionBox(
            left = (rect.left.toFloat() / outWidth).coerceIn(0f, 1f),
            top = (rect.top.toFloat() / outHeight).coerceIn(0f, 1f),
            right = (rect.right.toFloat() / outWidth).coerceIn(0f, 1f),
            bottom = (rect.bottom.toFloat() / outHeight).coerceIn(0f, 1f)
        )
    }

    private fun normalizePoint(
        x: Float,
        y: Float,
        sourceWidth: Int,
        sourceHeight: Int
    ): DetectionPoint? {
        if (sourceWidth <= 0 || sourceHeight <= 0) return null
        val outWidth = sourceWidth.toFloat()
        val outHeight = sourceHeight.toFloat()
        return DetectionPoint(
            x = (x / outWidth).coerceIn(0f, 1f),
            y = (y / outHeight).coerceIn(0f, 1f)
        )
    }

    private fun stabilizeBoxes(
        rawBoxes: List<DetectionBox>,
        sourceWidth: Int,
        sourceHeight: Int
    ): List<DetectionBox> {
        if (sourceWidth <= 0 || sourceHeight <= 0) {
            trackedDetections = emptyList()
            trackedSourceWidth = 0
            trackedSourceHeight = 0
            return emptyList()
        }

        if (trackedSourceWidth != sourceWidth || trackedSourceHeight != sourceHeight) {
            trackedDetections = emptyList()
        }
        trackedSourceWidth = sourceWidth
        trackedSourceHeight = sourceHeight

        val remaining = trackedDetections.toMutableList()
        val next = mutableListOf<TrackedDetection>()

        rawBoxes.forEach { raw ->
            val matchIndex = remaining.indices.minByOrNull { idx ->
                distanceBetweenCenters(remaining[idx].box, raw)
            } ?: -1
            if (matchIndex >= 0) {
                val previous = remaining[matchIndex]
                val distance = distanceBetweenCenters(previous.box, raw)
                if (distance <= MATCH_DISTANCE_THRESHOLD) {
                    remaining.removeAt(matchIndex)
                    next += TrackedDetection(
                        box = smoothBox(previous.box, raw, BOX_SMOOTHING_ALPHA),
                        missCount = 0
                    )
                } else {
                    next += TrackedDetection(raw, 0)
                }
            } else {
                next += TrackedDetection(raw, 0)
            }
        }

        remaining.forEach { stale ->
            val misses = stale.missCount + 1
            if (misses <= MAX_MISSED_FRAMES) {
                next += stale.copy(missCount = misses)
            }
        }

        trackedDetections = next
        return next.map { it.box }
    }

    private fun distanceBetweenCenters(a: DetectionBox, b: DetectionBox): Float {
        val ax = (a.left + a.right) * 0.5f
        val ay = (a.top + a.bottom) * 0.5f
        val bx = (b.left + b.right) * 0.5f
        val by = (b.top + b.bottom) * 0.5f
        val dx = ax - bx
        val dy = ay - by
        return kotlin.math.sqrt(dx * dx + dy * dy)
    }

    private fun smoothBox(previous: DetectionBox, current: DetectionBox, alpha: Float): DetectionBox {
        fun lerp(start: Float, end: Float): Float = start + (end - start) * alpha

        val corners = if (previous.corners.isNotEmpty() && previous.corners.size == current.corners.size) {
            previous.corners.zip(current.corners).map { (p, c) ->
                DetectionPoint(
                    x = lerp(p.x, c.x),
                    y = lerp(p.y, c.y)
                )
            }
        } else if (current.corners.isNotEmpty()) {
            current.corners
        } else {
            previous.corners
        }

        return DetectionBox(
            left = lerp(previous.left, current.left).coerceIn(0f, 1f),
            top = lerp(previous.top, current.top).coerceIn(0f, 1f),
            right = lerp(previous.right, current.right).coerceIn(0f, 1f),
            bottom = lerp(previous.bottom, current.bottom).coerceIn(0f, 1f),
            corners = corners
        )
    }
}
