//
// Copyright (c) 2017 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.
//
// InitOutputVariables_test.cpp: Tests correctness of the AST pass enabled through
// SH_INIT_OUTPUT_VARIABLES.
//

#include "common/angleutils.h"

#include "compiler/translator/FindMain.h"
#include "compiler/translator/IntermNode_util.h"
#include "compiler/translator/IntermTraverse.h"
#include "compiler/translator/SymbolTable.h"
#include "tests/test_utils/ShaderCompileTreeTest.h"

#include <algorithm>

namespace sh
{

namespace
{

typedef std::vector<TIntermTyped *> ExpectedLValues;

bool AreSymbolsTheSame(const TIntermSymbol *expected, const TIntermSymbol *candidate)
{
    if (expected == nullptr || candidate == nullptr)
    {
        return false;
    }
    const TType &expectedType  = expected->getType();
    const TType &candidateType = candidate->getType();
    const bool sameTypes       = expectedType == candidateType &&
                           expectedType.getPrecision() == candidateType.getPrecision() &&
                           expectedType.getQualifier() == candidateType.getQualifier();
    const bool sameSymbols = expected->getSymbol() == candidate->getSymbol();
    return sameSymbols && sameTypes;
}

bool AreLValuesTheSame(TIntermTyped *expected, TIntermTyped *candidate)
{
    const TIntermBinary *expectedBinary = expected->getAsBinaryNode();
    if (expectedBinary)
    {
        ASSERT(expectedBinary->getOp() == EOpIndexDirect);
        const TIntermBinary *candidateBinary = candidate->getAsBinaryNode();
        if (candidateBinary == nullptr || candidateBinary->getOp() != EOpIndexDirect)
        {
            return false;
        }
        if (expectedBinary->getRight()->getAsConstantUnion()->getIConst(0) !=
            candidateBinary->getRight()->getAsConstantUnion()->getIConst(0))
        {
            return false;
        }
        return AreSymbolsTheSame(expectedBinary->getLeft()->getAsSymbolNode(),
                                 candidateBinary->getLeft()->getAsSymbolNode());
    }
    return AreSymbolsTheSame(expected->getAsSymbolNode(), candidate->getAsSymbolNode());
}

TIntermTyped *CreateLValueNode(const TString &lValueName, const TType &type)
{
    // We're using a dummy symbol table here, don't need to assign proper symbol ids to these nodes.
    TSymbolTable symbolTable;
    return new TIntermSymbol(symbolTable.nextUniqueId(), lValueName, type);
}

ExpectedLValues CreateIndexedLValueNodeList(const TString &lValueName,
                                            TType elementType,
                                            unsigned arraySize)
{
    ASSERT(elementType.isArray() == false);
    elementType.makeArray(arraySize);

    // We're using a dummy symbol table here, don't need to assign proper symbol ids to these nodes.
    TSymbolTable symbolTable;
    TIntermSymbol *arraySymbol =
        new TIntermSymbol(symbolTable.nextUniqueId(), lValueName, elementType);

    ExpectedLValues expected(arraySize);
    for (unsigned index = 0u; index < arraySize; ++index)
    {
        expected[index] = new TIntermBinary(EOpIndexDirect, arraySymbol->deepCopy(),
                                            CreateIndexNode(static_cast<int>(index)));
    }
    return expected;
}

// VerifyOutputVariableInitializers traverses the subtree covering main and collects the lvalues in
// assignments for which the rvalue is an expression containing only zero constants.
class VerifyOutputVariableInitializers final : public TIntermTraverser
{
  public:
    VerifyOutputVariableInitializers(TIntermBlock *root) : TIntermTraverser(true, false, false)
    {
        ASSERT(root != nullptr);

        // The traversal starts in the body of main because this is where the varyings and output
        // variables are initialized.
        sh::TIntermFunctionDefinition *main = FindMain(root);
        ASSERT(main != nullptr);
        main->traverse(this);
    }

    bool visitBinary(Visit visit, TIntermBinary *node) override
    {
        if (node->getOp() == EOpAssign && IsZero(node->getRight()))
        {
            mCandidateLValues.push_back(node->getLeft());
            return false;
        }
        return true;
    }

    // The collected lvalues are considered valid if every expected lvalue in expectedLValues is
    // matched by name and type with any lvalue in mCandidateLValues.
    bool areAllExpectedLValuesFound(const ExpectedLValues &expectedLValues) const
    {
        for (size_t i = 0u; i < expectedLValues.size(); ++i)
        {
            if (!isExpectedLValueFound(expectedLValues[i]))
            {
                return false;
            }
        }
        return true;
    }

    bool isExpectedLValueFound(TIntermTyped *expectedLValue) const
    {
        bool isFound = false;
        for (size_t j = 0; j < mCandidateLValues.size() && !isFound; ++j)
        {
            isFound = AreLValuesTheSame(expectedLValue, mCandidateLValues[j]);
        }
        return isFound;
    }

    const ExpectedLValues &getCandidates() const { return mCandidateLValues; }

  private:
    ExpectedLValues mCandidateLValues;
};

// Traverses the AST and records a pointer to a structure with a given name.
class FindStructByName final : public TIntermTraverser
{
  public:
    FindStructByName(const TString &structName)
        : TIntermTraverser(true, false, false), mStructName(structName), mStructure(nullptr)
    {
    }

    void visitSymbol(TIntermSymbol *symbol) override
    {
        if (isStructureFound())
        {
            return;
        }

        TStructure *structure = symbol->getTypePointer()->getStruct();

        if (structure != nullptr && structure->name() == mStructName)
        {
            mStructure = structure;
        }
    }

    bool isStructureFound() const { return mStructure != nullptr; };
    TStructure *getStructure() const { return mStructure; }

  private:
    TString mStructName;
    TStructure *mStructure;
};

}  // namespace

class InitOutputVariablesWebGL2Test : public ShaderCompileTreeTest
{
  public:
    void SetUp() override
    {
        mExtraCompileOptions |= SH_VARIABLES;
        mExtraCompileOptions |= SH_INIT_OUTPUT_VARIABLES;
        if (getShaderType() == GL_VERTEX_SHADER)
        {
            mExtraCompileOptions |= SH_INIT_GL_POSITION;
        }
        ShaderCompileTreeTest::SetUp();
    }

  protected:
    ShShaderSpec getShaderSpec() const override { return SH_WEBGL2_SPEC; }
};

class InitOutputVariablesWebGL2VertexShaderTest : public InitOutputVariablesWebGL2Test
{
  protected:
    ::GLenum getShaderType() const override { return GL_VERTEX_SHADER; }
};

class InitOutputVariablesWebGL2FragmentShaderTest : public InitOutputVariablesWebGL2Test
{
  protected:
    ::GLenum getShaderType() const override { return GL_FRAGMENT_SHADER; }
    void initResources(ShBuiltInResources *resources) override
    {
        resources->EXT_draw_buffers = 1;
        resources->MaxDrawBuffers   = 2;
    }
};

class InitOutputVariablesWebGL1FragmentShaderTest : public ShaderCompileTreeTest
{
  public:
    InitOutputVariablesWebGL1FragmentShaderTest()
    {
        mExtraCompileOptions |= SH_VARIABLES;
        mExtraCompileOptions |= SH_INIT_OUTPUT_VARIABLES;
    }

  protected:
    ::GLenum getShaderType() const override { return GL_FRAGMENT_SHADER; }
    ShShaderSpec getShaderSpec() const override { return SH_WEBGL_SPEC; }
    void initResources(ShBuiltInResources *resources) override
    {
        resources->EXT_draw_buffers = 1;
        resources->MaxDrawBuffers   = 2;
    }
};

// Test the initialization of output variables with various qualifiers in a vertex shader.
TEST_F(InitOutputVariablesWebGL2VertexShaderTest, OutputAllQualifiers)
{
    const std::string &shaderString =
        "#version 300 es\n"
        "precision mediump float;\n"
        "precision lowp int;\n"
        "out vec4 out1;\n"
        "flat out int out2;\n"
        "centroid out float out3;\n"
        "smooth out float out4;\n"
        "void main() {\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    ExpectedLValues expectedLValues = {
        CreateLValueNode("out1", TType(EbtFloat, EbpMedium, EvqVertexOut, 4)),
        CreateLValueNode("out2", TType(EbtInt, EbpLow, EvqFlatOut)),
        CreateLValueNode("out3", TType(EbtFloat, EbpMedium, EvqCentroidOut)),
        CreateLValueNode("out4", TType(EbtFloat, EbpMedium, EvqSmoothOut))};
    EXPECT_TRUE(verifier.areAllExpectedLValuesFound(expectedLValues));
}

// Test the initialization of an output array in a vertex shader.
TEST_F(InitOutputVariablesWebGL2VertexShaderTest, OutputArray)
{
    const std::string &shaderString =
        "#version 300 es\n"
        "precision mediump float;\n"
        "out float out1[2];\n"
        "void main() {\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    ExpectedLValues expectedLValues =
        CreateIndexedLValueNodeList("out1", TType(EbtFloat, EbpMedium, EvqVertexOut), 2);
    EXPECT_TRUE(verifier.areAllExpectedLValuesFound(expectedLValues));
}

// Test the initialization of a struct output variable in a vertex shader.
TEST_F(InitOutputVariablesWebGL2VertexShaderTest, OutputStruct)
{
    const std::string &shaderString =
        "#version 300 es\n"
        "precision mediump float;\n"
        "struct MyS{\n"
        "   float a;\n"
        "   float b;\n"
        "};\n"
        "out MyS out1;\n"
        "void main() {\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    FindStructByName findStruct("MyS");
    mASTRoot->traverse(&findStruct);
    ASSERT(findStruct.isStructureFound());

    TType type(EbtStruct, EbpUndefined, EvqVertexOut);
    type.setStruct(findStruct.getStructure());

    TIntermTyped *expectedLValue = CreateLValueNode("out1", type);
    EXPECT_TRUE(verifier.isExpectedLValueFound(expectedLValue));
    delete expectedLValue;
}

// Test the initialization of a varying variable in an ESSL1 vertex shader.
TEST_F(InitOutputVariablesWebGL2VertexShaderTest, OutputFromESSL1Shader)
{
    const std::string &shaderString =
        "precision mediump float;\n"
        "varying vec4 out1;\n"
        "void main() {\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    TIntermTyped *expectedLValue =
        CreateLValueNode("out1", TType(EbtFloat, EbpMedium, EvqVaryingOut, 4));
    EXPECT_TRUE(verifier.isExpectedLValueFound(expectedLValue));
    delete expectedLValue;
}

// Test the initialization of output variables in a fragment shader.
TEST_F(InitOutputVariablesWebGL2FragmentShaderTest, Output)
{
    const std::string &shaderString =
        "#version 300 es\n"
        "precision mediump float;\n"
        "out vec4 out1;\n"
        "void main() {\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    TIntermTyped *expectedLValue =
        CreateLValueNode("out1", TType(EbtFloat, EbpMedium, EvqFragmentOut, 4));
    EXPECT_TRUE(verifier.isExpectedLValueFound(expectedLValue));
    delete expectedLValue;
}

// Test the initialization of gl_FragData in a WebGL2 ESSL1 fragment shader. Only writes to
// gl_FragData[0] should be found.
TEST_F(InitOutputVariablesWebGL2FragmentShaderTest, FragData)
{
    const std::string &shaderString =
        "precision mediump float;\n"
        "void main() {\n"
        "   gl_FragData[0] = vec4(1.);\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    ExpectedLValues expectedLValues =
        CreateIndexedLValueNodeList("gl_FragData", TType(EbtFloat, EbpMedium, EvqFragData, 4), 1);
    EXPECT_TRUE(verifier.isExpectedLValueFound(expectedLValues[0]));
    EXPECT_EQ(1u, verifier.getCandidates().size());
}

// Test the initialization of gl_FragData in a WebGL1 ESSL1 fragment shader. Only writes to
// gl_FragData[0] should be found.
TEST_F(InitOutputVariablesWebGL1FragmentShaderTest, FragData)
{
    const std::string &shaderString =
        "precision mediump float;\n"
        "void main() {\n"
        "   gl_FragData[0] = vec4(1.);\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    // In the symbol table, gl_FragData array has 2 elements. However, only the 1st one should be
    // initialized.
    ExpectedLValues expectedLValues =
        CreateIndexedLValueNodeList("gl_FragData", TType(EbtFloat, EbpMedium, EvqFragData, 4), 2);
    EXPECT_TRUE(verifier.isExpectedLValueFound(expectedLValues[0]));
    EXPECT_EQ(1u, verifier.getCandidates().size());
}

// Test the initialization of gl_FragData in a WebGL1 ESSL1 fragment shader with GL_EXT_draw_buffers
// enabled. All attachment slots should be initialized.
TEST_F(InitOutputVariablesWebGL1FragmentShaderTest, FragDataWithDrawBuffersExtEnabled)
{
    const std::string &shaderString =
        "#extension GL_EXT_draw_buffers : enable\n"
        "precision mediump float;\n"
        "void main() {\n"
        "   gl_FragData[0] = vec4(1.);\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    ExpectedLValues expectedLValues =
        CreateIndexedLValueNodeList("gl_FragData", TType(EbtFloat, EbpMedium, EvqFragData, 4), 2);
    EXPECT_TRUE(verifier.isExpectedLValueFound(expectedLValues[0]));
    EXPECT_TRUE(verifier.isExpectedLValueFound(expectedLValues[1]));
    EXPECT_EQ(2u, verifier.getCandidates().size());
}

// Test that gl_Position is initialized once in case it is not statically used and both
// SH_INIT_OUTPUT_VARIABLES and SH_INIT_GL_POSITION flags are set.
TEST_F(InitOutputVariablesWebGL2VertexShaderTest, InitGLPositionWhenNotStaticallyUsed)
{
    const std::string &shaderString =
        "#version 300 es\n"
        "precision highp float;\n"
        "void main() {\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    TIntermTyped *glPosition =
        CreateLValueNode("gl_Position", TType(EbtFloat, EbpHigh, EvqPosition, 4));
    EXPECT_TRUE(verifier.isExpectedLValueFound(glPosition));
    EXPECT_EQ(1u, verifier.getCandidates().size());
}

// Test that gl_Position is initialized once in case it is statically used and both
// SH_INIT_OUTPUT_VARIABLES and SH_INIT_GL_POSITION flags are set.
TEST_F(InitOutputVariablesWebGL2VertexShaderTest, InitGLPositionOnceWhenStaticallyUsed)
{
    const std::string &shaderString =
        "#version 300 es\n"
        "precision highp float;\n"
        "void main() {\n"
        "    gl_Position = vec4(1.0);\n"
        "}\n";
    compileAssumeSuccess(shaderString);
    VerifyOutputVariableInitializers verifier(mASTRoot);

    TIntermTyped *glPosition =
        CreateLValueNode("gl_Position", TType(EbtFloat, EbpHigh, EvqPosition, 4));
    EXPECT_TRUE(verifier.isExpectedLValueFound(glPosition));
    EXPECT_EQ(1u, verifier.getCandidates().size());
}

}  // namespace sh
