//
// Copyright (c) 2013 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.
//
// ValidateOutputs validates fragment shader outputs. It checks for conflicting locations,
// out-of-range locations, that locations are specified when using multiple outputs, and YUV output
// validity.

#include "compiler/translator/ValidateOutputs.h"

#include <set>

#include "compiler/translator/InfoSink.h"
#include "compiler/translator/ParseContext.h"
#include "compiler/translator/tree_util/IntermTraverse.h"

namespace sh
{

namespace
{

void error(const TIntermSymbol &symbol, const char *reason, TDiagnostics *diagnostics)
{
    diagnostics->error(symbol.getLine(), reason, symbol.getName().data());
}

class ValidateOutputsTraverser : public TIntermTraverser
{
  public:
    ValidateOutputsTraverser(const TExtensionBehavior &extBehavior, int maxDrawBuffers);

    void validate(TDiagnostics *diagnostics) const;

    void visitSymbol(TIntermSymbol *) override;

  private:
    int mMaxDrawBuffers;
    bool mAllowUnspecifiedOutputLocationResolution;
    bool mUsesFragDepth;

    typedef std::vector<TIntermSymbol *> OutputVector;
    OutputVector mOutputs;
    OutputVector mUnspecifiedLocationOutputs;
    OutputVector mYuvOutputs;
    std::set<int> mVisitedSymbols;  // Visited symbol ids.
};

ValidateOutputsTraverser::ValidateOutputsTraverser(const TExtensionBehavior &extBehavior,
                                                   int maxDrawBuffers)
    : TIntermTraverser(true, false, false),
      mMaxDrawBuffers(maxDrawBuffers),
      mAllowUnspecifiedOutputLocationResolution(
          IsExtensionEnabled(extBehavior, TExtension::EXT_blend_func_extended)),
      mUsesFragDepth(false)
{}

void ValidateOutputsTraverser::visitSymbol(TIntermSymbol *symbol)
{
    if (symbol->variable().symbolType() == SymbolType::Empty)
        return;

    if (mVisitedSymbols.count(symbol->uniqueId().get()) == 1)
        return;

    mVisitedSymbols.insert(symbol->uniqueId().get());

    TQualifier qualifier = symbol->getQualifier();
    if (qualifier == EvqFragmentOut)
    {
        if (symbol->getType().getLayoutQualifier().location != -1)
        {
            mOutputs.push_back(symbol);
        }
        else if (symbol->getType().getLayoutQualifier().yuv == true)
        {
            mYuvOutputs.push_back(symbol);
        }
        else
        {
            mUnspecifiedLocationOutputs.push_back(symbol);
        }
    }
    else if (qualifier == EvqFragDepth || qualifier == EvqFragDepthEXT)
    {
        mUsesFragDepth = true;
    }
}

void ValidateOutputsTraverser::validate(TDiagnostics *diagnostics) const
{
    ASSERT(diagnostics);
    OutputVector validOutputs(mMaxDrawBuffers, nullptr);
    OutputVector validSecondaryOutputs(mMaxDrawBuffers, nullptr);

    for (const auto &symbol : mOutputs)
    {
        const TType &type = symbol->getType();
        ASSERT(!type.isArrayOfArrays());  // Disallowed in GLSL ES 3.10 section 4.3.6.
        const size_t elementCount =
            static_cast<size_t>(type.isArray() ? type.getOutermostArraySize() : 1u);
        const size_t location = static_cast<size_t>(type.getLayoutQualifier().location);

        ASSERT(type.getLayoutQualifier().location != -1);

        OutputVector *validOutputsToUse = &validOutputs;
        // The default index is 0, so we only assign the output to secondary outputs in case the
        // index is explicitly set to 1.
        if (type.getLayoutQualifier().index == 1)
        {
            validOutputsToUse = &validSecondaryOutputs;
        }

        if (location + elementCount <= validOutputsToUse->size())
        {
            for (size_t elementIndex = 0; elementIndex < elementCount; elementIndex++)
            {
                const size_t offsetLocation = location + elementIndex;
                if ((*validOutputsToUse)[offsetLocation])
                {
                    std::stringstream strstr = sh::InitializeStream<std::stringstream>();
                    strstr << "conflicting output locations with previously defined output '"
                           << (*validOutputsToUse)[offsetLocation]->getName() << "'";
                    error(*symbol, strstr.str().c_str(), diagnostics);
                }
                else
                {
                    (*validOutputsToUse)[offsetLocation] = symbol;
                }
            }
        }
        else
        {
            if (elementCount > 0)
            {
                error(*symbol,
                      elementCount > 1 ? "output array locations would exceed MAX_DRAW_BUFFERS"
                                       : "output location must be < MAX_DRAW_BUFFERS",
                      diagnostics);
            }
        }
    }

    if (!mAllowUnspecifiedOutputLocationResolution &&
        ((!mOutputs.empty() && !mUnspecifiedLocationOutputs.empty()) ||
         mUnspecifiedLocationOutputs.size() > 1))
    {
        for (const auto &symbol : mUnspecifiedLocationOutputs)
        {
            error(*symbol,
                  "must explicitly specify all locations when using multiple fragment outputs",
                  diagnostics);
        }
    }

    if (!mYuvOutputs.empty() && (mYuvOutputs.size() > 1 || mUsesFragDepth || !mOutputs.empty() ||
                                 !mUnspecifiedLocationOutputs.empty()))
    {
        for (const auto &symbol : mYuvOutputs)
        {
            error(*symbol,
                  "not allowed to specify yuv qualifier when using depth or multiple color "
                  "fragment outputs",
                  diagnostics);
        }
    }
}

}  // anonymous namespace

bool ValidateOutputs(TIntermBlock *root,
                     const TExtensionBehavior &extBehavior,
                     int maxDrawBuffers,
                     TDiagnostics *diagnostics)
{
    ValidateOutputsTraverser validateOutputs(extBehavior, maxDrawBuffers);
    root->traverse(&validateOutputs);
    int numErrorsBefore = diagnostics->numErrors();
    validateOutputs.validate(diagnostics);
    return (diagnostics->numErrors() == numErrorsBefore);
}

}  // namespace sh
