package com.performancecoachlab.posedetection.camera

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageProxy
import androidx.compose.ui.geometry.Rect
import androidx.core.graphics.createBitmap
import co.touchlab.kermit.Logger
import com.google.android.gms.tasks.Tasks
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.pose.Pose
import com.google.mlkit.vision.pose.PoseDetector
import com.google.mlkit.vision.pose.PoseLandmark
import com.performancecoachlab.posedetection.custom.AndroidDetector
import com.performancecoachlab.posedetection.custom.ModelInfo
import com.performancecoachlab.posedetection.recording.AnalysisObject
import com.performancecoachlab.posedetection.recording.AnalysisResult
import com.performancecoachlab.posedetection.recording.FrameSize
import com.performancecoachlab.posedetection.skeleton.Skeleton
import org.tensorflow.lite.DataType
import org.tensorflow.lite.support.common.ops.CastOp
import org.tensorflow.lite.support.common.ops.NormalizeOp
import org.tensorflow.lite.support.image.ImageProcessor
import org.tensorflow.lite.support.image.TensorImage
import org.tensorflow.lite.support.tensorbuffer.TensorBuffer
import java.nio.ByteBuffer
import java.util.concurrent.Executors
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min

actual enum class PlatformType {
    ANDROID, IOS;

    actual companion object {
        actual fun getCurrentPlatform(): PlatformType = ANDROID
    }
}

private object BitmapPool {
    private var cached: Bitmap? = null
    private var cachedW: Int = 0
    private var cachedH: Int = 0
    private var cachedConfig: Bitmap.Config = Bitmap.Config.ARGB_8888

    fun obtain(width: Int, height: Int, config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
        val bmp = cached
        return if (
            bmp != null &&
            !bmp.isRecycled &&
            cachedW == width &&
            cachedH == height &&
            cachedConfig == config
        ) {
            bmp.eraseColor(android.graphics.Color.TRANSPARENT)
            bmp
        } else {
            createBitmap(width, height, config).also { newBmp ->
                cached = newBmp
                cachedW = width
                cachedH = height
                cachedConfig = config
            }
        }
    }
}

/**
 * Separate pool for analysis/masking bitmaps (must match analysisBitmap size).
 * We keep a single instance because analysis runs on a single-thread executor.
 */
private object AnalysisBitmapPool {
    private var cached: Bitmap? = null
    private var cachedW: Int = 0
    private var cachedH: Int = 0
    private var cachedConfig: Bitmap.Config = Bitmap.Config.ARGB_8888

    fun obtain(width: Int, height: Int, config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
        val bmp = cached
        return if (
            bmp != null &&
            !bmp.isRecycled &&
            cachedW == width &&
            cachedH == height &&
            cachedConfig == config
        ) {
            // Clear the bitmap; canvas draw will overwrite but masking may not cover entire area.
            bmp.eraseColor(android.graphics.Color.TRANSPARENT)
            bmp
        } else {
            createBitmap(width, height, config).also { newBmp ->
                cached = newBmp
                cachedW = width
                cachedH = height
                cachedConfig = config
            }
        }
    }
}

private object RgbaBufferPool {
    private var packed: ByteBuffer? = null
    private var packedCapacity: Int = 0

    fun packedBuffer(requiredBytes: Int): ByteBuffer {
        val buf = packed
        return if (buf != null && packedCapacity >= requiredBytes) {
            buf.clear()
            buf
        } else {
            ByteBuffer.allocateDirect(requiredBytes).also {
                packed = it
                packedCapacity = requiredBytes
            }
        }
    }
}

private fun Bitmap.rotateToNew(degrees: Int): Bitmap {
    if (degrees % 360 == 0) return this
    val m = Matrix().apply { postRotate(degrees.toFloat()) }
    return Bitmap.createBitmap(this, 0, 0, width, height, m, true)
}

/** Separate pool for the rotated bitmap so it never collides with the base AnalysisBitmapPool. */
private object RotatedBitmapPool {
    private var cached: Bitmap? = null
    private var cachedW: Int = 0
    private var cachedH: Int = 0
    private var cachedConfig: Bitmap.Config = Bitmap.Config.ARGB_8888

    fun obtain(width: Int, height: Int, config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
        val bmp = cached
        return if (
            bmp != null &&
            !bmp.isRecycled &&
            cachedW == width &&
            cachedH == height &&
            cachedConfig == config
        ) {
            bmp.eraseColor(android.graphics.Color.TRANSPARENT)
            bmp
        } else {
            createBitmap(width, height, config).also { newBmp ->
                cached = newBmp
                cachedW = width
                cachedH = height
                cachedConfig = config
            }
        }
    }
}

private fun Bitmap.rotateIntoPooled(degrees: Int): Bitmap {
    if (degrees % 360 == 0) return this
    val outW = if (degrees % 180 == 0) width else height
    val outH = if (degrees % 180 == 0) height else width

    val out = RotatedBitmapPool.obtain(outW, outH, this.config ?: Bitmap.Config.ARGB_8888)
    val c = Canvas(out)
    val m = Matrix().apply {
        postRotate(degrees.toFloat())
        // Translate back into view.
        when (degrees % 360) {
            90 -> postTranslate(outW.toFloat(), 0f)
            180 -> postTranslate(outW.toFloat(), outH.toFloat())
            270 -> postTranslate(0f, outH.toFloat())
        }
    }
    c.drawBitmap(this, m, Paint(Paint.FILTER_BITMAP_FLAG))
    return out
}

private fun resizeInto(src: Bitmap, dst: Bitmap) {
    val c = Canvas(dst)
    val paint = Paint(Paint.FILTER_BITMAP_FLAG)
    c.drawBitmap(src, null, android.graphics.Rect(0, 0, dst.width, dst.height), paint)
}

/**
 * Convert an RGBA_8888 ImageProxy (from ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
 * into a Bitmap. Reuses buffers/bitmaps to keep GC pressure low.
 */
private fun ImageProxy.toRgbaBitmapPooled(): Bitmap {
    val plane = planes.firstOrNull() ?: error("ImageProxy has no planes")
    val buffer: ByteBuffer = plane.buffer
    buffer.rewind()

    val rowStride = plane.rowStride
    val pixelStride = plane.pixelStride
    // For OUTPUT_IMAGE_FORMAT_RGBA_8888 this should be 4.
    if (pixelStride != 4) {
        // Fallback: still attempt a direct copy into a fresh bitmap.
        return createBitmap(width, height).also { it.copyPixelsFromBuffer(buffer) }
    }

    val bmp = AnalysisBitmapPool.obtain(width, height, Bitmap.Config.ARGB_8888)

    if (rowStride == width * 4) {
        bmp.copyPixelsFromBuffer(buffer)
        return bmp
    }

    val required = width * height * 4
    val packed = RgbaBufferPool.packedBuffer(required)

    val rowBytes = width * 4
    val row = ByteArray(rowBytes)
    val skip = ByteArray(rowStride - rowBytes)
    for (y in 0 until height) {
        buffer.get(row)
        packed.put(row)
        if (y < height - 1) buffer.get(skip)
    }
    packed.flip()
    bmp.copyPixelsFromBuffer(packed)
    return bmp
}

@OptIn(ExperimentalGetImage::class)
fun ImageProxy.process(
    objectDetector: AndroidDetector?,
    poseDetector: PoseDetector?,
    timestamp: Long,
    focusArea: Rect?,
    poseFocusMode: PoseFocusMode = PoseFocusMode.MASK,
    onComplete: (AnalysisResult, Bitmap) -> Unit
) {
    // If both are null, still close to avoid stalling the camera pipeline.
    if (objectDetector == null && poseDetector == null) {
        close()
        return
    }

    val rotationDegrees = imageInfo.rotationDegrees

    // Convert to bitmap ASAP so we can close the ImageProxy immediately.
    val baseBitmap: Bitmap = toRgbaBitmapPooled()
    close()

    // Rotate into a pooled bitmap if needed.
    val analysisBitmap: Bitmap = baseBitmap.rotateIntoPooled(rotationDegrees)

    // 2) Pose input: build MLKit image (optionally masked) and DOWNscale it.
    // Rotation is now 0 because analysisBitmap is upright.
    val mlKitPoseInput: MlKitPoseInput? = poseDetector?.let {
        buildMlKitPoseInput(
            analysisBitmap = analysisBitmap,
            focusArea = focusArea,
            poseFocusMode = poseFocusMode,
            downscaleMaxSidePx = 256
        )
    }

    // 3) Tensor input: resize into pooled bitmap (avoid allocating each frame).
    val processedTensorImage: TensorImage? = objectDetector?.modelInfo?.let { interpreter ->
        val tensorWidth = interpreter.inputWidth
        val tensorHeight = interpreter.inputHeight
        if (tensorWidth <= 0 || tensorHeight <= 0) return@let null

        val tensorBitmap = BitmapPool.obtain(tensorWidth, tensorHeight, Bitmap.Config.ARGB_8888)
        resizeInto(analysisBitmap, tensorBitmap)

        TensorImage(DataType.FLOAT32).also { ti ->
            ti.load(tensorBitmap)
        }.let { ti ->
            imageProcessor.process(ti)
        }
    }

    process(
        tensorImage = processedTensorImage,
        mlKitImage = mlKitPoseInput?.image,
        objectDetector = objectDetector,
        poseDetector = poseDetector,
        timestamp = timestamp,
        width = analysisBitmap.width,
        height = analysisBitmap.height,
        bitmap = analysisBitmap,
        onComplete = onComplete,
        mlKitScaleX = mlKitPoseInput?.scaleX ?: 1f,
        mlKitScaleY = mlKitPoseInput?.scaleY ?: 1f,
        mlKitOffsetX = mlKitPoseInput?.offsetX ?: 0f,
        mlKitOffsetY = mlKitPoseInput?.offsetY ?: 0f,
    )
}

// Shared executor for running pose detection in parallel with object detection.
private val poseExecutor = Executors.newSingleThreadExecutor()

// Core processing used by both live camera (ImageProxy) and offline frames (FrameAnalyser).
fun process(
    tensorImage: TensorImage?,
    mlKitImage: InputImage?,
    objectDetector: AndroidDetector?,
    poseDetector: PoseDetector?,
    timestamp: Long,
    width: Int,
    height: Int,
    bitmap: Bitmap,
    onComplete: (AnalysisResult, Bitmap) -> Unit,
    mlKitScaleX: Float = 1f,
    mlKitScaleY: Float = 1f,
    mlKitOffsetX: Float = 0f,
    mlKitOffsetY: Float = 0f,
) {
    // Launch pose detection on a separate thread so it runs in parallel with object detection.
    val poseFuture = if (poseDetector != null && mlKitImage != null) {
        poseExecutor.submit<Skeleton?> {
            runCatching {
                val pose = Tasks.await(poseDetector.process(mlKitImage))
                skeletonFromPoseScaled(
                    pose = pose,
                    timestamp = timestamp,
                    width = width,
                    height = height,
                    scaleX = mlKitScaleX,
                    scaleY = mlKitScaleY,
                    offsetX = mlKitOffsetX,
                    offsetY = mlKitOffsetY,
                )
            }.onFailure { t ->
                Logger.e(t) { "MLKit poseDetector.process failed" }
            }.getOrNull()
        }
    } else null

    // Run object detection on the current thread (concurrent with pose above).
    val objectsDetected = if (objectDetector != null && tensorImage != null) {
        val outputShape = objectDetector.modelInfo.outputShape
        val output = TensorBuffer.createFixedSize(outputShape, DataType.FLOAT32)

        try {
            objectDetector.interpreter.run(tensorImage.buffer, output.buffer)
        } catch (e: Exception) {
            Logger.e(e) { "TFLite interpreter.run failed" }
            val skeleton = poseFuture?.get()
            onComplete(AnalysisResult(skeleton, emptyList(), timestamp), bitmap)
            return
        }

        val array = output.floatArray
        if (outputShape.size != 3) emptyList() else {
            val dim1 = outputShape[1]
            val dim2 = outputShape[2]

            val elements: Int
            val channels: Int
            val isElementsFirst: Boolean

            when {
                dim2 == 6 -> {
                    elements = dim1
                    channels = dim2
                    isElementsFirst = true
                }

                dim1 == 6 -> {
                    channels = dim1
                    elements = dim2
                    isElementsFirst = false
                }

                else -> {
                    val skeleton = poseFuture?.get()
                    return onComplete(
                        AnalysisResult(skeleton = skeleton, objects = emptyList(), timestamp = timestamp),
                        bitmap
                    )
                }
            }

            fun valueAt(elementIndex: Int, channelIndex: Int): Float {
                return if (isElementsFirst) {
                    array[elementIndex * channels + channelIndex]
                } else {
                    array[channelIndex * elements + elementIndex]
                }
            }

            (0 until elements).mapNotNull { i ->
                val cnf = valueAt(i, 4)
                if (cnf > 0.25f) {
                    val x1 = valueAt(i, 0)
                    val y1 = valueAt(i, 1)
                    val x2 = valueAt(i, 2)
                    val y2 = valueAt(i, 3)
                    val cls = valueAt(i, 5).toInt()

                    val leftN = min(x1, x2).coerceIn(0f, 1f)
                    val topN = min(y1, y2).coerceIn(0f, 1f)
                    val rightN = max(x1, x2).coerceIn(0f, 1f)
                    val bottomN = max(y1, y2).coerceIn(0f, 1f)

                    val leftPx = leftN * width
                    val topPx = topN * height
                    val rightPx = rightN * width
                    val bottomPx = bottomN * height

                    val label = objectDetector.modelInfo.label(cls)

                    AnalysisObject(
                        boundingBox = Rect(
                            left = leftPx,
                            top = topPx,
                            right = rightPx,
                            bottom = bottomPx
                        ),
                        trackingId = 0,
                        labels = listOf(
                            com.performancecoachlab.posedetection.recording.Label(
                                label,
                                cnf
                            )
                        ),
                        frameSize = FrameSize(
                            width = width.absoluteValue,
                            height = height.absoluteValue
                        ),
                        timestamp = timestamp
                    )
                } else null
            }
        }
    } else emptyList()

    // Wait for pose result (already running or already done).
    val skeleton = poseFuture?.get()

    onComplete(
        AnalysisResult(
            skeleton = skeleton,
            objects = objectsDetected,
            timestamp = timestamp
        ),
        bitmap
    )
}

private fun Rect?.toGraphicsRect(width: Int, height: Int): android.graphics.Rect {
    return this?.let {
        android.graphics.Rect(
            (it.left * width).toInt(),
            (it.top * height).toInt(),
            (it.right * width).toInt(),
            (it.bottom * height).toInt()
        )
    } ?: android.graphics.Rect(0, 0, width, height)
}

private val imageProcessor = ImageProcessor.Builder()
    .add(NormalizeOp(0f, 255f))
    .add(CastOp(DataType.FLOAT32))
    .build()

fun Bitmap.cropToFocusArea(focusArea: Rect?): Bitmap {
    return focusArea?.let { rect ->
        val left = (rect.left * width.toFloat()).toInt().coerceIn(0, width)
        val top = (rect.top * height.toFloat()).toInt().coerceIn(0, height)
        val right = (rect.right * width.toFloat()).toInt().coerceIn(left, width)
        val bottom = (rect.bottom * height.toFloat()).toInt().coerceIn(top, height)

        val cropWidth = right - left
        val cropHeight = bottom - top

        if (cropWidth > 0 && cropHeight > 0) {
            Bitmap.createBitmap(this, left, top, cropWidth, cropHeight)
        } else {
            this
        }
    } ?: this
}


fun Bitmap.applyFocusAreaMask(focusArea: Rect?, angle: Int = 0): Bitmap {
    // Keep old API for call sites that truly want a new bitmap.
    return focusArea?.let { rect ->
        val result = this.copy(this.config ?: Bitmap.Config.ARGB_8888, true)
        val canvas = Canvas(result)
        val paint = Paint().apply {
            color = android.graphics.Color.BLACK
        }
        // Transform the rectangle coordinates based on the angle
        val transformedRect = when (angle % 360) {
            90 -> Rect(
                left = rect.top,
                top = 1f - rect.right,
                right = rect.bottom,
                bottom = 1f - rect.left
            )

            180 -> Rect(
                left = 1f - rect.right,
                top = 1f - rect.bottom,
                right = 1f - rect.left,
                bottom = 1f - rect.top
            )

            270 -> Rect(
                left = 1f - rect.bottom,
                top = rect.left,
                right = 1f - rect.top,
                bottom = rect.right
            )

            else -> rect // 0 degrees or any other angle
        }

        val focusRect = transformedRect.toGraphicsRect(width, height)

        // Black out top area
        if (focusRect.top > 0) {
            canvas.drawRect(0f, 0f, width.toFloat(), focusRect.top.toFloat(), paint)
        }

        // Black out bottom area
        if (focusRect.bottom < height) {
            canvas.drawRect(
                0f,
                focusRect.bottom.toFloat(),
                width.toFloat(),
                height.toFloat(),
                paint
            )
        }

        // Black out left area
        if (focusRect.left > 0) {
            canvas.drawRect(
                0f,
                focusRect.top.toFloat(),
                focusRect.left.toFloat(),
                focusRect.bottom.toFloat(),
                paint
            )
        }

        // Black out right area
        if (focusRect.right < width) {
            canvas.drawRect(
                focusRect.right.toFloat(),
                focusRect.top.toFloat(),
                width.toFloat(),
                focusRect.bottom.toFloat(),
                paint
            )
        }

        result
    } ?: this
}

/** Pool solely for pose-masked bitmaps so we never overwrite the analysis bitmap used by object detection. */
private object PoseMaskBitmapPool {
    private var cached: Bitmap? = null
    private var cachedW: Int = 0
    private var cachedH: Int = 0
    private var cachedConfig: Bitmap.Config = Bitmap.Config.ARGB_8888

    fun obtain(width: Int, height: Int, config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
        val bmp = cached
        return if (
            bmp != null &&
            !bmp.isRecycled &&
            cachedW == width &&
            cachedH == height &&
            cachedConfig == config
        ) {
            bmp.eraseColor(android.graphics.Color.TRANSPARENT)
            bmp
        } else {
            createBitmap(width, height, config).also { newBmp ->
                cached = newBmp
                cachedW = width
                cachedH = height
                cachedConfig = config
            }
        }
    }
}

/** In-place masking onto the receiver bitmap; assumes the receiver already contains the frame pixels. */
private fun Bitmap.applyFocusAreaMaskInPlace(focusArea: Rect, angle: Int = 0) {
    val area = focusArea.normalizedClamped()
    if (area.isEffectivelyFullViewport()) return
    if (area.width <= 1e-4f || area.height <= 1e-4f) return

    val canvas = Canvas(this)
    val paint = Paint().apply { color = android.graphics.Color.BLACK }

    val transformedRect = when (angle % 360) {
        90 -> Rect(
            left = area.top,
            top = 1f - area.right,
            right = area.bottom,
            bottom = 1f - area.left
        )

        180 -> Rect(
            left = 1f - area.right,
            top = 1f - area.bottom,
            right = 1f - area.left,
            bottom = 1f - area.top
        )

        270 -> Rect(
            left = 1f - area.bottom,
            top = area.left,
            right = 1f - area.top,
            bottom = area.right
        )

        else -> area
    }

    val focusRect = transformedRect.toGraphicsRect(width, height)

    if (focusRect.top > 0) canvas.drawRect(0f, 0f, width.toFloat(), focusRect.top.toFloat(), paint)
    if (focusRect.bottom < height) canvas.drawRect(
        0f,
        focusRect.bottom.toFloat(),
        width.toFloat(),
        height.toFloat(),
        paint
    )
    if (focusRect.left > 0) canvas.drawRect(
        0f,
        focusRect.top.toFloat(),
        focusRect.left.toFloat(),
        focusRect.bottom.toFloat(),
        paint
    )
    if (focusRect.right < width) canvas.drawRect(
        focusRect.right.toFloat(),
        focusRect.top.toFloat(),
        width.toFloat(),
        focusRect.bottom.toFloat(),
        paint
    )
}

// Keep applyFocusAreaMaskPooled for other call sites, but implement it via the in-place helper.
fun Bitmap.applyFocusAreaMaskPooled(focusArea: Rect?, angle: Int = 0): Bitmap {
    if (focusArea == null) return this

    val out = AnalysisBitmapPool.obtain(width, height, this.config ?: Bitmap.Config.ARGB_8888)
    val canvas = Canvas(out)
    canvas.drawBitmap(this, 0f, 0f, null)

    out.applyFocusAreaMaskInPlace(focusArea, angle)
    return out
}

data class BoundingBox(
    val x1: Float,
    val y1: Float,
    val x2: Float,
    val y2: Float,
    val cnf: Float,
    val cls: Int,
    val clsName: String
)

fun ModelInfo.label(cls: Int): String {
    this.labels.let { labelsList ->
        if (cls in labelsList.indices) {
            return labelsList[cls]
        }
    }
    return "$cls"
}

private fun Rect.isEffectivelyFullViewport(eps: Float = 1e-4f): Boolean {
    return left <= 0f + eps && top <= 0f + eps && right >= 1f - eps && bottom >= 1f - eps
}

private fun Rect.normalizedClamped(): Rect {
    val l = left.coerceIn(0f, 1f)
    val t = top.coerceIn(0f, 1f)
    val r = right.coerceIn(0f, 1f)
    val b = bottom.coerceIn(0f, 1f)
    val leftN = min(l, r)
    val rightN = max(l, r)
    val topN = min(t, b)
    val bottomN = max(t, b)
    return Rect(left = leftN, top = topN, right = rightN, bottom = bottomN)
}

/** Pool for the (smaller) MLKit pose input bitmap. */
private object PoseInputBitmapPool {
    private var cached: Bitmap? = null
    private var cachedW: Int = 0
    private var cachedH: Int = 0

    fun obtain(width: Int, height: Int): Bitmap {
        val bmp = cached
        return if (bmp != null && !bmp.isRecycled && cachedW == width && cachedH == height) {
            bmp.eraseColor(android.graphics.Color.TRANSPARENT)
            bmp
        } else {
            createBitmap(width, height, Bitmap.Config.ARGB_8888).also {
                cached = it
                cachedW = width
                cachedH = height
            }
        }
    }
}

private data class MlKitPoseInput(
    val image: InputImage,
    val scaleX: Float,
    val scaleY: Float,
    val offsetX: Float = 0f,
    val offsetY: Float = 0f,
)

private fun buildMlKitPoseInput(
    analysisBitmap: Bitmap,
    focusArea: Rect?,
    poseFocusMode: PoseFocusMode,
    downscaleMaxSidePx: Int = 360,
): MlKitPoseInput {
    // 1) Build the pose source bitmap — mask (black out non-focus region) or
    //    crop (geometrically restrict to focus region). Crop gives the model
    //    higher effective resolution of the focus area at the same downscale.
    //
    //    Crop-mode also records offsetX/offsetY in full-bitmap pixel coords so
    //    landmarks (which come back in crop-local coords) can be remapped.
    val useCrop = focusArea != null && poseFocusMode == PoseFocusMode.CROP

    val offsetX: Float
    val offsetY: Float
    val poseBitmapFull: Bitmap = when {
        focusArea == null -> {
            offsetX = 0f
            offsetY = 0f
            analysisBitmap
        }
        useCrop -> {
            val w = analysisBitmap.width
            val h = analysisBitmap.height
            val leftPx = (focusArea.left * w).toInt().coerceIn(0, w)
            val topPx = (focusArea.top * h).toInt().coerceIn(0, h)
            val rightPx = (focusArea.right * w).toInt().coerceIn(leftPx, w)
            val bottomPx = (focusArea.bottom * h).toInt().coerceIn(topPx, h)
            val cropW = rightPx - leftPx
            val cropH = bottomPx - topPx
            if (cropW <= 0 || cropH <= 0) {
                // Degenerate focus rect — fall back to full frame untouched.
                offsetX = 0f
                offsetY = 0f
                analysisBitmap
            } else {
                offsetX = leftPx.toFloat()
                offsetY = topPx.toFloat()
                Bitmap.createBitmap(analysisBitmap, leftPx, topPx, cropW, cropH)
            }
        }
        else -> {
            offsetX = 0f
            offsetY = 0f
            val out = PoseMaskBitmapPool.obtain(
                analysisBitmap.width,
                analysisBitmap.height,
                analysisBitmap.config ?: Bitmap.Config.ARGB_8888
            )
            Canvas(out).drawBitmap(analysisBitmap, 0f, 0f, null)
            out.applyFocusAreaMaskInPlace(focusArea = focusArea, angle = 0)
            out
        }
    }

    // 2) Downscale for MLKit (pose is generally robust at lower res).
    val srcW = poseBitmapFull.width
    val srcH = poseBitmapFull.height
    val srcMax = max(srcW, srcH)

    if (downscaleMaxSidePx <= 0 || srcMax <= downscaleMaxSidePx) {
        return MlKitPoseInput(
            image = InputImage.fromBitmap(poseBitmapFull, 0),
            scaleX = 1f,
            scaleY = 1f,
            offsetX = offsetX,
            offsetY = offsetY,
        )
    }

    val scale = downscaleMaxSidePx.toFloat() / srcMax.toFloat()
    val dstW = max(1, (srcW * scale).toInt())
    val dstH = max(1, (srcH * scale).toInt())

    val downscaled = PoseInputBitmapPool.obtain(dstW, dstH)
    resizeInto(poseBitmapFull, downscaled)

    // If we scaled down, x/y returned by MLKit are in downscaled pixel coords.
    val scaleBackX = srcW.toFloat() / dstW.toFloat()
    val scaleBackY = srcH.toFloat() / dstH.toFloat()

    return MlKitPoseInput(
        image = InputImage.fromBitmap(downscaled, 0),
        scaleX = scaleBackX,
        scaleY = scaleBackY,
        offsetX = offsetX,
        offsetY = offsetY,
    )
}

private fun PoseLandmark?.toSkeletonCoordsScaled(
    scaleX: Float,
    scaleY: Float,
    offsetX: Float,
    offsetY: Float,
): Skeleton.SkeletonCoordinate? {
    val pos = this?.position ?: return null
    return Skeleton.SkeletonCoordinate(
        x = pos.x * scaleX + offsetX,
        y = pos.y * scaleY + offsetY,
    )
}

private fun skeletonFromPoseScaled(
    pose: Pose?,
    timestamp: Long,
    width: Int,
    height: Int,
    scaleX: Float,
    scaleY: Float,
    offsetX: Float = 0f,
    offsetY: Float = 0f,
): Skeleton {
    return Skeleton(
        timestamp = timestamp,
        leftShoulder = pose?.getPoseLandmark(PoseLandmark.LEFT_SHOULDER)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        rightShoulder = pose?.getPoseLandmark(PoseLandmark.RIGHT_SHOULDER)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        leftElbow = pose?.getPoseLandmark(PoseLandmark.LEFT_ELBOW)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        rightElbow = pose?.getPoseLandmark(PoseLandmark.RIGHT_ELBOW)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        leftWrist = pose?.getPoseLandmark(PoseLandmark.LEFT_WRIST)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        rightWrist = pose?.getPoseLandmark(PoseLandmark.RIGHT_WRIST)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        leftHip = pose?.getPoseLandmark(PoseLandmark.LEFT_HIP)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        rightHip = pose?.getPoseLandmark(PoseLandmark.RIGHT_HIP)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        leftKnee = pose?.getPoseLandmark(PoseLandmark.LEFT_KNEE)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        rightKnee = pose?.getPoseLandmark(PoseLandmark.RIGHT_KNEE)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        leftAnkle = pose?.getPoseLandmark(PoseLandmark.LEFT_ANKLE)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        rightAnkle = pose?.getPoseLandmark(PoseLandmark.RIGHT_ANKLE)
            ?.toSkeletonCoordsScaled(scaleX, scaleY, offsetX, offsetY),
        width = width.toFloat(),
        height = height.toFloat(),
    )
}
