/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

//
// WARNING -------------------------- WARNING
// This code meant to be used for testing purposes only. It is not production
// level quality.
// Use on your own risk !!
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include "egl_dispatch.h"
#include "egl_ftable.h"
#include <cutils/log.h>
#include "ServerConnection.h"
#include "ThreadInfo.h"
#include <pthread.h>
#include "gl_wrapper_context.h"
#include "gl2_wrapper_context.h"

#define GLES_EMUL_TARGETS_FILE "/system/etc/gles_emul.cfg"
// implementation libraries;
#define GLESv1_enc_LIB "/system/lib/libGLESv1_enc.so"
#define GLESv2_enc_LIB "/system/lib/libGLESv2_enc.so"
#define GLES_android_LIB "/system/lib/egl/libGLES_android.so"
// driver libraries;
#define GLESv1_DRIVER "/system/lib/egl/libGLESv1_CM_emul.so"
#define GLESv2_DRIVER "/system/lib/egl/libGLESv2_emul.so"


static struct egl_dispatch *s_dispatch = NULL;
pthread_once_t dispatchTablesInitialized = PTHREAD_ONCE_INIT;

static bool s_needEncode = false;

static gl_wrapper_context_t *g_gl_dispatch = NULL;
static gl2_wrapper_context_t *g_gl2_dispatch = NULL;

template <class T>
int initApi(const char *driverLibName, const char *implLibName, T **dispatchTable, T *(*accessor)())
{
    void *driverLib = dlopen(driverLibName, RTLD_NOW | RTLD_LOCAL);
    if (driverLib == NULL) {
        ALOGE("failed to load %s : %s\n", driverLibName, dlerror());
        return -1;
    }

    typedef T *(*createFcn_t)(void *, T *(*accessor)());
    createFcn_t createFcn;
    createFcn = (createFcn_t) dlsym(driverLib, "createFromLib");
    if (createFcn == NULL) {
        ALOGE("failed to load createFromLib constructor function\n");
        return -1;
    }

    void *implLib = dlopen(implLibName, RTLD_NOW | RTLD_LOCAL);
    if (implLib == NULL) {
        ALOGE("couldn't open %s", implLibName);
        return -2;
    }
    *dispatchTable = createFcn(implLib, accessor);
    if (*dispatchTable == NULL) {
        return -3;
    }

    // XXX - we do close the impl library since it doesn't have data, as far as we concern.
    dlclose(implLib);

    // XXX - we do not dlclose the driver library, so its not initialized when
    // later loaded by android - is this required?
    ALOGD("loading %s into %s complete\n", implLibName, driverLibName);
    return 0;

}

static gl_wrapper_context_t *getGLContext()
{
    return g_gl_dispatch;
}

static gl2_wrapper_context_t *getGL2Context()
{
    return g_gl2_dispatch;
}

const char *getProcName()
{
    static constexpr size_t kMaxProcessNameLength = 100;
    static const char procname[kMaxProcessNameLength]{0};

    int rc = pthread_getname_np(pthread_self(), procname, kMaxProcessNameLength);

    if (rc == 0) {
        return procname;
    }

    return nullptr;
}



bool isNeedEncode()
{
    const char *procname = getProcName();
    if (procname == NULL) return false;
    ALOGD("isNeedEncode? for %s\n", procname);
    // check on our whitelist
    FILE *fp = fopen(GLES_EMUL_TARGETS_FILE, "rt");
    if (fp == NULL) {
        ALOGE("couldn't open %s\n", GLES_EMUL_TARGETS_FILE);
        return false;
    }

    char line[100];
    bool found = false;
    size_t  procnameLen = strlen(procname);

    while (fgets(line, sizeof(line), fp) != NULL) {
        if (strlen(line) >= procnameLen &&
            !strncmp(procname, line, procnameLen)) {
            char c = line[procnameLen];
            if (c == '\0' || c == ' ' || c == '\t' || c == '\n') {
                found = true;
                ALOGD("should use encoder for %s\n", procname);
                break;
            }
        }
    }
    fclose(fp);
    return found;
}

void initDispatchTables()
{
    //
    // Load our back-end implementation of EGL/GLES
    //
    ALOGD("Loading egl dispatch for %s\n", getProcName());

    void *gles_android = dlopen("/system/lib/egl/libGLES_android.so", RTLD_NOW | RTLD_LOCAL);
    if (!gles_android) {
        fprintf(stderr,"FATAL ERROR: Could not load libGLES_android lib\n");
        exit(-1);
    }

    //
    // Load back-end EGL implementation library
    //
    s_dispatch = create_egl_dispatch( gles_android );
    if (!s_dispatch) {
        fprintf(stderr,"FATAL ERROR: Could not create egl dispatch\n");
        exit(-1);
    }

    //
    // initialize gles
    //
    s_needEncode = isNeedEncode();
    void *gles_encoder = NULL;
    if (s_needEncode) {
        // initialize a connection to the server, and the GLESv1/v2 encoders;
        ServerConnection * connection = ServerConnection::s_getServerConnection();
        if (connection == NULL) {
            ALOGE("couldn't create server connection\n");
            s_needEncode = false;
        }
    }

    // init dispatch tabels for GLESv1 & GLESv2
    if (s_needEncode) {
        // XXX - we do not check the retrun value because there isn't much we can do here on failure.

        if (initApi<gl_wrapper_context_t>(GLESv1_DRIVER, GLESv1_enc_LIB, &g_gl_dispatch, getGLContext) < 0) {
            // fallback to android on faluire
            s_needEncode = false;
        } else {
            initApi<gl2_wrapper_context_t>(GLESv2_DRIVER, GLESv2_enc_LIB, &g_gl2_dispatch, getGL2Context);
        }
    }

    if (!s_needEncode) {
        ALOGD("Initializing native opengl for %s\n", getProcName());
        initApi<gl_wrapper_context_t>(GLESv1_DRIVER, GLES_android_LIB, &g_gl_dispatch, getGLContext);
        // try to initialize gl2 from GLES, though its probably going to fail
        initApi<gl2_wrapper_context_t>(GLESv2_DRIVER, GLES_android_LIB, &g_gl2_dispatch, getGL2Context);
    }
}

static struct egl_dispatch *getDispatch()
{
    pthread_once(&dispatchTablesInitialized, initDispatchTables);
    return s_dispatch;
}

__eglMustCastToProperFunctionPointerType eglGetProcAddress(const char *procname)
{

    // search in EGL function table
    for (int i=0; i<egl_num_funcs; i++) {
        if (!strcmp(egl_funcs_by_name[i].name, procname)) {
            return (__eglMustCastToProperFunctionPointerType)egl_funcs_by_name[i].proc;
        }
    }

    // we do not support eglGetProcAddress for GLESv1 & GLESv2. The loader
    // should be able to find this function through dynamic loading.
    return NULL;
}

////////////////  Path through functions //////////

EGLint eglGetError()
{
    return getDispatch()->eglGetError();
}

EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id)
{
    return getDispatch()->eglGetDisplay(display_id);
}

EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor)
{
    return getDispatch()->eglInitialize(dpy, major, minor);
}

EGLBoolean eglTerminate(EGLDisplay dpy)
{
    return getDispatch()->eglTerminate(dpy);
}

const char* eglQueryString(EGLDisplay dpy, EGLint name)
{
    return getDispatch()->eglQueryString(dpy, name);
}

EGLBoolean eglGetConfigs(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config)
{
    return getDispatch()->eglGetConfigs(dpy, configs, config_size, num_config);
}

static EGLint * filter_es2_bit(const EGLint *attrib_list, bool *isES2)
{
    if (attrib_list == NULL) {
        if (isES2 != NULL) *isES2 = false;
        return NULL;
    }

    EGLint *attribs = NULL;
    int nAttribs = 0;
    while(attrib_list[nAttribs] != EGL_NONE) nAttribs++;
    nAttribs++;

    attribs = new EGLint[nAttribs];
    memcpy(attribs, attrib_list, nAttribs * sizeof(EGLint));
    if (isES2 != NULL) *isES2 = false;

    // scan the attribute list for ES2 request and replace with ES1.
    for (int i = 0; i < nAttribs; i++) {
        if (attribs[i] == EGL_RENDERABLE_TYPE) {
            if (attribs[i + 1] & EGL_OPENGL_ES2_BIT) {
                attribs[i + 1] &= ~EGL_OPENGL_ES2_BIT;
                attribs[i + 1] |= EGL_OPENGL_ES_BIT;
                ALOGD("removing ES2 bit 0x%x\n", attribs[i + 1]);
                if (isES2 != NULL) *isES2 = true;
            }
        }
    }
    return attribs;
}

EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config)
{
    EGLBoolean res;
    if (s_needEncode) {
        EGLint *attribs = filter_es2_bit(attrib_list, NULL);
        res =  getDispatch()->eglChooseConfig(dpy,
                                              attribs,
                                              configs,
                                              config_size,
                                              num_config);
        ALOGD("eglChooseConfig: %d configs found\n", *num_config);
        if (*num_config == 0 && attribs != NULL) {
            ALOGD("requested attributes:\n");
            for (int i = 0; attribs[i] != EGL_NONE; i++) {
                ALOGD("%d: 0x%x\n", i, attribs[i]);
            }
        }

        delete attribs;
    } else {
        res = getDispatch()->eglChooseConfig(dpy, attrib_list, configs, config_size, num_config);
    }
    return res;
}

EGLBoolean eglGetConfigAttrib(EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint *value)
{
    if (s_needEncode && attribute == EGL_RENDERABLE_TYPE) {
        *value = EGL_OPENGL_ES_BIT | EGL_OPENGL_ES2_BIT;
        return EGL_TRUE;
    } else {
        return getDispatch()->eglGetConfigAttrib(dpy, config, attribute, value);
    }
}

EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list)
{
    EGLSurface surface =  getDispatch()->eglCreateWindowSurface(dpy, config, win, attrib_list);
    if (surface != EGL_NO_SURFACE) {
        ServerConnection *server;
        if (s_needEncode && (server = ServerConnection::s_getServerConnection()) != NULL) {
            server->utEnc()->createSurface(server->utEnc(), getpid(), (uint32_t)surface);
        }
    }
    return surface;
}

EGLSurface eglCreatePbufferSurface(EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list)
{
    EGLSurface surface =  getDispatch()->eglCreatePbufferSurface(dpy, config, attrib_list);
    if (surface != EGL_NO_SURFACE) {
        ServerConnection *server;
        if (s_needEncode && (server = ServerConnection::s_getServerConnection()) != NULL) {
            server->utEnc()->createSurface(server->utEnc(), getpid(), (uint32_t)surface);
        }
    }
    return surface;
}

EGLSurface eglCreatePixmapSurface(EGLDisplay dpy, EGLConfig config, EGLNativePixmapType pixmap, const EGLint *attrib_list)
{
    EGLSurface surface =  getDispatch()->eglCreatePixmapSurface(dpy, config, pixmap, attrib_list);
    if (surface != EGL_NO_SURFACE) {
        ServerConnection *server;
        if (s_needEncode && (server = ServerConnection::s_getServerConnection()) != NULL) {
            server->utEnc()->createSurface(server->utEnc(), getpid(), (uint32_t)surface);
        }
    }
    return surface;
}

EGLBoolean eglDestroySurface(EGLDisplay dpy, EGLSurface surface)
{
    EGLBoolean res =  getDispatch()->eglDestroySurface(dpy, surface);
    if (res && surface != EGL_NO_SURFACE) {
        ServerConnection *server;
        if (s_needEncode && (server = ServerConnection::s_getServerConnection()) != NULL) {
            server->utEnc()->destroySurface(server->utEnc(), getpid(), (uint32_t)surface);
        }
    }
    return res;
}

EGLBoolean eglQuerySurface(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint *value)
{
    EGLBoolean res = getDispatch()->eglQuerySurface(dpy, surface, attribute, value);
    if (res && attribute == EGL_RENDERABLE_TYPE) {
        *value |= EGL_OPENGL_ES2_BIT;
    }
    return res;
}

EGLBoolean eglBindAPI(EGLenum api)
{
    return getDispatch()->eglBindAPI(api);
}

EGLenum eglQueryAPI()
{
    return getDispatch()->eglQueryAPI();
}

EGLBoolean eglWaitClient()
{
    return getDispatch()->eglWaitClient();
}

EGLBoolean eglReleaseThread()
{
    return getDispatch()->eglReleaseThread();
}

EGLSurface eglCreatePbufferFromClientBuffer(EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, const EGLint *attrib_list)
{
    return getDispatch()->eglCreatePbufferFromClientBuffer(dpy, buftype, buffer, config, attrib_list);
}

EGLBoolean eglSurfaceAttrib(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value)
{
    return getDispatch()->eglSurfaceAttrib(dpy, surface, attribute, value);
}

EGLBoolean eglBindTexImage(EGLDisplay dpy, EGLSurface surface, EGLint buffer)
{
    return getDispatch()->eglBindTexImage(dpy, surface, buffer);
}

EGLBoolean eglReleaseTexImage(EGLDisplay dpy, EGLSurface surface, EGLint buffer)
{
    return getDispatch()->eglReleaseTexImage(dpy, surface, buffer);
}

EGLBoolean eglSwapInterval(EGLDisplay dpy, EGLint interval)
{
    return getDispatch()->eglSwapInterval(dpy, interval);
}

EGLContext eglCreateContext(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list)
{

    EGLContext share = share_context;
    if (share) share = ((EGLWrapperContext *)share_context)->aglContext;

    // check if are ES2, and convert it to ES1.
    int nAttribs = 0;
    if (attrib_list != NULL) {
        while(attrib_list[nAttribs] != EGL_NONE) {
            nAttribs++;
        }
        nAttribs++;
    }

    EGLint *attrib = NULL;
    if (nAttribs > 0) {
        attrib = new EGLint[nAttribs];
        memcpy(attrib, attrib_list, nAttribs * sizeof(EGLint));
    }

    int  version  = 1;
    for (int i = 0; i < nAttribs; i++) {
        if (attrib[i] == EGL_CONTEXT_CLIENT_VERSION &&
            attrib[i + 1] == 2) {
            version = 2;
            attrib[i + 1] = 1; // replace to version 1
        }
    }

    EGLContext ctx =  getDispatch()->eglCreateContext(dpy, config, share, attrib);
    delete[] attrib;
    EGLWrapperContext *wctx = new EGLWrapperContext(ctx, version);
    if (ctx != EGL_NO_CONTEXT) {
        ServerConnection *server;
        if (s_needEncode && (server = ServerConnection::s_getServerConnection()) != NULL) {
            wctx->clientState = new GLClientState();
            server->utEnc()->createContext(server->utEnc(), getpid(),
                                           (uint32_t)wctx,
                                           (uint32_t)(share_context == EGL_NO_CONTEXT ? 0 : share_context), wctx->version);
        }
    }
    return (EGLContext)wctx;
}

EGLBoolean eglDestroyContext(EGLDisplay dpy, EGLContext ctx)
{
    EGLWrapperContext *wctx = (EGLWrapperContext *)ctx;
    EGLBoolean res = EGL_FALSE;

    if (ctx && ctx != EGL_NO_CONTEXT) {
        res = getDispatch()->eglDestroyContext(dpy, wctx->aglContext);
        if (res) {
            EGLThreadInfo *ti = getEGLThreadInfo();
            ServerConnection *server;
            if (s_needEncode && (server = ServerConnection::s_getServerConnection())) {
                server->utEnc()->destroyContext(ti->serverConn->utEnc(), getpid(), (uint32_t)ctx);
            }
            if (ti->currentContext == wctx) ti->currentContext = NULL;
            delete wctx;
        }
    }

    return res;
}

EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx)
{
    EGLWrapperContext *wctx = (EGLWrapperContext *)ctx;
    EGLContext aglContext = (ctx == EGL_NO_CONTEXT ? EGL_NO_CONTEXT : wctx->aglContext);
    EGLThreadInfo *ti = getEGLThreadInfo();
    EGLBoolean res = getDispatch()->eglMakeCurrent(dpy, draw, read, aglContext);
    if (res ) {
        // NOTE - we do get a pointer to the server connection, (rather then using ti->serverConn)
        // for cases that this is the first egl call of the current thread.

        ServerConnection *server;
        if (s_needEncode && (server = ServerConnection::s_getServerConnection())) {
            server->utEnc()->makeCurrentContext(server->utEnc(), getpid(),
                                                (uint32_t) (draw == EGL_NO_SURFACE ? 0 : draw),
                                                (uint32_t) (read == EGL_NO_SURFACE ? 0 : read),
                                                (uint32_t) (ctx == EGL_NO_CONTEXT ? 0 : ctx));
            server->glEncoder()->setClientState( wctx ? wctx->clientState : NULL );
            server->gl2Encoder()->setClientState( wctx ? wctx->clientState : NULL );
        }

        // set current context in our thread info
        ti->currentContext = wctx;
    }
    return res;

}

EGLContext eglGetCurrentContext()
{
    EGLThreadInfo *ti = getEGLThreadInfo();
    return (ti->currentContext ? ti->currentContext : EGL_NO_CONTEXT);
}

EGLSurface eglGetCurrentSurface(EGLint readdraw)
{
    return getDispatch()->eglGetCurrentSurface(readdraw);
}

EGLDisplay eglGetCurrentDisplay()
{
    return getDispatch()->eglGetCurrentDisplay();
}

EGLBoolean eglQueryContext(EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint *value)
{
    EGLWrapperContext *wctx = (EGLWrapperContext *)ctx;
    if (wctx) {
        if (attribute == EGL_CONTEXT_CLIENT_VERSION) {
            *value = wctx->version;
            return EGL_TRUE;
        } else {
            return getDispatch()->eglQueryContext(dpy, wctx->aglContext, attribute, value);
        }
    }
    else {
        return EGL_BAD_CONTEXT;
    }
}

EGLBoolean eglWaitGL()
{
    return getDispatch()->eglWaitGL();
}

EGLBoolean eglWaitNative(EGLint engine)
{
    return getDispatch()->eglWaitNative(engine);
}

EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface)
{
    ServerConnection *server;
    if (s_needEncode && (server = ServerConnection::s_getServerConnection()) != NULL) {
        server->utEnc()->swapBuffers(server->utEnc(), getpid(), (uint32_t)surface);
        server->glEncoder()->flush();
        server->gl2Encoder()->flush();
        return 1;
    }
    return getDispatch()->eglSwapBuffers(dpy, surface);
}

EGLBoolean eglCopyBuffers(EGLDisplay dpy, EGLSurface surface, EGLNativePixmapType target)
{
    return getDispatch()->eglCopyBuffers(dpy, surface, target);
}

EGLBoolean eglLockSurfaceKHR(EGLDisplay display, EGLSurface surface, const EGLint *attrib_list)
{
    return getDispatch()->eglLockSurfaceKHR(display, surface, attrib_list);
}

EGLBoolean eglUnlockSurfaceKHR(EGLDisplay display, EGLSurface surface)
{
    return getDispatch()->eglUnlockSurfaceKHR(display, surface);
}

EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list)
{
    EGLWrapperContext *wctx = (EGLWrapperContext *)ctx;
    EGLContext aglContext = (wctx ? wctx->aglContext : EGL_NO_CONTEXT);
    return getDispatch()->eglCreateImageKHR(dpy, aglContext, target, buffer, attrib_list);
}

EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image)
{
    return getDispatch()->eglDestroyImageKHR(dpy, image);
}

EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint *attrib_list)
{
    return getDispatch()->eglCreateSyncKHR(dpy, type, attrib_list);
}

EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync)
{
    return getDispatch()->eglDestroySyncKHR(dpy, sync);
}

EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout)
{
    return getDispatch()->eglClientWaitSyncKHR(dpy, sync, flags, timeout);
}

EGLBoolean eglSignalSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLenum mode)
{
    return getDispatch()->eglSignalSyncKHR(dpy, sync, mode);
}

EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint *value)
{
    return getDispatch()->eglGetSyncAttribKHR(dpy, sync, attribute, value);
}

EGLBoolean eglSetSwapRectangleANDROID(EGLDisplay dpy, EGLSurface draw, EGLint left, EGLint top, EGLint width, EGLint height)
{
    return getDispatch()->eglSetSwapRectangleANDROID(dpy, draw, left, top, width, height);
}