/**
 * Copyright (c) 2026 Eazytec 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.
 */

#include "SkData.h"
#include "SkImage.h"
#include "SkBitmap.h"
#include "SkShader.h"
#include "SkEncodedImageFormat.h"
#include "include/gpu/ganesh/SkImageGanesh.h"
#include "gpu/ganesh/GrBackendSurface.h"
#include "gpu/ganesh/GrDirectContext.h"
#include "gpu/ganesh/GrTypes.h"
#include "gpu/ganesh/gl/GrGLTypes.h"
#include "gpu/ganesh/gl/GrGLBackendSurface.h"
#include "gpu/GpuTypes.h"
#include "SkAlphaType.h"
#include "SkColorSpace.h"
#include "SkColorType.h"
#include "common.h"
#include "Utils.h"

#define EGL_EGLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#define GL_GLEXT_PROTOTYPES
#include <GLES2/gl2ext.h>
#include "native_window/external_window.h"
#include <native_buffer/native_buffer.h>
#include <hilog/log.h>
#include <mutex>
#include <vector>

namespace {
const unsigned int LOG_PRINT_DOMAIN = 0xFF00;

// A/B: YUV NativeBuffer texture target.
// ExternalOes is the Khronos-correct target for YUV EGLImage siblings; Texture2D matches
// current OHOS sample snippets. Flip if device/Skia rejects one path.
enum class YuvImportTargetMode {
    Texture2D = 0,
    ExternalOes = 1,
};
static constexpr YuvImportTargetMode kYuvImportTargetMode = YuvImportTargetMode::ExternalOes;

struct OhosEglImageReleaseContext {
    EGLDisplay display = EGL_NO_DISPLAY;
    EGLImageKHR image = EGL_NO_IMAGE_KHR;
    GLuint textureId = 0;
    GLenum textureTarget = GL_TEXTURE_2D;
    OH_NativeBuffer* nativeBuffer = nullptr;
    OHNativeWindowBuffer* nativeWindowBuffer = nullptr;
};

static std::mutex gReleaseQueueMutex;
static std::vector<OhosEglImageReleaseContext *> gReleaseQueue;

static bool IsYuv420SpNativeFormat(int32_t format) {
    return format == NATIVEBUFFER_PIXEL_FMT_YCRCB_420_SP ||
           format == NATIVEBUFFER_PIXEL_FMT_YCBCR_420_SP;
}

static void OhosTextureReleaseProc(SkImages::ReleaseContext ctx) {
    auto releaseCtx = static_cast<OhosEglImageReleaseContext *>(ctx);
    if (!releaseCtx) {
        return;
    }
    std::lock_guard<std::mutex> lock(gReleaseQueueMutex);
    gReleaseQueue.push_back(releaseCtx);
}

static void DrainOhosReleaseQueueOnCurrentGLThread() {
    EGLContext curCtx = eglGetCurrentContext();
    if (curCtx == EGL_NO_CONTEXT) {
        return;
    }

    std::vector<OhosEglImageReleaseContext *> pending;
    {
        std::lock_guard<std::mutex> lock(gReleaseQueueMutex);
        pending.swap(gReleaseQueue);
    }
    if (pending.empty()) {
        return;
    }

    PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR =
        reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));

    for (auto* releaseCtx : pending) {
        if (!releaseCtx) continue;

        if (releaseCtx->textureId != 0) {
            glDeleteTextures(1, &releaseCtx->textureId);
        }
        if (eglDestroyImageKHR && releaseCtx->image != EGL_NO_IMAGE_KHR &&
            releaseCtx->display != EGL_NO_DISPLAY) {
            eglDestroyImageKHR(releaseCtx->display, releaseCtx->image);
        }
        if (releaseCtx->nativeWindowBuffer) {
            OH_NativeWindow_DestroyNativeWindowBuffer(releaseCtx->nativeWindowBuffer);
        }
        if (releaseCtx->nativeBuffer) {
            OH_NativeBuffer_Unreference(releaseCtx->nativeBuffer);
        }
        delete releaseCtx;
    }
}
}  // namespace

SKIKO_EXPORT KNativePointer
org_jetbrains_skia_Image__1nMakeFromOhosSurfaceBuffer(KNativePointer contextPtr,
                                                      KNativePointer surfaceBufferPtr,
                                                      KInt colorType,
                                                      KInt alphaType,
                                                      KNativePointer colorSpacePtr) {
    if (!contextPtr || !surfaceBufferPtr) return nullptr;

    auto* context = reinterpret_cast<GrDirectContext*>(contextPtr);
    auto* surfaceBuffer = reinterpret_cast<OH_NativeBuffer*>(surfaceBufferPtr);
    if (!context || !surfaceBuffer) return nullptr;

    EGLContext curCtx = eglGetCurrentContext();
    if (curCtx == EGL_NO_CONTEXT) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
                     "MakeFromOhosSurfaceBuffer failed: EGL_NO_CONTEXT");
        return nullptr;
    }

    DrainOhosReleaseQueueOnCurrentGLThread();

    EGLDisplay display = eglGetCurrentDisplay();
    if (display == EGL_NO_DISPLAY) display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (display == EGL_NO_DISPLAY) return nullptr;

    PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR =
        reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(eglGetProcAddress("eglCreateImageKHR"));
    PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR =
        reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
    if (!eglCreateImageKHR) return nullptr;

    OH_NativeBuffer_Config config = {};
    OH_NativeBuffer_GetConfig(surfaceBuffer, &config);
    const bool isYuvBuffer = IsYuv420SpNativeFormat(config.format);
    const GLenum textureTarget =
        (isYuvBuffer && kYuvImportTargetMode == YuvImportTargetMode::ExternalOes)
            ? GL_TEXTURE_EXTERNAL_OES
            : GL_TEXTURE_2D;

    GLint prevActiveTex = GL_TEXTURE0;
    GLint prevTex2D = 0;
    GLint prevTexExternal = 0;
    glGetIntegerv(GL_ACTIVE_TEXTURE, &prevActiveTex);
    glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevTex2D);
    if (textureTarget == GL_TEXTURE_EXTERNAL_OES) {
        glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &prevTexExternal);
    }

    OHNativeWindowBuffer* nativeWindowBuffer = nullptr;
    EGLImageKHR eglImage = EGL_NO_IMAGE_KHR;
    GLuint texId = 0;
    bool bufferRefHeld = false;
    KNativePointer outPtr = nullptr;

    auto cleanupFail = [&]() {
        if (texId != 0) glDeleteTextures(1, &texId);
        if (eglDestroyImageKHR && eglImage != EGL_NO_IMAGE_KHR) {
            eglDestroyImageKHR(display, eglImage);
        }
        if (nativeWindowBuffer) {
            OH_NativeWindow_DestroyNativeWindowBuffer(nativeWindowBuffer);
        }
        if (bufferRefHeld) {
            OH_NativeBuffer_Unreference(surfaceBuffer);
        }
    };

    nativeWindowBuffer = OH_NativeWindow_CreateNativeWindowBufferFromNativeBuffer(surfaceBuffer);
    if (!nativeWindowBuffer) {
        cleanupFail();
        return nullptr;
    }

    if (OH_NativeBuffer_Reference(surfaceBuffer) != 0) {
        cleanupFail();
        return nullptr;
    }
    bufferRefHeld = true;

    EGLint attrs[] = { EGL_IMAGE_PRESERVED, EGL_TRUE, EGL_NONE };
    eglImage = eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_OHOS, nativeWindowBuffer, attrs);
    if (eglImage == EGL_NO_IMAGE_KHR) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
                     "MakeFromOhosSurfaceBuffer: eglCreateImageKHR failed format=%{public}d yuv=%{public}d",
                     config.format, isYuvBuffer ? 1 : 0);
        cleanupFail();
        return nullptr;
    }

    glGenTextures(1, &texId);
    if (texId == 0) {
        cleanupFail();
        return nullptr;
    }

    glBindTexture(textureTarget, texId);
    glTexParameteri(textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glEGLImageTargetTexture2DOES(textureTarget, static_cast<GLeglImageOES>(eglImage));
    const GLenum bindErr = glGetError();
    if (bindErr != GL_NO_ERROR) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
                     "MakeFromOhosSurfaceBuffer: glEGLImageTargetTexture2DOES err=0x%{public}x "
                     "target=0x%{public}x format=%{public}d",
                     bindErr, textureTarget, config.format);
        cleanupFail();
        return nullptr;
    }

    if (textureTarget == GL_TEXTURE_EXTERNAL_OES) {
        glBindTexture(GL_TEXTURE_EXTERNAL_OES, static_cast<GLuint>(prevTexExternal));
    } else {
        glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(prevTex2D));
    }
    glActiveTexture(prevActiveTex);

    // GPU samples YUV EGLImage as RGB; Skia color type describes the sampled result.
#ifdef GL_RGBA8_OES
    const GrGLenum glSizedFormat = GL_RGBA8_OES;
#else
    const GrGLenum glSizedFormat = GL_RGBA8;
#endif
    // OH_NativeBuffer_GetConfig returns void, so validate the populated dimensions before
    // handing them to Skia. A zero or negative width/height would build a degenerate backend
    // texture (and likely a GL error downstream); reject early and run the same cleanup as
    // every other failure path so the EGL image, texture and NativeBuffer reference are freed.
    if (config.width <= 0 || config.height <= 0) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
                     "MakeFromOhosSurfaceBuffer failed: invalid NativeBuffer config w=%d h=%d",
                     config.width, config.height);
        cleanupFail();
        return nullptr;
    }

    GrGLTextureInfo textureInfo = { textureTarget, texId, glSizedFormat };

    GrBackendTexture backendTexture = GrBackendTextures::MakeGL(config.width,
                                                                config.height,
                                                                skgpu::Mipmapped::kNo,
                                                                textureInfo);
    auto cs = reinterpret_cast<SkColorSpace *>(colorSpacePtr);
    sk_sp<SkColorSpace> colorSpace = cs ? sk_ref_sp(cs) : nullptr;

    // For YUV buffers ignore caller color/alpha: sampled texels are opaque RGB.
    const SkColorType effectiveColorType =
        isYuvBuffer ? kRGBA_8888_SkColorType : static_cast<SkColorType>(colorType);
    const SkAlphaType effectiveAlphaType =
        isYuvBuffer ? kOpaque_SkAlphaType : static_cast<SkAlphaType>(alphaType);

    OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_PRINT_DOMAIN, "EGLCore",
                 "ImportContract: w=%{public}d h=%{public}d stride=%{public}d "
                 "nbFormat=%{public}d actualYuv=%{public}d glTarget=0x%{public}x "
                 "skColorType=%{public}d skAlphaType=%{public}d",
                 config.width,
                 config.height,
                 config.stride,
                 config.format,
                 isYuvBuffer ? 1 : 0,
                 textureTarget,
                 static_cast<int>(effectiveColorType),
                 static_cast<int>(effectiveAlphaType));

    context->resetContext(kAll_GrBackendState);

    auto releaseCtx = new OhosEglImageReleaseContext{
        display, eglImage, texId, textureTarget, surfaceBuffer, nativeWindowBuffer
    };

    sk_sp<SkImage> image = SkImages::BorrowTextureFrom(
        context,
        backendTexture,
        kTopLeft_GrSurfaceOrigin,
        effectiveColorType,
        effectiveAlphaType,
        colorSpace,
        OhosTextureReleaseProc,
        releaseCtx);

    if (!image) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
                     "MakeFromOhosSurfaceBuffer: BorrowTextureFrom failed yuv=%{public}d target=0x%{public}x",
                     isYuvBuffer ? 1 : 0, textureTarget);
        OhosTextureReleaseProc(releaseCtx);
        DrainOhosReleaseQueueOnCurrentGLThread();
        return nullptr;
    }

    outPtr = reinterpret_cast<KNativePointer>(image.release());
    return outPtr;
}

SKIKO_EXPORT void org_jetbrains_skia_Image__1nDrainOhosSurfaceBufferReleases() {
    DrainOhosReleaseQueueOnCurrentGLThread();
}
