//
// Copyright 2020 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//

// DrawBaseVertexVariantsTest: Tests variants of drawElements*BaseVertex* call from different
// extensions

#include "gpu_info_util/SystemInfo.h"
#include "test_utils/ANGLETest.h"
#include "test_utils/gl_raii.h"

#include <numeric>

using namespace angle;

namespace
{

// Create a kWidth * kHeight canvas equally split into kCountX * kCountY tiles
// each containing a quad partially covering each tile
constexpr uint32_t kWidth                   = 256;
constexpr uint32_t kHeight                  = 256;
constexpr uint32_t kCountX                  = 8;
constexpr uint32_t kCountY                  = 8;
constexpr int kBoxCount                     = kCountX * kCountY;
constexpr uint32_t kIndexPatternRepeatCount = 3;
constexpr std::array<GLfloat, 2> kTileSize  = {
    1.f / static_cast<GLfloat>(kCountX),
    1.f / static_cast<GLfloat>(kCountY),
};
constexpr std::array<uint32_t, 2> kTilePixelSize  = {kWidth / kCountX, kHeight / kCountY};
constexpr std::array<GLfloat, 2> kQuadRadius      = {0.25f * kTileSize[0], 0.25f * kTileSize[1]};
constexpr std::array<uint32_t, 2> kPixelCheckSize = {
    static_cast<uint32_t>(kQuadRadius[0] * kWidth),
    static_cast<uint32_t>(kQuadRadius[1] * kHeight)};
constexpr GLenum kBufferDataUsage[] = {GL_STATIC_DRAW, GL_DYNAMIC_DRAW, GL_STREAM_DRAW};

constexpr std::array<GLfloat, 2> GetTileCenter(uint32_t x, uint32_t y)
{
    return {
        kTileSize[0] * (0.5f + static_cast<GLfloat>(x)),
        kTileSize[1] * (0.5f + static_cast<GLfloat>(y)),
    };
}
constexpr std::array<std::array<GLfloat, 2>, 4> GetQuadVertices(uint32_t x, uint32_t y)
{
    const auto center = GetTileCenter(x, y);
    return {
        std::array<GLfloat, 2>{center[0] - kQuadRadius[0], center[1] - kQuadRadius[1]},
        std::array<GLfloat, 2>{center[0] + kQuadRadius[0], center[1] - kQuadRadius[1]},
        std::array<GLfloat, 2>{center[0] + kQuadRadius[0], center[1] + kQuadRadius[1]},
        std::array<GLfloat, 2>{center[0] - kQuadRadius[0], center[1] + kQuadRadius[1]},
    };
}

enum class DrawCallVariants
{
    DrawElementsBaseVertexEXT,
    DrawElementsBaseVertexOES,
    DrawElementsInstancedBaseVertexEXT,
    DrawElementsInstancedBaseVertexOES,
    DrawRangeElementsBaseVertexEXT,
    DrawRangeElementsBaseVertexOES,
    DrawElementsInstancedBaseVertexBaseInstanceEXT,
    DrawElementsInstancedBaseVertexBaseInstanceANGLE
};

using DrawBaseVertexVariantsTestParams = std::tuple<angle::PlatformParameters, GLenum>;

std::string DrawBaseVertexVariantsTestPrint(
    const ::testing::TestParamInfo<DrawBaseVertexVariantsTestParams> &paramsInfo)
{
    const DrawBaseVertexVariantsTestParams &params = paramsInfo.param;
    std::ostringstream out;

    out << std::get<0>(params) << "__";

    switch (std::get<1>(params))
    {
        case GL_STATIC_DRAW:
            out << "STATIC_DRAW";
            break;
        case GL_DYNAMIC_DRAW:
            out << "DYNAMIC_DRAW";
            break;
        case GL_STREAM_DRAW:
            out << "STREAM_DRAW";
            break;
        default:
            out << "UPDATE_THIS_SWITCH";
            break;
    }

    return out.str();
}

// These tests check correctness of variants of baseVertex draw calls from different extensions

class DrawBaseVertexVariantsTest : public ANGLETest<DrawBaseVertexVariantsTestParams>
{
  protected:
    DrawBaseVertexVariantsTest()
    {
        setWindowWidth(kWidth);
        setWindowHeight(kHeight);
        setConfigRedBits(8);
        setConfigGreenBits(8);
        setConfigBlueBits(8);
        setConfigAlphaBits(8);
        setExtensionsEnabled(false);

        std::array<GLushort, 6> indices = {0, 1, 2, 0, 2, 3};
        mIndices.resize(indices.size() * kIndexPatternRepeatCount);
        for (uint32_t i = 0; i < kIndexPatternRepeatCount; i++)
        {
            size_t o  = i * indices.size();
            size_t vo = i * 4;  // each quad has 4 vertices, index offset by 4
            for (size_t j = 0; j < indices.size(); j++)
            {
                mIndices[o + j] = vo + indices[j];
            }
        }

        mColorPalette = {GLColor(0x7f, 0x7f, 0x7f, 0xff),
                         GLColor::red,
                         GLColor::green,
                         GLColor::yellow,
                         GLColor::blue,
                         GLColor::magenta,
                         GLColor::cyan,
                         GLColor::white};

        for (uint32_t y = 0; y < kCountY; ++y)
        {
            for (uint32_t x = 0; x < kCountX; ++x)
            {
                // v3 ---- v2
                // |       |
                // |       |
                // v0 ---- v1

                const auto vs = ::GetQuadVertices(x, y);

                for (const auto &v : vs)
                {
                    mVertices.insert(mVertices.end(), v.begin(), v.end());
                }

                const auto &colorPicked = mColorPalette[(x + y) % mColorPalette.size()];
                for (int i = 0; i < 4; ++i)
                {
                    mVertexColors.push_back(colorPicked.R);
                    mVertexColors.push_back(colorPicked.G);
                    mVertexColors.push_back(colorPicked.B);
                    mVertexColors.push_back(colorPicked.A);
                }
            }
        }

        mRegularIndices.resize(kCountY * kCountX * mIndices.size());
        for (uint32_t y = 0; y < kCountY; y++)
        {
            for (uint32_t x = 0; x < kCountX; x++)
            {
                uint32_t i  = x + y * kCountX;
                uint32_t oi = 6 * i;
                uint32_t ov = 4 * i;
                for (uint32_t j = 0; j < 6; j++)
                {
                    mRegularIndices[oi + j] = mIndices[j] + ov;
                }
            }
        }
    }

    void setupProgram(GLProgram &program)
    {
        constexpr char vs[] = R"(
precision mediump float;
attribute vec2 vPosition;
attribute vec4 vColor;
varying vec4 color;
void main()
{
    gl_Position = vec4(vec3(vPosition, 1.0) * 2.0 - 1.0, 1.0);
    color = vColor;
})";
        constexpr char fs[] = R"(
precision mediump float;
varying vec4 color;
void main()
{
    gl_FragColor = color;
})";
        program.makeRaster(vs, fs);
        EXPECT_GL_NO_ERROR();
        ASSERT_TRUE(program.valid());
        glUseProgram(program);
        mPositionLoc = glGetAttribLocation(program, "vPosition");
        ASSERT_NE(-1, mPositionLoc);
        mColorLoc = glGetAttribLocation(program, "vColor");
        ASSERT_NE(-1, mColorLoc);
    }

    void setupIndexedBuffers(GLBuffer &vertexPositionBuffer,
                             GLBuffer &vertexColorBuffer,
                             GLBuffer &indexBuffer)
    {
        GLenum usage = std::get<1>(GetParam());

        glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(GLubyte) * mVertexColors.size(), mVertexColors.data(),
                     usage);

        glEnableVertexAttribArray(mColorLoc);
        glVertexAttribPointer(mColorLoc, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, 0);

        glBindBuffer(GL_ARRAY_BUFFER, vertexPositionBuffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * mVertices.size(), mVertices.data(), usage);

        glEnableVertexAttribArray(mPositionLoc);
        glVertexAttribPointer(mPositionLoc, 2, GL_FLOAT, GL_FALSE, 0, 0);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * mIndices.size(), mIndices.data(),
                     usage);

        ASSERT_GL_NO_ERROR();
    }

    void doDrawElementsBaseVertex(DrawCallVariants drawCallType,
                                  GLenum mode,
                                  GLsizei count,
                                  GLenum type,
                                  const void *indices,
                                  GLint basevertex)
    {
        switch (drawCallType)
        {
            case DrawCallVariants::DrawElementsBaseVertexEXT:
                glDrawElementsBaseVertexEXT(mode, count, type, indices, basevertex);
                break;
            case DrawCallVariants::DrawElementsBaseVertexOES:
                glDrawElementsBaseVertexOES(mode, count, type, indices, basevertex);
                break;
            default:
                UNREACHABLE();
                break;
        }
    }

    void doDrawElementsInstancedBaseVertex(DrawCallVariants drawCallType,
                                           GLenum mode,
                                           GLsizei count,
                                           GLenum type,
                                           const void *indices,
                                           GLsizei instancecount,
                                           GLint basevertex)
    {
        switch (drawCallType)
        {
            case DrawCallVariants::DrawElementsInstancedBaseVertexEXT:
                glDrawElementsInstancedBaseVertexEXT(mode, count, type, indices, instancecount,
                                                     basevertex);
                break;
            case DrawCallVariants::DrawElementsInstancedBaseVertexOES:
                glDrawElementsInstancedBaseVertexOES(mode, count, type, indices, instancecount,
                                                     basevertex);
                break;
            default:
                UNREACHABLE();
                break;
        }
    }

    void doDrawRangeElementsBaseVertex(DrawCallVariants drawCallType,
                                       GLenum mode,
                                       GLuint start,
                                       GLuint end,
                                       GLsizei count,
                                       GLenum type,
                                       const void *indices,
                                       GLint basevertex)
    {
        switch (drawCallType)
        {
            case DrawCallVariants::DrawRangeElementsBaseVertexEXT:
                glDrawRangeElementsBaseVertexEXT(mode, start, end, count, type, indices,
                                                 basevertex);
                break;
            case DrawCallVariants::DrawRangeElementsBaseVertexOES:
                glDrawRangeElementsBaseVertexOES(mode, start, end, count, type, indices,
                                                 basevertex);
                break;
            default:
                UNREACHABLE();
                break;
        }
    }

    void doDrawElementsInstancedBaseVertexBaseInstance(DrawCallVariants drawCallType,
                                                       GLenum mode,
                                                       GLsizei count,
                                                       GLenum type,
                                                       const void *indices,
                                                       GLsizei instancecount,
                                                       GLint basevertex,
                                                       GLuint baseinstance)
    {
        switch (drawCallType)
        {
            case DrawCallVariants::DrawElementsInstancedBaseVertexBaseInstanceEXT:
                glDrawElementsInstancedBaseVertexBaseInstanceEXT(
                    mode, count, type, indices, instancecount, basevertex, baseinstance);
                break;
            case DrawCallVariants::DrawElementsInstancedBaseVertexBaseInstanceANGLE:
                glDrawElementsInstancedBaseVertexBaseInstanceANGLE(
                    mode, count, type, indices, instancecount, basevertex, baseinstance);
                break;
            default:
                UNREACHABLE();
                break;
        }
    }

    void doDrawElementsBaseVertexVariants(DrawCallVariants drawCallType)
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        int baseRepetition = 0;
        int i              = 0;

        // Start at various repetitions within the patterned index buffer to exercise base
        // index.

        static_assert(kIndexPatternRepeatCount >= 3, "Repeat pattern count should be at least 3");

        while (i < kBoxCount)
        {
            int repetitionCount = std::min(3 - baseRepetition, kBoxCount - i);

            updateVertexColorData(i, repetitionCount);

            switch (drawCallType)
            {
                case DrawCallVariants::DrawElementsInstancedBaseVertexBaseInstanceEXT:
                case DrawCallVariants::DrawElementsInstancedBaseVertexBaseInstanceANGLE:
                    doDrawElementsInstancedBaseVertexBaseInstance(
                        drawCallType, GL_TRIANGLES, repetitionCount * 6, GL_UNSIGNED_SHORT,
                        reinterpret_cast<GLvoid *>(
                            static_cast<uintptr_t>(baseRepetition * 6 * sizeof(GLushort))),
                        1, (i - baseRepetition) * 4, 0);
                    break;
                case DrawCallVariants::DrawElementsBaseVertexEXT:
                case DrawCallVariants::DrawElementsBaseVertexOES:
                    doDrawElementsBaseVertex(
                        drawCallType, GL_TRIANGLES, repetitionCount * 6, GL_UNSIGNED_SHORT,
                        reinterpret_cast<GLvoid *>(
                            static_cast<uintptr_t>(baseRepetition * 6 * sizeof(GLushort))),
                        (i - baseRepetition) * 4);
                    break;
                case DrawCallVariants::DrawElementsInstancedBaseVertexEXT:
                case DrawCallVariants::DrawElementsInstancedBaseVertexOES:
                    doDrawElementsInstancedBaseVertex(
                        drawCallType, GL_TRIANGLES, repetitionCount * 6, GL_UNSIGNED_SHORT,
                        reinterpret_cast<GLvoid *>(
                            static_cast<uintptr_t>(baseRepetition * 6 * sizeof(GLushort))),
                        1, (i - baseRepetition) * 4);
                    break;
                case DrawCallVariants::DrawRangeElementsBaseVertexEXT:
                case DrawCallVariants::DrawRangeElementsBaseVertexOES:
                    doDrawRangeElementsBaseVertex(drawCallType, GL_TRIANGLES, baseRepetition * 4,
                                                  (baseRepetition + repetitionCount) * 4 - 1,
                                                  repetitionCount * 6, GL_UNSIGNED_SHORT,
                                                  reinterpret_cast<GLvoid *>(static_cast<uintptr_t>(
                                                      baseRepetition * 6 * sizeof(GLushort))),
                                                  (i - baseRepetition) * 4);
                    break;
                default:
                    EXPECT_TRUE(false);
                    break;
            }

            baseRepetition = (baseRepetition + 1) % 3;
            i += repetitionCount;
        }

        EXPECT_GL_NO_ERROR();
        checkDrawResult();
    }

    void updateVertexColorData(GLint drawnQuadCount, GLint toDrawQuadCount)
    {
        // update the vertex color of the next [count] of quads to draw
        if (std::get<1>(GetParam()) == GL_STATIC_DRAW)
        {
            return;
        }

        GLint offset = sizeof(GLubyte) * drawnQuadCount * 4 * sizeof(GLColor);

        for (GLint i = 0; i < toDrawQuadCount; i++)
        {
            const GLColor &color = mColorPalette[(drawnQuadCount + i) % mColorPalette.size()];
            for (GLint j = 0; j < 4; j++)
            {
                glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(GLColor), color.data());
                offset += sizeof(GLColor);
            }
        }
    }

    void checkDrawResult()
    {
        bool dynamicLayout = std::get<1>(GetParam()) == GL_STATIC_DRAW ? false : true;

        for (uint32_t y = 0; y < kCountY; ++y)
        {
            for (uint32_t x = 0; x < kCountX; ++x)
            {
                uint32_t center_x = x * kTilePixelSize[0] + kTilePixelSize[0] / 2;
                uint32_t center_y = y * kTilePixelSize[1] + kTilePixelSize[1] / 2;

                const auto &color =
                    mColorPalette[(dynamicLayout ? x : x + y) % mColorPalette.size()];

                EXPECT_PIXEL_NEAR(center_x - kPixelCheckSize[0] / 2,
                                  center_y - kPixelCheckSize[1] / 2, color[0], color[1], color[2],
                                  color[3], 1);
            }
        }
    }

    bool requestAngleBaseVertexBaseInstanceExtensions()
    {
        if (getClientMajorVersion() <= 2)
        {
            if (!EnsureGLExtensionEnabled("GL_ANGLE_instanced_arrays"))
            {
                return false;
            }
        }
        if (!EnsureGLExtensionEnabled("GL_ANGLE_base_vertex_base_instance"))
        {
            return false;
        }

        return EnsureGLExtensionEnabled("GL_ANGLE_base_vertex_base_instance_shader_builtin");
    }

    std::vector<GLushort> mIndices;
    std::vector<GLfloat> mVertices;
    std::vector<GLubyte> mVertexColors;

    std::vector<GLColor> mColorPalette;
    std::vector<GLushort> mRegularIndices;
    GLint mPositionLoc;
    GLint mColorLoc;
};

// Test drawElementsBaseVertex from EXT_draw_elements_base_vertex
TEST_P(DrawBaseVertexVariantsTest, DrawElementsBaseVertexEXT)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_draw_elements_base_vertex"));

    GLProgram program;
    setupProgram(program);

    GLBuffer indexBuffer;
    GLBuffer vertexPositionBuffer;
    GLBuffer vertexColorBuffer;
    setupIndexedBuffers(vertexPositionBuffer, vertexColorBuffer, indexBuffer);

    // for potential update vertex color later
    glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);

    doDrawElementsBaseVertexVariants(DrawCallVariants::DrawElementsBaseVertexEXT);
}

// Test drawElementsBaseVertex from OES_draw_elements_base_vertex
TEST_P(DrawBaseVertexVariantsTest, DrawElementsBaseVertexOES)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_draw_elements_base_vertex"));

    GLProgram program;
    setupProgram(program);

    GLBuffer indexBuffer;
    GLBuffer vertexPositionBuffer;
    GLBuffer vertexColorBuffer;
    setupIndexedBuffers(vertexPositionBuffer, vertexColorBuffer, indexBuffer);

    // for potential update vertex color later
    glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);

    doDrawElementsBaseVertexVariants(DrawCallVariants::DrawElementsBaseVertexOES);
}

// Test drawElementsInstancedBaseVertex from EXT_draw_elements_base_vertex
TEST_P(DrawBaseVertexVariantsTest, DrawElementsInstancedBaseVertexEXT)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_draw_elements_base_vertex"));

    GLProgram program;
    setupProgram(program);

    GLBuffer indexBuffer;
    GLBuffer vertexPositionBuffer;
    GLBuffer vertexColorBuffer;
    setupIndexedBuffers(vertexPositionBuffer, vertexColorBuffer, indexBuffer);

    // for potential update vertex color later
    glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);

    doDrawElementsBaseVertexVariants(DrawCallVariants::DrawElementsInstancedBaseVertexEXT);
}

// Test drawElementsInstancedBaseVertex from OES_draw_elements_base_vertex
TEST_P(DrawBaseVertexVariantsTest, DrawElementsInstancedBaseVertexOES)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_draw_elements_base_vertex"));

    GLProgram program;
    setupProgram(program);

    GLBuffer indexBuffer;
    GLBuffer vertexPositionBuffer;
    GLBuffer vertexColorBuffer;
    setupIndexedBuffers(vertexPositionBuffer, vertexColorBuffer, indexBuffer);

    // for potential update vertex color later
    glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);

    doDrawElementsBaseVertexVariants(DrawCallVariants::DrawElementsInstancedBaseVertexOES);
}

// Test drawRangeElementsBaseVertex from EXT_draw_elements_base_vertex
TEST_P(DrawBaseVertexVariantsTest, DrawRangeElementsBaseVertexEXT)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_draw_elements_base_vertex"));

    GLProgram program;
    setupProgram(program);

    GLBuffer indexBuffer;
    GLBuffer vertexPositionBuffer;
    GLBuffer vertexColorBuffer;
    setupIndexedBuffers(vertexPositionBuffer, vertexColorBuffer, indexBuffer);

    // for potential update vertex color later
    glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);

    doDrawElementsBaseVertexVariants(DrawCallVariants::DrawRangeElementsBaseVertexEXT);
}

// Test drawRangeElementsBaseVertex from OES_draw_elements_base_vertex
TEST_P(DrawBaseVertexVariantsTest, DrawRangeElementsBaseVertexOES)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_OES_draw_elements_base_vertex"));

    GLProgram program;
    setupProgram(program);

    GLBuffer indexBuffer;
    GLBuffer vertexPositionBuffer;
    GLBuffer vertexColorBuffer;
    setupIndexedBuffers(vertexPositionBuffer, vertexColorBuffer, indexBuffer);

    // for potential update vertex color later
    glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);

    doDrawElementsBaseVertexVariants(DrawCallVariants::DrawRangeElementsBaseVertexOES);
}

// Test drawElementsInstancedBaseVertexBaseInstance from EXT_base_instance
TEST_P(DrawBaseVertexVariantsTest, DrawElementsInstancedBaseVertexBaseInstanceEXT)
{
    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_base_instance") ||
                       !EnsureGLExtensionEnabled("GL_EXT_draw_elements_base_vertex"));

    GLProgram program;
    setupProgram(program);

    GLBuffer indexBuffer;
    GLBuffer vertexPositionBuffer;
    GLBuffer vertexColorBuffer;
    setupIndexedBuffers(vertexPositionBuffer, vertexColorBuffer, indexBuffer);

    // for potential update vertex color later
    glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);

    doDrawElementsBaseVertexVariants(
        DrawCallVariants::DrawElementsInstancedBaseVertexBaseInstanceEXT);
}

// Test drawElementsInstancedBaseVertexBaseInstance from ANGLE_base_vertex_base_instance
TEST_P(DrawBaseVertexVariantsTest, DrawElementsInstancedBaseVertexBaseInstanceANGLE)
{
    ANGLE_SKIP_TEST_IF(!requestAngleBaseVertexBaseInstanceExtensions());

    GLProgram program;
    setupProgram(program);

    GLBuffer indexBuffer;
    GLBuffer vertexPositionBuffer;
    GLBuffer vertexColorBuffer;
    setupIndexedBuffers(vertexPositionBuffer, vertexColorBuffer, indexBuffer);

    // for potential update vertex color later
    glBindBuffer(GL_ARRAY_BUFFER, vertexColorBuffer);

    doDrawElementsBaseVertexVariants(
        DrawCallVariants::DrawElementsInstancedBaseVertexBaseInstanceANGLE);
}

GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(DrawBaseVertexVariantsTest);
ANGLE_INSTANTIATE_TEST_COMBINE_1(
    DrawBaseVertexVariantsTest,
    DrawBaseVertexVariantsTestPrint,
    testing::ValuesIn(kBufferDataUsage),
    ES3_D3D11(),
    ES3_METAL(),
    ES3_OPENGL().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions),
    ES3_OPENGLES().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions),
    ES3_VULKAN());

}  // namespace
