/* Generated By:JJTree: Do not edit this line. OMatchStatement.java Version 4.3 */
/* JavaCCOptions:MULTI=true,NODE_USES_PARSER=false,VISITOR=true,TRACK_TOKENS=true,NODE_PREFIX=O,NODE_EXTENDS=,NODE_FACTORY=,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */
package com.orientechnologies.orient.core.sql.parser;

import com.orientechnologies.common.exception.OErrorCode;
import com.orientechnologies.common.listener.OProgressListener;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.util.OPair;
import com.orientechnologies.orient.core.command.*;
import com.orientechnologies.orient.core.db.ODatabase;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OSchema;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OCommandExecutorSQLResultsetDelegate;
import com.orientechnologies.orient.core.sql.OCommandExecutorSQLSelect;
import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.OIterableRecordSource;
import com.orientechnologies.orient.core.sql.executor.*;
import com.orientechnologies.orient.core.sql.filter.OSQLTarget;
import com.orientechnologies.orient.core.sql.query.OBasicLegacyResultSet;
import com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.stream.Collectors;

public class OMatchStatement extends OStatement implements OCommandExecutor, OIterableRecordSource {

  static final String DEFAULT_ALIAS_PREFIX = "$ORIENT_DEFAULT_ALIAS_";

  private OSQLAsynchQuery<ODocument> request;

  long threshold = 20;
  private int limitFromProtocol = -1;

  public List<ONestedProjection> getReturnNestedProjections() {
    return returnNestedProjections;
  }

  public void setReturnNestedProjections(List<ONestedProjection> returnNestedProjections) {
    this.returnNestedProjections = returnNestedProjections;
  }

  public class MatchContext {
    int currentEdgeNumber = 0;

    Map<String, Iterable>      candidates   = new LinkedHashMap<String, Iterable>();
    Map<String, OIdentifiable> matched      = new LinkedHashMap<String, OIdentifiable>();
    Map<PatternEdge, Boolean>  matchedEdges = new IdentityHashMap<PatternEdge, Boolean>();

    public MatchContext copy(String alias, OIdentifiable value) {
      MatchContext result = new MatchContext();

      result.candidates.putAll(candidates);
      result.candidates.remove(alias);

      result.matched.putAll(matched);
      result.matched.put(alias, value);

      result.matchedEdges.putAll(matchedEdges);
      result.currentEdgeNumber = currentEdgeNumber;
      return result;
    }

    public ODocument toDoc() {
      ODocument doc = new ODocument();
      doc.fromMap((Map) matched);
      return doc;
    }

  }

  public static class EdgeTraversal {
    boolean     out = true;
    PatternEdge edge;

    public EdgeTraversal(PatternEdge edge, boolean out) {
      this.edge = edge;
      this.out = out;
    }
  }

  public static class MatchExecutionPlan {
    public List<EdgeTraversal> sortedEdges;
    public Map<String, Long>   preFetchedAliases = new HashMap<String, Long>();
    public String              rootAlias;
  }

  public static final String                  KEYWORD_MATCH           = "MATCH";
  // parsed data
  protected           List<OMatchExpression>  matchExpressions        = new ArrayList<>();
  protected           List<OMatchExpression>  notMatchExpressions     = new ArrayList<>();
  protected           List<OExpression>       returnItems             = new ArrayList<>();
  protected           List<OIdentifier>       returnAliases           = new ArrayList<>();
  protected           List<ONestedProjection> returnNestedProjections = new ArrayList<>();
  protected           boolean                 returnDistinct          = false;
  protected           OGroupBy                groupBy;
  protected           OOrderBy                orderBy;
  protected           OUnwind                 unwind;
  protected           OSkip                   skip;
  protected           OLimit                  limit;

  // post-parsing generated data
  protected Pattern pattern;

  private Map<String, OWhereClause> aliasFilters;
  private Map<String, String>       aliasClasses;

  // execution data
  private OCommandContext   context;
  private OProgressListener progressListener;

  public OMatchStatement() {
    super(-1);
  }

  public OMatchStatement(int id) {
    super(id);
  }

  public OMatchStatement(OrientSql p, int id) {
    super(p, id);
  }

  @Override
  public OResultSet execute(ODatabase db, Object[] args, OCommandContext parentCtx, boolean usePlanCache) {
    OBasicCommandContext ctx = new OBasicCommandContext();
    if (parentCtx != null) {
      ctx.setParentWithoutOverridingChild(parentCtx);
    }
    ctx.setDatabase(db);
    Map<Object, Object> params = new HashMap<>();
    if (args != null) {
      for (int i = 0; i < args.length; i++) {
        params.put(i, args[i]);
      }
    }
    ctx.setInputParameters(params);
    OInternalExecutionPlan executionPlan;
    if (usePlanCache) {
      executionPlan = createExecutionPlan(ctx, false);
    } else {
      executionPlan = createExecutionPlanNoCache(ctx, false);
    }

    return new OLocalResultSet(executionPlan);
  }

  @Override
  public OResultSet execute(ODatabase db, Map params, OCommandContext parentCtx, boolean usePlanCache) {
    OBasicCommandContext ctx = new OBasicCommandContext();
    if (parentCtx != null) {
      ctx.setParentWithoutOverridingChild(parentCtx);
    }
    ctx.setDatabase(db);
    ctx.setInputParameters(params);
    OInternalExecutionPlan executionPlan;
    if (usePlanCache) {
      executionPlan = createExecutionPlan(ctx, false);
    } else {
      executionPlan = createExecutionPlanNoCache(ctx, false);
    }

    return new OLocalResultSet(executionPlan);
  }

  public OInternalExecutionPlan createExecutionPlan(OCommandContext ctx, boolean enableProfiling) {
    OMatchExecutionPlanner planner = new OMatchExecutionPlanner(this);
    OInternalExecutionPlan result = planner.createExecutionPlan(ctx, enableProfiling);
    result.setStatement(originalStatement);
    return result;
  }

  /**
   * Accept the visitor. *
   */
  public Object jjtAccept(OrientSqlVisitor visitor, Object data) {
    return visitor.visit(this, data);
  }

  // ------------------------------------------------------------------
  // query parsing and optimization
  // ------------------------------------------------------------------

  /**
   * this method parses the statement
   *
   * @param iRequest Command request implementation.
   * @param <RET>
   *
   * @return
   */
  @Override
  public <RET extends OCommandExecutor> RET parse(OCommandRequest iRequest) {
    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;
    if (iRequest instanceof OSQLSynchQuery) {
      request = (OSQLSynchQuery<ODocument>) iRequest;
    } else if (iRequest instanceof OSQLAsynchQuery) {
      request = (OSQLAsynchQuery<ODocument>) iRequest;
    } else {
      // BUILD A QUERY OBJECT FROM THE COMMAND REQUEST
      request = new OSQLSynchQuery<ODocument>(textRequest.getText());
      if (textRequest.getResultListener() != null) {
        request.setResultListener(textRequest.getResultListener());
      }
    }
    String queryText = textRequest.getText();

    // please, do not look at this... refactor this ASAP with new executor structure
    final InputStream is = new ByteArrayInputStream(queryText.getBytes());
    OrientSql osql = null;
    try {
      ODatabaseDocumentInternal db = getDatabase();
      if (db == null) {
        osql = new OrientSql(is);
      } else {
        osql = new OrientSql(is, db.getStorage().getConfiguration().getCharset());
      }
    } catch (UnsupportedEncodingException e) {
      OLogManager.instance().warn(this,
          "Invalid charset for database " + getDatabase() + " " + getDatabase().getStorage().getConfiguration().getCharset());
      osql = new OrientSql(is);
    }

    try {
      OMatchStatement result = (OMatchStatement) osql.parse();
      this.matchExpressions = result.matchExpressions;
      this.notMatchExpressions = result.notMatchExpressions;
      this.returnItems = result.returnItems;
      this.returnAliases = result.returnAliases;
      this.limit = result.limit;
    } catch (ParseException e) {
      OCommandSQLParsingException ex = new OCommandSQLParsingException(e, queryText);
      OErrorCode.QUERY_PARSE_ERROR.throwException(ex.getMessage(), ex);
    }

    buildPatterns();
    pattern.validate();
    return (RET) this;
  }

  protected void buildPatterns() {
    assignDefaultAliases(this.matchExpressions);
    pattern = new Pattern();
    for (OMatchExpression expr : this.matchExpressions) {
      pattern.addExpression(expr);
    }

    Map<String, OWhereClause> aliasFilters = new LinkedHashMap<String, OWhereClause>();
    Map<String, String> aliasClasses = new LinkedHashMap<String, String>();
    for (OMatchExpression expr : this.matchExpressions) {
      addAliases(expr, aliasFilters, aliasClasses, context);
    }

    this.aliasFilters = aliasFilters;
    this.aliasClasses = aliasClasses;

    rebindFilters(aliasFilters);
  }

  /**
   * rebinds filter (where) conditions to alias nodes after optimization
   *
   * @param aliasFilters
   */
  private void rebindFilters(Map<String, OWhereClause> aliasFilters) {
    for (OMatchExpression expression : matchExpressions) {
      OWhereClause newFilter = aliasFilters.get(expression.origin.getAlias());
      expression.origin.setFilter(newFilter);

      for (OMatchPathItem item : expression.items) {
        newFilter = aliasFilters.get(item.filter.getAlias());
        item.filter.setFilter(newFilter);
      }
    }
  }

  /**
   * assigns default aliases to pattern nodes that do not have an explicit alias
   *
   * @param matchExpressions
   */
  private void assignDefaultAliases(List<OMatchExpression> matchExpressions) {
    int counter = 0;
    for (OMatchExpression expression : matchExpressions) {
      if (expression.origin.getAlias() == null) {
        expression.origin.setAlias(DEFAULT_ALIAS_PREFIX + (counter++));
      }

      for (OMatchPathItem item : expression.items) {
        if (item.filter == null) {
          item.filter = new OMatchFilter(-1);
        }
        if (item.filter.getAlias() == null) {
          item.filter.setAlias(DEFAULT_ALIAS_PREFIX + (counter++));
        }
      }
    }
  }

  // ------------------------------------------------------------------
  // query execution
  // ------------------------------------------------------------------

  /**
   * this method works statefully, using request and context variables from current Match statement. This method will be deprecated
   * in next releases
   *
   * @param iArgs Optional variable arguments to pass to the command.
   *
   * @return
   */
  @Override
  public Object execute(Map<Object, Object> iArgs) {
    this.context.setInputParameters(iArgs);
    return execute(this.request, this.context, this.progressListener);
  }

  /**
   * executes the match statement. This is the preferred execute() method and it has to be used as the default one in the future.
   * This method works in stateless mode
   *
   * @param request
   * @param context
   *
   * @return
   */
  public Object execute(OSQLAsynchQuery<ODocument> request, OCommandContext context, OProgressListener progressListener) {
    if (orderBy != null) {
      throw new OCommandExecutionException("ORDER BY is not supported in MATCH on the legacy API");
    }
    if (groupBy != null) {
      throw new OCommandExecutionException("GROUP BY is not supported in MATCH on the legacy API");
    }
    if (unwind != null) {
      throw new OCommandExecutionException("UNWIND is not supported in MATCH on the legacy API");
    }
    if (skip != null) {
      throw new OCommandExecutionException("SKIP is not supported in MATCH on the legacy API");
    }

    Map<Object, Object> iArgs = context.getInputParameters();
    try {

      Map<String, Long> estimatedRootEntries = estimateRootEntries(aliasClasses, aliasFilters, context);
      if (estimatedRootEntries.values().contains(0l)) {
        return new OBasicLegacyResultSet();// some aliases do not match on any classes
      }

      List<EdgeTraversal> sortedEdges = getTopologicalSortedSchedule(estimatedRootEntries, pattern);
      MatchExecutionPlan executionPlan = new MatchExecutionPlan();
      executionPlan.sortedEdges = sortedEdges;

      calculateMatch(pattern, estimatedRootEntries, new MatchContext(), aliasClasses, aliasFilters, context, request,
          executionPlan);

      return getResult(request);
    } finally {
      if (request.getResultListener() != null) {
        request.getResultListener().end();
      }
    }

  }

  /**
   * Start a depth-first traversal from the starting node, adding all viable unscheduled edges and vertices.
   *
   * @param startNode             the node from which to start the depth-first traversal
   * @param visitedNodes          set of nodes that are already visited (mutated in this function)
   * @param visitedEdges          set of edges that are already visited and therefore don't need to be scheduled (mutated in this
   *                              function)
   * @param remainingDependencies dependency map including only the dependencies that haven't yet been satisfied (mutated in this
   *                              function)
   * @param resultingSchedule     the schedule being computed i.e. appended to (mutated in this function)
   */
  private void updateScheduleStartingAt(PatternNode startNode, Set<PatternNode> visitedNodes, Set<PatternEdge> visitedEdges,
      Map<String, Set<String>> remainingDependencies, List<EdgeTraversal> resultingSchedule) {
    // OrientDB requires the schedule to contain all edges present in the query, which is a stronger condition
    // than simply visiting all nodes in the query. Consider the following example query:
    //     MATCH {
    //         class: A,
    //         as: foo
    //     }.in() {
    //         as: bar
    //     }, {
    //         class: B,
    //         as: bar
    //     }.out() {
    //         as: foo
    //     } RETURN $matches
    // The schedule for the above query must have two edges, even though there are only two nodes and they can both
    // be visited with the traversal of a single edge.
    //
    // To satisfy it, we obey the following for each non-optional node:
    // - ignore edges to neighboring nodes which have unsatisfied dependencies;
    // - for visited neighboring nodes, add their edge if it wasn't already present in the schedule, but do not
    //   recurse into the neighboring node;
    // - for unvisited neighboring nodes with satisfied dependencies, add their edge and recurse into them.
    visitedNodes.add(startNode);
    for (Set<String> dependencies : remainingDependencies.values()) {
      dependencies.remove(startNode.alias);
    }

    Map<PatternEdge, Boolean> edges = new LinkedHashMap<PatternEdge, Boolean>();
    for (PatternEdge outEdge : startNode.out) {
      edges.put(outEdge, true);
    }
    for (PatternEdge inEdge : startNode.in) {
      edges.put(inEdge, false);
    }

    for (Map.Entry<PatternEdge, Boolean> edgeData : edges.entrySet()) {
      PatternEdge edge = edgeData.getKey();
      boolean isOutbound = edgeData.getValue();
      PatternNode neighboringNode = isOutbound ? edge.in : edge.out;

      if (!remainingDependencies.get(neighboringNode.alias).isEmpty()) {
        // Unsatisfied dependencies, ignore this neighboring node.
        continue;
      }

      if (visitedNodes.contains(neighboringNode)) {
        if (!visitedEdges.contains(edge)) {
          // If we are executing in this block, we are in the following situation:
          // - the startNode has not been visited yet;
          // - it has a neighboringNode that has already been visited;
          // - the edge between the startNode and the neighboringNode has not been scheduled yet.
          //
          // The isOutbound value shows us whether the edge is outbound from the point of view of the startNode.
          // However, if there are edges to the startNode, we must visit the startNode from an already-visited
          // neighbor, to preserve the validity of the traversal. Therefore, we negate the value of isOutbound
          // to ensure that the edge is always scheduled in the direction from the already-visited neighbor
          // toward the startNode. Notably, this is also the case when evaluating "optional" nodes -- we always
          // visit the optional node from its non-optional and already-visited neighbor.
          //
          // The only exception to the above is when we have edges with "while" conditions. We are not allowed
          // to flip their directionality, so we leave them as-is.
          boolean traversalDirection;
          if (startNode.optional || edge.item.isBidirectional()) {
            traversalDirection = !isOutbound;
          } else {
            traversalDirection = isOutbound;
          }

          visitedEdges.add(edge);
          resultingSchedule.add(new EdgeTraversal(edge, traversalDirection));
        }
      } else if (!startNode.optional) {
        // If the neighboring node wasn't visited, we don't expand the optional node into it, hence the above check.
        // Instead, we'll allow the neighboring node to add the edge we failed to visit, via the above block.
        if (visitedEdges.contains(edge)) {
          // Should never happen.
          throw new AssertionError("The edge was visited, but the neighboring vertex was not: " + edge + " " + neighboringNode);
        }

        visitedEdges.add(edge);
        resultingSchedule.add(new EdgeTraversal(edge, isOutbound));
        updateScheduleStartingAt(neighboringNode, visitedNodes, visitedEdges, remainingDependencies, resultingSchedule);
      }
    }
  }

  /**
   * Calculate the set of dependency aliases for each alias in the pattern.
   *
   * @param pattern
   *
   * @return map of alias to the set of aliases it depends on
   */
  private Map<String, Set<String>> getDependencies(Pattern pattern) {
    Map<String, Set<String>> result = new HashMap<String, Set<String>>();

    for (PatternNode node : pattern.aliasToNode.values()) {
      Set<String> currentDependencies = new HashSet<String>();

      OWhereClause filter = aliasFilters.get(node.alias);
      if (filter != null && filter.baseExpression != null) {
        List<String> involvedAliases = filter.baseExpression.getMatchPatternInvolvedAliases();
        if (involvedAliases != null) {
          currentDependencies.addAll(involvedAliases);
        }
      }

      result.put(node.alias, currentDependencies);
    }

    return result;
  }

  /**
   * sort edges in the order they will be matched
   */
  private List<EdgeTraversal> getTopologicalSortedSchedule(Map<String, Long> estimatedRootEntries, Pattern pattern) {
    List<EdgeTraversal> resultingSchedule = new ArrayList<EdgeTraversal>();
    Map<String, Set<String>> remainingDependencies = getDependencies(pattern);
    Set<PatternNode> visitedNodes = new HashSet<PatternNode>();
    Set<PatternEdge> visitedEdges = new HashSet<PatternEdge>();

    // Sort the possible root vertices in order of estimated size, since we want to start with a small vertex set.
    List<OPair<Long, String>> rootWeights = new ArrayList<OPair<Long, String>>();
    for (Map.Entry<String, Long> root : estimatedRootEntries.entrySet()) {
      rootWeights.add(new OPair<Long, String>(root.getValue(), root.getKey()));
    }
    Collections.sort(rootWeights);

    // Add the starting vertices, in the correct order, to an ordered set.
    Set<String> remainingStarts = new LinkedHashSet<String>();
    for (OPair<Long, String> item : rootWeights) {
      remainingStarts.add(item.getValue());
    }
    // Add all the remaining aliases after all the suggested start points.
    for (String alias : pattern.aliasToNode.keySet()) {
      if (!remainingStarts.contains(alias)) {
        remainingStarts.add(alias);
      }
    }

    while (resultingSchedule.size() < pattern.numOfEdges) {
      // Start a new depth-first pass, adding all nodes with satisfied dependencies.
      // 1. Find a starting vertex for the depth-first pass.
      PatternNode startingNode = null;
      List<String> startsToRemove = new ArrayList<String>();
      for (String currentAlias : remainingStarts) {
        PatternNode currentNode = pattern.aliasToNode.get(currentAlias);

        if (visitedNodes.contains(currentNode)) {
          // If a previous traversal already visited this alias, remove it from further consideration.
          startsToRemove.add(currentAlias);
        } else if (remainingDependencies.get(currentAlias).isEmpty()) {
          // If it hasn't been visited, and has all dependencies satisfied, visit it.
          startsToRemove.add(currentAlias);
          startingNode = currentNode;
          break;
        }
      }
      remainingStarts.removeAll(startsToRemove);

      if (startingNode == null) {
        // We didn't manage to find a valid root, and yet we haven't constructed a complete schedule.
        // This means there must be a cycle in our dependency graph, or all dependency-free nodes are optional.
        // Therefore, the query is invalid.
        throw new OCommandExecutionException("This query contains MATCH conditions that cannot be evaluated, "
            + "like an undefined alias or a circular dependency on a $matched condition.");
      }

      // 2. Having found a starting vertex, traverse its neighbors depth-first,
      //    adding any non-visited ones with satisfied dependencies to our schedule.
      updateScheduleStartingAt(startingNode, visitedNodes, visitedEdges, remainingDependencies, resultingSchedule);
    }

    if (resultingSchedule.size() != pattern.numOfEdges) {
      throw new AssertionError("Incorrect number of edges: " + resultingSchedule.size() + " vs " + pattern.numOfEdges);
    }

    return resultingSchedule;
  }

  protected Object getResult(OSQLAsynchQuery<ODocument> request) {
    if (request instanceof OSQLSynchQuery)
      return ((OSQLSynchQuery<ODocument>) request).getResult();

    return null;
  }

  private boolean calculateMatch(Pattern pattern, Map<String, Long> estimatedRootEntries, MatchContext matchContext,
      Map<String, String> aliasClasses, Map<String, OWhereClause> aliasFilters, OCommandContext iCommandContext,
      OSQLAsynchQuery<ODocument> request, MatchExecutionPlan executionPlan) {

    boolean rootFound = false;
    // find starting nodes with few entries
    for (Map.Entry<String, Long> entryPoint : estimatedRootEntries.entrySet()) {
      if (entryPoint.getValue() < threshold) {
        String nextAlias = entryPoint.getKey();
        Iterable<OIdentifiable> matches = fetchAliasCandidates(nextAlias, aliasFilters, iCommandContext, aliasClasses);

        Set<OIdentifiable> ids = new HashSet<OIdentifiable>();
        if (!matches.iterator().hasNext()) {
          if (pattern.get(nextAlias).isOptionalNode()) {
            continue;
          }
          return true;
        }

        matchContext.candidates.put(nextAlias, matches);
        executionPlan.preFetchedAliases.put(nextAlias, entryPoint.getValue());
        rootFound = true;
      }
    }
    // no nodes under threshold, guess the smallest one
    if (!rootFound) {
      String nextAlias = getNextAlias(estimatedRootEntries, matchContext);
      Iterable<OIdentifiable> matches = fetchAliasCandidates(nextAlias, aliasFilters, iCommandContext, aliasClasses);
      if (!matches.iterator().hasNext()) {
        return true;
      }
      matchContext.candidates.put(nextAlias, matches);
      executionPlan.preFetchedAliases.put(nextAlias, estimatedRootEntries.get(nextAlias));
    }

    // pick first edge (as sorted before)
    EdgeTraversal firstEdge = executionPlan.sortedEdges.size() == 0 ? null : executionPlan.sortedEdges.get(0);
    String smallestAlias = null;
    // and choose the most convenient starting point (the most convenient traversal direction)
    if (firstEdge != null) {
      smallestAlias = firstEdge.out ? firstEdge.edge.out.alias : firstEdge.edge.in.alias;
    } else {
      smallestAlias = pattern.aliasToNode.values().iterator().next().alias;
    }
    executionPlan.rootAlias = smallestAlias;
    Iterable<OIdentifiable> allCandidates = matchContext.candidates.get(smallestAlias);

    if (allCandidates == null) {
      OSelectStatement select = buildSelectStatement(aliasClasses.get(smallestAlias), aliasFilters.get(smallestAlias));
      allCandidates = (Iterable) getDatabase().query(new OSQLSynchQuery<Object>(select.toString()));
    }

    if (!processContextFromCandidates(pattern, executionPlan, matchContext, aliasClasses, aliasFilters, iCommandContext, request,
        allCandidates, smallestAlias, 0)) {
      return false;
    }
    return true;
  }

  private boolean processContextFromCandidates(Pattern pattern, MatchExecutionPlan executionPlan, MatchContext matchContext,
      Map<String, String> aliasClasses, Map<String, OWhereClause> aliasFilters, OCommandContext iCommandContext,
      OSQLAsynchQuery<ODocument> request, Iterable<OIdentifiable> candidates, String alias, int startFromEdge) {
    for (OIdentifiable id : candidates) {
      MatchContext childContext = matchContext.copy(alias, id);
      childContext.currentEdgeNumber = startFromEdge;
      if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
        return false;
      }
    }
    return true;
  }

  private Iterable<OIdentifiable> fetchAliasCandidates(String nextAlias, Map<String, OWhereClause> aliasFilters,
      OCommandContext iCommandContext, Map<String, String> aliasClasses) {
    Iterator<OIdentifiable> it = query(aliasClasses.get(nextAlias), aliasFilters.get(nextAlias), iCommandContext);
    Set<OIdentifiable> result = new HashSet<OIdentifiable>();
    while (it.hasNext()) {
      result.add(it.next().getIdentity());
    }

    return result;
  }

  private boolean processContext(Pattern pattern, MatchExecutionPlan executionPlan, MatchContext matchContext,
      Map<String, String> aliasClasses, Map<String, OWhereClause> aliasFilters, OCommandContext iCommandContext,
      OSQLAsynchQuery<ODocument> request) {

    iCommandContext.setVariable("$matched", matchContext.matched);

    if (pattern.getNumOfEdges() == matchContext.matchedEdges.size() && allNodesCalculated(matchContext, pattern)) {
      // false if limit reached
      return addResult(matchContext, request, iCommandContext);
    }
    if (executionPlan.sortedEdges.size() == matchContext.currentEdgeNumber) {
      // false if limit reached
      return expandCartesianProduct(pattern, matchContext, aliasClasses, aliasFilters, iCommandContext, request);
    }
    EdgeTraversal currentEdge = executionPlan.sortedEdges.get(matchContext.currentEdgeNumber);
    PatternNode rootNode = currentEdge.out ? currentEdge.edge.out : currentEdge.edge.in;

    if (currentEdge.out) {
      PatternEdge outEdge = currentEdge.edge;

      if (!matchContext.matchedEdges.containsKey(outEdge)) {

        OIdentifiable startingPoint = matchContext.matched.get(outEdge.out.alias);
        if (startingPoint == null) {
          //restart from candidates (disjoint patterns? optional? just could not proceed from last node?)
          Iterable rightCandidates = matchContext.candidates.get(outEdge.out.alias);
          if (rightCandidates != null) {
            if (!processContextFromCandidates(pattern, executionPlan, matchContext, aliasClasses, aliasFilters, iCommandContext,
                request, rightCandidates, outEdge.out.alias, matchContext.currentEdgeNumber)) {
              return false;
            }
          }
          return true;
        }
        Object rightValues = outEdge.executeTraversal(matchContext, iCommandContext, startingPoint, 0);

        if (outEdge.in.isOptionalNode() && (isEmptyResult(rightValues) || !contains(rightValues,
            matchContext.matched.get(outEdge.in.alias)))) {
          MatchContext childContext = matchContext.copy(outEdge.in.alias, null);
          childContext.matched.put(outEdge.in.alias, null);
          childContext.currentEdgeNumber = matchContext.currentEdgeNumber + 1; //TODO testOptional 3 match passa con +1
          childContext.matchedEdges.put(outEdge, true);

          if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
            return false;
          }
        }
        if (!(rightValues instanceof Iterable)) {
          rightValues = Collections.singleton(rightValues);
        }
        String rightClassName = aliasClasses.get(outEdge.in.alias);
        OClass rightClass = getDatabase().getMetadata().getSchema().getClass(rightClassName);
        for (OIdentifiable rightValue : (Iterable<OIdentifiable>) rightValues) {
          if (rightValue == null) {
            continue; //broken graph?, null reference
          }

          if (rightClass != null && !matchesClass(rightValue, rightClass)) {
            continue;
          }
          Iterable<OIdentifiable> prevMatchedRightValues = matchContext.candidates.get(outEdge.in.alias);

          if (matchContext.matched.containsKey(outEdge.in.alias)) {
            if (matchContext.matched.get(outEdge.in.alias).getIdentity().equals(rightValue.getIdentity())) {
              MatchContext childContext = matchContext.copy(outEdge.in.alias, rightValue.getIdentity());
              childContext.currentEdgeNumber = matchContext.currentEdgeNumber + 1;
              childContext.matchedEdges.put(outEdge, true);
              if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
                return false;
              }
              break;
            }
          } else if (prevMatchedRightValues != null && prevMatchedRightValues.iterator().hasNext()) {// just matching against
            // known
            // values
            for (OIdentifiable id : prevMatchedRightValues) {
              if (id.getIdentity().equals(rightValue.getIdentity())) {
                MatchContext childContext = matchContext.copy(outEdge.in.alias, id);
                childContext.currentEdgeNumber = matchContext.currentEdgeNumber + 1;
                childContext.matchedEdges.put(outEdge, true);
                if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
                  return false;
                }
              }
            }
          } else {// searching for neighbors
            MatchContext childContext = matchContext.copy(outEdge.in.alias, rightValue.getIdentity());
            childContext.currentEdgeNumber = matchContext.currentEdgeNumber + 1;
            childContext.matchedEdges.put(outEdge, true);
            if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
              return false;
            }
          }
        }
      }
    } else {
      PatternEdge inEdge = currentEdge.edge;
      if (!matchContext.matchedEdges.containsKey(inEdge)) {
        if (!inEdge.item.isBidirectional()) {
          throw new RuntimeException("Invalid pattern to match!");
        }
        if (!matchContext.matchedEdges.containsKey(inEdge)) {
          Object leftValues = inEdge.item.method.executeReverse(matchContext.matched.get(inEdge.in.alias), iCommandContext);
          if (inEdge.out.isOptionalNode() && (isEmptyResult(leftValues) || !contains(leftValues,
              matchContext.matched.get(inEdge.out.alias)))) {
            MatchContext childContext = matchContext.copy(inEdge.out.alias, null);
            childContext.matched.put(inEdge.out.alias, null);
            childContext.currentEdgeNumber = matchContext.currentEdgeNumber + 1;
            childContext.matchedEdges.put(inEdge, true);
            if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
              return false;
            }
          }
          if (leftValues instanceof OIdentifiable || !(leftValues instanceof Iterable)) {
            leftValues = Collections.singleton(leftValues);
          }

          String leftClassName = aliasClasses.get(inEdge.out.alias);
          OClass leftClass = getDatabase().getMetadata().getSchema().getClass(leftClassName);

          for (OIdentifiable leftValue : (Iterable<OIdentifiable>) leftValues) {
            if (leftValue == null) {
              continue; //broken graph? null reference
            }

            if (leftClass != null && !matchesClass(leftValue, leftClass)) {
              continue;
            }
            Iterable<OIdentifiable> prevMatchedRightValues = matchContext.candidates.get(inEdge.out.alias);

            if (matchContext.matched.containsKey(inEdge.out.alias)) {
              if (matchContext.matched.get(inEdge.out.alias).getIdentity().equals(leftValue.getIdentity())) {
                MatchContext childContext = matchContext.copy(inEdge.out.alias, leftValue.getIdentity());
                childContext.currentEdgeNumber = matchContext.currentEdgeNumber + 1;
                childContext.matchedEdges.put(inEdge, true);
                if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
                  return false;
                }
                break;
              }
            } else if (prevMatchedRightValues != null && prevMatchedRightValues.iterator().hasNext()) {// just matching against
              // known
              // values
              for (OIdentifiable id : prevMatchedRightValues) {
                if (id.getIdentity().equals(leftValue.getIdentity())) {
                  MatchContext childContext = matchContext.copy(inEdge.out.alias, id);
                  childContext.currentEdgeNumber = matchContext.currentEdgeNumber + 1;
                  childContext.matchedEdges.put(inEdge, true);

                  if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
                    return false;
                  }
                }
              }
            } else { // searching for neighbors
              OWhereClause where = aliasFilters.get(inEdge.out.alias);
              String className = aliasClasses.get(inEdge.out.alias);
              OClass oClass = getDatabase().getMetadata().getSchema().getClass(className);
              if ((oClass == null || matchesClass(leftValue, oClass)) && (where == null || where
                  .matchesFilters(leftValue, iCommandContext))) {
                MatchContext childContext = matchContext.copy(inEdge.out.alias, leftValue.getIdentity());
                childContext.currentEdgeNumber = matchContext.currentEdgeNumber + 1;
                childContext.matchedEdges.put(inEdge, true);
                if (!processContext(pattern, executionPlan, childContext, aliasClasses, aliasFilters, iCommandContext, request)) {
                  return false;
                }
              }
            }
          }
        }
      }
    }
    return true;
  }

  private boolean matchesClass(OIdentifiable identifiable, OClass oClass) {
    if (identifiable == null) {
      return false;
    }
    ORecord record = identifiable.getRecord();
    if (record == null) {
      return false;
    }
    if (record instanceof ODocument) {
      OClass schemaClass = ((ODocument) record).getSchemaClass();
      if (schemaClass == null) {
        return false;
      }
      return schemaClass.isSubClassOf(oClass);
    }
    return false;
  }

  private boolean contains(Object rightValues, OIdentifiable oIdentifiable) {
    if (oIdentifiable == null) {
      return true;
    }
    if (rightValues == null) {
      return false;
    }
    if (rightValues instanceof OIdentifiable) {
      return ((OIdentifiable) rightValues).getIdentity().equals(oIdentifiable.getIdentity());
    }
    Iterator iterator = null;
    if (rightValues instanceof Iterable) {
      iterator = ((Iterable) rightValues).iterator();
    }
    if (rightValues instanceof Iterator) {
      iterator = (Iterator) rightValues;
    }
    if (iterator != null) {
      while (iterator.hasNext()) {
        Object next = iterator.next();
        if (next instanceof OIdentifiable) {
          if (((OIdentifiable) next).getIdentity().equals(oIdentifiable.getIdentity())) {
            return true;
          }
        }
      }
    }
    return false;
  }

  private boolean isEmptyResult(Object rightValues) {
    if (rightValues == null) {
      return true;
    }
    if (rightValues instanceof Iterable) {
      Iterator iterator = ((Iterable) rightValues).iterator();
      if (!iterator.hasNext()) {
        return true;
      }
      while (iterator.hasNext()) {
        Object nextElement = iterator.next();
        if (nextElement != null) {
          return false;
        }
      }
      return true;
    }
    return false;
  }

  private boolean expandCartesianProduct(Pattern pattern, MatchContext matchContext, Map<String, String> aliasClasses,
      Map<String, OWhereClause> aliasFilters, OCommandContext iCommandContext, OSQLAsynchQuery<ODocument> request) {
    for (String alias : pattern.aliasToNode.keySet()) {
      if (!matchContext.matched.containsKey(alias)) {
        String target = aliasClasses.get(alias);
        if (target == null) {
          throw new OCommandExecutionException("Cannot execute MATCH statement on alias " + alias + ": class not defined");
        }

        Iterable<OIdentifiable> values = fetchAliasCandidates(alias, aliasFilters, iCommandContext, aliasClasses);
        for (OIdentifiable id : values) {
          MatchContext childContext = matchContext.copy(alias, id);
          if (allNodesCalculated(childContext, pattern)) {
            // false if limit reached
            boolean added = addResult(childContext, request, iCommandContext);
            if (!added) {
              return false;
            }
          } else {
            // false if limit reached
            boolean added = expandCartesianProduct(pattern, childContext, aliasClasses, aliasFilters, iCommandContext, request);
            if (!added) {
              return false;
            }
          }
        }
        break;
      }
    }
    return true;
  }

  private boolean allNodesCalculated(MatchContext matchContext, Pattern pattern) {
    for (String alias : pattern.aliasToNode.keySet()) {
      if (!matchContext.matched.containsKey(alias)) {
        return false;
      }
    }
    return true;
  }

  private boolean addResult(MatchContext matchContext, OSQLAsynchQuery<ODocument> request, OCommandContext ctx) {

    ODocument doc = null;
    if (returnsElements()) {
      for (Map.Entry<String, OIdentifiable> entry : matchContext.matched.entrySet()) {
        if (isExplicitAlias(entry.getKey()) && entry.getValue() != null) {
          ORecord record = entry.getValue().getRecord();
          if (request.getResultListener() != null && record != null) {
            if (!addSingleResult(request, (OBasicCommandContext) ctx, record))
              return false;
          }
        }
      }
    } else if (returnsPathElements()) {
      for (Map.Entry<String, OIdentifiable> entry : matchContext.matched.entrySet()) {
        if (entry.getValue() != null) {
          ORecord record = entry.getValue().getRecord();
          if (request.getResultListener() != null && record != null) {
            if (!addSingleResult(request, (OBasicCommandContext) ctx, record))
              return false;
          }
        }
      }
    } else if (returnsPatterns()) {
      doc = getDatabase().newInstance();
      doc.setTrackingChanges(false);
      for (Map.Entry<String, OIdentifiable> entry : matchContext.matched.entrySet()) {
        if (isExplicitAlias(entry.getKey())) {
          doc.field(entry.getKey(), entry.getValue());
        }
      }
    } else if (returnsPaths()) {
      doc = getDatabase().newInstance();
      doc.setTrackingChanges(false);
      for (Map.Entry<String, OIdentifiable> entry : matchContext.matched.entrySet()) {
        doc.field(entry.getKey(), entry.getValue());
      }
    } else if (returnsJson()) {
      doc = jsonToDoc(matchContext, ctx);
    } else {
      doc = getDatabase().newInstance();
      doc.setTrackingChanges(false);
      int i = 0;

      ODocument mapDoc = new ODocument();
      mapDoc.setTrackingChanges(false);
      mapDoc.fromMap((Map) matchContext.matched);
      ctx.setVariable("$current", mapDoc);
      for (OExpression item : returnItems) {
        OIdentifier returnAliasIdentifier = returnAliases.get(i);
        OIdentifier returnAlias;
        if (returnAliasIdentifier == null) {
          returnAlias = item.getDefaultAlias();
        } else {
          returnAlias = returnAliasIdentifier;
        }
        Object executed = item.execute(mapDoc, ctx);
        // Force Embedded Document
        if (executed instanceof ODocument && !((ODocument) executed).getIdentity().isValid()) {
          doc.setProperty(returnAlias.getStringValue(), executed, OType.EMBEDDED);
        } else {
          doc.setProperty(returnAlias.getStringValue(), executed);
        }
        i++;
      }
      doc.setTrackingChanges(true);
    }

    if (request.getResultListener() != null && doc != null) {
      if (!addSingleResult(request, (OBasicCommandContext) ctx, doc))
        return false;
    }

    return true;
  }

  /**
   * @param request
   * @param ctx
   * @param record
   *
   * @return false if limit was reached
   */
  private boolean addSingleResult(OSQLAsynchQuery<ODocument> request, OBasicCommandContext ctx, ORecord record) {
    if (((OBasicCommandContext) context).addToUniqueResult(record)) {
      request.getResultListener().result(record);
      long currentCount = ctx.getResultsProcessed().incrementAndGet();
      long limitValue = limitFromProtocol;
      if (limit != null) {
        limitValue = limit.num.getValue().longValue();
      }
      if (limitValue > -1 && limitValue <= currentCount) {
        return false;
      }
    }
    return true;
  }

  public boolean returnsPathElements() {
    for (OExpression item : returnItems) {
      if (item.toString().equalsIgnoreCase("$pathElements")) {
        return true;
      }
    }
    return false;
  }

  public boolean returnsElements() {
    for (OExpression item : returnItems) {
      if (item.toString().equalsIgnoreCase("$elements")) {
        return true;
      }
    }
    return false;
  }

  public boolean returnsPatterns() {
    for (OExpression item : returnItems) {
      if (item.toString().equalsIgnoreCase("$patterns")) {
        return true;
      }
      if (item.toString().equalsIgnoreCase("$matches")) {
        return true;
      }
    }
    return false;
  }

  public boolean returnsPaths() {
    for (OExpression item : returnItems) {
      if (item.toString().equalsIgnoreCase("$paths")) {
        return true;
      }
    }
    return false;
  }

  private boolean returnsJson() {
    if (returnItems.size() == 1 && (returnItems.get(0).value instanceof OJson) && returnAliases.get(0) == null) {
      return true;
    }
    return false;
  }

  private ODocument jsonToDoc(MatchContext matchContext, OCommandContext ctx) {
    if (returnItems.size() == 1 && (returnItems.get(0).value instanceof OJson) && returnAliases.get(0) == null) {
      ODocument result = new ODocument();
      result.setTrackingChanges(false);
      result.fromMap(((OJson) returnItems.get(0).value).toMap(matchContext.toDoc(), ctx));
      return result;
    }
    throw new IllegalStateException("Match RETURN statement is not a plain JSON");
  }

  private boolean isExplicitAlias(String key) {
    if (key.startsWith(DEFAULT_ALIAS_PREFIX)) {
      return false;
    }
    return true;
  }

  private Iterator<OIdentifiable> query(String className, OWhereClause oWhereClause, OCommandContext ctx) {
    final ODatabaseDocument database = getDatabase();
    OClass schemaClass = database.getMetadata().getSchema().getClass(className);
    database.checkSecurity(ORule.ResourceGeneric.CLASS, ORole.PERMISSION_READ, schemaClass.getName().toLowerCase(Locale.ENGLISH));

    Iterable<ORecord> baseIterable = fetchFromIndex(schemaClass, oWhereClause);

    // OSelectStatement stm = buildSelectStatement(className, oWhereClause);
    // return stm.execute(ctx);

    String text;
    if (oWhereClause == null) {
      text = "(select from " + className + ")";
    } else {
      StringBuilder builder = new StringBuilder();
      oWhereClause.toString(ctx.getInputParameters(), builder);

      //TODO make it more OO!
//      synchronized (oWhereClause) { //this instance is shared...
//        replaceIdentifier(oWhereClause, "$currentMatch", "@this"); //
//        newWhere = oWhereClause.replaceIdentifier("$currentMatch", "@this");
      text = "(select from " + className + " where " + builder.toString().replaceAll("\\$currentMatch", "@this") + ")";
//        replaceIdentifier(oWhereClause, "@this", "$currentMatch");
//      }
    }
    OSQLTarget target = new OSQLTarget(text, ctx);
    Iterable targetResult = (Iterable) target.getTargetRecords();
    if (targetResult == null) {
      return null;
    }

    if (targetResult instanceof OCommandExecutorSQLSelect) {
      ((OCommandExecutorSQLSelect) targetResult).getContext().setRecordingMetrics(ctx.isRecordingMetrics());
    } else if (targetResult instanceof OCommandExecutorSQLResultsetDelegate) {
      OCommandExecutor delegate = ((OCommandExecutorSQLResultsetDelegate) targetResult).getDelegate();
      if (delegate instanceof OCommandExecutorSQLSelect) {
        delegate.getContext().setRecordingMetrics(ctx.isRecordingMetrics());
      }
    }
    return targetResult.iterator();
  }

//  private void replaceIdentifier(SimpleNode node, String from, String to) {
//    if (node instanceof OIdentifier) {
//      if (from.equals(node.getValue())) {
//        ((OIdentifier) node).setStringValue(to);
//      }
//    } else {
//      for (int i = 0; i < node.jjtGetNumChildren(); i++) {
//        replaceIdentifier((SimpleNode) node.jjtGetChild(i), from, to);
//      }
//    }
//
//  }

  private OSelectStatement buildSelectStatement(String className, OWhereClause oWhereClause) {
    OSelectStatement stm = new OSelectStatement(-1);
    stm.whereClause = oWhereClause;
    stm.target = new OFromClause(-1);
    stm.target.item = new OFromItem(-1);
    stm.target.item.identifier = new OIdentifier(className);
    return stm;
  }

  private Iterable<ORecord> fetchFromIndex(OClass schemaClass, OWhereClause oWhereClause) {
    return null;// TODO
  }

  private String getNextAlias(Map<String, Long> estimatedRootEntries, MatchContext matchContext) {
    Map.Entry<String, Long> lowerValue = null;
    for (Map.Entry<String, Long> entry : estimatedRootEntries.entrySet()) {
      if (matchContext.matched.containsKey(entry.getKey())) {
        continue;
      }
      if (lowerValue == null) {
        lowerValue = entry;
      } else if (lowerValue.getValue() > entry.getValue()) {
        lowerValue = entry;
      }
    }

    if (lowerValue == null) {
      throw new OCommandExecutionException("Cannot calculate this pattern (maybe a circular dependency on $matched conditions)");
    }
    return lowerValue.getKey();
  }

  private Map<String, Long> estimateRootEntries(Map<String, String> aliasClasses, Map<String, OWhereClause> aliasFilters,
      OCommandContext ctx) {
    Set<String> allAliases = new LinkedHashSet<String>();
    allAliases.addAll(aliasClasses.keySet());
    allAliases.addAll(aliasFilters.keySet());

    OSchema schema = getDatabase().getMetadata().getSchema();

    Map<String, Long> result = new LinkedHashMap<String, Long>();
    for (String alias : allAliases) {
      String className = aliasClasses.get(alias);
      if (className == null) {
        continue;
      }

      if (!schema.existsClass(className)) {
        throw new OCommandExecutionException("class not defined: " + className);
      }
      OClass oClass = schema.getClass(className);
      long upperBound;
      OWhereClause filter = aliasFilters.get(alias);
      if (filter != null) {
        List<String> aliasesOnPattern = filter.baseExpression.getMatchPatternInvolvedAliases();
        if (aliasesOnPattern != null && aliasesOnPattern.size() > 0) {
          //skip root nodes that have a condition on $matched, because they have to be calculated as downstream
          continue;
        }
        upperBound = filter.estimate(oClass, this.threshold, ctx);
      } else {
        upperBound = oClass.count();
      }
      result.put(alias, upperBound);
    }
    return result;
  }

  private void addAliases(OMatchExpression expr, Map<String, OWhereClause> aliasFilters, Map<String, String> aliasClasses,
      OCommandContext context) {
    addAliases(expr.origin, aliasFilters, aliasClasses, context);
    for (OMatchPathItem item : expr.items) {
      if (item.filter != null) {
        addAliases(item.filter, aliasFilters, aliasClasses, context);
      }
    }
  }

  private void addAliases(OMatchFilter matchFilter, Map<String, OWhereClause> aliasFilters, Map<String, String> aliasClasses,
      OCommandContext context) {
    String alias = matchFilter.getAlias();
    OWhereClause filter = matchFilter.getFilter();
    if (alias != null) {
      if (filter != null && filter.baseExpression != null) {
        OWhereClause previousFilter = aliasFilters.get(alias);
        if (previousFilter == null) {
          previousFilter = new OWhereClause(-1);
          previousFilter.baseExpression = new OAndBlock(-1);
          aliasFilters.put(alias, previousFilter);
        }
        OAndBlock filterBlock = (OAndBlock) previousFilter.baseExpression;
        if (filter != null && filter.baseExpression != null) {
          filterBlock.subBlocks.add(filter.baseExpression);
        }
      }

      String clazz = matchFilter.getClassName(context);
      if (clazz != null) {
        String previousClass = aliasClasses.get(alias);
        if (previousClass == null) {
          aliasClasses.put(alias, clazz);
        } else {
          String lower = getLowerSubclass(clazz, previousClass);
          if (lower == null) {
            throw new OCommandExecutionException(
                "classes defined for alias " + alias + " (" + clazz + ", " + previousClass + ") are not in the same hierarchy");
          }
          aliasClasses.put(alias, lower);
        }
      }
    }
  }

  private String getLowerSubclass(String className1, String className2) {
    OSchema schema = getDatabase().getMetadata().getSchema();
    OClass class1 = schema.getClass(className1);
    OClass class2 = schema.getClass(className2);
    if (class1 == null) {
      throw new OCommandExecutionException("Class " + className1 + " not found in the schema");
    }
    if (class2 == null) {
      throw new OCommandExecutionException("Class " + className2 + " not found in the schema");
    }
    if (class1.isSubClassOf(class2)) {
      return class1.getName();
    }
    if (class2.isSubClassOf(class1)) {
      return class2.getName();
    }
    return null;
  }

  @Override
  public <RET extends OCommandExecutor> RET setProgressListener(OProgressListener progressListener) {
    this.progressListener = progressListener;
    return (RET) this;
  }

  @Override
  public <RET extends OCommandExecutor> RET setLimit(int iLimit) {
    limitFromProtocol = iLimit;
    return (RET) this;
  }

  @Override
  public String getFetchPlan() {
    return null;
  }

  @Override
  public Map<Object, Object> getParameters() {
    return null;
  }

  @Override
  public OCommandContext getContext() {
    return context;
  }

  @Override
  public void setContext(OCommandContext context) {
    this.context = context;
  }

  @Override
  public boolean isIdempotent() {
    return true;
  }

  @Override
  public Set<String> getInvolvedClusters() {
    return Collections.EMPTY_SET;
  }

  @Override
  public int getSecurityOperationType() {
    return ORole.PERMISSION_READ;
  }

  @Override
  public boolean involveSchema() {
    return false;
  }

  @Override
  public String getSyntax() {
    return "MATCH <match-statement> [, <match-statement] RETURN <alias>[, <alias>]";
  }

  @Override
  public boolean isLocalExecution() {
    return true;
  }

  @Override
  public boolean isCacheable() {
    return false;
  }

  @Override
  public long getDistributedTimeout() {
    return -1;
  }

  @Override
  public Object mergeResults(Map<String, Object> results) throws Exception {
    return results;
  }

  public void toString(Map<Object, Object> params, StringBuilder builder) {
    builder.append(KEYWORD_MATCH);
    builder.append(" ");
    boolean first = true;
    for (OMatchExpression expr : this.matchExpressions) {
      if (!first) {
        builder.append(", ");
      }
      expr.toString(params, builder);
      first = false;
    }
    builder.append(" RETURN ");
    if (returnDistinct) {
      builder.append("DISTINCT ");
    }
    first = true;
    int i = 0;
    for (OExpression expr : this.returnItems) {
      if (!first) {
        builder.append(", ");
      }
      expr.toString(params, builder);
      if (returnNestedProjections != null && i < returnNestedProjections.size() && returnNestedProjections.get(i) != null) {
        returnNestedProjections.get(i).toString(params, builder);
      }
      if (returnAliases != null && i < returnAliases.size() && returnAliases.get(i) != null) {
        builder.append(" AS ");
        returnAliases.get(i).toString(params, builder);
      }
      i++;
      first = false;
    }
    if (groupBy != null) {
      builder.append(" ");
      groupBy.toString(params, builder);
    }
    if (orderBy != null) {
      builder.append(" ");
      orderBy.toString(params, builder);
    }
    if (unwind != null) {
      builder.append(" ");
      unwind.toString(params, builder);
    }
    if (skip != null) {
      builder.append(" ");
      skip.toString(params, builder);
    }
    if (limit != null) {
      builder.append(" ");
      limit.toString(params, builder);
    }
  }

  @Override
  public Iterator<OIdentifiable> iterator(Map<Object, Object> iArgs) {
    if (context == null) {
      context = new OBasicCommandContext();
    }
    Object result = execute(iArgs);
    return ((Iterable) result).iterator();
  }

  @Override
  public OMatchStatement copy() {
    OMatchStatement result = new OMatchStatement(-1);
    result.matchExpressions = matchExpressions == null ?
        null :
        matchExpressions.stream().map(x -> x == null ? null : x.copy()).collect(Collectors.toList());
    result.notMatchExpressions = notMatchExpressions == null ?
        null :
        notMatchExpressions.stream().map(x -> x == null ? null : x.copy()).collect(Collectors.toList());
    result.returnItems =
        returnItems == null ? null : returnItems.stream().map(x -> x == null ? null : x.copy()).collect(Collectors.toList());
    result.returnAliases =
        returnAliases == null ? null : returnAliases.stream().map(x -> x == null ? null : x.copy()).collect(Collectors.toList());
    result.returnNestedProjections = returnNestedProjections == null ?
        null :
        returnNestedProjections.stream().map(x -> x == null ? null : x.copy()).collect(Collectors.toList());
    result.groupBy = groupBy == null ? null : groupBy.copy();
    result.orderBy = orderBy == null ? null : orderBy.copy();
    result.unwind = unwind == null ? null : unwind.copy();
    result.skip = skip == null ? null : skip.copy();
    result.limit = limit == null ? null : limit.copy();
    result.returnDistinct = this.returnDistinct;
    result.buildPatterns();
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o)
      return true;
    if (o == null || getClass() != o.getClass())
      return false;

    OMatchStatement that = (OMatchStatement) o;

    if (matchExpressions != null ? !matchExpressions.equals(that.matchExpressions) : that.matchExpressions != null)
      return false;
    if (notMatchExpressions != null ? !notMatchExpressions.equals(that.notMatchExpressions) : that.notMatchExpressions != null)
      return false;
    if (returnItems != null ? !returnItems.equals(that.returnItems) : that.returnItems != null)
      return false;
    if (returnAliases != null ? !returnAliases.equals(that.returnAliases) : that.returnAliases != null)
      return false;
    if (returnNestedProjections != null ?
        !returnNestedProjections.equals(that.returnNestedProjections) :
        that.returnNestedProjections != null)
      return false;
    if (groupBy != null ? !groupBy.equals(that.groupBy) : that.groupBy != null)
      return false;
    if (orderBy != null ? !orderBy.equals(that.orderBy) : that.orderBy != null)
      return false;
    if (unwind != null ? !unwind.equals(that.unwind) : that.unwind != null)
      return false;
    if (skip != null ? !skip.equals(that.skip) : that.skip != null)
      return false;
    if (limit != null ? !limit.equals(that.limit) : that.limit != null)
      return false;

    if (returnDistinct != that.returnDistinct)
      return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = matchExpressions != null ? matchExpressions.hashCode() : 0;
    result = 31 * result + (notMatchExpressions != null ? notMatchExpressions.hashCode() : 0);
    result = 31 * result + (returnItems != null ? returnItems.hashCode() : 0);
    result = 31 * result + (returnAliases != null ? returnAliases.hashCode() : 0);
    result = 31 * result + (returnNestedProjections != null ? returnNestedProjections.hashCode() : 0);
    result = 31 * result + (groupBy != null ? groupBy.hashCode() : 0);
    result = 31 * result + (orderBy != null ? orderBy.hashCode() : 0);
    result = 31 * result + (unwind != null ? unwind.hashCode() : 0);
    result = 31 * result + (skip != null ? skip.hashCode() : 0);
    result = 31 * result + (limit != null ? limit.hashCode() : 0);
    return result;
  }

  public OLimit getLimit() {
    return limit;
  }

  public void setLimit(OLimit limit) {
    this.limit = limit;
  }

  public List<OIdentifier> getReturnAliases() {
    return returnAliases;
  }

  public void setReturnAliases(List<OIdentifier> returnAliases) {
    this.returnAliases = returnAliases;
  }

  public List<OExpression> getReturnItems() {
    return returnItems;
  }

  public void setReturnItems(List<OExpression> returnItems) {
    this.returnItems = returnItems;
  }

  public List<OMatchExpression> getMatchExpressions() {
    return matchExpressions;
  }

  public void setMatchExpressions(List<OMatchExpression> matchExpressions) {
    this.matchExpressions = matchExpressions;
  }

  public List<OMatchExpression> getNotMatchExpressions() {
    return notMatchExpressions;
  }

  public void setNotMatchExpressions(List<OMatchExpression> notMatchExpressions) {
    this.notMatchExpressions = notMatchExpressions;
  }

  public boolean isReturnDistinct() {
    return returnDistinct;
  }

  public void setReturnDistinct(boolean returnDistinct) {
    this.returnDistinct = returnDistinct;
  }

  public OOrderBy getOrderBy() {
    return orderBy;
  }

  public void setOrderBy(OOrderBy orderBy) {
    this.orderBy = orderBy;
  }

  public OGroupBy getGroupBy() {
    return groupBy;
  }

  public void setGroupBy(OGroupBy groupBy) {
    this.groupBy = groupBy;
  }

  public OUnwind getUnwind() {
    return unwind;
  }

  public void setUnwind(OUnwind unwind) {
    this.unwind = unwind;
  }

  public OSkip getSkip() {
    return skip;
  }

  public void setSkip(OSkip skip) {
    this.skip = skip;
  }

  @Override
  public boolean refersToParent() {
    //TODO check this!
    return false;
  }
}
/* JavaCC - OriginalChecksum=6ff0afbe9d31f08b72159fcf24070c9f (do not edit this line) */