/*****************************************************************************
 * utils.c: shared code between Android vout modules.
 *****************************************************************************
 * Copyright (C) 2014-2015 VLC authors and VideoLAN
 *
 * Authors: Felix Abecassis <felix.abecassis@gmail.com>
 *          Thomas Guillem <thomas@gllm.fr>
 *
 * 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 "utils.h"
#include "env.h"
#include <vlc_threads.h>

#include <dlfcn.h>
#include <jni.h>
#include <pthread.h>
#include <assert.h>

#include <EGL/egl.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

typedef void (*ptr_ASurfaceTexture_getTransformMatrix)
                                        (ASurfaceTexture *st, float mtx[16]);
typedef ASurfaceTexture* (*ptr_ASurfaceTexture_fromSurfaceTexture)
                                        (JNIEnv* env, jobject surfacetexture);
typedef ANativeWindow* (*ptr_ASurfaceTexture_acquireANativeWindow)
                                        (ASurfaceTexture *st);
typedef jobject (*ptr_ANativeWindow_toSurface)
                                        (JNIEnv *env, ANativeWindow *window);
typedef int (*ptr_ASurfaceTexture_attachToGLContext)
                                        (ASurfaceTexture *st, uint32_t texName);
typedef int (*ptr_ASurfaceTexture_updateTexImage)(ASurfaceTexture* st);
typedef int (*ptr_ASurfaceTexture_detachFromGLContext)(ASurfaceTexture *st);
typedef void (*ptr_ASurfaceTexture_release)(ASurfaceTexture *st);

/*
 * Android SurfaceTexture handle
 */
struct vlc_asurfacetexture_priv {
    struct vlc_asurfacetexture surface;

    /* Underlying SurfaceTexture objects  (JNI and NDK)*/
    jobject         jtexture;
    ASurfaceTexture *texture;

    /* Android API are loaded into an AWindowHandler instance. */
    struct AWindowHandler *awh;
};

struct ASurfaceTextureAPI
{
    float   transMat[16];

    jobject surfacetexture;
    ASurfaceTexture *p_ast;

    ptr_ASurfaceTexture_updateTexImage pf_updateTexImage;
    ptr_ASurfaceTexture_fromSurfaceTexture pf_astFromst;
    ptr_ASurfaceTexture_attachToGLContext pf_attachToGL;
    ptr_ASurfaceTexture_detachFromGLContext pf_detachFromGL;
    ptr_ASurfaceTexture_getTransformMatrix pf_getTransMatrix;
    ptr_ASurfaceTexture_release pf_releaseAst;
    ptr_ASurfaceTexture_acquireANativeWindow pf_acquireAnw;
    ptr_ANativeWindow_toSurface pf_anwToSurface;
};

struct vlc_android_jfields
{
    struct {
        jclass clazz;
        jmethodID getVideoSurface;
        jmethodID getSubtitlesSurface;
        jmethodID registerNative;
        jmethodID unregisterNative;
        jmethodID setVideoLayout;
    } AWindow;
    struct {
          jclass clazz;
          jmethodID init_i;
          jmethodID init_iz;
          jmethodID init_z;
          jmethodID updateTexImage;
          jmethodID release;
          jmethodID releaseTexImage;
          jmethodID getTransformMatrix;
          jmethodID detachFromGLContext;
          jmethodID attachToGLContext;
    } SurfaceTexture;
    struct {
        jclass clazz;
        jmethodID init_st;
    } Surface;
};

struct AWindowHandler
{
    JavaVM *p_jvm;
    jobject jobj;
    vlc_window_t *wnd;

    struct vlc_android_jfields jfields;

    struct {
        jobject jsurface;
        ANativeWindow *p_anw;
    } views[AWindow_Max];

    void *p_anw_dl;

    struct ASurfaceTextureAPI ndk_ast_api;
    bool b_has_ndk_ast_api;

    struct {
        awh_events_t cb;
    } event;
    int capabilities;

    struct {
        jfloatArray jtransform_mtx_array;
        jfloat *jtransform_mtx;
    } stex;

    struct aimage_reader_api ndk_air_api;
    void *ndk_air_lib;
    void *ndk_sync_lib;
    bool b_has_ndk_air_api;

    struct asurface_control_api ndk_asc_api;
    bool b_has_ndk_asc_api;
};

#define JNI_CALL(what, obj, method, ...) \
    (*p_env)->what(p_env, obj, p_awh->jfields.method, ##__VA_ARGS__)
#define JNI_ANWCALL(what, method, ...) \
    (*p_env)->what(p_env, p_awh->jobj, p_awh->jfields.AWindow.method, ##__VA_ARGS__)
#define JNI_STEXCALL(what, method, ...) \
    (*p_env)->what(p_env, p_awh->jobj, p_awh->jfields.AWindow.method, ##__VA_ARGS__)

static int
NDKSurfaceTexture_attachToGLContext(
        struct vlc_asurfacetexture *surface,
        uint32_t texName)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);
    return handle->awh->ndk_ast_api.pf_attachToGL(handle->texture, texName);
}

static void
NDKSurfaceTexture_detachFromGLContext(
        struct vlc_asurfacetexture *surface)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);
    handle->awh->ndk_ast_api.pf_detachFromGL(handle->texture);
}

static int
NDKSurfaceTexture_updateTexImage(
        struct vlc_asurfacetexture *surface,
        const float **pp_transform_mtx)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);

    /* ASurfaceTexture_updateTexImage can fail, for example if calling it
     * before having produced a new image. */
    if (handle->awh->ndk_ast_api.pf_updateTexImage(handle->texture))
        return VLC_EGENERIC;

    handle->awh->ndk_ast_api.pf_getTransMatrix(handle->texture,
                                               handle->awh->ndk_ast_api.transMat);
    *pp_transform_mtx = handle->awh->ndk_ast_api.transMat;
    return VLC_SUCCESS;
}

static void NDKSurfaceTexture_destroy(
        struct vlc_asurfacetexture *surface)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);

    JNIEnv *p_env = android_getEnvCommon(NULL, handle->awh->p_jvm, "SurfaceTexture");
    if (!p_env)
        return;

    (*p_env)->CallVoidMethod(p_env, handle->jtexture,
                             handle->awh->jfields.SurfaceTexture.release);

    if (handle->surface.window)
        ANativeWindow_release(handle->surface.window);

    if (handle->surface.jsurface)
        (*p_env)->DeleteGlobalRef(p_env, handle->surface.jsurface);

    handle->awh->ndk_ast_api.pf_releaseAst(handle->texture);
    (*p_env)->DeleteGlobalRef(p_env, handle->jtexture);

    free(handle);
}

static void
JNISurfaceTexture_releaseTexImage(struct vlc_asurfacetexture *surface);

static const struct vlc_asurfacetexture_operations NDKSurfaceAPI =
{
    .attach_to_gl_context = NDKSurfaceTexture_attachToGLContext,
    .update_tex_image = NDKSurfaceTexture_updateTexImage,
    .detach_from_gl_context = NDKSurfaceTexture_detachFromGLContext,
    .release_tex_image = JNISurfaceTexture_releaseTexImage,
    .destroy = NDKSurfaceTexture_destroy,
};

static int
JNISurfaceTexture_attachToGLContext(
        struct vlc_asurfacetexture *surface,
        uint32_t tex_name)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);

    JNIEnv *p_env = android_getEnvCommon(NULL, handle->awh->p_jvm, "SurfaceTexture");
    if (!p_env)
        return VLC_EGENERIC;

    (*p_env)->CallVoidMethod(p_env, handle->jtexture,
                             handle->awh->jfields.SurfaceTexture.attachToGLContext,
                             tex_name);

    return VLC_SUCCESS;
}

static void
JNISurfaceTexture_detachFromGLContext(
        struct vlc_asurfacetexture *surface)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);

    AWindowHandler *p_awh = handle->awh;
    JNIEnv *p_env = android_getEnvCommon(NULL, p_awh->p_jvm, "SurfaceTexture");
    if (!p_env)
        return;

    (*p_env)->CallVoidMethod(p_env, handle->jtexture,
                             handle->awh->jfields.SurfaceTexture.detachFromGLContext);

    if (handle->awh->stex.jtransform_mtx != NULL)
    {
        (*p_env)->ReleaseFloatArrayElements(p_env, handle->awh->stex.jtransform_mtx_array,
                                            handle->awh->stex.jtransform_mtx,
                                            JNI_ABORT);
        handle->awh->stex.jtransform_mtx = NULL;
    }
}

static int
JNISurfaceTexture_updateTexImage(
        struct vlc_asurfacetexture *surface,
        const float **pp_transform_mtx)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);

    AWindowHandler *p_awh = handle->awh;
    JNIEnv *p_env = android_getEnvCommon(NULL, p_awh->p_jvm, "SurfaceTexture");
    if (!p_env)
        return VLC_EGENERIC;

    if (handle->awh->stex.jtransform_mtx != NULL)
        (*p_env)->ReleaseFloatArrayElements(p_env, handle->awh->stex.jtransform_mtx_array,
                                            handle->awh->stex.jtransform_mtx,
                                            JNI_ABORT);

    (*p_env)->CallVoidMethod(p_env, handle->jtexture,
                             handle->awh->jfields.SurfaceTexture.updateTexImage);

    (*p_env)->CallVoidMethod(p_env, handle->jtexture,
                             handle->awh->jfields.SurfaceTexture.getTransformMatrix,
                             handle->awh->stex.jtransform_mtx_array);
    handle->awh->stex.jtransform_mtx = (*p_env)->GetFloatArrayElements(p_env,
                                        handle->awh->stex.jtransform_mtx_array, NULL);

    *pp_transform_mtx = handle->awh->stex.jtransform_mtx;
    return VLC_SUCCESS;
}

static void
JNISurfaceTexture_releaseTexImage(
        struct vlc_asurfacetexture *surface)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);

    JNIEnv *p_env = android_getEnvCommon(NULL, handle->awh->p_jvm, "SurfaceTexture");
    if (!p_env)
        return;

    (*p_env)->CallVoidMethod(p_env, handle->jtexture,
                             handle->awh->jfields.SurfaceTexture.releaseTexImage);
}

static void JNISurfaceTexture_destroy(
        struct vlc_asurfacetexture *surface)
{
    struct vlc_asurfacetexture_priv *handle =
        container_of(surface, struct vlc_asurfacetexture_priv, surface);

    JNIEnv *p_env = android_getEnvCommon(NULL, handle->awh->p_jvm, "SurfaceTexture");
    if (!p_env)
        return;

    (*p_env)->CallVoidMethod(p_env, handle->jtexture,
                             handle->awh->jfields.SurfaceTexture.release);

    if (handle->surface.window)
        ANativeWindow_release(handle->surface.window);
    if (handle->surface.jsurface)
        (*p_env)->DeleteGlobalRef(p_env, handle->surface.jsurface);

    free(handle);
}

static const struct vlc_asurfacetexture_operations JNISurfaceAPI =
{
    .attach_to_gl_context = JNISurfaceTexture_attachToGLContext,
    .update_tex_image = JNISurfaceTexture_updateTexImage,
    .detach_from_gl_context = JNISurfaceTexture_detachFromGLContext,
    .release_tex_image = JNISurfaceTexture_releaseTexImage,
    .destroy = JNISurfaceTexture_destroy,
};

static int
LoadNDKSurfaceTextureAPI(AWindowHandler *p_awh, void *p_library)
{
    p_awh->ndk_ast_api.pf_astFromst = (ptr_ASurfaceTexture_fromSurfaceTexture)
        dlsym(p_library, "ASurfaceTexture_fromSurfaceTexture");
    if (p_awh->ndk_ast_api.pf_astFromst == NULL) return VLC_EGENERIC;

    p_awh->ndk_ast_api.pf_updateTexImage = (ptr_ASurfaceTexture_updateTexImage)
        dlsym(p_library, "ASurfaceTexture_updateTexImage");
    if (p_awh->ndk_ast_api.pf_updateTexImage == NULL) return VLC_EGENERIC;

    p_awh->ndk_ast_api.pf_attachToGL = (ptr_ASurfaceTexture_attachToGLContext)
        dlsym(p_library, "ASurfaceTexture_attachToGLContext");
    if (p_awh->ndk_ast_api.pf_attachToGL == NULL) return VLC_EGENERIC;

    p_awh->ndk_ast_api.pf_detachFromGL = (ptr_ASurfaceTexture_detachFromGLContext)
        dlsym(p_library, "ASurfaceTexture_detachFromGLContext");
    if (p_awh->ndk_ast_api.pf_detachFromGL == NULL) return VLC_EGENERIC;

    p_awh->ndk_ast_api.pf_getTransMatrix = (ptr_ASurfaceTexture_getTransformMatrix)
        dlsym(p_library, "ASurfaceTexture_getTransformMatrix");
    if (p_awh->ndk_ast_api.pf_getTransMatrix == NULL) return VLC_EGENERIC;

    p_awh->ndk_ast_api.pf_releaseAst = (ptr_ASurfaceTexture_release)
        dlsym(p_library, "ASurfaceTexture_release");
    if (p_awh->ndk_ast_api.pf_releaseAst == NULL) return VLC_EGENERIC;

    p_awh->ndk_ast_api.pf_acquireAnw = (ptr_ASurfaceTexture_acquireANativeWindow)
        dlsym(p_library, "ASurfaceTexture_acquireANativeWindow");
    if (p_awh->ndk_ast_api.pf_acquireAnw == NULL) return VLC_EGENERIC;

    p_awh->ndk_ast_api.pf_anwToSurface = (ptr_ANativeWindow_toSurface)
        dlsym(p_library, "ANativeWindow_toSurface");
    if (p_awh->ndk_ast_api.pf_anwToSurface == NULL) return VLC_EGENERIC;

    return VLC_SUCCESS;
}

static int
LoadAimageReaderAPI(AWindowHandler *p_awh, void *p_android_lib)
{
    /* AHardwareBuffer functions are in libandroid.so, API 31+ */
#define LOAD(object, name) \
    p_awh->ndk_air_api.object.name = dlsym(p_android_lib, #object "_" #name); \
    if (p_awh->ndk_air_api.object.name == NULL) return VLC_EGENERIC

    LOAD(AHardwareBuffer, getId);
    LOAD(AHardwareBuffer, describe);

#undef LOAD

    void *p_library = dlopen("libmediandk.so", RTLD_NOW);
    if (p_library == NULL)
        return VLC_EGENERIC;

#define LOAD(object, name) \
    p_awh->ndk_air_api.object.name = dlsym(p_library, #object "_" #name); \
    if (p_awh->ndk_air_api.object.name == NULL) goto error

    /* API 29+ */
    LOAD(AImageReader, newWithUsage);
    LOAD(AImageReader, delete);
    LOAD(AImageReader, getWindow);
    LOAD(AImageReader, acquireNextImageAsync);
    LOAD(AImageReader, setImageListener);

    LOAD(AImage, deleteAsync);
    LOAD(AImage, getHardwareBuffer);
    LOAD(AImage, getTimestamp);
    LOAD(AImage, getCropRect);
    LOAD(AImage, getWidth);
    LOAD(AImage, getHeight);

#undef LOAD

    /* sync_merge is in libsync.so */
    p_awh->ndk_sync_lib = dlopen("libsync.so", RTLD_NOW);
    if (p_awh->ndk_sync_lib != NULL)
    {
        p_awh->ndk_air_api.sync_merge = dlsym(p_awh->ndk_sync_lib, "sync_merge");
        if (p_awh->ndk_air_api.sync_merge == NULL)
        {
            dlclose(p_awh->ndk_sync_lib);
            p_awh->ndk_sync_lib = NULL;
            goto error;
        }
    }

    p_awh->ndk_air_lib = p_library;
    return VLC_SUCCESS;

error:
    dlclose(p_library);
    return VLC_EGENERIC;
}

static int
LoadASurfaceControlAPI(AWindowHandler *p_awh, void *p_library)
{
#define LOAD(object, name) \
    p_awh->ndk_asc_api.object.name = dlsym(p_library, #object "_" #name); \
    if (p_awh->ndk_asc_api.object.name == NULL) return VLC_EGENERIC

    /* API 31+ functions */
    LOAD(ASurfaceTransaction, setCrop);
    LOAD(ASurfaceTransaction, setPosition);
    LOAD(ASurfaceTransaction, setBufferTransform);
    LOAD(ASurfaceTransaction, setScale);
    LOAD(ASurfaceTransactionStats, getPreviousReleaseFenceFd);

    /* API 29+ functions */
    LOAD(ASurfaceControl, createFromWindow);
    LOAD(ASurfaceControl, release);
    LOAD(ASurfaceTransaction, create);
    LOAD(ASurfaceTransaction, delete);
    LOAD(ASurfaceTransaction, apply);
    LOAD(ASurfaceTransaction, setBuffer);
    LOAD(ASurfaceTransaction, setVisibility);
    LOAD(ASurfaceTransaction, setBufferTransparency);
    LOAD(ASurfaceTransaction, setBufferDataSpace);
    LOAD(ASurfaceTransaction, setHdrMetadata_smpte2086);
    LOAD(ASurfaceTransaction, setHdrMetadata_cta861_3);
    LOAD(ASurfaceTransaction, setOnComplete);
    LOAD(ASurfaceTransaction, setDesiredPresentTime);

#undef LOAD

    return VLC_SUCCESS;
}

/*
 * Android NativeWindow (post android 2.3)
 */
static void
LoadNativeWindowAPI(AWindowHandler *p_awh)
{
    void *p_library = dlopen("libandroid.so", RTLD_NOW);
    if (!p_library)
        return;

    p_awh->b_has_ndk_ast_api = LoadNDKSurfaceTextureAPI(p_awh, p_library) == VLC_SUCCESS;
    p_awh->b_has_ndk_air_api = LoadAimageReaderAPI(p_awh, p_library) == VLC_SUCCESS;
    p_awh->b_has_ndk_asc_api = LoadASurfaceControlAPI(p_awh, p_library) == VLC_SUCCESS;
    p_awh->p_anw_dl = p_library;
}

static void
AndroidNativeWindow_onMouseEvent(JNIEnv*, jobject, jlong, jint, jint, jint, jint);
static void
AndroidNativeWindow_onWindowSize(JNIEnv*, jobject, jlong, jint, jint );

const JNINativeMethod jni_callbacks[] = {
    { "nativeOnMouseEvent", "(JIIII)V",
        (void *)AndroidNativeWindow_onMouseEvent },
    { "nativeOnWindowSize", "(JII)V",
        (void *)AndroidNativeWindow_onWindowSize },
};

static int
InitJNIFields(JNIEnv *env, vlc_object_t *p_obj, jobject *jobj, AWindowHandler *awh)
{
    static vlc_mutex_t lock = VLC_STATIC_MUTEX;
    int i_init_state = -1;
    int ret;
    jclass clazz;

    vlc_mutex_lock(&lock);

    if (i_init_state != -1)
        goto end;

#define CHECK_EXCEPTION(what, critical) do { \
    if( (*env)->ExceptionCheck(env) ) \
    { \
        msg_Err(p_obj, "%s failed", what); \
        (*env)->ExceptionClear(env); \
        if (critical) { \
            i_init_state = 0; \
            goto end; \
        } \
    } \
} while( 0 )
#define GET_METHOD(id_clazz, id, str, args, critical) do { \
    awh->jfields.id_clazz.id = (*env)->GetMethodID(\
            env, awh->jfields.id_clazz.clazz, (str), (args)); \
    CHECK_EXCEPTION("GetMethodID("str")", critical); \
} while( 0 )

    if (jobj)
    {
        clazz = (*env)->GetObjectClass(env, jobj);
        CHECK_EXCEPTION("AndroidNativeWindow clazz", true);

        awh->jfields.AWindow.clazz = (*env)->NewGlobalRef(env, clazz);
        (*env)->DeleteLocalRef(env, clazz);

        GET_METHOD(AWindow, getVideoSurface,
                   "getVideoSurface", "()Landroid/view/Surface;", true);
        GET_METHOD(AWindow, getSubtitlesSurface,
                   "getSubtitlesSurface", "()Landroid/view/Surface;", true);
        GET_METHOD(AWindow, registerNative,
                   "registerNative", "(J)I", true);
        GET_METHOD(AWindow, unregisterNative,
                   "unregisterNative", "()V", true);
        GET_METHOD(AWindow, setVideoLayout,
                   "setVideoLayout", "(IIIIII)V", true);

        if ((*env)->RegisterNatives(env, awh->jfields.AWindow.clazz, jni_callbacks, 2) < 0)
        {
            msg_Err(p_obj, "RegisterNatives failed");
            i_init_state = 0;
            goto end;
        }
    }

    awh->jfields.SurfaceTexture.clazz = NULL;
    awh->jfields.Surface.clazz = NULL;

    jclass surfacetexture_class =
        (*env)->FindClass(env, "android/graphics/SurfaceTexture");
    CHECK_EXCEPTION("SurfaceTexture clazz", true);
    awh->jfields.SurfaceTexture.clazz =
        (*env)->NewGlobalRef(env, surfacetexture_class);
    (*env)->DeleteLocalRef(env, surfacetexture_class);
    if (awh->jfields.SurfaceTexture.clazz == NULL)
        goto end;

    GET_METHOD(SurfaceTexture, init_i, "<init>", "(I)V", false);
    GET_METHOD(SurfaceTexture, init_iz, "<init>", "(IZ)V", false);
    GET_METHOD(SurfaceTexture, init_z, "<init>", "(Z)V", false);

    GET_METHOD(SurfaceTexture, updateTexImage,
               "updateTexImage", "()V", true);

    GET_METHOD(SurfaceTexture, getTransformMatrix,
               "getTransformMatrix", "([F)V", true);

    GET_METHOD(SurfaceTexture, release,
               "release", "()V", true);

    GET_METHOD(SurfaceTexture, releaseTexImage,
               "releaseTexImage", "()V", false);

    GET_METHOD(SurfaceTexture, attachToGLContext,
               "attachToGLContext", "(I)V", true);

    GET_METHOD(SurfaceTexture, detachFromGLContext,
               "detachFromGLContext", "()V", true);


    /* We cannot create any SurfaceTexture if we cannot load the SurfaceTexture
     * methods. */
    if (!awh->jfields.SurfaceTexture.init_i &&
        !awh->jfields.SurfaceTexture.init_iz &&
        !awh->jfields.SurfaceTexture.init_z)
        goto error;

    jclass surface_class = (*env)->FindClass(env, "android/view/Surface");
    CHECK_EXCEPTION("android/view/Surface class", true);

    awh->jfields.Surface.clazz = (*env)->NewGlobalRef(env, surface_class);
    (*env)->DeleteLocalRef(env, surface_class);
    if (awh->jfields.Surface.clazz == NULL)
        goto error;

    GET_METHOD(Surface, init_st, "<init>",
               "(Landroid/graphics/SurfaceTexture;)V", true);

#undef GET_METHOD
#undef CHECK_EXCEPTION

    i_init_state = 1;
end:
    ret = i_init_state == 1 ? VLC_SUCCESS : VLC_EGENERIC;
    if (ret)
        msg_Err(p_obj, "AndroidNativeWindow jni init failed" );
    vlc_mutex_unlock(&lock);
    return ret;

error:
    i_init_state = 0;
    if (awh->jfields.SurfaceTexture.clazz)
        (*env)->DeleteGlobalRef(env, awh->jfields.SurfaceTexture.clazz);
    awh->jfields.SurfaceTexture.clazz = NULL;

    if (awh->jfields.Surface.clazz)
        (*env)->DeleteGlobalRef(env, awh->jfields.Surface.clazz);
    awh->jfields.Surface.clazz = NULL;

    vlc_mutex_unlock(&lock);
    msg_Err(p_obj, "Failed to load jfields table");
    return VLC_EGENERIC;
}

static JNIEnv*
AWindowHandler_getEnv(AWindowHandler *p_awh)
{
    if (p_awh->p_jvm == NULL)
        return NULL;
    return android_getEnvCommon(NULL, p_awh->p_jvm, "AWindowHandler");
}

AWindowHandler *
AWindowHandler_new(vlc_object_t *obj, vlc_window_t *wnd, awh_events_t *p_events)
{
#define AWINDOW_REGISTER_FLAGS_SUCCESS 0x1
#define AWINDOW_REGISTER_FLAGS_HAS_VIDEO_LAYOUT_LISTENER 0x2

    AWindowHandler *p_awh;
    JNIEnv *p_env;
    JavaVM *p_jvm = var_InheritAddress(obj, "android-jvm");
    jobject jobj = var_InheritAddress(obj, "drawable-androidwindow");

    if (!p_jvm)
    {
        msg_Err(obj, "libvlc_media_player options not set");
        return NULL;
    }

    p_env = android_getEnvCommon(NULL, p_jvm, "AWindowHandler");
    if (!p_env)
    {
        msg_Err(obj, "can't get JNIEnv");
        return NULL;
    }

    p_awh = calloc(1, sizeof(AWindowHandler));
    if (!p_awh)
        return NULL;

    p_awh->p_jvm = p_jvm;
    p_awh->jobj = jobj ? (*p_env)->NewGlobalRef(p_env, jobj) : NULL;

    /* Zero the jfields structure before usage. */
    p_awh->jfields = (struct vlc_android_jfields) { .AWindow.clazz = NULL };

    p_awh->wnd = wnd;
    if (p_events)
        p_awh->event.cb = *p_events;

    jfloatArray jarray = (*p_env)->NewFloatArray(p_env, 16);
    if ((*p_env)->ExceptionCheck(p_env))
    {
        (*p_env)->ExceptionClear(p_env);
        free(p_awh);
        return NULL;
    }
    p_awh->stex.jtransform_mtx_array = (*p_env)->NewGlobalRef(p_env, jarray);
    (*p_env)->DeleteLocalRef(p_env, jarray);
    p_awh->stex.jtransform_mtx = NULL;

    if (InitJNIFields(p_env, obj, jobj, p_awh) != VLC_SUCCESS)
    {
        msg_Err(obj, "InitJNIFields failed");
        return NULL;
    }

    /* If we don't have window, don't register window handler. */
    jint flags = 0;
    if (p_events != NULL && wnd != NULL)
    {
        flags = JNI_ANWCALL(CallIntMethod, registerNative,
                                       (jlong)(intptr_t)p_awh);
        if ((flags & AWINDOW_REGISTER_FLAGS_SUCCESS) == 0)
        {
            msg_Err(obj, "AWindow already registered");
            if (p_awh->jobj)
                (*p_env)->DeleteGlobalRef(p_env, p_awh->jobj);
            (*p_env)->DeleteGlobalRef(p_env, p_awh->stex.jtransform_mtx_array);
            free(p_awh);
            return NULL;
        }
    }
    LoadNativeWindowAPI(p_awh);
    msg_Dbg(obj, "has_anw: %d has_ast: %d has_air: %d has_asc: %d",
            p_awh->p_anw_dl != NULL, p_awh->b_has_ndk_ast_api,
            p_awh->b_has_ndk_air_api, p_awh->b_has_ndk_asc_api);

    p_awh->capabilities = 0;

    if (wnd && flags & AWINDOW_REGISTER_FLAGS_HAS_VIDEO_LAYOUT_LISTENER)
        p_awh->capabilities |= AWH_CAPS_SET_VIDEO_LAYOUT;

    if (p_awh->capabilities & AWH_CAPS_SET_VIDEO_LAYOUT && p_events != NULL)
    {
        /* XXX: HACK: force mediacodec to setup an OpenGL surface when the vout
         * is forced to gles2. Indeed, setting b_has_video_layout_listener to
         * false will result in mediacodec using a SurfaceTexture for output.
         */
        char *vout_modules = var_InheritString(wnd, "vout");
        if (vout_modules
         && (strncmp(vout_modules, "gles2", sizeof("gles2") - 1) == 0
          || strncmp(vout_modules, "opengles2", sizeof("opengles2") - 1) == 0))
        {
            p_awh->capabilities &= ~AWH_CAPS_SET_VIDEO_LAYOUT;
        }
        free(vout_modules);
    }

    p_awh->capabilities |= AWH_CAPS_SURFACE_VIEW;

    return p_awh;
}

AWindowHandler *
AWindowHandler_newFromANWs(vlc_object_t *obj, ANativeWindow *video,
                           ANativeWindow *subtitle)
{
    (void) obj;
    AWindowHandler *awh = calloc(1, sizeof(AWindowHandler));
    if (!awh)
        return NULL;

    awh->p_jvm = NULL;
    awh->jobj = NULL;
    awh->jfields = (struct vlc_android_jfields) { .AWindow.clazz = NULL };
    awh->wnd = NULL;

    LoadNativeWindowAPI(awh);

    awh->views[AWindow_Video].p_anw = video;
    ANativeWindow_acquire(video);
    awh->capabilities = 0;

    awh->views[AWindow_Subtitles].p_anw = subtitle;
    if (subtitle != NULL)
        ANativeWindow_acquire(subtitle);

    return awh;
}

static void
AWindowHandler_releaseANativeWindowEnv(AWindowHandler *p_awh, JNIEnv *p_env,
                                       enum AWindow_ID id)
{
    assert(id < AWindow_Max);

    if (p_awh->views[id].p_anw)
    {
        ANativeWindow_release(p_awh->views[id].p_anw);
        p_awh->views[id].p_anw = NULL;
    }

    if (p_env != NULL && p_awh->views[id].jsurface)
    {
        (*p_env)->DeleteGlobalRef(p_env, p_awh->views[id].jsurface);
        p_awh->views[id].jsurface = NULL;
    }
}

void
AWindowHandler_destroy(AWindowHandler *p_awh)
{
    JNIEnv *p_env = AWindowHandler_getEnv(p_awh);

    if (p_env)
    {
        if (p_awh->jfields.SurfaceTexture.clazz)
            (*p_env)->DeleteGlobalRef(p_env, p_awh->jfields.SurfaceTexture.clazz);

        if (p_awh->jfields.Surface.clazz)
            (*p_env)->DeleteGlobalRef(p_env, p_awh->jfields.Surface.clazz);

        if (p_awh->jobj)
            JNI_ANWCALL(CallVoidMethod, unregisterNative);
    }

    AWindowHandler_releaseANativeWindowEnv(p_awh, p_env, AWindow_Video);
    AWindowHandler_releaseANativeWindowEnv(p_awh, p_env, AWindow_Subtitles);

    if (p_env != NULL)
    {
        if (p_awh->jobj)
            (*p_env)->DeleteGlobalRef(p_env, p_awh->jobj);
        (*p_env)->DeleteGlobalRef(p_env, p_awh->stex.jtransform_mtx_array);
    }

    if (p_awh->ndk_sync_lib)
        dlclose(p_awh->ndk_sync_lib);
    if (p_awh->ndk_air_lib)
        dlclose(p_awh->ndk_air_lib);
    if (p_awh->p_anw_dl)
        dlclose(p_awh->p_anw_dl);
    free(p_awh);
}

struct aimage_reader_api *
AWindowHandler_getAImageReaderApi(AWindowHandler *p_awh)
{
    return p_awh->b_has_ndk_air_api && p_awh->b_has_ndk_asc_api ?
           &p_awh->ndk_air_api : NULL;
}

struct asurface_control_api *
AWindowHandler_getASurfaceControlApi(AWindowHandler *p_awh)
{
    return p_awh->b_has_ndk_air_api && p_awh->b_has_ndk_asc_api ?
           &p_awh->ndk_asc_api : NULL;
}

static struct vlc_asurfacetexture_priv* CreateSurfaceTexture(
        AWindowHandler *p_awh, JNIEnv *p_env, bool single_buffer)
{
    /* Needed in case of old API, see comments below. */
    EGLDisplay display = EGL_NO_DISPLAY;
    EGLSurface surface = EGL_NO_SURFACE;
    EGLContext context = EGL_NO_CONTEXT;

    EGLContext current_context = EGL_NO_CONTEXT;
    EGLContext current_surface = EGL_NO_SURFACE;

    jobject surfacetexture;

    struct vlc_asurfacetexture_priv *handle = malloc(sizeof *handle);
    if (handle == NULL)
        return NULL;

    handle->awh = p_awh;
    handle->texture = NULL;
    handle->jtexture = NULL;
    handle->surface.jsurface = NULL;
    handle->surface.window = NULL;
    handle->surface.ops = NULL;

    /* API 26 */
    if (p_awh->jfields.SurfaceTexture.init_z == NULL)
        goto init_iz;

    msg_Dbg(p_awh->wnd, "Using SurfaceTexture constructor init_z");

    /* We can create a SurfaceTexture in detached mode directly */
    surfacetexture = (*p_env)->NewObject(p_env,
      p_awh->jfields.SurfaceTexture.clazz, p_awh->jfields.SurfaceTexture.init_z, single_buffer);

    if (surfacetexture == NULL)
        goto error;

    goto success;

init_iz:
    msg_Dbg(p_awh->wnd, "Initializing OpenGL context to create SurfaceTexture");
    /* Old Android APIs are constructing SurfaceTexture in an attached state
     * so we need a dummy context before detaching it, for any other
     * constructor than the previous one. That's crap.
     * At least, Android EGL display are using reference counting so we don't
     * need to care about display lifecycle. */
    display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    EGLint major, minor;
    if (eglInitialize(display, &major, &minor) != EGL_TRUE)
        goto error;

    current_context = eglGetCurrentContext();
    current_surface = eglGetCurrentSurface(EGL_READ);

    static const EGLint conf_attr[] = {
        EGL_RED_SIZE, 5,
        EGL_GREEN_SIZE, 5,
        EGL_BLUE_SIZE, 5,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
        EGL_NONE
    };
    EGLConfig cfgv[1];
    EGLint cfgc;

    if (eglChooseConfig(display, conf_attr, cfgv, 1, &cfgc) != EGL_TRUE
     || cfgc == 0)
    {
        goto error;
    }

    static const EGLint surface_attr[] =
    {
        EGL_WIDTH, 1,
        EGL_HEIGHT, 1,
        EGL_NONE,
    };

    surface = eglCreatePbufferSurface(display, cfgv[0], surface_attr);
    if (surface == EGL_NO_SURFACE)
        goto error;

    static const EGLint context_attr[] =
    {
        EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL_NONE,
    };

    context = eglCreateContext(display, cfgv[0], EGL_NO_CONTEXT,
                               context_attr);

    eglMakeCurrent(display, surface, surface, context);

    /* We'll need to reserve a name in the opengl context */
    GLuint texture;
    glGenTextures(1, &texture);

    /* API 19 */
    if (p_awh->jfields.SurfaceTexture.init_iz == NULL)
        goto init_i;

    msg_Dbg(p_awh->wnd, "Using SurfaceTexture constructor init_iz");
    surfacetexture = (*p_env)->NewObject(p_env,
      p_awh->jfields.SurfaceTexture.clazz, p_awh->jfields.SurfaceTexture.init_iz, texture, single_buffer);

    if (surfacetexture == NULL)
        goto error;

    goto success;

init_i:
    /* We can't get here without this constructor being loaded. */
    assert(p_awh->jfields.SurfaceTexture.init_i != NULL);
    msg_Dbg(p_awh->wnd, "Using SurfaceTexture constructor init_i");

    if (single_buffer)
        goto error;

    surfacetexture = (*p_env)->NewObject(p_env,
      p_awh->jfields.SurfaceTexture.clazz, p_awh->jfields.SurfaceTexture.init_i, texture);

    if (surfacetexture == NULL)
        goto error;

    /* fall-through success */
success:

    handle->jtexture = (*p_env)->NewGlobalRef(p_env, surfacetexture);
    (*p_env)->DeleteLocalRef(p_env, surfacetexture);

    if (handle->jtexture == NULL)
        goto error;

    if (p_awh->b_has_ndk_ast_api)
    {
        msg_Dbg(p_awh->wnd, "Using NDK API to init vlc_asurfacetexture");
        handle->texture = p_awh->ndk_ast_api.pf_astFromst(p_env, handle->jtexture);
        handle->surface.ops = &NDKSurfaceAPI;
        handle->surface.window = p_awh->ndk_ast_api.pf_acquireAnw(handle->texture);
        jobject jsurface = p_awh->ndk_ast_api.pf_anwToSurface(p_env, handle->surface.window);
        if (jsurface == NULL)
            goto error;

        handle->surface.jsurface = (*p_env)->NewGlobalRef(p_env, jsurface);
        (*p_env)->DeleteLocalRef(p_env, jsurface);

        if (handle->surface.jsurface == NULL)
            goto error;
    }
    else
    {
        msg_Dbg(p_awh->wnd, "Using JNI API to init vlc_asurfacetexture");
        handle->texture = NULL;
        /* Create Surface(SurfaceTexture), ie. producer side of the buffer
         * queue in Android. */
        jobject jsurface = (*p_env)->NewObject(p_env,
            p_awh->jfields.Surface.clazz, p_awh->jfields.Surface.init_st, handle->jtexture);
        if (!jsurface)
            goto error;
        handle->surface.jsurface = (*p_env)->NewGlobalRef(p_env, jsurface);
        (*p_env)->DeleteLocalRef(p_env, jsurface);
        if (handle->surface.jsurface == NULL)
            goto error;

        handle->surface.window =
            ANativeWindow_fromSurface(p_env, handle->surface.jsurface);
        handle->surface.ops = &JNISurfaceAPI;
    }

    msg_Dbg(p_awh->wnd, "Successfully initialized SurfaceTexture");

    if (display != EGL_NO_DISPLAY)
    {
        handle->surface.ops->detach_from_gl_context(&handle->surface);
        glDeleteTextures(1, &texture);
        eglMakeCurrent(display, current_surface, current_surface, current_context);
        eglDestroyContext(display, context);
        eglDestroySurface(display, surface);
        eglTerminate(display);
    }

    return handle;

error:
    if (context != EGL_NO_CONTEXT)
    {
        glDeleteTextures(1, &texture);
        eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
        eglDestroyContext(display, context);
    }

    if (surface != EGL_NO_SURFACE)
        eglDestroySurface(display, surface);

    if (display != EGL_NO_DISPLAY)
        eglTerminate(display);

    if (handle->surface.window != NULL)
        ANativeWindow_release(handle->surface.window);

    if (handle->surface.jsurface != NULL)
        (*p_env)->DeleteGlobalRef(p_env, handle->surface.jsurface);

    if (handle->texture != NULL)
        p_awh->ndk_ast_api.pf_releaseAst(p_awh->ndk_ast_api.p_ast);

    if (handle->jtexture != NULL)
        (*p_env)->DeleteGlobalRef(p_env, handle->jtexture);

    free(handle);
    return NULL;
}

struct vlc_asurfacetexture *
vlc_asurfacetexture_New(AWindowHandler *p_awh, bool single_buffer)
{
    if (p_awh->p_jvm == NULL)
        return NULL;
    JNIEnv *p_env = android_getEnvCommon(NULL, p_awh->p_jvm, "SurfaceTexture");
    struct vlc_asurfacetexture_priv *surfacetexture =
        CreateSurfaceTexture(p_awh, p_env, single_buffer);
    if (surfacetexture == NULL)
        return NULL;
    return &surfacetexture->surface;
}

static int
WindowHandler_NewSurfaceEnv(AWindowHandler *p_awh, JNIEnv *p_env,
                            enum AWindow_ID id)
{
    jobject jsurface;

    switch (id)
    {
        case AWindow_Video:
            if (p_awh->wnd == NULL) return VLC_EGENERIC;
            assert(p_awh->jfields.AWindow.getVideoSurface != NULL);
            jsurface = JNI_ANWCALL(CallObjectMethod, getVideoSurface);
            break;
        case AWindow_Subtitles:
            if (p_awh->wnd == NULL) return VLC_EGENERIC;
            assert(p_awh->jfields.AWindow.getSubtitlesSurface!= NULL);
            jsurface = JNI_ANWCALL(CallObjectMethod, getSubtitlesSurface);
            break;
        default:
            vlc_assert_unreachable();
    }
    if (!jsurface)
        return VLC_EGENERIC;

    p_awh->views[id].jsurface = (*p_env)->NewGlobalRef(p_env, jsurface);
    (*p_env)->DeleteLocalRef(p_env, jsurface);
    return VLC_SUCCESS;
}

ANativeWindow *
AWindowHandler_getANativeWindow(AWindowHandler *p_awh, enum AWindow_ID id)
{
    assert(id < AWindow_Max);

    if (p_awh->views[id].p_anw)
        return p_awh->views[id].p_anw;

    JNIEnv *p_env = AWindowHandler_getEnv(p_awh);
    if (!p_env)
        return NULL;

    if (WindowHandler_NewSurfaceEnv(p_awh, p_env, id) != VLC_SUCCESS)
        return NULL;
    assert(p_awh->views[id].jsurface != NULL);

    if (!p_awh->views[id].p_anw)
        p_awh->views[id].p_anw = ANativeWindow_fromSurface(p_env,
                                                    p_awh->views[id].jsurface);

    return p_awh->views[id].p_anw;
}

void AWindowHandler_releaseANativeWindow(AWindowHandler *p_awh,
                                         enum AWindow_ID id)
{
    JNIEnv *p_env = AWindowHandler_getEnv(p_awh);
    AWindowHandler_releaseANativeWindowEnv(p_awh, p_env, id);
}

static inline AWindowHandler *jlong_AWindowHandler(jlong handle)
{
    return (AWindowHandler *)(intptr_t) handle;
}

static void
AndroidNativeWindow_onMouseEvent(JNIEnv* env, jobject clazz, jlong handle,
                                 jint action, jint button, jint x, jint y)
{
    (void) env; (void) clazz;
    AWindowHandler *p_awh = jlong_AWindowHandler(handle);

    p_awh->event.cb.on_new_mouse_coords(p_awh->wnd,
        & (struct awh_mouse_coords) { action, button, x, y });
}

static void
AndroidNativeWindow_onWindowSize(JNIEnv* env, jobject clazz, jlong handle,
                                 jint width, jint height)
{
    (void) env; (void) clazz;
    AWindowHandler *p_awh = jlong_AWindowHandler(handle);

    if (width >= 0 && height >= 0)
        p_awh->event.cb.on_new_window_size(p_awh->wnd, width, height);
}

int
AWindowHandler_getCapabilities(AWindowHandler *p_awh)
{
    return p_awh->capabilities;
}

int
AWindowHandler_setVideoLayout(AWindowHandler *p_awh,
                              int i_display_width, int i_display_height,
                              const vout_display_place_t *place)
{
    assert(p_awh->capabilities & AWH_CAPS_SET_VIDEO_LAYOUT);
    JNIEnv *p_env = AWindowHandler_getEnv(p_awh);
    if (!p_env)
        return VLC_EGENERIC;

    vout_display_place_t zero = {0};
    if (place == NULL)
        place = &zero;

    JNI_ANWCALL(CallVoidMethod, setVideoLayout, i_display_width, i_display_height,
                (int)place->width, (int)place->height, place->x, place->y);
    return VLC_SUCCESS;
}

int
SurfaceTexture_attachToGLContext(struct vlc_asurfacetexture *st, uint32_t tex_name)
{
    return st->ops->attach_to_gl_context(st, tex_name);
}

void
SurfaceTexture_detachFromGLContext(struct vlc_asurfacetexture *st)
{
    st->ops->detach_from_gl_context(st);
}

int
SurfaceTexture_updateTexImage(struct vlc_asurfacetexture *st, const float **pp_transform_mtx)
{
    return st->ops->update_tex_image(st, pp_transform_mtx);
}

void
SurfaceTexture_releaseTexImage(struct vlc_asurfacetexture *st)
{
    return st->ops->release_tex_image(st);
}
