/*
 * Copyright (C) 2014 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.
 */

#define LOG_TAG "Camera2-Legacy-PerfMeasurement-JNI"
#include <utils/Log.h>
#include <utils/Errors.h>
#include <utils/Trace.h>
#include <utils/Vector.h>

#include "jni.h"
#include "JNIHelp.h"
#include "core_jni_helpers.h"

#include <ui/GraphicBuffer.h>
#include <system/window.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

using namespace android;

// fully-qualified class name
#define PERF_MEASUREMENT_CLASS_NAME "android/hardware/camera2/legacy/PerfMeasurement"

/** GL utility methods copied from com_google_android_gles_jni_GLImpl.cpp */

// Check if the extension at the head of pExtensions is pExtension. Note that pExtensions is
// terminated by either 0 or space, while pExtension is terminated by 0.

static bool
extensionEqual(const GLubyte* pExtensions, const GLubyte* pExtension) {
    while (true) {
        char a = *pExtensions++;
        char b = *pExtension++;
        bool aEnd = a == '\0' || a == ' ';
        bool bEnd = b == '\0';
        if (aEnd || bEnd) {
            return aEnd == bEnd;
        }
        if (a != b) {
            return false;
        }
    }
}

static const GLubyte*
nextExtension(const GLubyte* pExtensions) {
    while (true) {
        char a = *pExtensions++;
        if (a == '\0') {
            return pExtensions-1;
        } else if ( a == ' ') {
            return pExtensions;
        }
    }
}

static bool
checkForExtension(const GLubyte* pExtensions, const GLubyte* pExtension) {
    for (; *pExtensions != '\0'; pExtensions = nextExtension(pExtensions)) {
        if (extensionEqual(pExtensions, pExtension)) {
            return true;
        }
    }
    return false;
}

/** End copied GL utility methods */

bool checkGlError(JNIEnv* env) {
    int error;
    if ((error = glGetError()) != GL_NO_ERROR) {
        jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
                "GLES20 error: 0x%d", error);
        return true;
    }
    return false;
}

/**
 * Asynchronous low-overhead GL performance measurement using
 * http://www.khronos.org/registry/gles/extensions/EXT/EXT_disjoint_timer_query.txt
 *
 * Measures the duration of GPU processing for a set of GL commands, delivering
 * the measurement asynchronously once processing completes.
 *
 * All calls must come from a single thread with a valid GL context active.
 **/
class PerfMeasurementContext {
  private:
    Vector<GLuint> mTimingQueries;
    size_t mTimingStartIndex;
    size_t mTimingEndIndex;
    size_t mTimingQueryIndex;
    size_t mFreeQueries;

    bool mInitDone;
  public:

    /**
     * maxQueryCount should be a conservative estimate of how many query objects
     * will be active at once, which is a function of the GPU's level of
     * pipelining and the frequency of queries.
     */
    PerfMeasurementContext(size_t maxQueryCount):
            mTimingStartIndex(0),
            mTimingEndIndex(0),
            mTimingQueryIndex(0) {
        mTimingQueries.resize(maxQueryCount);
        mFreeQueries = maxQueryCount;
        mInitDone = false;
    }

    int getMaxQueryCount() {
        return mTimingQueries.size();
    }

    /**
     * Start a measurement period using the next available query object.
     * Returns INVALID_OPERATION if called multiple times in a row,
     * and BAD_VALUE if no more query objects are available.
     */
    int startGlTimer() {
        // Lazy init of queries to avoid needing GL context during construction
        if (!mInitDone) {
            glGenQueriesEXT(mTimingQueries.size(), mTimingQueries.editArray());
            mInitDone = true;
        }

        if (mTimingEndIndex != mTimingStartIndex) {
            return INVALID_OPERATION;
        }

        if (mFreeQueries == 0) {
            return BAD_VALUE;
        }

        glBeginQueryEXT(GL_TIME_ELAPSED_EXT, mTimingQueries[mTimingStartIndex]);

        mTimingStartIndex = (mTimingStartIndex + 1) % mTimingQueries.size();
        mFreeQueries--;

        return OK;
    }

    /**
     * Finish the current measurement period
     * Returns INVALID_OPERATION if called before any startGLTimer calls
     * or if called multiple times in a row.
     */
    int stopGlTimer() {
        size_t nextEndIndex = (mTimingEndIndex + 1) % mTimingQueries.size();
        if (nextEndIndex != mTimingStartIndex) {
            return INVALID_OPERATION;
        }
        glEndQueryEXT(GL_TIME_ELAPSED_EXT);

        mTimingEndIndex = nextEndIndex;

        return OK;
    }

    static const nsecs_t NO_DURATION_YET = -1L;
    static const nsecs_t FAILED_MEASUREMENT = -2L;

    /**
     * Get the next available duration measurement.
     *
     * Returns NO_DURATION_YET if no new measurement is available,
     * and FAILED_MEASUREMENT if an error occurred during the next
     * measurement period.
     *
     * Otherwise returns a positive number of nanoseconds measuring the
     * duration of the oldest completed query.
     */
    nsecs_t getNextGlDuration() {
        if (!mInitDone) {
            // No start/stop called yet
            return NO_DURATION_YET;
        }

        GLint available;
        glGetQueryObjectivEXT(mTimingQueries[mTimingQueryIndex],
                GL_QUERY_RESULT_AVAILABLE_EXT, &available);
        if (!available) {
            return NO_DURATION_YET;
        }

        GLint64 duration = FAILED_MEASUREMENT;
        GLint disjointOccurred;
        glGetIntegerv(GL_GPU_DISJOINT_EXT, &disjointOccurred);

        if (!disjointOccurred) {
            glGetQueryObjecti64vEXT(mTimingQueries[mTimingQueryIndex],
                    GL_QUERY_RESULT_EXT,
                    &duration);
        }

        mTimingQueryIndex = (mTimingQueryIndex + 1) % mTimingQueries.size();
        mFreeQueries++;

        return static_cast<nsecs_t>(duration);
    }

    static bool isMeasurementSupported() {
        const GLubyte* extensions = glGetString(GL_EXTENSIONS);
        return checkForExtension(extensions,
                reinterpret_cast<const GLubyte*>("GL_EXT_disjoint_timer_query"));
    }

};

PerfMeasurementContext* getContext(jlong context) {
    return reinterpret_cast<PerfMeasurementContext*>(context);
}

extern "C" {

static jlong PerfMeasurement_nativeCreateContext(JNIEnv* env, jobject thiz,
        jint maxQueryCount) {
    PerfMeasurementContext *context = new PerfMeasurementContext(maxQueryCount);
    return reinterpret_cast<jlong>(context);
}

static void PerfMeasurement_nativeDeleteContext(JNIEnv* env, jobject thiz,
        jlong contextHandle) {
    PerfMeasurementContext *context = getContext(contextHandle);
    delete(context);
}

static jboolean PerfMeasurement_nativeQuerySupport(JNIEnv* env, jobject thiz) {
    bool supported = PerfMeasurementContext::isMeasurementSupported();
    checkGlError(env);
    return static_cast<jboolean>(supported);
}

static void PerfMeasurement_nativeStartGlTimer(JNIEnv* env, jobject thiz,
        jlong contextHandle) {

    PerfMeasurementContext *context = getContext(contextHandle);
    status_t err = context->startGlTimer();
    if (err != OK) {
        switch (err) {
            case INVALID_OPERATION:
                jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
                        "Mismatched start/end GL timing calls");
                return;
            case BAD_VALUE:
                jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
                        "Too many timing queries in progress, max %d",
                        context->getMaxQueryCount());
                return;
            default:
                jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
                        "Unknown error starting GL timing");
                return;
        }
    }
    checkGlError(env);
}

static void PerfMeasurement_nativeStopGlTimer(JNIEnv* env, jobject thiz,
            jlong contextHandle) {

    PerfMeasurementContext *context = getContext(contextHandle);
    status_t err = context->stopGlTimer();
    if (err != OK) {
        switch (err) {
            case INVALID_OPERATION:
                jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
                        "Mismatched start/end GL timing calls");
                return;
            default:
                jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
                        "Unknown error ending GL timing");
                return;
        }
    }
    checkGlError(env);
}

static jlong PerfMeasurement_nativeGetNextGlDuration(JNIEnv* env,
        jobject thiz, jlong contextHandle) {
    PerfMeasurementContext *context = getContext(contextHandle);
    nsecs_t duration = context->getNextGlDuration();

    checkGlError(env);
    return static_cast<jlong>(duration);
}

} // extern "C"

static const JNINativeMethod gPerfMeasurementMethods[] = {
    { "nativeCreateContext",
      "(I)J",
      (jlong *)PerfMeasurement_nativeCreateContext },
    { "nativeDeleteContext",
      "(J)V",
      (void *)PerfMeasurement_nativeDeleteContext },
    { "nativeQuerySupport",
      "()Z",
      (jboolean *)PerfMeasurement_nativeQuerySupport },
    { "nativeStartGlTimer",
      "(J)V",
      (void *)PerfMeasurement_nativeStartGlTimer },
    { "nativeStopGlTimer",
      "(J)V",
      (void *)PerfMeasurement_nativeStopGlTimer },
    { "nativeGetNextGlDuration",
      "(J)J",
      (jlong *)PerfMeasurement_nativeGetNextGlDuration }
};


// Get all the required offsets in java class and register native functions
int register_android_hardware_camera2_legacy_PerfMeasurement(JNIEnv* env)
{
    // Register native functions
    return RegisterMethodsOrDie(env,
            PERF_MEASUREMENT_CLASS_NAME,
            gPerfMeasurementMethods,
            NELEM(gPerfMeasurementMethods));
}
