/*
 *
 *  *  Copyright 2016 Orient Technologies LTD (info(at)orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://www.orientdb.com
 *
 */
package com.orientechnologies.orient.server.distributed;

import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.util.OCallable;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.exception.OConcurrentCreateException;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.server.distributed.ODistributedServerLog.DIRECTION;
import com.orientechnologies.orient.server.distributed.task.OAbstractReplicatedTask;
import com.orientechnologies.orient.server.distributed.task.ODistributedOperationException;
import com.orientechnologies.orient.server.distributed.task.ODistributedRecordLockedException;
import com.orientechnologies.orient.server.distributed.task.ORemoteTask;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Asynchronous response manager. All the public methods have to pay attention on synchronizing access by using
 * synchronousResponsesLock lock.
 * <p>
 * TODO: - set flags during collecting of response for fast computation on checking for the status
 *
 * @author Luca Garulli (l.garulli--at--orientdb.com)
 */
public class ODistributedResponseManagerImpl implements ODistributedResponseManager {
  public static final  int    ADDITIONAL_TIMEOUT_CLUSTER_SHAPE = 10000;
  private static final String NO_RESPONSE                      = "waiting-for-response";
  private final ODistributedServerManager dManager;
  private final ODistributedRequest       request;
  private final long                      sentOn;
  private final Set<String>               nodesConcurInQuorum;
  private final HashMap<String, Object> responses = new HashMap<String, Object>();
  private final boolean groupResponsesByResult;
  private final List<List<ODistributedResponse>> responseGroups = new ArrayList<List<ODistributedResponse>>();
  private final OCallable<Void, ODistributedResponseManager> endCallback;
  private       int                                          totalExpectedResponses;
  private final long                                         synchTimeout;
  private final long                                         totalTimeout;
  private final Lock           synchronousResponsesLock    = new ReentrantLock();
  private final CountDownLatch synchronousResponsesArrived = new CountDownLatch(1);
  private final int                  quorum;
  private final boolean              waitForLocalNode;
  private       ODistributedResponse localResponse;
  private volatile int receivedResponses = 0;
  private volatile boolean receivedCurrentNode;
  private       ODistributedResponse quorumResponse  = null;
  private final Set<String>          followupToNodes = new HashSet<String>();
  private       AtomicBoolean        canceled        = new AtomicBoolean(false);

  public ODistributedResponseManagerImpl(final ODistributedServerManager iManager, final ODistributedRequest iRequest,
      final Collection<String> expectedResponses, final Set<String> iNodesConcurInQuorum, final int iTotalExpectedResponses,
      final int iQuorum, final boolean iWaitForLocalNode, final long iSynchTimeout, final long iTotalTimeout,
      final boolean iGroupResponsesByResult, final OCallable<Void, ODistributedResponseManager> endCallback) {
    this.dManager = iManager;
    this.request = iRequest;
    this.sentOn = System.nanoTime();
    this.totalExpectedResponses = iTotalExpectedResponses;
    this.quorum = iQuorum;
    this.waitForLocalNode = iWaitForLocalNode;
    this.synchTimeout = iSynchTimeout;
    this.totalTimeout = iTotalTimeout;
    this.groupResponsesByResult = iGroupResponsesByResult;
    this.nodesConcurInQuorum = iNodesConcurInQuorum;
    this.endCallback = endCallback;

    for (String node : expectedResponses)
      responses.put(node, NO_RESPONSE);

    if (groupResponsesByResult)
      responseGroups.add(new ArrayList<ODistributedResponse>());
  }

  /**
   * Not synchronized, it's called when a message arrives
   *
   * @param response Received response to collect
   *
   * @return True if all the nodes responded, otherwise false
   */
  public boolean collectResponse(final ODistributedResponse response) {
    final String executorNode = response.getExecutorNodeName();
    final String senderNode = response.getSenderNodeName();
    response.setDistributedResponseManager(this);

    synchronousResponsesLock.lock();
    try {
      if (!executorNode.equals(dManager.getLocalNodeName()) && !responses.containsKey(executorNode)) {
        ODistributedServerLog.warn(this, senderNode, executorNode, DIRECTION.IN,
            "Received response for request (%s) from unexpected node. Expected are: %s", request, getExpectedNodes());

        Orient.instance().getProfiler()
            .updateCounter("distributed.node.unexpectedNodeResponse", "Number of responses from unexpected nodes", +1);

        return false;
      }

      dManager.getMessageService().updateLatency(executorNode, sentOn);

      responses.put(executorNode, response);
      receivedResponses++;

      if (waitForLocalNode && executorNode.equals(senderNode))
        receivedCurrentNode = true;

      if (ODistributedServerLog.isDebugEnabled())
        ODistributedServerLog.debug(this, senderNode, executorNode, DIRECTION.IN,
            "Received response '%s' for request (%s) (receivedCurrentNode=%s receivedResponses=%d totalExpectedResponses=%d quorum=%d)",
            response, request, receivedCurrentNode, receivedResponses, totalExpectedResponses, quorum);

      if (groupResponsesByResult) {
        // PUT THE RESPONSE IN THE RIGHT RESPONSE GROUP
        // TODO: AVOID TO KEEP ALL THE RESULT FOR THE SAME RESP GROUP, BUT RATHER THE FIRST ONE + COUNTER
        final Object responsePayload = response.getPayload();

        boolean foundBucket = false;
        for (int i = 0; i < responseGroups.size(); ++i) {
          final List<ODistributedResponse> responseGroup = responseGroups.get(i);

          if (responseGroup.isEmpty())
            // ABSENT
            foundBucket = true;
          else {
            final Object rgPayload = responseGroup.get(0).getPayload();

            if (rgPayload == null && responsePayload == null)
              // BOTH NULL
              foundBucket = true;
            else if (rgPayload != null) {
              if (rgPayload instanceof ODocument && responsePayload instanceof ODocument && !((ODocument) rgPayload).getIdentity()
                  .isValid() && ((ODocument) rgPayload).hasSameContentOf((ODocument) responsePayload))
                // SAME RESULT
                foundBucket = true;
              else if (rgPayload.equals(responsePayload))
                // SAME RESULT
                foundBucket = true;
              else if (rgPayload instanceof Collection && responsePayload instanceof Collection) {
                if (OMultiValue.equals((Collection) rgPayload, (Collection) responsePayload))
                  // COLLECTIONS WITH THE SAME VALUES
                  foundBucket = true;
              }
            }
          }

          if (foundBucket) {
            responseGroup.add(response);
            break;
          }
        }

        if (!foundBucket) {
          // CREATE A NEW BUCKET
          final ArrayList<ODistributedResponse> newBucket = new ArrayList<ODistributedResponse>();
          responseGroups.add(newBucket);
          newBucket.add(response);
        }
      }

      // FOR EVERY RESPONSE COLLECTED, COMPUTE THE FINAL QUORUM RESPONSE IF POSSIBLE
      computeQuorumResponse(false);

      return checkForCompletion();

    } finally {
      synchronousResponsesLock.unlock();
    }
  }

  private boolean checkForCompletion() {
    final boolean completed = getExpectedResponses() == receivedResponses;

    if (completed)
      // FINALIZE THE REQUEST
      end();

    if (completed || isMinimumQuorumReached(false)) {
      // NOTIFY TO THE WAITER THE RESPONSE IS COMPLETE NOW
      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
          "Sending signal of synchronous request completed (reqId=%s thread=%d)", request.getId(), Thread.currentThread().getId());

      synchronousResponsesArrived.countDown();
    }
    return completed;
  }

  public ODistributedRequestId getMessageId() {
    return request.getId();
  }

  public long getSentOn() {
    return sentOn;
  }

  /**
   * @param localNodeName
   * @param localResult
   *
   * @return
   */
  public boolean setLocalResult(final String localNodeName, final Object localResult) {
    localResponse = new ODistributedResponse(this, request.getId(), localNodeName, localNodeName, localResult);
    receivedCurrentNode = true;
    return collectResponse(localResponse);
  }

  public void removeServerBecauseUnreachable(final String node) {
    synchronousResponsesLock.lock();
    try {

      if (responses.remove(node) != null) {
        totalExpectedResponses--;
        nodesConcurInQuorum.remove(node);

        checkForCompletion();
      }

    } finally {
      synchronousResponsesLock.unlock();
    }
  }

  public int getQuorum() {
    return quorum;
  }

  /**
   * Waits until the minimum responses are collected or timeout occurs. If "waitForLocalNode" wait also for local node.
   *
   * @return True if the received responses are major or equals then the expected synchronous responses, otherwise false
   *
   * @throws InterruptedException
   */
  public boolean waitForSynchronousResponses() throws InterruptedException {
    final long beginTime = System.currentTimeMillis();
    try {

      boolean reachedTimeout = false;
      long currentTimeout = synchTimeout;
      while (currentTimeout > 0) {

        if (currentTimeout > 10000)
          // CUT THE TIMEOUT IN BLOCKS OF 10S EACH TO ALLOW CHECKING FOR ANY SERVER IF UNREACHABLE
          currentTimeout = 10000;

        if (ODistributedServerLog.isDebugEnabled())
          ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
              "Waiting max %dms for collecting all synchronous responses... (timeout=%d reqId=%s thread=%d)", currentTimeout,
              synchTimeout, request.getId(), Thread.currentThread().getId());

        // WAIT FOR THE RESPONSES
        if (synchronousResponsesArrived.await(currentTimeout, TimeUnit.MILLISECONDS)) {
          if (canceled.get())
            throw new ODistributedOperationException("Request has been canceled");

          // COMPLETED
          if (ODistributedServerLog.isDebugEnabled())
            ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
                "All synchronous responses collected in %dms (reqId=%s thread=%d)", (System.currentTimeMillis() - beginTime),
                request.getId(), Thread.currentThread().getId());

          return true;
        }

        if (ODistributedServerLog.isDebugEnabled())
          ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
              "All synchronous responses not collected in %dms, waiting again... (reqId=%s thread=%d)",
              (System.currentTimeMillis() - beginTime), request.getId(), Thread.currentThread().getId());

        if (Thread.currentThread().isInterrupted()) {
          // INTERRUPTED
          ODistributedServerLog
              .warn(this, dManager.getLocalNodeName(), null, DIRECTION.NONE, "Thread has been interrupted wait for request (%s)",
                  request);
          Thread.currentThread().interrupt();
          break;
        }

        synchronousResponsesLock.lock();
        try {

          final long now = System.currentTimeMillis();
          final long elapsed = now - beginTime;
          if (elapsed > synchTimeout)
            reachedTimeout = true;
          currentTimeout = synchTimeout - elapsed;

          // CHECK IF ANY NODE ARE UNREACHABLE IN THE MEANWHILE
          int synchronizingNodes = 0;
          int missingActiveNodes = 0;

          Map<String, ODistributedServerManager.DB_STATUS> missingResponseNodeStatuses = new HashMap<String, ODistributedServerManager.DB_STATUS>(
              responses.size());

          int missingResponses = 0;

          for (Iterator<Map.Entry<String, Object>> iter = responses.entrySet().iterator(); iter.hasNext(); ) {
            final Map.Entry<String, Object> curr = iter.next();

            if (curr.getValue() == NO_RESPONSE) {
              missingResponses++;

              // ANALYZE THE NODE WITHOUT A RESPONSE
              final ODistributedServerManager.DB_STATUS dbStatus = dManager.getDatabaseStatus(curr.getKey(), getDatabaseName());

              missingResponseNodeStatuses.put(curr.getKey(), dbStatus);

              switch (dbStatus) {
              case BACKUP:
              case SYNCHRONIZING:
                synchronizingNodes++;
                missingActiveNodes++;
                break;
              case ONLINE:
                missingActiveNodes++;
                break;
              }
            }
          }

          if (missingResponses == 0) {
            // ALL RESPONSE COLLECTED, BUT NO QUORUM REACHED
            ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
                "All responses collected %s, but no quorum reached (reqId=%s)", responses, request.getId());
            break;
          }

          request.getTask().checkIsValid(dManager);

          if (missingActiveNodes == 0) {
            // NO MORE ACTIVE NODES TO WAIT
            ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
                "No more active nodes to wait for request (%s): anticipate timeout (saved %d ms). Missing servers: %s", request,
                currentTimeout, missingResponseNodeStatuses);
            break;
          }

          final long lastClusterChange = dManager.getLastClusterChangeOn();
          if (lastClusterChange > 0 && now - lastClusterChange < (synchTimeout + ADDITIONAL_TIMEOUT_CLUSTER_SHAPE)) {
            // CHANGED CLUSTER SHAPE DURING WAIT: ENLARGE TIMEOUT
            currentTimeout = synchTimeout;
            ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
                "Cluster shape changed during request (%s): enlarge timeout +%dms, wait again for %dms", request, synchTimeout,
                currentTimeout);
            continue;
          } else if (synchronizingNodes > 0) {
            // SOME NODE IS SYNCHRONIZING: WAIT FOR THEM
            // currentTimeout = synchTimeout;
            // ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
            // "%d nodes are in synchronization mode during request (%s): enlarge timeout +%dms, wait again for %dms",
            // synchronizingNodes, request, synchTimeout, currentTimeout);
          }

        } finally {
          synchronousResponsesLock.unlock();
        }
      }

      if (canceled.get())
        throw new ODistributedOperationException("Request has been canceled");

      return isMinimumQuorumReached(reachedTimeout);

    } finally {
      Orient.instance().getProfiler()
          .stopChrono("distributed.synchResponses", "Time to collect all the synchronous responses from distributed nodes",
              beginTime);
    }
  }

  public boolean isWaitForLocalNode() {
    return waitForLocalNode;
  }

  public boolean isReceivedCurrentNode() {
    return receivedCurrentNode;
  }

  public ODistributedResponse getFinalResponse() {
    synchronousResponsesLock.lock();
    try {

      final RuntimeException failure = manageConflicts();
      if (failure != null)
        return new ODistributedResponse(this, request.getId(), dManager.getLocalNodeName(), dManager.getLocalNodeName(), failure);

      if (receivedResponses == 0) {
        if (quorum > 0 && !request.getTask().isIdempotent())
          throw new ODistributedException(
              "No response received from any of nodes " + getExpectedNodes() + " for request " + request + " after " + (
                  (System.nanoTime() - sentOn) / 1000000) + "ms");

        // NO QUORUM, RETURN NULL
        return null;
      }

      // MANAGE THE RESULT BASED ON RESULT STRATEGY
      switch (request.getTask().getResultStrategy()) {
      case UNION: {
        // COLLECT ALL THE RESPONSE IN A MAP OF <NODE, RESULT>
        final Map<String, Object> payloads = new HashMap<String, Object>();
        for (Map.Entry<String, Object> entry : responses.entrySet())
          if (entry.getValue() != NO_RESPONSE)
            payloads.put(entry.getKey(), ((ODistributedResponse) entry.getValue()).getPayload());

        if (payloads.isEmpty())
          return null;

        final ODistributedResponse response = getReceivedResponses().iterator().next();
        response.setExecutorNodeName(responses.keySet().toString());
        response.setPayload(payloads);
        return response;
      }

      default:
        // DEFAULT: RETURN BEST ANSWER
        return quorumResponse;
      }

    } finally {
      synchronousResponsesLock.unlock();
    }
  }

  public String getDatabaseName() {
    return request.getDatabaseName();
  }

  public long getSynchTimeout() {
    return synchTimeout;
  }

  public void timeout() {
    synchronousResponsesLock.lock();
    try {

      end();

      // NOTIFY TO THE WAITER THE RESPONSE IS COMPLETE NOW
      synchronousResponsesArrived.countDown();

    } finally {
      synchronousResponsesLock.unlock();
    }
  }

  public boolean isCanceled() {
    return canceled.get();
  }

  public void cancel() {
    this.canceled.set(true);

    // UNLOCK WAITER
    synchronousResponsesArrived.countDown();
  }

  /**
   * Returns the list of node names that didn't provide a response.
   */
  public List<String> getMissingNodes() {
    synchronousResponsesLock.lock();
    try {

      final List<String> missingNodes = new ArrayList<String>();
      for (Map.Entry<String, Object> entry : responses.entrySet())
        if (entry.getValue() == NO_RESPONSE)
          missingNodes.add(entry.getKey());
      return missingNodes;

    } finally {
      synchronousResponsesLock.unlock();
    }
  }

  public Set<String> getExpectedNodes() {
    synchronousResponsesLock.lock();
    try {
      return new HashSet<String>(responses.keySet());
    } finally {
      synchronousResponsesLock.unlock();
    }
  }

  public Set<String> getServersWithoutFollowup() {
    if (!groupResponsesByResult)
      return Collections.EMPTY_SET;

    synchronousResponsesLock.lock();
    try {
      final HashSet<String> servers = new HashSet<String>(responses.keySet());
      servers.removeAll(followupToNodes);
      return servers;
    } finally {
      synchronousResponsesLock.unlock();
    }
  }

  /**
   * Returns the list of node names that provided a response.
   */
  public List<String> getRespondingNodes() {
    final List<String> respondedNodes = new ArrayList<String>();
    synchronousResponsesLock.lock();
    try {
      for (Map.Entry<String, Object> entry : responses.entrySet())
        if (entry.getValue() != NO_RESPONSE)
          respondedNodes.add(entry.getKey());
    } finally {
      synchronousResponsesLock.unlock();
    }
    return respondedNodes;
  }

  public ODistributedRequest getRequest() {
    return request;
  }

  /**
   * Returns all the responses in conflict.
   */
  protected List<ODistributedResponse> getConflictResponses() {
    final List<ODistributedResponse> servers = new ArrayList<ODistributedResponse>();
    int bestGroupSoFar = getBestResponsesGroup();
    for (int i = 0; i < responseGroups.size(); ++i) {
      if (i != bestGroupSoFar) {
        for (ODistributedResponse r : responseGroups.get(i))
          servers.add(r);
      }
    }
    return servers;
  }

  protected int getExpectedResponses() {
    return responses.size();
  }

  public synchronized boolean isFinished() {
    return getExpectedResponses() - receivedResponses == 0;
  }

  protected int getReceivedResponsesCount() {
    return receivedResponses;
  }

  protected long getTotalTimeout() {
    return totalTimeout;
  }

  /**
   * Returns the biggest response group.
   *
   * @return
   */
  protected int getBestResponsesGroup() {
    int maxCoherentResponses = 0;
    int bestGroupSoFar = 0;
    for (int i = 0; i < responseGroups.size(); ++i) {
      final int currentGroupSize = responseGroups.get(i).size();
      if (currentGroupSize > maxCoherentResponses) {
        maxCoherentResponses = currentGroupSize;
        bestGroupSoFar = i;
      }
    }
    return bestGroupSoFar;
  }

  protected boolean isMinimumQuorumReached(final boolean reachedTimeout) {
    if (isWaitForLocalNode() && !isReceivedCurrentNode())
      return false;

    if (quorum == 0)
      return true;

    return computeQuorumResponse(reachedTimeout);
  }

  /**
   * Computes the quorum response if possible by returning true and setting the field quorumResponse with the ODistributedResponse.
   */
  private boolean computeQuorumResponse(boolean reachedTimeout) {
    if (quorumResponse != null)
      // ALREADY COMPUTED
      return true;

    if (groupResponsesByResult) {
      for (List<ODistributedResponse> group : responseGroups) {
        if (group.size() >= quorum) {
          int responsesForQuorum = 0;
          for (ODistributedResponse r : group) {
            if (nodesConcurInQuorum.contains(r.getExecutorNodeName())) {
              final Object payload = r.getPayload();

              if (payload instanceof Throwable) {
                if (payload instanceof ODistributedRecordLockedException)
                  // JUST ONE ODistributedRecordLockedException IS ENOUGH TO FAIL THE OPERATION BECAUSE RESOURCES CANNOT BE LOCKED
                  break;
                if (payload instanceof OConcurrentCreateException)
                  // JUST ONE OConcurrentCreateException IS ENOUGH TO FAIL THE OPERATION BECAUSE RID ARE DIFFERENT
                  break;
              } else if (++responsesForQuorum >= quorum) {
                // QUORUM REACHED
                setQuorumResponse(r);
                return true;
              }
            }
          }
        }
      }

    } else {
      if (receivedResponses >= quorum) {
        int responsesForQuorum = 0;
        for (Map.Entry<String, Object> response : responses.entrySet()) {
          if (response.getValue() != NO_RESPONSE && nodesConcurInQuorum.contains(response.getKey())
              && ++responsesForQuorum >= quorum) {
            // QUORUM REACHED
            ODistributedResponse resp = (ODistributedResponse) response.getValue();
            if (resp != null && !(resp.getPayload() instanceof Throwable))
              setQuorumResponse(resp);
            return true;
          }
        }
      }
    }

    return false;
  }

  /**
   * Returns the received response objects.
   */
  protected List<ODistributedResponse> getReceivedResponses() {
    final List<ODistributedResponse> parsed = new ArrayList<ODistributedResponse>();
    for (Object r : responses.values())
      if (r != NO_RESPONSE)
        parsed.add((ODistributedResponse) r);
    return parsed;
  }

  protected RuntimeException manageConflicts() {
    if (!groupResponsesByResult || request.getTask().getQuorumType() == OCommandDistributedReplicateRequest.QUORUM_TYPE.NONE)
      // NO QUORUM
      return null;

    if (dManager.getNodeStatus() != ODistributedServerManager.NODE_STATUS.ONLINE)
      // CURRENT NODE OFFLINE: JUST RETURN
      return null;

    final int bestResponsesGroupIndex = getBestResponsesGroup();
    final List<ODistributedResponse> bestResponsesGroup = responseGroups.get(bestResponsesGroupIndex);

    final int maxCoherentResponses = bestResponsesGroup.size();
    final int conflicts = getExpectedResponses() - (maxCoherentResponses);

    if (isMinimumQuorumReached(true)) {
      // QUORUM SATISFIED
      if (responseGroups.size() == 1)
        // NO CONFLICT
        return null;

      if (checkNoWinnerCase(bestResponsesGroup))
        // TODO: CALL THE RECORD CONFLICT PIPELINE
        return null;

      if (fixNodesInConflict(bestResponsesGroup, conflicts, false))
        // FIX SUCCEED
        return null;
    }

    // QUORUM HASN'T BEEN REACHED
    if (ODistributedServerLog.isDebugEnabled()) {
      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
          "Detected %d node(s) in timeout or in conflict and quorum (%d) has not been reached, rolling back changes for request (%s)",
          conflicts, quorum, request);

      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE, composeConflictMessage());
    }

    if (!undoRequest()) {
      // SKIP UNDO
      return null;
    }

    // CHECK IF THERE IS AT LEAST ONE ODistributedRecordLockedException or OConcurrentCreateException
    for (Object r : responses.values()) {
      if (r instanceof ODistributedRecordLockedException)
        throw (ODistributedRecordLockedException) r;
      if (r instanceof OConcurrentCreateException)
        throw (OConcurrentCreateException) r;
    }

    final Object goodResponsePayload = bestResponsesGroup.isEmpty() ? null : bestResponsesGroup.get(0).getPayload();
    if (goodResponsePayload instanceof RuntimeException)
      // RESPONSE IS ALREADY AN EXCEPTION: THROW THIS
      return (RuntimeException) goodResponsePayload;
    else if (goodResponsePayload instanceof Throwable)
      return OException
          .wrapException(new ODistributedOperationException(composeConflictMessage()), (Throwable) goodResponsePayload);
    else {
      if (responseGroups.size() <= 2) {
        // CHECK IF THE BAD RESPONSE IS AN EXCEPTION, THEN PROPAGATE IT
        for (int i = 0; i < responseGroups.size(); ++i) {
          if (i == bestResponsesGroupIndex)
            continue;
          final List<ODistributedResponse> badResponses = responseGroups.get(i);
          if (badResponses.get(0).getPayload() instanceof RuntimeException)
            return (RuntimeException) badResponses.get(0).getPayload();
        }
      }

      return new ODistributedOperationException(composeConflictMessage());
    }
  }

  private String composeConflictMessage() {
    final StringBuilder msg = new StringBuilder(256);
    msg.append("Quorum " + getQuorum() + " not reached for request (" + request + "). Elapsed=" + ((System.nanoTime() - getSentOn())
        / 1000000) + "ms.");
    final List<ODistributedResponse> res = getConflictResponses();
    if (res.isEmpty())
      msg.append(" No server in conflict. ");
    else {
      msg.append(" Servers in timeout/conflict are:");
      for (ODistributedResponse r : res) {
        msg.append("\n - ");
        msg.append(r.getExecutorNodeName());
        msg.append(": ");
        msg.append(r.getPayload());
      }
      msg.append("\n");
    }

    if (responses.isEmpty()) {
      msg.append("Received no responses");
    } else {
      msg.append("Received: ");
      for (Map.Entry<String, Object> response : responses.entrySet()) {
        msg.append("\n - ");
        msg.append(response.getKey());
        msg.append(": ");
        msg.append(response.getValue());
      }
    }

    return msg.toString();
  }

  protected boolean undoRequest() {
    final ORemoteTask task = request.getTask();

    if (task.isIdempotent()) {
      // NO UNDO IS NECESSARY
      ODistributedServerLog
          .warn(this, dManager.getLocalNodeName(), null, DIRECTION.NONE, "No undo because the task (%s) is idempotent", task);
      return false;
    }

    // DETERMINE IF ANY CREATE FAILED TO RESTORE RIDS
    for (ODistributedResponse r : getReceivedResponses()) {
      if (r.getPayload() instanceof Throwable)
        // NO NEED TO UNDO AN OPERATION THAT RETURNED EXCEPTION
        // TODO: CONSIDER DIFFERENT TYPE OF EXCEPTION, SOME OF THOSE COULD REQUIRE AN UNDO
        continue;

      if (r == localResponse)
        // SKIP LOCAL SERVER (IT'S MANAGED APART)
        continue;

      final String targetNode = r.getExecutorNodeName();
      if (targetNode.equals(dManager.getLocalNodeName()))
        // AVOID TO UNDO LOCAL NODE BECAUSE THE OPERATION IS MANAGED APART
        continue;

      if (task instanceof OAbstractReplicatedTask) {
        final List<String> servers = OMultiValue.getSingletonList(targetNode);
        final ORemoteTask undoTask = ((OAbstractReplicatedTask) task).getUndoTask(dManager, request.getId(), servers);

        if (undoTask != null) {
          ODistributedServerLog.debug(this, dManager.getLocalNodeName(), targetNode, DIRECTION.OUT,
              "Sending undo message (%s) for request (%s) database '%s' to server %s", undoTask, request, request.getDatabaseName(),
              targetNode);

          final ODistributedResponse result = dManager
              .sendRequest(request.getDatabaseName(), null, servers, undoTask, dManager.getNextMessageIdCounter(),
                  ODistributedRequest.EXECUTION_MODE.RESPONSE, null, null, null);

          ODistributedServerLog.debug(this, dManager.getLocalNodeName(), targetNode, DIRECTION.OUT,
              "Received response from undo message (%s) for request (%s) to server %s: %s", undoTask, request, targetNode, result);
        }
      }
    }
    return true;
  }

  protected boolean fixNodesInConflict(final List<ODistributedResponse> bestResponsesGroup, final int conflicts,
      final boolean cannotFixRecordLockException) {
    // NO FIFTY/FIFTY CASE: FIX THE CONFLICTED NODES BY OVERWRITING THE RECORD WITH THE WINNER'S RESULT
    final ODistributedResponse goodResponse = bestResponsesGroup.get(0);

    if (goodResponse.getPayload() instanceof Throwable)
      return false;

    ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
        "Detected %d conflicts, but the quorum (%d) has been reached. Checking responses and in case fixing remote records (reqId=%s)",
        conflicts, quorum, request.getId());

    for (List<ODistributedResponse> responseGroup : responseGroups) {
      if (responseGroup != bestResponsesGroup) {
        // CONFLICT GROUP: FIX THEM ONE BY ONE
        for (ODistributedResponse r : responseGroup) {
          if (cannotFixRecordLockException && r.getPayload() instanceof ODistributedRecordLockedException) {
            // NO FIX, THE RECORD WAS LOCKED
            if (ODistributedServerLog.isDebugEnabled())
              ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
                  "Node '%s' responded with a record lock exception: cannot fix the operation, undo the entire operation (reqId=%s)",
                  r.getExecutorNodeName(), request);
            return false;
          }

          final ORemoteTask fixTask = ((OAbstractReplicatedTask) request.getTask())
              .getFixTask(request, request.getTask(), r.getPayload(), goodResponse.getPayload(), r.getExecutorNodeName(), dManager);

          if (fixTask == null) {
            // FIX NOT AVAILABLE: UNDO THE ENTIRE OPERATION
            ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
                "No fix operation available: cannot fix the operation, undo the entire operation (reqId=%s)", request);
            return false;
          }

          executeFix(r.getExecutorNodeName(), fixTask, r.getPayload(), goodResponse);
        }
      }
    }
    return true;
  }

  private void executeFix(final String server, final ORemoteTask fixTask, final Object r, final Object goodResponse) {
    if (server.equals(dManager.getLocalNodeName())) {
      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), server, DIRECTION.OUT,
          "Executing the fix locally (%s) for response (%s) on request (%s) to be: %s", fixTask, r, request, goodResponse);

      ODatabaseDocumentInternal oldDb = ODatabaseRecordThreadLocal.instance().getIfDefined();
      final ODatabaseDocumentInternal database = dManager.getMessageService().getDatabase(getDatabaseName()).getDatabaseInstance();
      try {
        database.activateOnCurrentThread();
        dManager
            .executeOnLocalNode(new ODistributedRequestId(dManager.getLocalNodeId(), dManager.getNextMessageIdCounter()), fixTask,
                database);
      } finally {
        database.close();
        ODatabaseRecordThreadLocal.instance().set(oldDb);
      }
    } else {
      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), server, DIRECTION.OUT,
          "Sending fix message (%s) for response (%s) on request (%s) to be: %s", fixTask, r, request, goodResponse);

      dManager.sendRequest(request.getDatabaseName(), null, OMultiValue.getSingletonList(server), fixTask,
          dManager.getNextMessageIdCounter(), ODistributedRequest.EXECUTION_MODE.NO_RESPONSE, null, null, null);
    }
  }

  protected boolean checkNoWinnerCase(final List<ODistributedResponse> bestResponsesGroup) {
    // CHECK IF THERE ARE 2 PARTITIONS EQUAL IN SIZE
    final int maxCoherentResponses = bestResponsesGroup.size();

    if (maxCoherentResponses < quorum)
      return false;

    for (List<ODistributedResponse> responseGroup : responseGroups) {
      if (responseGroup != bestResponsesGroup && responseGroup.size() == maxCoherentResponses) {
        final List<String> a = new ArrayList<String>();
        Object aResponse = null;
        for (ODistributedResponse r : bestResponsesGroup) {
          a.add(r.getExecutorNodeName());
          aResponse = r.getPayload();
        }

        final List<String> b = new ArrayList<String>();
        Object bResponse = null;
        for (ODistributedResponse r : responseGroup) {
          b.add(r.getExecutorNodeName());
          bResponse = r.getPayload();
        }

        final StringBuilder details = new StringBuilder();
        details.append(" A=").append(aResponse);
        details.append(", B=").append(bResponse);

        ODistributedServerLog.error(this, dManager.getLocalNodeName(), null, DIRECTION.NONE,
            "Detected possible split brain network where 2 groups of servers A%s and B%s have different contents. Cannot decide who is the winner even if the quorum (%d) has been reached. Request (%s) responses:%s",
            a, b, quorum, request, details);

        // DON'T FIX RECORDS BECAUSE THERE ISN'T A CLEAR WINNER
        return true;
      }
    }
    return false;
  }

  private void setQuorumResponse(final ODistributedResponse quorumResponse) {
    this.quorumResponse = quorumResponse;
    ODistributedServerLog
        .debug(this, dManager.getLocalNodeName(), null, DIRECTION.NONE, "Reached the quorum (%d) for value '%s' (reqId=%s)", quorum,
            quorumResponse, request);
  }

  private void end() {
    if (endCallback != null)
      // CUSTOM CALLBACK
      endCallback.call(this);
    else {
      // DEFAULT CALLBACK
      if (groupResponsesByResult) {
        final String localNodeName = dManager.getLocalNodeName();

        final Set<String> serversToFollowup = getServersWithoutFollowup();
        serversToFollowup.remove(localNodeName);

        if (!serversToFollowup.isEmpty()) {
          ODistributedServerLog.debug(this, localNodeName, serversToFollowup.toString(), ODistributedServerLog.DIRECTION.OUT,
              "Distributed response (reqId=%s quorum=%d result=%s), checking for any fix needed...", request.getId(), quorum,
              quorumResponse);

          for (String s : serversToFollowup) {
            Object response = responses.get(s);
            if (response == NO_RESPONSE)
              response = null;

            if (quorumResponse != null && !quorumResponse.equals(response)) {
              // SEND FIX
              final Object payload = response instanceof ODistributedResponse ?
                  ((ODistributedResponse) response).getPayload() :
                  null;

              final ORemoteTask fixTask = ((OAbstractReplicatedTask) request.getTask())
                  .getFixTask(request, request.getTask(), payload, quorumResponse.getPayload(), s, dManager);

              if (fixTask == null) {
                // FIX NOT AVAILABLE: UNDO THE ENTIRE OPERATION
                ODistributedServerLog.debug(this, localNodeName, null, DIRECTION.NONE,
                    "No fix operation available: cannot fix the operation, undo the entire operation (reqId=%s)", request);
                continue;
              }

              executeFix(s, fixTask, response, quorumResponse.getPayload());
            }
          }
        }
      }
    }
  }
}


package com.orientechnologies.orient.core.sql.parser;

import com.orientechnologies.orient.core.command.OBasicCommandContext;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.config.OStorageConfiguration;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.OMetadataUpdateListener;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.index.OIndexManagerAbstract;
import com.orientechnologies.orient.core.metadata.schema.OSchemaShared;
import com.orientechnologies.orient.core.sql.executor.OExecutionPlan;
import com.orientechnologies.orient.core.sql.executor.OInternalExecutionPlan;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * This class is an LRU cache for already prepared SQL execution plans. It stores itself in the storage as a resource. It also acts
 * an an entry point for the SQL executor.
 *
 * @author Luigi Dell'Aquila (l.dellaquila-(at)-orientdb.com)
 */
public class OExecutionPlanCache implements OMetadataUpdateListener {

  Map<String, OInternalExecutionPlan> map;
  int                                 mapSize;

  protected long lastInvalidation = -1;

  /**
   * @param size the size of the cache
   */
  public OExecutionPlanCache(int size) {
    this.mapSize = size;
    map = new LinkedHashMap<String, OInternalExecutionPlan>(size) {
      protected boolean removeEldestEntry(final Map.Entry<String, OInternalExecutionPlan> eldest) {
        return super.size() > mapSize;
      }
    };
  }

  public static long getLastInvalidation(ODatabaseDocumentInternal db) {
    if (db == null) {
      throw new IllegalArgumentException("DB cannot be null");
    }

    OExecutionPlanCache resource = db.getSharedContext().getExecutionPlanCache();
    synchronized (resource) {
      return resource.lastInvalidation;
    }
  }

  /**
   * @param statement an SQL statement
   *
   * @return true if the corresponding executor is present in the cache
   */
  public boolean contains(String statement) {
    synchronized (map) {
      return map.containsKey(statement);
    }
  }

  /**
   * returns an already prepared SQL execution plan, taking it from the cache if it exists or creating a new one if it doesn't
   *
   * @param statement the SQL statement
   * @param ctx
   * @param db        the current DB instance
   *
   * @return a statement executor from the cache
   */
  public static OExecutionPlan get(String statement, OCommandContext ctx, ODatabaseDocumentInternal db) {
    if (db == null) {
      throw new IllegalArgumentException("DB cannot be null");
    }
    if (statement == null) {
      return null;
    }

    OExecutionPlanCache resource = db.getSharedContext().getExecutionPlanCache();
    OExecutionPlan result = resource.getInternal(statement, ctx, db);
    return result;
  }

  public static void put(String statement, OExecutionPlan plan, ODatabaseDocumentInternal db) {
    if (db == null) {
      throw new IllegalArgumentException("DB cannot be null");
    }
    if (statement == null) {
      return;
    }

    OExecutionPlanCache resource = db.getSharedContext().getExecutionPlanCache();
    resource.putInternal(statement, plan, db);
  }

  public void putInternal(String statement, OExecutionPlan plan, ODatabaseDocument db) {
    if (statement == null) {
      return;
    }
    synchronized (map) {
      OInternalExecutionPlan internal = (OInternalExecutionPlan) plan;
      OBasicCommandContext ctx = new OBasicCommandContext();
      ctx.setDatabase(db);
      internal = internal.copy(ctx);
      //this copy is never used, so it has to be closed to free resources
      internal.close();
      map.put(statement, internal);
    }
  }

  /**
   * @param statement an SQL statement
   * @param ctx
   *
   * @return the corresponding executor, taking it from the internal cache, if it exists
   */
  public OExecutionPlan getInternal(String statement, OCommandContext ctx, ODatabaseDocumentInternal db) {
    OInternalExecutionPlan result;
    if (statement == null) {
      return null;
    }
    synchronized (map) {
      //LRU
      result = map.remove(statement);
      if (result != null) {
        map.put(statement, result);
        result = result.copy(ctx);
      }
    }

    return result;
  }

  public void invalidate() {
    synchronized (this) {
      synchronized (map) {
        map.clear();
      }
      lastInvalidation = System.currentTimeMillis();
    }
  }

  @Override
  public void onSchemaUpdate(String database, OSchemaShared schema) {
    invalidate();
  }

  @Override
  public void onIndexManagerUpdate(String database, OIndexManagerAbstract indexManager) {
    invalidate();
  }

  @Override
  public void onFunctionLibraryUpdate(String database) {
    invalidate();
  }

  @Override
  public void onSequenceLibraryUpdate(String database) {
    invalidate();
  }

  @Override
  public void onStorageConfigurationUpdate(String database, OStorageConfiguration update) {
    invalidate();
  }

  public static OExecutionPlanCache instance(ODatabaseDocumentTx db) {
    if (db == null) {
      throw new IllegalArgumentException("DB cannot be null");
    }

    OExecutionPlanCache resource = db.getSharedContext().getExecutionPlanCache();
    return resource;
  }
}


/* Generated By:JJTree: Do not edit this line. OExpression.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.orient.core.collate.OCollate;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.record.OElement;
import com.orientechnologies.orient.core.sql.executor.AggregationContext;
import com.orientechnologies.orient.core.sql.executor.OResult;
import com.orientechnologies.orient.core.sql.executor.OResultInternal;

import java.util.List;
import java.util.Map;
import java.util.Set;

public class OExpression extends SimpleNode {

  protected Boolean singleQuotes;
  protected Boolean doubleQuotes;

  protected boolean                isNull = false;
  protected ORid                   rid;
  protected OMathExpression        mathExpression;
  protected OArrayConcatExpression arrayConcatExpression;
  protected OJson                  json;
  protected Boolean                booleanValue;

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

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

  public OExpression(OIdentifier identifier) {
    mathExpression = new OBaseExpression(identifier);
  }

  public OExpression(OIdentifier identifier, OModifier modifier) {

    mathExpression = new OBaseExpression(identifier, modifier);
  }

  public OExpression(ORecordAttribute attr, OModifier modifier) {
    mathExpression = new OBaseExpression(attr, modifier);
  }

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

  public Object execute(OIdentifiable iCurrentRecord, OCommandContext ctx) {
    if (isNull) {
      return null;
    }
    if (rid != null) {
      return rid.toRecordId(iCurrentRecord, ctx);
    }
    if (mathExpression != null) {
      return mathExpression.execute(iCurrentRecord, ctx);
    }
    if (arrayConcatExpression != null) {
      return arrayConcatExpression.execute(iCurrentRecord, ctx);
    }
    if (json != null) {
      return json.toMap(iCurrentRecord, ctx);
    }
    if (booleanValue != null) {
      return booleanValue;
    }
    if (value instanceof ONumber) {
      return ((ONumber) value).getValue();//only for old executor (manually replaced params)
    }

    //from here it's old stuff, only for the old executor
    if (value instanceof ORid) {
      ORid v = (ORid) value;
      return new ORecordId(v.cluster.getValue().intValue(), v.position.getValue().longValue());
    } else if (value instanceof OMathExpression) {
      return ((OMathExpression) value).execute(iCurrentRecord, ctx);
    } else if (value instanceof OArrayConcatExpression) {
      return ((OArrayConcatExpression) value).execute(iCurrentRecord, ctx);
    } else if (value instanceof OJson) {
      return ((OJson) value).toMap(iCurrentRecord, ctx);
    } else if (value instanceof String) {
      return value;
    } else if (value instanceof Number) {
      return value;
    }

    return value;
  }

  public Object execute(OResult iCurrentRecord, OCommandContext ctx) {
    if (isNull) {
      return null;
    }
    if (rid != null) {
      return rid.toRecordId(iCurrentRecord, ctx);
    }
    if (mathExpression != null) {
      return mathExpression.execute(iCurrentRecord, ctx);
    }
    if (arrayConcatExpression != null) {
      return arrayConcatExpression.execute(iCurrentRecord, ctx);
    }
    if (json != null) {
      return json.toObjectDetermineType(iCurrentRecord, ctx);
    }
    if (booleanValue != null) {
      return booleanValue;
    }
    if (value instanceof ONumber) {
      return ((ONumber) value).getValue();//only for old executor (manually replaced params)
    }

    //from here it's old stuff, only for the old executor
    if (value instanceof ORid) {
      ORid v = (ORid) value;
      return new ORecordId(v.cluster.getValue().intValue(), v.position.getValue().longValue());
    } else if (value instanceof OMathExpression) {
      return ((OMathExpression) value).execute(iCurrentRecord, ctx);
    } else if (value instanceof OArrayConcatExpression) {
      return ((OArrayConcatExpression) value).execute(iCurrentRecord, ctx);
    } else if (value instanceof OJson) {
      return ((OJson) value).toMap(iCurrentRecord, ctx);
    } else if (value instanceof String) {
      return value;
    } else if (value instanceof Number) {
      return value;
    }

    return value;
  }

  public boolean isBaseIdentifier() {
    if (mathExpression != null) {
      return mathExpression.isBaseIdentifier();
    }
    if (value instanceof OMathExpression) {//only backward stuff, remote it
      return ((OMathExpression) value).isBaseIdentifier();
    }

    return false;
  }

  public boolean isEarlyCalculated(OCommandContext ctx) {
    if (this.mathExpression != null) {
      return this.mathExpression.isEarlyCalculated(ctx);
    }
    if (this.arrayConcatExpression != null) {
      return this.arrayConcatExpression.isEarlyCalculated(ctx);
    }

    if (booleanValue != null) {
      return true;
    }
    if (rid != null) {
      return true;
    }
    if (value instanceof Number) {
      return true;
    }
    if (value instanceof String) {
      return true;
    }
    if (value instanceof OMathExpression) {
      return ((OMathExpression) value).isEarlyCalculated(ctx);
    }

    return false;
  }

  public OIdentifier getDefaultAlias() {
    OIdentifier identifier;
    if (isBaseIdentifier()) {
      identifier = new OIdentifier(((OBaseExpression) mathExpression).identifier.getSuffix().identifier.getStringValue());
    } else {
      identifier = new OIdentifier(this.toString());
    }
    return identifier;
  }

  public void toString(Map<Object, Object> params, StringBuilder builder) {
    //    if (value == null) {
    //      builder.append("null");
    //    } else if (value instanceof SimpleNode) {
    //      ((SimpleNode) value).toString(params, builder);
    //    } else if (value instanceof String) {
    //      if (Boolean.TRUE.equals(singleQuotes)) {
    //        builder.append("'" + value + "'");
    //      } else {
    //        builder.append("\"" + value + "\"");
    //      }
    //    } else {
    //      builder.append("" + value);
    //    }

    if (isNull) {
      builder.append("null");
    } else if (rid != null) {
      rid.toString(params, builder);
    } else if (mathExpression != null) {
      mathExpression.toString(params, builder);
    } else if (arrayConcatExpression != null) {
      arrayConcatExpression.toString(params, builder);
    } else if (json != null) {
      json.toString(params, builder);
    } else if (booleanValue != null) {
      builder.append(booleanValue.toString());
    } else if (value instanceof SimpleNode) {
      ((SimpleNode) value).toString(params, builder);//only for translated input params, will disappear with new executor
    } else if (value instanceof String) {
      if (Boolean.TRUE.equals(singleQuotes)) {
        builder.append("'" + value + "'");
      } else {
        builder.append("\"" + value + "\"");
      }

    } else {
      builder.append("" + value);//only for translated input params, will disappear with new executor
    }
  }

  public static String encode(String s) {
    StringBuilder builder = new StringBuilder(s.length());
    for (char c : s.toCharArray()) {
      if (c == '\n') {
        builder.append("\\n");
        continue;
      }
      if (c == '\t') {
        builder.append("\\t");
        continue;
      }
      if (c == '\\' || c == '"') {
        builder.append("\\");
      }
      builder.append(c);
    }
    return builder.toString();
  }

  public boolean supportsBasicCalculation() {
    if (mathExpression != null) {
      return mathExpression.supportsBasicCalculation();
    }
    if (arrayConcatExpression != null) {
      return arrayConcatExpression.supportsBasicCalculation();
    }
    return true;
  }

  public boolean isIndexedFunctionCal() {
    if (mathExpression != null) {
      return mathExpression.isIndexedFunctionCall();
    }
    return false;
  }

  public static String encodeSingle(String s) {

    StringBuilder builder = new StringBuilder(s.length());
    for (char c : s.toCharArray()) {
      if (c == '\n') {
        builder.append("\\n");
        continue;
      }
      if (c == '\t') {
        builder.append("\\t");
        continue;
      }
      if (c == '\\' || c == '\'') {
        builder.append("\\");
      }
      builder.append(c);
    }
    return builder.toString();
  }

  public long estimateIndexedFunction(OFromClause target, OCommandContext context, OBinaryCompareOperator operator, Object right) {
    if (mathExpression != null) {
      return mathExpression.estimateIndexedFunction(target, context, operator, right);
    }
    return -1;
  }

  public Iterable<OIdentifiable> executeIndexedFunction(OFromClause target, OCommandContext context,
      OBinaryCompareOperator operator, Object right) {
    if (mathExpression != null) {
      return mathExpression.executeIndexedFunction(target, context, operator, right);
    }
    return null;
  }

  /**
   * tests if current expression is an indexed function AND that function can also be executed without using the index
   *
   * @param target   the query target
   * @param context  the execution context
   * @param operator
   * @param right
   *
   * @return true if current expression is an indexed funciton AND that function can also be executed without using the index, false
   * otherwise
   */
  public boolean canExecuteIndexedFunctionWithoutIndex(OFromClause target, OCommandContext context, OBinaryCompareOperator operator,
      Object right) {
    if (mathExpression != null) {
      return mathExpression.canExecuteIndexedFunctionWithoutIndex(target, context, operator, right);
    }
    return false;
  }

  /**
   * tests if current expression is an indexed function AND that function can be used on this target
   *
   * @param target   the query target
   * @param context  the execution context
   * @param operator
   * @param right
   *
   * @return true if current expression involves an indexed function AND that function can be used on this target, false otherwise
   */
  public boolean allowsIndexedFunctionExecutionOnTarget(OFromClause target, OCommandContext context,
      OBinaryCompareOperator operator, Object right) {
    if (mathExpression != null) {
      return mathExpression.allowsIndexedFunctionExecutionOnTarget(target, context, operator, right);
    }
    return false;
  }

  /**
   * tests if current expression is an indexed function AND the function has also to be executed after the index search. In some
   * cases, the index search is accurate, so this condition can be excluded from further evaluation. In other cases the result from
   * the index is a superset of the expected result, so the function has to be executed anyway for further filtering
   *
   * @param target  the query target
   * @param context the execution context
   *
   * @return true if current expression involves an indexed function AND the function has also to be executed after the index
   * search.
   */
  public boolean executeIndexedFunctionAfterIndexSearch(OFromClause target, OCommandContext context,
      OBinaryCompareOperator operator, Object right) {
    if (mathExpression != null) {
      return mathExpression.executeIndexedFunctionAfterIndexSearch(target, context, operator, right);
    }
    return false;
  }

  public boolean isExpand() {
    if (mathExpression != null) {
      return mathExpression.isExpand();
    }
    return false;
  }

  public OExpression getExpandContent() {
    return mathExpression.getExpandContent();
  }

  public boolean needsAliases(Set<String> aliases) {
    if (mathExpression != null) {
      return mathExpression.needsAliases(aliases);
    }
    if (arrayConcatExpression != null) {
      return arrayConcatExpression.needsAliases(aliases);
    }
    if (json != null) {
      return json.needsAliases(aliases);
    }
    return false;
  }

  public boolean isAggregate() {
    if (mathExpression != null && mathExpression.isAggregate()) {
      return true;
    }
    if (arrayConcatExpression != null && arrayConcatExpression.isAggregate()) {
      return true;
    }
    if (json != null && json.isAggregate()) {
      return true;
    }
    return false;
  }

  public OExpression splitForAggregation(AggregateProjectionSplit aggregateSplit, OCommandContext ctx) {
    if (isAggregate()) {
      OExpression result = new OExpression(-1);
      if (mathExpression != null) {
        SimpleNode splitResult = mathExpression.splitForAggregation(aggregateSplit, ctx);
        if (splitResult instanceof OMathExpression) {
          result.mathExpression = (OMathExpression) splitResult;
        } else if (splitResult instanceof OExpression) {
          return (OExpression) splitResult;
        } else {
          throw new IllegalStateException("something went wrong while splitting expression for aggregate " + toString());
        }
      }
      if (arrayConcatExpression != null) {
        SimpleNode splitResult = arrayConcatExpression.splitForAggregation(aggregateSplit);
        if (splitResult instanceof OArrayConcatExpression) {
          result.arrayConcatExpression = (OArrayConcatExpression) splitResult;
        } else if (splitResult instanceof OExpression) {
          return (OExpression) splitResult;
        } else {
          throw new IllegalStateException("something went wrong while splitting expression for aggregate " + toString());
        }
      }
      if (json != null) {
        result.json = json.splitForAggregation(aggregateSplit, ctx);
      }
      return result;
    } else {
      return this;
    }
  }

  public AggregationContext getAggregationContext(OCommandContext ctx) {
    if (mathExpression != null) {
      return mathExpression.getAggregationContext(ctx);
    } else if (arrayConcatExpression != null) {
      return arrayConcatExpression.getAggregationContext(ctx);
    } else {
      throw new OCommandExecutionException("Cannot aggregate on " + toString());
    }
  }

  public OExpression copy() {

    OExpression result = new OExpression(-1);
    result.singleQuotes = singleQuotes;
    result.doubleQuotes = doubleQuotes;
    result.isNull = isNull;
    result.rid = rid == null ? null : rid.copy();
    result.mathExpression = mathExpression == null ? null : mathExpression.copy();
    result.arrayConcatExpression = arrayConcatExpression == null ? null : arrayConcatExpression.copy();
    result.json = json == null ? null : json.copy();
    result.booleanValue = booleanValue;

    return result;
  }

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

    OExpression that = (OExpression) o;

    if (isNull != that.isNull)
      return false;
    if (singleQuotes != null ? !singleQuotes.equals(that.singleQuotes) : that.singleQuotes != null)
      return false;
    if (doubleQuotes != null ? !doubleQuotes.equals(that.doubleQuotes) : that.doubleQuotes != null)
      return false;
    if (rid != null ? !rid.equals(that.rid) : that.rid != null)
      return false;
    if (mathExpression != null ? !mathExpression.equals(that.mathExpression) : that.mathExpression != null)
      return false;
    if (arrayConcatExpression != null ?
        !arrayConcatExpression.equals(that.arrayConcatExpression) :
        that.arrayConcatExpression != null)
      return false;
    if (json != null ? !json.equals(that.json) : that.json != null)
      return false;
    if (booleanValue != null ? !booleanValue.equals(that.booleanValue) : that.booleanValue != null)
      return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = singleQuotes != null ? singleQuotes.hashCode() : 0;
    result = 31 * result + (doubleQuotes != null ? doubleQuotes.hashCode() : 0);
    result = 31 * result + (isNull ? 1 : 0);
    result = 31 * result + (rid != null ? rid.hashCode() : 0);
    result = 31 * result + (mathExpression != null ? mathExpression.hashCode() : 0);
    result = 31 * result + (arrayConcatExpression != null ? arrayConcatExpression.hashCode() : 0);
    result = 31 * result + (json != null ? json.hashCode() : 0);
    result = 31 * result + (booleanValue != null ? booleanValue.hashCode() : 0);
    return result;
  }

  public void setMathExpression(OMathExpression mathExpression) {
    this.mathExpression = mathExpression;
  }

  public void extractSubQueries(SubQueryCollector collector) {
    if (mathExpression != null) {
      mathExpression.extractSubQueries(collector);
    }
    if (arrayConcatExpression != null) {
      arrayConcatExpression.extractSubQueries(collector);
    }
    if (json != null) {
      json.extractSubQueries(collector);
    }
  }

  public void extractSubQueries(OIdentifier letAlias, SubQueryCollector collector) {
    if (mathExpression != null) {
      mathExpression.extractSubQueries(letAlias, collector);
    }
    if (arrayConcatExpression != null) {
      arrayConcatExpression.extractSubQueries(collector);
    }
    if (json != null) {
      json.extractSubQueries(collector);
    }
  }

  public boolean refersToParent() {
    if (mathExpression != null && mathExpression.refersToParent()) {
      return true;
    }
    if (arrayConcatExpression != null && arrayConcatExpression.refersToParent()) {
      return true;
    }
    if (json != null && json.refersToParent()) {
      return true;
    }
    return false;
  }

  public ORid getRid() {
    return rid;
  }

  public void setRid(ORid rid) {
    this.rid = rid;
  }

  public OMathExpression getMathExpression() {
    return mathExpression;
  }

  /**
   * if the condition involved the current pattern (MATCH statement, eg. $matched.something = foo), returns the name of involved
   * pattern aliases ("something" in this case)
   *
   * @return a list of pattern aliases involved in this condition. Null it does not involve the pattern
   */
  List<String> getMatchPatternInvolvedAliases() {
    if (mathExpression != null)
      return mathExpression.getMatchPatternInvolvedAliases();
    if (arrayConcatExpression != null)
      return arrayConcatExpression.getMatchPatternInvolvedAliases();
    return null;
  }

  public void applyRemove(OResultInternal result, OCommandContext ctx) {
    if (mathExpression != null) {
      mathExpression.applyRemove(result, ctx);
    } else {
      throw new OCommandExecutionException("Cannot apply REMOVE " + toString());
    }
  }

  public boolean isCount() {
    if (mathExpression == null) {
      return false;
    }
    return mathExpression.isCount();
  }

  public OArrayConcatExpression getArrayConcatExpression() {
    return arrayConcatExpression;
  }

  public void setArrayConcatExpression(OArrayConcatExpression arrayConcatExpression) {
    this.arrayConcatExpression = arrayConcatExpression;
  }

  public OResult serialize() {
    OResultInternal result = new OResultInternal();
    result.setProperty("singleQuotes", singleQuotes);
    result.setProperty("doubleQuotes", doubleQuotes);
    result.setProperty("isNull", isNull);

    if (rid != null) {
      result.setProperty("rid", rid.serialize());
    }
    if (mathExpression != null) {
      result.setProperty("mathExpression", mathExpression.serialize());
    }
    if (arrayConcatExpression != null) {
      result.setProperty("arrayConcatExpression", arrayConcatExpression.serialize());
    }
    if (json != null) {
      result.setProperty("json", json.serialize());
    }
    result.setProperty("booleanValue", booleanValue);
    return result;
  }

  public void deserialize(OResult fromResult) {
    singleQuotes = fromResult.getProperty("singleQuotes");
    doubleQuotes = fromResult.getProperty("doubleQuotes");
    isNull = fromResult.getProperty("isNull");

    if (fromResult.getProperty("rid") != null) {
      rid = new ORid(-1);
      rid.deserialize(fromResult.getProperty("rid"));
    }
    if (fromResult.getProperty("mathExpression") != null) {
      mathExpression = OMathExpression.deserializeFromResult(fromResult.getProperty("mathExpression"));
    }
    if (fromResult.getProperty("arrayConcatExpression") != null) {
      arrayConcatExpression = new OArrayConcatExpression(-1);
      arrayConcatExpression.deserialize(fromResult.getProperty("arrayConcatExpression"));
    }
    if (fromResult.getProperty("json") != null) {
      json = new OJson(-1);
      json.deserialize(fromResult.getProperty("json"));
    }
    booleanValue = fromResult.getProperty("booleanValue");
  }

  public boolean isDefinedFor(OResult currentRecord) {
    if (mathExpression != null) {
      return mathExpression.isDefinedFor(currentRecord);
    } else {
      return true;
    }
  }

  public boolean isDefinedFor(OElement currentRecord) {
    if (mathExpression != null) {
      return mathExpression.isDefinedFor(currentRecord);
    } else {
      return true;
    }
  }

  public OCollate getCollate(OResult currentRecord, OCommandContext ctx) {
    if (mathExpression != null) {
      return mathExpression.getCollate(currentRecord, ctx);
    }
    return null;
  }

  public boolean isCacheable() {
    if (mathExpression != null) {
      return mathExpression.isCacheable();
    }
    if (arrayConcatExpression != null) {
      return arrayConcatExpression.isCacheable();
    }
    if (json != null) {
      return json.isCacheable();
    }

    return true;
  }

  public boolean isIndexChain(OCommandContext ctx, OClass clazz) {
    if (mathExpression != null) {
      return mathExpression.isIndexChain(ctx, clazz);
    }
    return false;
  }

  public boolean isFunctionAny() {
    if (mathExpression != null) {
      return mathExpression.isFunctionAny();
    }
    return false;
  }
}
/* JavaCC - OriginalChecksum=9c860224b121acdc89522ae97010be01 (do not edit this line) */


/* Generated By:JJTree: Do not edit this line. OBinaryCondition.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.OException;
import com.orientechnologies.orient.core.collate.OCollate;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
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.sql.executor.OResult;
import com.orientechnologies.orient.core.sql.executor.OResultInternal;

import java.util.*;

public class OBinaryCondition extends OBooleanExpression {
  protected OExpression            left;
  protected OBinaryCompareOperator operator;
  protected OExpression            right;

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

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

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

  @Override
  public boolean evaluate(OIdentifiable currentRecord, OCommandContext ctx) {
    return operator.execute(left.execute(currentRecord, ctx), right.execute(currentRecord, ctx));
  }

  @Override
  public boolean evaluate(OResult currentRecord, OCommandContext ctx) {
    if (left.isFunctionAny()) {
      return evaluateAny(currentRecord, ctx);
    }
    Object leftVal = left.execute(currentRecord, ctx);
    Object rightVal = right.execute(currentRecord, ctx);
    OCollate collate = left.getCollate(currentRecord, ctx);
    if (collate == null) {
      collate = right.getCollate(currentRecord, ctx);
    }
    if (collate != null) {
      leftVal = collate.transform(leftVal);
      rightVal = collate.transform(rightVal);
    }
    return operator.execute(leftVal, rightVal);
  }

  private boolean evaluateAny(OResult currentRecord, OCommandContext ctx) {
    for (String s : currentRecord.getPropertyNames()) {
      Object leftVal = currentRecord.getProperty(s);
      Object rightVal = right.execute(currentRecord, ctx);

      //TODO collate

      if (operator.execute(leftVal, rightVal)) {
        return true;
      }
    }
    return false;
  }

  public void toString(Map<Object, Object> params, StringBuilder builder) {
    left.toString(params, builder);
    builder.append(" ");
    builder.append(operator.toString());
    builder.append(" ");
    right.toString(params, builder);
  }

  protected boolean supportsBasicCalculation() {
    if (!operator.supportsBasicCalculation()) {
      return false;
    }
    return left.supportsBasicCalculation() && right.supportsBasicCalculation();

  }

  @Override
  protected int getNumberOfExternalCalculations() {
    int total = 0;
    if (!operator.supportsBasicCalculation()) {
      total++;
    }
    if (!left.supportsBasicCalculation()) {
      total++;
    }
    if (!right.supportsBasicCalculation()) {
      total++;
    }
    return total;
  }

  @Override
  protected List<Object> getExternalCalculationConditions() {
    List<Object> result = new ArrayList<Object>();
    if (!operator.supportsBasicCalculation()) {
      result.add(this);
    }
    if (!left.supportsBasicCalculation()) {
      result.add(left);
    }
    if (!right.supportsBasicCalculation()) {
      result.add(right);
    }
    return result;
  }

  public OBinaryCondition isIndexedFunctionCondition(OClass iSchemaClass, ODatabaseDocumentInternal database) {
    if (left.isIndexedFunctionCal()) {
      return this;
    }
    return null;
  }

  public long estimateIndexed(OFromClause target, OCommandContext context) {
    return left.estimateIndexedFunction(target, context, operator, right.execute((OResult) null, context));
  }

  public Iterable<OIdentifiable> executeIndexedFunction(OFromClause target, OCommandContext context) {
    return left.executeIndexedFunction(target, context, operator, right.execute((OResult) null, context));
  }

  /**
   * tests if current expression involves an indexed funciton AND that function can also be executed without using the index
   *
   * @param target  the query target
   * @param context the execution context
   *
   * @return true if current expression involves an indexed function AND that function can be used on this target, false otherwise
   */
  public boolean canExecuteIndexedFunctionWithoutIndex(OFromClause target, OCommandContext context) {
    return left.canExecuteIndexedFunctionWithoutIndex(target, context, operator, right.execute((OResult) null, context));
  }

  /**
   * tests if current expression involves an indexed function AND that function can be used on this target
   *
   * @param target  the query target
   * @param context the execution context
   *
   * @return true if current expression involves an indexed function AND that function can be used on this target, false otherwise
   */
  public boolean allowsIndexedFunctionExecutionOnTarget(OFromClause target, OCommandContext context) {
    return left.allowsIndexedFunctionExecutionOnTarget(target, context, operator, right.execute((OResult) null, context));
  }

  /**
   * tests if current expression involves an indexed function AND the function has also to be executed after the index search. In
   * some cases, the index search is accurate, so this condition can be excluded from further evaluation. In other cases the result
   * from the index is a superset of the expected result, so the function has to be executed anyway for further filtering
   *
   * @param target  the query target
   * @param context the execution context
   *
   * @return true if current expression involves an indexed function AND the function has also to be executed after the index
   * search.
   */
  public boolean executeIndexedFunctionAfterIndexSearch(OFromClause target, OCommandContext context) {
    return left.executeIndexedFunctionAfterIndexSearch(target, context, operator, right.execute((OResult) null, context));
  }

  public List<OBinaryCondition> getIndexedFunctionConditions(OClass iSchemaClass, ODatabaseDocumentInternal database) {
    if (left.isIndexedFunctionCal()) {
      return Collections.singletonList(this);
    }
    return null;
  }

  @Override
  public boolean needsAliases(Set<String> aliases) {
    if (left.needsAliases(aliases)) {
      return true;
    }
    if (right.needsAliases(aliases)) {
      return true;
    }
    return false;
  }

  @Override
  public OBinaryCondition copy() {
    OBinaryCondition result = new OBinaryCondition(-1);
    result.left = left.copy();
    result.operator = (OBinaryCompareOperator) operator.copy();
    result.right = right.copy();
    return result;
  }

  @Override
  public void extractSubQueries(SubQueryCollector collector) {
    left.extractSubQueries(collector);
    right.extractSubQueries(collector);
  }

  @Override
  public boolean refersToParent() {
    return left.refersToParent() || right.refersToParent();
  }

  @Override
  public Optional<OUpdateItem> transformToUpdateItem() {
    if (!checkCanTransformToUpdate()) {
      return Optional.empty();
    }
    if (operator instanceof OEqualsCompareOperator) {
      OUpdateItem result = new OUpdateItem(-1);
      result.operator = OUpdateItem.OPERATOR_EQ;
      OBaseExpression baseExp = ((OBaseExpression) left.mathExpression);
      result.left = baseExp.identifier.suffix.identifier.copy();
      result.leftModifier = baseExp.modifier == null ? null : baseExp.modifier.copy();
      result.right = right.copy();
      return Optional.of(result);
    }
    return super.transformToUpdateItem();
  }

  private boolean checkCanTransformToUpdate() {
    if (left == null || left.mathExpression == null || !(left.mathExpression instanceof OBaseExpression)) {
      return false;
    }
    OBaseExpression base = (OBaseExpression) left.mathExpression;
    if (base.identifier == null || base.identifier.suffix == null || base.identifier.suffix.identifier == null) {
      return false;
    }
    return true;
  }

  public OExpression getLeft() {
    return left;
  }

  public OBinaryCompareOperator getOperator() {
    return operator;
  }

  public OExpression getRight() {
    return right;
  }

  public void setLeft(OExpression left) {
    this.left = left;
  }

  public void setOperator(OBinaryCompareOperator operator) {
    this.operator = operator;
  }

  public void setRight(OExpression right) {
    this.right = right;
  }

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

    OBinaryCondition that = (OBinaryCondition) o;

    if (left != null ? !left.equals(that.left) : that.left != null)
      return false;
    if (operator != null ? !operator.equals(that.operator) : that.operator != null)
      return false;
    if (right != null ? !right.equals(that.right) : that.right != null)
      return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = left != null ? left.hashCode() : 0;
    result = 31 * result + (operator != null ? operator.hashCode() : 0);
    result = 31 * result + (right != null ? right.hashCode() : 0);
    return result;
  }

  @Override
  public List<String> getMatchPatternInvolvedAliases() {
    List<String> leftX = left.getMatchPatternInvolvedAliases();
    List<String> rightX = right.getMatchPatternInvolvedAliases();
    if (leftX == null) {
      return rightX;
    }
    if (rightX == null) {
      return leftX;
    }

    List<String> result = new ArrayList<String>();
    result.addAll(leftX);
    result.addAll(rightX);
    return result;
  }

  @Override
  public void translateLuceneOperator() {
    if (operator instanceof OLuceneOperator) {
      OExpression newLeft = new OExpression(-1);
      newLeft.mathExpression = new OBaseExpression(-1);
      OBaseIdentifier identifirer = new OBaseIdentifier(-1);
      ((OBaseExpression) newLeft.mathExpression).identifier = identifirer;
      identifirer.levelZero = new OLevelZeroIdentifier(-1);
      OFunctionCall function = new OFunctionCall(-1);
      identifirer.levelZero.functionCall = function;
      function.name = new OIdentifier("search_fields");
      function.params = new ArrayList<>();
      function.params.add(fieldNamesToStrings(left));
      function.params.add(right);
      left = newLeft;

      operator = new OEqualsCompareOperator(-1);
      right = new OExpression(-1);
      right.booleanValue = true;
    }
  }

  private OExpression fieldNamesToStrings(OExpression left) {
    if (left.isBaseIdentifier()) {
      OIdentifier identifier = ((OBaseExpression) left.mathExpression).identifier.suffix.identifier;
      OCollection newColl = new OCollection(-1);
      newColl.expressions = new ArrayList<>();
      newColl.expressions.add(identifierToStringExpr(identifier));
      OExpression result = new OExpression(-1);
      OBaseExpression newBase = new OBaseExpression(-1);
      result.mathExpression = newBase;
      newBase.identifier = new OBaseIdentifier(-1);
      newBase.identifier.levelZero = new OLevelZeroIdentifier(-1);
      newBase.identifier.levelZero.collection = newColl;
      return result;
    } else if (left.mathExpression instanceof OBaseExpression) {
      OBaseExpression base = (OBaseExpression) left.mathExpression;
      if (base.identifier != null && base.identifier.levelZero != null && base.identifier.levelZero.collection != null) {
        OCollection coll = base.identifier.levelZero.collection;

        OCollection newColl = new OCollection(-1);
        newColl.expressions = new ArrayList<>();

        for (OExpression exp : coll.expressions) {
          if (exp.isBaseIdentifier()) {
            OIdentifier identifier = ((OBaseExpression) exp.mathExpression).identifier.suffix.identifier;
            OExpression val = identifierToStringExpr(identifier);
            newColl.expressions.add(val);
          } else {
            throw new OCommandExecutionException("Cannot execute because of invalid LUCENE expression");
          }
        }
        OExpression result = new OExpression(-1);
        OBaseExpression newBase = new OBaseExpression(-1);
        result.mathExpression = newBase;
        newBase.identifier = new OBaseIdentifier(-1);
        newBase.identifier.levelZero = new OLevelZeroIdentifier(-1);
        newBase.identifier.levelZero.collection = newColl;
        return result;
      }
    }
    throw new OCommandExecutionException("Cannot execute because of invalid LUCENE expression");
  }

  private OExpression identifierToStringExpr(OIdentifier identifier) {
    OBaseExpression bexp = new OBaseExpression(identifier.getStringValue());

    OExpression result = new OExpression(-1);
    result.mathExpression = bexp;
    return result;
  }

  public OResult serialize() {
    OResultInternal result = new OResultInternal();
    result.setProperty("left", left.serialize());
    result.setProperty("operator", operator.getClass().getName());
    result.setProperty("right", right.serialize());
    return result;
  }

  public void deserialize(OResult fromResult) {
    left = new OExpression(-1);
    left.deserialize(fromResult.getProperty("left"));
    try {
      operator = (OBinaryCompareOperator) Class.forName(String.valueOf(fromResult.getProperty("operator"))).newInstance();
    } catch (Exception e) {
      throw OException.wrapException(new OCommandExecutionException(""), e);
    }
    right = new OExpression(-1);
    right.deserialize(fromResult.getProperty("right"));
  }

  @Override
  public boolean isCacheable() {
    return left.isCacheable() && right.isCacheable();
  }

  @Override
  public OBooleanExpression rewriteIndexChainsAsSubqueries(OCommandContext ctx, OClass clazz) {
    if (operator instanceof OEqualsCompareOperator && right.isEarlyCalculated(ctx) && left.isIndexChain(ctx, clazz)) {
      OInCondition result = new OInCondition(-1);

      result.left = new OExpression(-1);
      OBaseExpression base = new OBaseExpression(-1);
      base.identifier = new OBaseIdentifier(-1);
      base.identifier.suffix = new OSuffixIdentifier(-1);
      base.identifier.suffix.identifier = ((OBaseExpression) left.mathExpression).identifier.suffix.identifier;
      result.left.mathExpression = base;

      result.operator = new OInOperator(-1);

      OClass nextClazz = clazz.getProperty(base.identifier.suffix.identifier.getStringValue()).getLinkedClass();
      result.rightStatement = indexChainToStatement(((OBaseExpression) left.mathExpression).modifier, nextClazz, right, ctx);
      return result;
    }
    return this;
  }

  private OSelectStatement indexChainToStatement(OModifier modifier, OClass clazz, OExpression right, OCommandContext ctx) {
    OClass queryClass = clazz;

    OSelectStatement result = new OSelectStatement(-1);
    result.target = new OFromClause(-1);
    result.target.setItem(new OFromItem(-1));
    result.target.getItem().identifier = new OIdentifier(queryClass.getName());

    result.whereClause = new OWhereClause(-1);
    OBinaryCondition base = new OBinaryCondition(-1);
    result.whereClause.baseExpression = new ONotBlock(-1);
    ((ONotBlock) result.whereClause.baseExpression).sub = base;
    ((ONotBlock) result.whereClause.baseExpression).negate = false;

    base.left = new OExpression(-1);
    base.left.mathExpression = new OBaseExpression(-1);
    ((OBaseExpression) base.left.mathExpression).identifier = new OBaseIdentifier(modifier.suffix.identifier);
    ((OBaseExpression) base.left.mathExpression).modifier = modifier.next == null ? null : modifier.next.copy();

    base.operator = new OEqualsCompareOperator(-1);
    base.right = right.copy();

    return result;
  }
}
/* JavaCC - OriginalChecksum=99ed1dd2812eb730de8e1931b1764da5 (do not edit this line) */


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.command.script;

import com.orientechnologies.common.concur.resource.OPartitionedObjectPool;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.parser.OStringParser;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.command.OCommandManager;
import com.orientechnologies.orient.core.command.OScriptExecutor;
import com.orientechnologies.orient.core.command.OScriptExecutorRegister;
import com.orientechnologies.orient.core.command.script.formatter.*;
import com.orientechnologies.orient.core.command.script.transformer.OScriptTransformerImpl;
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.exception.OConfigurationException;
import com.orientechnologies.orient.core.metadata.function.OFunction;
import com.orientechnologies.orient.core.metadata.function.OFunctionUtilWrapper;
import com.orientechnologies.orient.core.sql.OSQLScriptEngine;
import com.orientechnologies.orient.core.sql.OSQLScriptEngineFactory;

import javax.script.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import static com.orientechnologies.common.util.OClassLoaderHelper.lookupProviderWithOrientClassLoader;

/**
 * Executes Script Commands.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 * @see OCommandScript
 */
public class OScriptManager {
  protected static final Object[] EMPTY_PARAMS       = new Object[] {};
  protected static final int      LINES_AROUND_ERROR = 5;
  protected final        String   DEF_LANGUAGE       = "javascript";
  protected              String   defaultLanguage    = DEF_LANGUAGE;
  protected ScriptEngineManager scriptEngineManager;
  protected Map<String, ScriptEngineFactory>                  engines            = new HashMap<String, ScriptEngineFactory>();
  protected Map<String, OScriptFormatter>                     formatters         = new HashMap<String, OScriptFormatter>();
  protected List<OScriptInjection>                            injections         = new ArrayList<OScriptInjection>();
  protected ConcurrentHashMap<String, ODatabaseScriptManager> dbManagers         = new ConcurrentHashMap<String, ODatabaseScriptManager>();
  protected Map<String, OScriptResultHandler>                 handlers           = new HashMap<String, OScriptResultHandler>();
  protected Map<String, Function<String, OScriptExecutor>>    executorsFactories = new HashMap<>();

  public OScriptManager() {
    scriptEngineManager = new ScriptEngineManager();

    executorsFactories.put("javascript", (lang) -> new OJavascriptScriptExecutor(lang, new OScriptTransformerImpl()));
    executorsFactories.put("ecmascript", (lang) -> new OJavascriptScriptExecutor(lang, new OScriptTransformerImpl()));

    for (ScriptEngineFactory f : scriptEngineManager.getEngineFactories()) {
      registerEngine(f.getLanguageName().toLowerCase(Locale.ENGLISH), f);

      if (defaultLanguage == null)
        defaultLanguage = f.getLanguageName();
    }

    if (!existsEngine(DEF_LANGUAGE)) {
      final ScriptEngine defEngine = scriptEngineManager.getEngineByName(DEF_LANGUAGE);
      if (defEngine == null) {
        OLogManager.instance().warnNoDb(this, "Cannot find default script language for %s", DEF_LANGUAGE);
      } else {
        // GET DIRECTLY THE LANGUAGE BY NAME (DON'T KNOW WHY SOMETIMES DOESN'T RETURN IT WITH getEngineFactories() ABOVE!
        registerEngine(DEF_LANGUAGE, defEngine.getFactory());
        defaultLanguage = DEF_LANGUAGE;
      }
    }

    registerFormatter(OSQLScriptEngine.NAME, new OSQLScriptFormatter());
    registerFormatter(DEF_LANGUAGE, new OJSScriptFormatter());
    registerFormatter("ruby", new ORubyScriptFormatter());
    registerFormatter("groovy", new OGroovyScriptFormatter());
    for (String lang : engines.keySet()) {
      Function<String, OScriptExecutor> factory = executorsFactories.get(lang);
      OScriptExecutor executor = null;
      if (factory != null) {
        executor = factory.apply(lang);
      } else {
        executor = new OJsr223ScriptExecutor(lang, new OScriptTransformerImpl());
      }
      OCommandManager.instance().registerScriptExecutor(lang, executor);
    }

    // Registring sql script engine after for not fight with the basic engine
    registerEngine(OSQLScriptEngine.NAME, new OSQLScriptEngineFactory());

    Iterator<OScriptExecutorRegister> customExecutors = lookupProviderWithOrientClassLoader(OScriptExecutorRegister.class);

    customExecutors.forEachRemaining(e -> e.registerExecutor(this, OCommandManager.instance()));

  }

  public String getFunctionDefinition(final OFunction iFunction) {
    final OScriptFormatter formatter = formatters.get(iFunction.getLanguage().toLowerCase(Locale.ENGLISH));
    if (formatter == null)
      throw new IllegalArgumentException("Cannot find script formatter for the language '" + iFunction.getLanguage() + "'");

    return formatter.getFunctionDefinition(iFunction);
  }

  public String getFunctionInvoke(final OFunction iFunction, final Object[] iArgs) {
    final OScriptFormatter formatter = formatters.get(iFunction.getLanguage().toLowerCase(Locale.ENGLISH));
    if (formatter == null)
      throw new IllegalArgumentException("Cannot find script formatter for the language '" + iFunction.getLanguage() + "'");

    return formatter.getFunctionInvoke(iFunction, iArgs);
  }

  /**
   * Formats the library of functions for a language.
   *
   * @param db        Current database instance
   * @param iLanguage Language as filter
   * @return String containing all the functions
   */
  public String getLibrary(final ODatabase<?> db, final String iLanguage) {
    if (db == null)
      // NO DB = NO LIBRARY
      return null;

    final StringBuilder code = new StringBuilder();

    final Set<String> functions = db.getMetadata().getFunctionLibrary().getFunctionNames();
    for (String fName : functions) {
      final OFunction f = db.getMetadata().getFunctionLibrary().getFunction(fName);

      if (f.getLanguage() == null)
        throw new OConfigurationException("Database function '" + fName + "' has no language");

      if (f.getLanguage().equalsIgnoreCase(iLanguage)) {
        final String def = getFunctionDefinition(f);
        if (def != null) {
          code.append(def);
          code.append("\n");
        }
      }
    }

    return code.length() == 0 ? null : code.toString();
  }

  public boolean existsEngine(String iLanguage) {
    if (iLanguage == null)
      return false;

    iLanguage = iLanguage.toLowerCase(Locale.ENGLISH);
    return engines.containsKey(iLanguage);
  }

  public ScriptEngine getEngine(final String iLanguage) {
    if (iLanguage == null)
      throw new OCommandScriptException("No language was specified");

    final String lang = iLanguage.toLowerCase(Locale.ENGLISH);

    final ScriptEngineFactory scriptEngineFactory = engines.get(lang);
    if (scriptEngineFactory == null)
      throw new OCommandScriptException(
          "Unsupported language: " + iLanguage + ". Supported languages are: " + getSupportedLanguages());

    return scriptEngineFactory.getScriptEngine();
  }

  /**
   * Acquires a database engine from the pool. Once finished using it, the instance MUST be returned in the pool by calling the
   * method #releaseDatabaseEngine(String, ScriptEngine).
   *
   * @param databaseName Database name
   * @param language     Script language
   * @return ScriptEngine instance with the function library already parsed
   * @see #releaseDatabaseEngine(String, String, OPartitionedObjectPool.PoolEntry)
   */
  public OPartitionedObjectPool.PoolEntry<ScriptEngine> acquireDatabaseEngine(final String databaseName, final String language) {
    ODatabaseScriptManager dbManager = dbManagers.get(databaseName);
    if (dbManager == null) {
      // CREATE A NEW DATABASE SCRIPT MANAGER
      dbManager = new ODatabaseScriptManager(this, databaseName);
      final ODatabaseScriptManager prev = dbManagers.putIfAbsent(databaseName, dbManager);
      if (prev != null) {
        dbManager.close();
        // GET PREVIOUS ONE
        dbManager = prev;
      }
    }

    return dbManager.acquireEngine(language);
  }

  /**
   * Acquires a database engine from the pool. Once finished using it, the instance MUST be returned in the pool by calling the
   * method
   *
   * @param iLanguage     Script language
   * @param iDatabaseName Database name
   * @param poolEntry     Pool entry to free
   * @see #acquireDatabaseEngine(String, String)
   */
  public void releaseDatabaseEngine(final String iLanguage, final String iDatabaseName,
      final OPartitionedObjectPool.PoolEntry<ScriptEngine> poolEntry) {
    final ODatabaseScriptManager dbManager = dbManagers.get(iDatabaseName);
    // We check if there is still a valid pool because it could be removed by the function reload
    if (dbManager != null) {
      dbManager.releaseEngine(iLanguage, poolEntry);
    }

  }

  public Iterable<String> getSupportedLanguages() {
    final HashSet<String> result = new HashSet<String>();
    result.addAll(engines.keySet());
    return result;
  }

  public Bindings bindContextVariables(ScriptEngine engine, final Bindings binding, final ODatabaseDocumentInternal db,
      final OCommandContext iContext, final Map<Object, Object> iArgs) {

    binding.put("db", new OScriptDatabaseWrapper(db));

    bindInjectors(engine, binding, db);

    bindContext(binding, iContext);

    bindParameters(binding, iArgs);

    return binding;
  }

  @Deprecated
  public Bindings bind(ScriptEngine scriptEngine, final Bindings binding, final ODatabaseDocumentInternal db,
      final OCommandContext iContext, final Map<Object, Object> iArgs) {

    bindLegacyDatabaseAndUtil(binding, db);

    bindDatabase(binding, db);

    bindInjectors(scriptEngine, binding, db);

    bindContext(binding, iContext);

    bindParameters(binding, iArgs);

    return binding;
  }

  private void bindInjectors(ScriptEngine engine, Bindings binding, ODatabaseDocument database) {
    for (OScriptInjection i : injections)
      i.bind(engine, binding, database);
  }

  private void bindContext(Bindings binding, OCommandContext iContext) {
    // BIND CONTEXT VARIABLE INTO THE SCRIPT
    if (iContext != null) {
      binding.put("ctx", iContext);
      for (Entry<String, Object> a : iContext.getVariables().entrySet()) {
        binding.put(a.getKey(), a.getValue());
      }
    }
  }

  private void bindLegacyDatabaseAndUtil(Bindings binding, ODatabaseDocumentInternal db) {
    if (db != null) {
      // BIND FIXED VARIABLES
      //      binding.put("db", new OScriptDocumentDatabaseWrapper(db));
      binding.put("orient", new OScriptOrientWrapper(db));
    }
    binding.put("util", new OFunctionUtilWrapper());
  }

  private void bindDatabase(Bindings binding, ODatabaseDocumentInternal db) {
    if (db != null) {
      binding.put("db", new OScriptDatabaseWrapper(db));
    }
  }

  private void bindParameters(Bindings binding, Map<Object, Object> iArgs) {
    // BIND PARAMETERS INTO THE SCRIPT
    if (iArgs != null) {
      for (Entry<Object, Object> a : iArgs.entrySet()) {
        binding.put(a.getKey().toString(), a.getValue());
      }

      binding.put("params", iArgs.values().toArray());
    } else
      binding.put("params", EMPTY_PARAMS);
  }

  public String throwErrorMessage(final ScriptException e, final String lib) {
    int errorLineNumber = e.getLineNumber();

    if (errorLineNumber <= 0) {
      // FIX TO RHINO: SOMETIMES HAS THE LINE NUMBER INSIDE THE TEXT :-(
      final String excMessage = e.toString();
      final int pos = excMessage.indexOf("<Unknown Source>#");
      if (pos > -1) {
        final int end = excMessage.indexOf(')', pos + "<Unknown Source>#".length());
        String lineNumberAsString = excMessage.substring(pos + "<Unknown Source>#".length(), end);
        errorLineNumber = Integer.parseInt(lineNumberAsString);
      }
    }

    if (errorLineNumber <= 0) {
      throw new OCommandScriptException(
          "Error on evaluation of the script library. Error: " + e.getMessage() + "\nScript library was:\n" + lib);
    } else {
      final StringBuilder code = new StringBuilder();
      final Scanner scanner = new Scanner(lib);
      try {
        scanner.useDelimiter("\n");
        String currentLine = null;
        String lastFunctionName = "unknown";

        for (int currentLineNumber = 1; scanner.hasNext(); currentLineNumber++) {
          currentLine = scanner.next();
          int pos = currentLine.indexOf("function");
          if (pos > -1) {
            final String[] words = OStringParser
                .getWords(currentLine.substring(Math.min(pos + "function".length() + 1, currentLine.length())), " \r\n\t");
            if (words.length > 0 && words[0] != "(")
              lastFunctionName = words[0];
          }

          if (currentLineNumber == errorLineNumber)
            // APPEND X LINES BEFORE
            code.append(String.format("%4d: >>> %s\n", currentLineNumber, currentLine));
          else if (Math.abs(currentLineNumber - errorLineNumber) <= LINES_AROUND_ERROR)
            // AROUND: APPEND IT
            code.append(String.format("%4d: %s\n", currentLineNumber, currentLine));
        }

        code.insert(0, String.format("ScriptManager: error %s.\nFunction %s:\n\n", e.getMessage(), lastFunctionName));

      } finally {
        scanner.close();
      }

      throw new OCommandScriptException(code.toString());
    }
  }

  /**
   * Unbinds variables
   *
   * @param binding
   */
  public void unbind(ScriptEngine scriptEngine, final Bindings binding, final OCommandContext iContext,
      final Map<Object, Object> iArgs) {
    for (OScriptInjection i : injections)
      i.unbind(scriptEngine, binding);

    binding.put("db", null);
    binding.put("orient", null);

    binding.put("util", null);

    binding.put("ctx", null);
    if (iContext != null) {
      for (Entry<String, Object> a : iContext.getVariables().entrySet())
        binding.put(a.getKey(), null);
    }

    if (iArgs != null) {
      for (Entry<Object, Object> a : iArgs.entrySet())
        binding.put(a.getKey().toString(), null);

    }
    binding.put("params", null);
  }

  public void registerInjection(final OScriptInjection iInj) {
    if (!injections.contains(iInj))
      injections.add(iInj);
  }

  public void unregisterInjection(final OScriptInjection iInj) {
    injections.remove(iInj);
  }

  public List<OScriptInjection> getInjections() {
    return injections;
  }

  public OScriptManager registerEngine(final String iLanguage, final ScriptEngineFactory iEngine) {
    engines.put(iLanguage, iEngine);
    return this;
  }

  public OScriptManager registerFormatter(final String iLanguage, final OScriptFormatter iFormatterImpl) {
    formatters.put(iLanguage.toLowerCase(Locale.ENGLISH), iFormatterImpl);
    return this;
  }

  public OScriptManager registerResultHandler(final String iLanguage, final OScriptResultHandler resultHandler) {
    handlers.put(iLanguage.toLowerCase(Locale.ENGLISH), resultHandler);
    return this;
  }

  public Object handleResult(String language, Object result, ScriptEngine engine, Bindings binding, ODatabaseDocument database) {
    OScriptResultHandler handler = handlers.get(language);
    if (handler != null) {
      return handler.handle(result, engine, binding, database);
    } else {
      return result;
    }
  }

  /**
   * Ask to the Script engine all the formatters
   *
   * @return Map containing all the formatters
   */
  public Map<String, OScriptFormatter> getFormatters() {
    return formatters;
  }

  /**
   * Closes the pool for a database. This is called at Orient shutdown and in case a function has been updated.
   *
   * @param iDatabaseName
   */
  public void close(final String iDatabaseName) {
    final ODatabaseScriptManager dbPool = dbManagers.remove(iDatabaseName);
    if (dbPool != null)
      dbPool.close();
  }

}