package com.orientechnologies.orient.core.storage.config;

import com.orientechnologies.common.concur.lock.OReadersWriterSpinLock;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.serialization.types.OByteSerializer;
import com.orientechnologies.common.serialization.types.OIntegerSerializer;
import com.orientechnologies.common.serialization.types.OStringSerializer;
import com.orientechnologies.orient.core.config.OContextConfiguration;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.config.OStorageClusterConfiguration;
import com.orientechnologies.orient.core.config.OStorageConfiguration;
import com.orientechnologies.orient.core.config.OStorageConfigurationUpdateListener;
import com.orientechnologies.orient.core.config.OStorageEntryConfiguration;
import com.orientechnologies.orient.core.config.OStorageFileConfiguration;
import com.orientechnologies.orient.core.config.OStoragePaginatedClusterConfiguration;
import com.orientechnologies.orient.core.config.OStorageSegmentConfiguration;
import com.orientechnologies.orient.core.exception.OSerializationException;
import com.orientechnologies.orient.core.exception.OStorageException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.storage.OPhysicalPosition;
import com.orientechnologies.orient.core.storage.ORawBuffer;
import com.orientechnologies.orient.core.storage.cache.OWriteCache;
import com.orientechnologies.orient.core.storage.cluster.OPaginatedCluster;
import com.orientechnologies.orient.core.storage.disk.OLocalPaginatedStorage;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;
import com.orientechnologies.orient.core.storage.impl.local.paginated.atomicoperations.OAtomicOperationsManager;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OPaginatedClusterFactory;
import com.orientechnologies.orient.core.storage.index.sbtree.singlevalue.OCellBTreeSingleValue;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

public final class OClusterBasedStorageConfiguration implements OStorageConfiguration {
  public static final String MAP_FILE_EXTENSION  = ".ccm";
  public static final String DATA_FILE_EXTENSION = ".cd";

  public static final String TREE_DATA_FILE_EXTENSION = ".bd";
  public static final String TREE_NULL_FILE_EXTENSION = ".nd";

  public static final  String COMPONENT_NAME                   = "config";
  private static final String VERSION_PROPERTY                 = "version";
  private static final String SCHEMA_RECORD_ID_PROPERTY        = "schemaRecordId";
  private static final String INDEX_MANAGER_RECORD_ID_PROPERTY = "indexManagerRecordId";

  private static final String LOCALE_LANGUAGE_PROPERTY = "localeLanguage";
  private static final String LOCALE_COUNTRY_PROPERTY  = "localeCountry";
  private static final String LOCALE_PROPERTY_INSTANCE = "localeInstance";

  private static final String DATE_FORMAT_PROPERTY      = "dateFormat";
  private static final String DATE_TIME_FORMAT_PROPERTY = "dateTimeFormat";

  private static final String TIME_ZONE_PROPERTY                 = "timeZone";
  private static final String CHARSET_PROPERTY                   = "charset";
  private static final String CONFLICT_STRATEGY_PROPERTY         = "conflictStrategy";
  private static final String BINARY_FORMAT_VERSION_PROPERTY     = "binaryFormatVersion";
  private static final String CLUSTER_SELECTION_PROPERTY         = "clusterSelection";
  private static final String MINIMUM_CLUSTERS_PROPERTY          = "minimumClusters";
  private static final String RECORD_SERIALIZER_PROPERTY         = "recordSerializer";
  private static final String RECORD_SERIALIZER_VERSION_PROPERTY = "recordSerializerVersion";
  private static final String CONFIGURATION_PROPERTY             = "configuration";
  private static final String CREATED_AT_VERSION_PROPERTY        = "createAtVersion";
  private static final String PAGE_SIZE_PROPERTY                 = "pageSize";
  private static final String FREE_LIST_BOUNDARY_PROPERTY        = "freeListBoundary";
  private static final String MAX_KEY_SIZE_PROPERTY              = "maxKeySize";

  private static final String CLUSTERS_PREFIX_PROPERTY = "cluster_";
  private static final String PROPERTY_PREFIX_PROPERTY = "property_";
  private static final String ENGINE_PREFIX_PROPERTY   = "engine_";

  private static final String PROPERTIES = "properties";
  private static final String CLUSTERS   = "clusters";
  private static final String UUID       = "UUID";

  private static final String[] INT_PROPERTIES = new String[] { MINIMUM_CLUSTERS_PROPERTY, VERSION_PROPERTY,
      BINARY_FORMAT_VERSION_PROPERTY, RECORD_SERIALIZER_VERSION_PROPERTY, PAGE_SIZE_PROPERTY, FREE_LIST_BOUNDARY_PROPERTY,
      MAX_KEY_SIZE_PROPERTY };

  private static final String[] STRING_PROPERTIES = new String[] { SCHEMA_RECORD_ID_PROPERTY, INDEX_MANAGER_RECORD_ID_PROPERTY,
      LOCALE_LANGUAGE_PROPERTY, LOCALE_COUNTRY_PROPERTY, DATE_FORMAT_PROPERTY, DATE_TIME_FORMAT_PROPERTY, TIME_ZONE_PROPERTY,
      CHARSET_PROPERTY, CONFLICT_STRATEGY_PROPERTY, CLUSTER_SELECTION_PROPERTY, RECORD_SERIALIZER_PROPERTY,
      CREATED_AT_VERSION_PROPERTY, UUID };

  private OContextConfiguration configuration;
  private boolean               validation;

  private final OCellBTreeSingleValue<String> btree;
  private final OPaginatedCluster             cluster;

  private final OAbstractPaginatedStorage storage;
  private final OAtomicOperationsManager  atomicOperationsManager;
  private final OReadersWriterSpinLock    lock = new OReadersWriterSpinLock();

  private final HashMap<String, Object> cache = new HashMap<>();

  private OStorageConfigurationUpdateListener updateListener;

  private final ThreadLocal<PausedNotificationsState> pauseNotifications = ThreadLocal.withInitial(PausedNotificationsState::new);

  public static boolean exists(final OWriteCache writeCache) {
    return writeCache.exists(COMPONENT_NAME + DATA_FILE_EXTENSION);
  }

  public OClusterBasedStorageConfiguration(final OAbstractPaginatedStorage storage) {
    cluster = OPaginatedClusterFactory
        .createCluster(COMPONENT_NAME, OPaginatedCluster.getLatestBinaryVersion(), storage, DATA_FILE_EXTENSION,
            MAP_FILE_EXTENSION);
    btree = new OCellBTreeSingleValue<>(COMPONENT_NAME, TREE_DATA_FILE_EXTENSION, TREE_NULL_FILE_EXTENSION, storage);
    this.atomicOperationsManager = storage.getAtomicOperationsManager();

    this.storage = storage;
  }

  public void create(final OContextConfiguration contextConfiguration) throws IOException {
    lock.acquireWriteLock();
    try {
      cluster.create(-1);
      btree.create(OStringSerializer.INSTANCE, null, 1, null);

      this.configuration = contextConfiguration;

      init();

      preloadIntProperties();
      preloadStringProperties();
      preloadClusters();
      preloadConfigurationProperties();
      setValidation(getContextConfiguration().getValueAsBoolean(OGlobalConfiguration.DB_VALIDATION));
      recalculateLocale();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void create(final OContextConfiguration contextConfiguration, final OStorageConfiguration source) throws IOException {
    lock.acquireWriteLock();
    try {
      create(contextConfiguration);
      copy(source);
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void delete() throws IOException {
    lock.acquireWriteLock();
    try {
      updateListener = null;

      cluster.delete();
      btree.delete();

      cache.clear();
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void close() {
    lock.acquireWriteLock();
    try {
      updateListener = null;

      updateConfigurationProperty();
      updateMinimumClusters();

      cache.clear();

      //tree and cluster will be closed by storage automatically
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void load(final OContextConfiguration configuration) throws OSerializationException, IOException {
    lock.acquireWriteLock();
    try {
      this.configuration = configuration;

      cluster.open();
      btree.load(COMPONENT_NAME, 1, null, OStringSerializer.INSTANCE, null);

      readConfiguration();
      readMinimumClusters();

      preloadIntProperties();
      preloadStringProperties();
      preloadConfigurationProperties();
      preloadClusters();
      recalculateLocale();

      validation = "true".equalsIgnoreCase(getProperty("validation"));
    } finally {
      lock.releaseWriteLock();
    }

  }

  public void pauseUpdateNotifications() {
    lock.acquireWriteLock();
    try {
      final PausedNotificationsState pausedNotificationsState = pauseNotifications.get();
      pausedNotificationsState.notificationsPaused = true;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void fireUpdateNotifications() {
    lock.acquireWriteLock();
    try {
      final PausedNotificationsState pausedNotificationsState = pauseNotifications.get();

      if (pausedNotificationsState.pendingChanges > 0 && updateListener != null) {
        updateListener.onUpdate(this);
        pausedNotificationsState.pendingChanges = 0;
      }

      pausedNotificationsState.notificationsPaused = false;
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void setMinimumClusters(final int minimumClusters) {
    lock.acquireWriteLock();
    try {
      getContextConfiguration().setValue(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS, minimumClusters);
      autoInitClusters();
    } finally {
      lock.releaseWriteLock();
    }
  }

  private void updateMinimumClusters() {
    updateIntProperty(MINIMUM_CLUSTERS_PROPERTY, getMinimumClusters());
  }

  private void readMinimumClusters() {
    if (containsProperty(MINIMUM_CLUSTERS_PROPERTY)) {
      setMinimumClusters(readIntProperty(MINIMUM_CLUSTERS_PROPERTY, false));
    }
  }

  public int getMinimumClusters() {
    lock.acquireReadLock();
    try {
      final int mc = getContextConfiguration().getValueAsInteger(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS);
      if (mc == 0) {
        autoInitClusters();
        return (Integer) getContextConfiguration().getValue(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS);
      }
      return mc;
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public OContextConfiguration getContextConfiguration() {
    lock.acquireReadLock();
    try {
      return configuration;
    } finally {
      lock.releaseReadLock();
    }
  }

  /**
   * Added version used for managed Network Versioning.
   */
  public byte[] toStream(final int iNetworkVersion, final Charset charset) throws OSerializationException {
    lock.acquireReadLock();
    try {
      final StringBuilder buffer = new StringBuilder(8192);

      write(buffer, CURRENT_VERSION);
      write(buffer, null);

      write(buffer, getSchemaRecordId());
      write(buffer, "");
      write(buffer, getIndexMgrRecordId());

      write(buffer, getLocaleLanguage());
      write(buffer, getLocaleCountry());
      write(buffer, getDateFormat());
      write(buffer, getDateFormat());

      final TimeZone timeZone = getTimeZone();
      assert timeZone != null;

      write(buffer, timeZone);
      write(buffer, charset);
      if (iNetworkVersion > 24) {
        write(buffer, getConflictStrategy());
      }

      phySegmentToStream(buffer, new OStorageSegmentConfiguration());

      final List<OStorageClusterConfiguration> clusters = getClusters();
      write(buffer, clusters.size());
      for (final OStorageClusterConfiguration c : clusters) {
        if (c == null) {
          write(buffer, -1);
          continue;
        }

        write(buffer, c.getId());
        write(buffer, c.getName());
        write(buffer, c.getDataSegmentId());

        if (c instanceof OStoragePaginatedClusterConfiguration) {
          write(buffer, "d");

          final OStoragePaginatedClusterConfiguration paginatedClusterConfiguration = (OStoragePaginatedClusterConfiguration) c;

          write(buffer, paginatedClusterConfiguration.useWal);
          write(buffer, paginatedClusterConfiguration.recordOverflowGrowFactor);
          write(buffer, paginatedClusterConfiguration.recordGrowFactor);
          write(buffer, paginatedClusterConfiguration.compression);

          if (iNetworkVersion >= 31) {
            write(buffer, paginatedClusterConfiguration.encryption);
          }
          if (iNetworkVersion > 24) {
            write(buffer, paginatedClusterConfiguration.conflictStrategy);
          }
          if (iNetworkVersion > 25) {
            write(buffer, paginatedClusterConfiguration.getStatus().name());
          }

          if (iNetworkVersion >= Integer.MAX_VALUE) {
            write(buffer, paginatedClusterConfiguration.getBinaryVersion());
          }
        }
      }
      if (iNetworkVersion <= 25) {
        // dataSegment array
        write(buffer, 0);
        // tx Segment File
        write(buffer, "");
        write(buffer, "");
        write(buffer, 0);
        // tx segment flags
        write(buffer, false);
        write(buffer, false);
      }

      final List<OStorageEntryConfiguration> properties = getProperties();
      write(buffer, properties.size());
      for (final OStorageEntryConfiguration e : properties) {
        entryToStream(buffer, e);
      }

      write(buffer, getBinaryFormatVersion());
      write(buffer, getClusterSelection());
      write(buffer, getMinimumClusters());

      if (iNetworkVersion > 24) {
        write(buffer, getRecordSerializer());
        write(buffer, getRecordSerializerVersion());

        // WRITE CONFIGURATION
        write(buffer, configuration.getContextSize());
        for (final String k : configuration.getContextKeys()) {
          final OGlobalConfiguration cfg = OGlobalConfiguration.findByKey(k);
          write(buffer, k);
          if (cfg != null) {
            write(buffer, cfg.isHidden() ? null : configuration.getValueAsString(cfg));
          } else {
            write(buffer, null);
            OLogManager.instance().warn(this, "Storing configuration for property:'" + k + "' not existing in current version");
          }
        }
      }

      final List<IndexEngineData> engines = loadIndexEngines();
      write(buffer, engines.size());
      for (final IndexEngineData engineData : engines) {
        write(buffer, engineData.getName());
        write(buffer, engineData.getAlgorithm());
        write(buffer, engineData.getIndexType() == null ? "" : engineData.getIndexType());

        write(buffer, engineData.getValueSerializerId());
        write(buffer, engineData.getKeySerializedId());

        write(buffer, engineData.isAutomatic());
        write(buffer, engineData.getDurableInNonTxMode());

        write(buffer, engineData.getVersion());
        write(buffer, engineData.isNullValuesSupport());
        write(buffer, engineData.getKeySize());
        write(buffer, engineData.getEncryption());
        write(buffer, engineData.getEncryptionOptions());

        if (engineData.getKeyTypes() != null) {
          write(buffer, engineData.getKeyTypes().length);
          for (final OType type : engineData.getKeyTypes()) {
            write(buffer, type.name());
          }
        } else {
          write(buffer, 0);
        }

        if (engineData.getEngineProperties() == null) {
          write(buffer, 0);
        } else {
          write(buffer, engineData.getEngineProperties().size());
          for (final Map.Entry<String, String> property : engineData.getEngineProperties().entrySet()) {
            write(buffer, property.getKey());
            write(buffer, property.getValue());
          }
        }

        write(buffer, engineData.getApiVersion());
        write(buffer, engineData.isMultivalue());
      }

      write(buffer, getCreatedAtVersion());
      write(buffer, getPageSize());
      write(buffer, getFreeListBoundary());
      write(buffer, getMaxKeySize());

      // PLAIN: ALLOCATE ENOUGH SPACE TO REUSE IT EVERY TIME
      buffer.append("|");

      return buffer.toString().getBytes(charset);
    } finally {
      lock.releaseReadLock();
    }
  }

  private static void entryToStream(final StringBuilder buffer, final OStorageEntryConfiguration entry) {
    write(buffer, entry.name);
    write(buffer, entry.value);
  }

  private static void phySegmentToStream(final StringBuilder buffer, final OStorageSegmentConfiguration segment) {
    write(buffer, segment.getLocation());
    write(buffer, segment.maxSize);
    write(buffer, segment.fileType);
    write(buffer, segment.fileStartSize);
    write(buffer, segment.fileMaxSize);
    write(buffer, segment.fileIncrementSize);
    write(buffer, segment.defrag);

    write(buffer, segment.infoFiles.length);
    for (final OStorageFileConfiguration f : segment.infoFiles) {
      fileToStream(buffer, f);
    }
  }

  private static void fileToStream(final StringBuilder iBuffer, final OStorageFileConfiguration iFile) {
    write(iBuffer, iFile.path);
    write(iBuffer, iFile.type);
    write(iBuffer, iFile.maxSize);
  }

  private static void write(final StringBuilder buffer, final Object value) {
    if (buffer.length() > 0) {
      buffer.append('|');
    }

    buffer.append(value != null ? value.toString() : ' ');
  }

  private void updateVersion() {
    updateIntProperty(VERSION_PROPERTY, CURRENT_VERSION);
  }

  private void updateVersion(final int version) {
    updateIntProperty(VERSION_PROPERTY, version);
  }

  @Override
  public int getVersion() {
    lock.acquireReadLock();
    try {
      return readIntProperty(VERSION_PROPERTY, true);
    } finally {
      lock.releaseReadLock();
    }

  }

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

  public void setSchemaRecordId(final String schemaRecordId) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(SCHEMA_RECORD_ID_PROPERTY, schemaRecordId, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getSchemaRecordId() {
    lock.acquireReadLock();
    try {
      return readStringProperty(SCHEMA_RECORD_ID_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setIndexMgrRecordId(final String indexMgrRecordId) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(INDEX_MANAGER_RECORD_ID_PROPERTY, indexMgrRecordId, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getIndexMgrRecordId() {
    lock.acquireReadLock();
    try {
      return readStringProperty(INDEX_MANAGER_RECORD_ID_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setLocaleLanguage(final String value) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(LOCALE_LANGUAGE_PROPERTY, value, true);

      recalculateLocale();
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getLocaleLanguage() {
    lock.acquireReadLock();
    try {
      return readStringProperty(LOCALE_LANGUAGE_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setLocaleCountry(final String value) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(LOCALE_COUNTRY_PROPERTY, value, true);

      recalculateLocale();
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getLocaleCountry() {
    lock.acquireReadLock();
    try {
      return readStringProperty(LOCALE_COUNTRY_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setDateFormat(final String dateFormat) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(DATE_FORMAT_PROPERTY, dateFormat, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getDateFormat() {
    lock.acquireReadLock();
    try {
      final String dateFormat = readStringProperty(DATE_FORMAT_PROPERTY);
      assert dateFormat != null;

      return dateFormat;
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public SimpleDateFormat getDateFormatInstance() {
    lock.acquireReadLock();
    try {
      final SimpleDateFormat dateFormatInstance = new SimpleDateFormat(getDateFormat());
      dateFormatInstance.setLenient(false);
      final TimeZone timeZone = getTimeZone();
      if (timeZone != null) {
        dateFormatInstance.setTimeZone(timeZone);
      }

      return dateFormatInstance;
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public String getDateTimeFormat() {
    lock.acquireReadLock();
    try {
      final String dateTimeFormat = readStringProperty(DATE_TIME_FORMAT_PROPERTY);
      assert dateTimeFormat != null;

      return dateTimeFormat;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setDateTimeFormat(final String dateTimeFormat) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(DATE_TIME_FORMAT_PROPERTY, dateTimeFormat, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void setUuid(final String uuid) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(UUID, uuid, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  public String getUuid() {
    lock.acquireReadLock();
    try {
      final String uuid = readStringProperty(UUID);
      assert uuid != null;

      return uuid;
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public SimpleDateFormat getDateTimeFormatInstance() {
    lock.acquireReadLock();
    try {
      final SimpleDateFormat dateTimeFormatInstance = new SimpleDateFormat(getDateTimeFormat());
      dateTimeFormatInstance.setLenient(false);
      final TimeZone timeZone = getTimeZone();
      if (timeZone != null) {
        dateTimeFormatInstance.setTimeZone(timeZone);
      }

      return dateTimeFormatInstance;
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setTimeZone(final TimeZone timeZone) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(TIME_ZONE_PROPERTY, timeZone.getID(), true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public TimeZone getTimeZone() {
    lock.acquireReadLock();
    try {
      final String timeZone = readStringProperty(TIME_ZONE_PROPERTY);
      if (timeZone == null) {
        return null;
      }

      return TimeZone.getTimeZone(timeZone);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setCharset(final String charset) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(CHARSET_PROPERTY, charset, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getCharset() {
    lock.acquireReadLock();
    try {
      return readStringProperty(CHARSET_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setConflictStrategy(final String conflictStrategy) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(CONFLICT_STRATEGY_PROPERTY, conflictStrategy, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getConflictStrategy() {
    lock.acquireReadLock();
    try {
      return readStringProperty(CONFLICT_STRATEGY_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  private void updateBinaryFormatVersion() {
    updateIntProperty(BINARY_FORMAT_VERSION_PROPERTY, CURRENT_BINARY_FORMAT_VERSION);
  }

  private void updateBinaryFormatVersion(final int version) {
    updateIntProperty(BINARY_FORMAT_VERSION_PROPERTY, version);
  }

  @Override
  public int getBinaryFormatVersion() {
    lock.acquireReadLock();
    try {
      return readIntProperty(BINARY_FORMAT_VERSION_PROPERTY, true);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setClusterSelection(final String clusterSelection) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(CLUSTER_SELECTION_PROPERTY, clusterSelection, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getClusterSelection() {
    lock.acquireReadLock();
    try {
      return readStringProperty(CLUSTER_SELECTION_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setRecordSerializer(final String recordSerializer) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(RECORD_SERIALIZER_PROPERTY, recordSerializer, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getRecordSerializer() {
    lock.acquireReadLock();
    try {
      return readStringProperty(RECORD_SERIALIZER_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setRecordSerializerVersion(final int recordSerializerVersion) {
    lock.acquireWriteLock();
    try {
      updateIntProperty(RECORD_SERIALIZER_VERSION_PROPERTY, recordSerializerVersion);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public int getRecordSerializerVersion() {
    lock.acquireReadLock();
    try {
      return readIntProperty(RECORD_SERIALIZER_VERSION_PROPERTY, true);
    } finally {
      lock.releaseReadLock();
    }
  }

  private void updateConfigurationProperty() {
    final List<byte[]> entries = new ArrayList<>(8);
    int totalSize = 0;

    final byte[] contextSize = new byte[OIntegerSerializer.INT_SIZE];
    totalSize += contextSize.length;
    entries.add(contextSize);

    OIntegerSerializer.INSTANCE.serializeNative(configuration.getContextSize(), contextSize, 0);

    for (final String k : configuration.getContextKeys()) {
      final OGlobalConfiguration cfg = OGlobalConfiguration.findByKey(k);
      final byte[] key = serializeStringValue(k);
      totalSize += key.length;
      entries.add(key);

      if (cfg != null) {
        final byte[] value = serializeStringValue(cfg.isHidden() ? null : configuration.getValueAsString(cfg));
        totalSize += value.length;
        entries.add(value);
      } else {
        final byte[] value = serializeStringValue(null);
        totalSize += value.length;
        entries.add(value);

        OLogManager.instance().warn(this, "Storing configuration for property:'" + k + "' not existing in current version");
      }
    }

    final byte[] property = mergeBinaryEntries(totalSize, entries);
    storeProperty(CONFIGURATION_PROPERTY, property);
  }

  private void readConfiguration() {
    final byte[] property = readProperty(CONFIGURATION_PROPERTY);

    if (property == null) {
      return;
    }

    int pos = 0;
    final int size = OIntegerSerializer.INSTANCE.deserializeNative(property, pos);
    pos += OIntegerSerializer.INT_SIZE;

    for (int i = 0; i < size; i++) {
      final String key = deserializeStringValue(property, pos);
      pos += getSerializedStringSize(property, pos);

      final String value = deserializeStringValue(property, pos);
      pos += getSerializedStringSize(property, pos);

      final OGlobalConfiguration cfg = OGlobalConfiguration.findByKey(key);
      if (cfg != null) {
        if (value != null) {
          configuration.setValue(key, OType.convert(value, cfg.getType()));
        }
      } else {
        OLogManager.instance().warn(this, "Ignored storage configuration because not supported: %s=%s", key, value);
      }
    }
  }

  public void setCreationVersion(final String version) {
    lock.acquireWriteLock();
    try {
      updateStringProperty(CREATED_AT_VERSION_PROPERTY, version, true);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public String getCreatedAtVersion() {
    lock.acquireReadLock();
    try {
      return readStringProperty(CREATED_AT_VERSION_PROPERTY);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setPageSize(final int pageSize) {
    lock.acquireWriteLock();
    try {
      updateIntProperty(PAGE_SIZE_PROPERTY, pageSize);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public int getPageSize() {
    lock.acquireReadLock();
    try {
      return readIntProperty(PAGE_SIZE_PROPERTY, true);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setFreeListBoundary(final int freeListBoundary) {
    lock.acquireWriteLock();
    try {
      updateIntProperty(FREE_LIST_BOUNDARY_PROPERTY, freeListBoundary);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public int getFreeListBoundary() {
    lock.acquireReadLock();
    try {
      return readIntProperty(FREE_LIST_BOUNDARY_PROPERTY, true);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setMaxKeySize(final int maxKeySize) {
    lock.acquireWriteLock();
    try {
      updateIntProperty(MAX_KEY_SIZE_PROPERTY, maxKeySize);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public int getMaxKeySize() {
    lock.acquireReadLock();
    try {
      return readIntProperty(MAX_KEY_SIZE_PROPERTY, true);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void setProperty(final String name, final String value) {
    lock.acquireWriteLock();
    try {
      if ("validation".equalsIgnoreCase(name)) {
        validation = "true".equalsIgnoreCase(value);
      }

      final String key = PROPERTY_PREFIX_PROPERTY + name;
      updateStringProperty(key, value, false);

      @SuppressWarnings("unchecked")
      final Map<String, String> properties = (Map<String, String>) cache.get(PROPERTIES);
      properties.put(name, value);
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void setValidation(final boolean validation) {
    setProperty("validation", validation ? "true" : "false");
  }

  @Override
  public boolean isValidationEnabled() {
    lock.acquireReadLock();
    try {
      return validation;
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public String getDirectory() {
    if (storage instanceof OLocalPaginatedStorage) {
      return ((OLocalPaginatedStorage) storage).getStoragePath().toString();
    } else {
      return null;
    }
  }

  @Override
  public String getProperty(final String name) {
    lock.acquireReadLock();
    try {
      @SuppressWarnings("unchecked")
      final Map<String, String> properties = (Map<String, String>) cache.get(PROPERTIES);
      return properties.get(name);
    } finally {
      lock.releaseReadLock();
    }
  }

  @Override
  public List<OStorageEntryConfiguration> getProperties() {
    lock.acquireReadLock();
    try {
      @SuppressWarnings("unchecked")
      final Map<String, String> properties = (Map<String, String>) cache.get(PROPERTIES);

      final List<OStorageEntryConfiguration> result = new ArrayList<>(8);

      for (final Map.Entry<String, String> entry : properties.entrySet()) {
        result.add(new OStorageEntryConfiguration(entry.getKey(), entry.getValue()));
      }

      return result;
    } finally {
      lock.releaseReadLock();
    }
  }

  private void preloadConfigurationProperties() throws IOException {
    final Map<String, String> properties = new HashMap<>(8);

    final OCellBTreeSingleValue.OSBTreeCursor<String, ORID> cursor = btree
        .iterateEntriesMajor(PROPERTY_PREFIX_PROPERTY, false, true);
    Map.Entry<String, ORID> entry = cursor.next(-1);
    while (entry != null) {
      if (!entry.getKey().startsWith(PROPERTY_PREFIX_PROPERTY)) {
        break;
      }

      final ORawBuffer buffer = cluster.readRecord(entry.getValue().getClusterPosition(), false);
      properties.put(entry.getKey().substring(PROPERTY_PREFIX_PROPERTY.length()), deserializeStringValue(buffer.buffer, 0));

      entry = cursor.next(-1);
    }

    cache.put(PROPERTIES, properties);
  }

  public Locale getLocaleInstance() {
    lock.acquireReadLock();
    try {
      return (Locale) cache.get(LOCALE_PROPERTY_INSTANCE);
    } finally {
      lock.releaseReadLock();
    }
  }

  private void recalculateLocale() {
    Locale locale;
    try {
      final String localeLanguage = getLocaleLanguage();
      final String localeCountry = getLocaleCountry();

      if (localeLanguage == null || localeCountry == null) {
        locale = Locale.getDefault();
      } else {
        locale = new Locale(getLocaleLanguage(), getLocaleCountry());
      }
    } catch (final RuntimeException e) {
      locale = Locale.getDefault();
    }

    cache.put(LOCALE_PROPERTY_INSTANCE, locale);
  }

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

  public void clearProperties() {
    lock.acquireWriteLock();
    try {
      final OCellBTreeSingleValue.OSBTreeCursor<String, ORID> cursor = btree
          .iterateEntriesMajor(PROPERTY_PREFIX_PROPERTY, false, true);

      final List<String> keysToRemove = new ArrayList<>(8);
      final List<ORID> ridsToRemove = new ArrayList<>(8);

      Map.Entry<String, ORID> entry = cursor.next(-1);
      while (entry != null) {
        if (!entry.getKey().startsWith(PROPERTY_PREFIX_PROPERTY)) {
          break;
        }

        keysToRemove.add(entry.getKey());
        ridsToRemove.add(entry.getValue());

        entry = cursor.next(-1);
      }

      boolean rollback = false;
      atomicOperationsManager.startAtomicOperation(COMPONENT_NAME, true);
      try {
        for (final String key : keysToRemove) {
          btree.remove(key);
        }

        for (final ORID rid : ridsToRemove) {
          cluster.deleteRecord(rid.getClusterPosition());
        }

        @SuppressWarnings("unchecked")
        final Map<String, String> properties = (Map<String, String>) cache.get(PROPERTIES);
        properties.clear();
      } catch (final Exception e) {
        rollback = true;
        throw e;
      } finally {
        atomicOperationsManager.endAtomicOperation(rollback);
      }
    } catch (final IOException e) {
      throw OException.wrapException(new OStorageException("Error during clear of configuration properties"), e);
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void removeProperty(final String name) {
    lock.acquireWriteLock();
    try {
      dropProperty(PROPERTY_PREFIX_PROPERTY + name);
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void addIndexEngine(final String name, final IndexEngineData engineData) {
    lock.acquireWriteLock();
    try {
      final ORID identifiable = btree.get(ENGINE_PREFIX_PROPERTY + name);
      if (identifiable != null) {
        OLogManager.instance()
            .warn(this, "Index engine with name '" + engineData.getName() + "' already contained in database configuration");
      } else {
        storeProperty(ENGINE_PREFIX_PROPERTY + name, serializeIndexEngineProperty(engineData));
      }
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void deleteIndexEngine(final String name) {
    lock.acquireWriteLock();
    try {
      dropProperty(ENGINE_PREFIX_PROPERTY + name);
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public Set<String> indexEngines() {
    lock.acquireReadLock();
    try {
      final OCellBTreeSingleValue.OSBTreeCursor<String, ORID> cursor = btree
          .iterateEntriesMajor(ENGINE_PREFIX_PROPERTY, false, true);

      final Set<String> result = new HashSet<>(4);
      Map.Entry<String, ORID> entry = cursor.next(-1);
      while (entry != null) {
        if (!entry.getKey().startsWith(ENGINE_PREFIX_PROPERTY)) {
          break;
        }

        result.add(entry.getKey().substring(ENGINE_PREFIX_PROPERTY.length()));

        entry = cursor.next(-1);
      }

      return result;
    } finally {
      lock.releaseReadLock();
    }
  }

  private List<IndexEngineData> loadIndexEngines() {
    try {
      final OCellBTreeSingleValue.OSBTreeCursor<String, ORID> cursor = btree
          .iterateEntriesMajor(ENGINE_PREFIX_PROPERTY, false, true);
      final List<IndexEngineData> result = new ArrayList<>(4);

      Map.Entry<String, ORID> entry = cursor.next(-1);
      while (entry != null) {
        if (!entry.getKey().startsWith(ENGINE_PREFIX_PROPERTY)) {
          break;
        }

        final String name = entry.getKey().substring(ENGINE_PREFIX_PROPERTY.length());
        final ORawBuffer buffer = cluster.readRecord(entry.getValue().getClusterPosition(), false);
        final IndexEngineData engine = deserializeIndexEngineProperty(name, buffer.buffer);
        result.add(engine);

        entry = cursor.next(-1);
      }

      return result;
    } catch (final IOException e) {
      throw OException.wrapException(new OStorageException("Can not fetch list of index engines"), e);
    }
  }

  @Override
  public IndexEngineData getIndexEngine(final String name) {
    lock.acquireReadLock();
    try {
      final byte[] property = readProperty(ENGINE_PREFIX_PROPERTY + name);
      if (property == null) {
        return null;
      }

      return deserializeIndexEngineProperty(name, property);
    } finally {
      lock.releaseReadLock();
    }
  }

  public void updateCluster(final OStorageClusterConfiguration config) {
    lock.acquireWriteLock();
    try {
      @SuppressWarnings("unchecked")
      final List<OStorageClusterConfiguration> clusters = (List<OStorageClusterConfiguration>) cache.get(CLUSTERS);
      if (config.getId() < clusters.size()) {
        clusters.set(config.getId(), config);
      } else {
        final int diff = config.getId() - clusters.size();
        for (int i = 0; i < diff; i++) {
          clusters.add(null);
        }

        clusters.add(config);
        assert clusters.size() - 1 == config.getId();
      }

      storeProperty(CLUSTERS_PREFIX_PROPERTY + config.getId(), updateClusterConfig(config));
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void setClusterStatus(final int clusterId, final OStorageClusterConfiguration.STATUS status) {
    lock.acquireWriteLock();
    try {
      @SuppressWarnings("unchecked")
      final List<OStorageClusterConfiguration> clusters = (List<OStorageClusterConfiguration>) cache.get(CLUSTERS);

      if (clusterId < clusters.size()) {
        final OStorageClusterConfiguration config = clusters.get(clusterId);
        config.setStatus(status);
      }

      final byte[] property = readProperty(CLUSTERS_PREFIX_PROPERTY + clusterId);
      if (property != null) {
        final OStorageClusterConfiguration clusterCfg = deserializeStorageClusterConfig(clusterId, property);
        clusterCfg.setStatus(status);
        updateCluster(clusterCfg);
      }
    } finally {
      lock.releaseWriteLock();
    }
  }

  @Override
  public List<OStorageClusterConfiguration> getClusters() {
    lock.acquireReadLock();
    try {
      //noinspection unchecked
      return Collections.unmodifiableList((List<OStorageClusterConfiguration>) cache.get(CLUSTERS));
    } finally {
      lock.releaseReadLock();
    }
  }

  private void preloadClusters() throws IOException {
    final List<OStorageClusterConfiguration> clusters = new ArrayList<>(1024);
    final OCellBTreeSingleValue.OSBTreeCursor<String, ORID> cursor = btree
        .iterateEntriesMajor(CLUSTERS_PREFIX_PROPERTY, false, true);

    Map.Entry<String, ORID> entry = cursor.next(-1);
    while (entry != null) {
      if (!entry.getKey().startsWith(CLUSTERS_PREFIX_PROPERTY)) {
        break;
      }

      final int id = Integer.parseInt(entry.getKey().substring(CLUSTERS_PREFIX_PROPERTY.length()));
      final ORawBuffer buffer = cluster.readRecord(entry.getValue().getClusterPosition(), false);

      if (clusters.size() <= id) {
        final int diff = id - clusters.size();

        for (int i = 0; i < diff; i++) {
          clusters.add(null);
        }

        clusters.add(deserializeStorageClusterConfig(id, buffer.buffer));
        assert id == clusters.size() - 1;
      } else {
        clusters.set(id, deserializeStorageClusterConfig(id, buffer.buffer));
      }

      entry = cursor.next(-1);
    }

    cache.put(CLUSTERS, clusters);
  }

  public void dropCluster(final int clusterId) {
    lock.acquireWriteLock();
    try {
      @SuppressWarnings("unchecked")
      final List<OStorageClusterConfiguration> clusters = (List<OStorageClusterConfiguration>) cache.get(CLUSTERS);
      if (clusterId < clusters.size()) {
        clusters.set(clusterId, null);
      }

      dropProperty(CLUSTERS_PREFIX_PROPERTY + clusterId);
    } finally {
      lock.releaseWriteLock();
    }
  }

  public void setConfigurationUpdateListener(final OStorageConfigurationUpdateListener updateListener) {
    lock.acquireWriteLock();
    try {
      this.updateListener = updateListener;
    } finally {
      lock.releaseWriteLock();
    }
  }

  private static byte[] serializeIndexEngineProperty(final IndexEngineData indexEngineData) {
    int totalSize = 0;
    final List<byte[]> entries = new ArrayList<>(16);

    final byte[] numericProperties = new byte[3 * OIntegerSerializer.INT_SIZE + 5 * OByteSerializer.BYTE_SIZE];
    totalSize += numericProperties.length;
    entries.add(numericProperties);

    {
      int pos = 0;
      OIntegerSerializer.INSTANCE.serializeNative(indexEngineData.getVersion(), numericProperties, pos);
      pos += OIntegerSerializer.INT_SIZE;
      OIntegerSerializer.INSTANCE.serializeNative(indexEngineData.getApiVersion(), numericProperties, pos);
      pos += OIntegerSerializer.INT_SIZE;

      numericProperties[pos] = indexEngineData.getValueSerializerId();
      pos++;
      numericProperties[pos] = indexEngineData.getKeySerializedId();
      pos++;
      numericProperties[pos] = indexEngineData.isAutomatic() ? (byte) 1 : 0;
      pos++;
      numericProperties[pos] = indexEngineData.isNullValuesSupport() ? (byte) 1 : 0;
      pos++;
      numericProperties[pos] = indexEngineData.isMultivalue() ? (byte) 1 : 0;
      pos++;

      OIntegerSerializer.INSTANCE.serializeNative(indexEngineData.getKeySize(), numericProperties, pos);
    }

    final byte[] algorithm = serializeStringValue(indexEngineData.getAlgorithm());
    totalSize += algorithm.length;
    entries.add(algorithm);

    final byte[] indexType = serializeStringValue(indexEngineData.getIndexType() == null ? "" : indexEngineData.getIndexType());
    entries.add(indexType);
    totalSize += indexType.length;

    final byte[] encryption = serializeStringValue(indexEngineData.getEncryption());
    totalSize += encryption.length;
    entries.add(encryption);

    final OType[] keyTypesValue = indexEngineData.getKeyTypes();
    final byte[] keyTypesSize = new byte[4];
    OIntegerSerializer.INSTANCE.serializeNative(keyTypesValue.length, keyTypesSize, 0);
    totalSize += keyTypesSize.length;
    entries.add(keyTypesSize);

    for (final OType typeValue : keyTypesValue) {
      final byte[] keyTypeName = serializeStringValue(typeValue.name());
      totalSize += keyTypeName.length;
      entries.add(keyTypeName);
    }

    final Map<String, String> engineProperties = indexEngineData.getEngineProperties();
    final byte[] enginePropertiesSize = new byte[OIntegerSerializer.INT_SIZE];
    totalSize += enginePropertiesSize.length;
    entries.add(enginePropertiesSize);

    if (engineProperties != null) {
      OIntegerSerializer.INSTANCE.serializeNative(engineProperties.size(), enginePropertiesSize, 0);

      for (final Map.Entry<String, String> engineProperty : engineProperties.entrySet()) {
        final byte[] key = serializeStringValue(engineProperty.getKey());
        totalSize += key.length;
        entries.add(key);

        final byte[] value = serializeStringValue(engineProperty.getValue());
        totalSize += value.length;
        entries.add(value);
      }
    }

    return mergeBinaryEntries(totalSize, entries);
  }

  private IndexEngineData deserializeIndexEngineProperty(final String name, final byte[] property) {
    int pos = 0;

    final int version = OIntegerSerializer.INSTANCE.deserializeNative(property, pos);
    pos += OIntegerSerializer.INT_SIZE;

    final int apiVersion = OIntegerSerializer.INSTANCE.deserializeNative(property, pos);
    pos += OIntegerSerializer.INT_SIZE;

    final byte valueSerializerId = property[pos];
    pos++;

    final byte keySerializerId = property[pos];
    pos++;

    final boolean isAutomatic = property[pos] == 1;
    pos++;

    final boolean isNullValueSupport = property[pos] == 1;
    pos++;

    final boolean isMultiValue = property[pos] == 1;
    pos++;

    final int keySize = OIntegerSerializer.INSTANCE.deserializeNative(property, pos);
    pos += OIntegerSerializer.INT_SIZE;

    final String algorithm = deserializeStringValue(property, pos);
    pos += getSerializedStringSize(property, pos);

    final String indexType = deserializeStringValue(property, pos);
    pos += getSerializedStringSize(property, pos);

    final String encryption = deserializeStringValue(property, pos);
    pos += getSerializedStringSize(property, pos);

    final int keyTypesSize = OIntegerSerializer.INSTANCE.deserializeNative(property, pos);
    pos += OIntegerSerializer.INT_SIZE;

    final OType[] keyTypes = new OType[keyTypesSize];
    for (int i = 0; i < keyTypesSize; i++) {
      final String typeName = deserializeStringValue(property, pos);
      pos += getSerializedStringSize(property, pos);

      keyTypes[i] = OType.valueOf(typeName);
    }

    final Map<String, String> engineProperties = new HashMap<>(8);
    final int enginePropertiesSize = OIntegerSerializer.INSTANCE.deserializeNative(property, pos);
    pos += OIntegerSerializer.INT_SIZE;

    for (int i = 0; i < enginePropertiesSize; i++) {
      final String key = deserializeStringValue(property, pos);
      pos += getSerializedStringSize(property, pos);

      final String value = deserializeStringValue(property, pos);
      pos += getSerializedStringSize(property, pos);

      engineProperties.put(key, value);
    }

    return new IndexEngineData(name, algorithm, indexType, true, version, apiVersion, isMultiValue, valueSerializerId,
        keySerializerId, isAutomatic, keyTypes, isNullValueSupport, keySize, encryption,
        configuration.getValueAsString(OGlobalConfiguration.STORAGE_ENCRYPTION_KEY), engineProperties);
  }

  private static byte[] mergeBinaryEntries(final int totalSize, final List<byte[]> entries) {
    final byte[] property = new byte[totalSize];
    int pos = 0;
    for (final byte[] entry : entries) {
      System.arraycopy(entry, 0, property, pos, entry.length);
      pos += entry.length;
    }

    assert pos == property.length;
    return property;
  }

  private static byte[] updateClusterConfig(final OStorageClusterConfiguration cluster) {
    int totalSize = 0;
    final List<byte[]> entries = new ArrayList<>(8);

    final byte[] name = serializeStringValue(cluster.getName());
    totalSize += name.length;
    entries.add(name);

    final OStoragePaginatedClusterConfiguration paginatedClusterConfiguration = (OStoragePaginatedClusterConfiguration) cluster;
    final byte[] numericData = new byte[OIntegerSerializer.INT_SIZE + OByteSerializer.BYTE_SIZE];
    totalSize += numericData.length;
    entries.add(numericData);

    numericData[0] = paginatedClusterConfiguration.useWal ? (byte) 1 : 0;

    OIntegerSerializer.INSTANCE.serializeNative(paginatedClusterConfiguration.getBinaryVersion(), numericData, 1);

    final byte[] encryption = serializeStringValue(paginatedClusterConfiguration.encryption);
    totalSize += encryption.length;
    entries.add(encryption);

    final byte[] conflictStrategy = serializeStringValue(paginatedClusterConfiguration.conflictStrategy);
    totalSize += conflictStrategy.length;
    entries.add(conflictStrategy);

    final byte[] status = serializeStringValue(paginatedClusterConfiguration.getStatus().name());
    totalSize += status.length;
    entries.add(status);

    final byte[] compression = serializeStringValue(paginatedClusterConfiguration.compression);
    entries.add(compression);
    totalSize += compression.length;

    return mergeBinaryEntries(totalSize, entries);
  }

  private OStorageClusterConfiguration deserializeStorageClusterConfig(final int id, final byte[] property) {
    int pos = 0;

    final String name = deserializeStringValue(property, pos);
    pos += getSerializedStringSize(property, pos);

    final boolean useWal = (property[pos] == 1);
    pos++;

    final int binaryVersion = OIntegerSerializer.INSTANCE.deserializeNative(property, pos);
    pos += OIntegerSerializer.INT_SIZE;

    final String encryption = deserializeStringValue(property, pos);
    pos += getSerializedStringSize(property, pos);

    final String conflictStrategy = deserializeStringValue(property, pos);
    pos += getSerializedStringSize(property, pos);

    final String status = deserializeStringValue(property, pos);
    pos += getSerializedStringSize(property, pos);

    final String compression = deserializeStringValue(property, pos);

    return new OStoragePaginatedClusterConfiguration(this, id, name, null, useWal, 0, 0, compression, encryption,
        configuration.getValueAsString(OGlobalConfiguration.STORAGE_ENCRYPTION_KEY), conflictStrategy,
        OStorageClusterConfiguration.STATUS.valueOf(status), binaryVersion);
  }

  private void dropProperty(final String name) {
    try {
      boolean rollback = false;
      atomicOperationsManager.startAtomicOperation(COMPONENT_NAME, true);
      try {
        final ORID identifiable = btree.remove(name);

        if (identifiable != null) {
          cluster.deleteRecord(identifiable.getClusterPosition());
        }
      } catch (final Exception e) {
        rollback = true;
        throw e;
      } finally {
        atomicOperationsManager.endAtomicOperation(rollback);
      }
    } catch (final IOException e) {
      throw OException.wrapException(new OStorageException("Error during drop of property " + name), e);
    }

    final PausedNotificationsState pausedNotificationsState = pauseNotifications.get();
    if (updateListener != null) {
      if (!pausedNotificationsState.notificationsPaused) {
        updateListener.onUpdate(this);
        pausedNotificationsState.pendingChanges = 0;
      } else {
        pausedNotificationsState.pendingChanges++;
      }
    }
  }

  private void updateStringProperty(final String name, final String value, final boolean useCache) {
    if (useCache) {
      cache.put(name, value);
    }

    final byte[] property = serializeStringValue(value);

    storeProperty(name, property);
  }

  private static byte[] serializeStringValue(final String value) {
    final byte[] property;
    if (value == null) {
      property = new byte[1];
    } else {
      final byte[] rawString = value.getBytes(StandardCharsets.UTF_16);
      property = new byte[rawString.length + 1 + OIntegerSerializer.INT_SIZE];
      property[0] = 1;

      OIntegerSerializer.INSTANCE.serializeNative(rawString.length, property, 1);

      System.arraycopy(rawString, 0, property, 5, rawString.length);
    }

    return property;
  }

  private static String deserializeStringValue(final byte[] raw, final int start) {
    if (raw[start] == 0) {
      return null;
    }

    final int stringSize = OIntegerSerializer.INSTANCE.deserializeNative(raw, start + 1);
    return new String(raw, start + 5, stringSize, StandardCharsets.UTF_16);
  }

  private static int getSerializedStringSize(final byte[] raw, final int start) {
    if (raw[start] == 0) {
      return 1;
    }

    return OIntegerSerializer.INSTANCE.deserializeNative(raw, start + 1) + 5;
  }

  private void updateIntProperty(final String name, final int value) {
    cache.put(name, value);

    final byte[] property = new byte[OIntegerSerializer.INT_SIZE];
    OIntegerSerializer.INSTANCE.serializeNative(value, property, 0);

    storeProperty(name, property);
  }

  private void storeProperty(final String name, final byte[] property) {
    try {
      boolean rollback = false;
      atomicOperationsManager.startAtomicOperation(COMPONENT_NAME, true);
      try {
        ORID identity = btree.get(name);
        if (identity == null) {
          final OPhysicalPosition position = cluster.createRecord(property, 0, (byte) 0, null);
          identity = new ORecordId(0, position.clusterPosition);
          btree.put(name, identity);
        } else {
          cluster.updateRecord(identity.getClusterPosition(), property, -1, (byte) 0);
        }
      } catch (final Exception e) {
        rollback = true;
        throw e;
      } finally {
        atomicOperationsManager.endAtomicOperation(rollback);
      }
    } catch (final IOException e) {
      throw OException.wrapException(new OStorageException("Error during update of configuration property " + name), e);
    }

    final PausedNotificationsState pausedNotificationsState = pauseNotifications.get();
    if (updateListener != null) {
      if (!pausedNotificationsState.notificationsPaused) {
        pausedNotificationsState.pendingChanges = 0;
        updateListener.onUpdate(this);
      } else {
        pausedNotificationsState.pendingChanges++;
      }
    }
  }

  private byte[] readProperty(final String name) {
    try {
      final ORID rid = btree.get(name);
      if (rid == null) {
        return null;
      }

      final ORawBuffer buffer = cluster.readRecord(rid.getClusterPosition(), false);
      return buffer.buffer;
    } catch (final IOException e) {
      throw OException.wrapException(new OStorageException("Error during read of configuration property " + name), e);
    }
  }

  private boolean containsProperty(@SuppressWarnings("SameParameterValue") final String name) {
    return btree.get(name) != null;
  }

  private String readStringProperty(final String name) {
    return (String) cache.get(name);
  }

  private int readIntProperty(final String name, final boolean useCache) {
    if (useCache) {
      final Object cachedValue = cache.get(name);
      return (int) cachedValue;
    }

    final byte[] property = readProperty(name);
    if (property == null) {
      throw new IllegalStateException("Property " + name + " is absent");
    }

    if (property.length < 4) {
      throw new IllegalStateException("Invalid length of property " + name + " len = " + property.length);
    }

    return OIntegerSerializer.INSTANCE.deserializeNative(property, 0);
  }

  private void preloadIntProperties() {
    for (final String name : INT_PROPERTIES) {
      final byte[] property = readProperty(name);

      if (property != null) {
        cache.put(name, OIntegerSerializer.INSTANCE.deserializeNative(property, 0));
      }
    }
  }

  private void preloadStringProperties() {
    for (final String name : STRING_PROPERTIES) {
      final byte[] property = readProperty(name);

      if (property != null) {
        cache.put(name, deserializeStringValue(property, 0));
      }
    }
  }

  private void init() {
    updateVersion();
    updateBinaryFormatVersion();

    setCharset(DEFAULT_CHARSET);
    setDateFormat(DEFAULT_DATE_FORMAT);
    setDateTimeFormat(DEFAULT_DATETIME_FORMAT);
    setLocaleLanguage(Locale.getDefault().getLanguage());
    setLocaleCountry(Locale.getDefault().getCountry());
    setTimeZone(TimeZone.getDefault());

    setPageSize(-1);
    setFreeListBoundary(-1);
    setMaxKeySize(-1);

    if (!configuration.getContextKeys().contains(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS.getKey())) {
      configuration.setValue(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS,
          OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS.getValueAsInteger()); // 0 = AUTOMATIC
    }
    autoInitClusters();

    updateMinimumClusters();//store inside of configuration

    setRecordSerializerVersion(0);
  }

  private void copy(final OStorageConfiguration storageConfiguration) {
    updateVersion(storageConfiguration.getVersion());
    updateBinaryFormatVersion(storageConfiguration.getBinaryFormatVersion());

    setCharset(storageConfiguration.getCharset());
    setSchemaRecordId(storageConfiguration.getSchemaRecordId());
    setIndexMgrRecordId(storageConfiguration.getIndexMgrRecordId());

    final TimeZone timeZone = storageConfiguration.getTimeZone();
    assert timeZone != null;

    setTimeZone(timeZone);
    setDateFormat(storageConfiguration.getDateFormat());
    setDateTimeFormat(storageConfiguration.getDateTimeFormat());

    this.configuration = storageConfiguration.getContextConfiguration();

    setMinimumClusters(storageConfiguration.getMinimumClusters());

    setLocaleCountry(storageConfiguration.getLocaleCountry());
    setLocaleLanguage(storageConfiguration.getLocaleLanguage());

    final List<OStorageEntryConfiguration> properties = storageConfiguration.getProperties();
    for (final OStorageEntryConfiguration property : properties) {
      setProperty(property.name, property.value);
    }

    setClusterSelection(storageConfiguration.getClusterSelection());
    setConflictStrategy(storageConfiguration.getConflictStrategy());
    setValidation(storageConfiguration.isValidationEnabled());

    final Set<String> indexEngines = storageConfiguration.indexEngines();
    for (final String engine : indexEngines) {
      addIndexEngine(engine, storageConfiguration.getIndexEngine(engine));
    }

    setRecordSerializer(storageConfiguration.getRecordSerializer());
    setRecordSerializerVersion(storageConfiguration.getRecordSerializerVersion());

    final List<OStorageClusterConfiguration> clusters = storageConfiguration.getClusters();
    for (final OStorageClusterConfiguration cluster : clusters) {
      if (cluster != null) {
        updateCluster(cluster);
      }
    }

    setCreationVersion(storageConfiguration.getCreatedAtVersion());
    setPageSize(storageConfiguration.getPageSize());
    setFreeListBoundary(storageConfiguration.getFreeListBoundary());
    setMaxKeySize(storageConfiguration.getMaxKeySize());
  }

  private void autoInitClusters() {
    if (getContextConfiguration().getValueAsInteger(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS) == 0) {
      @SuppressWarnings("SpellCheckingInspection")
      final int cpus = Runtime.getRuntime().availableProcessors();
      getContextConfiguration().setValue(OGlobalConfiguration.CLASS_MINIMUM_CLUSTERS, cpus > 64 ? 64 : cpus);
    }
  }

  private static final class PausedNotificationsState {
    private boolean notificationsPaused;
    private long    pendingChanges;
  }
}


/*
 *
 *  *  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.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import org.apache.commons.configuration.Configuration;

import com.orientechnologies.common.util.OPair;
import com.orientechnologies.orient.core.db.OPartitionedDatabasePool;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.ORecordNotFoundException;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentInternal;
import com.tinkerpop.blueprints.Direction;
import com.tinkerpop.blueprints.Features;
import com.tinkerpop.blueprints.util.ExceptionFactory;

/**
 * A Blueprints implementation of the graph database OrientDB (http://orientdb.com)
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com) (http://orientdb.com)
 */
public class OrientGraph extends OrientTransactionalGraph {
  private boolean          featuresInitialized = false;

  protected final Features FEATURES            = new Features();

  /**
   * Creates a new Transactional Graph using an existent database instance. User and password are passed in case of re-open.
   *
   * @param iDatabase
   *          Underlying database object to attach
   */
  public OrientGraph(final ODatabaseDocumentInternal iDatabase, final String iUserName, final String iUserPasswd) {
    super(iDatabase, true, iUserName, iUserPasswd);
  }

  /**
   * Creates a new Transactional Graph using an existent database instance and the auto-start setting to determine if auto start a
   * transaction.
   *
   * @param iDatabase
   *          Underlying database object to attach
   * @param iAutoStartTx
   *          True to auto start a transaction at the beginning and after each commit/rollback
   */
  public OrientGraph(final ODatabaseDocumentInternal iDatabase, final boolean iAutoStartTx) {
    super(iDatabase, iAutoStartTx, null, null);
  }

  /**
   * Creates a new Transactional Graph from an URL using default user (admin) and password (admin).
   *
   * @param url
   *          OrientDB URL
   */
  public OrientGraph(final String url) {
    super(url, ADMIN, ADMIN);
  }

  /**
   * Creates a new Transactional Graph from an URL using default user (admin) and password (admin). It receives also the auto-start
   * setting to determine if auto start a transaction.
   *
   * @param url
   *          OrientDB URL
   * @param iAutoStartTx
   *          True to auto start a transaction at the beginning and after each commit/rollback
   */
  public OrientGraph(final String url, final boolean iAutoStartTx) {
    super(url, ADMIN, ADMIN, iAutoStartTx);
  }

  /**
   * Creates a new Transactional Graph from an URL using a username and a password.
   *
   * @param url
   *          OrientDB URL
   * @param username
   *          Database user name
   * @param password
   *          Database user password
   */
  public OrientGraph(final String url, final String username, final String password) {
    super(url, username, password);
  }

  /**
   *
   * Creates a new Transactional Graph from an URL using a username and a password. It receives also the auto-start setting to
   * determine if auto start a transaction.
   *
   * @param url
   *          OrientDB URL
   * @param username
   *          Database user name
   * @param password
   *          Database user password
   * @param iAutoStartTx
   *          True to auto start a transaction at the beginning and after each commit/rollback
   */
  public OrientGraph(final String url, final String username, final String password, final boolean iAutoStartTx) {
    super(url, username, password, iAutoStartTx);
  }

  /**
   * Creates a new Transactional Graph from a pool.
   *
   * @param pool
   *          Database pool where to acquire a database instance
   */
  public OrientGraph(final OPartitionedDatabasePool pool) {
    super(pool);
  }

  public OrientGraph(final OPartitionedDatabasePool pool, final Settings configuration) {
    super(pool, configuration);
  }

  /**
   * Builds a OrientGraph instance passing a configuration. Supported configuration settings are:
   *

   *

   * Name
   * Description
   * Default value
   *

   *

   * blueprints.orientdb.url
   * Database URL
   * -
   *

   *

   * blueprints.orientdb.username
   * User name
   * admin
   *

   *

   * blueprints.orientdb.password
   * User password
   * admin
   *

   *

   * blueprints.orientdb.saveOriginalIds
   * Saves the original element IDs by using the property _id. This could be useful on import of graph to preserve original ids
   *
   * false
   *

   *

   * blueprints.orientdb.keepInMemoryReferences
   * Avoid to keep records in memory but only RIDs
   * false
   *

   *

   * blueprints.orientdb.useCustomClassesForEdges
   * Use Edge's label as OrientDB class. If doesn't exist create it under the hood
   * true
   *

   *

   * blueprints.orientdb.useCustomClassesForVertex
   * Use Vertex's label as OrientDB class. If doesn't exist create it under the hood
   * true
   *

   *

   * blueprints.orientdb.useVertexFieldsForEdgeLabels
   * Store the edge relationships in vertex by using the Edge's class. This allow to use multiple fields and make faster
   * traversal by edge's label (class)
   * true
   *

   *

   * blueprints.orientdb.lightweightEdges
   * Uses lightweight edges. This avoid to create a physical document per edge. Documents are created only when they have
   * properties
   * true
   *

   *

   * blueprints.orientdb.autoStartTx
   * Auto start a transaction as soon the graph is changed by adding/remote vertices and edges and properties
   * true
   *

   *

   *
   * @param iConfiguration
   *          graph settings see the details above.
   */
  public OrientGraph(final Configuration iConfiguration) {
    super(iConfiguration);
  }

  /**
   * Creates a new Transactional Graph using an existent database instance.
   *
   * @param iDatabase
   *          Underlying database object to attach
   */
  public OrientGraph(final ODatabaseDocumentInternal iDatabase) {
    super(iDatabase);
  }

  /**
   * Creates a new Transactional Graph using an existent database instance.
   *
   * @param iDatabase
   *          Underlying database object to attach
   */
  public OrientGraph(final ODatabaseDocumentInternal iDatabase, final String iUser, final String iPassword,
      final Settings iConfiguration) {
    super(iDatabase, iUser, iPassword, iConfiguration);
  }

  /**
   * Returns the current Graph settings.
   *
   * @return Features object
   */
  public Features getFeatures() {
    makeActive();

    if (!featuresInitialized) {
      FEATURES.supportsDuplicateEdges = true;
      FEATURES.supportsSelfLoops = true;
      FEATURES.isPersistent = true;
      FEATURES.supportsVertexIteration = true;
      FEATURES.supportsVertexIndex = true;
      FEATURES.ignoresSuppliedIds = true;
      FEATURES.supportsTransactions = true;
      FEATURES.supportsVertexKeyIndex = true;
      FEATURES.supportsKeyIndices = true;
      FEATURES.isWrapper = false;
      FEATURES.supportsIndices = true;
      FEATURES.supportsVertexProperties = true;
      FEATURES.supportsEdgeProperties = true;

      // For more information on supported types, please see:
      // http://code.google.com/p/orient/wiki/Types
      FEATURES.supportsSerializableObjectProperty = true;
      FEATURES.supportsBooleanProperty = true;
      FEATURES.supportsDoubleProperty = true;
      FEATURES.supportsFloatProperty = true;
      FEATURES.supportsIntegerProperty = true;
      FEATURES.supportsPrimitiveArrayProperty = true;
      FEATURES.supportsUniformListProperty = true;
      FEATURES.supportsMixedListProperty = true;
      FEATURES.supportsLongProperty = true;
      FEATURES.supportsMapProperty = true;
      FEATURES.supportsStringProperty = true;
      FEATURES.supportsThreadedTransactions = false;
      FEATURES.supportsThreadIsolatedTransactions = false;

      // DYNAMIC FEATURES BASED ON CONFIGURATION
      FEATURES.supportsEdgeIndex = !isUseLightweightEdges();
      FEATURES.supportsEdgeKeyIndex = !isUseLightweightEdges();
      FEATURES.supportsEdgeIteration = !isUseLightweightEdges();
      FEATURES.supportsEdgeRetrieval = !isUseLightweightEdges();

      featuresInitialized = true;
    }

    return FEATURES;
  }

  OrientEdge addEdgeInternal(final OrientVertex currentVertex, String label, final OrientVertex inVertex, final String iClassName,
      final String iClusterName, final Object... fields) {
    if (currentVertex.checkDeletedInTx())
      throw new ORecordNotFoundException(currentVertex.getIdentity(),
          "The vertex " + currentVertex.getIdentity() + " has been deleted");

    if (inVertex.checkDeletedInTx())
      throw new ORecordNotFoundException(inVertex.getIdentity(), "The vertex " + inVertex.getIdentity() + " has been deleted");

    autoStartTransaction();

    // TEMPORARY STATIC LOCK TO AVOID MT PROBLEMS AGAINST OMVRBTreeRID
    final ODocument outDocument = currentVertex.getRecord();
    if (outDocument == null)
      throw new IllegalArgumentException("source vertex is invalid (rid=" + currentVertex.getIdentity() + ")");

    if (!ODocumentInternal.getImmutableSchemaClass(outDocument).isVertexType())
      throw new IllegalArgumentException("source record is not a vertex");

    ODocument inDocument = inVertex.getRecord();
    if (inDocument == null)
      throw new IllegalArgumentException("destination vertex is invalid (rid=" + inVertex.getIdentity() + ")");

    if (!ODocumentInternal.getImmutableSchemaClass(outDocument).isVertexType())
      throw new IllegalArgumentException("destination record is not a vertex");

    OIdentifiable to;
    OIdentifiable from;

    label = OrientBaseGraph.encodeClassName(label);
    if (label == null && iClassName != null)
      // RETRO-COMPATIBILITY WITH THE SYNTAX CLASS:<CLASS-NAME>
      label = OrientBaseGraph.encodeClassName(iClassName);

    if (isUseClassForEdgeLabel()) {
      final OrientEdgeType edgeType = getEdgeType(label);
      if (edgeType == null)
        // AUTO CREATE CLASS
        createEdgeType(label);
      else
        // OVERWRITE CLASS NAME BECAUSE ATTRIBUTES ARE CASE SENSITIVE
        label = edgeType.getName();
    }

    final String outFieldName = currentVertex.getConnectionFieldName(Direction.OUT, label,
        settings.isUseVertexFieldsForEdgeLabels());
    final String inFieldName = currentVertex.getConnectionFieldName(Direction.IN, label, settings.isUseVertexFieldsForEdgeLabels());

    // since the label for the edge can potentially get re-assigned
    // before being pushed into the OrientEdge, the
    // null check has to go here.
    if (label == null)
      throw ExceptionFactory.edgeLabelCanNotBeNull();

    OrientEdge edge = null;
    if (currentVertex.canCreateDynamicEdge(outDocument, inDocument, outFieldName, inFieldName, fields, label)) {
      // CREATE A LIGHTWEIGHT DYNAMIC EDGE
      from = currentVertex.rawElement;
      to = inDocument;
      if (edge == null) {
        if (settings.isKeepInMemoryReferences())
          edge = getEdgeInstance( from.getIdentity(), to.getIdentity(), label);
        else
          edge = getEdgeInstance( from, to, label);
      }
    } else {
      if (edge == null) {
        // CREATE THE EDGE DOCUMENT TO STORE FIELDS TOO
        edge = getEdgeInstance(label, fields);

        if (settings.isKeepInMemoryReferences())
          edge.getRecord().fields(OrientBaseGraph.CONNECTION_OUT, currentVertex.rawElement.getIdentity(),
              OrientBaseGraph.CONNECTION_IN, inDocument.getIdentity());
        else
          edge.getRecord().fields(OrientBaseGraph.CONNECTION_OUT, currentVertex.rawElement, OrientBaseGraph.CONNECTION_IN,
              inDocument);
      }

      from = edge.getRecord();
      to = edge.getRecord();
    }

    if (settings.isKeepInMemoryReferences()) {
      // USES REFERENCES INSTEAD OF DOCUMENTS
      from = from.getIdentity();
      to = to.getIdentity();
    }

    // OUT-VERTEX ---> IN-VERTEX/EDGE
    currentVertex.createLink(this, outDocument, to, outFieldName);

    // IN-VERTEX ---> OUT-VERTEX/EDGE
    currentVertex.createLink(this, inDocument, from, inFieldName);
    // THE DIRTY MANAGER MANAGE THE SAVE OF ALL LINKED ENTITIES.
    if (!edge.isLightweight())
      edge.save(iClusterName);
    else
      outDocument.save();
    return edge;
  }

  /**
   * Removes the Edge from the Graph. Connected vertices aren't removed.
   */
  public void removeEdgeInternal(final OrientEdge edge) {
    // OUT VERTEX
    final OIdentifiable inVertexEdge = edge.vIn != null ? edge.vIn : edge.rawElement;

    final String edgeClassName = OrientBaseGraph.encodeClassName(edge.getLabel());

    final boolean useVertexFieldsForEdgeLabels = settings.isUseVertexFieldsForEdgeLabels();

    final OIdentifiable outVertex = edge.getOutVertex();
    ODocument outVertexRecord = null;
    boolean outVertexChanged = false;

    if (outVertex != null) {
      outVertexRecord = outVertex.getRecord();
      if (outVertexRecord != null) {
        final String outFieldName = OrientVertex.getConnectionFieldName(Direction.OUT, edgeClassName, useVertexFieldsForEdgeLabels);
        outVertexChanged = edge.dropEdgeFromVertex(inVertexEdge, outVertexRecord, outFieldName,
            outVertexRecord.field(outFieldName));
      }
    }

    // IN VERTEX
    final OIdentifiable outVertexEdge = edge.vOut != null ? edge.vOut : edge.rawElement;

    final OIdentifiable inVertex = edge.getInVertex();
    ODocument inVertexRecord = null;
    boolean inVertexChanged = false;

    if (inVertex != null) {
      inVertexRecord = inVertex.getRecord();
      if (inVertexRecord != null) {
        final String inFieldName = OrientVertex.getConnectionFieldName(Direction.IN, edgeClassName, useVertexFieldsForEdgeLabels);
        inVertexChanged = edge.dropEdgeFromVertex(outVertexEdge, inVertexRecord, inFieldName, inVertexRecord.field(inFieldName));
      }
    }

    if (outVertexChanged)
      outVertexRecord.save();
    if (inVertexChanged)
      inVertexRecord.save();

    if (edge.rawElement != null)
      // NON-LIGHTWEIGHT EDGE
      edge.removeRecord();
  }

  @Override
  void removeEdgesInternal(OrientVertex vertex, ODocument iVertex, OIdentifiable iVertexToRemove, boolean iAlsoInverse,
      boolean useVertexFieldsForEdgeLabels, boolean autoScaleEdgeType) {

    for (String fieldName : iVertex.fieldNames()) {
      final OPair<Direction, String> connection = vertex.getConnection(Direction.BOTH, fieldName);
      if (connection == null)
        // SKIP THIS FIELD
        continue;

      removeEdges(this, iVertex, fieldName, iVertexToRemove, iAlsoInverse, useVertexFieldsForEdgeLabels, autoScaleEdgeType, false);
    }
  }
}


/* Generated By:JJTree: Do not edit this line. OCreateLinkStatement.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.command.OCommandContext;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordLazyList;
import com.orientechnologies.orient.core.db.record.ORecordLazySet;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.intent.OIntentMassiveInsert;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentHelper;
import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.executor.OInternalResultSet;
import com.orientechnologies.orient.core.sql.executor.OResultInternal;
import com.orientechnologies.orient.core.sql.executor.OResultSet;

import java.util.*;

public class OCreateLinkStatement extends OSimpleExecStatement {

  protected OIdentifier      name;
  protected OIdentifier      type;
  protected OIdentifier      sourceClass;
  protected OIdentifier      sourceField;
  protected ORecordAttribute sourceRecordAttr;
  protected OIdentifier      destClass;
  protected OIdentifier      destField;
  protected ORecordAttribute destRecordAttr;
  protected boolean inverse = false;

  boolean breakExec = false;//for timeout

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

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

  @Override public OResultSet executeSimple(OCommandContext ctx) {
    execute(ctx);
    OInternalResultSet rs = new OInternalResultSet();
    OResultInternal result = new OResultInternal();
    result.setProperty("operation", "create link");
    result.setProperty("name", name.getValue());
    result.setProperty("fromClass", sourceClass.getStringValue());
    result.setProperty("toClass", destClass.getStringValue());
    rs.add(result);
    return rs;
  }

  /**
   * Execute the CREATE LINK.
   */
  private Object execute(OCommandContext ctx) {
    if (destField == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");

    final ODatabaseDocumentInternal database = getDatabase();
    if (!(database.getDatabaseOwner() instanceof ODatabaseDocument))
      throw new OCommandSQLParsingException(
          "This command supports only the database type ODatabaseDocumentTx and type '" + database.getClass() + "' was found");

    final ODatabaseDocument db = (ODatabaseDocument) database.getDatabaseOwner();

    final OClass sourceClass = database.getMetadata().getSchema().getClass(getSourceClass().getStringValue());
    if (sourceClass == null)
      throw new OCommandExecutionException("Source class '" + getSourceClass().getStringValue() + "' not found");

    final OClass destClass = database.getMetadata().getSchema().getClass(getDestClass().getStringValue());
    if (destClass == null)
      throw new OCommandExecutionException("Destination class '" + getDestClass().getStringValue() + "' not found");

    Object value;

    String cmd = "select from ";
    if (!ODocumentHelper.ATTRIBUTE_RID.equals(destField)) {
      cmd = "select from " + getDestClass() + " where " + destField + " = ";
    }

    List<ODocument> result;
    ODocument target;
    Object oldValue;
    long total = 0;

    String linkName = name == null ? sourceField.getStringValue() : name.getStringValue();

    boolean multipleRelationship;
    OType linkType = OType.valueOf(type.getStringValue().toUpperCase(Locale.ENGLISH));
    if (linkType != null)
      // DETERMINE BASED ON FORCED TYPE
      multipleRelationship = linkType == OType.LINKSET || linkType == OType.LINKLIST;
    else
      multipleRelationship = false;

    long totRecords = db.countClass(sourceClass.getName());
    long currRecord = 0;

    database.declareIntent(new OIntentMassiveInsert());
    try {
      // BROWSE ALL THE RECORDS OF THE SOURCE CLASS
      for (ODocument doc : db.browseClass(sourceClass.getName())) {
        if (breakExec) {
          break;
        }
        value = doc.getProperty(sourceField.getStringValue());

        if (value != null) {
          if (value instanceof ODocument || value instanceof ORID) {
            // ALREADY CONVERTED
          } else if (value instanceof Collection<?>) {
            // TODO
          } else {
            // SEARCH THE DESTINATION RECORD
            target = null;

            if (!ODocumentHelper.ATTRIBUTE_RID.equals(destField) && value instanceof String)
              if (((String) value).length() == 0)
                value = null;
              else
                value = "'" + value + "'";

            OResultSet rs = database.query(cmd + value);
            result = toList(rs);
            rs.close();

            if (result == null || result.size() == 0)
              value = null;
            else if (result.size() > 1)
              throw new OCommandExecutionException(
                  "Cannot create link because multiple records was found in class '" + destClass.getName() + "' with value " + value
                      + " in field '" + destField + "'");
            else {
              target = result.get(0);
              value = target;
            }

            if (target != null && inverse) {
              // INVERSE RELATIONSHIP
              oldValue = target.getProperty(linkName);

              if (oldValue != null) {
                if (!multipleRelationship)
                  multipleRelationship = true;

                Collection<ODocument> coll;
                if (oldValue instanceof Collection) {
                  // ADD IT IN THE EXISTENT COLLECTION
                  coll = (Collection<ODocument>) oldValue;
                  target.setDirty();
                } else {
                  // CREATE A NEW COLLECTION FOR BOTH
                  coll = new ArrayList<ODocument>(2);
                  target.setProperty(linkName, coll);
                  coll.add((ODocument) oldValue);
                }
                coll.add(doc);
              } else {
                if (linkType != null)
                  if (linkType == OType.LINKSET) {
                    value = new ORecordLazySet(target);
                    ((Set<OIdentifiable>) value).add(doc);
                  } else if (linkType == OType.LINKLIST) {
                    value = new ORecordLazyList(target);
                    ((ORecordLazyList) value).add(doc);
                  } else
                    // IGNORE THE TYPE, SET IT AS LINK
                    value = doc;
                else
                  value = doc;

                target.setProperty(linkName, value);
              }
              target.save();

            } else {
              // SET THE REFERENCE
              doc.setProperty(linkName, value);
              doc.save();
            }

            total++;
          }
        }
      }

      if (total > 0) {
        if (inverse) {
          // REMOVE THE OLD PROPERTY IF ANY
          OProperty prop = destClass.getProperty(linkName);
          if (prop != null)
            destClass.dropProperty(linkName);

          if (linkType == null)
            linkType = multipleRelationship ? OType.LINKSET : OType.LINK;

          // CREATE THE PROPERTY
          destClass.createProperty(linkName, linkType, sourceClass);

        } else {

          // REMOVE THE OLD PROPERTY IF ANY
          OProperty prop = sourceClass.getProperty(linkName);
          if (prop != null)
            sourceClass.dropProperty(linkName);

          // CREATE THE PROPERTY
          sourceClass.createProperty(linkName, OType.LINK, destClass);
        }
      }
    } catch (Exception e) {
      throw OException.wrapException(new OCommandExecutionException("Error on creation of links"), e);
    } finally {
      database.declareIntent(null);
    }
    return total;
  }

  private List<ODocument> toList(OResultSet rs) {
    if (!rs.hasNext()) {
      return null;
    }
    List<ODocument> result = new ArrayList<>();
    while (rs.hasNext()) {
      result.add((ODocument) rs.next().getElement().orElse(null));
    }
    return result;
  }

  @Override public void toString(Map<Object, Object> params, StringBuilder builder) {
    builder.append("CREATE LINK ");
    name.toString(params, builder);
    builder.append(" TYPE ");
    type.toString(params, builder);
    builder.append(" FROM ");
    sourceClass.toString(params, builder);
    builder.append(".");
    if (sourceField != null) {
      sourceField.toString(params, builder);
    } else {
      sourceRecordAttr.toString(params, builder);
    }
    builder.append(" TO ");
    destClass.toString(params, builder);
    builder.append(".");
    if (destField != null) {
      destField.toString(params, builder);
    } else {
      destRecordAttr.toString(params, builder);
    }
    if (inverse) {
      builder.append(" INVERSE");
    }
  }

  @Override public OCreateLinkStatement copy() {
    OCreateLinkStatement result = new OCreateLinkStatement(-1);
    result.name = name == null ? null : name.copy();
    result.type = type == null ? null : type.copy();
    result.sourceClass = sourceClass == null ? null : sourceClass.copy();
    result.sourceField = sourceField == null ? null : sourceField.copy();
    result.sourceRecordAttr = sourceRecordAttr == null ? null : sourceRecordAttr.copy();
    result.destClass = destClass == null ? null : destClass.copy();
    result.destField = destField == null ? null : destField.copy();
    result.destRecordAttr = destRecordAttr == null ? null : destRecordAttr.copy();
    result.inverse = inverse;
    return result;
  }

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

    OCreateLinkStatement that = (OCreateLinkStatement) o;

    if (inverse != that.inverse)
      return false;
    if (name != null ? !name.equals(that.name) : that.name != null)
      return false;
    if (type != null ? !type.equals(that.type) : that.type != null)
      return false;
    if (sourceClass != null ? !sourceClass.equals(that.sourceClass) : that.sourceClass != null)
      return false;
    if (sourceField != null ? !sourceField.equals(that.sourceField) : that.sourceField != null)
      return false;
    if (sourceRecordAttr != null ? !sourceRecordAttr.equals(that.sourceRecordAttr) : that.sourceRecordAttr != null)
      return false;
    if (destClass != null ? !destClass.equals(that.destClass) : that.destClass != null)
      return false;
    if (destField != null ? !destField.equals(that.destField) : that.destField != null)
      return false;
    if (destRecordAttr != null ? !destRecordAttr.equals(that.destRecordAttr) : that.destRecordAttr != null)
      return false;

    return true;
  }

  @Override public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + (type != null ? type.hashCode() : 0);
    result = 31 * result + (sourceClass != null ? sourceClass.hashCode() : 0);
    result = 31 * result + (sourceField != null ? sourceField.hashCode() : 0);
    result = 31 * result + (sourceRecordAttr != null ? sourceRecordAttr.hashCode() : 0);
    result = 31 * result + (destClass != null ? destClass.hashCode() : 0);
    result = 31 * result + (destField != null ? destField.hashCode() : 0);
    result = 31 * result + (destRecordAttr != null ? destRecordAttr.hashCode() : 0);
    result = 31 * result + (inverse ? 1 : 0);
    return result;
  }

  public OIdentifier getName() {
    return name;
  }

  public OIdentifier getType() {
    return type;
  }

  public OIdentifier getSourceClass() {
    return sourceClass;
  }

  public OIdentifier getSourceField() {
    return sourceField;
  }

  public ORecordAttribute getSourceRecordAttr() {
    return sourceRecordAttr;
  }

  public OIdentifier getDestClass() {
    return destClass;
  }

  public OIdentifier getDestField() {
    return destField;
  }

  public ORecordAttribute getDestRecordAttr() {
    return destRecordAttr;
  }

  public boolean isInverse() {
    return inverse;
  }
}
/* JavaCC - OriginalChecksum=de46c9bdaf3b36691764a78cd89d1c2b (do not edit this line) */