package com.performancecoachlab.posedetection.camera

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.rotate
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import co.touchlab.kermit.Logger
import com.performancecoachlab.posedetection.custom.CustomObjectRespository
import com.performancecoachlab.posedetection.custom.ObjectModel
import com.performancecoachlab.posedetection.recording.AnalysisObject
import com.performancecoachlab.posedetection.skeleton.Skeleton
import com.performancecoachlab.posedetection.skeleton.SkeletonRepository
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.get
import kotlinx.cinterop.refTo
import kotlinx.cinterop.useContents
import kotlinx.cinterop.usePinned
import org.jetbrains.skia.ColorAlphaType
import org.jetbrains.skia.ColorSpace
import org.jetbrains.skia.ColorType
import org.jetbrains.skia.Image
import org.jetbrains.skia.ImageInfo
import platform.AVFoundation.AVCaptureConnection
import platform.AVFoundation.AVCaptureDevice
import platform.AVFoundation.AVCaptureDeviceDiscoverySession
import platform.AVFoundation.AVCaptureDeviceInput
import platform.AVFoundation.AVCaptureDevicePositionBack
import platform.AVFoundation.AVCaptureDevicePositionFront
import platform.AVFoundation.AVCaptureDevicePositionUnspecified
import platform.AVFoundation.AVCaptureDeviceTypeBuiltInUltraWideCamera
import platform.AVFoundation.AVCaptureDeviceTypeBuiltInWideAngleCamera
import platform.AVFoundation.AVCaptureFileOutput
import platform.AVFoundation.AVCaptureInput
import platform.AVFoundation.AVCaptureMovieFileOutput
import platform.AVFoundation.AVCaptureOutput
import platform.AVFoundation.AVCaptureSession
import platform.AVFoundation.AVCaptureSessionPresetMedium
import platform.AVFoundation.AVCaptureVideoDataOutput
import platform.AVFoundation.AVCaptureVideoDataOutputSampleBufferDelegateProtocol
import platform.AVFoundation.AVCaptureVideoOrientation
import platform.AVFoundation.AVCaptureVideoOrientationLandscapeLeft
import platform.AVFoundation.AVCaptureVideoOrientationLandscapeRight
import platform.AVFoundation.AVCaptureVideoOrientationPortrait
import platform.AVFoundation.AVCaptureVideoOrientationPortraitUpsideDown
import platform.AVFoundation.AVCaptureVideoPreviewLayer
import platform.AVFoundation.AVLayerVideoGravityResizeAspect
import platform.AVFoundation.AVLayerVideoGravityResizeAspectFill
import platform.AVFoundation.AVMediaTypeVideo
import platform.AVFoundation.AVVideoCodecJPEG
import platform.AVFoundation.AVVideoCodecKey
import platform.AVFoundation.displayVideoZoomFactorMultiplier
import platform.AVFoundation.position
import platform.CoreFoundation.CFDataGetBytePtr
import platform.CoreFoundation.CFDataGetLength
import platform.CoreFoundation.CFRelease
import platform.CoreGraphics.CGBitmapContextCreate
import platform.CoreGraphics.CGBitmapContextCreateImage
import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB
import platform.CoreGraphics.CGDataProviderCopyData
import platform.CoreGraphics.CGImageAlphaInfo
import platform.CoreGraphics.CGImageGetAlphaInfo
import platform.CoreGraphics.CGImageGetBytesPerRow
import platform.CoreGraphics.CGImageGetDataProvider
import platform.CoreGraphics.CGImageGetHeight
import platform.CoreGraphics.CGImageGetWidth
import platform.CoreGraphics.CGImageRelease
import platform.CoreGraphics.CGPointMake
import platform.CoreGraphics.CGSize
import platform.CoreMedia.CMSampleBufferGetImageBuffer
import platform.CoreMedia.CMSampleBufferRef
import platform.Foundation.NSData
import platform.Foundation.NSError
import platform.Foundation.NSURL
import platform.Foundation.stringByAppendingPathComponent
import platform.UIKit.UIApplication
import platform.UIKit.UIDevice
import platform.UIKit.UIDeviceOrientation
import platform.UIKit.UIImage
import platform.UIKit.UIInterfaceOrientationLandscapeLeft
import platform.UIKit.UIInterfaceOrientationLandscapeRight
import platform.UIKit.UIInterfaceOrientationPortrait
import platform.UIKit.UIInterfaceOrientationPortraitUpsideDown
import platform.UIKit.UIView
import platform.UIKit.UIViewController
import platform.UIKit.UIViewControllerTransitionCoordinatorProtocol
import platform.darwin.NSObject
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
import platform.darwin.dispatch_queue_create
import platform.darwin.dispatch_sync
import platform.posix.memcpy
import kotlin.math.abs
import kotlin.native.runtime.NativeRuntimeApi

fun interfaceOrientationToVideoOrientation(): AVCaptureVideoOrientation {
    val orientation = UIApplication.sharedApplication.keyWindow?.windowScene?.interfaceOrientation
        ?: UIInterfaceOrientationPortrait

    return when (orientation) {
        UIInterfaceOrientationPortrait -> AVCaptureVideoOrientationPortrait
        UIInterfaceOrientationPortraitUpsideDown -> AVCaptureVideoOrientationPortraitUpsideDown
        UIInterfaceOrientationLandscapeLeft -> AVCaptureVideoOrientationLandscapeLeft
        UIInterfaceOrientationLandscapeRight -> AVCaptureVideoOrientationLandscapeRight
        else -> AVCaptureVideoOrientationPortrait
    }
}

class CameraEngine : UIViewController(null, null) {
    val cameraController = CameraController()
    private var onVideoSaved: ((String) -> Unit)? = null

    private var textMeasurer: androidx.compose.ui.text.TextMeasurer? = null

    private val memoryManager = MemoryManager

    override fun viewDidLoad() {
        super.viewDidLoad()
        MemoryManager.initialize()
        setupCamera()

    }

    override fun viewWillAppear(animated: Boolean) {
        super.viewWillAppear(animated)
        MemoryManager.updateMemoryStatus()
    }

    override fun viewDidDisappear(animated: Boolean) {
        super.viewDidDisappear(animated)

        MemoryManager.clearBufferPools()
    }

    @OptIn(ExperimentalForeignApi::class)
    override fun viewWillTransitionToSize(
        size: CValue<CGSize>,
        withTransitionCoordinator: UIViewControllerTransitionCoordinatorProtocol
    ) {
        super.viewWillTransitionToSize(size, withTransitionCoordinator)
        val orientation = interfaceOrientationToVideoOrientation()
        cameraController.cameraPreviewLayer?.connection?.videoOrientation = orientation
        // Also update the video-data output connection (used for Vision/CoreML)
        cameraController.updateVideoOutputOrientation(orientation)
    }

    fun getCameraPreviewLayer() = cameraController.cameraPreviewLayer

    internal fun currentVideoOrientation(): AVCaptureVideoOrientation {
        val orientation = UIDevice.currentDevice.orientation
        return when (orientation) {
            UIDeviceOrientation.UIDeviceOrientationPortrait -> AVCaptureVideoOrientationPortrait
            UIDeviceOrientation.UIDeviceOrientationPortraitUpsideDown -> AVCaptureVideoOrientationPortraitUpsideDown
            UIDeviceOrientation.UIDeviceOrientationLandscapeLeft -> AVCaptureVideoOrientationLandscapeRight
            UIDeviceOrientation.UIDeviceOrientationLandscapeRight -> AVCaptureVideoOrientationLandscapeLeft
            else -> AVCaptureVideoOrientationPortrait
        }
    }

    @OptIn(ExperimentalForeignApi::class)
    private fun setupCamera() {
        // Run session setup synchronously so callers can safely start the session afterwards
        cameraController.setupSession()
        cameraController.setupPreviewLayer(view)
        startSession()
        cameraController.onError = { error ->
            //println("Camera Error: $error")
        }
    }


    @OptIn(ExperimentalForeignApi::class)
    fun NSData.toByteArray(reuseBuffer: ByteArray? = null): ByteArray {
        val length = this.length.toInt()


        val buffer = if (reuseBuffer != null && reuseBuffer.size >= length) {
            reuseBuffer
        } else {
            ByteArray(length)
        }

        buffer.usePinned {
            memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length)
        }

        return buffer
    }


    @OptIn(ExperimentalForeignApi::class)
    override fun viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        cameraController.cameraPreviewLayer?.setFrame(view.bounds)
        cameraController.cameraPreviewLayer?.contentsGravity = AVLayerVideoGravityResizeAspectFill
    }

    fun setDetectMode(detectMode: DetectMode) {
        cameraController.setDetectMode(detectMode)
    }

    fun toggleCameraLens() {
        MemoryManager.updateMemoryStatus()
        if (MemoryManager.isUnderMemoryPressure()) MemoryManager.clearBufferPools()
        cameraController.switchCamera()
    }

    /**
     * Deterministically select which lens to use.
     * This is idempotent (won't switch if already on the requested lens),
     * which makes it safe to call from Compose recomposition.
     */
    fun setUseFrontCamera(useFront: Boolean) {
        MemoryManager.updateMemoryStatus()
        if (MemoryManager.isUnderMemoryPressure()) MemoryManager.clearBufferPools()
        cameraController.setUseFrontCamera(useFront)
    }

    fun setUseUltraWide(useUltraWide: Boolean) {
        MemoryManager.updateMemoryStatus()
        if (MemoryManager.isUnderMemoryPressure()) MemoryManager.clearBufferPools()
        cameraController.setUseUltraWide(useUltraWide)
    }

    fun startSession() {
        MemoryManager.clearBufferPools()
        cameraController.startSession()
    }

    fun stopSession() {
        cameraController.stopSession()
    }

    fun addSkeletonRepository(repository: SkeletonRepository) {
        cameraController.skeletonRepository = repository
    }

    fun addCustomObjectRepository(repository: CustomObjectRespository) {
        cameraController.customObjectRepository = repository
    }

    fun addCameraViewController(controller: CameraViewController?) {
        cameraController.cameraViewController = controller
    }

    fun addFrameListener(listener: FrameRepository) {
        cameraController.frameListener = listener
    }

    fun startRecording() = cameraController.startRecording()

    fun stopRecording() {
        cameraController.stopRecording()
    }

    fun splitRecording() = cameraController.splitRecording()

    fun setOnVideoSavedCallback(callback: (String) -> Unit) {
        onVideoSaved = callback
        cameraController.onVideoSaved = callback
    }

    fun setTextMeasurer(textMeasurer: androidx.compose.ui.text.TextMeasurer) {
        cameraController.textMeasurer = textMeasurer
    }

    fun setDrawOptions(
        drawSkeleton: Boolean, drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)?
    ) {
        cameraController.drawSkeleton = drawSkeleton
        cameraController.drawObjects = drawObjects
    }

    fun setFocusArea(focusArea: Rect?) {
        cameraController.setFocusArea(focusArea)
    }

    fun setObjectModel(objectModel: ObjectModel?) {
        cameraController.setObjectModel(objectModel)
    }

    fun setPreviewFillMode(mode: PreviewFillMode) {
        // ResizeAspect == fit (show full frame)
        // ResizeAspectFill == fill/crop
        cameraController.cameraPreviewLayer?.videoGravity = when (mode) {
            PreviewFillMode.FIT -> AVLayerVideoGravityResizeAspect
            PreviewFillMode.FILL -> AVLayerVideoGravityResizeAspectFill
        }
    }
}

class CameraController : NSObject(), AVCaptureVideoDataOutputSampleBufferDelegateProtocol,
    platform.AVFoundation.AVCaptureFileOutputRecordingDelegateProtocol {
    private var captureSession: AVCaptureSession? = null
    private var backCamera: AVCaptureDevice? = null
    private var frontCamera: AVCaptureDevice? = null
    private var currentCamera: AVCaptureDevice? = null
    private var videoOutput: AVCaptureVideoDataOutput? = null
    private var movieFileOutput: AVCaptureMovieFileOutput? = null
    var cameraPreviewLayer: AVCaptureVideoPreviewLayer? = null
    var isUsingFrontCamera = false
    var skeletonRepository: SkeletonRepository? = null
    var customObjectRepository: CustomObjectRespository? = null
    var cameraViewController: CameraViewController? = null
    var frameListener: FrameRepository? = null
    var onError: ((CameraException) -> Unit)? = null
    var startTime: Long? = null
    var onVideoSaved: ((String) -> Unit)? = null
    var drawSkeleton: Boolean = true
    var drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)? = null
    var textMeasurer: androidx.compose.ui.text.TextMeasurer? = null

    // Serial queue to serialize session configuration and start/stop calls
    private val sessionQueue =
        dispatch_queue_create("com.performancecoachlab.captureSessionQueue", null)

    // Prevent overlapping switch operations (which can deadlock/jank the UI).
    private var isSwitchingCamera: Boolean = false

    // Reuse a single queue for frame processing work.
    private val frameProcessingQueue =
        dispatch_queue_create("com.performancecoachlab.frameProcessing", null)

    // iOS 14+ introduces a new way to access the ultra-wide camera, which we can use conditionally.
    private var backUltraWideCamera: AVCaptureDevice? = null
    private var backWideCamera: AVCaptureDevice? = null
    private var useUltraWideBack: Boolean = false

    sealed class CameraException : Exception() {
        class DeviceNotAvailable : CameraException()
        class ConfigurationError(message: String) : CameraException()
        class CaptureError(message: String) : CameraException()
    }

    fun startRecording(): String? {
        // Ensure we add outputs on the session queue to avoid races
        dispatch_sync(sessionQueue) {
            if (movieFileOutput == null) {
                val candidate = AVCaptureMovieFileOutput()
                movieFileOutput = candidate
                if (captureSession?.canAddOutput(candidate) == true) {
                    captureSession?.addOutput(movieFileOutput!!)
                }
            }
        }

        val outputURL = generateSegmentURL()
        movieFileOutput?.connections?.firstOrNull()?.let { connection ->
            (connection as AVCaptureConnection).let { avConnection ->
                if (avConnection.isVideoOrientationSupported()) {
                    avConnection.videoOrientation = currentVideoOrientation()
                }
            }

        }
        movieFileOutput?.startRecordingToOutputFileURL(outputURL, this)
        return outputURL.path
    }

    fun stopRecording() {
        movieFileOutput?.stopRecording()
    }

    fun splitRecording(): String? {
        movieFileOutput?.stopRecording()
        val outputURL = generateSegmentURL()
        movieFileOutput?.startRecordingToOutputFileURL(outputURL, this)
        return outputURL.path
    }

    private fun generateSegmentURL(): platform.Foundation.NSURL {
        val tempDir = platform.Foundation.NSTemporaryDirectory()
        val fileName = platform.Foundation.NSUUID().UUIDString + ".mov"
        val filePath =
            (tempDir as platform.Foundation.NSString).stringByAppendingPathComponent(fileName)
        return platform.Foundation.NSURL.fileURLWithPath(filePath)
    }

    fun setFocusArea(focusArea: Rect?) {
        frameProcessor.setFocusArea(focusArea)
    }

    fun setDetectMode(detectMode: DetectMode) {
        frameProcessor.setDetectMode(detectMode)
    }

    fun setObjectModel(objectModel: ObjectModel?) {
        frameProcessor.setObjectModel(objectModel)
    }

    /**
     * Deterministically select which lens to use.
     * No-ops if the requested lens is already active.
     */
    fun setUseFrontCamera(useFront: Boolean) {
        if (isUsingFrontCamera == useFront) return
        switchCamera()
    }

    /**
     * Deterministically select ultra-wide (0.5x) vs wide (1.0x) when using the back camera.
     * No-ops if the requested lens is already active.
     */
    fun setUseUltraWide(useUltraWide: Boolean) {
        if (useUltraWideBack == useUltraWide) return
        useUltraWideBack = useUltraWide
        if (!isUsingFrontCamera) {
            // Reconfigure session to switch back lens.
            switchCamera(forceReconfigure = true)
        }
    }

    fun setupSession() {
        // Run synchronously on sessionQueue so callers (like setupCamera) can safely call startSession afterwards
        dispatch_sync(sessionQueue) {
            var configurationBegan = false
            try {
                captureSession = AVCaptureSession()
                captureSession?.beginConfiguration()
                configurationBegan = true
                captureSession?.sessionPreset = AVCaptureSessionPresetMedium

                if (!setupInputs()) {
                    throw CameraException.DeviceNotAvailable()
                }
                setupVideoOutput()

                // Add movie file output for recording only if it can be added
                if (movieFileOutput == null) {
                    val candidate = AVCaptureMovieFileOutput()
                    if (captureSession?.canAddOutput(candidate) == true) {
                        movieFileOutput = candidate
                        captureSession?.addOutput(movieFileOutput!!)
                    } else {
                        // keep the instance (not added) so it can be added later when appropriate
                        movieFileOutput = candidate
                    }
                }
            } catch (e: CameraException) {
                cleanupSession()
                onError?.invoke(e)
                return@dispatch_sync
            } finally {
                if (configurationBegan) {
                    captureSession?.commitConfiguration()
                }
            }
        }
        // Do not start recording here; startRecording() should be called explicitly after session is running
    }

    private fun setupVideoOutput() {
        videoOutput = AVCaptureVideoDataOutput()
        // Deliver frames off the main thread; toggling/config and UI should not compete with capture callbacks.
        videoOutput?.setSampleBufferDelegate(this, sessionQueue)
        videoOutput?.videoSettings = mapOf(AVVideoCodecKey to AVVideoCodecJPEG)

        if (captureSession?.canAddOutput(videoOutput!!) == true) {
            captureSession?.addOutput(videoOutput!!)
        } else {
            throw CameraException.ConfigurationError("Cannot add video output")
        }
    }

    @OptIn(ExperimentalForeignApi::class)
    private fun setupInputs(): Boolean {
        val availableDevices = AVCaptureDeviceDiscoverySession.discoverySessionWithDeviceTypes(
            listOf(
                AVCaptureDeviceTypeBuiltInUltraWideCamera,
                AVCaptureDeviceTypeBuiltInWideAngleCamera
            ),
            AVMediaTypeVideo,
            AVCaptureDevicePositionUnspecified
        ).devices

        if (availableDevices.isEmpty()) return false

        // Reset cached devices
        backUltraWideCamera = null
        backWideCamera = null
        frontCamera = null

        for (deviceAny in availableDevices) {
            val device = deviceAny as AVCaptureDevice

            when (device.position) {
                AVCaptureDevicePositionBack -> {
                    // Heuristic: ultra-wide cameras typically report a zoom multiplier < 1.0
                    if (device.displayVideoZoomFactorMultiplier < 1.0) {
                        backUltraWideCamera = device
                    } else {
                        backWideCamera = device
                    }
                }

                AVCaptureDevicePositionFront -> {
                    // Front camera
                    frontCamera = device
                }
            }

            Logger.d { "device: ${device.localizedName}, zoom: ${device.displayVideoZoomFactorMultiplier}, position: ${device.position}" }
        }

        // Keep existing backCamera/frontCamera fields used elsewhere.
        backCamera = if (useUltraWideBack) backUltraWideCamera ?: backWideCamera else backWideCamera
            ?: backUltraWideCamera

        currentCamera = if (isUsingFrontCamera) {
            frontCamera ?: backCamera ?: return false
        } else {
            backCamera ?: frontCamera ?: return false
        }

        try {
            val input =
                AVCaptureDeviceInput.deviceInputWithDevice(currentCamera!!, null) ?: return false

            if (captureSession?.canAddInput(input) == true) {
                captureSession?.addInput(input)
                return true
            }
        } catch (e: Exception) {
            throw CameraException.ConfigurationError(e.message ?: "Unknown error")
        }
        return false
    }

    fun startSession() {
        // Run on sessionQueue to avoid overlapping with configuration
        dispatch_async(sessionQueue) {
            if (captureSession?.isRunning() == false) {
                captureSession?.startRunning()
            }
        }
    }

    fun stopSession() {
        dispatch_async(sessionQueue) {
            if (captureSession?.isRunning() == true) {
                captureSession?.stopRunning()
            }
        }
    }

    fun cleanupSession() {
        stopSession()
        cameraPreviewLayer?.removeFromSuperlayer()
        cameraPreviewLayer = null
        captureSession = null
        currentCamera = null
        backCamera = null
        frontCamera = null
    }

    @OptIn(ExperimentalForeignApi::class)
    fun setupPreviewLayer(view: UIView) {
        captureSession?.let { session ->
            val newPreviewLayer = AVCaptureVideoPreviewLayer(session = session).apply {
                videoGravity = AVLayerVideoGravityResizeAspectFill
                setFrame(view.bounds)
                connection?.videoOrientation = interfaceOrientationToVideoOrientation()

                connection?.let {
                    if (it.isVideoMirroringSupported()) {
                        it.automaticallyAdjustsVideoMirroring = false
                        it.videoMirrored = isUsingFrontCamera
                    }
                }

            }

            view.layer.addSublayer(newPreviewLayer)
            cameraPreviewLayer = newPreviewLayer

            cameraPreviewLayer?.connection?.let { connection ->
                connection.videoOrientation = interfaceOrientationToVideoOrientation()
                // Ensure video-data output connection matches the same orientation/mirroring
                updateVideoOutputOrientation(interfaceOrientationToVideoOrientation())
            }
        }
    }

    fun currentVideoOrientation(): AVCaptureVideoOrientation {
        val orientation = UIDevice.currentDevice.orientation
        return when (orientation) {
            UIDeviceOrientation.UIDeviceOrientationPortrait -> AVCaptureVideoOrientationPortrait
            UIDeviceOrientation.UIDeviceOrientationPortraitUpsideDown -> AVCaptureVideoOrientationPortraitUpsideDown
            UIDeviceOrientation.UIDeviceOrientationLandscapeLeft -> AVCaptureVideoOrientationLandscapeRight
            UIDeviceOrientation.UIDeviceOrientationLandscapeRight -> AVCaptureVideoOrientationLandscapeLeft
            else -> AVCaptureVideoOrientationPortrait
        }
    }

    @OptIn(ExperimentalForeignApi::class)
    fun switchCamera(forceReconfigure: Boolean = false) {
        guard(captureSession != null) { return@guard }

        // Never block the caller thread (often the main thread). Serialize on sessionQueue.
        dispatch_async(sessionQueue) {
            if (isSwitchingCamera) return@dispatch_async
            isSwitchingCamera = true

            // Hide preview while switching to avoid transient rotated frames being displayed.
            dispatch_async(dispatch_get_main_queue()) {
                cameraPreviewLayer?.hidden = true
            }

            var configurationBegan = false
            try {
                captureSession?.beginConfiguration()
                configurationBegan = true

                captureSession?.inputs?.firstOrNull()?.let { input ->
                    captureSession?.removeInput(input as AVCaptureInput)
                }

                if (!forceReconfigure) {
                    isUsingFrontCamera = !isUsingFrontCamera
                }

                // Recompute which back camera we want.
                backCamera =
                    if (useUltraWideBack) backUltraWideCamera ?: backWideCamera else backWideCamera
                        ?: backUltraWideCamera

                currentCamera = if (isUsingFrontCamera) {
                    frontCamera ?: backCamera
                } else {
                    backCamera ?: frontCamera
                }

                val newCamera = currentCamera ?: throw CameraException.DeviceNotAvailable()

                val newInput = AVCaptureDeviceInput.deviceInputWithDevice(
                    newCamera,
                    null
                ) ?: throw CameraException.ConfigurationError("Failed to create input")

                if (captureSession?.canAddInput(newInput) == true) {
                    captureSession?.addInput(newInput)
                } else {
                    throw CameraException.ConfigurationError("Cannot add input")
                }

                // Apply orientation + mirroring on main queue while still hidden.
                dispatch_async(dispatch_get_main_queue()) {
                    cameraPreviewLayer?.connection?.let { connection ->
                        connection.videoOrientation = interfaceOrientationToVideoOrientation()
                        if (connection.isVideoMirroringSupported()) {
                            connection.automaticallyAdjustsVideoMirroring = false
                            connection.setVideoMirrored(isUsingFrontCamera)
                        }
                    }
                    updateVideoOutputOrientation(interfaceOrientationToVideoOrientation())

                    cameraPreviewLayer?.apply {
                        // Use a tiny fade-in to avoid a harsh blink.
                        opacity = 0f
                        hidden = false
                        val anim =
                            platform.QuartzCore.CABasicAnimation.animationWithKeyPath("opacity")
                        anim.fromValue = 0.0
                        anim.toValue = 1.0
                        anim.duration = 0.12
                        addAnimation(anim, forKey = "fadeIn")
                        opacity = 1f
                    }
                }
            } catch (e: CameraException) {
                onError?.invoke(e)
            } catch (e: Exception) {
                onError?.invoke(CameraException.ConfigurationError(e.message ?: "Unknown error"))
            } finally {
                if (configurationBegan) {
                    captureSession?.commitConfiguration()
                }

                // Ensure the connection is correct after commit, then show the preview again.
                dispatch_async(dispatch_get_main_queue()) {
                    cameraPreviewLayer?.connection?.let { connection ->
                        connection.videoOrientation = interfaceOrientationToVideoOrientation()
                        if (connection.isVideoMirroringSupported()) {
                            connection.automaticallyAdjustsVideoMirroring = false
                            connection.setVideoMirrored(isUsingFrontCamera)
                        }
                    }

                    cameraPreviewLayer?.apply {
                        // Use a tiny fade-in to avoid a harsh blink.
                        opacity = 0f
                        hidden = false
                        val anim =
                            platform.QuartzCore.CABasicAnimation.animationWithKeyPath("opacity")
                        anim.fromValue = 0.0
                        anim.toValue = 1.0
                        anim.duration = 0.12
                        addAnimation(anim, forKey = "fadeIn")
                        opacity = 1f
                    }
                }

                isSwitchingCamera = false
            }
        }
    }

    private val frameProcessor = FrameProcessor(null)

    @OptIn(ExperimentalForeignApi::class, NativeRuntimeApi::class)
    override fun captureOutput(
        output: AVCaptureOutput,
        didOutputSampleBuffer: CMSampleBufferRef?,
        fromConnection: AVCaptureConnection
    ) {
        timeStamp().also { timestamp ->
            runCatching {
                dispatch_async(frameProcessingQueue) {
                    try {
                        // If we're switching cameras, don't publish overlay frames.
                        // This avoids brief flashes of stale frames from the previous lens.
                        if (isSwitchingCamera) return@dispatch_async

                        var detectedSkeleton: Skeleton? = null
                        var detectedObjects: List<AnalysisObject> = emptyList()

                        frameProcessor.analyseBufferForAll(
                            CMSampleBufferGetImageBuffer(didOutputSampleBuffer),
                            timestamp,
                            preview = cameraPreviewLayer,
                            captureConnection = fromConnection,
                            onSkeletonProcessed = { skeleton ->
                                detectedSkeleton = skeleton
                            },
                            onObjectsProcessed = { objects ->
                                detectedObjects = objects
                            }
                        )

                        cameraPreviewLayer?.also { preview ->
                            val previewSkeleton = detectedSkeleton?.let {
                                mapSkeletonToPreview(
                                    skeleton = it,
                                    previewLayer = preview,
                                    width = it.width,
                                    height = it.height
                                )
                            }

                            previewSkeleton?.also {
                                skeletonRepository?.updateSkeleton(it)
                            }

                            val previewObjects = detectedObjects.map { obj ->
                                obj.copy(
                                    boundingBox = mapBoxToPreview(
                                        obj.boundingBox,
                                        preview,
                                        width = obj.frameSize.width.toFloat(),
                                        height = obj.frameSize.height.toFloat()
                                    ),
                                    // Keep the original analysis frame size; don't try to derive it in preview space.
                                    frameSize = obj.frameSize
                                )
                            }

                            customObjectRepository?.updateCustomObject(previewObjects)

                            preview.bounds.useContents {
                                Pair(size.width.toInt(), size.height.toInt())
                            }.also { bo ->
                                ImageBitmap(bo.first, bo.second).drawResults(
                                    if (drawSkeleton) previewSkeleton else null,
                                    drawObjects?.invoke(previewObjects) ?: emptyList(),
                                    textMeasurer = textMeasurer
                                ).also { drawn ->
                                    cameraViewController?.setRequestDataProvider {
                                        CameraViewData(
                                            width = bo.first.toFloat(),
                                            height = bo.second.toFloat(),
                                            rotation = when (currentVideoOrientation()) {
                                                AVCaptureVideoOrientationPortrait -> SensorRotation.ROTATION_90
                                                AVCaptureVideoOrientationLandscapeRight -> SensorRotation.ROTATION_0
                                                AVCaptureVideoOrientationPortraitUpsideDown -> SensorRotation.ROTATION_270
                                                AVCaptureVideoOrientationLandscapeLeft -> SensorRotation.ROTATION_180
                                                else -> SensorRotation.ROTATION_0
                                            }
                                        )
                                    }

                                    // One more guard right at publish time, in case a switch started mid-frame.
                                    if (!isSwitchingCamera) {
                                        frameListener?.updateFrame(drawn)
                                    }
                                }
                            }
                        }
                    } catch (_: Exception) {
                        // ignore frame processing errors
                    }
                }
            }
        }

        MemoryManager.updateMemoryStatus()
        kotlin.native.runtime.GC.collect()
    }

    /**
     * Keeps the AVCaptureVideoDataOutput connection (used for Vision/CoreML) aligned with UI orientation.
     * This must be updated separately from the preview layer connection.
     */
    @OptIn(ExperimentalForeignApi::class)
    fun updateVideoOutputOrientation(orientation: AVCaptureVideoOrientation) {
        val output = videoOutput ?: return
        val connection =
            output.connectionWithMediaType(AVMediaTypeVideo) as? AVCaptureConnection ?: return
        if (connection.isVideoOrientationSupported()) {
            connection.videoOrientation = orientation
        }
        if (connection.isVideoMirroringSupported()) {
            connection.automaticallyAdjustsVideoMirroring = false
            connection.videoMirrored = isUsingFrontCamera
        }
    }

    fun ImageBitmap.copy(): ImageBitmap {
        val original = this
        val copied = ImageBitmap(width, height, config, hasAlpha, colorSpace)
        val canvas = Canvas(copied)
        val drawScope = CanvasDrawScope()
        val size = Size(width.toFloat(), height.toFloat())
        drawScope.draw(
            Density(1f), LayoutDirection.Ltr, canvas, size
        ) {
            drawImage(original)
        }
        return copied
    }

    private inline fun guard(condition: Boolean, crossinline block: () -> Unit) {
        if (!condition) block()
    }


    private fun ImageBitmap.normaliseRotation(): ImageBitmap {
        return when (UIDevice.currentDevice.orientation) {
            UIDeviceOrientation.UIDeviceOrientationPortrait -> this.rotateLeft()
            UIDeviceOrientation.UIDeviceOrientationPortraitUpsideDown -> this.rotateRight()
            UIDeviceOrientation.UIDeviceOrientationLandscapeLeft -> if (!isUsingFrontCamera) this.rotate180() else this
            else -> if (!isUsingFrontCamera) this else this.rotate180()
        }
    }

    // Replace mapBoxToPreview implementation to map oriented analysis pixels -> raw buffer normalized -> preview space.
    @OptIn(ExperimentalForeignApi::class)
    private fun orientedNormalizedToCaptureDeviceNormalized(
        uTopLeft: Double,
        vTopLeft: Double,
        previewLayer: AVCaptureVideoPreviewLayer,
    ): Pair<Double, Double> {
        // Clamp first to avoid ever returning out-of-range points.
        val u = uTopLeft.coerceIn(0.0, 1.0)
        val v = vTopLeft.coerceIn(0.0, 1.0)

        val orientation =
            previewLayer.connection?.videoOrientation ?: AVCaptureVideoOrientationLandscapeRight
        val mirrored = previewLayer.connection?.videoMirrored ?: false

        // Convert from oriented (top-left origin) to capture-device normalized expected by pointForCaptureDevicePointOfInterest.
        // Empirically, in portrait the old mapping appears 90° clockwise, so we apply a 90° counter-clockwise fix:
        // (u,v) -> (x=v, y=1-u)
        var (x, y) = when (orientation) {
            AVCaptureVideoOrientationPortrait -> Pair(v, 1.0 - u)
            AVCaptureVideoOrientationPortraitUpsideDown -> Pair(1.0 - v, u)
            AVCaptureVideoOrientationLandscapeRight -> Pair(u, v)
            AVCaptureVideoOrientationLandscapeLeft -> Pair(1.0 - u, 1.0 - v)
            else -> Pair(u, v)
        }

        if (mirrored) {
            x = 1.0 - x
        }

        return Pair(x.coerceIn(0.0, 1.0), y.coerceIn(0.0, 1.0))
    }

    @OptIn(ExperimentalForeignApi::class)
    fun mapBoxToPreview(
        box: Rect,
        previewLayer: AVCaptureVideoPreviewLayer,
        width: Float,
        height: Float,
    ): Rect {
        fun mapPoint(point: Offset): Offset {
            val u = (point.x.toDouble() / width.toDouble())
            val v = (point.y.toDouble() / height.toDouble())
            val (cx, cy) = orientedNormalizedToCaptureDeviceNormalized(u, v, previewLayer)

            val normalizedPoint = CGPointMake(cx, cy)
            val screenPoint = previewLayer.pointForCaptureDevicePointOfInterest(normalizedPoint)
            return Offset(
                screenPoint.useContents { x.toFloat() },
                screenPoint.useContents { y.toFloat() }
            )
        }

        // Map all 4 corners to ensure correct rect under rotations.
        val p1 = mapPoint(Offset(box.left, box.top))
        val p2 = mapPoint(Offset(box.right, box.top))
        val p3 = mapPoint(Offset(box.right, box.bottom))
        val p4 = mapPoint(Offset(box.left, box.bottom))

        val left = minOf(p1.x, p2.x, p3.x, p4.x)
        val top = minOf(p1.y, p2.y, p3.y, p4.y)
        val right = maxOf(p1.x, p2.x, p3.x, p4.x)
        val bottom = maxOf(p1.y, p2.y, p3.y, p4.y)

        return Rect(left = left, top = top, right = right, bottom = bottom).normalize()
    }

    // Re-add the FileOutput delegate callback (was accidentally removed during edits)
    override fun captureOutput(
        output: AVCaptureFileOutput,
        didFinishRecordingToOutputFileAtURL: NSURL,
        fromConnections: List<*>,
        error: NSError?
    ) {
        onVideoSaved?.invoke(didFinishRecordingToOutputFileAtURL.path ?: "")
    }
}

@OptIn(ExperimentalForeignApi::class)
internal fun UIImage.toSkiaImage(): Image? {
    val imageRef = this.CGImage ?: return null

    val width = CGImageGetWidth(imageRef).toInt()
    val height = CGImageGetHeight(imageRef).toInt()

    val bytesPerRow = CGImageGetBytesPerRow(imageRef)
    val data = CGDataProviderCopyData(CGImageGetDataProvider(imageRef))
    val bytePointer = CFDataGetBytePtr(data)
    val length = CFDataGetLength(data)

    val alphaType = when (CGImageGetAlphaInfo(imageRef)) {
        CGImageAlphaInfo.kCGImageAlphaPremultipliedFirst, CGImageAlphaInfo.kCGImageAlphaPremultipliedLast -> ColorAlphaType.PREMUL

        CGImageAlphaInfo.kCGImageAlphaFirst, CGImageAlphaInfo.kCGImageAlphaLast -> ColorAlphaType.UNPREMUL

        CGImageAlphaInfo.kCGImageAlphaNone, CGImageAlphaInfo.kCGImageAlphaNoneSkipFirst, CGImageAlphaInfo.kCGImageAlphaNoneSkipLast -> ColorAlphaType.OPAQUE

        else -> ColorAlphaType.UNKNOWN
    }

    val byteArray = ByteArray(length.toInt()) { index ->
        bytePointer!![index].toByte()
    }

    CFRelease(data)
    CGImageRelease(imageRef)

    val skiaColorSpace = ColorSpace.sRGB
    val colorType = ColorType.RGBA_8888

    // Convert RGBA to BGRA
    for (i in byteArray.indices step 4) {
        val r = byteArray[i]
        val g = byteArray[i + 1]
        val b = byteArray[i + 2]
        val a = byteArray[i + 3]

        byteArray[i] = b
        byteArray[i + 2] = r
    }

    return Image.makeRaster(
        imageInfo = ImageInfo(
            width = width,
            height = height,
            colorType = colorType,
            alphaType = alphaType,
            colorSpace = skiaColorSpace
        ),
        bytes = byteArray,
        rowBytes = bytesPerRow.toInt(),
    )
}

fun UIImage.toImageBitmap(): ImageBitmap {
    return toSkiaImage()!!.toComposeImageBitmap()
}

fun ImageBitmap.rotateRight(): ImageBitmap {
    val original = this
    val rotated = ImageBitmap(height, width, config, hasAlpha, colorSpace)
    val canvas = Canvas(rotated).also {
        it.rotate(-90f, width.toFloat() / 2f, width.toFloat() / 2f)
    }
    val drawScope = CanvasDrawScope()
    val size = Size(width.toFloat(), height.toFloat())
    drawScope.draw(
        Density(1f),
        LayoutDirection.Ltr,
        canvas,
        size,
    ) {
        drawImage(original)
    }
    return rotated
}

fun ImageBitmap.rotateLeft(): ImageBitmap {
    val original = this
    val rotated = ImageBitmap(height, width, config, hasAlpha, colorSpace)
    val canvas = Canvas(rotated).also {
        it.rotate(90f, height.toFloat() / 2f, height.toFloat() / 2f)
    }
    val drawScope = CanvasDrawScope()
    val size = Size(width.toFloat(), height.toFloat())
    drawScope.draw(
        Density(1f),
        LayoutDirection.Ltr,
        canvas,
        size,
    ) {
        drawImage(original)
    }
    return rotated
}

fun ImageBitmap.rotate180(): ImageBitmap {
    val original = this
    val rotated = ImageBitmap(width, height, config, hasAlpha, colorSpace)
    val canvas = Canvas(rotated).also {
        it.rotate(180f, width.toFloat() / 2f, height.toFloat() / 2f)
    }
    val drawScope = CanvasDrawScope()
    val size = Size(width.toFloat(), height.toFloat())
    drawScope.draw(
        Density(1f),
        LayoutDirection.Ltr,
        canvas,
        size,
    ) {
        drawImage(original)
    }
    return rotated
}

@OptIn(ExperimentalForeignApi::class)
fun ImageBitmap.toUIImage(): UIImage? {
    val width = this.width
    val height = this.height
    val buffer = IntArray(width * height)

    this.readPixels(buffer)

    val colorSpace = CGColorSpaceCreateDeviceRGB()
    val context = CGBitmapContextCreate(
        data = buffer.refTo(0),
        width = width.toULong(),
        height = height.toULong(),
        bitsPerComponent = 8u,
        bytesPerRow = (4 * width).toULong(),
        space = colorSpace,
        bitmapInfo = CGImageAlphaInfo.kCGImageAlphaPremultipliedLast.value
    )

    val cgImage = CGBitmapContextCreateImage(context)
    return cgImage?.let { UIImage.imageWithCGImage(it) }
}

@OptIn(ExperimentalForeignApi::class)
fun mapSkeletonToPreview(
    skeleton: Skeleton,
    previewLayer: AVCaptureVideoPreviewLayer,
    width: Float,
    height: Float
): Skeleton {
    fun orientedNormalizedToCaptureDeviceNormalized(
        uTopLeft: Double,
        vTopLeft: Double
    ): Pair<Double, Double> {
        val u = uTopLeft.coerceIn(0.0, 1.0)
        val v = vTopLeft.coerceIn(0.0, 1.0)

        val orientation =
            previewLayer.connection?.videoOrientation ?: AVCaptureVideoOrientationLandscapeRight
        val mirrored = previewLayer.connection?.videoMirrored ?: false

        var (x, y) = when (orientation) {
            AVCaptureVideoOrientationPortrait -> Pair(v, 1.0 - u)
            AVCaptureVideoOrientationPortraitUpsideDown -> Pair(1.0 - v, u)
            AVCaptureVideoOrientationLandscapeRight -> Pair(u, v)
            AVCaptureVideoOrientationLandscapeLeft -> Pair(1.0 - u, 1.0 - v)
            else -> Pair(u, v)
        }

        if (mirrored) x = 1.0 - x
        return Pair(x.coerceIn(0.0, 1.0), y.coerceIn(0.0, 1.0))
    }

    fun mapPoint(point: Skeleton.SkeletonCoordinate?): Skeleton.SkeletonCoordinate? {
        if (point == null) return null

        val u = (point.x.toDouble() / width.toDouble())
        val v = (point.y.toDouble() / height.toDouble())
        val (cx, cy) = orientedNormalizedToCaptureDeviceNormalized(u, v)

        val normalizedPoint = CGPointMake(cx, cy)
        val screenPoint = previewLayer.pointForCaptureDevicePointOfInterest(normalizedPoint)

        return Skeleton.SkeletonCoordinate(
            screenPoint.useContents { x.toFloat() },
            screenPoint.useContents { y.toFloat() }
        )
    }

    val minbounds = previewLayer.pointForCaptureDevicePointOfInterest(CGPointMake(0.0, 0.0))
        .useContents { Pair(x.toFloat(), y.toFloat()) }
    val maxbounds = previewLayer.pointForCaptureDevicePointOfInterest(CGPointMake(1.0, 1.0))
        .useContents { Pair(x.toFloat(), y.toFloat()) }
    val bounds = Pair(
        abs(maxbounds.first - minbounds.first),
        abs(maxbounds.second - minbounds.second)
    )

    return Skeleton(
        timestamp = skeleton.timestamp,
        leftShoulder = mapPoint(skeleton.leftShoulder),
        rightShoulder = mapPoint(skeleton.rightShoulder),
        leftElbow = mapPoint(skeleton.leftElbow),
        rightElbow = mapPoint(skeleton.rightElbow),
        leftWrist = mapPoint(skeleton.leftWrist),
        rightWrist = mapPoint(skeleton.rightWrist),
        leftHip = mapPoint(skeleton.leftHip),
        rightHip = mapPoint(skeleton.rightHip),
        leftKnee = mapPoint(skeleton.leftKnee),
        rightKnee = mapPoint(skeleton.rightKnee),
        leftAnkle = mapPoint(skeleton.leftAnkle),
        rightAnkle = mapPoint(skeleton.rightAnkle),
        width = bounds.first,
        height = bounds.second,
    )
}
