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

#include "CoreProfileEngine.h"

#include "CoreProfileEngineShaders.h"
#include "GLcommon/GLEScontext.h"
#include "GLcommon/GLESpointer.h"
#include "GLEScmContext.h"

#include "aemu/base/containers/Lookup.h"
#include "host-common/crash_reporter.h"

#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <glm/gtc/matrix_inverse.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

CoreProfileEngine::CoreProfileEngine(GLEScmContext* ctx, bool onGles) :
    mCtx(ctx),
    mOnGles(onGles) {
    getGeometryDrawState();
}

CoreProfileEngine::~CoreProfileEngine() {
    teardown();
}

static const uint32_t sDrawTexIbo[] = {
    0, 1, 2, 0, 2, 3,
};

const CoreProfileEngine::DrawTexOESCoreState& CoreProfileEngine::getDrawTexOESCoreState() {
    if (!m_drawTexOESCoreState.program) {
        m_drawTexOESCoreState.vshader =
            GLEScontext::compileAndValidateCoreShader(
                GL_VERTEX_SHADER,
                mOnGles ? kDrawTexOESGles2_vshader : kDrawTexOESCore_vshader);
        m_drawTexOESCoreState.fshader =
            GLEScontext::compileAndValidateCoreShader(
                GL_FRAGMENT_SHADER,
                mOnGles ? kDrawTexOESGles2_fshader : kDrawTexOESCore_fshader);
        m_drawTexOESCoreState.program =
            GLEScontext::linkAndValidateProgram(m_drawTexOESCoreState.vshader,
                                                m_drawTexOESCoreState.fshader);
    }
    if (!m_drawTexOESCoreState.vao) {
        GLDispatch& gl = GLEScontext::dispatcher();

        gl.glGenVertexArrays(1, &m_drawTexOESCoreState.vao);
        gl.glBindVertexArray(m_drawTexOESCoreState.vao);

        // Initialize VBO
        // Save IBO, attrib arrays/pointers to VAO
        gl.glGenBuffers(1, &m_drawTexOESCoreState.ibo);
        gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_drawTexOESCoreState.ibo);
        gl.glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(sDrawTexIbo), sDrawTexIbo, GL_STATIC_DRAW);

        gl.glGenBuffers(1, &m_drawTexOESCoreState.vbo);
        gl.glBindBuffer(GL_ARRAY_BUFFER, m_drawTexOESCoreState.vbo);

        gl.glEnableVertexAttribArray(0); // pos
        gl.glEnableVertexAttribArray(1); // texcoord

        gl.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (GLvoid*)0);
        gl.glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
                                 (GLvoid*)(uintptr_t)(3 * sizeof(float)));

        gl.glBindVertexArray(0);

        gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
        gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }

    return m_drawTexOESCoreState;
}

void CoreProfileEngine::teardown() {
    GLDispatch& gl = GLEScontext::dispatcher();

    if (m_drawTexOESCoreState.program) {
        gl.glDeleteProgram(m_drawTexOESCoreState.program);
        m_drawTexOESCoreState.program = 0;
    }

    if (m_drawTexOESCoreState.vao) {
        gl.glBindVertexArray(0);
        gl.glDeleteVertexArrays(1, &m_drawTexOESCoreState.vao);
        gl.glDeleteBuffers(1, &m_drawTexOESCoreState.ibo);
        gl.glDeleteBuffers(1, &m_drawTexOESCoreState.vbo);
        m_drawTexOESCoreState.vao = 0;
        m_drawTexOESCoreState.vbo = 0;
        m_drawTexOESCoreState.ibo = 0;
    }

    if (m_geometryDrawState.program) {
        gl.glDeleteProgram(m_geometryDrawState.program);
        m_geometryDrawState.program = 0;
    }

    if (m_geometryDrawState.programFlat) {
        gl.glDeleteProgram(m_geometryDrawState.programFlat);
        m_geometryDrawState.programFlat = 0;
    }

    if (m_geometryDrawState.vao) {
        gl.glDeleteVertexArrays(1, &m_geometryDrawState.vao);
        m_geometryDrawState.vao = 0;
    }
    if (m_geometryDrawState.posVbo) {
        gl.glDeleteBuffers(1, &m_geometryDrawState.posVbo);
        m_geometryDrawState.posVbo = 0;
    }

    if (m_geometryDrawState.normalVbo) {
        gl.glDeleteBuffers(1, &m_geometryDrawState.normalVbo);
        m_geometryDrawState.normalVbo = 0;
    }

    if (m_geometryDrawState.colorVbo) {
        gl.glDeleteBuffers(1, &m_geometryDrawState.colorVbo);
        m_geometryDrawState.colorVbo = 0;
    }

    if (m_geometryDrawState.pointsizeVbo) {
        gl.glDeleteBuffers(1, &m_geometryDrawState.pointsizeVbo);
        m_geometryDrawState.pointsizeVbo = 0;
    }

    if (m_geometryDrawState.texcoordVbo) {
        gl.glDeleteBuffers(1, &m_geometryDrawState.texcoordVbo);
        m_geometryDrawState.texcoordVbo = 0;
    }

    if (m_geometryDrawState.ibo) {
        gl.glDeleteBuffers(1, &m_geometryDrawState.ibo);
        m_geometryDrawState.ibo = 0;
    }
}

// Match attribute locations in the shader below.
static GLint arrayTypeToCoreAttrib(GLenum type) {
    switch (type) {
    case GL_VERTEX_ARRAY:
        return 0;
    case GL_NORMAL_ARRAY:
        return 1;
    case GL_COLOR_ARRAY:
        return 2;
    case GL_POINT_SIZE_ARRAY_OES:
        return 3;
    case GL_TEXTURE_COORD_ARRAY:
        return 4;
    }
    fprintf(stderr, "%s: error: unsupported array type 0x%x\n",
            __func__, type);
    return 0;
}

static std::string sMakeGeometryDrawShader(bool isGles, GLenum shaderType, bool flat) {
    // Set up a std::string to hold the result of
    // interpolating the template. We will require some extra padding
    // in the result string depending on how many characters
    // we could potentially insert.
    // For now it's just 10 chars and for a
    // single interpolation qualifier |flat|.
    static const char versionPartEssl300[] = "#version 300 es\n";
    static const char versionPart330Core[] = "#version 330 core\n";
    static const char flatKeyword[] = "flat";

    size_t extraStringLengthRequired = 10 +
        sizeof(versionPartEssl300) +
        sizeof(versionPart330Core) +
        sizeof(flatKeyword);

    size_t reservation = extraStringLengthRequired;
    std::string res;
    const char* shaderTemplate = nullptr;

    switch (shaderType) {
    case GL_VERTEX_SHADER:
        reservation += sizeof(kGeometryDrawVShaderSrcTemplateCore);
        shaderTemplate = kGeometryDrawVShaderSrcTemplateCore;
        break;
    case GL_FRAGMENT_SHADER:
        reservation += sizeof(kGeometryDrawFShaderSrcTemplateCore);
        shaderTemplate = kGeometryDrawFShaderSrcTemplateCore;
        break;
    default:
        break;
        // emugl_crash_reporter(
        //     "%s: unknown shader type 0x%x (memory corrupt)\n", __func__,
        //     shaderType);
    }

    if (shaderTemplate) {
        res.resize(reservation);
        snprintf(&res[0], res.size(), shaderTemplate,
                isGles ? versionPartEssl300 : versionPart330Core,
                flat ? flatKeyword : "");
    }
    return res;
}

const CoreProfileEngine::GeometryDrawState& CoreProfileEngine::getGeometryDrawState() {
    auto& gl = GLEScontext::dispatcher();

    if (!m_geometryDrawState.program) {

        // Non-flat shader
        m_geometryDrawState.vshader =
            GLEScontext::compileAndValidateCoreShader(
                GL_VERTEX_SHADER,
                sMakeGeometryDrawShader(mOnGles, GL_VERTEX_SHADER, false /* no flat shading */).c_str());

        m_geometryDrawState.fshader =
            GLEScontext::compileAndValidateCoreShader(
                GL_FRAGMENT_SHADER,
                sMakeGeometryDrawShader(mOnGles, GL_FRAGMENT_SHADER, false /* no flat shading */).c_str());

        m_geometryDrawState.program =
            GLEScontext::linkAndValidateProgram(m_geometryDrawState.vshader,
                                                m_geometryDrawState.fshader);

        m_geometryDrawState.vshaderFlat =
            GLEScontext::compileAndValidateCoreShader(
                    GL_VERTEX_SHADER,
                    sMakeGeometryDrawShader(mOnGles, GL_VERTEX_SHADER, true /* flat shading */).c_str());
        m_geometryDrawState.fshaderFlat =
            GLEScontext::compileAndValidateCoreShader(
                    GL_FRAGMENT_SHADER,
                    sMakeGeometryDrawShader(mOnGles, GL_FRAGMENT_SHADER, true /* flat shading */).c_str());
        m_geometryDrawState.programFlat =
            GLEScontext::linkAndValidateProgram(m_geometryDrawState.vshaderFlat,
                                                m_geometryDrawState.fshaderFlat);

        m_geometryDrawState.projMatrixLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "projection");
        m_geometryDrawState.modelviewMatrixLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "modelview");
        m_geometryDrawState.modelviewInvTrLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "modelview_invtr");
        m_geometryDrawState.textureMatrixLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "texture_matrix");
        m_geometryDrawState.textureSamplerLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "tex_sampler");
        m_geometryDrawState.textureCubeSamplerLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "tex_cube_sampler");

        m_geometryDrawState.enableTextureLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "enable_textures");
        m_geometryDrawState.enableLightingLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "enable_lighting");
        m_geometryDrawState.enableRescaleNormalLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "enable_rescale_normal");
        m_geometryDrawState.enableNormalizeLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "enable_normalize");
        m_geometryDrawState.enableColorMaterialLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "enable_color_material");
        m_geometryDrawState.enableFogLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "enable_fog");
        m_geometryDrawState.enableReflectionMapLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "enable_reflection_map");

        m_geometryDrawState.textureEnvModeLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "texture_env_mode");
        m_geometryDrawState.textureFormatLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "texture_format");

        m_geometryDrawState.materialAmbientLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "material_ambient");
        m_geometryDrawState.materialDiffuseLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "material_diffuse");
        m_geometryDrawState.materialSpecularLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "material_specular");
        m_geometryDrawState.materialEmissiveLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "material_emissive");
        m_geometryDrawState.materialSpecularExponentLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "material_specular_exponent");

        m_geometryDrawState.lightModelSceneAmbientLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_model_scene_ambient");
        m_geometryDrawState.lightModelTwoSidedLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_model_two_sided");

        m_geometryDrawState.lightEnablesLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_enables");
        m_geometryDrawState.lightAmbientsLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_ambients");
        m_geometryDrawState.lightDiffusesLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_diffuses");
        m_geometryDrawState.lightSpecularsLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_speculars");
        m_geometryDrawState.lightPositionsLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_positions");
        m_geometryDrawState.lightDirectionsLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_directions");
        m_geometryDrawState.lightSpotlightExponentsLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_spotlight_exponents");
        m_geometryDrawState.lightSpotlightCutoffAnglesLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_spotlight_cutoff_angles");
        m_geometryDrawState.lightAttenuationConstsLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_attenuation_consts");
        m_geometryDrawState.lightAttenuationLinearsLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_attenuation_linears");
        m_geometryDrawState.lightAttenuationQuadraticsLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "light_attenuation_quadratics");

        m_geometryDrawState.fogModeLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "fog_mode");
        m_geometryDrawState.fogDensityLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "fog_density");
        m_geometryDrawState.fogStartLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "fog_start");
        m_geometryDrawState.fogEndLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "fog_end");
        m_geometryDrawState.fogColorLoc =
            gl.glGetUniformLocation(m_geometryDrawState.program, "fog_color");

    }

    if (!m_geometryDrawState.vao) {

        gl.glGenBuffers(1, &m_geometryDrawState.posVbo);
        gl.glGenBuffers(1, &m_geometryDrawState.normalVbo);
        gl.glGenBuffers(1, &m_geometryDrawState.colorVbo);
        gl.glGenBuffers(1, &m_geometryDrawState.pointsizeVbo);
        gl.glGenBuffers(1, &m_geometryDrawState.texcoordVbo);

        gl.glGenVertexArrays(1, &m_geometryDrawState.vao);
        gl.glBindVertexArray(m_geometryDrawState.vao);

        gl.glGenBuffers(1, &m_geometryDrawState.ibo);
        gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_geometryDrawState.ibo);

        gl.glBindVertexArray(0);

        gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
        gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }

    return m_geometryDrawState;
}

GLuint CoreProfileEngine::getVboFor(GLenum type) {
    switch (type) {
    case GL_VERTEX_ARRAY:
        return m_geometryDrawState.posVbo;
    case GL_NORMAL_ARRAY:
        return m_geometryDrawState.normalVbo;
    case GL_COLOR_ARRAY:
        return m_geometryDrawState.colorVbo;
    case GL_POINT_SIZE_ARRAY_OES:
        return m_geometryDrawState.pointsizeVbo;
    case GL_TEXTURE_COORD_ARRAY:
        return m_geometryDrawState.texcoordVbo;
    }
    return 0;
}

size_t CoreProfileEngine::sizeOfType(GLenum type) {
    switch (type) {
    case GL_BYTE:
    case GL_UNSIGNED_BYTE:
        return 1;
    case GL_SHORT:
    case GL_UNSIGNED_SHORT:
    case GL_HALF_FLOAT_OES:
        return 2;
    case GL_INT:
    case GL_UNSIGNED_INT:
    case GL_FLOAT:
        return 4;
    }
    return 4;
}

template <class T>
static GLsizei sNeededVboCount(GLsizei indicesCount, const T* indices) {
    T maxIndex = 0;
    for (GLsizei i = 0; i < indicesCount; i++) {
        T index = indices[i];
        maxIndex = (index > maxIndex) ? index : maxIndex;
    }
    return (GLsizei)maxIndex + 1;
}

void CoreProfileEngine::setupArrayForDraw(
    GLenum arrayType,
    GLESpointer* p,
    GLint first, GLsizei count,
    bool isIndexed, GLenum indicesType, const GLvoid* indices) {

    auto& gl = GLEScontext::dispatcher();
    GLint attribNum = -1;

    gl.glBindVertexArray(m_geometryDrawState.vao);

    attribNum = arrayTypeToCoreAttrib(arrayType);

    GLsizei vboCount = 0;

    if (isIndexed) {

        GLsizei indexSize = 4;
        GLsizei indicesCount = count;

        switch (indicesType) {
        case GL_UNSIGNED_BYTE:
            indexSize = 1;
            vboCount = sNeededVboCount(indicesCount, (unsigned char*)indices);
            break;
        case GL_UNSIGNED_SHORT:
            indexSize = 2;
            vboCount = sNeededVboCount(indicesCount, (uint16_t*)indices);
            break;
        case GL_UNSIGNED_INT:
        default:
            indexSize = 4;
            vboCount = sNeededVboCount(indicesCount, (uint32_t*)indices);
        }

        // And send the indices
        gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_geometryDrawState.ibo);
        gl.glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexSize * indicesCount, indices, GL_STREAM_DRAW);
    } else {
        gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        vboCount = count;
    }

    if (p->isEnable()) {
        gl.glEnableVertexAttribArray(attribNum);
        gl.glBindBuffer(GL_ARRAY_BUFFER, getVboFor(arrayType));

        GLESConversionArrays arrs;

        bool convert = mCtx->doConvert(arrs, first, count, indicesType, indices, !isIndexed, p, arrayType);
        ArrayData currentArr = arrs.getCurrentArray();

        GLint size = p->getSize();
        GLenum dataType = convert ? currentArr.type : p->getType();
        GLsizei stride = convert ? currentArr.stride : p->getStride();

        GLsizei effectiveStride =
            stride ? stride :
                     (size * sizeOfType(dataType));

        char* bufData = convert ? (char*)currentArr.data : (char*)p->getData();
        uint32_t offset = first * effectiveStride;
        uint32_t bufSize = offset + vboCount * effectiveStride;

        gl.glBufferData(GL_ARRAY_BUFFER, bufSize, bufData, GL_STREAM_DRAW);

        gl.glVertexAttribDivisor(attribNum, 0);
        GLboolean shouldNormalize = false;

        if (arrayType == GL_COLOR_ARRAY &&
            (dataType == GL_BYTE ||
             dataType == GL_UNSIGNED_BYTE ||
             dataType == GL_INT ||
             dataType == GL_UNSIGNED_INT ||
             dataType == GL_FIXED))
            shouldNormalize = true;

        gl.glVertexAttribPointer(attribNum, size, dataType,
                                 shouldNormalize ? GL_TRUE : GL_FALSE /* normalized */,
                                 effectiveStride, nullptr /* no offset into vbo */);
        gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
    } else {
        if (arrayType == GL_COLOR_ARRAY ||
            arrayType == GL_NORMAL_ARRAY ||
            arrayType == GL_TEXTURE_COORD_ARRAY) {

            gl.glEnableVertexAttribArray(attribNum);
            gl.glBindBuffer(GL_ARRAY_BUFFER, getVboFor(arrayType));

            GLint size;
            std::vector<float> buffer;
            switch (arrayType) {
                case GL_COLOR_ARRAY:
                    size = 4;
                    mCtx->getColor(count, buffer);
                    break;
                case GL_NORMAL_ARRAY:
                    size = 3;
                    mCtx->getNormal(count, buffer);
                    break;
                case GL_TEXTURE_COORD_ARRAY:
                    size = 4;
                    mCtx->getMultiTexCoord(count, mCtx->getActiveTextureUnit(), buffer);
                    break;
                default:
                    fprintf(stderr, "Error: Invalid array type %d.\n", arrayType);
                    mCtx->setGLerror(GL_INVALID_OPERATION);
                    return;
            }

            // We need a buffer large enough to hold `count` copies of the vertex attributes provided
            // above.
            GLsizei bufSize = count * size * sizeof(float);
            gl.glBufferData(GL_ARRAY_BUFFER, bufSize, buffer.data(), GL_STREAM_DRAW);
            // Stride is set to 0 to indicate that our values are tightly packed -- the driver will
            // do the calculation.
            gl.glVertexAttribPointer(attribNum, size, GL_FLOAT, GL_FALSE, 0, nullptr);
            gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
        } else {
            gl.glDisableVertexAttribArray(attribNum);
        }
    }

    gl.glBindVertexArray(0);
}

// API

void CoreProfileEngine::enable(GLenum cap) {
    switch (cap) {
        case GL_TEXTURE_2D:
        case GL_TEXTURE_CUBE_MAP_OES:
        case GL_TEXTURE_GEN_STR_OES:
        case GL_RESCALE_NORMAL:
        case GL_NORMALIZE:
        case GL_LIGHTING:
        case GL_LIGHT0:
        case GL_LIGHT1:
        case GL_LIGHT2:
        case GL_LIGHT3:
        case GL_LIGHT4:
        case GL_LIGHT5:
        case GL_LIGHT6:
        case GL_LIGHT7:
        case GL_COLOR_MATERIAL:
        case GL_FOG:
        case GL_POINT_SMOOTH:
        case GL_ALPHA_TEST:
            return;
        default:
            break;
    }

    auto& gl = GLEScontext::dispatcher();
    gl.glEnable(cap);
}

void CoreProfileEngine::disable(GLenum cap) {
    switch (cap) {
        case GL_TEXTURE_2D:
        case GL_TEXTURE_CUBE_MAP_OES:
        case GL_TEXTURE_GEN_STR_OES:
        case GL_RESCALE_NORMAL:
        case GL_NORMALIZE:
        case GL_LIGHTING:
        case GL_LIGHT0:
        case GL_LIGHT1:
        case GL_LIGHT2:
        case GL_LIGHT3:
        case GL_LIGHT4:
        case GL_LIGHT5:
        case GL_LIGHT6:
        case GL_LIGHT7:
        case GL_COLOR_MATERIAL:
        case GL_FOG:
        case GL_POINT_SMOOTH:
        case GL_ALPHA_TEST:
            return;
        default:
            break;
    }

    auto& gl = GLEScontext::dispatcher();
    gl.glDisable(cap);
}

void CoreProfileEngine::shadeModel(GLenum mode) {
    // no-op
}

void CoreProfileEngine::matrixMode(GLenum mode) {
    // no-op
}

void CoreProfileEngine::loadIdentity() {
    // no-op
}

void CoreProfileEngine::loadMatrixf(const GLfloat* m) {
    // no-op
}

void CoreProfileEngine::pushMatrix() {
    // no-op
}

void CoreProfileEngine::popMatrix() {
    // no-op
}

void CoreProfileEngine::multMatrixf(const GLfloat* m) {
    // no-op
}

void CoreProfileEngine::orthof(GLfloat left, GLfloat right, GLfloat bottom, GLfloat top, GLfloat zNear, GLfloat zFar) {
    // no-op
}

void CoreProfileEngine::frustumf(GLfloat left, GLfloat right, GLfloat bottom, GLfloat top, GLfloat zNear, GLfloat zFar) {
    // no-op
}

void CoreProfileEngine::texEnvf(GLenum target, GLenum pname, GLfloat param) {
    // no-op
}

void CoreProfileEngine::texEnvfv(GLenum target, GLenum pname, const GLfloat* params) {
    // no-op
}

void CoreProfileEngine::texEnvi(GLenum target, GLenum pname, GLint param) {
    // no-op
}

void CoreProfileEngine::texEnviv(GLenum target, GLenum pname, const GLint* params) {
    // no-op
}

void CoreProfileEngine::getTexEnvfv(GLenum env, GLenum pname, GLfloat* params) {
    // no-op
}

void CoreProfileEngine::getTexEnviv(GLenum env, GLenum pname, GLint* params) {
    // no-op
}

void CoreProfileEngine::texGenf(GLenum coord, GLenum pname, GLfloat param) {
    // no-op
}

void CoreProfileEngine::texGenfv(GLenum coord, GLenum pname, const GLfloat* params) {
    // no-op
}

void CoreProfileEngine::texGeni(GLenum coord, GLenum pname, GLint param) {
    // no-op
}

void CoreProfileEngine::texGeniv(GLenum coord, GLenum pname, const GLint* params) {
    // no-op
}

void CoreProfileEngine::getTexGeniv(GLenum coord, GLenum pname, GLint* params) {
    // no-op
}

void CoreProfileEngine::getTexGenfv(GLenum coord, GLenum pname, GLfloat* params) {
    // no-op
}

void CoreProfileEngine::enableClientState(GLenum clientState) {
    // no-op
}

void CoreProfileEngine::disableClientState(GLenum clientState) {
    // no-op
}

void CoreProfileEngine::drawTexOES(float x, float y, float z, float width, float height) {
    auto& gl = GLEScontext::dispatcher();

    // get viewport
    GLint viewport[4] = {};
    gl.glGetIntegerv(GL_VIEWPORT,viewport);

    // track previous vbo/ibo
    GLuint prev_vbo;
    GLuint prev_ibo;
    gl.glGetIntegerv(GL_ARRAY_BUFFER_BINDING, (GLint*)&prev_vbo);
    gl.glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, (GLint*)&prev_ibo);

    // compile shaders, generate vbo/ibo if not done already
    CoreProfileEngine::DrawTexOESCoreState drawTexState =
        getDrawTexOESCoreState();

    GLuint prog = drawTexState.program;
    GLuint vbo = drawTexState.vbo;
    GLuint vao = drawTexState.vao;

    gl.glUseProgram(prog);

    gl.glBindVertexArray(vao);
    // This is not strictly needed, but Swiftshader indirect VAO
    // can forget its ELEMENT_ARRAY_BUFFER binding.
    gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_drawTexOESCoreState.ibo);

    GLint samplerLoc = gl.glGetUniformLocation(prog, "tex_sampler");

    // Compute screen coordinates for our texture.
    // Recenter, rescale. (e.g., [0, 0, 1080, 1920] -> [-1, -1, 1, 1])
    float xNdc = 2.0f * (float)(x - viewport[0] - viewport[2] / 2) / (float)viewport[2];
    float yNdc = 2.0f * (float)(y - viewport[1] - viewport[3] / 2) / (float)viewport[3];
    float wNdc = 2.0f * (float)width / (float)viewport[2];
    float hNdc = 2.0f * (float)height / (float)viewport[3];
    z = z >= 1.0f ? 1.0f : z;
    z = z <= 0.0f ? 0.0f : z;
    float zNdc = z * 2.0f - 1.0f;

    for (int i = 0; i < mCtx->getMaxTexUnits(); i++) {
        if (mCtx->isTextureUnitEnabled(GL_TEXTURE0 + i)) {
            GLuint bindedTex = mCtx->getBindedTexture(GL_TEXTURE0 + i, GL_TEXTURE_2D);
            ObjectLocalName tex = mCtx->getTextureLocalName(GL_TEXTURE_2D, bindedTex);

            auto objData = mCtx->shareGroup()->getObjectData(NamedObjectType::TEXTURE, tex);

            if (objData) {
                TextureData* texData = (TextureData*)objData;

                float texCropX = (float)(texData->crop_rect[0]);
                float texCropY = (float)(texData->crop_rect[1]);

                float texCropW = (float)(texData->crop_rect[2]);
                float texCropH = (float)(texData->crop_rect[3]);

                float texW = (float)(texData->width);
                float texH = (float)(texData->height);

                // Now we know the vertex attributes (pos, texcoord).
                // Our vertex attributes are formatted with interleaved
                // position and texture coordinate:
                float vertexAttrs[] = {
                    xNdc, yNdc, zNdc,
                    texCropX / texW, texCropY / texH,

                    xNdc + wNdc, yNdc, zNdc,
                    (texCropX + texCropW) / texW, texCropY / texH,

                    xNdc + wNdc, yNdc + hNdc, zNdc,
                    (texCropX + texCropW) / texW, (texCropY + texCropH) / texH,

                    xNdc, yNdc + hNdc, zNdc,
                    texCropX / texW, (texCropY + texCropH) / texH,
                };

                gl.glBindBuffer(GL_ARRAY_BUFFER, vbo);
                gl.glBufferData(GL_ARRAY_BUFFER, sizeof(vertexAttrs),
                                vertexAttrs, GL_STREAM_DRAW);
            }

            gl.glActiveTexture(GL_TEXTURE0 + i);
            gl.glUniform1i(samplerLoc, i);
            gl.glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        }
    }

    gl.glBindVertexArray(0);

    gl.glUseProgram(0);

    gl.glBindBuffer(GL_ARRAY_BUFFER, prev_vbo);
    gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, prev_ibo);
}

void CoreProfileEngine::rotatef(float angle, float x, float y, float z) {
    // no-op
}

void CoreProfileEngine::scalef(float x, float y, float z) {
    // no-op
}

void CoreProfileEngine::translatef(float x, float y, float z) {
    // no-op
}

void CoreProfileEngine::color4f(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {
    // no-op
}

void CoreProfileEngine::color4ub(GLubyte red, GLubyte green, GLubyte blue, GLubyte alpha) {
    // no-op
}

void CoreProfileEngine::activeTexture(GLenum unit) {
    // Use 2 texture image units for each texture unit of GLES1.
    // This allows us to simultaneously use GL_TEXTURE_2D
    // and GL_TEXTURE_CUBE_MAP with the same texture unit through
    // using different samplers for the image units.
    GLEScontext::dispatcher().glActiveTexture(
        GL_TEXTURE0 + mCtx->getActiveTextureUnit() * 2);
}

void CoreProfileEngine::clientActiveTexture(GLenum unit) {
    GLEScontext::dispatcher().glActiveTexture(
        GL_TEXTURE0 + mCtx->getActiveTextureUnit() * 2);
}


void CoreProfileEngine::preDrawTextureUnitEmulation() {
    auto& gl = GLEScontext::dispatcher();
    unsigned int currTextureUnit = mCtx->getActiveTextureUnit();

    gl.glUniform1i(m_geometryDrawState.enableTextureLoc,
                   mCtx->isEnabled(GL_TEXTURE_2D) &&
                   mCtx->isArrEnabled(GL_TEXTURE_COORD_ARRAY));

    gl.glUniform1i(m_geometryDrawState.textureSamplerLoc, currTextureUnit * 2);
    gl.glUniform1i(m_geometryDrawState.textureCubeSamplerLoc, currTextureUnit * 2 + 1);

    if (auto cubeMapTex = mCtx->getBindedTexture(currTextureUnit + GL_TEXTURE0, GL_TEXTURE_CUBE_MAP)) {
        GLuint cubeMapTexGlobal = mCtx->shareGroup()->getGlobalName(
                                      NamedObjectType::TEXTURE, cubeMapTex);
        gl.glActiveTexture(GL_TEXTURE0 + currTextureUnit * 2);
        gl.glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
        gl.glActiveTexture(GL_TEXTURE0 + currTextureUnit * 2 + 1);
        gl.glBindTexture(GL_TEXTURE_CUBE_MAP, cubeMapTexGlobal);
        gl.glActiveTexture(GL_TEXTURE0 + currTextureUnit * 2);
    }

    GLenum textureGenMode = mCtx->getTextureGenMode();
    if (textureGenMode == GL_REFLECTION_MAP_OES) {
        gl.glUniform1i(m_geometryDrawState.enableTextureLoc, 1);
        gl.glUniform1i(m_geometryDrawState.enableReflectionMapLoc, 1);
    } else {
        gl.glUniform1i(m_geometryDrawState.enableReflectionMapLoc, 0);
    }

    auto bindedTex = mCtx->getBindedTexture(GL_TEXTURE_2D);
    ObjectLocalName tex = mCtx->getTextureLocalName(GL_TEXTURE_2D, bindedTex);
    auto objData = mCtx->shareGroup()->getObjectData(NamedObjectType::TEXTURE, tex);

    if (objData) {
        TextureData* texData = (TextureData*)objData;
        gl.glUniform1i(m_geometryDrawState.textureFormatLoc, texData->internalFormat);
    } else {
        gl.glUniform1i(m_geometryDrawState.textureFormatLoc, GL_RGBA);
    }

    gl.glUniform1i(m_geometryDrawState.enableLightingLoc, 0);
    gl.glUniform1i(m_geometryDrawState.textureEnvModeLoc, mCtx->getTextureEnvMode());
}

void CoreProfileEngine::postDrawTextureUnitEmulation() {
    auto& gl = GLEScontext::dispatcher();
    unsigned int currTextureUnit = mCtx->getActiveTextureUnit();

    GLuint cubeMapTex = mCtx->getBindedTexture(currTextureUnit + GL_TEXTURE0, GL_TEXTURE_CUBE_MAP);

    if (cubeMapTex) {
        GLuint cubeMapTexGlobal = mCtx->shareGroup()->getGlobalName(
                                      NamedObjectType::TEXTURE, cubeMapTex);
        gl.glActiveTexture(GL_TEXTURE0 + currTextureUnit * 2);
        gl.glBindTexture(GL_TEXTURE_CUBE_MAP, cubeMapTexGlobal);
        gl.glActiveTexture(GL_TEXTURE0 + currTextureUnit * 2 + 1);
        gl.glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
        gl.glActiveTexture(GL_TEXTURE0 + currTextureUnit * 2);
    }
}

void CoreProfileEngine::preDrawVertexSetup() {
    auto& gl = GLEScontext::dispatcher();

    glm::mat4 currProjMatrix = mCtx->getProjMatrix();
    glm::mat4 currModelviewMatrix = mCtx->getModelviewMatrix();
    glm::mat4 currTextureMatrix = mCtx->getTextureMatrix();
    glm::mat4 currModelviewMatrixInvTr = glm::inverseTranspose(currModelviewMatrix);

    gl.glBindVertexArray(m_geometryDrawState.vao);

    gl.glUseProgram(mCtx->getShadeModel() == GL_FLAT ?
                    m_geometryDrawState.programFlat :
                    m_geometryDrawState.program);
    gl.glUniformMatrix4fv(m_geometryDrawState.projMatrixLoc, 1, GL_FALSE, glm::value_ptr(currProjMatrix));
    gl.glUniformMatrix4fv(m_geometryDrawState.modelviewMatrixLoc, 1, GL_FALSE, glm::value_ptr(currModelviewMatrix));
    gl.glUniformMatrix4fv(m_geometryDrawState.modelviewInvTrLoc, 1, GL_FALSE, glm::value_ptr(currModelviewMatrixInvTr));
    gl.glUniformMatrix4fv(m_geometryDrawState.textureMatrixLoc, 1, GL_FALSE, glm::value_ptr(currTextureMatrix));
}

void CoreProfileEngine::postDrawVertexSetup() {
    auto& gl = GLEScontext::dispatcher();

    gl.glBindVertexArray(0);
}

void CoreProfileEngine::setupLighting() {
    auto& gl = GLEScontext::dispatcher();

    gl.glUniform1i(m_geometryDrawState.enableLightingLoc,
                   mCtx->isEnabled(GL_LIGHTING));
    gl.glUniform1i(m_geometryDrawState.enableRescaleNormalLoc,
                   mCtx->isEnabled(GL_RESCALE_NORMAL));
    gl.glUniform1i(m_geometryDrawState.enableNormalizeLoc,
                   mCtx->isEnabled(GL_NORMALIZE));
    gl.glUniform1i(m_geometryDrawState.enableColorMaterialLoc,
                   mCtx->isEnabled(GL_COLOR_MATERIAL));

    const auto& material = mCtx->getMaterialInfo();

    gl.glUniform4fv(m_geometryDrawState.materialAmbientLoc, 1, material.ambient);
    gl.glUniform4fv(m_geometryDrawState.materialDiffuseLoc, 1, material.diffuse);
    gl.glUniform4fv(m_geometryDrawState.materialSpecularLoc, 1, material.specular);
    gl.glUniform4fv(m_geometryDrawState.materialEmissiveLoc, 1, material.emissive);
    gl.glUniform1f(m_geometryDrawState.materialSpecularExponentLoc, material.specularExponent);

    const auto& lightModel = mCtx->getLightModelInfo();

    gl.glUniform4fv(m_geometryDrawState.lightModelSceneAmbientLoc, 1, lightModel.color);
    gl.glUniform1i(m_geometryDrawState.lightModelTwoSidedLoc, lightModel.twoSided);

    assert(kMaxLights == GLEScmContext::kMaxLights);

    for (int i = 0; i < GLEScmContext::kMaxLights; i++) {
        m_lightingBuffer.lightEnables[i] = mCtx->isEnabled(GL_LIGHT0 + i);
        const auto& light = mCtx->getLightInfo(i);
        memcpy(m_lightingBuffer.lightAmbients + 4 * i, &light.ambient, 4 * sizeof(GLfloat));
        memcpy(m_lightingBuffer.lightDiffuses + 4 * i, &light.diffuse, 4 * sizeof(GLfloat));
        memcpy(m_lightingBuffer.lightSpeculars + 4 * i, &light.specular, 4 * sizeof(GLfloat));
        memcpy(m_lightingBuffer.lightPositions + 4 * i, &light.position, 4 * sizeof(GLfloat));
        memcpy(m_lightingBuffer.lightDirections + 3 * i, &light.direction, 3 * sizeof(GLfloat));
        m_lightingBuffer.spotlightExponents[i] = light.spotlightExponent;
        m_lightingBuffer.spotlightCutoffAngles[i] = light.spotlightCutoffAngle;
        m_lightingBuffer.attenuationConsts[i] = light.attenuationConst;
        m_lightingBuffer.attenuationLinears[i] = light.attenuationLinear;
        m_lightingBuffer.attenuationQuadratics[i] = light.attenuationQuadratic;
    }

    gl.glUniform1iv(m_geometryDrawState.lightEnablesLoc, GLEScmContext::kMaxLights, m_lightingBuffer.lightEnables);
    gl.glUniform4fv(m_geometryDrawState.lightAmbientsLoc, GLEScmContext::kMaxLights, m_lightingBuffer.lightAmbients);
    gl.glUniform4fv(m_geometryDrawState.lightDiffusesLoc, GLEScmContext::kMaxLights, m_lightingBuffer.lightDiffuses);
    gl.glUniform4fv(m_geometryDrawState.lightSpecularsLoc, GLEScmContext::kMaxLights, m_lightingBuffer.lightSpeculars);
    gl.glUniform4fv(m_geometryDrawState.lightPositionsLoc, GLEScmContext::kMaxLights, m_lightingBuffer.lightPositions);
    gl.glUniform3fv(m_geometryDrawState.lightDirectionsLoc, GLEScmContext::kMaxLights, m_lightingBuffer.lightDirections);
    gl.glUniform1fv(m_geometryDrawState.lightSpotlightExponentsLoc, GLEScmContext::kMaxLights, m_lightingBuffer.spotlightExponents);
    gl.glUniform1fv(m_geometryDrawState.lightSpotlightCutoffAnglesLoc, GLEScmContext::kMaxLights, m_lightingBuffer.spotlightCutoffAngles);
    gl.glUniform1fv(m_geometryDrawState.lightAttenuationConstsLoc, GLEScmContext::kMaxLights, m_lightingBuffer.attenuationConsts);
    gl.glUniform1fv(m_geometryDrawState.lightAttenuationLinearsLoc, GLEScmContext::kMaxLights, m_lightingBuffer.attenuationLinears);
    gl.glUniform1fv(m_geometryDrawState.lightAttenuationQuadraticsLoc, GLEScmContext::kMaxLights, m_lightingBuffer.attenuationQuadratics);
}

void CoreProfileEngine::setupFog() {
    auto& gl = GLEScontext::dispatcher();

    gl.glUniform1i(m_geometryDrawState.enableFogLoc,
                   mCtx->isEnabled(GL_FOG));

    const auto& fogInfo = mCtx->getFogInfo();

    gl.glUniform1i(m_geometryDrawState.fogModeLoc, fogInfo.mode);
    gl.glUniform1f(m_geometryDrawState.fogDensityLoc, fogInfo.density);
    gl.glUniform1f(m_geometryDrawState.fogStartLoc, fogInfo.start);
    gl.glUniform1f(m_geometryDrawState.fogEndLoc, fogInfo.end);

    gl.glUniform4fv(m_geometryDrawState.fogColorLoc, 1, fogInfo.color);
}

void CoreProfileEngine::drawArrays(GLenum type, GLint first, GLsizei count) {
    auto& gl = GLEScontext::dispatcher();

    preDrawVertexSetup();
    preDrawTextureUnitEmulation();

    setupLighting();
    setupFog();

    gl.glDrawArrays(type, first, count);

    postDrawVertexSetup();
    postDrawTextureUnitEmulation();
}

void CoreProfileEngine::drawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices) {
    auto& gl = GLEScontext::dispatcher();

    preDrawVertexSetup();
    preDrawTextureUnitEmulation();

    setupLighting();
    setupFog();

    gl.glDrawElements(mode, count, type, (GLvoid*)0);

    postDrawVertexSetup();
    postDrawTextureUnitEmulation();
}
