// Copyright 2019 The Bazel Authors. All rights reserved.
//
// 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.

package com.google.devtools.build.lib.bazel.rules.ninja.parser;

import com.google.devtools.build.lib.bazel.rules.ninja.file.DeclarationConsumer;
import com.google.devtools.build.lib.bazel.rules.ninja.file.FileFragment;
import com.google.devtools.build.lib.bazel.rules.ninja.file.GenericParsingException;
import com.google.devtools.build.lib.bazel.rules.ninja.lexer.NinjaLexer;
import com.google.devtools.build.lib.bazel.rules.ninja.lexer.NinjaToken;
import com.google.devtools.build.lib.bazel.rules.ninja.parser.NinjaFileParseResult.NinjaPromise;
import com.google.devtools.build.lib.bazel.rules.ninja.pipeline.NinjaPipeline;
import com.google.devtools.build.lib.util.Pair;
import java.io.IOException;

/**
 * Ninja parser: an instance will be created per a fragment of Ninja file, to avoid synchronization
 * while parsing independent values.
 *
 * <p>Populates the {@link NinjaFileParseResult} with variables and rules.
 */
public class NinjaParser implements DeclarationConsumer {
  private final NinjaPipeline pipeline;
  private final NinjaFileParseResult parseResult;
  private final String ninjaFileName;

  public NinjaParser(
      NinjaPipeline pipeline, NinjaFileParseResult parseResult, String ninjaFileName) {
    this.pipeline = pipeline;
    this.parseResult = parseResult;
    this.ninjaFileName = ninjaFileName;
  }

  @Override
  public void declaration(FileFragment fragment) throws GenericParsingException, IOException {
    long offset = fragment.getFragmentOffset();

    NinjaLexer lexer = new NinjaLexer(fragment, pipeline.getPathFragmentInterner());
    if (!lexer.hasNextToken()) {
      throw new IllegalStateException("Empty fragment passed as declaration.");
    }
    NinjaToken token = lexer.nextToken();
    // Skip possible leading newlines in the fragment for parsing.
    while (lexer.hasNextToken() && NinjaToken.NEWLINE.equals(token)) {
      token = lexer.nextToken();
    }
    if (!lexer.hasNextToken()) {
      // If fragment contained only newlines.
      return;
    }
    long declarationStart = offset + lexer.getLastStart();
    lexer.undo();
    NinjaParserStep parser = new NinjaParserStep(lexer);

    switch (token) {
      case IDENTIFIER:
        Pair<String, NinjaVariableValue> variable = parser.parseVariable();
        parseResult.addVariable(variable.getFirst(), declarationStart, variable.getSecond());
        break;
      case RULE:
        NinjaRule rule = parser.parseNinjaRule();
        parseResult.addRule(declarationStart, rule);
        break;
      case POOL:
        parseResult.addPool(declarationStart, parser.parseNinjaPool());
        break;
      case INCLUDE:
        NinjaVariableValue includeStatement = parser.parseIncludeStatement();
        NinjaPromise<NinjaFileParseResult> includeFuture =
            pipeline.createChildFileParsingPromise(
                includeStatement, declarationStart, ninjaFileName);
        parseResult.addIncludeScope(declarationStart, includeFuture);
        break;
      case SUBNINJA:
        NinjaVariableValue subNinjaStatement = parser.parseSubNinjaStatement();
        NinjaPromise<NinjaFileParseResult> subNinjaFuture =
            pipeline.createChildFileParsingPromise(
                subNinjaStatement, declarationStart, ninjaFileName);
        parseResult.addSubNinjaScope(declarationStart, subNinjaFuture);
        break;
      case BUILD:
        FileFragment targetFragment;
        if (declarationStart == offset) {
          targetFragment = fragment;
        } else {
          // Method subFragment accepts only the offset *inside fragment*.
          // So we should subtract the offset of fragment's buffer in file
          // (fragment.getFileOffset()),
          // and start of fragment inside buffer (fragment.getStartIncl()).
          long fragmentStart =
              declarationStart - fragment.getFileOffset() - fragment.getStartIncl();

          // While the absolute offset is typed as long (because of larger ninja files), the
          // fragments are only at most Integer.MAX_VALUE long, so fragmentStart cannot be
          // larger than that. Sanity check this here.
          if (fragmentStart > Integer.MAX_VALUE) {
            throw new GenericParsingException(
                String.format(
                    "The fragmentStart value %s is not expected to be larger than max-int, "
                        + "since each fragment is at most max-int long.",
                    fragmentStart));
          }
          targetFragment = fragment.subFragment((int) fragmentStart, fragment.length());
        }
        parseResult.addTarget(targetFragment);
        break;
      case DEFAULT:
        // Do nothing.
        break;
      case ZERO:
      case EOF:
        return;
      case COLON:
      case EQUALS:
      case ESCAPED_TEXT:
      case INDENT:
      case NEWLINE:
      case PIPE:
      case PIPE2:
      case TEXT:
      case VARIABLE:
        throw new UnsupportedOperationException(
            "Unsupported type of Ninja lexeme for the parser at this state: "
                + token.name()
                + ". These are unexpected lexemes at this state for the parser, and running into a "
                + "token of this lexeme hints that the parser did not progress the lexer to a "
                + "valid state during a previous parsing step.");
      case ERROR:
        throw new GenericParsingException(lexer.getError());
        // Omit default case on purpose. Explicitly specify *all* NinjaToken enum cases above or the
        // compilation will fail.
    }
  }
}

@@ -1723,16 +1723,396 @@
 eName;%0A%0A
+  private static final String UNSUPPORTED_TOKEN_MESSAGE =%0A      %22 is an unsupported type of Ninja lexeme for the parser at this state. %22%0A          + %22These are unexpected lexemes at this state for the parser, and running into a %22%0A          + %22token of this lexeme hints that the parser did not progress the lexer to a %22%0A          + %22valid state during a previous parsing step.%22;%0A%0A
   public
@@ -5949,378 +5949,48 @@
 ion(
-%0A            %22Unsupported type of Ninja lexeme for the parser at this state: %22%0A                + token.name()%0A                + %22. These are unexpected lexemes at this state for the parser, and running into a %22%0A                + %22token of this lexeme hints that the parser did not progress the lexer to a %22%0A                + %22valid state during a previous parsing step.%22
+token.name() + UNSUPPORTED_TOKEN_MESSAGE
 );%0A 

Make NinjaParser's unsupported token message a static final constant.