/*
 * Copyright (c) 2026 Huawei Device Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:OptIn(
    kotlinx.cinterop.ExperimentalForeignApi::class,
    org.jetbrains.skiko.ExperimentalSkikoApi::class,
)

package org.jetbrains.skia

import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.COpaquePointer
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.cstr
import kotlinx.cinterop.memScoped
import org.jetbrains.skia.impl.InteropPointer
import org.jetbrains.skia.impl.Native
import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl.Stats
import org.jetbrains.skia.impl.interopScope
import org.jetbrains.skiko.ExperimentalSkikoApi

/**
 * Renderer-independent OHOS image decode request.
 *
 * The same request and result contract is published by the Skia and Fusion Renderer artifacts.
 */
@ExperimentalSkikoApi
sealed class OhosImageDecodeRequest {
    class EncodedTargetSize(
        val bytes: ByteArray,
        val desiredWidthPx: Int,
        val desiredHeightPx: Int,
        val cropX: Int = 0,
        val cropY: Int = 0,
        val cropWidth: Int = 0,
        val cropHeight: Int = 0,
    ) : OhosImageDecodeRequest()

    class RawFile(
        // Nullable so request validation can classify a null manager as INVALID_INPUT. Callers
        // that obtain the manager from OhosAssetReader pass a non-null value unchanged.
        val nativeResourceManager: COpaquePointer?,
        val path: String,
        val targetWidthPx: Int? = null,
        val targetHeightPx: Int? = null,
        val contentScaleMode: Int,
        val decodeSizeBucketPx: Int,
    ) : OhosImageDecodeRequest()

    class MediaResource(
        val nativeResourceManager: COpaquePointer?,
        val resourceId: Int,
        val targetWidthPx: Int? = null,
        val targetHeightPx: Int? = null,
        val contentScaleMode: Int,
        val decodeSizeBucketPx: Int,
    ) : OhosImageDecodeRequest()
}

@ExperimentalSkikoApi
enum class OhosImageDecodeFailureReason {
    INVALID_INPUT,
    DECODE_FAILED,
}

/**
 * Result of an OHOS image decode.
 *
 * A successful result owns [image]; the caller must close it.
 */
@ExperimentalSkikoApi
sealed class OhosImageDecodeResult {
    class Success(val image: Image) : OhosImageDecodeResult()

    class Failure(
        val reason: OhosImageDecodeFailureReason,
        val message: String,
        val cause: Throwable? = null,
    ) : OhosImageDecodeResult()
}

/**
 * Stable OHOS decode facade shared by Skia and Fusion Renderer artifacts.
 *
 * This is a **synchronous, blocking** call. Long-running decodes should be dispatched
 * to a background thread by the caller (e.g. `withContext(Dispatchers.Default)`).
 *
 * This operation does not require a renderer or [DirectContext]. The returned [Image] is
 * renderer-independent and owned by the caller — the caller must [Image.close] it when done.
 *
 * For [OhosImageDecodeRequest.RawFile] and [OhosImageDecodeRequest.MediaResource], the
 * `nativeResourceManager` must remain valid for the entire duration of this call.
 */
@ExperimentalSkikoApi
object OhosImageDecoder {
    /**
     * Decodes an OHOS image synchronously.
     *
     * @param request the decode request; must pass [OhosImageDecodeRequest.isValid].
     * @return [OhosImageDecodeResult.Success] with an [Image] the caller owns and must close,
     *         or [OhosImageDecodeResult.Failure] with a reason and message.
     */
    fun decode(request: OhosImageDecodeRequest): OhosImageDecodeResult {
        if (!request.isValid()) {
            return OhosImageDecodeResult.Failure(
                OhosImageDecodeFailureReason.INVALID_INPUT,
                "Invalid OHOS image decode request",
            )
        }
        val image = try {
            when (request) {
                is OhosImageDecodeRequest.EncodedTargetSize ->
                    makeFromEncodedOhosTargetSizeShared(
                        bytes = request.bytes,
                        desiredWidthPx = request.desiredWidthPx,
                        desiredHeightPx = request.desiredHeightPx,
                        cropX = request.cropX,
                        cropY = request.cropY,
                        cropWidth = request.cropWidth,
                        cropHeight = request.cropHeight,
                    )
                is OhosImageDecodeRequest.RawFile ->
                    makeFromRawFileOhosShared(
                        // isValid() already guaranteed a non-null manager; !! is safe on this path.
                        nativeResourceManager = request.nativeResourceManager!!,
                        rawFilePath = request.path,
                        targetWidthPx = request.targetWidthPx,
                        targetHeightPx = request.targetHeightPx,
                        contentScaleMode = request.contentScaleMode,
                        decodeSizeBucketPx = request.decodeSizeBucketPx,
                    )
                is OhosImageDecodeRequest.MediaResource ->
                    makeFromMediaOhosShared(
                        // isValid() already guaranteed a non-null manager; !! is safe on this path.
                        nativeResourceManager = request.nativeResourceManager!!,
                        mediaResId = request.resourceId,
                        targetWidthPx = request.targetWidthPx,
                        targetHeightPx = request.targetHeightPx,
                        contentScaleMode = request.contentScaleMode,
                        decodeSizeBucketPx = request.decodeSizeBucketPx,
                    )
            }
        } catch (error: Exception) {
            // Catch only Exception, not Error/OOM/LinkageError — let fatal errors propagate.
            return OhosImageDecodeResult.Failure(
                OhosImageDecodeFailureReason.DECODE_FAILED,
                "OHOS image decode failed",
                error,
            )
        }
        return if (image != null) {
            OhosImageDecodeResult.Success(image)
        } else {
            OhosImageDecodeResult.Failure(
                OhosImageDecodeFailureReason.DECODE_FAILED,
                "OHOS image decode failed",
            )
        }
    }
}

private fun OhosImageDecodeRequest.isValid(): Boolean = when (this) {
    is OhosImageDecodeRequest.EncodedTargetSize -> {
        val noCrop = cropX == 0 && cropY == 0 && cropWidth == 0 && cropHeight == 0
        val validCrop = cropX >= 0 && cropY >= 0 && cropWidth > 0 && cropHeight > 0
        bytes.isNotEmpty() && desiredWidthPx > 0 && desiredHeightPx > 0 && (noCrop || validCrop)
    }
    is OhosImageDecodeRequest.RawFile ->
        // COpaquePointer is nullable; a null resource-manager pointer must be classified as
        // INVALID_INPUT rather than reaching native code, which rejects it inconsistently.
        nativeResourceManager != null &&
            path.isNotEmpty() &&
            hasValidOptionalTargetSize(targetWidthPx, targetHeightPx)
    is OhosImageDecodeRequest.MediaResource ->
        nativeResourceManager != null &&
            resourceId > 0 &&
            hasValidOptionalTargetSize(targetWidthPx, targetHeightPx)
}

internal fun makeFromEncodedOhosTargetSizeShared(
    bytes: ByteArray,
    desiredWidthPx: Int,
    desiredHeightPx: Int,
    cropX: Int = 0,
    cropY: Int = 0,
    cropWidth: Int = 0,
    cropHeight: Int = 0,
): Image? {
    if (bytes.isEmpty() || desiredWidthPx <= 0 || desiredHeightPx <= 0) return null

    Stats.onNativeCall()
    val ptr = interopScope {
        _nMakeFromEncodedOhosTargetSize(
            bytes = toInterop(bytes),
            encodedLength = bytes.size,
            desiredWidthPx = desiredWidthPx,
            desiredHeightPx = desiredHeightPx,
            cropX = cropX,
            cropY = cropY,
            cropWidth = cropWidth,
            cropHeight = cropHeight,
        )
    }
    return if (ptr == Native.NullPointer) null else Image(ptr)
}

internal fun makeFromRawFileOhosShared(
    nativeResourceManager: COpaquePointer,
    rawFilePath: String,
    targetWidthPx: Int? = null,
    targetHeightPx: Int? = null,
    contentScaleMode: Int,
    decodeSizeBucketPx: Int,
): Image? {
    if (rawFilePath.isEmpty() || !hasValidOptionalTargetSize(targetWidthPx, targetHeightPx)) return null

    Stats.onNativeCall()
    val ptr = memScoped {
        _nMakeFromRawFileOhos(
            nativeResourceManager = nativeResourceManager,
            rawFilePath = rawFilePath.cstr.ptr,
            targetWidthPx = targetWidthPx ?: 0,
            targetHeightPx = targetHeightPx ?: 0,
            contentScaleMode = contentScaleMode,
            decodeSizeBucketPx = decodeSizeBucketPx,
        )
    }
    return if (ptr == Native.NullPointer) null else Image(ptr)
}

internal fun makeFromMediaOhosShared(
    nativeResourceManager: COpaquePointer,
    mediaResId: Int,
    targetWidthPx: Int? = null,
    targetHeightPx: Int? = null,
    contentScaleMode: Int,
    decodeSizeBucketPx: Int,
): Image? {
    if (mediaResId <= 0 || !hasValidOptionalTargetSize(targetWidthPx, targetHeightPx)) return null

    Stats.onNativeCall()
    val ptr = _nMakeFromMediaOhos(
        nativeResourceManager = nativeResourceManager,
        mediaResId = mediaResId,
        targetWidthPx = targetWidthPx ?: 0,
        targetHeightPx = targetHeightPx ?: 0,
        contentScaleMode = contentScaleMode,
        decodeSizeBucketPx = decodeSizeBucketPx,
    )
    return if (ptr == Native.NullPointer) null else Image(ptr)
}

private fun hasValidOptionalTargetSize(targetWidthPx: Int?, targetHeightPx: Int?): Boolean =
    (targetWidthPx == null && targetHeightPx == null) ||
        (targetWidthPx != null && targetWidthPx > 0 && targetHeightPx != null && targetHeightPx > 0)

@ExternalSymbolName("org_jetbrains_skia_Image__1nMakeFromEncodedOhosTargetSize")
private external fun _nMakeFromEncodedOhosTargetSize(
    bytes: InteropPointer,
    encodedLength: Int,
    desiredWidthPx: Int,
    desiredHeightPx: Int,
    cropX: Int,
    cropY: Int,
    cropWidth: Int,
    cropHeight: Int,
): NativePointer

@ExternalSymbolName("org_jetbrains_skia_Image__1nMakeFromRawFileOhos")
private external fun _nMakeFromRawFileOhos(
    nativeResourceManager: COpaquePointer,
    rawFilePath: CPointer<ByteVar>,
    targetWidthPx: Int,
    targetHeightPx: Int,
    contentScaleMode: Int,
    decodeSizeBucketPx: Int,
): NativePointer

@ExternalSymbolName("org_jetbrains_skia_Image__1nMakeFromMediaOhos")
private external fun _nMakeFromMediaOhos(
    nativeResourceManager: COpaquePointer,
    mediaResId: Int,
    targetWidthPx: Int,
    targetHeightPx: Int,
    contentScaleMode: Int,
    decodeSizeBucketPx: Int,
): NativePointer

@ExperimentalSkikoApi
enum class OhosImageInteropCapability {
    SURFACE_BUFFER_ZERO_COPY,
}

@ExperimentalSkikoApi
enum class OhosImageTransferMode {
    ZERO_COPY,
}

/**
 * Result of importing an OHOS native image into the active renderer.
 */
@ExperimentalSkikoApi
sealed class OhosImageImportResult {
    class Success(
        val image: Image,
        val transferMode: OhosImageTransferMode,
    ) : OhosImageImportResult()

    class Unsupported(
        val capability: OhosImageInteropCapability,
        val message: String,
    ) : OhosImageImportResult()

    class Failure(
        val message: String,
        val cause: Throwable? = null,
    ) : OhosImageImportResult()
}

/**
 * Renderer-scoped image interop contract.
 *
 * Implementations hide backend-only state such as a Skia [DirectContext]. Callers can use this
 * interface without branching on the active renderer.
 *
 * **Ownership contract for [importSurfaceBuffer]:**
 * - The caller retains ownership of the passed `SurfaceBuffer` reference. On Skia success,
 *   the implementation internally calls `OH_NativeBuffer_Reference` so the produced [Image]
 *   survives independently; the caller may unreference its own ref after the call returns.
 * - On [OhosImageImportResult.Success], the returned [Image] is owned by the caller —
 *   it **must** be [Image.close]d when no longer needed.
 * - The Fusion Renderer returns [OhosImageImportResult.Unsupported] and does **not** create
 *   or require a [DirectContext].
 */
@ExperimentalSkikoApi
interface OhosImageInteropSession {
    fun supports(capability: OhosImageInteropCapability): Boolean

    /**
     * Imports an OHOS SurfaceBuffer into the active renderer without copying its pixels.
     *
     * Call this only on the active renderer thread. A session is scoped to its renderer lifecycle;
     * callers must handle [OhosImageImportResult.Failure] when a previously captured session has
     * expired. A successful result owns its image, which must be closed by the caller.
     */
    fun importSurfaceBuffer(
        surfaceBufferPtr: NativePointer,
        colorType: ColorType = ColorType.RGBA_8888,
        alphaType: ColorAlphaType = ColorAlphaType.PREMUL,
        colorSpace: ColorSpace = ColorSpace.sRGB,
    ): OhosImageImportResult

    companion object {
        val Unsupported: OhosImageInteropSession = object : OhosImageInteropSession {
            override fun supports(capability: OhosImageInteropCapability): Boolean = false

            override fun importSurfaceBuffer(
                surfaceBufferPtr: NativePointer,
                colorType: ColorType,
                alphaType: ColorAlphaType,
                colorSpace: ColorSpace,
            ): OhosImageImportResult = OhosImageImportResult.Unsupported(
                OhosImageInteropCapability.SURFACE_BUFFER_ZERO_COPY,
                "The active renderer does not support OHOS SurfaceBuffer import",
            )
        }
    }
}
