/****************************************************************************
 Copyright (c) 2019-2023 Xiamen Yaji Software Co., Ltd.

 http://www.cocos.com

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights to
 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 of the Software, and to permit persons to whom the Software is furnished to do so,
 subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
****************************************************************************/

#include <thread>
#include "GLES2GPUObjects.h"
#include "base/StringUtil.h"

#if CC_SWAPPY_ENABLED
    #include "swappy/swappyGL.h"
#endif

#define FORCE_DISABLE_VALIDATION 1

namespace cc {
namespace gfx {

#if CC_DEBUG > 0 && !FORCE_DISABLE_VALIDATION
constexpr uint32_t DISABLE_VALIDATION_ASSERTIONS = 1; // 0 for default behavior, otherwise assertions will be disabled

void GL_APIENTRY GLES2EGLDebugProc(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam) {
    String sourceDesc;
    switch (source) {
        case GL_DEBUG_SOURCE_API_KHR: sourceDesc = "API"; break;
        case GL_DEBUG_SOURCE_SHADER_COMPILER_KHR: sourceDesc = "SHADER_COMPILER"; break;
        case GL_DEBUG_SOURCE_WINDOW_SYSTEM_KHR: sourceDesc = "WINDOW_SYSTEM"; break;
        case GL_DEBUG_SOURCE_THIRD_PARTY_KHR: sourceDesc = "THIRD_PARTY"; break;
        case GL_DEBUG_SOURCE_APPLICATION_KHR: sourceDesc = "APPLICATION"; break;
        default: sourceDesc = "OTHER";
    }

    String typeDesc;
    switch (type) {
        case GL_DEBUG_TYPE_ERROR_KHR: typeDesc = "ERROR"; break;
        case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR_KHR: typeDesc = "PEPRECATED_BEHAVIOR"; break;
        case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR_KHR: typeDesc = "UNDEFINED_BEHAVIOR"; break;
        case GL_DEBUG_TYPE_PERFORMANCE_KHR: typeDesc = "PERFORMANCE"; break;
        case GL_DEBUG_TYPE_PORTABILITY_KHR: typeDesc = "PORTABILITY"; break;
        case GL_DEBUG_TYPE_MARKER_KHR: typeDesc = "MARKER"; break;
        case GL_DEBUG_TYPE_PUSH_GROUP_KHR: typeDesc = "PUSH_GROUP"; break;
        case GL_DEBUG_TYPE_POP_GROUP_KHR: typeDesc = "POP_GROUP"; break;
        default: typeDesc = "OTHER";
    }

    String severityDesc;
    switch (severity) {
        case GL_DEBUG_SEVERITY_HIGH_KHR: severityDesc = "HIGH"; break;
        case GL_DEBUG_SEVERITY_MEDIUM_KHR: severityDesc = "MEDIUM"; break;
        case GL_DEBUG_SEVERITY_LOW_KHR: severityDesc = "LOW"; break;
        default: severityDesc = "NOTIFICATION";
    }

    String msg = StringUtil::format("source: %s, type: %s, severity: %s, message: %s",
                                    sourceDesc.c_str(), typeDesc.c_str(), severityDesc.c_str(), message);

    if (severity == GL_DEBUG_SEVERITY_HIGH_KHR) {
        CC_LOG_ERROR(msg.c_str());
        CC_ASSERT(DISABLE_VALIDATION_ASSERTIONS);
    } else if (severity == GL_DEBUG_SEVERITY_MEDIUM_KHR) {
        CC_LOG_WARNING(msg.c_str());
    } else {
        CC_LOG_DEBUG(msg.c_str());
    }
}
#endif

bool GLES2GPUContext::initialize(GLES2GPUStateCache *stateCache, GLES2GPUConstantRegistry *constantRegistry) {
    _stateCache = stateCache;
    _constantRegistry = constantRegistry;

    if (!gles2wInit()) {
        return false;
    }

    EGL_CHECK(eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY));

    if (eglDisplay == EGL_NO_DISPLAY) {
        CC_LOG_ERROR("eglGetDisplay() - FAILED.");
        return false;
    }

    EGLBoolean success{false};
    EGL_CHECK(success = eglInitialize(eglDisplay, &eglMajorVersion, &eglMinorVersion));

    if (!success) {
        CC_LOG_ERROR("eglInitialize() - FAILED.");
        return false;
    }

    EGL_CHECK(eglBindAPI(EGL_OPENGL_ES_API));

    bool msaaEnabled{false};
    bool qualityPreferred{false};

    EGLint redSize{8};
    EGLint greenSize{8};
    EGLint blueSize{8};
    EGLint alphaSize{8};
    EGLint depthSize{24};
    EGLint stencilSize{8};
    EGLint sampleBufferSize{msaaEnabled ? EGL_DONT_CARE : 0};
    EGLint sampleSize{msaaEnabled ? EGL_DONT_CARE : 0};

    EGLint defaultAttribs[]{
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_BLUE_SIZE, blueSize,
        EGL_GREEN_SIZE, greenSize,
        EGL_RED_SIZE, redSize,
        EGL_ALPHA_SIZE, alphaSize,
        EGL_DEPTH_SIZE, depthSize,
        EGL_STENCIL_SIZE, stencilSize,
        EGL_SAMPLE_BUFFERS, sampleBufferSize,
        EGL_SAMPLES, sampleSize,
        EGL_NONE};

    int numConfig{0};
    ccstd::vector<EGLConfig> eglConfigs;

    EGL_CHECK(success = eglChooseConfig(eglDisplay, defaultAttribs, nullptr, 0, &numConfig));
    if (success) {
        eglConfigs.resize(numConfig);
    } else {
        CC_LOG_ERROR("Query GLES2 configuration failed.");
        return false;
    }

    int count = numConfig;
    EGL_CHECK(success = eglChooseConfig(eglDisplay, defaultAttribs, eglConfigs.data(), count, &numConfig));
    if (!success || !numConfig) {
        CC_LOG_ERROR("eglChooseConfig configuration failed.");
        return false;
    }

    EGLint depth{0};
    EGLint stencil{0};
    EGLint sampleBuffers{0};
    EGLint sampleCount{0};
    EGLint params[8]{0};
    uint64_t lastScore = qualityPreferred ? std::numeric_limits<uint64_t>::min() : std::numeric_limits<uint64_t>::max();

    for (int i = 0; i < numConfig; i++) {
        int depthValue{0};
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_RED_SIZE, &params[0]);
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_GREEN_SIZE, &params[1]);
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_BLUE_SIZE, &params[2]);
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_ALPHA_SIZE, &params[3]);
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_DEPTH_SIZE, &params[4]);
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_STENCIL_SIZE, &params[5]);
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_SAMPLE_BUFFERS, &params[6]);
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_SAMPLES, &params[7]);
        eglGetConfigAttrib(eglDisplay, eglConfigs[i], EGL_DEPTH_ENCODING_NV, &depthValue);

        int bNonLinearDepth = (depthValue == EGL_DEPTH_ENCODING_NONLINEAR_NV) ? 1 : 0;

        /*------------------------------------------ANGLE's priority-----------------------------------------------*/
        // Favor EGLConfigLists by RGB, then Depth, then Non-linear Depth, then Stencil, then Alpha
        uint64_t currScore{0};
        EGLint colorScore = std::abs(params[0] - redSize) + std::abs(params[1] - greenSize) + std::abs(params[2] - blueSize);
        currScore |= static_cast<uint64_t>(std::min(std::max(params[6], 0), 15)) << 29;
        currScore |= static_cast<uint64_t>(std::min(std::max(params[7], 0), 31)) << 24;
        currScore |= static_cast<uint64_t>(std::min(colorScore, 127)) << 17;
        currScore |= static_cast<uint64_t>(std::min(std::abs(params[4] - depthSize), 63)) << 11;
        currScore |= static_cast<uint64_t>(std::min(std::abs(1 - bNonLinearDepth), 1)) << 10;
        currScore |= static_cast<uint64_t>(std::min(std::abs(params[5] - stencilSize), 31)) << 6;
        currScore |= static_cast<uint64_t>(std::min(std::abs(params[3] - alphaSize), 31)) << 0;
        /*------------------------------------------ANGLE's priority-----------------------------------------------*/

        // if msaaEnabled, sampleBuffers and sampleCount should be greater than 0, until iterate to the last one(can't find).
        bool msaaLimit = (msaaEnabled ? (params[6] > 0 && params[7] > 0) : (params[6] == 0 && params[7] == 0));
        // performancePreferred ? [>=] : [<] , egl configurations store in "ascending order"
        bool filter = (currScore < lastScore) ^ qualityPreferred;
        if ((filter && msaaLimit) || (!eglConfig && i == numConfig - 1)) {
            eglConfig = eglConfigs[i];
            depth = params[4];
            stencil = params[5];
            sampleBuffers = params[6];
            sampleCount = params[7];
            lastScore = currScore;
        }
    }

    CC_LOG_INFO("Setup EGLConfig: depth [%d] stencil [%d] sampleBuffer [%d] sampleCount [%d]", depth, stencil, sampleBuffers, sampleCount);

    EGL_CHECK(_extensions = StringUtil::split(eglQueryString(eglDisplay, EGL_EXTENSIONS), " "));

    bool hasKHRCreateCtx = checkExtension(CC_TOSTR(EGL_KHR_create_context));
    if (hasKHRCreateCtx) {
        eglAttributes.push_back(EGL_CONTEXT_MAJOR_VERSION_KHR);
        eglAttributes.push_back(2);
        eglAttributes.push_back(EGL_CONTEXT_MINOR_VERSION_KHR);
        eglAttributes.push_back(0);
#if CC_DEBUG > 0 && !FORCE_DISABLE_VALIDATION
        eglAttributes.push_back(EGL_CONTEXT_FLAGS_KHR);
        eglAttributes.push_back(EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR);
#endif
        eglAttributes.push_back(EGL_NONE);

        EGL_CHECK(eglDefaultContext = eglCreateContext(eglDisplay, eglConfig, nullptr, eglAttributes.data()));
    } else {
        eglAttributes.push_back(EGL_CONTEXT_CLIENT_VERSION);
        eglAttributes.push_back(2);
        eglAttributes.push_back(EGL_NONE);

        EGL_CHECK(eglDefaultContext = eglCreateContext(eglDisplay, eglConfig, nullptr, eglAttributes.data()));
    }

    if (!eglDefaultContext) {
        CC_LOG_ERROR("Create EGL context failed.");
        return false;
    }

    EGLint pbufferAttribs[]{
        EGL_WIDTH, 1,
        EGL_HEIGHT, 1,
        EGL_NONE};
    EGL_CHECK(eglDefaultSurface = eglCreatePbufferSurface(eglDisplay, eglConfig, pbufferAttribs));

    size_t threadID{std::hash<std::thread::id>{}(std::this_thread::get_id())};
    _sharedContexts[threadID] = eglDefaultContext;

    bindContext(true);

    return true;
}

void GLES2GPUContext::destroy() {
    if (eglDisplay) {
        makeCurrent(EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    }

    if (eglDefaultSurface) {
        EGL_CHECK(eglDestroySurface(eglDisplay, eglDefaultSurface));
        eglDefaultSurface = EGL_NO_SURFACE;
    }

    for (auto pair : _sharedContexts) {
        if (pair.second != eglDefaultContext) {
            EGL_CHECK(eglDestroyContext(eglDisplay, pair.second));
        }
    }
    _sharedContexts.clear();

    if (eglDefaultContext) {
        EGL_CHECK(eglDestroyContext(eglDisplay, eglDefaultContext));
        eglDefaultContext = EGL_NO_SURFACE;
    }

    if (eglDisplay) {
        EGL_CHECK(eglTerminate(eglDisplay));
        eglDisplay = EGL_NO_DISPLAY;
    }

    gles2wExit();
}

void GLES2GPUContext::bindContext(bool bound) {
    if (bound) {
        makeCurrent(eglDefaultSurface, eglDefaultSurface, eglDefaultContext);
        resetStates();
    } else {
        makeCurrent(EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT, false);
        _eglCurrentDrawSurface = EGL_NO_SURFACE;
        _eglCurrentReadSurface = EGL_NO_SURFACE;
    }
}

void GLES2GPUContext::makeCurrent(const GLES2GPUSwapchain *drawSwapchain, const GLES2GPUSwapchain *readSwapchain) {
    EGLSurface drawSurface = drawSwapchain ? drawSwapchain->eglSurface : _eglCurrentDrawSurface;
    EGLSurface readSurface = readSwapchain ? readSwapchain->eglSurface : _eglCurrentReadSurface;
    EGLContext prevContext = eglGetCurrentContext();

    if (_eglCurrentDrawSurface == drawSurface && _eglCurrentReadSurface == readSurface && _eglCurrentContext == prevContext) {
        return;
    }

    makeCurrent(drawSurface, readSurface, _eglCurrentContext);

    if (prevContext != _eglCurrentContext) {
        resetStates();
    }
}

void GLES2GPUContext::present(const GLES2GPUSwapchain *swapchain) {
#if CC_SWAPPY_ENABLED
    if (swapchain->swappyEnabled) {
        //fallback to normal eglswap if swappy_swap failed
        if (SwappyGL_swap(eglDisplay, swapchain->eglSurface)) {
            return;
        }
    }
#endif
    if (_eglCurrentInterval != swapchain->eglSwapInterval) {
        if (!eglSwapInterval(eglDisplay, swapchain->eglSwapInterval)) {
            CC_LOG_ERROR("eglSwapInterval() - FAILED.");
        }
        _eglCurrentInterval = swapchain->eglSwapInterval;
    }
    EGL_CHECK(eglSwapBuffers(eglDisplay, swapchain->eglSurface));
}

EGLContext GLES2GPUContext::getSharedContext() {
    size_t threadID{std::hash<std::thread::id>{}(std::this_thread::get_id())};
    if (_sharedContexts.count(threadID)) return _sharedContexts[threadID];

    EGLContext context = EGL_NO_CONTEXT;
    EGL_CHECK(context = eglCreateContext(eglDisplay, eglConfig, eglDefaultContext, eglAttributes.data()));

    if (!context) {
        CC_LOG_ERROR("Create shared context failed.");
        return EGL_NO_CONTEXT;
    }

    _sharedContexts[threadID] = context;
    return context;
}

bool GLES2GPUContext::makeCurrent(EGLSurface drawSurface, EGLSurface readSurface, EGLContext context, bool updateCache) {
    bool succeeded;
    EGL_CHECK(succeeded = eglMakeCurrent(eglDisplay, drawSurface, readSurface, context));
    if (succeeded && updateCache) {
        _eglCurrentDrawSurface = drawSurface;
        _eglCurrentReadSurface = readSurface;
        _eglCurrentContext = context;
    }
    return succeeded;
}

void GLES2GPUContext::resetStates() const {
    GL_CHECK(glPixelStorei(GL_PACK_ALIGNMENT, 1));
    GL_CHECK(glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
    GL_CHECK(glActiveTexture(GL_TEXTURE0));

    //////////////////////////////////////////////////////////////////////////

    GL_CHECK(glEnable(GL_SCISSOR_TEST));
    GL_CHECK(glEnable(GL_CULL_FACE));
    GL_CHECK(glCullFace(GL_BACK));

    GL_CHECK(glFrontFace(GL_CCW));

    GL_CHECK(glDisable(GL_SAMPLE_COVERAGE));

    //////////////////////////////////////////////////////////////////////////

    GL_CHECK(glEnable(GL_DEPTH_TEST));
    GL_CHECK(glDepthMask(GL_TRUE));
    GL_CHECK(glDepthFunc(GL_LESS));

    GL_CHECK(glStencilFuncSeparate(GL_FRONT, GL_ALWAYS, 1, 0xffffffff));
    GL_CHECK(glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_KEEP));
    GL_CHECK(glStencilMaskSeparate(GL_FRONT, 0xffffffff));
    GL_CHECK(glStencilFuncSeparate(GL_BACK, GL_ALWAYS, 1, 0xffffffff));
    GL_CHECK(glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_KEEP));
    GL_CHECK(glStencilMaskSeparate(GL_BACK, 0xffffffff));

    GL_CHECK(glDisable(GL_STENCIL_TEST));

    //////////////////////////////////////////////////////////////////////////

    GL_CHECK(glDisable(GL_SAMPLE_ALPHA_TO_COVERAGE));
    GL_CHECK(glDisable(GL_BLEND));
    GL_CHECK(glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD));
    GL_CHECK(glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO));
    GL_CHECK(glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE));
    GL_CHECK(glBlendColor(0.0F, 0.0F, 0.0F, 0.0F));

    GL_CHECK(glUseProgram(0));

    if (_constantRegistry->useVAO) {
        GL_CHECK(glBindVertexArrayOES(0));
    }

    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, 0));
    GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));

    GL_CHECK(glBindTexture(GL_TEXTURE_2D, 0));
    GL_CHECK(glBindTexture(GL_TEXTURE_CUBE_MAP, 0));

    GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER, 0));

#if CC_DEBUG > 0 && !FORCE_DISABLE_VALIDATION
    GL_CHECK(glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR));
    if (glDebugMessageControlKHR) {
        GL_CHECK(glDebugMessageControlKHR(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, GL_TRUE));
    }
    if (glDebugMessageCallbackKHR) {
        GL_CHECK(glDebugMessageCallbackKHR(GLES2EGLDebugProc, NULL));
    }
#endif

    if (_constantRegistry->mFBF == FBFSupportLevel::NON_COHERENT_QCOM) {
        GL_CHECK(glEnable(GL_FRAMEBUFFER_FETCH_NONCOHERENT_QCOM));
    }

    _stateCache->reset();

    _constantRegistry->currentBoundThreadID = std::hash<std::thread::id>{}(std::this_thread::get_id());

    CC_LOG_DEBUG("EGL context bounded to thread %llx", _constantRegistry->currentBoundThreadID);
}

} // namespace gfx
} // namespace cc
