/*
 * Copyright (C) 2012 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 ATRACE_TAG ATRACE_TAG_ALWAYS

#include <gui/GraphicBufferAlloc.h>
#include <gui/Surface.h>
#include <gui/SurfaceControl.h>
#include <gui/GLConsumer.h>
#include <gui/Surface.h>
#include <ui/Fence.h>
#include <utils/Trace.h>

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

#include <math.h>
#include <getopt.h>

#include "Flatland.h"
#include "GLHelper.h"

using namespace ::android;

static uint32_t g_SleepBetweenSamplesMs = 0;
static bool     g_PresentToWindow       = false;
static size_t   g_BenchmarkNameLen      = 0;

struct BenchmarkDesc {
    // The name of the test.
    const char* name;

    // The dimensions of the space in which window layers are specified.
    uint32_t width;
    uint32_t height;

    // The screen heights at which to run the test.
    uint32_t runHeights[MAX_TEST_RUNS];

    // The list of window layers.
    LayerDesc layers[MAX_NUM_LAYERS];
};

static const BenchmarkDesc benchmarks[] = {
    { "16:10 Single Static Window",
        2560, 1600, { 800, 1600, 2400 },
        {
            {   // Window
                0, staticGradient, opaque,
                0,    50,     2560,   1454,
            },
            {   // Status bar
                0, staticGradient, opaque,
                0,    0,      2560,   50,
            },
            {   // Navigation bar
                0, staticGradient, opaque,
                0,    1504,   2560,   96,
            },
        },
    },

    { "16:10 App -> Home Transition",
        2560, 1600, { 800, 1600, 2400 },
        {
            {   // Wallpaper
                0, staticGradient, opaque,
                0,    50,     2560,   1454,
            },
            {   // Launcher
                0, staticGradient, blend,
                0,    50,     2560,   1454,
            },
            {   // Outgoing activity
                0, staticGradient, blendShrink,
                20,    70,     2520,   1414,
            },
            {   // Status bar
                0, staticGradient, opaque,
                0,    0,      2560,   50,
            },
            {   // Navigation bar
                0, staticGradient, opaque,
                0,    1504,   2560,   96,
            },
        },
    },

    { "16:10 SurfaceView -> Home Transition",
        2560, 1600, { 800, 1600, 2400 },
        {
            {   // Wallpaper
                0, staticGradient, opaque,
                0,    50,     2560,   1454,
            },
            {   // Launcher
                0, staticGradient, blend,
                0,    50,     2560,   1454,
            },
            {   // Outgoing SurfaceView
                0, staticGradient, blendShrink,
                20,    70,     2520,   1414,
            },
            {   // Outgoing activity
                0, staticGradient, blendShrink,
                20,    70,     2520,   1414,
            },
            {   // Status bar
                0, staticGradient, opaque,
                0,    0,      2560,   50,
            },
            {   // Navigation bar
                0, staticGradient, opaque,
                0,    1504,   2560,   96,
            },
        },
    },
};

static const ShaderDesc shaders[] = {
    {
        name: "Blit",
        vertexShader: {
            "precision mediump float;",
            "",
            "attribute vec4 position;",
            "attribute vec4 uv;",
            "",
            "varying vec4 texCoords;",
            "",
            "uniform mat4 objToNdc;",
            "uniform mat4 uvToTex;",
            "",
            "void main() {",
            "    gl_Position = objToNdc * position;",
            "    texCoords = uvToTex * uv;",
            "}",
        },
        fragmentShader: {
            "#extension GL_OES_EGL_image_external : require",
            "precision mediump float;",
            "",
            "varying vec4 texCoords;",
            "",
            "uniform samplerExternalOES blitSrc;",
            "uniform vec4 modColor;",
            "",
            "void main() {",
            "    gl_FragColor = texture2D(blitSrc, texCoords.xy);",
            "    gl_FragColor *= modColor;",
            "}",
        },
    },

    {
        name: "Gradient",
        vertexShader: {
            "precision mediump float;",
            "",
            "attribute vec4 position;",
            "attribute vec4 uv;",
            "",
            "varying float interp;",
            "",
            "uniform mat4 objToNdc;",
            "uniform mat4 uvToInterp;",
            "",
            "void main() {",
            "    gl_Position = objToNdc * position;",
            "    interp = (uvToInterp * uv).x;",
            "}",
        },
        fragmentShader: {
            "precision mediump float;",
            "",
            "varying float interp;",
            "",
            "uniform vec4 color0;",
            "uniform vec4 color1;",
            "",
            "uniform sampler2D ditherKernel;",
            "uniform float invDitherKernelSize;",
            "uniform float invDitherKernelSizeSq;",
            "",
            "void main() {",
            "    float dither = texture2D(ditherKernel,",
            "            gl_FragCoord.xy * invDitherKernelSize).a;",
            "    dither *= invDitherKernelSizeSq;",
            "    vec4 color = mix(color0, color1, clamp(interp, 0.0, 1.0));",
            "    gl_FragColor = color + vec4(dither, dither, dither, 0.0);",
            "}",
        },
    },
};

class Layer {

public:

    Layer() :
        mFirstFrame(true),
        mGLHelper(NULL),
        mSurface(EGL_NO_SURFACE) {
    }

    bool setUp(const LayerDesc& desc, GLHelper* helper) {
        bool result;

        mDesc = desc;
        mGLHelper = helper;

        result = mGLHelper->createSurfaceTexture(mDesc.width, mDesc.height,
                &mGLConsumer, &mSurface, &mTexName);
        if (!result) {
            return false;
        }

        mRenderer = desc.rendererFactory();
        result = mRenderer->setUp(helper);
        if (!result) {
            return false;
        }

        mComposer = desc.composerFactory();
        result = mComposer->setUp(desc, helper);
        if (!result) {
            return false;
        }

        return true;
    }

    void tearDown() {
        if (mComposer != NULL) {
            mComposer->tearDown();
            delete mComposer;
            mComposer = NULL;
        }

        if (mRenderer != NULL) {
            mRenderer->tearDown();
            delete mRenderer;
            mRenderer = NULL;
        }

        if (mSurface != EGL_NO_SURFACE) {
            mGLHelper->destroySurface(&mSurface);
            mGLConsumer->abandon();
        }
        mGLHelper = NULL;
        mGLConsumer.clear();
    }

    bool render() {
        return mRenderer->render(mSurface);
    }

    bool prepareComposition() {
        status_t err;

        err = mGLConsumer->updateTexImage();
        if (err < 0) {
            fprintf(stderr, "GLConsumer::updateTexImage error: %d\n", err);
            return false;
        }

        return true;
    }

    bool compose() {
        return mComposer->compose(mTexName, mGLConsumer);
    }

private:
    bool mFirstFrame;

    LayerDesc mDesc;

    GLHelper* mGLHelper;

    GLuint mTexName;
    sp<GLConsumer> mGLConsumer;
    EGLSurface mSurface;

    Renderer* mRenderer;
    Composer* mComposer;
};

class BenchmarkRunner {

public:

    BenchmarkRunner(const BenchmarkDesc& desc, size_t instance) :
        mDesc(desc),
        mInstance(instance),
        mNumLayers(countLayers(desc)),
        mGLHelper(NULL),
        mSurface(EGL_NO_SURFACE),
        mWindowSurface(EGL_NO_SURFACE) {
    }

    bool setUp() {
        ATRACE_CALL();

        bool result;
        EGLint resulte;

        float scaleFactor = float(mDesc.runHeights[mInstance]) /
            float(mDesc.height);
        uint32_t w = uint32_t(scaleFactor * float(mDesc.width));
        uint32_t h = mDesc.runHeights[mInstance];

        mGLHelper = new GLHelper();
        result = mGLHelper->setUp(shaders, NELEMS(shaders));
        if (!result) {
            return false;
        }

        GLuint texName;
        result = mGLHelper->createSurfaceTexture(w, h, &mGLConsumer, &mSurface,
                &texName);
        if (!result) {
            return false;
        }

        for (size_t i = 0; i < mNumLayers; i++) {
            // Scale the layer to match the current screen size.
            LayerDesc ld = mDesc.layers[i];
            ld.x = int32_t(scaleFactor * float(ld.x));
            ld.y = int32_t(scaleFactor * float(ld.y));
            ld.width = uint32_t(scaleFactor * float(ld.width));
            ld.height = uint32_t(scaleFactor * float(ld.height));

            // Set up the layer.
            result = mLayers[i].setUp(ld, mGLHelper);
            if (!result) {
                return false;
            }
        }

        if (g_PresentToWindow) {
            result = mGLHelper->createWindowSurface(w, h, &mSurfaceControl,
                    &mWindowSurface);
            if (!result) {
                return false;
            }

            result = doFrame(mWindowSurface);
            if (!result) {
                return false;
            }
        }

        return true;
    }

    void tearDown() {
        ATRACE_CALL();

        for (size_t i = 0; i < mNumLayers; i++) {
            mLayers[i].tearDown();
        }

        if (mGLHelper != NULL) {
            if (mWindowSurface != EGL_NO_SURFACE) {
                mGLHelper->destroySurface(&mWindowSurface);
            }
            mGLHelper->destroySurface(&mSurface);
            mGLConsumer->abandon();
            mGLConsumer.clear();
            mSurfaceControl.clear();
            mGLHelper->tearDown();
            delete mGLHelper;
            mGLHelper = NULL;
        }
    }

    nsecs_t run(uint32_t warmUpFrames, uint32_t totalFrames) {
        ATRACE_CALL();

        bool result;
        status_t err;

        resetColorGenerator();

        // Do the warm-up frames.
        for (uint32_t i = 0; i < warmUpFrames; i++) {
            result = doFrame(mSurface);
            if (!result) {
                return -1;
            }
        }

        // Grab the fence for the start timestamp.
        sp<Fence> startFence = mGLConsumer->getCurrentFence();

        //  the timed frames.
        for (uint32_t i = warmUpFrames; i < totalFrames; i++) {
            result = doFrame(mSurface);
            if (!result) {
                return -1;
            }
        }

        // Grab the fence for the end timestamp.
        sp<Fence> endFence = mGLConsumer->getCurrentFence();

        // Keep doing frames until the end fence has signaled.
        while (endFence->wait(0) == -ETIME) {
            result = doFrame(mSurface);
            if (!result) {
                return -1;
            }
        }

        // Compute the time delta.
        nsecs_t startTime = startFence->getSignalTime();
        nsecs_t endTime = endFence->getSignalTime();

        return endTime - startTime;
    }

private:

    bool doFrame(EGLSurface surface) {
        bool result;
        status_t err;

        for (size_t i = 0; i < mNumLayers; i++) {
            result = mLayers[i].render();
            if (!result) {
                return false;
            }
        }

        for (size_t i = 0; i < mNumLayers; i++) {
            result = mLayers[i].prepareComposition();
            if (!result) {
                return false;
            }
        }

        result = mGLHelper->makeCurrent(surface);
        if (!result) {
            return false;
        }

        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        for (size_t i = 0; i < mNumLayers; i++) {
            result = mLayers[i].compose();
            if (!result) {
                return false;
            }
        }

        result = mGLHelper->swapBuffers(surface);
        if (!result) {
            return false;
        }

        err = mGLConsumer->updateTexImage();
        if (err < 0) {
            fprintf(stderr, "GLConsumer::updateTexImage error: %d\n", err);
            return false;
        }

        return true;
    }

    static size_t countLayers(const BenchmarkDesc& desc) {
        size_t i;
        for (i = 0; i < MAX_NUM_LAYERS; i++) {
            if (desc.layers[i].rendererFactory == NULL) {
                break;
            }
        }
        return i;
    }

    const BenchmarkDesc& mDesc;
    const size_t mInstance;
    const size_t mNumLayers;

    GLHelper* mGLHelper;

    // The surface into which layers are composited
    sp<GLConsumer> mGLConsumer;
    EGLSurface mSurface;

    // Used for displaying the surface to a window.
    EGLSurface mWindowSurface;
    sp<SurfaceControl> mSurfaceControl;

    Layer mLayers[MAX_NUM_LAYERS];
};

static int cmpDouble(const double* lhs, const double* rhs) {
    if (*lhs < *rhs) {
        return -1;
    } else if (*rhs < *lhs) {
        return 1;
    }
    return 0;
}

// Run a single benchmark and print the result.
static bool runTest(const BenchmarkDesc b, size_t run) {
    bool success = true;
    double prevResult = 0.0, result = 0.0;
    Vector<double> samples;

    uint32_t runHeight = b.runHeights[run];
    uint32_t runWidth = b.width * runHeight / b.height;
    printf(" %-*s | %4d x %4d | ", g_BenchmarkNameLen, b.name,
            runWidth, runHeight);
    fflush(stdout);

    BenchmarkRunner r(b, run);
    if (!r.setUp()) {
        fprintf(stderr, "error initializing runner.\n");
        return false;
    }

    // The slowest 1/outlierFraction sample results are ignored as potential
    // outliers.
    const uint32_t outlierFraction = 16;
    const double threshold = .0025;

    uint32_t warmUpFrames = 1;
    uint32_t totalFrames = 5;

    // Find the number of frames needed to run for over 100ms.
    double runTime = 0.0;
    while (true) {
        runTime = double(r.run(warmUpFrames, totalFrames));
        if (runTime < 50e6) {
            warmUpFrames *= 2;
            totalFrames *= 2;
        } else {
            break;
        }
    }


    if (totalFrames - warmUpFrames > 16) {
        // The test runs too fast to get a stable result.  Skip it.
        printf("  fast");
        goto done;
    } else if (totalFrames == 5 && runTime > 200e6) {
        // The test runs too slow to be very useful.  Skip it.
        printf("  slow");
        goto done;
    }

    do {
        size_t newSamples = samples.size();
        if (newSamples == 0) {
            newSamples = 4*outlierFraction;
        }

        if (newSamples > 512) {
            printf("varies");
            goto done;
        }

        for (size_t i = 0; i < newSamples; i++) {
            double sample = double(r.run(warmUpFrames, totalFrames));

            if (g_SleepBetweenSamplesMs > 0) {
                usleep(g_SleepBetweenSamplesMs  * 1000);
            }

            if (sample < 0.0) {
                success = false;
                goto done;
            }

            samples.add(sample);
        }

        samples.sort(cmpDouble);

        prevResult = result;
        size_t elem = (samples.size() * (outlierFraction-1) / outlierFraction);
        result = (samples[elem-1] + samples[elem]) * 0.5;
    } while (fabs(result - prevResult) > threshold * result);

    printf("%6.3f", result / double(totalFrames - warmUpFrames) / 1e6);

done:

    printf("\n");
    fflush(stdout);
    r.tearDown();

    return success;
}

static void printResultsTableHeader() {
    const char* scenario = "Scenario";
    size_t len = strlen(scenario);
    size_t leftPad = (g_BenchmarkNameLen - len) / 2;
    size_t rightPad = g_BenchmarkNameLen - len - leftPad;
    printf(" %*s%s%*s | Resolution  | Time (ms)\n", leftPad, "",
            "Scenario", rightPad, "");
}

// Run ALL the benchmarks!
static bool runTests() {
    printResultsTableHeader();

    for (size_t i = 0; i < NELEMS(benchmarks); i++) {
        const BenchmarkDesc& b = benchmarks[i];
        for (size_t j = 0; j < MAX_TEST_RUNS && b.runHeights[j]; j++) {
            if (!runTest(b, j)) {
                return false;
            }
        }
    }
    return true;
}

// Return the length longest benchmark name.
static size_t maxBenchmarkNameLen() {
    size_t maxLen = 0;
    for (size_t i = 0; i < NELEMS(benchmarks); i++) {
        const BenchmarkDesc& b = benchmarks[i];
        size_t len = strlen(b.name);
        if (len > maxLen) {
            maxLen = len;
        }
    }
    return maxLen;
}

// Print the command usage help to stderr.
static void showHelp(const char *cmd) {
    fprintf(stderr, "usage: %s [options]\n", cmd);
    fprintf(stderr, "options include:\n"
                    "  -s N            sleep for N ms between samples\n"
                    "  -d              display the test frame to a window\n"
                    "  --help          print this helpful message and exit\n"
            );
}

int main(int argc, char** argv) {
    if (argc == 2 && 0 == strcmp(argv[1], "--help")) {
        showHelp(argv[0]);
        exit(0);
    }

    for (;;) {
        int ret;
        int option_index = 0;
        static struct option long_options[] = {
            {"help",     no_argument, 0,  0 },
            {     0,               0, 0,  0 }
        };

        ret = getopt_long(argc, argv, "ds:",
                          long_options, &option_index);

        if (ret < 0) {
            break;
        }

        switch(ret) {
            case 'd':
                g_PresentToWindow = true;
            break;

            case 's':
                g_SleepBetweenSamplesMs = atoi(optarg);
            break;

            case 0:
                if (strcmp(long_options[option_index].name, "help")) {
                    showHelp(argv[0]);
                    exit(0);
                }
            break;

            default:
                showHelp(argv[0]);
                exit(2);
        }
    }

    g_BenchmarkNameLen = maxBenchmarkNameLen();

    printf(" cmdline:");
    for (int i = 0; i < argc; i++) {
        printf(" %s", argv[i]);
    }
    printf("\n");

    if (!runTests()) {
        fprintf(stderr, "exiting due to error.\n");
        return 1;
    }
}