/*
 * Copyright 2010, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "slang_rs_pragma_handler.h"

#include <map>
#include <sstream>
#include <string>

#include "clang/AST/ASTContext.h"

#include "clang/Basic/TokenKinds.h"

#include "clang/Lex/LiteralSupport.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Lex/Token.h"

#include "slang_assert.h"
#include "slang_rs_context.h"
#include "slang_rs_export_reduce.h"
#include "slang_version.h"

namespace slang {

namespace {  // Anonymous namespace

class RSExportTypePragmaHandler : public RSPragmaHandler {
 private:
  void handleItem(const std::string &Item) {
    mContext->addPragma(this->getName(), Item);
    mContext->addExportType(Item);
  }

 public:
  RSExportTypePragmaHandler(llvm::StringRef Name, RSContext *Context)
      : RSPragmaHandler(Name, Context) { }

  void HandlePragma(clang::Preprocessor &PP,
                    clang::PragmaIntroducerKind Introducer,
                    clang::Token &FirstToken) {
    this->handleItemListPragma(PP, FirstToken);
  }
};

class RSJavaPackageNamePragmaHandler : public RSPragmaHandler {
 public:
  RSJavaPackageNamePragmaHandler(llvm::StringRef Name, RSContext *Context)
      : RSPragmaHandler(Name, Context) { }

  void HandlePragma(clang::Preprocessor &PP,
                    clang::PragmaIntroducerKind Introducer,
                    clang::Token &FirstToken) {
    // FIXME: Need to validate the extracted package name from pragma.
    // Currently "all chars" specified in pragma will be treated as package
    // name.
    //
    // 18.1 The Grammar of the Java Programming Language
    // (http://java.sun.com/docs/books/jls/third_edition/html/syntax.html#18.1)
    //
    // CompilationUnit:
    //     [[Annotations] package QualifiedIdentifier   ;  ] {ImportDeclaration}
    //     {TypeDeclaration}
    //
    // QualifiedIdentifier:
    //     Identifier { . Identifier }
    //
    // Identifier:
    //     IDENTIFIER
    //
    // 3.8 Identifiers
    // (http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.8)
    //
    //

    clang::Token &PragmaToken = FirstToken;
    std::string PackageName;

    // Skip first token, "java_package_name"
    PP.LexUnexpandedToken(PragmaToken);

    // Now, the current token must be clang::tok::lpara
    if (PragmaToken.isNot(clang::tok::l_paren))
      return;

    while (PragmaToken.isNot(clang::tok::eod)) {
      // Lex package name
      PP.LexUnexpandedToken(PragmaToken);

      bool Invalid;
      std::string Spelling = PP.getSpelling(PragmaToken, &Invalid);
      if (!Invalid)
        PackageName.append(Spelling);

      // Pre-mature end (syntax error will be triggered by preprocessor later)
      if (PragmaToken.is(clang::tok::eod) || PragmaToken.is(clang::tok::eof)) {
        break;
      } else {
        // Next token is ')' (end of pragma)
        const clang::Token &NextTok = PP.LookAhead(0);
        if (NextTok.is(clang::tok::r_paren)) {
          mContext->addPragma(this->getName(), PackageName);
          mContext->setReflectJavaPackageName(PackageName);
          // Lex until meets clang::tok::eod
          do {
            PP.LexUnexpandedToken(PragmaToken);
          } while (PragmaToken.isNot(clang::tok::eod));
          break;
        }
      }
    }
  }
};

class RSReducePragmaHandler : public RSPragmaHandler {
 public:
  RSReducePragmaHandler(llvm::StringRef Name, RSContext *Context)
      : RSPragmaHandler(Name, Context) { }

  void HandlePragma(clang::Preprocessor &PP,
                    clang::PragmaIntroducerKind Introducer,
                    clang::Token &FirstToken) override {
    // #pragma rs reduce(name)
    //   initializer(initializename)
    //   accumulator(accumulatename)
    //   combiner(combinename)
    //   outconverter(outconvertname)
    //   halter(haltname)

    const clang::SourceLocation PragmaLocation = FirstToken.getLocation();

    clang::Token &PragmaToken = FirstToken;

    // Grab "reduce(name)" ("reduce" is already known to be the first
    // token) and all the "keyword(value)" contributions
    KeywordValueMapType KeywordValueMap({std::make_pair(RSExportReduce::KeyReduce, ""),
                                         std::make_pair(RSExportReduce::KeyInitializer, ""),
                                         std::make_pair(RSExportReduce::KeyAccumulator, ""),
                                         std::make_pair(RSExportReduce::KeyCombiner, ""),
                                         std::make_pair(RSExportReduce::KeyOutConverter, "")});
    if (mContext->getTargetAPI() >= SLANG_FEATURE_GENERAL_REDUCTION_HALTER_API) {
      // Halter functionality has not been released, nor has its
      // specification been finalized with partners.  We do not have a
      // specification that extends through the full RenderScript
      // software stack, either.
      KeywordValueMap.insert(std::make_pair(RSExportReduce::KeyHalter, ""));
    }
    while (PragmaToken.is(clang::tok::identifier)) {
      if (!ProcessKeywordAndValue(PP, PragmaToken, KeywordValueMap))
        return;
    }

    // Make sure there's no end-of-line garbage
    if (PragmaToken.isNot(clang::tok::eod)) {
      PP.Diag(PragmaToken.getLocation(),
              PP.getDiagnostics().getCustomDiagID(
                clang::DiagnosticsEngine::Error,
                "did not expect '%0' here for '#pragma rs %1'"))
          << PP.getSpelling(PragmaToken) << getName();
      return;
    }

    // Make sure we have an accumulator
    if (KeywordValueMap[RSExportReduce::KeyAccumulator].empty()) {
      PP.Diag(PragmaLocation, PP.getDiagnostics().getCustomDiagID(
                                clang::DiagnosticsEngine::Error,
                                "missing '%0' for '#pragma rs %1'"))
          << RSExportReduce::KeyAccumulator << getName();
      return;
    }

    // Make sure the reduction kernel name is unique.  (If we were
    // worried there might be a VERY large number of pragmas, then we
    // could do something more efficient than walking a list to search
    // for duplicates.)
    for (auto I = mContext->export_reduce_begin(),
              E = mContext->export_reduce_end();
         I != E; ++I) {
      if ((*I)->getNameReduce() == KeywordValueMap[RSExportReduce::KeyReduce]) {
        PP.Diag(PragmaLocation, PP.getDiagnostics().getCustomDiagID(
                                  clang::DiagnosticsEngine::Error,
                                  "reduction kernel '%0' declared multiple "
                                  "times (first one is at %1)"))
            << KeywordValueMap[RSExportReduce::KeyReduce]
            << (*I)->getLocation().printToString(PP.getSourceManager());
        return;
      }
    }

    // Check API version.
    if (mContext->getTargetAPI() < SLANG_FEATURE_GENERAL_REDUCTION_API) {
      PP.Diag(PragmaLocation,
              PP.getDiagnostics().getCustomDiagID(
                clang::DiagnosticsEngine::Error,
                "reduction kernels are not supported in SDK levels %0-%1"))
          << SLANG_MINIMUM_TARGET_API
          << (SLANG_FEATURE_GENERAL_REDUCTION_API - 1);
      return;
    }

    // Handle backward reference from pragma (see Backend::HandleTopLevelDecl for forward reference).
    MarkUsed(PP, KeywordValueMap[RSExportReduce::KeyInitializer]);
    MarkUsed(PP, KeywordValueMap[RSExportReduce::KeyAccumulator]);
    MarkUsed(PP, KeywordValueMap[RSExportReduce::KeyCombiner]);
    MarkUsed(PP, KeywordValueMap[RSExportReduce::KeyOutConverter]);
    MarkUsed(PP, KeywordValueMap[RSExportReduce::KeyHalter]);

    mContext->addExportReduce(RSExportReduce::Create(mContext, PragmaLocation,
                                                     KeywordValueMap[RSExportReduce::KeyReduce],
                                                     KeywordValueMap[RSExportReduce::KeyInitializer],
                                                     KeywordValueMap[RSExportReduce::KeyAccumulator],
                                                     KeywordValueMap[RSExportReduce::KeyCombiner],
                                                     KeywordValueMap[RSExportReduce::KeyOutConverter],
                                                     KeywordValueMap[RSExportReduce::KeyHalter]));
  }

 private:
  typedef std::map<std::string, std::string> KeywordValueMapType;

  void MarkUsed(clang::Preprocessor &PP, const std::string &FunctionName) {
    if (FunctionName.empty())
      return;

    clang::ASTContext &ASTC = mContext->getASTContext();
    clang::TranslationUnitDecl *TUDecl = ASTC.getTranslationUnitDecl();
    slangAssert(TUDecl);
    if (const clang::IdentifierInfo *II = PP.getIdentifierInfo(FunctionName)) {
      for (auto Decl : TUDecl->lookup(II)) {
        clang::FunctionDecl *FDecl = Decl->getAsFunction();
        if (!FDecl || !FDecl->isThisDeclarationADefinition())
          continue;
        // Handle backward reference from pragma (see
        // Backend::HandleTopLevelDecl for forward reference).
        mContext->markUsedByReducePragma(FDecl, RSContext::CheckNameNo);
      }
    }
  }

  // Return comma-separated list of all keys in the map
  static std::string ListKeywords(const KeywordValueMapType &KeywordValueMap) {
    std::string Ret;
    bool First = true;
    for (auto const &entry : KeywordValueMap) {
      if (First)
        First = false;
      else
        Ret += ", ";
      Ret += "'";
      Ret += entry.first;
      Ret += "'";
    }
    return Ret;
  }

  // Parse "keyword(value)" and set KeywordValueMap[keyword] = value.  (Both
  // "keyword" and "value" are identifiers.)
  // Does both syntactic validation and the following semantic validation:
  // - The keyword must be present in the map.
  // - The map entry for the keyword must not contain a value.
  bool ProcessKeywordAndValue(clang::Preprocessor &PP,
                              clang::Token &PragmaToken,
                              KeywordValueMapType &KeywordValueMap) {
    // The current token must be an identifier in KeywordValueMap
    KeywordValueMapType::iterator Entry;
    if (PragmaToken.isNot(clang::tok::identifier) ||
        ((Entry = KeywordValueMap.find(
            PragmaToken.getIdentifierInfo()->getName())) ==
         KeywordValueMap.end())) {
      // Note that we should never get here for the "reduce" token
      // itself, which should already have been recognized.
      PP.Diag(PragmaToken.getLocation(),
              PP.getDiagnostics().getCustomDiagID(
                clang::DiagnosticsEngine::Error,
                "did not recognize '%0' for '#pragma %1'; expected one of "
                "the following keywords: %2"))
          << PragmaToken.getIdentifierInfo()->getName() << getName()
          << ListKeywords(KeywordValueMap);
      return false;
    }
    // ... and there must be no value for this keyword yet
    if (!Entry->second.empty()) {
      PP.Diag(PragmaToken.getLocation(),
              PP.getDiagnostics().getCustomDiagID(
                clang::DiagnosticsEngine::Error,
                "more than one '%0' for '#pragma rs %1'"))
          << Entry->first << getName();
      return false;
    }
    PP.LexUnexpandedToken(PragmaToken);

    // The current token must be clang::tok::l_paren
    if (PragmaToken.isNot(clang::tok::l_paren)) {
      PP.Diag(PragmaToken.getLocation(),
              PP.getDiagnostics().getCustomDiagID(
                clang::DiagnosticsEngine::Error,
                "missing '(' after '%0' for '#pragma rs %1'"))
          << Entry->first << getName();
      return false;
    }
    PP.LexUnexpandedToken(PragmaToken);

    // The current token must be an identifier (a name)
    if (PragmaToken.isNot(clang::tok::identifier)) {
      PP.Diag(PragmaToken.getLocation(),
              PP.getDiagnostics().getCustomDiagID(
                clang::DiagnosticsEngine::Error,
                "missing name after '%0(' for '#pragma rs %1'"))
          << Entry->first << getName();
      return false;
    }
    const std::string Name = PragmaToken.getIdentifierInfo()->getName();
    PP.LexUnexpandedToken(PragmaToken);

    // The current token must be clang::tok::r_paren
    if (PragmaToken.isNot(clang::tok::r_paren)) {
      PP.Diag(PragmaToken.getLocation(),
              PP.getDiagnostics().getCustomDiagID(
                clang::DiagnosticsEngine::Error,
                "missing ')' after '%0(%1' for '#pragma rs %2'"))
          << Entry->first << Name << getName();
      return false;
    }
    PP.LexUnexpandedToken(PragmaToken);

    // Success
    Entry->second = Name;
    return true;
  }
};

class RSReflectLicensePragmaHandler : public RSPragmaHandler {
 private:
  void handleItem(const std::string &Item) {
    mContext->addPragma(this->getName(), Item);
    mContext->setLicenseNote(Item);
  }

 public:
  RSReflectLicensePragmaHandler(llvm::StringRef Name, RSContext *Context)
      : RSPragmaHandler(Name, Context) { }

  void HandlePragma(clang::Preprocessor &PP,
                    clang::PragmaIntroducerKind Introducer,
                    clang::Token &FirstToken) {
    this->handleOptionalStringLiteralParamPragma(PP, FirstToken);
  }
};

class RSVersionPragmaHandler : public RSPragmaHandler {
 private:
  void handleInt(clang::Preprocessor &PP,
                 clang::Token &Tok,
                 const int v) {
    if (v != 1) {
      PP.Diag(Tok,
              PP.getDiagnostics().getCustomDiagID(
                  clang::DiagnosticsEngine::Error,
                  "pragma for version in source file must be set to 1"));
      mContext->setVersion(1);
      return;
    }
    std::stringstream ss;
    ss << v;
    mContext->addPragma(this->getName(), ss.str());
    mContext->setVersion(v);
  }

 public:
  RSVersionPragmaHandler(llvm::StringRef Name, RSContext *Context)
      : RSPragmaHandler(Name, Context) { }

  void HandlePragma(clang::Preprocessor &PP,
                    clang::PragmaIntroducerKind Introducer,
                    clang::Token &FirstToken) {
    this->handleIntegerParamPragma(PP, FirstToken);
  }
};

// Handles the pragmas rs_fp_full, rs_fp_relaxed, and rs_fp_imprecise.
// There's one instance of this handler for each of the above values.
// Only getName() differs between the instances.
class RSPrecisionPragmaHandler : public RSPragmaHandler {
public:
  RSPrecisionPragmaHandler(llvm::StringRef Name, RSContext *Context)
      : RSPragmaHandler(Name, Context) {}

  void HandlePragma(clang::Preprocessor &PP,
                    clang::PragmaIntroducerKind Introducer,
                    clang::Token &Token) {
    std::string Precision = getName();
    // We are deprecating rs_fp_imprecise.
    if (Precision == "rs_fp_imprecise") {
      PP.Diag(Token, PP.getDiagnostics().getCustomDiagID(
                         clang::DiagnosticsEngine::Warning,
                         "rs_fp_imprecise is deprecated.  Assuming "
                         "rs_fp_relaxed instead."));
      Precision = "rs_fp_relaxed";
    }
    // Check if we have already encountered a precision pragma already.
    std::string PreviousPrecision = mContext->getPrecision();
    if (!PreviousPrecision.empty()) {
      // If the previous specified a different value, it's an error.
      if (PreviousPrecision != Precision) {
        PP.Diag(Token, PP.getDiagnostics().getCustomDiagID(
                           clang::DiagnosticsEngine::Error,
                           "Multiple float precisions specified.  Encountered "
                           "%0 previously."))
            << PreviousPrecision;
      }
      // Otherwise we ignore redundant entries.
      return;
    }

    mContext->addPragma(Precision, "");
    mContext->setPrecision(Precision);
  }
};

}  // namespace

void RSPragmaHandler::handleItemListPragma(clang::Preprocessor &PP,
                                           clang::Token &FirstToken) {
  clang::Token &PragmaToken = FirstToken;

  // Skip first token, like "export_var"
  PP.LexUnexpandedToken(PragmaToken);

  // Now, the current token must be clang::tok::lpara
  if (PragmaToken.isNot(clang::tok::l_paren))
    return;

  while (PragmaToken.isNot(clang::tok::eod)) {
    // Lex variable name
    PP.LexUnexpandedToken(PragmaToken);
    if (PragmaToken.is(clang::tok::identifier))
      this->handleItem(PP.getSpelling(PragmaToken));
    else
      break;

    slangAssert(PragmaToken.isNot(clang::tok::eod));

    PP.LexUnexpandedToken(PragmaToken);

    if (PragmaToken.isNot(clang::tok::comma))
      break;
  }
}

void RSPragmaHandler::handleNonParamPragma(clang::Preprocessor &PP,
                                           clang::Token &FirstToken) {
  clang::Token &PragmaToken = FirstToken;

  // Skip first token, like "export_var_all"
  PP.LexUnexpandedToken(PragmaToken);

  // Should be end immediately
  if (PragmaToken.isNot(clang::tok::eod))
    if (PragmaToken.isNot(clang::tok::r_paren)) {
      PP.Diag(PragmaToken,
              PP.getDiagnostics().getCustomDiagID(
                  clang::DiagnosticsEngine::Error,
                  "expected a ')'"));
      return;
    }
}

void RSPragmaHandler::handleOptionalStringLiteralParamPragma(
    clang::Preprocessor &PP, clang::Token &FirstToken) {
  clang::Token &PragmaToken = FirstToken;

  // Skip first token, like "set_reflect_license"
  PP.LexUnexpandedToken(PragmaToken);

  // Now, the current token must be clang::tok::lpara
  if (PragmaToken.isNot(clang::tok::l_paren))
    return;

  // If not ')', eat the following string literal as the license
  PP.LexUnexpandedToken(PragmaToken);
  if (PragmaToken.isNot(clang::tok::r_paren)) {
    // Eat the whole string literal
    clang::StringLiteralParser StringLiteral(PragmaToken, PP);
    if (StringLiteral.hadError) {
      // Diagnostics will be generated automatically
      return;
    } else {
      this->handleItem(std::string(StringLiteral.GetString()));
    }

    // The current token should be clang::tok::r_para
    PP.LexUnexpandedToken(PragmaToken);
    if (PragmaToken.isNot(clang::tok::r_paren)) {
      PP.Diag(PragmaToken,
              PP.getDiagnostics().getCustomDiagID(
                  clang::DiagnosticsEngine::Error,
                  "expected a ')'"));
      return;
    }
  } else {
    // If no argument, remove the license
    this->handleItem("");
  }
}

void RSPragmaHandler::handleIntegerParamPragma(
    clang::Preprocessor &PP, clang::Token &FirstToken) {
  clang::Token &PragmaToken = FirstToken;

  // Skip first token, like "version"
  PP.LexUnexpandedToken(PragmaToken);

  // Now, the current token must be clang::tok::lpara
  if (PragmaToken.isNot(clang::tok::l_paren)) {
    // If no argument, set the version to 0
    this->handleInt(PP, PragmaToken, 0);
    return;
  }
  PP.LexUnexpandedToken(PragmaToken);

  if (PragmaToken.is(clang::tok::numeric_constant)) {
    llvm::SmallString<128> SpellingBuffer;
    SpellingBuffer.resize(PragmaToken.getLength() + 1);
    llvm::StringRef TokSpelling = PP.getSpelling(PragmaToken, SpellingBuffer);
    clang::NumericLiteralParser NumericLiteral(TokSpelling,
        PragmaToken.getLocation(), PP);
    if (NumericLiteral.hadError) {
      // Diagnostics will be generated automatically
      return;
    } else {
      llvm::APInt Val(32, 0);
      NumericLiteral.GetIntegerValue(Val);
      this->handleInt(PP, PragmaToken, static_cast<int>(Val.getSExtValue()));
    }
    PP.LexUnexpandedToken(PragmaToken);
  } else {
    // If no argument, set the version to 0
    this->handleInt(PP, PragmaToken, 0);
  }

  if (PragmaToken.isNot(clang::tok::r_paren)) {
    PP.Diag(PragmaToken,
            PP.getDiagnostics().getCustomDiagID(
                clang::DiagnosticsEngine::Error,
                "expected a ')'"));
    return;
  }

  do {
    PP.LexUnexpandedToken(PragmaToken);
  } while (PragmaToken.isNot(clang::tok::eod));
}

void AddPragmaHandlers(clang::Preprocessor &PP, RSContext *RsContext) {
  // For #pragma rs export_type
  PP.AddPragmaHandler("rs",
                      new RSExportTypePragmaHandler("export_type", RsContext));

  // For #pragma rs java_package_name
  PP.AddPragmaHandler(
      "rs", new RSJavaPackageNamePragmaHandler("java_package_name", RsContext));

  // For #pragma rs reduce
  PP.AddPragmaHandler(
      "rs", new RSReducePragmaHandler(RSExportReduce::KeyReduce, RsContext));

  // For #pragma rs set_reflect_license
  PP.AddPragmaHandler(
      "rs", new RSReflectLicensePragmaHandler("set_reflect_license", RsContext));

  // For #pragma version
  PP.AddPragmaHandler(new RSVersionPragmaHandler("version", RsContext));

  // For #pragma rs_fp*
  PP.AddPragmaHandler(new RSPrecisionPragmaHandler("rs_fp_full", RsContext));
  PP.AddPragmaHandler(new RSPrecisionPragmaHandler("rs_fp_relaxed", RsContext));
  PP.AddPragmaHandler(new RSPrecisionPragmaHandler("rs_fp_imprecise", RsContext));
}


}  // namespace slang