//
// Copyright 2018 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.
//
// AttributeLayoutTest:
//   Test various layouts of vertex attribute data:
//   - in memory, in buffer object, or combination of both
//   - float, integer, or combination of both
//   - sequential or interleaved

#include <array>
#include <vector>

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

using namespace angle;

namespace
{

// Test will draw these four triangles.
// clang-format off
const GLfloat triangleData[] = {
    // xy       rgb
    0,0,        1,1,0,
    -1,+1,      1,1,0,
    +1,+1,      1,1,0,

    0,0,        0,1,0,
    +1,+1,      0,1,0,
    +1,-1,      0,1,0,

    0,0,        0,1,1,
    +1,-1,      0,1,1,
    -1,-1,      0,1,1,

    0,0,        1,0,1,
    -1,-1,      1,0,1,
    -1,+1,      1,0,1,
};
// clang-format on

constexpr size_t kNumVertices = ArraySize(triangleData) / 5;

// A container for one or more vertex attributes.
class Container
{
  public:
    static constexpr size_t kSize = 1024;

    void open(void) { memset(mMemory, 0xff, kSize); }

    void fill(size_t numItem, size_t itemSize, const char *src, unsigned offset, unsigned stride)
    {
        while (numItem--)
        {
            ASSERT(offset + itemSize <= kSize);
            memcpy(mMemory + offset, src, itemSize);
            src += itemSize;
            offset += stride;
        }
    }

    virtual void close(void) {}
    virtual ~Container() {}
    virtual const char *getAddress() = 0;
    virtual GLuint getBuffer()       = 0;

  protected:
    char mMemory[kSize];
};

// Vertex attribute data in client memory.
class Memory : public Container
{
  public:
    const char *getAddress() override { return mMemory; }
    GLuint getBuffer() override { return 0; }
};

// Vertex attribute data in buffer object.
class Buffer : public Container
{
  public:
    void close(void) override
    {
        glBindBuffer(GL_ARRAY_BUFFER, mBuffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(mMemory), mMemory, GL_STATIC_DRAW);
    }

    const char *getAddress() override { return nullptr; }
    GLuint getBuffer() override { return mBuffer; }

  protected:
    GLBuffer mBuffer;
};

// clang-format off
template<class Type> struct GLType {};
template<> struct GLType<GLbyte  > { static constexpr GLenum kGLType = GL_BYTE; };
template<> struct GLType<GLubyte > { static constexpr GLenum kGLType = GL_UNSIGNED_BYTE; };
template<> struct GLType<GLshort > { static constexpr GLenum kGLType = GL_SHORT; };
template<> struct GLType<GLushort> { static constexpr GLenum kGLType = GL_UNSIGNED_SHORT; };
template<> struct GLType<GLfloat > { static constexpr GLenum kGLType = GL_FLOAT; };
// clang-format on

// Encapsulates the data for one vertex attribute, where it lives, and how it is layed out.
class Attrib
{
  public:
    template <class T>
    Attrib(std::shared_ptr<Container> container, unsigned offset, unsigned stride, const T &data)
        : mContainer(container),
          mOffset(offset),
          mStride(stride),
          mData(reinterpret_cast<const char *>(data.data())),
          mDimension(data.size() / kNumVertices),
          mAttribSize(mDimension * sizeof(typename T::value_type)),
          mGLType(GLType<typename T::value_type>::kGLType)
    {
        // Compiler complains about unused variable without these.
        (void)GLType<GLbyte>::kGLType;
        (void)GLType<GLubyte>::kGLType;
        (void)GLType<GLshort>::kGLType;
        (void)GLType<GLushort>::kGLType;
        (void)GLType<GLfloat>::kGLType;
    }

    void openContainer(void) const { mContainer->open(); }
    void fillContainer(void) const
    {
        mContainer->fill(kNumVertices, mAttribSize, mData, mOffset, mStride);
    }
    void closeContainer(void) const { mContainer->close(); }

    void enable(unsigned index) const
    {
        glBindBuffer(GL_ARRAY_BUFFER, mContainer->getBuffer());
        glVertexAttribPointer(index, static_cast<int>(mDimension), mGLType, GL_FALSE, mStride,
                              mContainer->getAddress() + mOffset);
        glEnableVertexAttribArray(index);
    }

    bool inClientMemory(void) const { return mContainer->getAddress() != nullptr; }

  protected:
    std::shared_ptr<Container> mContainer;
    unsigned mOffset;
    unsigned mStride;
    const char *mData;
    size_t mDimension;
    size_t mAttribSize;
    GLenum mGLType;
};

typedef std::vector<Attrib> TestCase;

bool UsesClientMemory(const TestCase &tc)
{
    for (const Attrib &a : tc)
    {
        if (a.inClientMemory())
            return true;
    }
    return false;
}

void PrepareTestCase(const TestCase &tc)
{
    for (const Attrib &a : tc)
    {
        a.openContainer();
    }
    for (const Attrib &a : tc)
    {
        a.fillContainer();
    }
    for (const Attrib &a : tc)
    {
        a.closeContainer();
    }
    unsigned i = 0;
    for (const Attrib &a : tc)
    {
        a.enable(i++);
    }
}

template <class Type, size_t Dimension>
using VertexData = std::array<Type, Dimension * kNumVertices>;

class AttributeLayoutTest : public ANGLETest
{
  protected:
    AttributeLayoutTest() : mProgram(0)
    {
        setWindowWidth(128);
        setWindowHeight(128);
        setConfigRedBits(8);
        setConfigGreenBits(8);
        setConfigBlueBits(8);
        setConfigAlphaBits(8);
    }

    void PrepareVertexData(void);
    void GetTestCases(void);

    void SetUp() override
    {
        ANGLETest::SetUp();

        glClearColor(.2f, .2f, .2f, .0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glDisable(GL_DEPTH_TEST);

        const std::string vertexSource =
            "attribute mediump vec2 coord;\n"
            "attribute mediump vec3 color;\n"
            "varying mediump vec3 vcolor;\n"
            "void main(void)\n"
            "{\n"
            "    gl_Position = vec4(coord, 0, 1);\n"
            "    vcolor = color;\n"
            "}\n";

        const std::string fragmentSource =
            "varying mediump vec3 vcolor;\n"
            "void main(void)\n"
            "{\n"
            "    gl_FragColor = vec4(vcolor, 0);\n"
            "}\n";

        mProgram = CompileProgram(vertexSource, fragmentSource);
        ASSERT_NE(0u, mProgram);
        glUseProgram(mProgram);

        glGenBuffers(1, &mIndexBuffer);

        PrepareVertexData();
        GetTestCases();
    }

    void TearDown() override
    {
        mTestCases.clear();
        glDeleteProgram(mProgram);
        glDeleteBuffers(1, &mIndexBuffer);
        ANGLETest::TearDown();
    }

    virtual bool Skip(const TestCase &) { return false; }
    virtual void Draw(int firstVertex, unsigned vertexCount, const GLushort *indices) = 0;

    void Run(bool drawFirstTriangle)
    {
        glViewport(0, 0, getWindowWidth(), getWindowHeight());
        glUseProgram(mProgram);

        for (unsigned i = 0; i < mTestCases.size(); ++i)
        {
            if (Skip(mTestCases[i]))
                continue;

            PrepareTestCase(mTestCases[i]);

            glClear(GL_COLOR_BUFFER_BIT);

            std::string testCase;
            if (drawFirstTriangle)
            {
                Draw(0, kNumVertices, mIndices);
                testCase = "draw";
            }
            else
            {
                Draw(3, kNumVertices - 3, mIndices + 3);
                testCase = "skip";
            }

            testCase += " first triangle case ";
            int w = getWindowWidth() / 4;
            int h = getWindowHeight() / 4;
            if (drawFirstTriangle)
            {
                EXPECT_PIXEL_EQ(w * 2, h * 3, 255, 255, 0, 0) << testCase << i;
            }
            else
            {
                EXPECT_PIXEL_EQ(w * 2, h * 3, 51, 51, 51, 0) << testCase << i;
            }
            EXPECT_PIXEL_EQ(w * 3, h * 2, 0, 255, 0, 0) << testCase << i;
            EXPECT_PIXEL_EQ(w * 2, h * 1, 0, 255, 255, 0) << testCase << i;
            EXPECT_PIXEL_EQ(w * 1, h * 2, 255, 0, 255, 0) << testCase << i;
        }
    }

    static const GLushort mIndices[kNumVertices];

    GLuint mProgram;
    GLuint mIndexBuffer;

    std::vector<TestCase> mTestCases;

    VertexData<GLfloat, 2> mCoord;
    VertexData<GLfloat, 3> mColor;
    VertexData<GLbyte, 3> mBColor;
};
const GLushort AttributeLayoutTest::mIndices[kNumVertices] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

void AttributeLayoutTest::PrepareVertexData(void)
{
    mCoord.fill(0);
    mColor.fill(0);
    mBColor.fill(0);

    for (unsigned i = 0; i < kNumVertices; ++i)
    {
        GLfloat x = triangleData[i * 5 + 0];
        GLfloat y = triangleData[i * 5 + 1];
        GLfloat r = triangleData[i * 5 + 2];
        GLfloat g = triangleData[i * 5 + 3];
        GLfloat b = triangleData[i * 5 + 4];

        mCoord[i * 2 + 0] = x;
        mCoord[i * 2 + 1] = y;

        mColor[i * 3 + 0] = r;
        mColor[i * 3 + 1] = g;
        mColor[i * 3 + 2] = b;

        mBColor[i * 3 + 0] = r;
        mBColor[i * 3 + 1] = g;
        mBColor[i * 3 + 2] = b;
    }
}

void AttributeLayoutTest::GetTestCases(void)
{
    std::shared_ptr<Container> M0 = std::make_shared<Memory>();
    std::shared_ptr<Container> M1 = std::make_shared<Memory>();
    std::shared_ptr<Container> B0 = std::make_shared<Buffer>();
    std::shared_ptr<Container> B1 = std::make_shared<Buffer>();

    // 0. two buffers
    mTestCases.push_back({Attrib(B0, 0, 8, mCoord), Attrib(B1, 0, 12, mColor)});

    // 1. two memory
    mTestCases.push_back({Attrib(M0, 0, 8, mCoord), Attrib(M1, 0, 12, mColor)});

    // 2. one memory, sequential
    mTestCases.push_back({Attrib(M0, 0, 8, mCoord), Attrib(M0, 96, 12, mColor)});

    // 3. one memory, interleaved
    mTestCases.push_back({Attrib(M0, 0, 20, mCoord), Attrib(M0, 8, 20, mColor)});

    // 4. buffer and memory
    mTestCases.push_back({Attrib(B0, 0, 8, mCoord), Attrib(M0, 0, 12, mColor)});

    // 5. stride != size
    mTestCases.push_back({Attrib(B0, 0, 16, mCoord), Attrib(B1, 0, 12, mColor)});

    if (IsVulkan())
    {
        std::cout << "cases skipped on Vulkan: integer data, non-zero buffer offsets" << std::endl;
        return;
    }

    // 6. one buffer, sequential
    mTestCases.push_back({Attrib(B0, 0, 8, mCoord), Attrib(B0, 96, 12, mColor)});

    // 7. one buffer, interleaved
    mTestCases.push_back({Attrib(B0, 0, 20, mCoord), Attrib(B0, 8, 20, mColor)});

    // 8. memory and buffer, float and integer
    mTestCases.push_back({Attrib(M0, 0, 8, mCoord), Attrib(B0, 0, 12, mBColor)});

    // 9. buffer and memory, unusual offset and stride
    mTestCases.push_back({Attrib(B0, 11, 13, mCoord), Attrib(M0, 23, 17, mColor)});
}

class AttributeLayoutNonIndexed : public AttributeLayoutTest
{
    void Draw(int firstVertex, unsigned vertexCount, const GLushort *indices) override
    {
        glDrawArrays(GL_TRIANGLES, firstVertex, vertexCount);
    }
};

class AttributeLayoutMemoryIndexed : public AttributeLayoutTest
{
    void Draw(int firstVertex, unsigned vertexCount, const GLushort *indices) override
    {
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        glDrawElements(GL_TRIANGLES, vertexCount, GL_UNSIGNED_SHORT, indices);
    }
};

class AttributeLayoutBufferIndexed : public AttributeLayoutTest
{
    bool Skip(const TestCase &tc) override
    {
        if (IsVulkan() && UsesClientMemory(tc))
        {
            std::cout
                << "test case skipped on Vulkan: indexed draw with vertex data in client memory"
                << std::endl;
            return true;
        }
        return false;
    }

    void Draw(int firstVertex, unsigned vertexCount, const GLushort *indices) override
    {
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuffer);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(*mIndices) * vertexCount, indices,
                     GL_STATIC_DRAW);
        glDrawElements(GL_TRIANGLES, vertexCount, GL_UNSIGNED_SHORT, nullptr);
    }
};

TEST_P(AttributeLayoutNonIndexed, Test)
{
    Run(true);

    if (IsWindows() && IsAMD() && IsOpenGL())
    {
        std::cout << "test skipped on Windows ATI OpenGL: non-indexed non-zero vertex start"
                  << std::endl;
        return;
    }

    Run(false);
}

TEST_P(AttributeLayoutMemoryIndexed, Test)
{
    if (IsVulkan())
    {
        std::cout << "test skipped on Vulkan: indices in memory not supported" << std::endl;
        return;
    }

    Run(true);

    if (IsWindows() && IsAMD() && (IsOpenGL() || GetParam() == ES2_D3D11_FL9_3()))
    {
        std::cout << "test skipped on Windows ATI OpenGL and D3D11_9_3: indexed non-zero vertex start"
                  << std::endl;
        return;
    }

    Run(false);
}

TEST_P(AttributeLayoutBufferIndexed, Test)
{

    Run(true);

    if (IsWindows() && IsAMD() && (IsOpenGL() || GetParam() == ES2_D3D11_FL9_3()))
    {
        std::cout << "test skipped on Windows ATI OpenGL and D3D11_9_3: indexed non-zero vertex start"
                  << std::endl;
        return;
    }

    Run(false);
}

#define PARAMS                                                                            \
    ES2_VULKAN(), ES2_OPENGL(), ES2_D3D9(), ES2_D3D11(), ES2_D3D11_FL9_3(), ES3_OPENGL(), \
        ES2_OPENGLES(), ES3_OPENGLES()

ANGLE_INSTANTIATE_TEST(AttributeLayoutNonIndexed, PARAMS);
ANGLE_INSTANTIATE_TEST(AttributeLayoutMemoryIndexed, PARAMS);
ANGLE_INSTANTIATE_TEST(AttributeLayoutBufferIndexed, PARAMS);

}  // anonymous namespace
