// Copyright 2012 Google Inc. 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.collide.client.code.autocomplete.codegraph;

import static com.google.collide.codemirror2.Token.LITERAL_PERIOD;
import static com.google.collide.codemirror2.TokenType.NULL;
import static com.google.collide.codemirror2.TokenType.REGEXP;
import static com.google.collide.codemirror2.TokenType.STRING;
import static com.google.collide.codemirror2.TokenType.VARIABLE;
import static com.google.collide.codemirror2.TokenType.VARIABLE2;
import static com.google.collide.codemirror2.TokenType.WHITESPACE;

import javax.annotation.Nonnull;

import com.google.collide.client.documentparser.DocumentParser;
import com.google.collide.client.documentparser.ParseResult;
import com.google.collide.codemirror2.State;
import com.google.collide.codemirror2.Token;
import com.google.collide.codemirror2.TokenType;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.base.Preconditions;

/**
 * Set of utilities to perform code parsing and parse results processing.
 */
public class ParseUtils {

  private static final String SPACE = " ";
  private static final String[] CONTEXT_START = new String[] {"[", "(", "{"};
  private static final String SIMPLE_CONTEXT_END = "])";
  private static final String CONTEXT_END = SIMPLE_CONTEXT_END + "}";

  /**
   * Collect ids interleaved with periods, omitting parenthesis groups.
   *
   * @param tokens source tokens array; destroyed in runtime.
   * @param expectingPeriod state before parsing:
   *        {@code true} if period token is expected
   * @param contextParts output collector; only ids are collected
   * @return state after parsing: {@code true} if period token is expected
   */
  static boolean buildInvocationSequenceContext(
      JsonArray<Token> tokens, boolean expectingPeriod, JsonArray<String> contextParts) {
    // right-to-left tokens processing loop.
    while (!tokens.isEmpty()) {
      Token lastToken = tokens.pop();
      TokenType lastTokenType = lastToken.getType();
      String lastTokenValue = lastToken.getValue();

      // Omit whitespaces.
      if (lastTokenType == WHITESPACE) {
        continue;
      }

      if (expectingPeriod) {
        // If we are expecting period, then no other tokens are allowed.
        if ((lastTokenType != NULL) || !LITERAL_PERIOD.equals(lastTokenValue)) {
          return expectingPeriod;
        }
        expectingPeriod = false;
      } else {
        // Not expecting period means that previously processed token (located
        // to the right of the current token) was not id.
        // That way we expect id or parenthesis group.
        if (lastTokenType == VARIABLE || lastTokenType == VARIABLE2
            || lastTokenType == TokenType.PROPERTY) {
          contextParts.add(lastTokenValue);
          // Period is obligatory to the left of id to continue the chain.
          expectingPeriod = true;
        } else if ((lastTokenType == NULL) && (lastTokenValue.length() == 1)
            && SIMPLE_CONTEXT_END.contains(lastTokenValue)) {
          // We are to enter parenthesis group.
          if (!bypassParenthesizedGroup(tokens, lastToken)) {
            // If we were unable to properly close group - exit
            return expectingPeriod;
          }
          // After group is closed, we again expect id or parenthesis group.
        } else {
          // Token type we don't expect - exit
          return expectingPeriod;
        }
      }
    }
    return expectingPeriod;
  }

  /**
   * Pops tokens until context closed with lastToken is not opened,
   * or inconsistency found.
   *
   * @return {@code true} if context was successfully removed from tokens.
   */
  static boolean bypassParenthesizedGroup(JsonArray<Token> tokens, Token lastToken) {
    JsonArray<String> stack = JsonCollections.createArray();
    // Push char that corresponds to opening parenthesis.
    stack.add(CONTEXT_START[(CONTEXT_END.indexOf(lastToken.getValue()))]);
    while (!tokens.isEmpty()) {
      lastToken = tokens.pop();
      String lastTokenValue = lastToken.getValue();
      // Bypass non-parenthesis.
      if (lastToken.getType() != NULL || (lastTokenValue.length() != 1)) {
        continue;
      }

      // Dive deeper.
      if (CONTEXT_END.contains(lastTokenValue)) {
        stack.add(CONTEXT_START[(CONTEXT_END.indexOf(lastToken.getValue()))]);
        continue;
      }

      // Check if token corresponds to stack head
      if (CONTEXT_START[0].equals(lastTokenValue)
          || CONTEXT_START[1].equals(lastTokenValue) || CONTEXT_START[2].equals(lastTokenValue)) {
        if (!stack.peek().equals(lastTokenValue)) {
          // Got opening parenthesis not matching stack -> exit with error.
          return false;
        }
        stack.pop();
        // If initial group is closed - we've finished.
        if (stack.isEmpty()) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Types of situations.
   *
   * <p>When expanding line with letters leads to expanding last token,
   * it means that token is not finished. Strings and comments have such
   * behaviour ({@link #IN_STRING}, {@link #IN_COMMENT}).
   *
   * <p>When parse result is {@code null}, then no further analysis can be done
   * ({@link #NOT_PARSED}).
   *
   * <p>Otherwise (the most common and interesting situation) we suppose to be
   * somewhere in code ({@link #IN_CODE}).
   */
  public enum Context {
    IN_STRING, IN_COMMENT, NOT_PARSED, IN_CODE
  }

  /**
   * Bean that wraps together {@link ParseResult} and {@link Context}.
   *
   * @param <T> language-specific {@link State} type.
   */
  public static class ExtendedParseResult<T extends State> {

    private final ParseResult<T> parseResult;
    private final Context context;

    public ExtendedParseResult(ParseResult<T> parseResult, Context context) {
      this.parseResult = parseResult;
      this.context = context;
    }

    /**
     * @return {@code null} if parsing is failed, or token list is empty.
     */
    public String getLastTokenValue() {
      if (parseResult == null) {
        return null;
      }
      JsonArray<Token> tokens = parseResult.getTokens();
      if (tokens.isEmpty()) {
        return null;
      }
      Token lastToken = tokens.peek();
      return lastToken.getValue();
    }

    ParseResult<T> getParseResult() {
      return parseResult;
    }

    Context getContext() {
      return context;
    }
  }

  /**
   * Parses the line to specified position and returns parse result.
   *
   * @param parser current document parser
   * @param position point of interest
   */
  public static <T extends State> ExtendedParseResult<T> getExtendedParseResult(
      Class<T> stateClass, @Nonnull DocumentParser parser, Position position) {
    int column = position.getColumn();
    String text = position.getLine().getText().substring(0, column);

    // Add space if we are not sure that comment/literal is finished
    boolean addSpace = (column == 0)
        || text.endsWith("*/")
        || text.endsWith("'") || text.endsWith("\"");
    ParseResult<T> result = parser.getState(stateClass, position, addSpace ? SPACE : null);

    if (result == null) {
      return new ExtendedParseResult<T>(null, Context.NOT_PARSED);
    }

    JsonArray<Token> tokens = result.getTokens();
    Token lastToken = tokens.peek();
    Preconditions.checkNotNull(lastToken,
        "Last token expected to be non-null; text='%s', position=%s", text, position);
    TokenType lastTokenType = lastToken.getType();
    String lastTokenValue = lastToken.getValue();
    if (!addSpace) {
      if (lastTokenType == STRING || lastTokenType == REGEXP) {
        return new ExtendedParseResult<T>(result, Context.IN_STRING);
      } else if (lastTokenType == TokenType.COMMENT) {
        return new ExtendedParseResult<T>(result, Context.IN_COMMENT);
      }

      // Python parser, for a purpose of simplicity, parses period and variable
      // name as a single token. If period is not followed by identifier, parser
      // states that this is and error, which is, generally, not truth.
      if ((lastTokenType == TokenType.ERROR) && LITERAL_PERIOD.equals(lastTokenValue)) {
        tokens.pop();
        tokens.add(new Token(lastToken.getMode(), TokenType.NULL, LITERAL_PERIOD));
      }

      return new ExtendedParseResult<T>(result, Context.IN_CODE);
    }

    // Remove / shorten last token to omit added whitespace.
    tokens.pop();
    if (lastTokenType == STRING || lastTokenType == REGEXP || lastTokenType == TokenType.COMMENT) {
      // Whitespace stuck to token - strip it.
      lastTokenValue = lastTokenValue.substring(0, lastTokenValue.length() - 1);
      tokens.add(new Token(lastToken.getMode(), lastTokenType, lastTokenValue));
      if (lastTokenType == STRING || lastTokenType == REGEXP) {
        return new ExtendedParseResult<T>(result, Context.IN_STRING);
      } else {
        return new ExtendedParseResult<T>(result, Context.IN_COMMENT);
      }
    }
    // Otherwise whitespace was stated as a standalone token.
    return new ExtendedParseResult<T>(result, Context.IN_CODE);
  }
}
