/*
 * This file is part of libplacebo.
 *
 * libplacebo is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * libplacebo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with libplacebo.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <ctype.h>

#include "common.h"
#include "utils.h"
#include "gpu.h"

const struct pl_opengl_params pl_opengl_default_params = {0};

static void GLAPIENTRY debug_cb(GLenum source, GLenum type, GLuint id,
                                GLenum severity, GLsizei length,
                                const GLchar *message, const void *userParam)
{
    pl_log log = (void *) userParam;
    enum pl_log_level level = PL_LOG_ERR;

    switch (severity) {
    case GL_DEBUG_SEVERITY_NOTIFICATION:level = PL_LOG_DEBUG; break;
    case GL_DEBUG_SEVERITY_LOW:         level = PL_LOG_INFO; break;
    case GL_DEBUG_SEVERITY_MEDIUM:      level = PL_LOG_WARN; break;
    case GL_DEBUG_SEVERITY_HIGH:        level = PL_LOG_ERR; break;
    }

    pl_msg(log, level, "GL: %s", message);

    if (level <= PL_LOG_ERR)
        pl_log_stack_trace(log, level);
}

static void GLAPIENTRY debug_cb_egl(EGLenum error, const char *command,
                                    EGLint messageType, EGLLabelKHR threadLabel,
                                    EGLLabelKHR objectLabel, const char *message)
{
    pl_log log = threadLabel;
    enum pl_log_level level = PL_LOG_ERR;

    switch (messageType) {
    case EGL_DEBUG_MSG_CRITICAL_KHR:    level = PL_LOG_FATAL; break;
    case EGL_DEBUG_MSG_ERROR_KHR:       level = PL_LOG_ERR; break;
    case EGL_DEBUG_MSG_WARN_KHR:        level = PL_LOG_WARN; break;
    case EGL_DEBUG_MSG_INFO_KHR:        level = PL_LOG_DEBUG; break;
    }

    pl_msg(log, level, "EGL: %s: %s %s", command, egl_err_str(error),
           message);

    if (level <= PL_LOG_ERR)
        pl_log_stack_trace(log, level);
}

// Guards access to the (thread-unsafe) glad global EGL state
static pl_static_mutex glad_egl_mutex = PL_STATIC_MUTEX_INITIALIZER;

void pl_opengl_destroy(pl_opengl *ptr)
{
    pl_opengl pl_gl = *ptr;
    if (!pl_gl)
        return;

    struct gl_ctx *p = PL_PRIV(pl_gl);
    gl_funcs *gl = &p->func;
    if (!gl_make_current(pl_gl)) {
        PL_WARN(p, "Failed uninitializing OpenGL context, leaking resources!");
        return;
    }

    if (p->is_debug)
        gl->DebugMessageCallback(NULL, NULL);

    if (p->is_debug_egl)
        eglDebugMessageControlKHR(NULL, NULL);

    pl_gpu_destroy(pl_gl->gpu);

#ifdef PL_HAVE_GL_PROC_ADDR
    if (p->is_gles) {
        gladLoaderUnloadGLES2Context(gl);
    } else {
        gladLoaderUnloadGLContext(gl);
    }

    bool used_loader = !p->params.get_proc_addr && !p->params.get_proc_addr_ex;
    if (p->params.egl_display && used_loader) {
        pl_static_mutex_lock(&glad_egl_mutex);
        gladLoaderUnloadEGL();
        pl_static_mutex_unlock(&glad_egl_mutex);
    }
#endif

    gl_release_current(pl_gl);
    pl_mutex_destroy(&p->lock);
    pl_free_ptr((void **) ptr);

}

typedef PL_ARRAY(const char *) ext_arr_t;
static void add_exts_str(void *alloc, ext_arr_t *arr, const char *extstr)
{
    pl_str rest = pl_str_strip(pl_str0(pl_strdup0(alloc, pl_str0(extstr))));
    while (rest.len) {
        pl_str ext = pl_str_split_char(rest, ' ', &rest);
        ext.buf[ext.len] = '\0'; // re-use separator for terminator
        PL_ARRAY_APPEND(alloc, *arr, (char *) ext.buf);
    }
}

pl_opengl pl_opengl_create(pl_log log, const struct pl_opengl_params *params)
{
    params = PL_DEF(params, &pl_opengl_default_params);
    struct pl_opengl_t *pl_gl = pl_zalloc_obj(NULL, pl_gl, struct gl_ctx);
    struct gl_ctx *p = PL_PRIV(pl_gl);
    gl_funcs *gl = &p->func;
    p->params = *params;
    p->log = log;

    pl_mutex_init_type(&p->lock, PL_MUTEX_RECURSIVE);
    if (!gl_make_current(pl_gl)) {
        pl_free(pl_gl);
        return NULL;
    }

    bool ok;
    if (params->get_proc_addr_ex) {
        ok = gladLoadGLContextUserPtr(gl, params->get_proc_addr_ex, params->proc_ctx);
    } else if (params->get_proc_addr) {
        ok = gladLoadGLContext(gl, params->get_proc_addr);
    } else {
#ifdef PL_HAVE_GL_PROC_ADDR
        ok = gladLoaderLoadGLContext(gl);
#else
        PL_FATAL(p, "No `glGetProcAddress` function provided, and libplacebo "
                 "built without its built-in OpenGL loader!");
        goto error;
#endif
    }

    if (!ok) {
        PL_INFO(p, "Failed loading core GL, retrying as GLES...");
    } else if (gl_is_gles(pl_gl)) {
        PL_INFO(p, "GL context seems to be OpenGL ES, reloading as GLES...");
        ok = false;
    }

    if (!ok) {
        memset(gl, 0, sizeof(*gl));
        if (params->get_proc_addr_ex) {
            ok = gladLoadGLES2ContextUserPtr(gl, params->get_proc_addr_ex, params->proc_ctx);
        } else if (params->get_proc_addr) {
            ok = gladLoadGLES2Context(gl, params->get_proc_addr);
        } else {
#ifdef PL_HAVE_GL_PROC_ADDR
            ok = gladLoaderLoadGLES2Context(gl);
#else
            pl_unreachable();
#endif
        }
        p->is_gles = ok;
    }

    if (!ok) {
        PL_FATAL(p, "Failed to initialize OpenGL context - make sure a valid "
                 "OpenGL context is bound to the current thread!");
        goto error;
    }

    const char *version = (const char *) gl->GetString(GL_VERSION);
    if (version) {
        const char *ver = version;
        while (!isdigit(*ver) && *ver != '\0')
            ver++;
        if (sscanf(ver, "%d.%d", &pl_gl->major, &pl_gl->minor) != 2) {
            PL_FATAL(p, "Invalid GL_VERSION string: %s\n", version);
            goto error;
        }
    }

    if (!pl_gl->major) {
        PL_FATAL(p, "No OpenGL version detected - make sure an OpenGL context "
                 "is bound to the current thread!");
        goto error;
    }

    static const int gl_ver_req = 3;
    if (pl_gl->major < gl_ver_req) {
        PL_FATAL(p, "OpenGL version too old (%d < %d), please use a newer "
                 "OpenGL implementation or downgrade libplacebo!",
                 pl_gl->major, gl_ver_req);
        goto error;
    }

    PL_INFO(p, "Detected OpenGL version strings:");
    PL_INFO(p, "    GL_VERSION:  %s", version);
    PL_INFO(p, "    GL_VENDOR:   %s", (char *) gl->GetString(GL_VENDOR));
    PL_INFO(p, "    GL_RENDERER: %s", (char *) gl->GetString(GL_RENDERER));

    ext_arr_t exts = {0};
    if (pl_gl->major >= 3) {
        gl->GetIntegerv(GL_NUM_EXTENSIONS, &exts.num);
        PL_ARRAY_RESIZE(pl_gl, exts, exts.num);
        for (int i = 0; i < exts.num; i++)
            exts.elem[i] = (const char *) gl->GetStringi(GL_EXTENSIONS, i);
    } else {
        add_exts_str(pl_gl, &exts, (const char *) gl->GetString(GL_EXTENSIONS));
    }

    if (pl_msg_test(log, PL_LOG_DEBUG)) {
        PL_DEBUG(p, "    GL_EXTENSIONS:");
        for (int i = 0; i < exts.num; i++)
            PL_DEBUG(p, "        %s", exts.elem[i]);
    }

    if (params->egl_display) {
        pl_static_mutex_lock(&glad_egl_mutex);
        if (params->get_proc_addr_ex) {
            ok = gladLoadEGLUserPtr(params->egl_display, params->get_proc_addr_ex,
                                    params->proc_ctx);
        } else if (params->get_proc_addr) {
            ok = gladLoadEGL(params->egl_display, params->get_proc_addr);
        } else {
#ifdef PL_HAVE_GL_PROC_ADDR
            ok = gladLoaderLoadEGL(params->egl_display);
#else
            pl_unreachable();
#endif
        }
        pl_static_mutex_unlock(&glad_egl_mutex);

        if (!ok) {
            PL_FATAL(p, "Failed loading EGL functions - double check EGLDisplay?");
            goto error;
        }

        int start = exts.num;
        add_exts_str(pl_gl, &exts, eglQueryString(params->egl_display,
                                                  EGL_EXTENSIONS));
        if (exts.num > start) {
            PL_DEBUG(p, "    EGL_EXTENSIONS:");
            for (int i = start; i < exts.num; i++)
                PL_DEBUG(p, "        %s", exts.elem[i]);
        }
    }

    pl_gl->extensions = exts.elem;
    pl_gl->num_extensions = exts.num;

    if (!params->allow_software && gl_is_software(pl_gl)) {
        PL_FATAL(p, "OpenGL context is suspected to be a software rasterizer, "
                 "but `allow_software` is false.");
        goto error;
    }

    if (params->debug) {
        if (pl_opengl_has_ext(pl_gl, "GL_KHR_debug")) {
            gl->DebugMessageCallback(debug_cb, log);
            gl->Enable(GL_DEBUG_OUTPUT);
            p->is_debug = true;
        } else {
            PL_WARN(p, "OpenGL debugging requested, but GL_KHR_debug is not "
                    "available... ignoring!");
        }

        if (params->egl_display && pl_opengl_has_ext(pl_gl, "EGL_KHR_debug")) {
            static const EGLAttrib attribs[] = {
                // Enable everything under the sun, because the `pl_ctx` log
                // level may change at runtime.
                EGL_DEBUG_MSG_CRITICAL_KHR, EGL_TRUE,
                EGL_DEBUG_MSG_ERROR_KHR,    EGL_TRUE,
                EGL_DEBUG_MSG_WARN_KHR,     EGL_TRUE,
                EGL_DEBUG_MSG_INFO_KHR,     EGL_TRUE,
                EGL_NONE,
            };

            eglDebugMessageControlKHR(debug_cb_egl, attribs);
            eglLabelObjectKHR(NULL, EGL_OBJECT_THREAD_KHR, NULL, (void *) log);
            p->is_debug_egl = true;
        }
    }

    pl_gl->gpu = pl_gpu_create_gl(log, pl_gl, params);
    if (!pl_gl->gpu)
        goto error;

    gl_release_current(pl_gl);
    return pl_gl;

error:
    PL_FATAL(p, "Failed initializing opengl context!");
    gl_release_current(pl_gl);
    pl_opengl_destroy((pl_opengl *) &pl_gl);
    return NULL;
}

bool gl_make_current(pl_opengl pl_gl)
{
    struct gl_ctx *p = PL_PRIV(pl_gl);
    pl_mutex_lock(&p->lock);
    if (!p->count && p->params.make_current) {
        if (!p->params.make_current(p->params.priv)) {
            PL_ERR(p, "Failed making OpenGL context current on calling thread!");
            pl_mutex_unlock(&p->lock);
            return false;
        }
    }

    p->count++;
    return true;
}

void gl_release_current(pl_opengl pl_gl)
{
    struct gl_ctx *p = PL_PRIV(pl_gl);
    p->count--;
    if (!p->count && p->params.release_current)
        p->params.release_current(p->params.priv);
    pl_mutex_unlock(&p->lock);
}
