/*
 * This file is part of mpv.
 *
 * mpv 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.
 *
 * mpv 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 mpv.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <X11/Xlib.h>
#include <GL/glx.h>

#include "video/vdpau.h"
#include "video/out/x11_common.h"
#include "context.h"

// This is a GL_NV_vdpau_interop specification bug, and headers (unfortunately)
// follow it. I'm not sure about the original nvidia headers.
#define BRAINDEATH(x) ((void *)(uintptr_t)(x))

struct surface {
    int w, h;
    VdpOutputSurface surface;
    // This nested shitshow of handles to the same object piss me off.
    GLvdpauSurfaceNV registered;
    bool mapped;
    GLuint texture;
    GLuint fbo;
};

struct priv {
    GL gl;
    GLXContext context;
    struct mp_vdpau_ctx *vdp;
    VdpPresentationQueueTarget vdp_target;
    VdpPresentationQueue vdp_queue;
    struct surface *surfaces;
    int num_surfaces;
    int idx_surfaces;
};

typedef GLXContext (*glXCreateContextAttribsARBProc)
    (Display*, GLXFBConfig, GLXContext, Bool, const int*);

static bool create_context_x11(struct ra_ctx *ctx)
{
    struct priv *p = ctx->priv;
    struct vo *vo = ctx->vo;

    int glx_major, glx_minor;
    if (!glXQueryVersion(vo->x11->display, &glx_major, &glx_minor)) {
        MP_ERR(vo, "GLX not found.\n");
        return false;
    }

    if (!ra_gl_ctx_test_version(ctx, MPGL_VER(glx_major, glx_minor), false))
        return false;

    int glx_attribs[] = {
        GLX_X_RENDERABLE, True,
        GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
        GLX_RED_SIZE, 1,
        GLX_GREEN_SIZE, 1,
        GLX_BLUE_SIZE, 1,
        GLX_DOUBLEBUFFER, True,
        None
    };
    int fbcount;
    GLXFBConfig *fbcs = glXChooseFBConfig(vo->x11->display, vo->x11->screen,
                                          glx_attribs, &fbcount);
    if (!fbcs)
        return false;
    // The list in fbc is sorted (so that the first element is the best).
    GLXFBConfig fbc = fbcount > 0 ? fbcs[0] : NULL;
    XFree(fbcs);
    if (!fbc) {
        MP_ERR(vo, "no GLX support present\n");
        return false;
    }

    glXCreateContextAttribsARBProc glXCreateContextAttribsARB =
        (glXCreateContextAttribsARBProc)
            glXGetProcAddressARB((const GLubyte *)"glXCreateContextAttribsARB");

    const char *glxstr =
        glXQueryExtensionsString(vo->x11->display, vo->x11->screen);
    bool have_ctx_ext = glxstr && !!strstr(glxstr, "GLX_ARB_create_context");

    if (!(have_ctx_ext && glXCreateContextAttribsARB)) {
        return false;
    }

    int ctx_flags = ctx->opts.debug ? GLX_CONTEXT_DEBUG_BIT_ARB : 0;
    int context_attribs[] = {
        GLX_CONTEXT_MAJOR_VERSION_ARB, 4,
        GLX_CONTEXT_MINOR_VERSION_ARB, 0,
        GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
        GLX_CONTEXT_FLAGS_ARB, ctx_flags,
        None
    };
    GLXContext context = glXCreateContextAttribsARB(vo->x11->display, fbc, 0,
                                                    True, context_attribs);
    if (!context)
        return false;

    // Pass 0 as drawable for offscreen use. This is probably (?) not valid in
    // standard GLX, but the nVidia drivers accept it.
    if (!glXMakeCurrent(vo->x11->display, 0, context)) {
        MP_FATAL(vo, "Could not set GLX context!\n");
        glXDestroyContext(vo->x11->display, context);
        return false;
    }

    p->context = context;
    mpgl_load_functions(&p->gl, (void *)glXGetProcAddressARB, glxstr, vo->log);
    return true;
}

static int create_vdpau_objects(struct ra_ctx *ctx)
{
    struct priv *p = ctx->priv;
    struct GL *gl = &p->gl;
    VdpDevice dev = p->vdp->vdp_device;
    struct vdp_functions *vdp = &p->vdp->vdp;
    VdpStatus vdp_st;

    gl->VDPAUInitNV(BRAINDEATH(dev), p->vdp->get_proc_address);

    vdp_st = vdp->presentation_queue_target_create_x11(dev, ctx->vo->x11->window,
                                                       &p->vdp_target);
    CHECK_VDP_ERROR(ctx, "creating vdp target");

    vdp_st = vdp->presentation_queue_create(dev, p->vdp_target, &p->vdp_queue);
    CHECK_VDP_ERROR(ctx, "creating vdp presentation queue");

    return 0;
}

static void destroy_vdpau_surface(struct ra_ctx *ctx,
                                  struct surface *surface)
{
    struct priv *p = ctx->priv;
    struct vdp_functions *vdp = &p->vdp->vdp;
    VdpStatus vdp_st;
    GL *gl = &p->gl;

    if (surface->mapped)
        gl->VDPAUUnmapSurfacesNV(1, &surface->registered);

    gl->DeleteFramebuffers(1, &surface->fbo);
    gl->DeleteTextures(1, &surface->texture);

    if (surface->registered)
        gl->VDPAUUnregisterSurfaceNV(surface->registered);

    if (surface->surface != VDP_INVALID_HANDLE) {
        vdp_st = vdp->output_surface_destroy(surface->surface);
        CHECK_VDP_WARNING(ctx, "destroying vdpau surface");
    }

    *surface = (struct surface){
        .surface = VDP_INVALID_HANDLE,
    };
}

static bool recreate_vdpau_surface(struct ra_ctx *ctx,
                                   struct surface *surface)
{
    struct priv *p = ctx->priv;
    VdpDevice dev = p->vdp->vdp_device;
    struct vdp_functions *vdp = &p->vdp->vdp;
    VdpStatus vdp_st;
    GL *gl = &p->gl;

    destroy_vdpau_surface(ctx, surface);

    surface->w = ctx->vo->dwidth;
    surface->h = ctx->vo->dheight;

    vdp_st = vdp->output_surface_create(dev, VDP_RGBA_FORMAT_B8G8R8A8,
                                        surface->w, surface->h,
                                        &surface->surface);
    CHECK_VDP_ERROR_NORETURN(ctx, "creating vdp output surface");
    if (vdp_st != VDP_STATUS_OK)
        goto error;

    gl->GenTextures(1, &surface->texture);

    surface->registered =
        gl->VDPAURegisterOutputSurfaceNV(BRAINDEATH(surface->surface),
                                         GL_TEXTURE_2D,
                                         1, &surface->texture);
    if (!surface->registered) {
        MP_ERR(ctx, "could not register vdpau surface with GL\n");
        goto error;
    }

    gl->VDPAUSurfaceAccessNV(surface->registered, GL_WRITE_DISCARD_NV);
    gl->VDPAUMapSurfacesNV(1, &surface->registered);
    surface->mapped = true;

    gl->GenFramebuffers(1, &surface->fbo);
    gl->BindFramebuffer(GL_FRAMEBUFFER, surface->fbo);
    gl->FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                             GL_TEXTURE_2D, surface->texture, 0);
    GLenum err = gl->CheckFramebufferStatus(GL_FRAMEBUFFER);
    if (err != GL_FRAMEBUFFER_COMPLETE) {
        MP_ERR(ctx, "Framebuffer completeness check failed (error=%d).\n",
               (int)err);
        goto error;
    }
    gl->BindFramebuffer(GL_FRAMEBUFFER, 0);

    gl->VDPAUUnmapSurfacesNV(1, &surface->registered);
    surface->mapped = false;

    return true;

error:
    destroy_vdpau_surface(ctx, surface);
    return false;
}

static void vdpau_swap_buffers(struct ra_ctx *ctx)
{
    struct priv *p = ctx->priv;
    struct vdp_functions *vdp = &p->vdp->vdp;
    VdpStatus vdp_st;

    // This is the *next* surface we will be rendering to. By delaying the
    // block_until_idle, we're essentially allowing p->num_surfaces - 1
    // in-flight surfaces, plus the one currently visible surface.
    struct surface *surf = &p->surfaces[p->idx_surfaces];
    if (surf->surface == VDP_INVALID_HANDLE)
        return;

    VdpTime prev_vsync_time;
    vdp_st = vdp->presentation_queue_block_until_surface_idle(p->vdp_queue,
                                                              surf->surface,
                                                              &prev_vsync_time);
    CHECK_VDP_WARNING(ctx, "waiting for surface failed");
}

static void vdpau_uninit(struct ra_ctx *ctx)
{
    struct priv *p = ctx->priv;
    ra_gl_ctx_uninit(ctx);

    if (p->vdp) {
        struct vdp_functions *vdp = &p->vdp->vdp;
        VdpStatus vdp_st;

        for (int n = 0; n < p->num_surfaces; n++)
            destroy_vdpau_surface(ctx, &p->surfaces[n]);

        if (p->vdp_queue != VDP_INVALID_HANDLE) {
            vdp_st = vdp->presentation_queue_destroy(p->vdp_queue);
            CHECK_VDP_WARNING(ctx, "destroying presentation queue");
        }

        if (p->vdp_target != VDP_INVALID_HANDLE) {
            vdp_st = vdp->presentation_queue_target_destroy(p->vdp_target);
            CHECK_VDP_WARNING(ctx, "destroying presentation target");
        }

        mp_vdpau_destroy(p->vdp);
    }

    if (p->context) {
        Display *display = ctx->vo->x11->display;
        glXMakeCurrent(display, None, NULL);
        glXDestroyContext(display, p->context);
    }

    vo_x11_uninit(ctx->vo);
}

static const struct ra_swapchain_fns vdpau_swapchain;

static bool vdpau_init(struct ra_ctx *ctx)
{
    struct vo *vo = ctx->vo;
    struct priv *p = ctx->priv = talloc_zero(ctx, struct priv);

    p->vdp_queue = VDP_INVALID_HANDLE;
    p->vdp_target = VDP_INVALID_HANDLE;

    if (ctx->vo->probing)
        goto uninit;

    if (!vo_x11_init(ctx->vo))
        goto uninit;

    p->vdp = mp_vdpau_create_device_x11(ctx->log, ctx->vo->x11->display, false);
    if (!p->vdp)
        goto uninit;

    if (!vo_x11_create_vo_window(vo, NULL, "vdpauglx"))
        goto uninit;

    if (!create_context_x11(ctx))
        goto uninit;

    if (!(p->gl.mpgl_caps & MPGL_CAP_VDPAU))
        goto uninit;

    if (create_vdpau_objects(ctx) < 0)
        goto uninit;

    p->num_surfaces = ctx->opts.swapchain_depth + 1; // +1 for the visible image
    p->surfaces = talloc_zero_array(p, struct surface, p->num_surfaces);
    for (int n = 0; n < p->num_surfaces; n++)
        p->surfaces[n].surface = VDP_INVALID_HANDLE;

    struct ra_gl_ctx_params params = {
        .swap_buffers = vdpau_swap_buffers,
        .external_swapchain = &vdpau_swapchain,
        .flipped = true,
    };

    if (!ra_gl_ctx_init(ctx, &p->gl, params))
        goto uninit;

    return true;

uninit:
    vdpau_uninit(ctx);
    return false;
}

static bool vdpau_start_frame(struct ra_swapchain *sw, struct ra_fbo *out_fbo)
{
    struct priv *p = sw->ctx->priv;
    struct vo *vo = sw->ctx->vo;
    GL *gl = &p->gl;

    struct surface *surf = &p->surfaces[p->idx_surfaces];
    if (surf->w != vo->dwidth || surf->h != vo->dheight ||
        surf->surface == VDP_INVALID_HANDLE)
    {
        if (!recreate_vdpau_surface(sw->ctx, surf))
            return NULL;
    }

    assert(!surf->mapped);
    gl->VDPAUMapSurfacesNV(1, &surf->registered);
    surf->mapped = true;

    ra_gl_ctx_resize(sw, surf->w, surf->h, surf->fbo);
    return ra_gl_ctx_start_frame(sw, out_fbo);
}

static bool vdpau_submit_frame(struct ra_swapchain *sw,
                               const struct vo_frame *frame)
{
    struct priv *p = sw->ctx->priv;
    GL *gl = &p->gl;
    struct vdp_functions *vdp = &p->vdp->vdp;
    VdpStatus vdp_st;

    struct surface *surf = &p->surfaces[p->idx_surfaces];
    assert(surf->surface != VDP_INVALID_HANDLE);
    assert(surf->mapped);
    gl->VDPAUUnmapSurfacesNV(1, &surf->registered);
    surf->mapped = false;

    vdp_st = vdp->presentation_queue_display(p->vdp_queue, surf->surface, 0, 0, 0);
    CHECK_VDP_WARNING(sw->ctx, "trying to present vdp surface");

    p->idx_surfaces = (p->idx_surfaces + 1) % p->num_surfaces;
    return ra_gl_ctx_submit_frame(sw, frame) && vdp_st == VDP_STATUS_OK;
}

static bool vdpau_reconfig(struct ra_ctx *ctx)
{
    vo_x11_config_vo_window(ctx->vo);
    return true;
}

static int vdpau_control(struct ra_ctx *ctx, int *events, int request, void *arg)
{
    return vo_x11_control(ctx->vo, events, request, arg);
}

static void vdpau_wakeup(struct ra_ctx *ctx)
{
    vo_x11_wakeup(ctx->vo);
}

static void vdpau_wait_events(struct ra_ctx *ctx, int64_t until_time_us)
{
    vo_x11_wait_events(ctx->vo, until_time_us);
}

static const struct ra_swapchain_fns vdpau_swapchain = {
    .start_frame   = vdpau_start_frame,
    .submit_frame  = vdpau_submit_frame,
};

const struct ra_ctx_fns ra_ctx_vdpauglx = {
    .type           = "opengl",
    .name           = "vdpauglx",
    .reconfig       = vdpau_reconfig,
    .control        = vdpau_control,
    .wakeup         = vdpau_wakeup,
    .wait_events    = vdpau_wait_events,
    .init           = vdpau_init,
    .uninit         = vdpau_uninit,
};
