//
// Copyright (c) 2002-2016 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.
//
// ValidateMultiviewWebGL.cpp:
//   Validate the AST according to rules in the WEBGL_multiview extension spec. Only covers those
//   rules not already covered in ParseContext.
//

#include "compiler/translator/ValidateMultiviewWebGL.h"

#include <array>

#include "angle_gl.h"
#include "compiler/translator/Diagnostics.h"
#include "compiler/translator/FindSymbolNode.h"
#include "compiler/translator/IntermNode.h"
#include "compiler/translator/SymbolTable.h"

namespace sh
{

namespace
{

class ValidateMultiviewTraverser : public TIntermTraverser
{
  public:
    // Check for errors and write error messages to diagnostics. Returns true if there are no
    // errors.
    static bool validate(TIntermBlock *root,
                         GLenum shaderType,
                         bool multiview2,
                         TDiagnostics *diagnostics);

    bool isValid() { return mValid; }

  protected:
    void visitSymbol(TIntermSymbol *node) override;
    bool visitBinary(Visit visit, TIntermBinary *node) override;
    bool visitUnary(Visit visit, TIntermUnary *node) override;
    bool visitIfElse(Visit visit, TIntermIfElse *node) override;
    bool visitAggregate(Visit visit, TIntermAggregate *node) override;

  private:
    ValidateMultiviewTraverser(GLenum shaderType, bool multiview2, TDiagnostics *diagnostics);

    static bool IsGLPosition(TIntermNode *node);
    static bool IsGLViewIDOVR(TIntermNode *node);
    static bool IsSimpleAssignmentToGLPositionX(TIntermBinary *node);
    static bool IsSimpleAssignmentToGLPosition(TIntermBinary *node);

    void validateAndTraverseViewIDConditionalBlock(TIntermBlock *block, const char *token);

    bool mValid;
    bool mMultiview2;
    GLenum mShaderType;

    bool mInsideViewIDConditional;  // Only set if mMultiview2 is false.
    bool mInsideRestrictedAssignment;
    bool mGLPositionAllowed;
    bool mViewIDOVRAllowed;

    TDiagnostics *mDiagnostics;
};

bool ValidateMultiviewTraverser::validate(TIntermBlock *root,
                                          GLenum shaderType,
                                          bool multiview2,
                                          TDiagnostics *diagnostics)
{
    ValidateMultiviewTraverser validate(shaderType, multiview2, diagnostics);
    ASSERT(root);
    root->traverse(&validate);
    return validate.isValid();
}

ValidateMultiviewTraverser::ValidateMultiviewTraverser(GLenum shaderType,
                                                       bool multiview2,
                                                       TDiagnostics *diagnostics)
    : TIntermTraverser(true, true, true),
      mValid(true),
      mMultiview2(multiview2),
      mShaderType(shaderType),
      mInsideViewIDConditional(false),
      mInsideRestrictedAssignment(false),
      mGLPositionAllowed(multiview2),
      mViewIDOVRAllowed(multiview2),
      mDiagnostics(diagnostics)
{
}

bool ValidateMultiviewTraverser::IsGLPosition(TIntermNode *node)
{
    TIntermSymbol *symbolNode = node->getAsSymbolNode();
    return (symbolNode && symbolNode->getName().getString() == "gl_Position");
}

bool ValidateMultiviewTraverser::IsGLViewIDOVR(TIntermNode *node)
{
    TIntermSymbol *symbolNode = node->getAsSymbolNode();
    return (symbolNode && symbolNode->getName().getString() == "gl_ViewID_OVR");
}

bool ValidateMultiviewTraverser::IsSimpleAssignmentToGLPositionX(TIntermBinary *node)
{
    if (node->getOp() == EOpAssign)
    {
        TIntermSwizzle *leftAsSwizzle = node->getLeft()->getAsSwizzleNode();
        if (leftAsSwizzle && IsGLPosition(leftAsSwizzle->getOperand()) &&
            leftAsSwizzle->offsetsMatch(0))
        {
            return true;
        }
        TIntermBinary *leftAsBinary = node->getLeft()->getAsBinaryNode();
        if (leftAsBinary && leftAsBinary->getOp() == EOpIndexDirect &&
            IsGLPosition(leftAsBinary->getLeft()) &&
            leftAsBinary->getRight()->getAsConstantUnion()->getIConst(0) == 0)
        {
            return true;
        }
    }
    return false;
}

bool ValidateMultiviewTraverser::IsSimpleAssignmentToGLPosition(TIntermBinary *node)
{
    if (node->getOp() == EOpAssign)
    {
        if (IsGLPosition(node->getLeft()))
        {
            return true;
        }
        TIntermSwizzle *leftAsSwizzle = node->getLeft()->getAsSwizzleNode();
        if (leftAsSwizzle && IsGLPosition(leftAsSwizzle->getOperand()))
        {
            return true;
        }
        TIntermBinary *leftAsBinary = node->getLeft()->getAsBinaryNode();
        if (leftAsBinary && leftAsBinary->getOp() == EOpIndexDirect &&
            IsGLPosition(leftAsBinary->getLeft()))
        {
            return true;
        }
    }
    return false;
}

void ValidateMultiviewTraverser::visitSymbol(TIntermSymbol *node)
{
    if (IsGLPosition(node) && !mGLPositionAllowed)
    {
        ASSERT(!mMultiview2);
        mDiagnostics->error(node->getLine(),
                            "Disallowed use of gl_Position when using OVR_multiview",
                            "gl_Position");
        mValid = false;
    }
    else if (IsGLViewIDOVR(node) && !mViewIDOVRAllowed)
    {
        ASSERT(!mMultiview2);
        mDiagnostics->error(node->getLine(),
                            "Disallowed use of gl_ViewID_OVR when using OVR_multiview",
                            "gl_ViewID_OVR");
        mValid = false;
    }
    else if (!mMultiview2 && mShaderType == GL_FRAGMENT_SHADER)
    {
        std::array<const char *, 3> disallowedFragmentShaderSymbols{
            {"gl_FragCoord", "gl_PointCoord", "gl_FrontFacing"}};
        for (auto disallowedSymbol : disallowedFragmentShaderSymbols)
        {
            if (node->getSymbol() == disallowedSymbol)
            {
                mDiagnostics->error(
                    node->getLine(),
                    "Disallowed use of a built-in variable when using OVR_multiview",
                    disallowedSymbol);
                mValid = false;
            }
        }
    }
}

bool ValidateMultiviewTraverser::visitBinary(Visit visit, TIntermBinary *node)
{
    if (!mMultiview2 && mShaderType == GL_VERTEX_SHADER)
    {
        if (visit == PreVisit)
        {
            ASSERT(!mInsideViewIDConditional || mInsideRestrictedAssignment);
            if (node->isAssignment())
            {
                if (mInsideRestrictedAssignment)
                {
                    mDiagnostics->error(node->getLine(),
                                        "Disallowed assignment inside assignment to gl_Position.x "
                                        "when using OVR_multiview",
                                        GetOperatorString(node->getOp()));
                    mValid = false;
                }
                else if (IsSimpleAssignmentToGLPositionX(node) &&
                         FindSymbolNode(node->getRight(), "gl_ViewID_OVR", EbtUInt))
                {
                    if (!getParentNode()->getAsBlock())
                    {
                        mDiagnostics->error(node->getLine(),
                                            "Disallowed use of assignment to gl_Position.x "
                                            "when using OVR_multiview",
                                            "=");
                        mValid = false;
                    }
                    mInsideRestrictedAssignment = true;
                    mViewIDOVRAllowed           = true;
                    node->getRight()->traverse(this);
                    mInsideRestrictedAssignment = false;
                    mViewIDOVRAllowed           = false;
                    return false;
                }
                else if (IsSimpleAssignmentToGLPosition(node))
                {
                    node->getRight()->traverse(this);
                    return false;
                }
            }
        }
    }
    return true;
}

bool ValidateMultiviewTraverser::visitUnary(Visit visit, TIntermUnary *node)
{
    if (visit == PreVisit && !mMultiview2 && mInsideRestrictedAssignment)
    {
        if (node->isAssignment())
        {
            mDiagnostics->error(node->getLine(),
                                "Disallowed unary operator inside assignment to gl_Position.x when "
                                "using OVR_multiview",
                                GetOperatorString(node->getOp()));
            mValid = false;
        }
    }
    return true;
}

void ValidateMultiviewTraverser::validateAndTraverseViewIDConditionalBlock(TIntermBlock *block,
                                                                           const char *token)
{
    if (block->getSequence()->size() > 1)
    {
        mDiagnostics->error(block->getLine(),
                            "Only one assignment to gl_Position allowed inside if block dependent "
                            "on gl_ViewID_OVR when using OVR_multiview",
                            token);
        mValid = false;
    }
    else if (block->getSequence()->size() == 1)
    {
        TIntermBinary *assignment = block->getSequence()->at(0)->getAsBinaryNode();
        if (assignment && IsSimpleAssignmentToGLPositionX(assignment))
        {
            mInsideRestrictedAssignment = true;
            assignment->getRight()->traverse(this);
            mInsideRestrictedAssignment = false;
        }
        else
        {
            mDiagnostics->error(block->getLine(),
                                "Only one assignment to gl_Position.x allowed inside if block "
                                "dependent on gl_ViewID_OVR when using OVR_multiview",
                                token);
            mValid = false;
        }
    }
}

bool ValidateMultiviewTraverser::visitIfElse(Visit visit, TIntermIfElse *node)
{
    if (!mMultiview2 && mShaderType == GL_VERTEX_SHADER)
    {
        ASSERT(visit == PreVisit);

        // Check if the if statement refers to gl_ViewID_OVR and if it does so correctly
        TIntermBinary *binaryCondition              = node->getCondition()->getAsBinaryNode();
        bool conditionIsAllowedComparisonWithViewID = false;
        if (binaryCondition && binaryCondition->getOp() == EOpEqual)
        {
            conditionIsAllowedComparisonWithViewID =
                IsGLViewIDOVR(binaryCondition->getLeft()) &&
                binaryCondition->getRight()->getAsConstantUnion() &&
                binaryCondition->getRight()->getQualifier() == EvqConst;
            if (!conditionIsAllowedComparisonWithViewID)
            {
                conditionIsAllowedComparisonWithViewID =
                    IsGLViewIDOVR(binaryCondition->getRight()) &&
                    binaryCondition->getLeft()->getAsConstantUnion() &&
                    binaryCondition->getLeft()->getQualifier() == EvqConst;
            }
        }
        if (conditionIsAllowedComparisonWithViewID)
        {
            mInsideViewIDConditional = true;
            if (node->getTrueBlock())
            {
                validateAndTraverseViewIDConditionalBlock(node->getTrueBlock(), "if");
            }
            else
            {
                mDiagnostics->error(node->getLine(), "Expected assignment to gl_Position.x", "if");
            }
            if (node->getFalseBlock())
            {
                validateAndTraverseViewIDConditionalBlock(node->getFalseBlock(), "else");
            }
            mInsideViewIDConditional = false;
        }
        else
        {
            node->getCondition()->traverse(this);
            if (node->getTrueBlock())
            {
                node->getTrueBlock()->traverse(this);
            }
            if (node->getFalseBlock())
            {
                node->getFalseBlock()->traverse(this);
            }
        }
        return false;
    }
    return true;
}

bool ValidateMultiviewTraverser::visitAggregate(Visit visit, TIntermAggregate *node)
{
    if (visit == PreVisit && !mMultiview2 && mInsideRestrictedAssignment)
    {
        // Check if the node is an user-defined function call or if an l-value is required, or if
        // there are possible visible side effects, such as image writes.
        if (node->getOp() == EOpFunctionCall)
        {
            if (node->isUserDefined())
            {
                mDiagnostics->error(node->getLine(),
                                    "Disallowed user defined function call inside assignment to "
                                    "gl_Position.x when using OVR_multiview",
                                    GetOperatorString(node->getOp()));
                mValid = false;
            }
            else if (TFunction::unmangleName(node->getFunctionSymbolInfo()->getName()) ==
                     "imageStore")
            {
                // TODO(oetuaho@nvidia.com): Record which built-in functions have side effects in
                // the symbol info instead.
                mDiagnostics->error(node->getLine(),
                                    "Disallowed function call with side effects inside assignment "
                                    "to gl_Position.x when using OVR_multiview",
                                    GetOperatorString(node->getOp()));
                mValid = false;
            }
        }
        else if (node->getOp() == EOpModf)
        {
            // TODO(oetuaho@nvidia.com): It's quite hacky to hard-code modf - should maybe refactor
            // out parameter detecting functionality in LValueTrackingTraverser so that it could be
            // used here as well?
            // LValueTrackingTraverser itself seems like a bad fit with the needs of this traverser.
            mDiagnostics->error(node->getLine(),
                                "Disallowed use of a function with an out parameter inside "
                                "assignment to gl_Position.x when using OVR_multiview",
                                GetOperatorString(node->getOp()));
            mValid = false;
        }
    }
    return true;
}

}  // anonymous namespace

bool ValidateMultiviewWebGL(TIntermBlock *root,
                            GLenum shaderType,
                            bool multiview2,
                            TDiagnostics *diagnostics)
{
    if (shaderType == GL_VERTEX_SHADER && !FindSymbolNode(root, "gl_ViewID_OVR", EbtUInt))
    {
        return true;
    }
    return ValidateMultiviewTraverser::validate(root, shaderType, multiview2, diagnostics);
}

}  // namespace sh
