/*****************************************************************************
 * display.c: Android video output module
 *****************************************************************************
 * Copyright (C) 2014 VLC authors and VideoLAN
 *
 * Authors: Thomas Guillem <thomas@gllm.fr>
 *          Felix Abecassis <felix.abecassis@gmail.com>
 *          Ming Hu <tewilove@gmail.com>
 *          Ludovic Fauvet <etix@l0cal.com>
 *          Sébastien Toque <xilasz@gmail.com>
 *
 * 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 <vlc_common.h>
#include <vlc_threads.h>
#include <vlc_plugin.h>
#include <vlc_vout_display.h>
#include <vlc_subpicture.h>

#include <vlc_vector.h>
#include <vlc_opengl.h>
#include "utils.h"
#include "../opengl/gl_api.h"
#include "../opengl/sub_renderer.h"

struct sub_region
{
    int x;
    int y;
    unsigned int width;
    unsigned int height;
};

struct subpicture
{
    vlc_window_t *window;
    vlc_gl_t *gl;
    struct vlc_gl_api api;
    struct vlc_gl_interop *interop;
    struct vlc_gl_sub_renderer *renderer;
    vout_display_place_t place;
    bool place_changed;
    bool is_dirty;
    bool clear;

    int64_t last_order;
    struct VLC_VECTOR(struct sub_region) regions;

    struct {
        PFNGLFLUSHPROC Flush;
    } vt;
};

struct sys
{
    AWindowHandler *awh;
    android_video_context_t *avctx;
    video_format_t fmt;
    struct subpicture sub;
};

static void FlipVerticalAlign(struct vout_display_placement *dp)
{
    /* Reverse vertical alignment as the GL tex are Y inverted */
    if (dp->align.vertical == VLC_VIDEO_ALIGN_TOP)
        dp->align.vertical = VLC_VIDEO_ALIGN_BOTTOM;
    else if (dp->align.vertical == VLC_VIDEO_ALIGN_BOTTOM)
        dp->align.vertical = VLC_VIDEO_ALIGN_TOP;
}

static int subpicture_Control(vout_display_t *vd, int query)
{
    struct sys *sys = vd->sys;
    struct subpicture *sub = &sys->sub;

    switch (query)
    {
    case VOUT_DISPLAY_CHANGE_DISPLAY_SIZE:
    case VOUT_DISPLAY_CHANGE_DISPLAY_FILLED:
    case VOUT_DISPLAY_CHANGE_ZOOM:
    {
        struct vout_display_placement dp = vd->cfg->display;

        FlipVerticalAlign(&dp);
        vout_display_PlacePicture(&sub->place, vd->source, &dp);
        sub->place_changed = true;
        vlc_gl_Resize(sub->gl, dp.width, dp.height);
        return VLC_SUCCESS;
    }

    case VOUT_DISPLAY_CHANGE_SOURCE_ASPECT:
    case VOUT_DISPLAY_CHANGE_SOURCE_CROP:
        return VLC_SUCCESS;
    default:
        break;
    }
    return VLC_EGENERIC;
}

static bool subpicture_NeedDraw(vout_display_t *vd, subpicture_t *subpicture)
{
    struct sys *sys = vd->sys;
    struct subpicture *sub = &sys->sub;

    if (subpicture == NULL)
    {
        if (!sub->clear)
            return false;
        sub->clear = false;
        /* Need to draw one last time in order to clear the current subpicture */
        return true;
    }

    sub->clear = true;

    size_t count = 0;
    for (subpicture_region_t *r = subpicture->p_region;
         r != NULL; r = r->p_next)
        count++;

    if (subpicture->i_order != sub->last_order)
    {
        sub->last_order = subpicture->i_order;
        /* Subpicture content is different */
        goto end;
    }

    bool draw = false;

    if (count == sub->regions.size)
    {
        size_t i = 0;
        for (subpicture_region_t *r = subpicture->p_region;
             r != NULL; r = r->p_next)
        {
            struct sub_region *cmp = &sub->regions.data[i++];
            if (cmp->x != r->i_x || cmp->y != r->i_y
             || cmp->width != r->fmt.i_visible_width
             || cmp->height != r->fmt.i_visible_height)
            {
                /* Subpicture regions are different */
                draw = true;
                break;
            }
        }
    }
    else
    {
        /* Subpicture region count is different */
        draw = true;
    }

    if (!draw)
        return false;

end:
    /* Store the current subpicture regions in order to compare then later.
     */
    if (!vlc_vector_reserve(&sub->regions, count))
        return false;

    sub->regions.size = 0;

    for (subpicture_region_t *r = subpicture->p_region;
         r != NULL; r = r->p_next)
    {
        struct sub_region reg = {
            .x = r->i_x,
            .y = r->i_y,
            .width = r->fmt.i_visible_width,
            .height = r->fmt.i_visible_height,
        };
        bool res = vlc_vector_push(&sub->regions, reg);
        /* Already checked with vlc_vector_reserve */
        assert(res); (void) res;
    }

    return true;
}

static void subpicture_Prepare(vout_display_t *vd, subpicture_t *subpicture)
{
    struct sys *sys = vd->sys;
    struct subpicture *sub = &sys->sub;

    if (!subpicture_NeedDraw(vd, subpicture))
    {
        sub->is_dirty = false;
        return;
    }

    if (vlc_gl_MakeCurrent(sub->gl) != VLC_SUCCESS)
        return;

    sub->api.vt.ClearColor(0.f, 0.f, 0.f, 0.f);
    sub->api.vt.Clear(GL_COLOR_BUFFER_BIT);

    int ret = vlc_gl_sub_renderer_Prepare(sub->renderer, subpicture);
    if (ret != VLC_SUCCESS)
        goto error;
    sub->vt.Flush();

    if (sub->place_changed)
    {
        sub->api.vt.Viewport(sub->place.x, sub->place.y,
                             sub->place.width, sub->place.height);
        sub->place_changed = false;
    }

    ret = vlc_gl_sub_renderer_Draw(sub->renderer);
    if (ret != VLC_SUCCESS)
        goto error;
    sub->vt.Flush();

    sub->is_dirty = true;
error:
    vlc_gl_ReleaseCurrent(sub->gl);
}

static void subpicture_Display(vout_display_t *vd)
{
    struct sys *sys = vd->sys;
    struct subpicture *sub = &sys->sub;

    if (sub->is_dirty)
        vlc_gl_Swap(sub->gl);
}

static void subpicture_CloseDisplay(vout_display_t *vd)
{
    struct sys *sys = vd->sys;
    struct subpicture *sub = &sys->sub;

    int ret = vlc_gl_MakeCurrent(sub->gl);

    if (ret == 0)
    {
        /* Clear the surface */
        sub->api.vt.ClearColor(0.f, 0.f, 0.f, 0.f);
        sub->api.vt.Clear(GL_COLOR_BUFFER_BIT);
        vlc_gl_Swap(sub->gl);
    }

    vlc_gl_sub_renderer_Delete(sub->renderer);
    vlc_gl_interop_Delete(sub->interop);

    vlc_gl_ReleaseCurrent(sub->gl);

    vlc_gl_Delete(sub->gl);

    vlc_window_Disable(sub->window);
    vlc_window_Delete(sub->window);

    vlc_vector_destroy(&sub->regions);
}

static void subpicture_window_Resized(struct vlc_window *wnd, unsigned width,
                                    unsigned height, vlc_window_ack_cb cb,
                                    void *opaque)
{
    if (cb != NULL)
        cb(wnd, width, height, opaque);
}

static int subpicture_window_Open(vlc_window_t *wnd)
{
    static const struct vlc_window_operations ops = {
    };

    wnd->type = VLC_WINDOW_TYPE_ANDROID_NATIVE;
    wnd->display.anativewindow = wnd->owner.sys;
    wnd->handle.android_id = AWindow_Subtitles;
    wnd->ops = &ops;
    return VLC_SUCCESS;
}

static int subpicture_OpenDisplay(vout_display_t *vd)
{
    struct sys *sys = vd->sys;
    struct subpicture *sub = &sys->sub;

    sub->is_dirty = false;
    sub->clear = false;
    sub->last_order = -1;
    vlc_vector_init(&sub->regions);

    /* Create a VLC sub window that will hold the subpicture surface */
    static const struct vlc_window_callbacks win_cbs = {
        .resized = subpicture_window_Resized,
    };

    const vlc_window_cfg_t win_cfg = {
        .is_fullscreen = true,
        .is_decorated = false,
        .width = sys->fmt.i_width,
        .height = sys->fmt.i_height,
    };

    const vlc_window_owner_t win_owner = {
        .cbs = &win_cbs,
        .sys = vd->cfg->window->display.anativewindow,
    };

    sub->window = vlc_window_New(VLC_OBJECT(vd), "android-subpicture",
                                 &win_owner, &win_cfg);
    if (sub->window == NULL)
        return -1;

    if (vlc_window_Enable(sub->window) != 0)
        goto delete_win;

    /* Create the OpenGLES2 context on the subpicture window/surface */
    vout_display_cfg_t vd_cfg = *vd->cfg;
    vd_cfg.window = sub->window;

    struct vlc_gl_cfg gl_cfg = { .need_alpha = true };
    sub->gl = vlc_gl_Create(&vd_cfg, VLC_OPENGL_ES2, NULL, &gl_cfg);
    if (sub->gl == NULL)
        goto disable_win;

    /* Initialize and configure subpicture renderer/interop */
    struct vout_display_placement dp = vd->cfg->display;
    FlipVerticalAlign(&dp);
    vout_display_PlacePicture(&sub->place, vd->source, &dp);
    sub->place_changed = true;
    vlc_gl_Resize(sub->gl, dp.width, dp.height);

    if (vlc_gl_MakeCurrent(sub->gl))
        goto delete_gl;

    sub->vt.Flush = vlc_gl_GetProcAddress(sub->gl, "glFlush");
    if (sub->vt.Flush == NULL)
        goto release_gl;

    int ret = vlc_gl_api_Init(&sub->api, sub->gl);
    if (ret != VLC_SUCCESS)
        goto release_gl;

    sub->interop = vlc_gl_interop_NewForSubpictures(sub->gl);
    if (sub->interop == NULL)
    {
        msg_Err(vd, "Could not create sub interop");
        goto release_gl;
    }

    sub->renderer = vlc_gl_sub_renderer_New(sub->gl, &sub->api, sub->interop);
    if (sub->renderer == NULL)
        goto delete_interop;

    vlc_gl_ReleaseCurrent(sub->gl);

    static const vlc_fourcc_t gl_subpicture_chromas[] = {
        VLC_CODEC_RGBA,
        0
    };
    vd->info.subpicture_chromas = gl_subpicture_chromas;

    return 0;

delete_interop:
    vlc_gl_interop_Delete(sub->interop);
release_gl:
    vlc_gl_ReleaseCurrent(sub->gl);
delete_gl:
    vlc_gl_Delete(sub->gl);
disable_win:
    vlc_window_Disable(sub->window);
delete_win:
    vlc_window_Delete(sub->window);
    sub->window = NULL;
    return -1;
}

static void Prepare(vout_display_t *vd, picture_t *picture,
                    subpicture_t *subpicture, vlc_tick_t date)
{
    struct sys *sys = vd->sys;

    assert(picture->context);
    if (sys->avctx->render_ts != NULL)
        sys->avctx->render_ts(picture->context, date);

    if (sys->sub.window != NULL)
        subpicture_Prepare(vd, subpicture);
}

static void Display(vout_display_t *vd, picture_t *picture)
{
    struct sys *sys = vd->sys;
    assert(picture->context);
    sys->avctx->render(picture->context);

    if (sys->sub.window != NULL)
        subpicture_Display(vd);
}

static int Control(vout_display_t *vd, int query)
{
    struct sys *sys = vd->sys;

    if (sys->sub.window != NULL)
        subpicture_Control(vd, query);

    switch (query) {
    case VOUT_DISPLAY_CHANGE_SOURCE_CROP:
    {
        msg_Dbg(vd, "change source crop: %ux%u @ %ux%u",
                vd->source->i_x_offset, vd->source->i_y_offset,
                vd->source->i_visible_width,
                vd->source->i_visible_height);

        video_format_CopyCrop(&sys->fmt, vd->source);

        video_format_t rot_fmt;
        video_format_ApplyRotation(&rot_fmt, &sys->fmt);
        AWindowHandler_setVideoLayout(sys->awh, 0, 0,
                                      rot_fmt.i_visible_width,
                                      rot_fmt.i_visible_height,
                                      0, 0);
        return VLC_SUCCESS;
    }
    case VOUT_DISPLAY_CHANGE_SOURCE_ASPECT:
    {
        msg_Dbg(vd, "change source aspect: %d/%d", vd->source->i_sar_num,
                vd->source->i_sar_den);

        sys->fmt.i_sar_num = vd->source->i_sar_num;
        sys->fmt.i_sar_den = vd->source->i_sar_den;
        video_format_t rot_fmt;
        video_format_ApplyRotation(&rot_fmt, &sys->fmt);
        if (rot_fmt.i_sar_num != 0 && rot_fmt.i_sar_den != 0)
            AWindowHandler_setVideoLayout(sys->awh, 0, 0, 0, 0,
                                          rot_fmt.i_sar_num, rot_fmt.i_sar_den);

        return VLC_SUCCESS;
    }
    case VOUT_DISPLAY_CHANGE_DISPLAY_SIZE:
    {
        msg_Dbg(vd, "change display size: %dx%d", vd->cfg->display.width,
                                                  vd->cfg->display.height);
        return VLC_SUCCESS;
    }
    case VOUT_DISPLAY_CHANGE_ZOOM:
    case VOUT_DISPLAY_CHANGE_DISPLAY_FILLED:
        return VLC_SUCCESS;
    default:
        msg_Warn(vd, "Unknown request in android-display: %d", query);
        return VLC_EGENERIC;
    }
}

static void Close(vout_display_t *vd)
{
    struct sys *sys = vd->sys;

    AWindowHandler_setVideoLayout(sys->awh, 0, 0, 0, 0, 0, 0);

    if (sys->sub.window != NULL)
        subpicture_CloseDisplay(vd);

    free(sys);
}

static int Open(vout_display_t *vd,
                video_format_t *fmtp, vlc_video_context *context)
{
    vlc_window_t *embed = vd->cfg->window;
    AWindowHandler *awh = embed->display.anativewindow;

    if (embed->type != VLC_WINDOW_TYPE_ANDROID_NATIVE
     || fmtp->i_chroma != VLC_CODEC_ANDROID_OPAQUE
     || context == NULL
     || !AWindowHandler_canSetVideoLayout(awh))
        return VLC_EGENERIC;

    if (!vd->obj.force && fmtp->projection_mode != PROJECTION_MODE_RECTANGULAR)
    {
        /* Let the gles2 vout handle projection */
        return VLC_EGENERIC;
    }

    struct sys *sys;
    vd->sys = sys = malloc(sizeof(*sys));
    if (sys == NULL)
        return VLC_ENOMEM;

    video_format_ApplyRotation(&sys->fmt, fmtp);

    sys->awh = awh;
    sys->avctx = vlc_video_context_GetPrivate(context, VLC_VIDEO_CONTEXT_AWINDOW);
    assert(sys->avctx);
    if (sys->avctx->texture != NULL)
    {
        /* video context configured for opengl */
        free(sys);
        return VLC_EGENERIC;
    }

    const bool has_subtitle_surface =
        AWindowHandler_getANativeWindow(sys->awh, AWindow_Subtitles) != NULL;
    if (has_subtitle_surface)
    {
        int ret = subpicture_OpenDisplay(vd);
        if (ret != 0 && !vd->obj.force)
        {
            msg_Warn(vd, "cannot blend subtitle with an opaque surface, "
                         "trying next vout");
            free(sys);
            return VLC_EGENERIC;
        }
    }
    else
    {
        msg_Warn(vd, "using android display without subtitles support");
        sys->sub.window = NULL;
    }

    video_format_t rot_fmt;
    video_format_ApplyRotation(&rot_fmt, &sys->fmt);

    AWindowHandler_setVideoLayout(sys->awh, rot_fmt.i_width, rot_fmt.i_height,
                                  rot_fmt.i_visible_width,
                                  rot_fmt.i_visible_height,
                                  rot_fmt.i_sar_num, rot_fmt.i_sar_den);

    static const struct vlc_display_operations ops = {
        .close = Close,
        .prepare = Prepare,
        .display = Display,
        .control = Control,
        .set_viewpoint = NULL,
    };

    vd->ops = &ops;

    return VLC_SUCCESS;
}

vlc_module_begin()
    set_subcategory(SUBCAT_VIDEO_VOUT)
    set_description("Android video output")
    add_shortcut("android-display")
    add_obsolete_string("android-display-chroma") /* since 4.0.0 */
    set_callback_display(Open, 280)
    add_submodule ()
        set_capability("vout window", 0)
        set_callback(subpicture_window_Open)
        add_shortcut("android-subpicture")
vlc_module_end()
