/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.gfx;

import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.util.ThreadUtils;

import android.util.Log;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;

/**
 * This class is a singleton that tracks EGL and compositor things over
 * the lifetime of Fennec running.
 * We only ever create one C++ compositor over Fennec's lifetime, but
 * most of the Java-side objects (e.g. LayerView, GeckoLayerClient,
 * LayerRenderer) can all get destroyed and re-created if the GeckoApp
 * activity is destroyed. This GLController is never destroyed, so that
 * the mCompositorCreated field and other state variables are always
 * accurate.
 */
public class GLController {
    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static final String LOGTAG = "GeckoGLController";

    private static GLController sInstance;

    private LayerView mView;
    private boolean mSurfaceValid;
    private int mWidth, mHeight;

    /* This is written by the compositor thread (while the UI thread
     * is blocked on it) and read by the UI thread. */
    private volatile boolean mCompositorCreated;

    private EGL10 mEGL;
    private EGLDisplay mEGLDisplay;
    private EGLConfig mEGLConfig;
    private EGLSurface mEGLSurface;

    private static final int LOCAL_EGL_OPENGL_ES2_BIT = 4;

    private static final int[] CONFIG_SPEC = {
        EGL10.EGL_RED_SIZE, 5,
        EGL10.EGL_GREEN_SIZE, 6,
        EGL10.EGL_BLUE_SIZE, 5,
        EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
        EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT,
        EGL10.EGL_NONE
    };

    private GLController() {
        // Here we start the GfxInfo thread, which will query OpenGL
        // system information for Gecko. This must be done early enough that the data will be
        // ready by the time it's needed to initialize the compositor (it takes about 100 ms
        // to obtain).
        GfxInfoThread.startThread();
    }

    static GLController getInstance(LayerView view) {
        if (sInstance == null) {
            sInstance = new GLController();
        }
        sInstance.mView = view;
        return sInstance;
    }

    synchronized void surfaceDestroyed() {
        ThreadUtils.assertOnUiThread();

        mSurfaceValid = false;
        mEGLSurface = null;

        // We need to coordinate with Gecko when pausing composition, to ensure
        // that Gecko never executes a draw event while the compositor is paused.
        // This is sent synchronously to make sure that we don't attempt to use
        // any outstanding Surfaces after we call this (such as from a
        // surfaceDestroyed notification), and to make sure that any in-flight
        // Gecko draw events have been processed.  When this returns, composition is
        // definitely paused -- it'll synchronize with the Gecko event loop, which
        // in turn will synchronize with the compositor thread.
        if (mCompositorCreated) {
            GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorPauseEvent());
        }
    }

    synchronized void surfaceChanged(int newWidth, int newHeight) {
        ThreadUtils.assertOnUiThread();

        mWidth = newWidth;
        mHeight = newHeight;

        if (mSurfaceValid) {
            // We need to make this call even when the compositor isn't currently
            // paused (e.g. during an orientation change), to make the compositor
            // aware of the changed surface.
            resumeCompositor(mWidth, mHeight);
            return;
        }
        mSurfaceValid = true;

        // If we get here, we supposedly have a valid surface where previously we
        // did not. So we're going to create the window surface and hold on to it
        // until the compositor comes asking for it. However, we can't call
        // eglCreateWindowSurface right away because the UI thread isn't *actually*
        // done setting up - for some reason Android will send us a surfaceChanged
        // notification before the surface is actually ready. So, we need to do the
        // call to eglCreateWindowSurface in a runnable posted back to the UI thread
        // that will run once this call unwinds all the way out and Android finishes
        // doing its thing.

        mView.post(new Runnable() {
            @Override
            public void run() {
                // If we haven't yet created the compositor, and the GfxInfoThread
                // isn't done it's data gathering activities, then postpone creating
                // the compositor a little bit more. Don't block though, since this is
                // the UI thread we're running on.
                if (!mCompositorCreated && !GfxInfoThread.hasData()) {
                    mView.postDelayed(this, 1);
                    return;
                }

                try {
                    // Re-check mSurfaceValid in case the surface was destroyed between
                    // where we set it to true above and this runnable getting run.
                    // If mSurfaceValid is still true, try to create mEGLSurface. If
                    // mSurfaceValid is false, leave mEGLSurface as null. So at the end
                    // of this block mEGLSurface will be null (or EGL_NO_SURFACE) if
                    // eglCreateWindowSurface failed or if mSurfaceValid changed to false.
                    if (mSurfaceValid) {
                        if (mEGL == null) {
                            initEGL();
                        }

                        mEGLSurface = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, mView.getNativeWindow(), null);
                    }
                } catch (Exception e) {
                    Log.e(LOGTAG, "Unable to create window surface", e);
                }
                if (mEGLSurface == null || mEGLSurface == EGL10.EGL_NO_SURFACE) {
                    mSurfaceValid = false;
                    mEGLSurface = null; // normalize EGL_NO_SURFACE to null to simplify later checks
                    Log.e(LOGTAG, "EGL window surface could not be created: " + getEGLError());
                    return;
                }
                // At this point mSurfaceValid is true and mEGLSurface is a valid surface. Try
                // to create the compositor if it hasn't been created already.
                createCompositor();
            }
        });
    }

    void createCompositor() {
        ThreadUtils.assertOnUiThread();

        if (mCompositorCreated) {
            // If the compositor has already been created, just resume it instead. We don't need
            // to block here because if the surface is destroyed before the compositor grabs it,
            // we can handle that gracefully (i.e. the compositor will remain paused).
            resumeCompositor(mWidth, mHeight);
            return;
        }

        // Only try to create the compositor if we have a valid surface and gecko is up. When these
        // two conditions are satisfied, we can be relatively sure that the compositor creation will
        // happen without needing to block anyhwere. Do it with a sync gecko event so that the
        // android doesn't have a chance to destroy our surface in between.
        if (mEGLSurface != null && GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
            GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorCreateEvent(mWidth, mHeight));
        }
    }

    void compositorCreated() {
        // This is invoked on the compositor thread, while the java UI thread
        // is blocked on the gecko sync event in createCompositor() above
        mCompositorCreated = true;
    }

    public boolean hasValidSurface() {
        return mSurfaceValid;
    }

    private void initEGL() {
        mEGL = (EGL10)EGLContext.getEGL();

        mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
            throw new GLControllerException("eglGetDisplay() failed");
        }

        mEGLConfig = chooseConfig();
    }

    private EGLConfig chooseConfig() {
        int[] numConfigs = new int[1];
        if (!mEGL.eglChooseConfig(mEGLDisplay, CONFIG_SPEC, null, 0, numConfigs) ||
                numConfigs[0] <= 0) {
            throw new GLControllerException("No available EGL configurations " +
                                            getEGLError());
        }

        EGLConfig[] configs = new EGLConfig[numConfigs[0]];
        if (!mEGL.eglChooseConfig(mEGLDisplay, CONFIG_SPEC, configs, numConfigs[0], numConfigs)) {
            throw new GLControllerException("No EGL configuration for that specification " +
                                            getEGLError());
        }

        // Select the first 565 RGB configuration.
        int[] red = new int[1], green = new int[1], blue = new int[1];
        for (EGLConfig config : configs) {
            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_RED_SIZE, red);
            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_GREEN_SIZE, green);
            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_BLUE_SIZE, blue);
            if (red[0] == 5 && green[0] == 6 && blue[0] == 5) {
                return config;
            }
        }

        throw new GLControllerException("No suitable EGL configuration found");
    }

    /* This function is invoked by JNI on the compositor thread */
    private EGLSurface provideEGLSurface() {
        return mEGLSurface;
    }

    private String getEGLError() {
        return "Error " + (mEGL == null ? "(no mEGL)" : mEGL.eglGetError());
    }

    void resumeCompositor(int width, int height) {
        // Asking Gecko to resume the compositor takes too long (see
        // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
        // resume the compositor directly. We still need to inform Gecko about
        // the compositor resuming, so that Gecko knows that it can now draw.
        // It is important to not notify Gecko until after the compositor has
        // been resumed, otherwise Gecko may send updates that get dropped.
        if (mCompositorCreated) {
            GeckoAppShell.scheduleResumeComposition(width, height);
            GeckoAppShell.sendEventToGecko(GeckoEvent.createCompositorResumeEvent());
        }
    }

    public static class GLControllerException extends RuntimeException {
        public static final long serialVersionUID = 1L;

        GLControllerException(String e) {
            super(e);
        }
    }
}
