/**
 * @file egl.c
 * @brief EGL OpenGL extension module
 */
/*****************************************************************************
 * Copyright © 2010-2014 Rémi Denis-Courmont
 *
 * This program 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.
 *
 * This program 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 this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdlib.h>
#include <assert.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>

#include <vlc_common.h>
#include <vlc_threads.h>
#include <vlc_plugin.h>
#include <vlc_opengl.h>
#include <vlc_window.h>
#ifdef USE_PLATFORM_X11
# include <vlc_xlib.h>
# include <X11/Xutil.h>
#endif
#ifdef USE_PLATFORM_XCB
# include <xcb/xcb.h>
# include "../xcb/events.h"
#endif
#ifdef USE_PLATFORM_WAYLAND
# include <wayland-egl.h>
#endif
#if defined (USE_PLATFORM_ANDROID)
# include "../android/utils.h"
#endif

static const char *clientExts;
#ifdef EGL_EXT_platform_base
static PFNEGLGETPLATFORMDISPLAYEXTPROC getPlatformDisplayEXT;
static PFNEGLCREATEPLATFORMWINDOWSURFACEEXTPROC createPlatformWindowSurfaceEXT;
#endif

typedef struct vlc_gl_sys_t
{
    EGLDisplay display;
    EGLSurface surface;
    EGLContext context;
#if defined (USE_PLATFORM_X11)
    Display *x11;
    unsigned long x11_black_pixel;
    Window x11_win;
#endif
#if defined (USE_PLATFORM_XCB)
    xcb_connection_t *conn;
    uint32_t xcb_black_pixel;
    xcb_window_t xcb_win;
#endif
#if defined (USE_PLATFORM_WAYLAND)
    struct wl_egl_window *window;
#endif
    bool is_current;
} vlc_gl_sys_t;

static bool CheckAPI(EGLDisplay dpy, const char *api)
{
    const char *apis = eglQueryString(dpy, EGL_CLIENT_APIS);
    return vlc_gl_StrHasToken(apis, api);
}

static bool CheckClientExt(const char *name)
{
    return vlc_gl_StrHasToken(clientExts, name);
}

static bool CheckDisplayExt(EGLDisplay display, const char *name)
{
    const char *exts = eglQueryString(display, EGL_EXTENSIONS);
    return vlc_gl_StrHasToken(exts, name);
}

static void LogDeviceInfo(vlc_gl_t *gl,
                          PFNEGLQUERYDEVICESTRINGEXTPROC eglQueryDeviceStringEXT,
                          EGLDeviceEXT device)
{
    const char *device_ext = eglQueryDeviceStringEXT(device, EGL_EXTENSIONS);
    if (device_ext == NULL)
        device_ext = "";

    /* EGL_EXT_device_query_name is a device extension, so it must be tested
     * against the device's own extension string, not the client one. */
    if (vlc_gl_StrHasToken(device_ext, "EGL_EXT_device_query_name"))
    {
        const char *vendor = eglQueryDeviceStringEXT(device, EGL_VENDOR);
        const char *renderer = eglQueryDeviceStringEXT(device, EGL_RENDERER_EXT);
        msg_Dbg(gl, "   vendor='%s' renderer='%s'",
                vendor ? vendor : "unknown", renderer ? renderer : "unknown");
    }

    msg_Dbg(gl, "   extensions: %s", device_ext);

    if (vlc_gl_StrHasToken(device_ext, "EGL_EXT_device_drm"))
    {
        const char *path = eglQueryDeviceStringEXT(device, EGL_DRM_DEVICE_FILE_EXT);
        if (path != NULL)
            msg_Dbg(gl, "   drm device: %s", path);
    }
    if (vlc_gl_StrHasToken(device_ext, "EGL_EXT_device_drm_render_node"))
    {
        const char *path = eglQueryDeviceStringEXT(device, EGL_DRM_RENDER_NODE_FILE_EXT);
        if (path != NULL)
            msg_Dbg(gl, "   drm render node: %s", path);
    }
}

static void DisplayAvailableDevices(vlc_gl_t *gl)
{
    PFNEGLQUERYDEVICESEXTPROC eglQueryDevicesEXT =
        (PFNEGLQUERYDEVICESEXTPROC)eglGetProcAddress("eglQueryDevicesEXT");
    PFNEGLQUERYDEVICESTRINGEXTPROC eglQueryDeviceStringEXT =
        (PFNEGLQUERYDEVICESTRINGEXTPROC)eglGetProcAddress("eglQueryDeviceStringEXT");

    EGLint device_count = 0;
    if (eglQueryDevicesEXT(0, NULL, &device_count) != EGL_TRUE
     || device_count <= 0)
    {
        msg_Dbg(gl, "could not enumerate EGL devices");
        return;
    }

    EGLDeviceEXT *devices = vlc_alloc(device_count, sizeof(*devices));
    if (devices == NULL)
        return;

    if (eglQueryDevicesEXT(device_count, devices, &device_count) != EGL_TRUE)
    {
        free(devices);
        return;
    }

    msg_Dbg(gl, "EGL has %d devices:", device_count);
    for (EGLint i = 0; i < device_count; ++i)
    {
        msg_Dbg(gl, " - device %d:", i);
        LogDeviceInfo(gl, eglQueryDeviceStringEXT, devices[i]);
    }

    free(devices);
}

struct gl_api
{
   const char name[10];
   EGLenum    api;
   EGLint     min_minor;
   EGLint     render_bit;
   EGLint     attr[3];
};

static int MakeCurrent (vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    if (eglMakeCurrent (sys->display, sys->surface, sys->surface,
                        sys->context) != EGL_TRUE)
        return VLC_EGENERIC;
    sys->is_current = true;
    return VLC_SUCCESS;
}

static void ReleaseCurrent (vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    eglMakeCurrent (sys->display, EGL_NO_SURFACE, EGL_NO_SURFACE,
                    EGL_NO_CONTEXT);
    sys->is_current = false;
}

#ifdef USE_PLATFORM_WAYLAND
static void Resize (vlc_gl_t *gl, unsigned width, unsigned height)
{
    vlc_gl_sys_t *sys = gl->sys;

    wl_egl_window_resize(sys->window, width, height, 0, 0);
}

static void DestroySurface(vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    wl_egl_window_destroy(sys->window);
}

static EGLSurface CreateSurface(vlc_gl_t *gl, EGLDisplay dpy, EGLConfig config,
                                unsigned int width, unsigned int height)
{
    vlc_gl_sys_t *sys = gl->sys;
    EGLSurface surface;

    sys->window = wl_egl_window_create(gl->surface->handle.wl, width, height);
    if (sys->window == NULL)
        return EGL_NO_SURFACE;

# if defined(EGL_VERSION_1_5)
    assert(CheckClientExt("EGL_KHR_platform_wayland"));
    surface = eglCreatePlatformWindowSurface(dpy, config, sys->window, NULL);

# elif defined(EGL_EXT_platform_base)
    assert(CheckClientExt("EGL_EXT_platform_wayland"));
    surface = createPlatformWindowSurfaceEXT(dpy, config, sys->window, NULL);

# else
    surface = EGL_NO_SURFACE;
# endif

    if (surface == EGL_NO_SURFACE)
        wl_egl_window_destroy(sys->window);

    return surface;
}

static void ReleaseDisplay(vlc_gl_t *gl)
{
    (void) gl;
}

static EGLDisplay OpenDisplay(vlc_gl_t *gl)
{
    vlc_window_t *surface = gl->surface;

    if (surface->type != VLC_WINDOW_TYPE_WAYLAND)
        return EGL_NO_DISPLAY;
    if (!CheckClientExt("EGL_KHR_display_reference")) {
        msg_Warn(gl, "EGL display reference counting not supported");
        return EGL_NO_DISPLAY;
    }

# ifdef EGL_KHR_display_reference
    static const EGLAttrib attrs[] = {
        EGL_TRACK_REFERENCES_KHR, EGL_TRUE,
        EGL_NONE
    };

#  if defined(EGL_VERSION_1_5)
#   ifdef EGL_KHR_platform_wayland
    if (CheckClientExt("EGL_KHR_platform_wayland"))
        return eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR,
                                     surface->display.wl, attrs);
#   endif

#  elif defined(EGL_EXT_platform_wayland)
    if (CheckClientExt("EGL_EXT_platform_wayland"))
        return getPlatformDisplayEXT(EGL_PLATFORM_WAYLAND_EXT,
                                     surface->display.wl, attrs);
#  endif
# endif
    return EGL_NO_DISPLAY;
}

#elif defined(USE_PLATFORM_X11)
static void Resize (vlc_gl_t *gl, unsigned width, unsigned height)
{
    vlc_gl_sys_t *sys = gl->sys;
    EGLint val;

    EGLDisplay old_display = eglGetCurrentDisplay();
    EGLSurface old_draw = eglGetCurrentSurface(EGL_DRAW);
    EGLSurface old_read = eglGetCurrentSurface(EGL_READ);
    EGLContext old_ctx = eglGetCurrentContext();

    bool made_current = eglMakeCurrent(sys->display, sys->surface, sys->surface, sys->context);
    if (made_current)
        eglWaitClient();

    unsigned long init_serial = LastKnownRequestProcessed(sys->x11);
    unsigned long resize_serial = NextRequest(sys->x11);
    XResizeWindow(sys->x11, sys->x11_win, width, height);

    if (made_current)
    {
        eglQuerySurface(sys->display, sys->surface, EGL_HEIGHT, &val); /* force Mesa to see new size in time for next draw */
        eglWaitNative(EGL_CORE_NATIVE_ENGINE);
        if (old_display != EGL_NO_DISPLAY)
            eglMakeCurrent(old_display, old_draw, old_read, old_ctx);
        else
            eglMakeCurrent(sys->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    }
    if (LastKnownRequestProcessed(sys->x11) - init_serial < resize_serial - init_serial)
        XSync(sys->x11, False);
}

static void DestroySurface(vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    XUnmapWindow(sys->x11, sys->x11_win);
}

static EGLSurface CreateSurface(vlc_gl_t *gl, EGLDisplay dpy, EGLConfig config,
                                unsigned int width, unsigned int height)
{
    vlc_gl_sys_t *sys = gl->sys;

    EGLint val;
    if (eglGetConfigAttrib(dpy, config, EGL_NATIVE_VISUAL_ID, &val) == EGL_FALSE)
        return EGL_NO_SURFACE;
    XVisualInfo vinfo_template;
    int vinfoc;
    vinfo_template.visualid = (VisualID) val;
    XVisualInfo *vinfov = XGetVisualInfo(sys->x11, VisualIDMask, &vinfo_template, &vinfoc);
    if (vinfov == NULL || vinfoc == 0)
        return EGL_NO_SURFACE;
    Visual *visual = vinfov[0].visual;
    int depth = vinfov[0].depth;
    XFree(vinfov);

    unsigned long mask =
        CWBackPixel | CWBorderPixel | CWBitGravity | CWColormap;
    XSetWindowAttributes swa = {
        .background_pixel = sys->x11_black_pixel,
        .border_pixel = sys->x11_black_pixel,
        .bit_gravity = NorthWestGravity,
        .colormap = XCreateColormap(sys->x11, gl->surface->handle.xid, visual,
                                    AllocNone),
    };
    Window win = XCreateWindow(sys->x11, gl->surface->handle.xid, 0, 0, width,
                               height, 0, depth, InputOutput, visual, mask,
                               &swa);
    sys->x11_win = win;
    XMapWindow(sys->x11, win);

# if defined(EGL_VERSION_1_5)
    if (CheckClientExt("EGL_KHR_platform_x11"))
        return eglCreatePlatformWindowSurface(dpy, config, &win, NULL);

# elif defined(EGL_EXT_platform_base)
    if (CheckClientExt("EGL_EXT_platform_x11"))
        return createPlatformWindowSurfaceEXT(dpy, config, &win, NULL);

# endif
    return eglCreateWindowSurface(dpy, config, win, NULL);
}

static void ReleaseDisplay(vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    XCloseDisplay(sys->x11);
}

static EGLDisplay OpenDisplay(vlc_gl_t *gl)
{
    vlc_window_t *surface = gl->surface;
    vlc_gl_sys_t *sys = gl->sys;

    if (surface->type != VLC_WINDOW_TYPE_XID || !vlc_xlib_init(VLC_OBJECT(gl)))
        return EGL_NO_DISPLAY;

    Display *x11 = XOpenDisplay(surface->display.x11);
    if (x11 == NULL)
        return EGL_NO_DISPLAY;

    XWindowAttributes wa;

    if (!XGetWindowAttributes(x11, surface->handle.xid, &wa))
        goto error;

    EGLDisplay display;
    int snum = XScreenNumberOfScreen(wa.screen);

# if defined(EGL_VERSION_1_5)
#  ifdef EGL_KHR_paltform_x11
    if (CheckClientExt("EGL_KHR_platform_x11")) {
        const EGLAttrib attrs[] = {
            EGL_PLATFORM_X11_SCREEN_KHR, snum,
            EGL_NONE
        };

        display = getPlatformDisplayEXT(EGL_PLATFORM_X11_KHR, x11, attrs);
    } else
#  endif
# elif defined(EGL_EXT_platform_x11)
    if (CheckClientExt("EGL_EXT_platform_x11")) {
        const EGLint attrs[] = {
            EGL_PLATFORM_X11_SCREEN_EXT, snum,
            EGL_NONE
        };

        display = getPlatformDisplayEXT(EGL_PLATFORM_X11_EXT, x11, attrs);
    } else
# endif
# ifdef __unix__
    if (snum == XDefaultScreen(x11))
        display = eglGetDisplay(x11);
    else
# endif
        display = EGL_NO_DISPLAY;

    if (display == EGL_NO_DISPLAY)
        goto error;

    sys->x11 = x11;
    sys->x11_black_pixel = BlackPixelOfScreen(wa.screen);
    return display;
error:
    XCloseDisplay(x11);
    return EGL_NO_DISPLAY;
}

#elif defined(USE_PLATFORM_XCB)
static int GetScreenNum(xcb_connection_t *conn, const xcb_screen_t *scr)
{
    xcb_screen_iterator_t i = xcb_setup_roots_iterator(xcb_get_setup(conn));
    int n = 0;

    while (scr != i.data) {
         xcb_screen_next(&i);
         n++;
    }
    return n;
}

static void Resize (vlc_gl_t *gl, unsigned width, unsigned height)
{
    vlc_gl_sys_t *sys = gl->sys;
    EGLint val;

    EGLDisplay old_display = eglGetCurrentDisplay();
    EGLSurface old_draw = eglGetCurrentSurface(EGL_DRAW);
    EGLSurface old_read = eglGetCurrentSurface(EGL_READ);
    EGLContext old_ctx = eglGetCurrentContext();

    bool made_current = eglMakeCurrent(sys->display, sys->surface, sys->surface, sys->context);
    if (made_current)
        eglWaitClient();

    uint16_t mask = XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT;
    const uint32_t values[] = { width, height };
    xcb_void_cookie_t cookie = xcb_configure_window_checked(sys->conn, sys->xcb_win, mask, values);

    if (made_current)
    {
        eglQuerySurface(sys->display, sys->surface, EGL_HEIGHT, &val); /* force Mesa to see new size in time for next draw */
        eglWaitNative(EGL_CORE_NATIVE_ENGINE);
        if (old_display != EGL_NO_DISPLAY)
            eglMakeCurrent(old_display, old_draw, old_read, old_ctx);
        else
            eglMakeCurrent(sys->display, NULL, NULL, NULL);
    }
    free(xcb_request_check(sys->conn, cookie));
}

static void DestroySurface(vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    xcb_unmap_window(sys->conn, sys->xcb_win);
}

static uint8_t VisualIdToDepth(xcb_connection_t *conn, xcb_visualid_t vid)
{
    xcb_screen_iterator_t si = xcb_setup_roots_iterator (xcb_get_setup (conn));
    for (; si.rem; xcb_screen_next (&si)) {
        xcb_depth_iterator_t di = xcb_screen_allowed_depths_iterator (si.data);
        for (; di.rem; xcb_depth_next (&di)) {
            xcb_visualtype_iterator_t vi = xcb_depth_visuals_iterator (di.data);
            for (; vi.rem; xcb_visualtype_next (&vi)) {
                if (vid == vi.data->visual_id) {
                    return di.data->depth;
                }
            }
        }
    }
    return 0;
}

static EGLSurface CreateSurface(vlc_gl_t *gl, EGLDisplay dpy, EGLConfig config,
                                unsigned int width, unsigned int height)
{
# ifdef EGL_EXT_platform_base
    vlc_gl_sys_t *sys = gl->sys;
    xcb_connection_t *conn = sys->conn;

    EGLint val;
    if (eglGetConfigAttrib(dpy, config, EGL_NATIVE_VISUAL_ID, &val) == EGL_FALSE)
        return EGL_NO_SURFACE;
    xcb_visualid_t vid = (xcb_visualid_t) val;
    xcb_colormap_t cmap = xcb_generate_id(conn);
    xcb_create_colormap(conn, XCB_COLORMAP_ALLOC_NONE,
                        cmap, gl->surface->handle.xid, vid);
    uint8_t depth = VisualIdToDepth(conn, vid);
    if (depth == 0)
        return EGL_NO_SURFACE;

    xcb_window_t win = xcb_generate_id(conn);
    uint32_t mask =
        XCB_CW_BACK_PIXEL |
        XCB_CW_BORDER_PIXEL |
        XCB_CW_BIT_GRAVITY |
        XCB_CW_COLORMAP;
    const uint32_t values[] = {
        /* XCB_CW_BACK_PIXEL */
        sys->xcb_black_pixel,
        /* XCB_CW_BORDER_PIXEL */
        sys->xcb_black_pixel,
        /* XCB_CW_BIT_GRAVITY */
        XCB_GRAVITY_NORTH_WEST,
        /* XCB_CW_COLORMAP */
        cmap,
    };
    xcb_create_window(conn, depth, win, gl->surface->handle.xid, 0, 0,
                      width, height, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, vid,
                      mask, values);
    xcb_map_window(conn, win);
    sys->xcb_win = win;

    assert(CheckClientExt("EGL_EXT_platform_xcb"));
    return createPlatformWindowSurfaceEXT(dpy, config, &win, NULL);
# else
    return EGL_NO_SURFACE;
# endif
}

static void ReleaseDisplay(vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    xcb_disconnect(sys->conn);
}

static EGLDisplay OpenDisplay(vlc_gl_t *gl)
{
# ifdef EGL_EXT_platform_xcb
    vlc_window_t *surface = gl->surface;
    vlc_gl_sys_t *sys = gl->sys;
    xcb_connection_t *conn;
    const xcb_screen_t *scr;

    if (surface->type != VLC_WINDOW_TYPE_XID)
        return EGL_NO_DISPLAY;
    if (!CheckClientExt("EGL_EXT_platform_xcb"))
        return EGL_NO_DISPLAY;
    if (vlc_xcb_parent_Create(gl->obj.logger, surface, &conn,
                              &scr) != VLC_SUCCESS)
        return EGL_NO_DISPLAY;

    const EGLint attrs[] = {
        EGL_PLATFORM_XCB_SCREEN_EXT, GetScreenNum(conn, scr),
        EGL_NONE
    };

    EGLDisplay display = getPlatformDisplayEXT(EGL_PLATFORM_XCB_EXT, conn,
                                               attrs);
    if (display == EGL_NO_DISPLAY) {
        xcb_disconnect(conn);
        goto out;
    }

    sys->conn = conn;
    sys->xcb_black_pixel = scr->black_pixel;
out:
    return display;
# else
    return EGL_NO_DISPLAY;
# endif
}

#elif defined (USE_PLATFORM_ANDROID)
# define Resize (NULL)

static void DestroySurface(vlc_gl_t *gl)
{
    AWindowHandler_releaseANativeWindow(gl->surface->display.anativewindow,
                                        gl->surface->handle.android_id);
}

static EGLSurface CreateSurface(vlc_gl_t *gl, EGLDisplay dpy, EGLConfig config,
                                unsigned int width, unsigned int height)
{
    ANativeWindow *anw =
        AWindowHandler_getANativeWindow(gl->surface->display.anativewindow,
                                        gl->surface->handle.android_id);

    if (anw == NULL)
        return EGL_NO_SURFACE;

    int awh_caps = AWindowHandler_getCapabilities(gl->surface->display.anativewindow);
    if ((awh_caps & AWH_CAPS_SURFACE_VIEW) == 0)
    {
        ANativeWindow_setBuffersGeometry(anw, width, height, AHARDWAREBUFFER_FORMAT_BLOB);
    }

    return eglCreateWindowSurface(dpy, config, anw, NULL);
}

static void ReleaseDisplay(vlc_gl_t *gl)
{
    (void) gl;
}

static EGLDisplay OpenDisplay(vlc_gl_t *gl)
{
# if defined (__ANDROID__) || defined (ANDROID)
    if (gl->surface->type == VLC_WINDOW_TYPE_ANDROID_NATIVE)
        return eglGetDisplay(EGL_DEFAULT_DISPLAY);
# endif
    return EGL_NO_DISPLAY;
}

#else
# define Resize (NULL)

static void DestroySurface(vlc_gl_t *gl)
{
    (void) gl;
}

static EGLSurface CreateSurface(vlc_gl_t *gl, EGLDisplay dpy, EGLConfig config,
                                unsigned int width, unsigned int height)
{
    HWND window = gl->surface->handle.hwnd;

    (void) width; (void) height;
    return eglCreateWindowSurface(dpy, config, window, NULL);
}

static void ReleaseDisplay(vlc_gl_t *gl)
{
    (void) gl;
}

static EGLDisplay OpenDisplay(vlc_gl_t *gl)
{
# if defined (_WIN32) || defined (__VC32__) \
  && !defined (__CYGWIN__) && !defined (__SCITECH_SNAP__)
    if (gl->surface->type == VLC_WINDOW_TYPE_HWND)
        return eglGetDisplay(EGL_DEFAULT_DISPLAY);
# endif
    return EGL_NO_DISPLAY;
}
#endif

static void SwapBuffers (vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    if (!sys->is_current)
    {
        EGLDisplay previous_display = eglGetCurrentDisplay();
        EGLSurface s_read = eglGetCurrentSurface(EGL_READ);
        EGLSurface s_draw = eglGetCurrentSurface(EGL_DRAW);
        EGLContext previous_context = eglGetCurrentContext();

        eglMakeCurrent(sys->display, sys->surface, sys->surface, sys->context);
        eglSwapBuffers (sys->display, sys->surface);
        if (previous_display != EGL_NO_DISPLAY)
            eglMakeCurrent(previous_display, s_draw, s_read, previous_context);
        else
            eglMakeCurrent(sys->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    }
    else
        eglSwapBuffers (sys->display, sys->surface);
}

static void *GetSymbol(vlc_gl_t *gl, const char *procname)
{
    (void) gl;
    return (void *)eglGetProcAddress (procname);
}

static void Close(vlc_gl_t *gl)
{
    vlc_gl_sys_t *sys = gl->sys;

    if (sys->context != EGL_NO_CONTEXT)
        eglDestroyContext(sys->display, sys->context);
    if (sys->surface != EGL_NO_SURFACE) {
        eglDestroySurface(sys->display, sys->surface);
        DestroySurface(gl);
    }
    eglTerminate(sys->display);
    ReleaseDisplay(gl);
    free (sys);
}

static void InitEGL(void)
{
    static vlc_once_t once = VLC_STATIC_ONCE;

    if (unlikely(!vlc_once_begin(&once))) {
        clientExts = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
        if (!clientExts)
            clientExts = "";

#ifdef EGL_EXT_platform_base
        getPlatformDisplayEXT =
            (void *) eglGetProcAddress("eglGetPlatformDisplayEXT");
        createPlatformWindowSurfaceEXT =
            (void *) eglGetProcAddress("eglCreatePlatformWindowSurfaceEXT");
#endif
        vlc_once_complete(&once);
    }
}

/**
 * Probe EGL display availability
 */
static int Open(vlc_gl_t *gl, const struct gl_api *api,
                unsigned width, unsigned height,
                const struct vlc_gl_cfg *gl_cfg)
{
    InitEGL();

    int ret = VLC_EGENERIC;
    vlc_object_t *obj = VLC_OBJECT(gl);
    vlc_gl_sys_t *sys = malloc(sizeof (*sys));
    if (unlikely(sys == NULL))
        return VLC_ENOMEM;

    msg_Dbg(obj, "EGL client extensions: %s", clientExts);

    gl->sys = sys;
    sys->display = EGL_NO_DISPLAY;
    sys->surface = EGL_NO_SURFACE;
    sys->context = EGL_NO_CONTEXT;
    sys->is_current = false;

    sys->display = OpenDisplay(gl);
    if (sys->display == EGL_NO_DISPLAY) {
        free(sys);
        return VLC_ENOTSUP;
    }

    /* Initialize EGL display */
    EGLint major, minor;
    if (eglInitialize(sys->display, &major, &minor) != EGL_TRUE)
        goto error;
    msg_Dbg(obj, "EGL version %s by %s",
            eglQueryString(sys->display, EGL_VERSION),
            eglQueryString(sys->display, EGL_VENDOR));

    const char *ext = eglQueryString(sys->display, EGL_EXTENSIONS);
    if (*ext)
        msg_Dbg(obj, "EGL display extensions: %s", ext);

    if (major != 1 || minor < api->min_minor
     || !CheckAPI(sys->display, api->name))
    {
        msg_Err(obj, "cannot select %s API", api->name);
        goto error;
    }

    bool has_device_enum = CheckClientExt("EGL_EXT_device_enumeration");
    bool has_device_query = CheckClientExt("EGL_EXT_device_query");

    if (has_device_query && has_device_enum)
    {
        DisplayAvailableDevices(gl);
        PFNEGLQUERYDISPLAYATTRIBEXTPROC eglQueryDisplayAttribEXT =
            (PFNEGLQUERYDISPLAYATTRIBEXTPROC)eglGetProcAddress("eglQueryDisplayAttribEXT");
        PFNEGLQUERYDEVICESTRINGEXTPROC eglQueryDeviceStringEXT =
            (PFNEGLQUERYDEVICESTRINGEXTPROC)eglGetProcAddress("eglQueryDeviceStringEXT");

        EGLAttrib device_attr;
        if (eglQueryDisplayAttribEXT(sys->display, EGL_DEVICE_EXT,
                                     &device_attr) == EGL_TRUE)
        {
            EGLDeviceEXT device = (EGLDeviceEXT)device_attr;
            msg_Dbg(gl, "Using EGL device:");
            LogDeviceInfo(gl, eglQueryDeviceStringEXT, device);
        }
        else
            msg_Dbg(gl, "could not query the EGL device in use");
    }

    bool has_query_driver =
        CheckDisplayExt(sys->display, "EGL_MESA_query_driver");

    if (has_query_driver)
    {
        PFNEGLGETDISPLAYDRIVERNAMEPROC eglGetDisplayDriverName =
            (PFNEGLGETDISPLAYDRIVERNAMEPROC) eglGetProcAddress("eglGetDisplayDriverName");
        const char *driver = eglGetDisplayDriverName(sys->display);
        msg_Dbg(gl, "Using EGL driver: %s", driver ? driver : "unknown");
    }
    else
        msg_Dbg(gl, "EGL_MESA_query_driver is unavailable");

    const EGLint conf_attr[] = {
        EGL_RED_SIZE, 5,
        EGL_GREEN_SIZE, 5,
        EGL_BLUE_SIZE, 5,
        EGL_ALPHA_SIZE, gl_cfg->need_alpha ? 5 : 0,
        EGL_RENDERABLE_TYPE, api->render_bit,
        EGL_NONE
    };
    EGLConfig cfgv[1];
    EGLint cfgc;

    if (eglChooseConfig(sys->display, conf_attr, cfgv, 1, &cfgc) != EGL_TRUE
     || cfgc == 0)
    {
        msg_Err (obj, "cannot choose EGL configuration");
        goto error;
    }

    /* Create a drawing surface */
    sys->surface = CreateSurface(gl, sys->display, cfgv[0], width, height);
    if (sys->surface == EGL_NO_SURFACE)
    {
        msg_Err (obj, "cannot create EGL window surface");
        goto error;
    }

    if (eglBindAPI (api->api) != EGL_TRUE)
    {
        msg_Err (obj, "cannot bind EGL API");
        goto error;
    }

    EGLContext ctx = eglCreateContext(sys->display, cfgv[0], EGL_NO_CONTEXT,
                                      api->attr);
    if (ctx == EGL_NO_CONTEXT)
    {
        msg_Err (obj, "cannot create EGL context");
        goto error;
    }
    sys->context = ctx;

    /* Initialize OpenGL callbacks */
    static const struct vlc_gl_operations ops =
    {
        .make_current = MakeCurrent,
        .release_current = ReleaseCurrent,
        .resize = Resize,
        .swap = SwapBuffers,
        .get_proc_address = GetSymbol,
        .close = Close,
    };
    gl->ops = &ops;

    return VLC_SUCCESS;

error:
    Close(gl);
    return ret;
}

static int OpenGLES2(vlc_gl_t *gl, unsigned width, unsigned height,
                     const struct vlc_gl_cfg *gl_cfg)
{
    static const struct gl_api api = {
        "OpenGL_ES", EGL_OPENGL_ES_API, 4, EGL_OPENGL_ES2_BIT,
        { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE },
    };
    return Open(gl, &api, width, height, gl_cfg);
}

static int OpenGL(vlc_gl_t *gl, unsigned width, unsigned height,
                  const struct vlc_gl_cfg *gl_cfg)
{
    static const struct gl_api api = {
        "OpenGL", EGL_OPENGL_API, 4, EGL_OPENGL_BIT,
        { EGL_NONE },
    };
    return Open(gl, &api, width, height, gl_cfg);
}

#ifdef USE_PLATFORM_XCB
# define VLC_PRIORITY 60
#endif
#ifndef VLC_PRIORITY
# define VLC_PRIORITY 50
#endif

vlc_module_begin ()
    set_shortname (N_("EGL"))
    set_description (N_("EGL extension for OpenGL"))
    set_subcategory (SUBCAT_VIDEO_VOUT)
    set_callback_opengl(OpenGL, VLC_PRIORITY)
    add_shortcut ("egl")

    add_submodule ()
    set_callback_opengl_es2(OpenGLES2, VLC_PRIORITY)
    add_shortcut ("egl")

vlc_module_end ()
