/*
 *
 *  *  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.index;

import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.concur.lock.OOneEntryPerKeyLockManager;
import com.orientechnologies.common.concur.lock.OPartitionedLockManager;
import com.orientechnologies.common.concur.lock.OReadersWriterSpinLock;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.listener.OProgressListener;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.serialization.types.OBinarySerializer;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.exception.OInvalidIndexEngineIdException;
import com.orientechnologies.orient.core.exception.OTooBigIndexKeyException;
import com.orientechnologies.orient.core.index.engine.OBaseIndexEngine;
import com.orientechnologies.orient.core.intent.OIntentMassiveInsert;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentInternal;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.cache.OReadCache;
import com.orientechnologies.orient.core.storage.cache.OWriteCache;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;
import com.orientechnologies.orient.core.storage.impl.local.paginated.atomicoperations.OAtomicOperation;
import com.orientechnologies.orient.core.storage.impl.local.paginated.atomicoperations.OAtomicOperationsManager;
import com.orientechnologies.orient.core.storage.ridbag.sbtree.OIndexRIDContainer;
import com.orientechnologies.orient.core.tx.OTransactionIndexChanges;
import com.orientechnologies.orient.core.tx.OTransactionIndexChangesPerKey;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Handles indexing when records change. The underlying lock manager for keys can be the {@link OPartitionedLockManager}, the
 * default one, or the {@link OOneEntryPerKeyLockManager} in case of distributed. This is to avoid deadlock situation between nodes
 * where keys have the same hash code.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public abstract class OIndexAbstract<T> implements OIndexInternal<T> {

  protected static final String                    CONFIG_MAP_RID  = "mapRid";
  private static final   String                    CONFIG_CLUSTERS = "clusters";
  final                  String                    type;
  protected final        ODocument                 metadata;
  protected final        OAbstractPaginatedStorage storage;
  private final          String                    databaseName;
  private final          String                    name;
  private final          OReadersWriterSpinLock    rwLock          = new OReadersWriterSpinLock();
  private final          AtomicLong                rebuildVersion  = new AtomicLong();
  private final          int                       version;
  volatile               IndexConfiguration        configuration;
  String valueContainerAlgorithm;

  protected int indexId    = -1;
  protected int apiVersion = -1;

  protected        Set<String>         clustersToIndex  = new HashSet<>();
  private          String              algorithm;
  private volatile OIndexDefinition    indexDefinition;
  private volatile boolean             rebuilding       = false;
  private          Map<String, String> engineProperties = new HashMap<>();
  final            int                 binaryFormatVersion;

  public OIndexAbstract(String name, final String type, final String algorithm, final String valueContainerAlgorithm,
      final ODocument metadata, final int version, final OStorage storage, int binaryFormatVersion) {
    this.binaryFormatVersion = binaryFormatVersion;
    acquireExclusiveLock();
    try {
      databaseName = storage.getName();

      this.version = version;
      this.name = name;
      this.type = type;
      this.algorithm = algorithm;
      this.metadata = metadata;
      this.valueContainerAlgorithm = valueContainerAlgorithm;
      this.storage = (OAbstractPaginatedStorage) storage.getUnderlying();
    } finally {
      releaseExclusiveLock();
    }
  }

  public static OIndexMetadata loadMetadataInternal(final ODocument config, final String type, final String algorithm,
      final String valueContainerAlgorithm) {
    final String indexName = config.field(OIndexInternal.CONFIG_NAME);

    final ODocument indexDefinitionDoc = config.field(OIndexInternal.INDEX_DEFINITION);
    OIndexDefinition loadedIndexDefinition = null;
    if (indexDefinitionDoc != null) {
      try {
        final String indexDefClassName = config.field(OIndexInternal.INDEX_DEFINITION_CLASS);
        final Class<?> indexDefClass = Class.forName(indexDefClassName);
        loadedIndexDefinition = (OIndexDefinition) indexDefClass.getDeclaredConstructor().newInstance();
        loadedIndexDefinition.fromStream(indexDefinitionDoc);

      } catch (final ClassNotFoundException | IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
        throw OException.wrapException(new OIndexException("Error during deserialization of index definition"), e);
      }
    } else {
      // @COMPATIBILITY 1.0rc6 new index model was implemented
      final Boolean isAutomatic = config.field(OIndexInternal.CONFIG_AUTOMATIC);
      OIndexFactory factory = OIndexes.getFactory(type, algorithm);
      if (Boolean.TRUE.equals(isAutomatic)) {
        final int pos = indexName.lastIndexOf('.');
        if (pos < 0)
          throw new OIndexException(
              "Cannot convert from old index model to new one. " + "Invalid index name. Dot (.) separator should be present");
        final String className = indexName.substring(0, pos);
        final String propertyName = indexName.substring(pos + 1);

        final String keyTypeStr = config.field(OIndexInternal.CONFIG_KEYTYPE);
        if (keyTypeStr == null)
          throw new OIndexException("Cannot convert from old index model to new one. " + "Index key type is absent");
        final OType keyType = OType.valueOf(keyTypeStr.toUpperCase(Locale.ENGLISH));

        loadedIndexDefinition = new OPropertyIndexDefinition(className, propertyName, keyType);

        config.removeField(OIndexInternal.CONFIG_AUTOMATIC);
        config.removeField(OIndexInternal.CONFIG_KEYTYPE);
      } else if (config.field(OIndexInternal.CONFIG_KEYTYPE) != null) {
        final String keyTypeStr = config.field(OIndexInternal.CONFIG_KEYTYPE);
        final OType keyType = OType.valueOf(keyTypeStr.toUpperCase(Locale.ENGLISH));

        loadedIndexDefinition = new OSimpleKeyIndexDefinition(keyType);

        config.removeField(OIndexInternal.CONFIG_KEYTYPE);
      }
    }

    final Set<String> clusters = new HashSet<>(config.field(CONFIG_CLUSTERS, OType.EMBEDDEDSET));

    return new OIndexMetadata(indexName, loadedIndexDefinition, clusters, type, algorithm, valueContainerAlgorithm);
  }

  public void flush() {
  }

  @Override
  public boolean hasRangeQuerySupport() {

    acquireSharedLock();
    try {
      while (true)
        try {
          return storage.hasIndexRangeQuerySupport(indexId);
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }
    } finally {
      releaseSharedLock();
    }

  }

  /**
   * Creates the index.
   *
   * @param clusterIndexName Cluster name where to place the TreeMap
   */
  public OIndexInternal<?> create(final OIndexDefinition indexDefinition, final String clusterIndexName,
      final Set<String> clustersToIndex, boolean rebuild, final OProgressListener progressListener,
      final OBinarySerializer valueSerializer) {
    acquireExclusiveLock();
    try {
      configuration = indexConfigurationInstance(new ODocument().setTrackingChanges(false));

      this.indexDefinition = indexDefinition;

      if (clustersToIndex != null)
        this.clustersToIndex = new HashSet<>(clustersToIndex);
      else
        this.clustersToIndex = new HashSet<>();

      // do not remove this, it is needed to remove index garbage if such one exists
      try {
        if (apiVersion == 0) {
          removeValuesContainer();
        }
      } catch (Exception e) {
        OLogManager.instance().error(this, "Error during deletion of index '%s'", e, name);
      }

      indexId = storage.addIndexEngine(name, algorithm, type, indexDefinition, valueSerializer, isAutomatic(), true, version, 1,
          this instanceof OIndexMultiValues, getEngineProperties(), clustersToIndex, metadata);
      apiVersion = OAbstractPaginatedStorage.extractEngineAPIVersion(indexId);

      assert indexId >= 0;
      assert apiVersion >= 0;

      onIndexEngineChange(indexId);

      if (rebuild)
        fillIndex(progressListener, false);

      updateConfiguration();
    } catch (Exception e) {
      OLogManager.instance().error(this, "Exception during index '%s' creation", e, name);

      while (true)
        try {
          if (indexId >= 0)
            storage.deleteIndexEngine(indexId);
          break;
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        } catch (Exception ex) {
          OLogManager.instance().error(this, "Exception during index '%s' deletion", ex, name);
        }

      if (e instanceof OIndexException)
        throw (OIndexException) e;

      throw OException.wrapException(new OIndexException("Cannot create the index '" + name + "'"), e);

    } finally {
      releaseExclusiveLock();
    }

    return this;
  }

  protected void doReloadIndexEngine() {
    indexId = storage.loadIndexEngine(name);
    apiVersion = OAbstractPaginatedStorage.extractEngineAPIVersion(indexId);

    if (indexId < 0) {
      throw new IllegalStateException("Index " + name + " can not be loaded");
    }
  }

  public long count(final Object iKey) {
    final Object result = get(iKey);
    if (result == null)
      return 0;
    else if (OMultiValue.isMultiValue(result))
      return OMultiValue.getSize(result);
    return 1;
  }

  public boolean loadFromConfiguration(final ODocument config) {
    acquireExclusiveLock();
    try {
      configuration = indexConfigurationInstance(config);
      clustersToIndex.clear();

      final OIndexMetadata indexMetadata = loadMetadata(config);
      indexDefinition = indexMetadata.getIndexDefinition();
      clustersToIndex.addAll(indexMetadata.getClustersToIndex());
      algorithm = indexMetadata.getAlgorithm();
      valueContainerAlgorithm = indexMetadata.getValueContainerAlgorithm();

      try {
        indexId = storage.loadIndexEngine(name);
        apiVersion = OAbstractPaginatedStorage.extractEngineAPIVersion(indexId);

        if (indexId == -1) {
          indexId = storage
              .loadExternalIndexEngine(name, algorithm, type, indexDefinition, determineValueSerializer(), isAutomatic(), true,
                  version, 1, this instanceof OIndexMultiValues, getEngineProperties());
          apiVersion = OAbstractPaginatedStorage.extractEngineAPIVersion(indexId);
        }

        if (indexId == -1) {
          return false;
        }

        onIndexEngineChange(indexId);

      } catch (Exception e) {
        OLogManager.instance().error(this, "Error during load of index '%s'", e, name != null ? name : "null");

        if (isAutomatic()) {
          // AUTOMATIC REBUILD IT
          OLogManager.instance().warn(this, "Cannot load index '%s' rebuilt it from scratch", getName());
          try {
            rebuild();
          } catch (Exception t) {
            OLogManager.instance()
                .error(this, "Cannot rebuild index '%s' because '" + t + "'. The index will be removed in configuration", e,
                    getName());
            // REMOVE IT
            return false;
          }
        }
      }

      return true;
    } finally {
      releaseExclusiveLock();
    }
  }

  private Map<String, String> getEngineProperties() {
    return engineProperties;
  }

  @Override
  public OIndexMetadata loadMetadata(final ODocument config) {
    return loadMetadataInternal(config, type, algorithm, valueContainerAlgorithm);
  }

  public boolean contains(Object key) {
    key = getCollatingValue(key);

    acquireSharedLock();
    try {
      assert indexId >= 0;

      while (true)
        try {
          return storage.indexContainsKey(indexId, key);
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }
    } finally {
      releaseSharedLock();
    }
  }

  /**
   * {@inheritDoc}
   */
  public long rebuild() {
    return rebuild(new OIndexRebuildOutputListener(this));
  }

  @Override
  public void setRebuildingFlag() {
    rebuilding = true;
  }

  @Override
  public void close() {

  }

  @Override
  public Object getFirstKey() {
    acquireSharedLock();
    try {
      while (true)
        try {
          return storage.getIndexFirstKey(indexId);
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }
    } finally {
      releaseSharedLock();
    }
  }

  @Override
  public Object getLastKey() {
    acquireSharedLock();
    try {
      while (true)
        try {
          return storage.getIndexLastKey(indexId);
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }
    } finally {
      releaseSharedLock();
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public long getRebuildVersion() {
    return 0;
  }

  /**
   * {@inheritDoc}
   */
  public long rebuild(final OProgressListener iProgressListener) {
    long documentIndexed;

    final boolean intentInstalled = getDatabase().declareIntent(new OIntentMassiveInsert());

    acquireExclusiveLock();
    try {
      // DO NOT REORDER 2 assignments bellow
      // see #getRebuildVersion()
      rebuilding = true;
      rebuildVersion.incrementAndGet();

      try {
        if (indexId >= 0) {
          storage.deleteIndexEngine(indexId);
        }
      } catch (Exception e) {
        OLogManager.instance().error(this, "Error during index '%s' delete", e, name);
      }

      removeValuesContainer();

      indexId = storage
          .addIndexEngine(name, algorithm, type, indexDefinition, determineValueSerializer(), isAutomatic(), true, version, 1,
              this instanceof OIndexMultiValues, getEngineProperties(), clustersToIndex, metadata);
      apiVersion = OAbstractPaginatedStorage.extractEngineAPIVersion(indexId);

      onIndexEngineChange(indexId);
    } catch (Exception e) {
      try {
        if (indexId >= 0)
          storage.clearIndex(indexId);
      } catch (Exception e2) {
        OLogManager.instance().error(this, "Error during index rebuild", e2);
        // IGNORE EXCEPTION: IF THE REBUILD WAS LAUNCHED IN CASE OF RID INVALID CLEAR ALWAYS GOES IN ERROR
      }

      rebuilding = false;
      throw OException.wrapException(new OIndexException("Error on rebuilding the index for clusters: " + clustersToIndex), e);
    } finally {
      releaseExclusiveLock();
    }

    acquireSharedLock();
    try {
      documentIndexed = fillIndex(iProgressListener, true);
    } catch (final Exception e) {
      OLogManager.instance().error(this, "Error during index rebuild", e);
      try {
        if (indexId >= 0)
          storage.clearIndex(indexId);
      } catch (Exception e2) {
        OLogManager.instance().error(this, "Error during index rebuild", e2);
        // IGNORE EXCEPTION: IF THE REBUILD WAS LAUNCHED IN CASE OF RID INVALID CLEAR ALWAYS GOES IN ERROR
      }

      throw OException.wrapException(new OIndexException("Error on rebuilding the index for clusters: " + clustersToIndex), e);
    } finally {
      rebuilding = false;

      if (intentInstalled)
        getDatabase().declareIntent(null);

      releaseSharedLock();
    }

    return documentIndexed;
  }

  private long fillIndex(final OProgressListener iProgressListener, final boolean rebuild) {
    long documentIndexed = 0;
    try {
      long documentNum = 0;
      long documentTotal = 0;

      for (final String cluster : clustersToIndex)
        documentTotal += storage.count(storage.getClusterIdByName(cluster));

      if (iProgressListener != null)
        iProgressListener.onBegin(this, documentTotal, rebuild);

      // INDEX ALL CLUSTERS
      for (final String clusterName : clustersToIndex) {
        final long[] metrics = indexCluster(clusterName, iProgressListener, documentNum, documentIndexed, documentTotal);
        documentNum = metrics[0];
        documentIndexed = metrics[1];
      }

      if (iProgressListener != null)
        iProgressListener.onCompletition(this, true);
    } catch (final RuntimeException e) {
      if (iProgressListener != null)
        iProgressListener.onCompletition(this, false);
      throw e;
    }
    return documentIndexed;
  }

  public boolean remove(Object key, final OIdentifiable value) {
    return remove(key);
  }

  public boolean remove(Object key) {
    key = getCollatingValue(key);

    acquireSharedLock();
    try {
      while (true)
        try {
          return storage.removeKeyFromIndex(indexId, key);
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }
    } finally {
      releaseSharedLock();
    }
  }

  public OIndex<T> clear() {
    acquireSharedLock();
    try {
      while (true)
        try {
          storage.clearIndex(indexId);
          break;
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }
      return this;
    } finally {
      releaseSharedLock();
    }
  }

  public OIndexInternal<T> delete() {
    acquireExclusiveLock();

    try {
      while (true)
        try {
          storage.deleteIndexEngine(indexId);
          break;
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }

      // REMOVE THE INDEX ALSO FROM CLASS MAP
      if (getDatabase().getMetadata() != null)
        //noinspection deprecation
        getDatabase().getMetadata().getIndexManager().removeClassPropertyIndex(this);

      removeValuesContainer();
      return this;
    } finally {
      releaseExclusiveLock();
    }
  }

  public String getName() {
    return name;
  }

  public String getType() {
    return type;
  }

  @Override
  public void setType(OType type) {
    indexDefinition = new OSimpleKeyIndexDefinition(type);
    updateConfiguration();
  }

  @Override
  public String getAlgorithm() {
    acquireSharedLock();
    try {
      return algorithm;
    } finally {
      releaseSharedLock();
    }
  }

  @Override
  public String toString() {
    acquireSharedLock();
    try {
      return name;
    } finally {
      releaseSharedLock();
    }
  }

  public OIndexInternal<T> getInternal() {
    return this;
  }

  public Set<String> getClusters() {
    acquireSharedLock();
    try {
      return Collections.unmodifiableSet(clustersToIndex);
    } finally {
      releaseSharedLock();
    }
  }

  public OIndexAbstract<T> addCluster(final String clusterName) {
    acquireExclusiveLock();
    try {
      if (clustersToIndex.add(clusterName)) {
        updateConfiguration();

        // INDEX SINGLE CLUSTER
        indexCluster(clusterName, null, 0, 0, 0);
      }

      return this;
    } finally {
      releaseExclusiveLock();
    }
  }

  public OIndexAbstract<T> removeCluster(String iClusterName) {
    acquireExclusiveLock();
    try {
      if (clustersToIndex.remove(iClusterName)) {
        updateConfiguration();
        rebuild();
      }

      return this;
    } finally {
      releaseExclusiveLock();
    }
  }

  @Override
  public int getVersion() {
    return version;
  }

  public ODocument updateConfiguration() {
    configuration.updateConfiguration(type, name, version, indexDefinition, clustersToIndex, algorithm, valueContainerAlgorithm);
    if (metadata != null)
      configuration.document.field(OIndexInternal.METADATA, metadata, OType.EMBEDDED);
    return configuration.getDocument();
  }

  public void addTxOperation(IndexTxSnapshot snapshots, final OTransactionIndexChanges changes) {
    acquireSharedLock();
    try {
      if (changes.cleared)
        clearSnapshot(snapshots);
      final Map<Object, Object> snapshot = snapshots.indexSnapshot;
      for (final OTransactionIndexChangesPerKey entry : changes.changesPerKey.values()) {
        applyIndexTxEntry(snapshot, entry);
      }
      applyIndexTxEntry(snapshot, changes.nullKeyChanges);

    } finally {
      releaseSharedLock();
    }
  }

  /**
   * Interprets transaction index changes for a certain key. Override it to customize index behaviour on interpreting index changes.
   * This may be viewed as an optimization, but in some cases this is a requirement. For example, if you put multiple values under
   * the same key during the transaction for single-valued/unique index, but remove all of them except one before commit, there is
   * no point in throwing {@link com.orientechnologies.orient.core.storage.ORecordDuplicatedException} while applying index changes.
   *
   * @param changes the changes to interpret.
   *
   * @return the interpreted index key changes.
   */
  protected Iterable<OTransactionIndexChangesPerKey.OTransactionIndexEntry> interpretTxKeyChanges(
      OTransactionIndexChangesPerKey changes) {
    return changes.entries;
  }

  private void applyIndexTxEntry(Map<Object, Object> snapshot, OTransactionIndexChangesPerKey entry) {
    for (OTransactionIndexChangesPerKey.OTransactionIndexEntry op : interpretTxKeyChanges(entry)) {
      switch (op.operation) {
      case PUT:
        putInSnapshot(entry.key, op.value, snapshot);
        break;
      case REMOVE:
        if (op.value != null)
          removeFromSnapshot(entry.key, op.value, snapshot);
        else
          removeFromSnapshot(entry.key, snapshot);
        break;
      case CLEAR:
        // SHOULD NEVER BE THE CASE HANDLE BY cleared FLAG
        break;
      }
    }
  }

  public void commit(IndexTxSnapshot snapshots) {
    acquireSharedLock();
    try {
      if (snapshots.clear)
        clear();

      commitSnapshot(snapshots.indexSnapshot);
    } finally {
      releaseSharedLock();
    }
  }

  public void preCommit(IndexTxSnapshot snapshots) {
  }

  public void postCommit(IndexTxSnapshot snapshots) {
  }

  public ODocument getConfiguration() {
    return configuration.getDocument();
  }

  @Override
  public ODocument getMetadata() {
    return metadata;
  }

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

  public boolean isAutomatic() {
    acquireSharedLock();
    try {
      return indexDefinition != null && indexDefinition.getClassName() != null;
    } finally {
      releaseSharedLock();
    }
  }

  public OType[] getKeyTypes() {
    acquireSharedLock();
    try {
      if (indexDefinition == null)
        return null;

      return indexDefinition.getTypes();
    } finally {
      releaseSharedLock();
    }
  }

  @Override
  public OIndexKeyCursor keyCursor() {
    acquireSharedLock();
    try {
      while (true)
        try {
          return storage.getIndexKeyCursor(indexId);
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }
    } finally {
      releaseSharedLock();
    }
  }

  public OIndexDefinition getDefinition() {
    return indexDefinition;
  }

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

      final OIndexAbstract<?> that = (OIndexAbstract<?>) o;

      return name.equals(that.name);
    } finally {
      releaseSharedLock();
    }
  }

  @Override
  public int hashCode() {
    acquireSharedLock();
    try {
      return name.hashCode();
    } finally {
      releaseSharedLock();
    }
  }

  public int getIndexId() {
    return indexId;
  }

  public String getDatabaseName() {
    return databaseName;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isRebuilding() {
    return rebuilding;
  }

  protected abstract OBinarySerializer determineValueSerializer();

  private void populateIndex(ODocument doc, Object fieldValue) {
    if (fieldValue instanceof Collection) {
      for (final Object fieldValueItem : (Collection<?>) fieldValue) {
        put(fieldValueItem, doc);
      }
    } else
      put(fieldValue, doc);
  }

  public Object getCollatingValue(final Object key) {
    if (key != null && getDefinition() != null)
      return getDefinition().getCollate().transform(key);
    return key;
  }

  protected void commitSnapshot(Map<Object, Object> snapshot) {
    // do nothing by default
    // storage will delay real operations till the end of tx
  }

  protected void putInSnapshot(Object key, OIdentifiable value, Map<Object, Object> snapshot) {
    // storage will delay real operations till the end of tx
    put(key, value);
  }

  protected void removeFromSnapshot(Object key, OIdentifiable value, Map<Object, Object> snapshot) {
    // storage will delay real operations till the end of tx
    remove(key, value);
  }

  private void removeFromSnapshot(Object key, Map<Object, Object> snapshot) {
    // storage will delay real operations till the end of tx
    remove(key);
  }

  protected void clearSnapshot(IndexTxSnapshot indexTxSnapshot) {
    // storage will delay real operations till the end of tx
    clear();
  }

  @Override
  public int compareTo(OIndex<T> index) {
    acquireSharedLock();
    try {
      final String name = index.getName();
      return this.name.compareTo(name);
    } finally {
      releaseSharedLock();
    }
  }

  @Override
  public String getIndexNameByKey(final Object key) {
    OBaseIndexEngine engine;

    while (true) {
      try {
        engine = storage.getIndexEngine(indexId);
        break;
      } catch (OInvalidIndexEngineIdException ignore) {
        doReloadIndexEngine();
      }
    }
    return engine.getIndexNameByKey(key);
  }

  @Override
  public boolean acquireAtomicExclusiveLock(Object key) {
    OBaseIndexEngine engine;

    while (true) {
      try {
        engine = storage.getIndexEngine(indexId);
        break;
      } catch (OInvalidIndexEngineIdException ignore) {
        doReloadIndexEngine();
      }
    }

    return engine.acquireAtomicExclusiveLock(key);
  }

  protected static ODatabaseDocumentInternal getDatabase() {
    return ODatabaseRecordThreadLocal.instance().get();
  }

  private long[] indexCluster(final String clusterName, final OProgressListener iProgressListener, long documentNum,
      long documentIndexed, long documentTotal) {
    try {
      for (final ORecord record : getDatabase().browseCluster(clusterName)) {
        if (Thread.interrupted())
          throw new OCommandExecutionException("The index rebuild has been interrupted");

        if (record instanceof ODocument) {
          final ODocument doc = (ODocument) record;

          if (indexDefinition == null)
            throw new OConfigurationException(
                "Index '" + name + "' cannot be rebuilt because has no a valid definition (" + indexDefinition + ")");

          final Object fieldValue = indexDefinition.getDocumentValueToIndex(doc);

          if (fieldValue != null || !indexDefinition.isNullValuesIgnored()) {
            try {
              populateIndex(doc, fieldValue);
            } catch (OTooBigIndexKeyException | OIndexException e) {
              OLogManager.instance().error(this,
                  "Exception during index rebuild. Exception was caused by following key/ value pair - key %s, value %s."
                      + " Rebuild will continue from this point", e, fieldValue, doc.getIdentity());
            }

            ++documentIndexed;
          }
        }
        documentNum++;

        if (iProgressListener != null)
          iProgressListener.onProgress(this, documentNum, (float) (documentNum * 100.0 / documentTotal));
      }
    } catch (NoSuchElementException ignore) {
      // END OF CLUSTER REACHED, IGNORE IT
    }

    return new long[] { documentNum, documentIndexed };
  }

  protected void releaseExclusiveLock() {
    rwLock.releaseWriteLock();
  }

  protected void acquireExclusiveLock() {
    rwLock.acquireWriteLock();
  }

  protected void releaseSharedLock() {
    rwLock.releaseReadLock();
  }

  protected void acquireSharedLock() {
    rwLock.acquireReadLock();
  }

  private void removeValuesContainer() {
    if (valueContainerAlgorithm.equals(ODefaultIndexFactory.SBTREE_BONSAI_VALUE_CONTAINER)) {

      final OAtomicOperation atomicOperation = OAtomicOperationsManager.getCurrentOperation();

      final OReadCache readCache = storage.getReadCache();
      final OWriteCache writeCache = storage.getWriteCache();

      if (atomicOperation == null) {
        try {
          final String fileName = getName() + OIndexRIDContainer.INDEX_FILE_EXTENSION;
          if (writeCache.exists(fileName)) {
            final long fileId = writeCache.loadFile(fileName);
            readCache.deleteFile(fileId, writeCache);
          }
        } catch (IOException e) {
          OLogManager.instance().error(this, "Cannot delete file for value containers", e);
        }
      } else {
        try {
          final String fileName = getName() + OIndexRIDContainer.INDEX_FILE_EXTENSION;
          if (atomicOperation.isFileExists(fileName)) {
            final long fileId = atomicOperation.loadFile(fileName);
            atomicOperation.deleteFile(fileId);
          }
        } catch (IOException e) {
          OLogManager.instance().error(this, "Cannot delete file for value containers", e);
        }
      }

    }
  }

  protected void onIndexEngineChange(final int indexId) {
    while (true)
      try {
        storage.callIndexEngine(false, false, indexId, engine -> {
          engine.init(getName(), getType(), getDefinition(), isAutomatic(), getMetadata());
          return null;
        });
        break;
      } catch (OInvalidIndexEngineIdException ignore) {
        doReloadIndexEngine();
      }
  }

  IndexConfiguration indexConfigurationInstance(final ODocument document) {
    return new IndexConfiguration(document);
  }

  public static final class IndexTxSnapshot {
    public Map<Object, Object> indexSnapshot = new HashMap<>();
    public boolean             clear         = false;
  }

  protected static class IndexConfiguration {
    final ODocument document;

    public IndexConfiguration(ODocument document) {
      this.document = document;
    }

    ODocument getDocument() {
      return document;
    }

    synchronized void updateConfiguration(String type, String name, int version, OIndexDefinition indexDefinition,
        Set<String> clustersToIndex, String algorithm, String valueContainerAlgorithm) {
      document.field(OIndexInternal.CONFIG_TYPE, type);
      document.field(OIndexInternal.CONFIG_NAME, name);
      document.field(OIndexInternal.INDEX_VERSION, version);

      if (indexDefinition != null) {

        final ODocument indexDefDocument = indexDefinition.toStream();
        if (!indexDefDocument.hasOwners())
          ODocumentInternal.addOwner(indexDefDocument, document);

        document.field(OIndexInternal.INDEX_DEFINITION, indexDefDocument, OType.EMBEDDED);
        document.field(OIndexInternal.INDEX_DEFINITION_CLASS, indexDefinition.getClass().getName());
      } else {
        document.removeField(OIndexInternal.INDEX_DEFINITION);
        document.removeField(OIndexInternal.INDEX_DEFINITION_CLASS);
      }

      document.field(CONFIG_CLUSTERS, clustersToIndex, OType.EMBEDDEDSET);
      document.field(ALGORITHM, algorithm);
      document.field(VALUE_CONTAINER_ALGORITHM, valueContainerAlgorithm);

    }
  }
}


/*
 *
 *  *  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.tinkerpop.blueprints.impls.orient;

import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;
import com.orientechnologies.orient.core.tx.OTransaction;
import com.tinkerpop.blueprints.*;
import com.tinkerpop.blueprints.util.DefaultGraphQuery;

import java.util.*;

/**
 * OrientDB implementation for Graph query.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com) (http://orientdb.com)
 */
public class OrientGraphQuery extends DefaultGraphQuery {

  protected static final char   SPACE              = ' ';
  protected static final String OPERATOR_DIFFERENT = "<>";
  protected static final String OPERATOR_NOT       = "not ";
  protected static final String OPERATOR_IS_NOT    = "is not";
  protected static final String OPERATOR_LET       = "<=";
  protected static final char   OPERATOR_LT        = '<';
  protected static final String OPERATOR_GTE       = ">=";
  protected static final char   OPERATOR_GT        = '>';
  protected static final String OPERATOR_EQUALS    = "=";
  protected static final String OPERATOR_IS        = "is";
  protected static final String OPERATOR_IN        = " in ";
  protected static final String OPERATOR_LIKE      = " like ";

  protected static final String QUERY_FILTER_AND  = " and ";
  protected static final String QUERY_FILTER_OR   = " or ";
  protected static final char   QUERY_STRING      = '\'';
  protected static final char   QUERY_SEPARATOR   = ',';
  protected static final char   COLLECTION_BEGIN  = '[';
  protected static final char   COLLECTION_END    = ']';
  protected static final char   PARENTHESIS_BEGIN = '(';
  protected static final char   PARENTHESIS_END   = ')';
  protected static final String QUERY_LABEL_BEGIN = " label in [";
  protected static final String QUERY_LABEL_END   = "]";
  protected static final String QUERY_WHERE       = " where ";
  protected static final String QUERY_SELECT_FROM = "select from ";
  protected static final String SKIP              = " SKIP ";
  protected static final String LIMIT             = " LIMIT ";
  protected static final String ORDERBY           = " ORDER BY ";
  public                 int    skip              = 0;
  public                 String orderBy           = "";
  public                 String orderByDir        = "desc";
  protected String fetchPlan;

  public class OrientGraphQueryIterable<T extends Element> extends DefaultGraphQueryIterable<T> {
    public OrientGraphQueryIterable(final boolean forVertex, final String[] labels) {
      super(forVertex);

      if (labels != null && labels.length > 0)
        // TREAT CLASS AS LABEL

        has("_class", Contains.IN, Arrays.asList(labels));
    }

    protected Set<String> getIndexedKeys(final Class<? extends Element> elementClass) {
      return ((OrientBaseGraph) graph).getIndexedKeys(elementClass, true);
    }

  }

  protected OrientGraphQuery(final Graph iGraph) {
    super(iGraph);
  }

  /**
   * (Blueprints Extension) Sets the labels to filter. Labels are bound to Class names by default.
   *
   * @param labels String vararg of labels
   *
   * @return Current Query Object to allow calls in chain.
   */
  public Query labels(final String... labels) {
    this.labels = labels;
    return this;
  }

  /**
   * Skips first iSkip items from the result set.
   *
   * @param iSkip Number of items to skip on result set
   *
   * @return Current Query Object to allow calls in chain.
   */
  public Query skip(final int iSkip) {
    this.skip = iSkip;
    return this;
  }

  /**
   * (Blueprints Extension) Sets the order of results by a field in ascending (asc) order. This is translated on ORDER BY in the
   * underlying SQL query.
   *
   * @param props Field to order by
   *
   * @return Current Query Object to allow calls in chain.
   */
  public Query order(final String props) {
    this.order(props, orderByDir);
    return this;
  }

  /**
   * (Blueprints Extension) Sets the order of results by a field in ascending (asc) or descending (desc) order based on dir
   * parameter. This is translated on ORDER BY in the underlying SQL query.
   *
   * @param props Field to order by
   * @param dir   Direction. Use "asc" for ascending and "desc" for descending
   *
   * @return Current Query Object to allow calls in chain.
   */
  public Query order(final String props, final String dir) {
    this.orderBy = props;
    this.orderByDir = dir;
    return this;
  }

  /**
   * Returns the result set of the query as iterable vertices.
   */
  @Override
  public Iterable<Vertex> vertices() {
    if (limit == 0)
      return Collections.emptyList();

    OTransaction transaction = ((OrientBaseGraph) graph).getRawGraph().getTransaction();
    if (transaction.isActive() && transaction.getEntryCount() > 0 || hasCustomPredicate()) {
      // INSIDE TRANSACTION QUERY DOESN'T SEE IN MEMORY CHANGES, UNTIL
      // SUPPORTED USED THE BASIC IMPL
      String[] classes = allSubClassesLabels();
      return new OrientGraphQueryIterable<Vertex>(true, classes);
    }
    final StringBuilder text = new StringBuilder(512);

    // GO DIRECTLY AGAINST E CLASS AND SUB-CLASSES
    text.append(QUERY_SELECT_FROM);

    if (((OrientBaseGraph) graph).isUseClassForVertexLabel() && labels != null && labels.length > 0) {
      // FILTER PER CLASS SAVING CHECKING OF LABEL PROPERTY
      if (labels.length == 1)
        // USE THE CLASS NAME
        text.append(OrientBaseGraph.encodeClassName(labels[0]));
      else {
        // MULTIPLE CLASSES NOT SUPPORTED DIRECTLY: CREATE A SUB-QUERY
        String[] classes = allSubClassesLabels();
        return new OrientGraphQueryIterable<Vertex>(true, classes);
      }
    } else
      text.append(OrientVertexType.CLASS_NAME);

    final List<Object> queryParams = manageFilters(text);
    if (!((OrientBaseGraph) graph).isUseClassForVertexLabel())
      manageLabels(queryParams.size() > 0, text);

    if (orderBy.length() > 1) {
      text.append(ORDERBY);
      text.append(orderBy);
      text.append(" ").append(orderByDir).append(" ");
    }
    if (skip > 0 && skip < Integer.MAX_VALUE) {
      text.append(SKIP);
      text.append(skip);
    }

    if (limit > 0 && limit < Integer.MAX_VALUE) {
      text.append(LIMIT);
      text.append(limit);
    }

    final OSQLSynchQuery<OIdentifiable> query = new OSQLSynchQuery<OIdentifiable>(text.toString());

    if (fetchPlan != null)
      query.setFetchPlan(fetchPlan);

    return new OrientElementIterable<Vertex>(((OrientBaseGraph) graph),
        ((OrientBaseGraph) graph).getRawGraph().query(query, queryParams.toArray()));
  }

  private String[] allSubClassesLabels() {

    String[] classes = null;

    if (labels != null && labels.length > 0) {
      List<String> tmpClasses = new ArrayList<String>();
      for (String label : labels) {
        OrientVertexType vertexType = ((OrientBaseGraph) graph).getVertexType(label);
        tmpClasses.add(vertexType.getName());
        Collection<OClass> allSubclasses = vertexType.getAllSubclasses();
        for (OClass klass : allSubclasses) {
          tmpClasses.add(klass.getName());
        }
      }
      classes = tmpClasses.toArray(new String[tmpClasses.size()]);
    }

    return classes;
  }

  /**
   * Returns the result set of the query as iterable edges.
   */
  @Override
  public Iterable<Edge> edges() {
    if (limit == 0)
      return Collections.emptyList();

    if (((OrientBaseGraph) graph).getRawGraph().getTransaction().isActive() || hasCustomPredicate())
      // INSIDE TRANSACTION QUERY DOESN'T SEE IN MEMORY CHANGES, UNTIL
      // SUPPORTED USED THE BASIC IMPL
      return new OrientGraphQueryIterable<Edge>(false, labels);

    if (((OrientBaseGraph) graph).isUseLightweightEdges())
      return new OrientGraphQueryIterable<Edge>(false, labels);

    final StringBuilder text = new StringBuilder(512);

    // GO DIRECTLY AGAINST E CLASS AND SUB-CLASSES
    text.append(QUERY_SELECT_FROM);

    if (((OrientBaseGraph) graph).isUseClassForEdgeLabel() && labels != null && labels.length > 0) {
      // FILTER PER CLASS SAVING CHECKING OF LABEL PROPERTY
      if (labels.length == 1)
        // USE THE CLASS NAME
        text.append(OrientBaseGraph.encodeClassName(labels[0]));
      else {
        // MULTIPLE CLASSES NOT SUPPORTED DIRECTLY: CREATE A SUB-QUERY
        return new OrientGraphQueryIterable<Edge>(false, labels);
      }
    } else
      text.append(OrientEdgeType.CLASS_NAME);

    List<Object> queryParams = manageFilters(text);
    if (!((OrientBaseGraph) graph).isUseClassForEdgeLabel())
      manageLabels(queryParams.size() > 0, text);

    final OSQLSynchQuery<OIdentifiable> query = new OSQLSynchQuery<OIdentifiable>(text.toString());

    if (fetchPlan != null)
      query.setFetchPlan(fetchPlan);

    if (limit > 0 && limit < Integer.MAX_VALUE)
      query.setLimit(limit);

    return new OrientElementIterable<Edge>(((OrientBaseGraph) graph),
        ((OrientBaseGraph) graph).getRawGraph().query(query, queryParams.toArray()));
  }

  /**
   * (Blueprints Extension) Returns the fetch plan used.
   */
  public String getFetchPlan() {
    return fetchPlan;
  }

  /**
   * (Blueprints Extension) Sets the fetch plan to use on returning result set.
   */
  public void setFetchPlan(final String fetchPlan) {
    this.fetchPlan = fetchPlan;
  }

  protected void manageLabels(final boolean usedWhere, final StringBuilder text) {
    if (labels != null && labels.length > 0) {

      if (!usedWhere) {
        // APPEND WHERE
        text.append(QUERY_WHERE);
      } else
        text.append(QUERY_FILTER_AND);

      text.append(QUERY_LABEL_BEGIN);
      for (int i = 0; i < labels.length; ++i) {
        if (i > 0)
          text.append(QUERY_SEPARATOR);
        text.append(QUERY_STRING);
        text.append(labels[i]);
        text.append(QUERY_STRING);
      }
      text.append(QUERY_LABEL_END);
    }
  }

  protected boolean hasCustomPredicate() {
    for (HasContainer has : hasContainers) {
      if (!(has.predicate instanceof Contains) && !(has.predicate instanceof com.tinkerpop.blueprints.Compare))
        return true;
    }
    return false;
  }

  @SuppressWarnings("unchecked")
  protected List<Object> manageFilters(final StringBuilder text) {
    boolean firstPredicate = true;
    List<Object> params = new ArrayList<Object>();
    for (HasContainer has : hasContainers) {
      if (!firstPredicate)
        text.append(QUERY_FILTER_AND);
      else {
        text.append(QUERY_WHERE);
        firstPredicate = false;
      }

      if (has.predicate instanceof Contains) {
        // IN AND NOT_IN
        if (has.predicate == Contains.NOT_IN) {
          text.append(OPERATOR_NOT);
          text.append(PARENTHESIS_BEGIN);
        }
        text.append('`').append(has.key).append('`');

        if (has.value instanceof String) {
          text.append(OPERATOR_LIKE);
          text.append("?");
          params.add(has.value);
//          generateFilterValue(text, has.value);
        } else {
          text.append(OPERATOR_IN);
          text.append(COLLECTION_BEGIN);

          boolean firstItem = true;
          for (Object o : (Collection<Object>) has.value) {
            if (!firstItem)
              text.append(QUERY_SEPARATOR);
            else
              firstItem = false;
            text.append("?");
            params.add(o);
//            generateFilterValue(text, o);
          }

          text.append(COLLECTION_END);
        }

        if (has.predicate == Contains.NOT_IN)
          text.append(PARENTHESIS_END);
      } else {
        // ANY OTHER OPERATORS
        text.append('`').append(has.key).append('`');
        text.append(SPACE);

        if (has.predicate instanceof com.tinkerpop.blueprints.Compare) {
          final com.tinkerpop.blueprints.Compare compare = (com.tinkerpop.blueprints.Compare) has.predicate;
          boolean appendParam = true;
          switch (compare) {
          case EQUAL:
            if (has.value == null) {
              // IS
              text.append(OPERATOR_IS);
              text.append(" NULL ");
              appendParam = false;
            } else
              // EQUALS
              text.append(OPERATOR_EQUALS);
            break;
          case GREATER_THAN:
            text.append(OPERATOR_GT);
            break;
          case GREATER_THAN_EQUAL:
            text.append(OPERATOR_GTE);
            break;
          case LESS_THAN:
            text.append(OPERATOR_LT);
            break;
          case LESS_THAN_EQUAL:
            text.append(OPERATOR_LET);
            break;
          case NOT_EQUAL:
            if (has.value == null) {
              text.append(OPERATOR_IS_NOT);
              text.append(" NULL ");
              appendParam = false;
            }else
              text.append(OPERATOR_DIFFERENT);
            break;
          }
          text.append(SPACE);
          if (appendParam) {
            text.append("?");
            params.add(has.value);
          }
//          generateFilterValue(text, has.value);
        }

        if (has.value instanceof Collection<?>)
          text.append(PARENTHESIS_END);
      }
    }
    return params;
  }

  protected void generateFilterValue(final StringBuilder text, final Object iValue) {
    if (iValue instanceof String)
      text.append(QUERY_STRING);

    final Object value;

    if (iValue instanceof Date)
      value = ((Date) iValue).getTime();
    else if (iValue != null)
      value = iValue.toString().replace("'", "\\'");
    else
      value = null;

    text.append(value);

    if (iValue instanceof String)
      text.append(QUERY_STRING);
  }
}


/*
 *
 *  *  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.server.distributed.impl.task;

import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OLogSequenceNumber;
import com.orientechnologies.orient.server.distributed.*;
import com.orientechnologies.orient.server.distributed.ODistributedServerLog.DIRECTION;
import com.orientechnologies.orient.server.distributed.task.OAbstractRecordReplicatedTask;
import com.orientechnologies.orient.server.distributed.task.OAbstractRemoteTask;
import com.orientechnologies.orient.server.distributed.task.OAbstractReplicatedTask;
import com.orientechnologies.orient.server.distributed.task.ORemoteTask;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Distributed two phase commit task. Operations can keep locks on records on the distributed servers. A second message of
 * {@link OCompleted2pcTask} is requested to unlock the records. Note that locks are freed after a (configurable) while anyway.
 *
 * @author Luca Garulli (l.garulli--at--orientdb.com)
 */
public abstract class OAbstract2pcTask extends OAbstractReplicatedTask {
  protected static final long   serialVersionUID  = 1L;
  public static final    String NON_LOCAL_CLUSTER = "_non_local_cluster";

  protected List<OAbstractRecordReplicatedTask> tasks = new ArrayList<OAbstractRecordReplicatedTask>();

  protected transient List<OAbstractRemoteTask> localUndoTasks = new ArrayList<OAbstractRemoteTask>();
  protected transient OTxTaskResult result;

  public OAbstract2pcTask() {
  }

  public void add(final OAbstractRecordReplicatedTask iTask) {
    tasks.add(iTask);
  }

  @Override
  public boolean isIdempotent() {
    for (OAbstractRecordReplicatedTask t : tasks)
      if (t != null && !t.isIdempotent())
        return false;
    return true;
  }

  /**
   * Return the partition keys of all the sub-tasks.
   */
  @Override
  public int[] getPartitionKey() {
    if (tasks.size() == 1)
      // ONE TASK, USE THE INNER TASK'S PARTITION KEY
      return tasks.get(0).getPartitionKey();

    // MULTIPLE PARTITIONS
    final int[] partitions = new int[tasks.size()];
    for (int i = 0; i < tasks.size(); ++i) {
      final OAbstractRecordReplicatedTask task = tasks.get(i);
      partitions[i] = task.getPartitionKey()[0];
    }

    return partitions;
  }

  @Override
  public OCommandDistributedReplicateRequest.QUORUM_TYPE getQuorumType() {
    return OCommandDistributedReplicateRequest.QUORUM_TYPE.WRITE;
  }

  @Override
  public void toStream(final DataOutput out) throws IOException {
    out.writeInt(tasks.size());
    for (OAbstractRecordReplicatedTask task : tasks) {
      out.writeByte(task.getFactoryId());
      task.toStream(out);
    }
    if (lastLSN != null) {
      out.writeBoolean(true);
      lastLSN.toStream(out);
    } else
      out.writeBoolean(false);
  }

  @Override
  public void fromStream(final DataInput in, final ORemoteTaskFactory factory) throws IOException {
    final int size = in.readInt();
    for (int i = 0; i < size; ++i) {
      final ORemoteTask task = factory.createTask(in.readByte());
      task.fromStream(in, factory);
      tasks.add((OAbstractRecordReplicatedTask) task);
    }
    final boolean hasLastLSN = in.readBoolean();
    if (hasLastLSN)
      lastLSN = new OLogSequenceNumber(in);
  }

  /**
   * Computes the timeout according to the transaction size.
   *
   * @return
   */
  @Override
  public long getDistributedTimeout() {
    final long to = OGlobalConfiguration.DISTRIBUTED_CRUD_TASK_SYNCH_TIMEOUT.getValueAsLong();
    return to + ((to / 2) * tasks.size());
  }

  public List<OAbstractRecordReplicatedTask> getTasks() {
    return tasks;
  }

  @Override
  public void setNodeSource(final String nodeSource) {
    super.setNodeSource(nodeSource);
    for (OAbstractRecordReplicatedTask t : tasks) {
      t.setNodeSource(nodeSource);
    }
  }

  public void setLocalUndoTasks(final List<OAbstractRemoteTask> undoTasks) {
    this.localUndoTasks = undoTasks;
  }

  public void setLastLSN(final OLogSequenceNumber lastLSN) {
    this.lastLSN = lastLSN;
  }

  @Override
  public String toString() {
    final StringBuilder buffer = new StringBuilder();

    buffer.append("tx[");
    buffer.append(tasks.size());
    buffer.append("]{");

    for (int i = 0; i < tasks.size(); ++i) {
      final OAbstractRecordReplicatedTask task = tasks.get(i);
      if (i > 0)
        buffer.append(',');
      buffer.append(task);
    }

    buffer.append("}");
    return buffer.toString();
  }
}


/*
 *
 *  *  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.index;

import com.orientechnologies.orient.core.collate.OCollate;
import com.orientechnologies.orient.core.collate.ODefaultCollate;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OSQLEngine;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class OSimpleKeyIndexDefinition extends OAbstractIndexDefinition {
  private OType[] keyTypes;

  public OSimpleKeyIndexDefinition(final OType... keyTypes) {
    super();

    this.keyTypes = keyTypes;
  }

  public OSimpleKeyIndexDefinition() {
  }

  public OSimpleKeyIndexDefinition(OType[] keyTypes2, List<OCollate> collatesList) {
    super();

    this.keyTypes = Arrays.copyOf(keyTypes2, keyTypes2.length);

    if (keyTypes.length > 1) {
      OCompositeCollate collate = new OCompositeCollate(this);
      if (collatesList != null) {
        for (OCollate oCollate : collatesList) {
          collate.addCollate(oCollate);
        }
      } else {
        final int typesSize = keyTypes.length;
        final OCollate defCollate = OSQLEngine.getCollate(ODefaultCollate.NAME);
        for (int i = 0; i < typesSize; i++) {
          collate.addCollate(defCollate);
        }
      }
      this.collate = collate;
    }

  }

  public List<String> getFields() {
    return Collections.emptyList();
  }

  public List<String> getFieldsToIndex() {
    return Collections.emptyList();
  }

  public String getClassName() {
    return null;
  }

  public Object createValue(final List<?> params) {
    return createValue(params != null ? params.toArray() : null);
  }

  public Object createValue(final Object... params) {
    if (params == null || params.length == 0)
      return null;

    if (keyTypes.length == 1)
      return OType.convert(params[0], keyTypes[0].getDefaultJavaType());

    final OCompositeKey compositeKey = new OCompositeKey();

    for (int i = 0; i < params.length; ++i) {
      final Comparable<?> paramValue = (Comparable<?>) OType.convert(params[i], keyTypes[i].getDefaultJavaType());

      if (paramValue == null)
        return null;
      compositeKey.addKey(paramValue);
    }

    return compositeKey;
  }

  public int getParamCount() {
    return keyTypes.length;
  }

  public OType[] getTypes() {
    return Arrays.copyOf(keyTypes, keyTypes.length);
  }

  @Override
  public ODocument toStream() {
    document.setInternalStatus(ORecordElement.STATUS.UNMARSHALLING);
    try {
      serializeToStream();
      return document;
    } finally {
      document.setInternalStatus(ORecordElement.STATUS.LOADED);
    }
  }

  @Override
  protected void serializeToStream() {
    super.serializeToStream();

    final List<String> keyTypeNames = new ArrayList<>(keyTypes.length);

    for (final OType keyType : keyTypes)
      keyTypeNames.add(keyType.toString());

    document.field("keyTypes", keyTypeNames, OType.EMBEDDEDLIST);
    if (collate instanceof OCompositeCollate) {
      List<String> collatesNames = new ArrayList<>();
      for (OCollate curCollate : ((OCompositeCollate) this.collate).getCollates())
        collatesNames.add(curCollate.getName());
      document.field("collates", collatesNames, OType.EMBEDDEDLIST);
    } else
      document.field("collate", collate.getName());

    document.field("nullValuesIgnored", isNullValuesIgnored());
  }

  @Override
  protected void fromStream() {
    serializeFromStream();
  }

  @Override
  protected void serializeFromStream() {
    super.serializeFromStream();

    final List<String> keyTypeNames = document.field("keyTypes");
    keyTypes = new OType[keyTypeNames.size()];

    int i = 0;
    for (final String keyTypeName : keyTypeNames) {
      keyTypes[i] = OType.valueOf(keyTypeName);
      i++;
    }
    String collate = document.field("collate");
    if (collate != null) {
      setCollate(collate);
    } else {
      final List<String> collatesNames = document.field("collates");
      if( collatesNames != null ) {
        OCompositeCollate collates = new OCompositeCollate(this);
        for (String collateName : collatesNames)
          collates.addCollate(OSQLEngine.getCollate(collateName));
        this.collate = collates;
      }
    }

    setNullValuesIgnored(!Boolean.FALSE.equals(document.<Boolean>field("nullValuesIgnored")));
  }

  public Object getDocumentValueToIndex(final ODocument iDocument) {
    throw new OIndexException("This method is not supported in given index definition.");
  }

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

    final OSimpleKeyIndexDefinition that = (OSimpleKeyIndexDefinition) o;
    return Arrays.equals(keyTypes, that.keyTypes);
  }

  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + (keyTypes != null ? Arrays.hashCode(keyTypes) : 0);
    return result;
  }

  @Override
  public String toString() {
    return "OSimpleKeyIndexDefinition{" + "keyTypes=" + (keyTypes == null ? null : Arrays.asList(keyTypes)) + '}';
  }

  /**
   * {@inheritDoc}
   *
   * @param indexName
   * @param indexType
   */
  public String toCreateIndexDDL(final String indexName, final String indexType, final String engine) {
    final StringBuilder ddl = new StringBuilder("create index `");
    ddl.append(indexName).append("` ").append(indexType).append(' ');

    if (keyTypes != null && keyTypes.length > 0) {
      ddl.append(keyTypes[0].toString());
      for (int i = 1; i < keyTypes.length; i++) {
        ddl.append(", ").append(keyTypes[i].toString());
      }
    }
    return ddl.toString();
  }

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