/*
 * 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 "common.h"
#include "formats.h"
#include "gpu.h"
#include "swapchain.h"
#include "utils.h"
#include "pl_thread.h"

struct priv {
    struct pl_sw_fns impl;

    struct pl_opengl_swapchain_params params;
    pl_opengl gl;
    pl_mutex lock;
    bool has_sync;

    // current parameters
    pl_tex fb;
    bool frame_started;

    // vsync fences
    int swapchain_depth;
    PL_ARRAY(GLsync) vsync_fences;
};

static const struct pl_sw_fns opengl_swapchain;

pl_swapchain pl_opengl_create_swapchain(pl_opengl pl_gl,
                              const struct pl_opengl_swapchain_params *params)
{
    pl_gpu gpu = pl_gl->gpu;

    if (params->max_swapchain_depth < 0) {
        PL_ERR(gpu, "Tried specifying negative swapchain depth?");
        return NULL;
    }

    if (!gl_make_current(pl_gl))
        return NULL;

    struct pl_swapchain_t *sw = pl_zalloc_obj(NULL, sw, struct priv);
    sw->log = gpu->log;
    sw->gpu = gpu;

    struct priv *p = PL_PRIV(sw);
    pl_mutex_init(&p->lock);
    p->impl = opengl_swapchain;
    p->params = *params;
    p->has_sync = pl_opengl_has_ext(pl_gl, "GL_ARB_sync");
    p->gl = pl_gl;

    gl_release_current(pl_gl);
    return sw;
}

static void gl_sw_destroy(pl_swapchain sw)
{
    pl_gpu gpu = sw->gpu;
    struct priv *p = PL_PRIV(sw);

    pl_gpu_flush(gpu);
    pl_tex_destroy(gpu, &p->fb);
    pl_mutex_destroy(&p->lock);
    pl_free((void *) sw);
}

static int gl_sw_latency(pl_swapchain sw)
{
    struct priv *p = PL_PRIV(sw);
    return p->params.max_swapchain_depth;
}

static bool gl_sw_resize(pl_swapchain sw, int *width, int *height)
{
    struct priv *p = PL_PRIV(sw);
    const int w = *width, h = *height;

    pl_mutex_lock(&p->lock);
    if (p->fb && w == p->fb->params.w && h == p->fb->params.h) {
        pl_mutex_unlock(&p->lock);
        return true;
    }

    if (p->frame_started && (w || h)) {
        PL_ERR(sw, "Tried resizing the swapchain while a frame was in progress! "
               "Please submit the current frame first.");
        pl_mutex_unlock(&p->lock);
        return false;
    }

    if (w && h) {
        pl_tex_destroy(sw->gpu, &p->fb);
        p->fb = pl_opengl_wrap(sw->gpu, pl_opengl_wrap_params(
            .framebuffer = p->params.framebuffer.id,
            .width = w,
            .height = h,
        ));
        if (!p->fb) {
            PL_ERR(sw, "Failed wrapping OpenGL framebuffer!");
            pl_mutex_unlock(&p->lock);
            return false;
        }
    }

    if (!p->fb) {
        PL_ERR(sw, "Tried calling `pl_swapchain_resize` with unknown size! "
               "This is forbidden for OpenGL. The first call to "
               "`pl_swapchain_resize` must include the width and height of the "
               "swapchain, because there's no way to figure this out from "
               "within the API.");
        pl_mutex_unlock(&p->lock);
        return false;
    }

    *width = p->fb->params.w;
    *height = p->fb->params.h;
    pl_mutex_unlock(&p->lock);
    return true;
}

void pl_opengl_swapchain_update_fb(pl_swapchain sw,
                                   const struct pl_opengl_framebuffer *fb)
{
    struct priv *p = PL_PRIV(sw);
    pl_mutex_lock(&p->lock);
    if (p->frame_started) {
        PL_ERR(sw,"Tried calling `pl_opengl_swapchain_update_fb` while a frame "
               "was in progress! Please submit the current frame first.");
        pl_mutex_unlock(&p->lock);
        return;
    }

    if (p->params.framebuffer.id != fb->id)
        pl_tex_destroy(sw->gpu, &p->fb);

    p->params.framebuffer = *fb;
    pl_mutex_unlock(&p->lock);
}

static bool gl_sw_start_frame(pl_swapchain sw,
                              struct pl_swapchain_frame *out_frame)
{
    struct priv *p = PL_PRIV(sw);
    pl_mutex_lock(&p->lock);
    bool ok = false;

    if (!p->fb) {
        PL_ERR(sw, "Unknown framebuffer size. Please call `pl_swapchain_resize` "
               "before `pl_swapchain_start_frame` for OpenGL swapchains!");
        goto error;
    }

    if (p->frame_started) {
        PL_ERR(sw, "Attempted calling `pl_swapchain_start` while a frame was "
               "already in progress! Call `pl_swapchain_submit_frame` first.");
        goto error;
    }

    if (!gl_make_current(p->gl))
        goto error;

    *out_frame = (struct pl_swapchain_frame) {
        .fbo = p->fb,
        .flipped = !p->params.framebuffer.flipped,
        .color_repr = {
            .sys = PL_COLOR_SYSTEM_RGB,
            .levels = PL_COLOR_LEVELS_FULL,
            .alpha = p->fb->params.format->num_components == 4
                        ? PL_ALPHA_PREMULTIPLIED
                        : PL_ALPHA_NONE,
            .bits = {
                // Just use the red channel in the absence of anything more
                // sane to do, because the red channel is both guaranteed to
                // exist and also typically has the minimum number of bits
                // (which is arguably what matters for dithering)
                .sample_depth = p->fb->params.format->component_depth[0],
                .color_depth = p->fb->params.format->component_depth[0],
            },
        },
        .color_space = pl_color_space_monitor,
    };

    p->frame_started = gl_check_err(sw->gpu, "gl_sw_start_frame");
    if (!p->frame_started)
        goto error;

    // keep p->lock held
    gl_release_current(p->gl);
    return true;

error:
    gl_release_current(p->gl);
    pl_mutex_unlock(&p->lock);
    return ok;
}

static bool gl_sw_submit_frame(pl_swapchain sw)
{
    struct priv *p = PL_PRIV(sw);
    struct gl_ctx *glctx = PL_PRIV(p->gl);
    const gl_funcs *gl = &glctx->func;
    if (!gl_make_current(p->gl)) {
        p->frame_started = false;
        pl_mutex_unlock(&p->lock);
        return false;
    }

    pl_assert(p->frame_started);
    if (p->has_sync && p->params.max_swapchain_depth) {
        GLsync fence = gl->FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
        if (fence)
            PL_ARRAY_APPEND(sw, p->vsync_fences, fence);
    }

    gl->Flush();
    p->frame_started = false;
    bool ok = gl_check_err(sw->gpu, "gl_sw_submit_frame");
    gl_release_current(p->gl);
    pl_mutex_unlock(&p->lock);

    return ok;
}

static void gl_sw_swap_buffers(pl_swapchain sw)
{
    struct priv *p = PL_PRIV(sw);
    struct gl_ctx *glctx = PL_PRIV(p->gl);
    const gl_funcs *gl = &glctx->func;
    if (!p->params.swap_buffers) {
        PL_ERR(sw, "`pl_swapchain_swap_buffers` called but no "
               "`params.swap_buffers` callback set!");
        return;
    }

    pl_mutex_lock(&p->lock);
    if (!gl_make_current(p->gl)) {
        pl_mutex_unlock(&p->lock);
        return;
    }

    p->params.swap_buffers(p->params.priv);

    const int max_depth = p->params.max_swapchain_depth;
    while (max_depth && p->vsync_fences.num >= max_depth) {
        gl->ClientWaitSync(p->vsync_fences.elem[0], GL_SYNC_FLUSH_COMMANDS_BIT, 1e9);
        gl->DeleteSync(p->vsync_fences.elem[0]);
        PL_ARRAY_REMOVE_AT(p->vsync_fences, 0);
    }

    gl_check_err(sw->gpu, "gl_sw_swap_buffers");
    gl_release_current(p->gl);
    pl_mutex_unlock(&p->lock);
}

static const struct pl_sw_fns opengl_swapchain = {
    .destroy      = gl_sw_destroy,
    .latency      = gl_sw_latency,
    .resize       = gl_sw_resize,
    .start_frame  = gl_sw_start_frame,
    .submit_frame = gl_sw_submit_frame,
    .swap_buffers = gl_sw_swap_buffers,
};
