/*
 * Copyright 2016 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.idea.blaze.base.lang.projectview.parser;

import com.google.idea.blaze.base.lang.projectview.language.ProjectViewKeywords;
import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewElementType;
import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewElementTypes;
import com.google.idea.blaze.base.projectview.section.ScalarSectionParser;
import com.intellij.lang.PsiBuilder;
import com.intellij.openapi.application.ApplicationManager;
import javax.annotation.Nullable;

/** Project view psi parser. */
public class ProjectViewPsiParser {

  private final PsiBuilder builder;

  public ProjectViewPsiParser(PsiBuilder builder) {
    this.builder = builder;
  }

  public void parseFile() {
    builder.setDebugMode(ApplicationManager.getApplication().isUnitTestMode());
    while (!builder.eof()) {
      if (matches(ProjectViewTokenType.NEWLINE)) {
        continue;
      }
      parseSection();
    }
  }

  /** A block is one of: - scalar section - list section */
  private void parseSection() {
    PsiBuilder.Marker marker = builder.mark();
    if (matches(ProjectViewTokenType.LIST_KEYWORD)) {
      expect(ProjectViewTokenType.COLON);
      skipPastNewline();
      parseListItems();
      marker.done(ProjectViewElementTypes.LIST_SECTION);
      return;
    }
    if (currentToken() == ProjectViewTokenType.SCALAR_KEYWORD) {
      ScalarSectionParser parser =
          ProjectViewKeywords.SCALAR_KEYWORD_MAP.get(builder.getTokenText());
      if (parser != null) {
        parseScalarSection(parser);
        marker.done(ProjectViewElementTypes.SCALAR_SECTION);
        return;
      }
    }
    // handle each of the error cases
    if (matches(ProjectViewTokenType.INDENT)) {
      skipBlockAndError(
          marker, "Invalid indentation. Indented lines must be preceded by a list keyword");
      return;
    }
    if (matches(ProjectViewTokenType.COLON)) {
      skipBlockAndError(marker, "Invalid section: lines cannot begin with a colon.");
      return;
    }
    skipBlockAndError(marker, "Unrecognized keyword: " + builder.getTokenText());
  }

  private void parseListItems() {
    while (!builder.eof()) {
      if (matches(ProjectViewTokenType.NEWLINE)) {
        continue;
      }
      if (!matches(ProjectViewTokenType.INDENT)) {
        return;
      }
      PsiBuilder.Marker marker = builder.mark();
      skipToNewlineToken();
      marker.done(ProjectViewElementTypes.LIST_ITEM);
      builder.advanceLexer();
    }
  }

  private void parseScalarSection(ScalarSectionParser parser) {
    boolean whitespaceDivider = builder.rawLookup(1) == ProjectViewTokenType.WHITESPACE;
    builder.advanceLexer();

    char divider = parser.getDivider();
    if (divider == ' ') {
      if (!whitespaceDivider) {
        builder.error("Whitespace divider expected after '" + parser.getName() + "'");
        builder.advanceLexer();
      }
      parseScalarItem();
      return;
    }
    if (whitespaceDivider || !Character.toString(divider).equals(builder.getTokenText())) {
      builder.error(String.format("'%s' expected", divider));
    }
    if (!whitespaceDivider) {
      builder.advanceLexer();
    }
    parseScalarItem();
  }

  private void parseScalarItem() {
    PsiBuilder.Marker marker = builder.mark();
    skipToNewlineToken();
    marker.done(ProjectViewElementTypes.SCALAR_ITEM);
    builder.advanceLexer();
  }

  /** Consumes the current token iff it matches the expected type. Otherwise, returns false */
  private boolean matches(ProjectViewTokenType kind) {
    if (currentToken() == kind) {
      builder.advanceLexer();
      return true;
    }
    return false;
  }

  /**
   * Consumes the current token if it's of the expected type. Otherwise, returns false and reports
   * an error.
   */
  private boolean expect(ProjectViewTokenType kind) {
    if (matches(kind)) {
      return true;
    }
    builder.error(String.format("'%s' expected", kind));
    return false;
  }

  /** Checks if the upcoming sequence of tokens match that expected. Doesn't advance the parser. */
  private boolean atTokenSequence(ProjectViewTokenType... kinds) {
    for (int i = 0; i < kinds.length; i++) {
      if (builder.lookAhead(i) != kinds[i]) {
        return false;
      }
    }
    return true;
  }

  @Nullable
  private ProjectViewTokenType currentToken() {
    return (ProjectViewTokenType) builder.getTokenType();
  }

  private void skipBlockAndError(PsiBuilder.Marker marker, String message) {
    skipToNextBlock();
    marker.error(message);
  }

  /** Skip to the start of the next unindented line */
  private void skipToNextBlock() {
    while (!builder.eof()) {
      if (atTokenSequence(ProjectViewTokenType.NEWLINE, ProjectViewTokenType.IDENTIFIER)) {
        builder.advanceLexer();
        return;
      }
      builder.advanceLexer();
    }
  }

  /** Skip to the start of the next line */
  private void skipPastNewline() {
    while (!builder.eof()) {
      if (matches(ProjectViewTokenType.NEWLINE)) {
        return;
      }
      builder.advanceLexer();
    }
  }

  /** Skip to the end of the current line */
  private void skipToNewlineToken() {
    while (!builder.eof()) {
      if (currentToken() == ProjectViewTokenType.NEWLINE) {
        return;
      }
      builder.advanceLexer();
    }
  }

  private void buildTokenElement(ProjectViewElementType type) {
    PsiBuilder.Marker marker = builder.mark();
    builder.advanceLexer();
    marker.done(type);
  }
}

@@ -840,87 +840,8 @@
 pe;%0A
-import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewElementType;%0A
 impo
@@ -5921,174 +5921,7 @@
 %0A  %7D
-%0A%0A  private void buildTokenElement(ProjectViewElementType type) %7B%0A    PsiBuilder.Marker marker = builder.mark();%0A    builder.advanceLexer();%0A    marker.done(type);%0A  %7D
 %0A%7D%0A

Remove unused method