/*
 * Copyright 2005 Sascha Weinreuter
 *
 * 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 org.intellij.lang.xpath;

import com.intellij.lang.ASTNode;
import com.intellij.lang.PsiBuilder;
import com.intellij.lang.PsiParser;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import org.intellij.plugins.xpathView.XPathBundle;
import org.jetbrains.annotations.NotNull;

public class XPathParser implements PsiParser {
  private static final boolean DBG_MODE = Boolean.getBoolean(XPathParser.class.getName() + ".debug") || ApplicationManager.getApplication().isUnitTestMode();

  @Override
  public @NotNull ASTNode parse(@NotNull IElementType root, @NotNull PsiBuilder builder) {
    builder.setDebugMode(DBG_MODE);

    final PsiBuilder.Marker rootMarker = builder.mark();
    if (!builder.eof()) {
      boolean avt = false;
      if (builder.getTokenType() == XPathTokenTypes.LBRACE) {
        builder.advanceLexer();
        avt = true;
      }
      if (!parseExpr(builder)) {
        builder.error(XPathBundle.message("parsing.error.xpath.expression.expected"));
      }
      if (avt) {
        checkMatches(builder, XPathTokenTypes.RBRACE, XPathBundle.message("parsing.error.closing.brace.expected"));
      }
      consumeBadTokens(builder, TokenSet.EMPTY);
    }
    rootMarker.done(root);
    return builder.getTreeBuilt();
  }

  protected boolean parsePrimaryExpr(PsiBuilder builder) {
    final IElementType tokenType = builder.getTokenType();

    if (tokenType == XPathTokenTypes.DOLLAR) {
      parseVariable(builder);
    } else if (tokenType == XPathTokenTypes.LPAREN) {
      parseParenthesizedExpr(builder);
    } else if (tokenType == XPathTokenTypes.STRING_LITERAL) {
      parseLiteral(builder);
    } else if (tokenType == XPathTokenTypes.NUMBER) {
      parseNumber(builder);
    } else if (tokenType == XPathTokenTypes.FUNCTION_NAME || tokenType == XPathTokenTypes.EXT_PREFIX) {
      parseFunction(builder);
    } else {
      return false;
    }
    return true;
  }

  private void parseParenthesizedExpr(PsiBuilder builder) {
    final PsiBuilder.Marker marker = builder.mark();
    builder.advanceLexer();
    if (!parseParenExpr(builder)) {
      builder.error(XPathBundle.message("parsing.error.expression.expected"));
    }
    checkMatches(builder, XPathTokenTypes.RPAREN, XPathBundle.message("parsing.error.closing.parenthesis.expected"));
    marker.done(XPathElementTypes.PARENTHESIZED_EXPR);
  }

  protected boolean parseParenExpr(PsiBuilder builder) {
    return parseExpr(builder);
  }

  protected boolean parseFunction(PsiBuilder builder) {
    IElementType tokenType = builder.getTokenType();
    if (tokenType == XPathTokenTypes.FUNCTION_NAME || tokenType == XPathTokenTypes.EXT_PREFIX) {
      final PsiBuilder.Marker func = builder.mark();
      builder.advanceLexer();
      if (builder.getTokenType() == XPathTokenTypes.COL) {
        builder.advanceLexer();
        checkMatches(builder, XPathTokenTypes.FUNCTION_NAME, XPathBundle.message("parsing.error.function.name.expected"));
      }
      parseArgumentList(builder);

      func.done(XPathElementTypes.FUNCTION_CALL);
      return true;
    }
    return false;
  }

  protected void parseArgumentList(PsiBuilder builder) {
    checkMatches(builder, XPathTokenTypes.LPAREN, XPathBundle.message("parsing.error.opening.parenthesis.expected"));

    if (builder.getTokenType() != XPathTokenTypes.RPAREN) {
      if (!parseArgument(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
    }
    while (builder.getTokenType() == XPathTokenTypes.COMMA) {
      builder.advanceLexer();
      if (!parseArgument(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
    }
    checkMatches(builder, XPathTokenTypes.RPAREN, XPathBundle.message("parsing.error.closing.parenthesis.expected"));
  }

  protected boolean parseArgument(PsiBuilder builder) {
    return parseExpr(builder);
  }

  private static void parseNumber(PsiBuilder builder) {
    final PsiBuilder.Marker marker = builder.mark();
    builder.advanceLexer();
    marker.done(XPathElementTypes.NUMBER);
  }

  private static void parseLiteral(PsiBuilder builder) {
    final PsiBuilder.Marker marker = builder.mark();
    builder.advanceLexer();
    marker.done(XPathElementTypes.STRING);
  }

  private static void parseVariable(PsiBuilder builder) {
    parseVariable(builder, XPathElementTypes.VARIABLE_REFERENCE);
  }

  protected static void parseVariable(PsiBuilder builder, IElementType elementType) {
    final PsiBuilder.Marker marker = builder.mark();
    builder.advanceLexer();
    if (builder.getTokenType() == XPathTokenTypes.VARIABLE_PREFIX) {
      builder.advanceLexer();
      checkMatches(builder, XPathTokenTypes.COL, XPathBundle.message("parsing.error.colon.expected"));
    }
    checkMatches(builder, XPathTokenTypes.VARIABLE_NAME, XPathBundle.message("parsing.error.variable.expected"));
    marker.done(elementType);
  }

  protected boolean parseExpr(PsiBuilder builder) {
    return parseOrExpr(builder);
  }

  protected boolean parseOrExpr(PsiBuilder builder) {
    PsiBuilder.Marker expr = builder.mark();
    if (!parseAndExpression(builder)) {
      expr.drop();
      return false;
    }

    while (builder.getTokenType() == XPathTokenTypes.OR) {
      makeToken(builder);
      if (!parseAndExpression(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
      expr.done(XPathElementTypes.BINARY_EXPRESSION);
      expr = expr.precede();
    }

    expr.drop();
    return true;
  }

  protected boolean parseAndExpression(PsiBuilder builder) {
    PsiBuilder.Marker expr = builder.mark();
    if (!parseEqualityExpression(builder)) {
      expr.drop();
      return false;
    }

    while (builder.getTokenType() == XPathTokenTypes.AND) {
      makeToken(builder);
      if (!parseEqualityExpression(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
      expr.done(XPathElementTypes.BINARY_EXPRESSION);
      expr = expr.precede();
    }

    expr.drop();
    return true;
  }

  protected boolean parseEqualityExpression(PsiBuilder builder) {
    PsiBuilder.Marker expr = builder.mark();
    if (!parseRelationalExpression(builder)) {
      expr.drop();
      return false;
    }

    while (XPathTokenTypes.EQUALITY_OPS.contains(builder.getTokenType())) {
      makeToken(builder);
      if (!parseRelationalExpression(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
      expr.done(XPathElementTypes.BINARY_EXPRESSION);
      expr = expr.precede();
    }

    expr.drop();
    return true;

  }

  private boolean parseRelationalExpression(PsiBuilder builder) {
    PsiBuilder.Marker expr = builder.mark();
    if (!parseAdditiveExpression(builder)) {
      expr.drop();
      return false;
    }

    while (XPathTokenTypes.REL_OPS.contains(builder.getTokenType())) {
      makeToken(builder);
      if (!parseAdditiveExpression(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
      expr.done(XPathElementTypes.BINARY_EXPRESSION);
      expr = expr.precede();
    }

    expr.drop();
    return true;

  }

  protected boolean parseAdditiveExpression(PsiBuilder builder) {
    PsiBuilder.Marker expr = builder.mark();
    if (!parseMultiplicativeExpression(builder)) {
      expr.drop();
      return false;
    }

    while (XPathTokenTypes.ADD_OPS.contains(builder.getTokenType())) {
      makeToken(builder);
      if (!parseMultiplicativeExpression(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
      expr.done(XPathElementTypes.BINARY_EXPRESSION);
      expr = expr.precede();
    }

    expr.drop();
    return true;

  }

  protected boolean parseMultiplicativeExpression(PsiBuilder builder) {
    PsiBuilder.Marker expr = builder.mark();
    if (!parseUnaryExpression(builder)) {
      expr.drop();
      return false;
    }

    while (XPathTokenTypes.MUL_OPS.contains(builder.getTokenType())) {
      makeToken(builder);
      if (!parseUnaryExpression(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
      expr.done(XPathElementTypes.BINARY_EXPRESSION);
      expr = expr.precede();
    }

    expr.drop();
    return true;

  }

  protected boolean parseUnaryExpression(PsiBuilder builder) {
    if (builder.getTokenType() == XPathTokenTypes.MINUS) {
      final PsiBuilder.Marker expr = builder.mark();
      builder.advanceLexer();
      if (!parseUnionExpression(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
      expr.done(XPathElementTypes.PREFIX_EXPRESSION);
      return true;
    } else {
      return parseUnionExpression(builder);
    }
  }

  protected boolean parseUnionExpression(final PsiBuilder builder) {
    PsiBuilder.Marker expr = builder.mark();
    if (!parsePathExpression(builder)) {
      expr.drop();
      return false;
    }

    while (unionOps().contains(builder.getTokenType())) {
      makeToken(builder);
      if (!parsePathExpression(builder)) {
        builder.error(XPathBundle.message("parsing.error.expression.expected"));
      }
      expr.done(XPathElementTypes.BINARY_EXPRESSION);
      expr = expr.precede();
    }

    expr.drop();
    return true;
  }

  protected TokenSet unionOps() {
    return TokenSet.create(XPathTokenTypes.UNION);
  }

  /**
   * [19]    PathExpr    ::=    LocationPath
   * | FilterExpr
   * | FilterExpr '/' RelativeLocationPath
   * | FilterExpr '//' RelativeLocationPath
   */
  protected boolean parsePathExpression(PsiBuilder builder) {
    final PsiBuilder.Marker marker = builder.mark();

    if (!parseLocationPath(builder, false) && !parseAbsoluteLocationPath(builder)) {
      final PsiBuilder.Marker m2 = builder.mark();
      if (!parseFilterExpression(builder)) {
        m2.drop();
        marker.drop();
        return false;
      }

      if (XPathTokenTypes.PATH_OPS.contains(builder.getTokenType())) {
        makeToken(builder);
        if (!parseLocationPath(builder, false, m2)) {
          builder.error(XPathBundle.message("parsing.error.location.path.expected"));
        }
        marker.done(XPathElementTypes.LOCATION_PATH);
      } else {
        m2.drop();
        marker.drop();
      }
    } else {
      marker.done(XPathElementTypes.LOCATION_PATH);
    }

    return true;
  }

  protected static void makeToken(PsiBuilder builder) {
    final IElementType tokenType = builder.getTokenType();
    final PsiBuilder.Marker marker = builder.mark();
    builder.advanceLexer();
    marker.done(tokenType);
  }

  /**
   * [3]    RelativeLocationPath    ::=    Step
   * | RelativeLocationPath '/' Step
   * | RelativeLocationPath '//' Step
   */
  private boolean parseLocationPath(PsiBuilder builder, boolean isAbsolute, PsiBuilder.Marker... m) {
    assert m.length <= 1;

    PsiBuilder.Marker marker = m.length == 1 ? m[0] : builder.mark();
    if (isAbsolute) {
      // root step
      makeToken(builder);
      if (builder.getTokenType() == null) {
        marker.done(XPathElementTypes.STEP);
        return true;
      }
    }
    if (!parseStep(builder)) {
      marker.drop();
      return false;
    }
    marker.done(XPathElementTypes.STEP);
    marker = marker.precede();

    if (XPathTokenTypes.PATH_OPS.contains(builder.getTokenType())) {
      do {
        makeToken(builder);
        if (!parseStep(builder)) {
          builder.error(XPathBundle.message("parsing.error.location.step.expected"));
        }
        marker.done(XPathElementTypes.STEP);
        marker = marker.precede();
      } while (XPathTokenTypes.PATH_OPS.contains(builder.getTokenType()));
    }

    marker.drop();
    return true;
  }

  /**
   * [4]    Step    ::=    AxisSpecifier NodeTest Predicate*
   * | AbbreviatedStep
   */
  private boolean parseStep(PsiBuilder builder) {
    if (parseAxisSpecifier(builder)) {
      if (!parseNodeTest(builder)) {
        builder.error(XPathBundle.message("parsing.error.node.test.expected"));
      }
      while (builder.getTokenType() == XPathTokenTypes.LBRACKET) {
        parsePredicate(builder);
      }

      return true;
    } else if (parseAbbreviatedStep(builder)) {
      return true;
    }
    return false;
  }

  /**
   * [12]    AbbreviatedStep    ::=    '.' | '..'
   */
  private static boolean parseAbbreviatedStep(PsiBuilder builder) {
    if (builder.getTokenType() == XPathTokenTypes.DOT || builder.getTokenType() == XPathTokenTypes.DOTDOT) {
      makeToken(builder);
      return true;
    }
    return false;
  }

  /**
   * [7]    NodeTest    ::=    NameTest | NodeType '(' ')' | 'processing-instruction' '(' Literal ')'
   */
  protected boolean parseNodeTest(PsiBuilder builder) {
    final PsiBuilder.Marker marker = builder.mark();
    if (!parseNameTest(builder)) {
      if (!parseNodeType(builder)) {
        marker.drop();
        return false;
      }
    }
    marker.done(XPathElementTypes.NODE_TEST);
    return true;
  }

  protected boolean parseNodeType(PsiBuilder builder) {
    if (builder.getTokenType() == XPathTokenTypes.NODE_TYPE) {
      final PsiBuilder.Marker m = builder.mark();
      builder.advanceLexer();
      parseArgumentList(builder);
      m.done(XPathElementTypes.NODE_TYPE);
      return true;
    }
    return false;
  }

  /**
   * [37]    NameTest    ::=    '*' | NCName ':' '*' | QName
   */
  protected boolean parseNameTest(PsiBuilder builder) {
    if (builder.getTokenType() == XPathTokenTypes.STAR) {
      return parseWildcard(builder);
    } else if (builder.getTokenType() == XPathTokenTypes.NCNAME) {
      builder.advanceLexer();

      if (builder.getTokenType() == XPathTokenTypes.COL) {
        builder.advanceLexer();
        if (builder.getTokenType() != XPathTokenTypes.STAR) {
          if (builder.getTokenType() != XPathTokenTypes.NCNAME) {
            builder.error(XPathBundle.message("parsing.error.star.or.ncname.expected"));
          } else {
            builder.advanceLexer();
          }
        } else {
          builder.advanceLexer();
        }
      }
      return true;
    }
    return false;
  }

  protected boolean parseWildcard(PsiBuilder builder) {
    builder.advanceLexer();
    return true;
  }

  /**
   * [5]    AxisSpecifier    ::=    AxisName '::' | AbbreviatedAxisSpecifier
   * <p/>
   * [13]   AbbreviatedAxisSpecifier    ::=    '@'?
   */
  private boolean parseAxisSpecifier(PsiBuilder builder) {
    final PsiBuilder.Marker marker = builder.mark();
    final IElementType tokenType = builder.getTokenType();
    if (XPathTokenTypes.AXIS.contains(tokenType)) {
      builder.advanceLexer();
      checkMatches(builder, XPathTokenTypes.COLCOL, XPathBundle.message("parsing.error.double.colon.expected"));
    } else {
      if (tokenType == XPathTokenTypes.AT) {
        builder.advanceLexer();
      } else if (tokenType == XPathTokenTypes.DOT || tokenType == XPathTokenTypes.DOTDOT) {
        marker.drop();
        return false;
      } else {
        final PsiBuilder.Marker m = builder.mark();
        final boolean b = parseNodeTest(builder);
        m.rollbackTo();
        if (!b) {
          marker.drop();
          return false;
        }
      }
    }
    marker.done(XPathElementTypes.AXIS_SPECIFIER);
    return true;
  }

  /**
   * [20]    FilterExpr    ::=    PrimaryExpr | FilterExpr Predicate
   */
  private boolean parseFilterExpression(PsiBuilder builder) {
    PsiBuilder.Marker expr = builder.mark();
    if (!parsePrimaryExpr(builder)) {
      expr.drop();
      return false;
    }

    while (XPathTokenTypes.LBRACKET == builder.getTokenType()) {
      parsePredicate(builder);

      expr.done(XPathElementTypes.FILTER_EXPRESSION);
      expr = expr.precede();
    }
    expr.drop();

    return true;
  }

  /**
   * [8]    Predicate         ::=    '[' PredicateExpr ']'
   * [9]    PredicateExpr     ::=    Expr
   */
  protected boolean parsePredicate(PsiBuilder builder) {
    final PsiBuilder.Marker marker = builder.mark();
    if (builder.getTokenType() != XPathTokenTypes.LBRACKET) {
      marker.drop();
      return false;
    }
    builder.advanceLexer();
    if (!parseExpr(builder)) {
      builder.error(XPathBundle.message("parsing.error.expression.expected"));
    }
    checkMatches(builder, XPathTokenTypes.RBRACKET, XPathBundle.message("parsing.error.closing.bracket.expected"));
    marker.done(XPathElementTypes.PREDICATE);
    return true;
  }

  /**
   * [2]    AbsoluteLocationPath    ::=    '/' RelativeLocationPath? | AbbreviatedAbsoluteLocationPath
   */
  private boolean parseAbsoluteLocationPath(PsiBuilder builder) {
    if (builder.getTokenType() == XPathTokenTypes.PATH) {
      parseLocationPath(builder, true);
      return true;
    } else {
      return parseAbbreviatedAbsoluteLocationPath(builder);
    }
  }

  /**
   * [10]    AbbreviatedAbsoluteLocationPath    ::=    '//' RelativeLocationPath
   */
  private boolean parseAbbreviatedAbsoluteLocationPath(PsiBuilder builder) {
    if (builder.getTokenType() == XPathTokenTypes.ANY_PATH) {
      if (!parseLocationPath(builder, true)) {
        builder.error(XPathBundle.message("parsing.error.location.path.expected"));
      }
      return true;
    }
    return false;
  }

  private static void consumeBadTokens(PsiBuilder builder, TokenSet nextAccepted) {
    if (nextAccepted.getTypes().length == 0 && builder.eof()) {
      return;
    }
    if (!nextAccepted.contains(builder.getTokenType())) {
      builder.error(XPathBundle.message("parsing.error.unexpected.token"));
      do {
        if (builder.eof()) {
          builder.error(XPathBundle.message("parsing.error.unexpected.end.file"));
          break;
        }
        builder.advanceLexer();
      } while (!nextAccepted.contains(builder.getTokenType()));
    }
  }

  protected static void checkMatches(final PsiBuilder builder, final IElementType token, @NotNull @NlsContexts.ParsingError String message) {
    if (builder.getTokenType() == token) {
      builder.advanceLexer();
    } else {
      builder.error(message);
    }
  }

  protected static void checkMatches(final PsiBuilder builder, final TokenSet tokens, @NotNull @NlsContexts.ParsingError String message) {
    if (tokens.contains(builder.getTokenType())) {
      builder.advanceLexer();
    } else {
      builder.error(message);
    }
  }
}
