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.recording.FrameSize
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.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.AVLayerVideoGravityResizeAspectFill
import platform.AVFoundation.AVMediaTypeVideo
import platform.AVFoundation.AVVideoCodecJPEG
import platform.AVFoundation.AVVideoCodecKey
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.UIDevice
import platform.UIKit.UIDeviceOrientation
import platform.UIKit.UIImage
import platform.UIKit.UIView
import platform.UIKit.UIViewController
import platform.darwin.DISPATCH_QUEUE_PRIORITY_HIGH
import platform.darwin.NSObject
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_global_queue
import platform.darwin.dispatch_get_main_queue
import platform.darwin.dispatch_queue_create
import platform.posix.memcpy
import kotlin.math.abs
import kotlin.native.runtime.NativeRuntimeApi
import platform.UIKit.UIApplication
import platform.UIKit.UIInterfaceOrientationLandscapeLeft
import platform.UIKit.UIInterfaceOrientationLandscapeRight
import platform.UIKit.UIInterfaceOrientationPortrait
import platform.UIKit.UIInterfaceOrientationPortraitUpsideDown
import platform.UIKit.UIViewControllerTransitionCoordinatorProtocol

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 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)
        cameraController.cameraPreviewLayer?.connection?.videoOrientation = interfaceOrientationToVideoOrientation()
    }

    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() {
        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 normaliseImageRotation(image: NSData): ImageBitmap {
        val uiImage = UIImage(image).toImageBitmap()
        return when (UIDevice.currentDevice.orientation) {
            UIDeviceOrientation.UIDeviceOrientationPortrait -> uiImage.rotateLeft()
            UIDeviceOrientation.UIDeviceOrientationPortraitUpsideDown -> uiImage.rotateRight()
            UIDeviceOrientation.UIDeviceOrientationLandscapeLeft -> if (cameraController.isUsingFrontCamera) uiImage.rotate180() else uiImage
            else -> if (cameraController.isUsingFrontCamera) uiImage else uiImage.rotate180()
        }
    }

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

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

    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 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)
    }
}

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 = true
    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

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

    fun startRecording(): String? {
        if (movieFileOutput == null) {
            movieFileOutput = AVCaptureMovieFileOutput()
            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)
    }

    fun setupSession() {
        try {
            captureSession = AVCaptureSession()
            captureSession?.beginConfiguration()
            captureSession?.sessionPreset = AVCaptureSessionPresetMedium

            if (!setupInputs()) {
                throw CameraException.DeviceNotAvailable()
            }
            setupVideoOutput()
            // Add movie file output for recording
            if (movieFileOutput == null) {
                movieFileOutput = AVCaptureMovieFileOutput()
                captureSession?.addOutput(movieFileOutput!!)
            }
            captureSession?.commitConfiguration()
            startRecording()
        } catch (e: CameraException) {
            cleanupSession()
            onError?.invoke(e)
        }
    }

    private fun setupVideoOutput() {
        videoOutput = AVCaptureVideoDataOutput()
        videoOutput?.setSampleBufferDelegate(this, dispatch_get_main_queue())
        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(AVCaptureDeviceTypeBuiltInWideAngleCamera),
            AVMediaTypeVideo,
            AVCaptureDevicePositionUnspecified
        ).devices

        if (availableDevices.isEmpty()) return false

        for (device in availableDevices) {
            when ((device as AVCaptureDevice).position) {
                AVCaptureDevicePositionBack -> backCamera = device
                AVCaptureDevicePositionFront -> frontCamera = device
            }
        }
        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() {
        if (captureSession?.isRunning() == false) {
            dispatch_async(
                dispatch_get_global_queue(
                    DISPATCH_QUEUE_PRIORITY_HIGH.toLong(), 0u
                )
            ) {
                captureSession?.startRunning()
            }
        }
    }

    fun stopSession() {
        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
        }
    }

    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() {
        guard(captureSession != null) { return@guard }

        captureSession?.beginConfiguration()

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

            isUsingFrontCamera = !isUsingFrontCamera
            currentCamera = if (isUsingFrontCamera) frontCamera else backCamera

            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")
            }

            cameraPreviewLayer?.connection?.let { connection ->
                if (connection.isVideoMirroringSupported()) {
                    connection.automaticallyAdjustsVideoMirroring = false
                    connection.setVideoMirrored(isUsingFrontCamera)
                }
            }

            captureSession?.commitConfiguration()
        } catch (e: CameraException) {
            captureSession?.commitConfiguration()
            onError?.invoke(e)
        } catch (e: Exception) {
            captureSession?.commitConfiguration()
            onError?.invoke(CameraException.ConfigurationError(e.message ?: "Unknown error"))
        }
    }

    private val frameProcessor = FrameProcessor(null)

    @OptIn(ExperimentalForeignApi::class, NativeRuntimeApi::class)
    override fun captureOutput(
        output: AVCaptureOutput,
        didOutputSampleBuffer: CMSampleBufferRef?,
        fromConnection: AVCaptureConnection
    ) {
        timeStamp().also { timestamp ->
            runCatching {
                dispatch_queue_create(
                    "com.performancecoachlab.frameProcessing", null
                ).also { processingQueue ->
                    dispatch_async(processingQueue) {
                        try {
                            var detectedSkeleton: Skeleton? = null
                            var detectedObjects: List<AnalysisObject> = emptyList()
                            frameProcessor.analyseBufferForAll(
                                CMSampleBufferGetImageBuffer(didOutputSampleBuffer),
                                timestamp,
                                preview = cameraPreviewLayer,
                                onSkeletonProcessed = { skeleton ->
                                    detectedSkeleton = skeleton
                                },
                                onObjectsProcessed = { objects ->
                                    detectedObjects = objects
                                })
                            cameraPreviewLayer?.also { preview ->
                                val previewSkeleton = detectedSkeleton?.let {
                                    mapSkeletonToPreview(
                                        skeleton = it,
                                        previewLayer = preview,
                                        width = 480f,
                                        height = 360f
                                    )
                                }
                                previewSkeleton?.also {
                                    skeletonRepository?.updateSkeleton(
                                        it
                                    )
                                }
                                val previewObjects = detectedObjects.map {
                                    it.copy(
                                        boundingBox = mapBoxToPreview(
                                            it.boundingBox, preview, width = 480f, height = 360f
                                        ), frameSize = it.frameSize.let {
                                            mapBoxToPreview(
                                                Rect(Offset.Zero, Size(480f, 360f)),
                                                preview,
                                                width = 480f,
                                                height = 360f
                                            ).let { FrameSize(it.width.toInt(), it.height.toInt()) }
                                        })
                                }
                                previewObjects?.also { objects ->
                                    customObjectRepository?.updateCustomObject(objects)
                                }
                                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()
                                    ).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
                                                }
                                            )
                                        }
                                        frameListener?.updateFrame(
                                            drawn
                                        )
                                    }
                                }
                            }
                        } catch (e: Exception) {
                            //println(e.message ?: "Unknown error in frame processing")
                        }
                    }
                }
            }
        }
        MemoryManager.updateMemoryStatus()
        kotlin.native.runtime.GC.collect()
    }

    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()
        }
    }

    @OptIn(ExperimentalForeignApi::class)
    fun mapBoxToPreview(
        box: Rect,
        previewLayer: AVCaptureVideoPreviewLayer,
        width: Float,
        height: Float,
    ): Rect {
        fun mapPoint(point: Offset): Offset {
            val normalizedPoint =
                CGPointMake(point.x.toDouble() / width, point.y.toDouble() / height)
            val screenPoint = previewLayer.pointForCaptureDevicePointOfInterest(normalizedPoint)
            return Offset(
                screenPoint.useContents { x.toFloat() },
                screenPoint.useContents { y.toFloat() })
        }

        val topLeft = mapPoint(box.topLeft)
        val bottomRight = mapPoint(box.bottomRight)
        return Rect(topLeft = topLeft, bottomRight = bottomRight)
    }

    override fun captureOutput(
        output: AVCaptureFileOutput,
        didFinishRecordingToOutputFileAtURL: NSURL,
        fromConnections: List<*>,
        error: NSError?
    ) {
        onVideoSaved?.invoke(didFinishRecordingToOutputFileAtURL.path ?: "")
    }

}

@OptIn(ExperimentalForeignApi::class)
fun mapSkeletonToPreview(
    skeleton: Skeleton, previewLayer: AVCaptureVideoPreviewLayer, width: Float, height: Float
): Skeleton {
    fun mapPoint(point: Skeleton.SkeletonCoordinate?): Skeleton.SkeletonCoordinate? {
        if (point == null) return null

        // Normalize the point
        val normalizedPoint = CGPointMake(point.x.toDouble() / width, point.y.toDouble() / height)
        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,
    )
}


@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) }
}
