/*
 *
 * 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.
 */
package com.orientechnologies.orient.object.enhancement;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.reflection.OReflectionHelper;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.object.ODatabaseObject;
import com.orientechnologies.orient.core.db.object.OObjectLazyMultivalueElement;
import com.orientechnologies.orient.core.db.record.*;
import com.orientechnologies.orient.core.exception.OSerializationException;
import com.orientechnologies.orient.core.hook.ORecordHook.TYPE;
import com.orientechnologies.orient.core.id.ORID;
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.ORecordAbstract;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentInternal;
import com.orientechnologies.orient.object.db.OObjectLazyList;
import com.orientechnologies.orient.object.db.OObjectLazyMap;
import com.orientechnologies.orient.object.db.OObjectLazySet;
import com.orientechnologies.orient.object.enhancement.field.ODocumentFieldHandler;
import com.orientechnologies.orient.object.enumerations.OObjectEnumLazyList;
import com.orientechnologies.orient.object.enumerations.OObjectEnumLazyMap;
import com.orientechnologies.orient.object.enumerations.OObjectEnumLazySet;
import com.orientechnologies.orient.object.enumerations.OObjectLazyEnumSerializer;
import com.orientechnologies.orient.object.serialization.OObjectCustomSerializerList;
import com.orientechnologies.orient.object.serialization.OObjectCustomSerializerMap;
import com.orientechnologies.orient.object.serialization.OObjectCustomSerializerSet;
import com.orientechnologies.orient.object.serialization.OObjectLazyCustomSerializer;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyObject;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * @author Luca Molino (molino.luca--at--gmail.com)
 */
public class OObjectProxyMethodHandler implements MethodHandler {

  protected final Map<String, Integer> loadedFields;
  protected final Set<ORID> orphans = new HashSet<ORID>();
  protected ODocument   doc;
  protected ProxyObject parentObject;
  protected boolean     reading;

  public OObjectProxyMethodHandler(ODocument iDocument) {
    doc = iDocument;
    final ODatabaseDocumentInternal db = ODatabaseRecordThreadLocal.instance().get();
    if (db.getDatabaseOwner() instanceof ODatabaseObject && !((ODatabaseObject) db.getDatabaseOwner()).isLazyLoading())
      doc.detach();
    loadedFields = new HashMap<String, Integer>();
  }

  public ODocument getDoc() {
    return doc;
  }

  public void setDoc(ODocument iDoc) {
    doc = iDoc;
  }

  public ProxyObject getParentObject() {
    return parentObject;
  }

  public void setParentObject(ProxyObject parentDoc) {
    this.parentObject = parentDoc;
  }

  public Set<ORID> getOrphans() {
    return orphans;
  }

  public Object invoke(final Object self, final Method m, final Method proceed, final Object[] args) throws Throwable {
    final OObjectMethodFilter filter = OObjectEntityEnhancer.getInstance().getMethodFilter(self.getClass());
    if (filter.isSetterMethod(m)) {
      return manageSetMethod(self, m, proceed, args);
    } else if (filter.isGetterMethod(m)) {
      return manageGetMethod(self, m, proceed, args);
    }
    return proceed.invoke(self, args);
  }

  /**
   * Method that detaches all fields contained in the document to the given object
   *
   * @param self :- The object containing this handler instance
   *
   * @throws InvocationTargetException
   * @throws IllegalAccessException
   * @throws NoSuchMethodException
   */
  public void detach(final Object self, final boolean nonProxiedInstance)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {

    final Class<?> selfClass = self.getClass();

    for (String fieldName : doc.fieldNames()) {
      Object value = getValue(self, fieldName, false, null, true);
      if (value instanceof OObjectLazyMultivalueElement) {
        ((OObjectLazyMultivalueElement<?>) value).detach(nonProxiedInstance);
        if (nonProxiedInstance)
          value = ((OObjectLazyMultivalueElement<?>) value).getNonOrientInstance();
      }
      OObjectEntitySerializer.setFieldValue(OObjectEntitySerializer.getField(fieldName, selfClass), self, value);
    }
    OObjectEntitySerializer.setIdField(selfClass, self, doc.getIdentity());
    OObjectEntitySerializer.setVersionField(selfClass, self, doc.getVersion());
  }

  /**
   * Method that detaches all fields contained in the document to the given object
   *
   * @param self :- The object containing this handler instance
   *
   * @throws InvocationTargetException
   * @throws IllegalAccessException
   * @throws NoSuchMethodException
   */
  public void detachAll(final Object self, final boolean nonProxiedInstance, final Map<Object, Object> alreadyDetached,
      final Map<Object, Object> lazyObjects) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    final Class<?> selfClass = self.getClass();

    for (String fieldName : doc.fieldNames()) {
      final Field field = OObjectEntitySerializer.getField(fieldName, selfClass);
      if (field != null) {
        Object value = getValue(self, fieldName, false, null, true);
        if (value instanceof OObjectLazyMultivalueElement) {
          ((OObjectLazyMultivalueElement<?>) value).detachAll(nonProxiedInstance, alreadyDetached, lazyObjects);
          if (nonProxiedInstance)
            value = ((OObjectLazyMultivalueElement<?>) value).getNonOrientInstance();
        } else if (value instanceof Proxy) {
          OObjectProxyMethodHandler handler = (OObjectProxyMethodHandler) ((ProxyObject) value).getHandler();
          if (nonProxiedInstance) {
            value = OObjectEntitySerializer.getNonProxiedInstance(value);
          }

          if (OObjectEntitySerializer.isFetchLazyField(self.getClass(), fieldName)) {
            // just make a placeholder with only the id, so it can be fetched later (but not by orient
            // internally)
            // do not use the already detached map for this, that might mix up lazy and non-lazy objects
            Object lazyValue = lazyObjects.get(handler.doc.getIdentity());
            if (lazyValue != null) {
              value = lazyValue;
            } else {
              OObjectEntitySerializer.setIdField(field.getType(), value, handler.doc.getIdentity());
              lazyObjects.put(handler.doc.getIdentity(), value);
            }
          } else {
            Object detachedValue = alreadyDetached.get(handler.doc.getIdentity());
            if (detachedValue != null) {
              value = detachedValue;
            } else {
              ORID identity = handler.doc.getIdentity();
              if (identity.isValid())
                alreadyDetached.put(identity, value);
              handler.detachAll(value, nonProxiedInstance, alreadyDetached, lazyObjects);
            }
          }
        } else if (value instanceof OTrackedMap && nonProxiedInstance) {
          Map newValue = new LinkedHashMap<>();
          newValue.putAll((Map) value);
          value = newValue;
        } else if (value instanceof OTrackedList && nonProxiedInstance) {
          List newValue = new ArrayList();
          newValue.addAll((Collection) value);
          value = newValue;
        } else if (value instanceof OTrackedSet && nonProxiedInstance) {
          Set newValue = new LinkedHashSet();
          newValue.addAll((Collection) value);
          value = newValue;
        }
        OObjectEntitySerializer.setFieldValue(field, self, value);
      }
    }
    OObjectEntitySerializer.setIdField(selfClass, self, doc.getIdentity());
    OObjectEntitySerializer.setVersionField(selfClass, self, doc.getVersion());
  }

  /**
   * Method that attaches all data contained in the object to the associated document
   *
   * @param self :- The object containing this handler instance
   *
   * @throws IllegalAccessException
   * @throws IllegalArgumentException
   * @throws InvocationTargetException
   * @throws NoSuchMethodException
   */
  public void attach(final Object self)
      throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    for (Class<?> currentClass = self.getClass(); currentClass != Object.class; ) {
      if (Proxy.class.isAssignableFrom(currentClass)) {
        currentClass = currentClass.getSuperclass();
        continue;
      }
      for (Field f : currentClass.getDeclaredFields()) {
        final String fieldName = f.getName();
        final Class<?> declaringClass = f.getDeclaringClass();

        if (OObjectEntitySerializer.isTransientField(declaringClass, fieldName) || OObjectEntitySerializer
            .isVersionField(declaringClass, fieldName) || OObjectEntitySerializer.isIdField(declaringClass, fieldName))
          continue;

        Object value = OObjectEntitySerializer.getFieldValue(f, self);
        value = setValue(self, fieldName, value);
        OObjectEntitySerializer.setFieldValue(f, self, value);
      }
      currentClass = currentClass.getSuperclass();

      if (currentClass == null || currentClass.equals(ODocument.class))
        // POJO EXTENDS ODOCUMENT: SPECIAL CASE: AVOID TO CONSIDER
        // ODOCUMENT FIELDS
        currentClass = Object.class;
    }
  }

  public void setDirty() {
    doc.setDirty();
    if (parentObject != null)
      ((OObjectProxyMethodHandler) parentObject.getHandler()).setDirty();
  }

  public void updateLoadedFieldMap(final Object proxiedObject, final boolean iReload) {
    final Set<String> fields = new HashSet<String>(loadedFields.keySet());
    for (String fieldName : fields) {
      try {
        if (iReload) {
          // FORCE POJO FIELD VALUE TO DEFAULT VALUE, WHICH CAN BE null, 0 or false
          final Field fieldToReset = OObjectEntitySerializer.getField(fieldName, proxiedObject.getClass());
          OObjectEntitySerializer.setFieldValue(fieldToReset, proxiedObject, getDefaultValueForField(fieldToReset));
        } else {
          final Object value = getValue(proxiedObject, fieldName, false, null);

          if (value instanceof OObjectLazyMultivalueElement) {
            if (((OObjectLazyMultivalueElement<?>) value).getUnderlying() != doc.field(fieldName))
              loadedFields.remove(fieldName);
          } else {
            loadedFields.put(fieldName, doc.getVersion());
          }
        }
      } catch (IllegalArgumentException e) {
        throw OException
            .wrapException(new OSerializationException("Error updating object after save of class " + proxiedObject.getClass()), e);
      } catch (IllegalAccessException e) {
        throw OException
            .wrapException(new OSerializationException("Error updating object after save of class " + proxiedObject.getClass()), e);
      } catch (NoSuchMethodException e) {
        throw OException
            .wrapException(new OSerializationException("Error updating object after save of class " + proxiedObject.getClass()), e);
      } catch (InvocationTargetException e) {
        throw OException
            .wrapException(new OSerializationException("Error updating object after save of class " + proxiedObject.getClass()), e);
      }
    }

    if (iReload) {
      // RESET LOADED FIELDS, SO THE MUST BE RELOADED FROM DATABASE
      loadedFields.clear();
    }
  }

  protected Object manageGetMethod(final Object self, final Method m, final Method proceed, final Object[] args)
      throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException, IllegalArgumentException,
      NoSuchFieldException {
    this.reading = true;
    final String fieldName = OObjectEntityEnhancer.getInstance().getMethodFilter(self.getClass()).getFieldName(m);

    final ORID docRID = doc.getIdentity();

    final boolean idOrVersionField;
    if (OObjectEntitySerializer.isIdField(m.getDeclaringClass(), fieldName)) {
      idOrVersionField = true;
      OObjectEntitySerializer.setIdField(m.getDeclaringClass(), self, docRID);
    } else if (OObjectEntitySerializer.isVersionField(m.getDeclaringClass(), fieldName)) {
      idOrVersionField = true;
      if (docRID.isValid() && !docRID.isTemporary())
        OObjectEntitySerializer.setVersionField(m.getDeclaringClass(), self, doc.getVersion());
    } else
      idOrVersionField = false;

    Object value = proceed.invoke(self, args);

    value = getValue(self, fieldName, idOrVersionField, value);
    if (docRID.isValid() && !docRID.isTemporary())
      loadedFields.put(fieldName, doc.getVersion());
    else
      loadedFields.put(fieldName, 0);
    this.reading = false;
    return value;
  }

  protected Object getValue(final Object self, final String fieldName, final boolean idOrVersionField, Object value)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    return getValue(self, fieldName, idOrVersionField, value, false);
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  protected Object getValue(final Object self, final String fieldName, final boolean idOrVersionField, Object value,
      final boolean iIgnoreLoadedFields) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    if (!idOrVersionField) {
      if (value == null) {
        if (!iIgnoreLoadedFields && loadedFields.containsKey(fieldName)
            && loadedFields.get(fieldName).compareTo(doc.getVersion()) == 0) {
          return null;
        } else {
          final Object docValue = getDocFieldValue(self, fieldName);
          if (docValue != null) {
            value = lazyLoadField(self, fieldName, docValue, value);
          }
        }
      } else {
        if (((value instanceof Collection<?> || value instanceof Map<?, ?>) && !(value instanceof OObjectLazyMultivalueElement))
            || value.getClass().isArray()) {
          final Class<?> genericMultiValueType = OReflectionHelper
              .getGenericMultivalueType(OObjectEntitySerializer.getField(fieldName, self.getClass()));
          if (genericMultiValueType == null || !OReflectionHelper.isJavaType(genericMultiValueType)) {
            final Field f = OObjectEntitySerializer.getField(fieldName, self.getClass());
            if (OObjectEntitySerializer.isSerializedType(f) && !(value instanceof OObjectLazyCustomSerializer)) {
              value = manageSerializedCollections(self, fieldName, value);
            } else if (genericMultiValueType != null && genericMultiValueType.isEnum()
                && !(value instanceof OObjectLazyEnumSerializer)) {
              value = manageEnumCollections(self, f.getName(), genericMultiValueType, value);
            } else {
              value = manageObjectCollections(self, fieldName, value);
            }
          } else {
            final Object docValue = getDocFieldValue(self, fieldName);
            if (docValue == null) {
              if (value.getClass().isArray()) {
                OClass schemaClass = doc.getSchemaClass();
                OProperty schemaProperty = null;
                if (schemaClass != null)
                  schemaProperty = schemaClass.getProperty(fieldName);

                doc.field(fieldName, OObjectEntitySerializer
                    .typeToStream(value, schemaProperty != null ? schemaProperty.getType() : null, getDatabase(), doc));
              } else
                doc.field(fieldName, value);

            } else if (!loadedFields.containsKey(fieldName)) {
              value = manageArrayFieldObject(OObjectEntitySerializer.getField(fieldName, self.getClass()), self, docValue);
              Method setMethod = getSetMethod(self.getClass().getSuperclass(), getSetterFieldName(fieldName), value);
              setMethod.invoke(self, value);
            } else if ((value instanceof Set || value instanceof Map) && loadedFields.get(fieldName).compareTo(doc.getVersion()) < 0
                && !OReflectionHelper.isJavaType(genericMultiValueType)) {
              if (value instanceof Set)
                value = new OObjectLazySet(self, (Set<OIdentifiable>) docValue,
                    OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), fieldName));
              else
                value = new OObjectLazyMap(self, (Map<Object, OIdentifiable>) docValue,
                    OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), fieldName));
              final Method setMethod = getSetMethod(self.getClass().getSuperclass(), getSetterFieldName(fieldName), value);
              setMethod.invoke(self, value);
            }
          }
        } else if (!loadedFields.containsKey(fieldName) || loadedFields.get(fieldName).compareTo(doc.getVersion()) < 0) {
          final Object docValue = getDocFieldValue(self, fieldName);
          if (docValue != null && !docValue.equals(value)) {
            value = lazyLoadField(self, fieldName, docValue, value);
          }
        }
      }
    }
    return value;
  }

  protected Object getDocFieldValue(final Object self, final String fieldName) {
    final OClass cls = doc.getSchemaClass();
    if (cls != null && cls.existsProperty(fieldName))
      return doc.field(fieldName);
    else {
      OType expected = OObjectEntitySerializer.getTypeByClass(self.getClass(), fieldName);
      return ODocumentFieldHandler.getStrategy(this.getDatabase()).load(doc, fieldName, expected);
    }
  }

  protected Object setDocFieldValue(final String fieldName, final Object value, final OType type) {
    return ODocumentFieldHandler.getStrategy(this.getDatabase()).store(doc, fieldName, value, type);
  }

  protected Object manageObjectCollections(final Object self, final String fieldName, Object value)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    boolean customSerialization = false;
    final Field f = OObjectEntitySerializer.getField(fieldName, self.getClass());
    if (OObjectEntitySerializer.isSerializedType(f)) {
      customSerialization = true;
    }
    if (value instanceof Collection<?>) {
      value = manageCollectionSave(self, f, (Collection<?>) value, customSerialization, false);
    } else if (value instanceof Map<?, ?>) {
      value = manageMapSave(self, f, (Map<?, ?>) value, customSerialization);
    } else if (value.getClass().isArray()) {
      value = manageArraySave(fieldName, (Object[]) value);
    }
    OObjectEntitySerializer.setFieldValue(OObjectEntitySerializer.getField(fieldName, self.getClass()), self, value);
    return value;
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected Object manageSerializedCollections(final Object self, final String fieldName, Object value)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    if (value instanceof Collection<?>) {
      if (value instanceof List) {
        List<Object> docList = doc.field(fieldName, OType.EMBEDDEDLIST);
        if (docList == null) {
          docList = new ArrayList<Object>();
          setDocFieldValue(fieldName, docList, OType.EMBEDDEDLIST);
        }
        value = new OObjectCustomSerializerList(
            OObjectEntitySerializer.getSerializedType(OObjectEntitySerializer.getField(fieldName, self.getClass())), doc, docList,
            (List<?>) value);
      } else if (value instanceof Set) {
        Set<Object> docSet = doc.field(fieldName, OType.EMBEDDEDSET);
        if (docSet == null) {
          docSet = new HashSet<Object>();
          setDocFieldValue(fieldName, docSet, OType.EMBEDDEDSET);
        }
        value = new OObjectCustomSerializerSet(
            OObjectEntitySerializer.getSerializedType(OObjectEntitySerializer.getField(fieldName, self.getClass())), doc, docSet,
            (Set<?>) value);
      }
    } else if (value instanceof Map<?, ?>) {
      Map<Object, Object> docMap = doc.field(fieldName, OType.EMBEDDEDMAP);
      if (docMap == null) {
        docMap = new HashMap<Object, Object>();
        setDocFieldValue(fieldName, docMap, OType.EMBEDDEDMAP);
      }
      value = new OObjectCustomSerializerMap(
          OObjectEntitySerializer.getSerializedType(OObjectEntitySerializer.getField(fieldName, self.getClass())), doc, docMap,
          (Map<Object, Object>) value);
    } else if (value.getClass().isArray()) {
      value = manageArraySave(fieldName, (Object[]) value);
    }
    OObjectEntitySerializer.setFieldValue(OObjectEntitySerializer.getField(fieldName, self.getClass()), self, value);
    return value;
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected Object manageEnumCollections(final Object self, final String fieldName, final Class<?> enumClass, Object value)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    if (value instanceof Collection<?>) {
      if (value instanceof List) {
        List<Object> docList = doc.field(fieldName, OType.EMBEDDEDLIST);
        if (docList == null) {
          docList = new ArrayList<Object>();
          setDocFieldValue(fieldName, docList, OType.EMBEDDEDLIST);
        }
        value = new OObjectEnumLazyList(enumClass, doc, docList, (List<?>) value);
      } else if (value instanceof Set) {
        Set<Object> docSet = doc.field(fieldName, OType.EMBEDDEDSET);
        if (docSet == null) {
          docSet = new HashSet<Object>();
          setDocFieldValue(fieldName, docSet, OType.EMBEDDEDSET);
        }
        value = new OObjectEnumLazySet(enumClass, doc, docSet, (Set<?>) value);
      }
    } else if (value instanceof Map<?, ?>) {
      Map<Object, Object> docMap = doc.field(fieldName, OType.EMBEDDEDMAP);
      if (docMap == null) {
        docMap = new HashMap<Object, Object>();
        setDocFieldValue(fieldName, docMap, OType.EMBEDDEDMAP);
      }
      value = new OObjectEnumLazyMap(enumClass, doc, docMap, (Map<?, ?>) value);
    } else if (value.getClass().isArray()) {
      value = manageArraySave(fieldName, (Object[]) value);
    }
    OObjectEntitySerializer.setFieldValue(OObjectEntitySerializer.getField(fieldName, self.getClass()), self, value);
    return value;
  }

  protected Object manageArraySave(final String iFieldName, final Object[] value) {
    if (value.length > 0) {
      final Object o = ((Object[]) value)[0];
      if (o instanceof Proxy || o.getClass().isEnum()) {
        Object[] newValue = new Object[value.length];
        convertArray(value, newValue, o.getClass().isEnum());
        doc.field(iFieldName, newValue);
      }
    }
    return value;
  }

  @SuppressWarnings("rawtypes")
  protected void convertArray(final Object[] value, Object[] newValue, boolean isEnum) {
    for (int i = 0; i < value.length; i++) {
      if (isEnum) {
        newValue[i] = value[i] != null ? ((Enum) value[i]).name() : null;
      } else {
        newValue[i] = value[i] != null ? OObjectEntitySerializer.getDocument((Proxy) value[i]) : null;
      }
    }
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  protected Object manageMapSave(final Object self, final Field f, Map<?, ?> value, final boolean customSerialization) {
    final Class genericType = OReflectionHelper.getGenericMultivalueType(f);
    if (customSerialization) {
      Map<Object, Object> map = new HashMap<Object, Object>();
      setDocFieldValue(f.getName(), map, OType.EMBEDDEDMAP);
      value = new OObjectCustomSerializerMap<TYPE>(OObjectEntitySerializer.getSerializedType(f), doc, map,
          (Map<Object, Object>) value);
    } else if (genericType != null && genericType.isEnum()) {
      Map<Object, Object> map = new HashMap<Object, Object>();
      setDocFieldValue(f.getName(), map, OType.EMBEDDEDMAP);
      value = new OObjectEnumLazyMap(genericType, doc, map, (Map<Object, Object>) value);
    } else if (!(value instanceof OObjectLazyMultivalueElement)) {
      OType type = OObjectEntitySerializer.isEmbeddedField(self.getClass(), f.getName()) ? OType.EMBEDDEDMAP : OType.LINKMAP;
      if (doc.fieldType(f.getName()) != type)
        doc.field(f.getName(), doc.field(f.getName()), type);
      Map<Object, OIdentifiable> docMap = doc.field(f.getName(), type);
      if (docMap == null) {
        if (OType.EMBEDDEDMAP == type)
          docMap = new OTrackedMap<OIdentifiable>(doc);
        else
          docMap = new ORecordLazyMap(doc);
        setDocFieldValue(f.getName(), docMap, type);
      }
      value = new OObjectLazyMap(self, docMap, value, OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), f.getName()));
    }
    return value;
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected Object manageCollectionSave(final Object self, final Field f, Collection<?> value, final boolean customSerialization,
      final boolean isFieldUpdate) {
    final Class genericType = OReflectionHelper.getGenericMultivalueType(f);
    if (customSerialization) {
      if (value instanceof List<?>) {
        final List<Object> list = new ArrayList<Object>();
        setDocFieldValue(f.getName(), list, OType.EMBEDDEDLIST);
        value = new OObjectCustomSerializerList(OObjectEntitySerializer.getSerializedType(f), doc, new ArrayList<Object>(),
            (List<Object>) value);
      } else {
        final Set<Object> set = new HashSet<Object>();
        setDocFieldValue(f.getName(), set, OType.EMBEDDEDSET);
        value = new OObjectCustomSerializerSet(OObjectEntitySerializer.getSerializedType(f), doc, set, (Set<Object>) value);
      }
    } else if (genericType != null && genericType.isEnum()) {
      if (value instanceof List<?>) {
        final List<Object> list = new ArrayList<Object>();
        setDocFieldValue(f.getName(), list, OType.EMBEDDEDLIST);
        value = new OObjectEnumLazyList(genericType, doc, list, (List<Object>) value);
      } else {
        final Set<Object> set = new HashSet<Object>();
        setDocFieldValue(f.getName(), set, OType.EMBEDDEDSET);
        value = new OObjectEnumLazySet(genericType, doc, set, (Set<Object>) value);
      }
    } else if (!(value instanceof OObjectLazyMultivalueElement)) {
      boolean embedded = OObjectEntitySerializer.isEmbeddedField(self.getClass(), f.getName());
      if (value instanceof List) {
        OType type = embedded ? OType.EMBEDDEDLIST : OType.LINKLIST;
        List<OIdentifiable> docList = doc.field(f.getName(), type);
        if (docList == null) {
          if (embedded)
            docList = new OTrackedList<OIdentifiable>(doc);
          else
            docList = new ORecordLazyList(doc);
          setDocFieldValue(f.getName(), docList, type);
        } else if (isFieldUpdate) {
          docList.clear();
        }
        value = new OObjectLazyList(self, docList, value,
            OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), f.getName()));
      } else if (value instanceof Set) {
        OType type = embedded ? OType.EMBEDDEDSET : OType.LINKSET;
        Set<OIdentifiable> docSet = doc.field(f.getName(), type);
        if (docSet == null) {
          if (embedded)
            docSet = new OTrackedSet<OIdentifiable>(doc);
          else
            docSet = new ORecordLazySet(doc);
          setDocFieldValue(f.getName(), docSet, type);
        } else if (isFieldUpdate) {
          docSet.clear();
        }
        value = new OObjectLazySet(self, docSet, (Set<?>) value,
            OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), f.getName()));
      }
    }
    if (!((ODatabaseObject) ODatabaseRecordThreadLocal.instance().get().getDatabaseOwner()).isLazyLoading())
      ((OObjectLazyMultivalueElement) value).detach(false);
    return value;
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  protected Object lazyLoadField(final Object self, final String fieldName, Object docValue, final Object currentValue)
      throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    boolean customSerialization = false;
    final Field f = OObjectEntitySerializer.getField(fieldName, self.getClass());
    if (f == null)
      return currentValue;
    if (OObjectEntitySerializer.isSerializedType(f)) {
      customSerialization = true;
    }
    if (docValue instanceof OIdentifiable) {
      if (OIdentifiable.class.isAssignableFrom(f.getType())) {
        if (ORecordAbstract.class.isAssignableFrom(f.getType())) {
          ORecordAbstract record = ((OIdentifiable) docValue).getRecord();
          OObjectEntitySerializer.setFieldValue(f, self, record);
          return record;
        } else {
          OObjectEntitySerializer.setFieldValue(f, self, docValue);
          return docValue;
        }
      } else {
        docValue = convertDocumentToObject((ODocument) ((OIdentifiable) docValue).getRecord(), self);
      }
    } else if (docValue instanceof Collection<?>) {
      docValue = manageCollectionLoad(f, self, docValue, customSerialization);
    } else if (docValue instanceof Map<?, ?>) {
      docValue = manageMapLoad(f, self, docValue, customSerialization);
    } else if (docValue.getClass().isArray() && !docValue.getClass().getComponentType().isPrimitive()) {
      docValue = manageArrayLoad(docValue, f);
    } else if (customSerialization) {
      docValue = OObjectEntitySerializer
          .deserializeFieldValue(OObjectEntitySerializer.getField(fieldName, self.getClass()).getType(), docValue);
    } else {
      if (f.getType().isEnum()) {
        if (docValue instanceof Number)
          docValue = ((Class<Enum>) f.getType()).getEnumConstants()[((Number) docValue).intValue()];
        else
          docValue = Enum.valueOf((Class<Enum>) f.getType(), docValue.toString());
      }
    }
    OObjectEntitySerializer.setFieldValue(f, self, docValue);
    return docValue;
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  protected Object manageArrayLoad(Object value, Field f) {
    if (((Object[]) value).length > 0) {
      Object o = ((Object[]) value)[0];
      if (o instanceof OIdentifiable) {
        Object[] newValue = new Object[((Object[]) value).length];
        for (int i = 0; i < ((Object[]) value).length; i++) {
          ODocument doc = ((OIdentifiable) ((Object[]) value)[i]).getRecord();
          newValue[i] = OObjectEntitySerializer.getDocument((Proxy) doc);
        }
        value = newValue;
      } else {
        final Class genericType = OReflectionHelper.getGenericMultivalueType(f);
        if (genericType != null && genericType.isEnum()) {
          Object newValue = Array.newInstance(genericType, ((Object[]) value).length);
          for (int i = 0; i < ((Object[]) value).length; i++) {
            o = ((Object[]) value)[i];
            if (o instanceof Number)
              o = genericType.getEnumConstants()[((Number) o).intValue()];
            else
              o = Enum.valueOf(genericType, o.toString());
            ((Enum[]) newValue)[i] = (Enum) o;
          }
          value = newValue;
        }
      }
    }
    return value;
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected Object manageMapLoad(final Field f, final Object self, Object value, final boolean customSerialization) {
    final Class genericType = OReflectionHelper.getGenericMultivalueType(f);
    if (value instanceof ORecordLazyMap || (value instanceof OTrackedMap<?> && (genericType == null || !OReflectionHelper
        .isJavaType(genericType)) && !customSerialization && (genericType == null || !genericType.isEnum()))) {
      value = new OObjectLazyMap(self, (OTrackedMap<?>) value,
          OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), f.getName()));
    } else if (customSerialization) {
      value = new OObjectCustomSerializerMap<TYPE>(OObjectEntitySerializer.getSerializedType(f), doc, (Map<Object, Object>) value);
    } else if (genericType != null && genericType.isEnum()) {
      value = new OObjectEnumLazyMap(genericType, doc, (Map<Object, Object>) value);
    }
    return value;
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected Object manageCollectionLoad(final Field f, final Object self, Object value, final boolean customSerialization) {
    final Class genericType = OReflectionHelper.getGenericMultivalueType(f);
    if (value instanceof ORecordLazyList || (value instanceof OTrackedList<?> && (genericType == null || !OReflectionHelper
        .isJavaType(genericType)) && !customSerialization && (genericType == null || !genericType.isEnum()))) {
      value = new OObjectLazyList(self, (List<OIdentifiable>) value,
          OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), f.getName()));
    } else if (value instanceof ORecordLazySet || (value instanceof OTrackedSet<?> && (genericType == null || !OReflectionHelper
        .isJavaType(genericType)) && !customSerialization && (genericType == null || !genericType.isEnum()))) {
      value = new OObjectLazySet(self, (Set) value, OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), f.getName()));
    } else if (customSerialization) {
      if (value instanceof List<?>) {
        value = new OObjectCustomSerializerList(OObjectEntitySerializer.getSerializedType(f), doc, (List<Object>) value);
      } else {
        value = new OObjectCustomSerializerSet(OObjectEntitySerializer.getSerializedType(f), doc, (Set<Object>) value);
      }
    } else if (genericType != null && genericType.isEnum()) {
      if (value instanceof List<?>) {
        value = new OObjectEnumLazyList(genericType, doc, (List<Object>) value);
      } else {
        value = new OObjectEnumLazySet(genericType, doc, (Set<Object>) value);
      }
    }

    return manageArrayFieldObject(f, self, value);
  }

  protected Object manageArrayFieldObject(final Field field, final Object self, Object value) {
    if (field.getType().isArray()) {
      final Collection<?> collectionValue = ((Collection<?>) value);
      final Object newArray = Array.newInstance(field.getType().getComponentType(), collectionValue.size());
      int i = 0;
      for (final Object collectionItem : collectionValue) {
        Array.set(newArray, i, collectionItem);
        i++;
      }

      return newArray;
    } else
      return value;
  }

  protected Object convertDocumentToObject(final ODocument value, final Object self) {
    if (value == null)
      return null;
    return OObjectEntityEnhancer.getInstance().getProxiedInstance(value.getClassName(), getDatabase().getEntityManager(), value,
        (self instanceof ProxyObject ? (ProxyObject) self : null));
  }

  protected Object manageSetMethod(final Object self, final Method m, final Method proceed, final Object[] args)
      throws IllegalAccessException, InvocationTargetException {
    if (!reading) {
      final String fieldName;
      fieldName = OObjectEntityEnhancer.getInstance().getMethodFilter(self.getClass()).getFieldName(m);
      args[0] = setValue(self, fieldName, args[0]);
    }
    return proceed.invoke(self, args);
  }

  @SuppressWarnings("rawtypes")
  protected Object setValue(final Object self, final String fieldName, Object valueToSet) {

    OType objectOType = OObjectEntitySerializer.getTypeByClass(self.getClass(), fieldName);

    if (valueToSet == null) {
      Object oldValue = doc.field(fieldName);
      if (OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), fieldName) && oldValue instanceof OIdentifiable)
        orphans.add(((OIdentifiable) oldValue).getIdentity());
      setDocFieldValue(fieldName, valueToSet, objectOType);
    } else if (!valueToSet.getClass().isAnonymousClass()) {
      if (valueToSet instanceof Proxy) {
        ODocument docToSet = OObjectEntitySerializer.getDocument((Proxy) valueToSet);
        if (OObjectEntitySerializer.isEmbeddedField(self.getClass(), fieldName))
          ODocumentInternal.addOwner(docToSet, doc);
        Object oldValue = doc.field(fieldName);
        if (OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), fieldName) && oldValue instanceof OIdentifiable)
          orphans.add(((OIdentifiable) oldValue).getIdentity());
        setDocFieldValue(fieldName, docToSet, objectOType);
      } else if (valueToSet instanceof OIdentifiable) {
        if (valueToSet instanceof ODocument && OObjectEntitySerializer.isEmbeddedField(self.getClass(), fieldName))
          ODocumentInternal.addOwner((ODocument) valueToSet, doc);
        Object oldValue = doc.field(fieldName);
        if (OObjectEntitySerializer.isCascadeDeleteField(self.getClass(), fieldName) && oldValue instanceof OIdentifiable)
          orphans.add(((OIdentifiable) oldValue).getIdentity());
        setDocFieldValue(fieldName, valueToSet, objectOType);
      } else if (((valueToSet instanceof Collection<?> || valueToSet instanceof Map<?, ?>)) || valueToSet.getClass().isArray()) {
        Class<?> genericMultiValueType = OReflectionHelper
            .getGenericMultivalueType(OObjectEntitySerializer.getField(fieldName, self.getClass()));
        if (genericMultiValueType != null && !OReflectionHelper.isJavaType(genericMultiValueType)) {
          if (!(valueToSet instanceof OObjectLazyMultivalueElement)) {
            if (valueToSet instanceof Collection<?>) {
              boolean customSerialization = false;
              final Field f = OObjectEntitySerializer.getField(fieldName, self.getClass());
              if (OObjectEntitySerializer.isSerializedType(f)) {
                customSerialization = true;
              }
              valueToSet = manageCollectionSave(self, f, (Collection<?>) valueToSet, customSerialization, true);
            } else if (valueToSet instanceof Map<?, ?>) {
              boolean customSerialization = false;
              final Field f = OObjectEntitySerializer.getField(fieldName, self.getClass());
              if (OObjectEntitySerializer.isSerializedType(f)) {
                customSerialization = true;
              }
              valueToSet = manageMapSave(self, f, (Map<?, ?>) valueToSet, customSerialization);
            } else if (valueToSet.getClass().isArray()) {
              valueToSet = manageArraySave(fieldName, (Object[]) valueToSet);
            }
          }
        } else {
          if (OObjectEntitySerializer.isToSerialize(valueToSet.getClass())) {
            setDocFieldValue(fieldName, OObjectEntitySerializer
                    .serializeFieldValue(OObjectEntitySerializer.getField(fieldName, self.getClass()).getType(), valueToSet),
                objectOType);
          } else {
            if (valueToSet.getClass().isArray()) {
              final OClass schemaClass = doc.getSchemaClass();
              OProperty schemaProperty = null;
              if (schemaClass != null)
                schemaProperty = schemaClass.getProperty(fieldName);

              setDocFieldValue(fieldName, OObjectEntitySerializer
                      .typeToStream(valueToSet, schemaProperty != null ? schemaProperty.getType() : objectOType, getDatabase(), doc),
                  objectOType);
            } else
              setDocFieldValue(fieldName, valueToSet, objectOType);
          }
        }
      } else if (valueToSet.getClass().isEnum()) {
        setDocFieldValue(fieldName, ((Enum) valueToSet).name(), objectOType);
      } else {
        if (OObjectEntitySerializer.isToSerialize(valueToSet.getClass())) {
          setDocFieldValue(fieldName, OObjectEntitySerializer
                  .serializeFieldValue(OObjectEntitySerializer.getField(fieldName, self.getClass()).getType(), valueToSet),
              objectOType);
        } else if (getDatabase().getEntityManager().getEntityClass(valueToSet.getClass().getSimpleName()) != null && !valueToSet
            .getClass().isEnum()) {
          valueToSet = OObjectEntitySerializer.serializeObject(valueToSet, getDatabase());
          final ODocument docToSet = OObjectEntitySerializer.getDocument((Proxy) valueToSet);
          if (OObjectEntitySerializer.isEmbeddedField(self.getClass(), fieldName))
            ODocumentInternal.addOwner((ODocument) docToSet, doc);

          setDocFieldValue(fieldName, docToSet, objectOType);
        } else {
          setDocFieldValue(fieldName, valueToSet, objectOType);
        }
      }
      loadedFields.put(fieldName, doc.getVersion());
      setDirty();
    } else {
      OLogManager.instance().warn(this,
          "Setting property '%s' in proxied class '%s' with an anonymous class '%s'. The document won't have this property.",
          fieldName, self.getClass().getName(), valueToSet.getClass().getName());
    }
    return valueToSet;
  }

  protected String getSetterFieldName(final String fieldName) {
    final StringBuffer methodName = new StringBuffer("set");
    methodName.append(Character.toUpperCase(fieldName.charAt(0)));
    for (int i = 1; i < fieldName.length(); i++) {
      methodName.append(fieldName.charAt(i));
    }
    return methodName.toString();
  }

  protected Method getSetMethod(final Class<?> iClass, final String fieldName, Object value) throws NoSuchMethodException {
    for (Method m : iClass.getDeclaredMethods()) {
      if (m.getName().equals(fieldName)) {
        if (m.getParameterTypes().length == 1)
          if (m.getParameterTypes()[0].isAssignableFrom(value.getClass()))
            return m;
      }
    }
    if (iClass.getSuperclass().equals(Object.class))
      return null;
    return getSetMethod(iClass.getSuperclass(), fieldName, value);
  }

  private ODatabaseObject getDatabase() {
    return (ODatabaseObject) ODatabaseRecordThreadLocal.instance().get().getDatabaseOwner();
  }

  private Object getDefaultValueForField(Field field) {
    if (field.getType() == Byte.TYPE)
      return Byte.valueOf("0");

    if (field.getType() == Short.TYPE)
      return Short.valueOf("0");

    if (field.getType() == Integer.TYPE)
      return 0;

    if (field.getType() == Long.TYPE)
      return 0L;

    if (field.getType() == Float.TYPE)
      return 0.0f;

    if (field.getType() == Double.TYPE)
      return 0.0d;

    if (field.getType() == Boolean.TYPE)
      return false;

    return null;
  }
}


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

import com.orientechnologies.orient.core.collate.OCollate;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.db.ODatabase;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.index.OIndex;
import com.orientechnologies.orient.core.index.OIndexDefinition;
import com.orientechnologies.orient.core.index.OIndexDefinitionFactory;
import com.orientechnologies.orient.core.index.OIndexException;
import com.orientechnologies.orient.core.index.OIndexFactory;
import com.orientechnologies.orient.core.index.OIndexes;
import com.orientechnologies.orient.core.index.OSimpleKeyIndexDefinition;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OClassImpl;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OSQLEngine;
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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

public class OCreateIndexStatement extends ODDLStatement {

  protected OIndexName        name;
  protected OIdentifier       className;
  protected List<Property>    propertyList = new ArrayList<Property>();
  protected OIdentifier       type;
  protected OIdentifier       engine;
  protected List<OIdentifier> keyTypes     = new ArrayList<OIdentifier>();
  protected OJson             metadata;
  protected boolean           ifNotExists  = false;

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

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

  @Override
  public OResultSet executeDDL(OCommandContext ctx) {
    Object execResult = execute(ctx);
    OInternalResultSet rs = new OInternalResultSet();
    if (execResult != null) {
      OResultInternal result = new OResultInternal();
      result.setProperty("operation", "create index");
      result.setProperty("name", name.getValue());
      rs.add(result);
    }
    return rs;
  }

  Object execute(OCommandContext ctx) {
    final ODatabase database = ctx.getDatabase();

    if (database.getMetadata().getIndexManager().existsIndex(name.getValue())) {
      if (ifNotExists) {
        return null;
      } else {
        throw new OCommandExecutionException("Index " + name + " already exists");
      }
    }

    final OIndex<?> idx;
    List<OCollate> collatesList = calculateCollates(ctx);
    String engine = this.engine == null ? null : this.engine.getStringValue().toUpperCase(Locale.ENGLISH);
    ODocument metadataDoc = calculateMetadata(ctx);

    if (propertyList == null || propertyList.size() == 0) {
      OIndexFactory factory = OIndexes.getFactory(type.getStringValue(), null);

      OType[] keyTypes = calculateKeyTypes(ctx);

      if (keyTypes != null && keyTypes.length > 0) {
        idx = database.getMetadata().getIndexManager()
            .createIndex(name.getValue(), type.getStringValue(), new OSimpleKeyIndexDefinition(keyTypes, collatesList), null, null,
                metadataDoc, engine);
      } else if (keyTypes != null && keyTypes.length == 0 && "LUCENE_CROSS_CLASS".equalsIgnoreCase(engine)) {
        //handle special case of cross class  Lucene index: awful but works
        OIndexDefinition keyDef = new OSimpleKeyIndexDefinition(new OType[] { OType.STRING }, collatesList);
        idx = database.getMetadata().getIndexManager()
            .createIndex(name.getValue(), type.getStringValue(), keyDef, null, null, metadataDoc, engine);

      } else if (className == null && keyTypes == null || keyTypes.length == 0) {
        //legacy: create index without specifying property names
        String[] split = name.getValue().split("\\.");
        if (split.length != 2) {
          throw new ODatabaseException(
              "Impossible to create an index without specify class and property name nor key types: " + toString());
        }
        OClass oClass = database.getClass(split[0]);
        if (oClass == null) {
          throw new ODatabaseException("Impossible to create an index, class not found: " + split[0]);
        }
        if (oClass.getProperty(split[1]) == null) {
          throw new ODatabaseException("Impossible to create an index, property not found: " + name.getValue());
        }
        String[] fields = new String[] { split[1] };
        idx = getoIndex(oClass, fields, engine, database, collatesList, metadataDoc);

      } else {
        throw new ODatabaseException(
            "Impossible to create an index without specify the key type or the associated property: " + toString());
      }
    } else {
      String[] fields = calculateProperties(ctx);
      OClass oClass = getIndexClass(ctx);
      idx = getoIndex(oClass, fields, engine, database, collatesList, metadataDoc);
    }

    if (idx != null)
      return idx.getSize();

    return null;
  }

  private OIndex<?> getoIndex(OClass oClass, String[] fields, String engine, ODatabase database, List<OCollate> collatesList,
      ODocument metadataDoc) {
    OIndex<?> idx;
    if ((keyTypes == null || keyTypes.size() == 0) && collatesList == null) {

      idx = oClass.createIndex(name.getValue(), type.getStringValue(), null, metadataDoc, engine, fields);
    } else {
      final List<OType> fieldTypeList;
      if (keyTypes == null || keyTypes.size() == 0 && fields.length > 0) {
        for (final String fieldName : fields) {
          if (!fieldName.equals("@rid") && !oClass.existsProperty(fieldName))
            throw new OIndexException(
                "Index with name : '" + name.getValue() + "' cannot be created on class : '" + oClass.getName()
                    + "' because field: '" + fieldName + "' is absent in class definition.");
        }
        fieldTypeList = ((OClassImpl) oClass).extractFieldTypes(fields);
      } else
        fieldTypeList = keyTypes.stream().map(x -> OType.valueOf(x.getStringValue())).collect(Collectors.toList());

      final OIndexDefinition idxDef = OIndexDefinitionFactory
          .createIndexDefinition(oClass, Arrays.asList(fields), fieldTypeList, collatesList, type.getStringValue(), null);

      idx = database.getMetadata().getIndexManager()
          .createIndex(name.getValue(), type.getStringValue(), idxDef, oClass.getPolymorphicClusterIds(), null, metadataDoc,
              engine);
    }
    return idx;
  }

  /***
   * returns the list of property names to be indexed
   *
   * @param ctx
   * @return
   */
  private String[] calculateProperties(OCommandContext ctx) {
    if (propertyList == null) {
      return null;
    }
    return propertyList.stream().map(x -> x.getCompleteKey()).collect(Collectors.toList()).toArray(new String[] {});
  }

  /**
   * calculates the indexed class based on the class name
   */
  private OClass getIndexClass(OCommandContext ctx) {
    if (className == null) {
      return null;
    }
    OClass result = ctx.getDatabase().getMetadata().getSchema().getClass(className.getStringValue());
    if (result == null) {
      throw new OCommandExecutionException("Cannot find class " + className);
    }
    return result;
  }

  /**
   * returns index metadata as an ODocuemnt (as expected by Index API)
   */
  private ODocument calculateMetadata(OCommandContext ctx) {
    if (metadata == null) {
      return null;
    }
    return metadata.toDocument(null, ctx);
  }

  private OType[] calculateKeyTypes(OCommandContext ctx) {
    if (keyTypes == null) {
      return new OType[0];
    }
    return keyTypes.stream().map(x -> OType.valueOf(x.getStringValue())).collect(Collectors.toList()).toArray(new OType[] {});
  }

  private List<OCollate> calculateCollates(OCommandContext ctx) {
    List<OCollate> result = new ArrayList<>();
    boolean found = false;
    for (Property prop : this.propertyList) {
      String collate = prop.collate == null ? null : prop.collate.getStringValue();
      if (collate != null) {
        final OCollate col = OSQLEngine.getCollate(collate);
        result.add(col);
        found = true;
      } else {
        result.add(null);
      }
    }
    if (!found) {
      return null;
    }
    return result;
  }

  @Override
  public void toString(Map<Object, Object> params, StringBuilder builder) {
    builder.append("CREATE INDEX ");
    name.toString(params, builder);
    if (className != null) {
      builder.append(" ON ");
      className.toString(params, builder);
      builder.append(" (");
      boolean first = true;
      for (Property prop : propertyList) {
        if (!first) {
          builder.append(", ");
        }
        if (prop.name != null) {
          prop.name.toString(params, builder);
        } else {
          prop.recordAttribute.toString(params, builder);
        }
        if (prop.byKey) {
          builder.append(" BY KEY");
        } else if (prop.byValue) {
          builder.append(" BY VALUE");
        }
        if (prop.collate != null) {
          builder.append(" COLLATE ");
          prop.collate.toString(params, builder);
        }
        first = false;
      }
      builder.append(")");
    }
    builder.append(" ");
    type.toString(params, builder);
    if (engine != null) {
      builder.append(" ENGINE ");
      engine.toString(params, builder);
    }
    if (keyTypes != null && keyTypes.size() > 0) {
      boolean first = true;
      builder.append(" ");
      for (OIdentifier keyType : keyTypes) {
        if (!first) {
          builder.append(",");
        }
        keyType.toString(params, builder);
        first = false;
      }
    }
    if (metadata != null) {
      builder.append(" METADATA ");
      metadata.toString(params, builder);
    }
  }

  @Override
  public OCreateIndexStatement copy() {
    OCreateIndexStatement result = new OCreateIndexStatement(-1);
    result.name = name == null ? null : name.copy();
    result.className = className == null ? null : className.copy();
    result.propertyList = propertyList == null ? null : propertyList.stream().map(x -> x.copy()).collect(Collectors.toList());
    result.type = type == null ? null : type.copy();
    result.engine = engine == null ? null : engine.copy();
    result.keyTypes = keyTypes == null ? null : keyTypes.stream().map(x -> x.copy()).collect(Collectors.toList());
    result.metadata = metadata == null ? null : metadata.copy();
    return result;
  }

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

    OCreateIndexStatement that = (OCreateIndexStatement) o;

    if (name != null ? !name.equals(that.name) : that.name != null)
      return false;
    if (className != null ? !className.equals(that.className) : that.className != null)
      return false;
    if (propertyList != null ? !propertyList.equals(that.propertyList) : that.propertyList != null)
      return false;
    if (type != null ? !type.equals(that.type) : that.type != null)
      return false;
    if (engine != null ? !engine.equals(that.engine) : that.engine != null)
      return false;
    if (keyTypes != null ? !keyTypes.equals(that.keyTypes) : that.keyTypes != null)
      return false;
    return metadata != null ? metadata.equals(that.metadata) : that.metadata == null;
  }

  @Override
  public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + (className != null ? className.hashCode() : 0);
    result = 31 * result + (propertyList != null ? propertyList.hashCode() : 0);
    result = 31 * result + (type != null ? type.hashCode() : 0);
    result = 31 * result + (engine != null ? engine.hashCode() : 0);
    result = 31 * result + (keyTypes != null ? keyTypes.hashCode() : 0);
    result = 31 * result + (metadata != null ? metadata.hashCode() : 0);
    return result;
  }

  public static class Property {
    protected OIdentifier      name;
    protected ORecordAttribute recordAttribute;
    protected boolean          byKey   = false;
    protected boolean          byValue = false;
    protected OIdentifier      collate;

    public Property copy() {
      Property result = new Property();
      result.name = name == null ? null : name.copy();
      result.recordAttribute = recordAttribute == null ? null : recordAttribute.copy();
      result.byKey = byKey;
      result.byValue = byValue;
      result.collate = collate == null ? null : collate.copy();
      return result;
    }

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

      Property property = (Property) o;

      if (byKey != property.byKey)
        return false;
      if (byValue != property.byValue)
        return false;
      if (name != null ? !name.equals(property.name) : property.name != null)
        return false;
      if (recordAttribute != null ? !recordAttribute.equals(property.recordAttribute) : property.recordAttribute != null)
        return false;
      return collate != null ? collate.equals(property.collate) : property.collate == null;
    }

    @Override
    public int hashCode() {
      int result = name != null ? name.hashCode() : 0;
      result = 31 * result + (recordAttribute != null ? recordAttribute.hashCode() : 0);
      result = 31 * result + (byKey ? 1 : 0);
      result = 31 * result + (byValue ? 1 : 0);
      result = 31 * result + (collate != null ? collate.hashCode() : 0);
      return result;
    }

    /**
     * returns the complete key to index, eg. property name or "property by key/value"
     */
    public String getCompleteKey() {
      StringBuilder result = new StringBuilder();
      if (name != null)
        result.append(name.getStringValue());
      else if (recordAttribute != null)
        result.append(recordAttribute.getName());

      if (byKey) {
        result.append(" by key");
      }
      if (byValue) {
        result.append(" by value");
      }
      return result.toString();
    }
  }
}
/* JavaCC - OriginalChecksum=bd090e02c4346ad390a6b8c77f1b9dba (do not edit this line) */


/*
 *
 *  *  Copyright 2014 OrientDB LTD (info(at)orientdb.com)
 *  *
 *  *  Licensed under the Apache License, Version 2.0 (the "License");
 *  *  you may not use this file except in compliance with the License.
 *  *  You may obtain a copy of the License at
 *  *
 *  *       http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *  Unless required by applicable law or agreed to in writing, software
 *  *  distributed under the License is distributed on an "AS IS" BASIS,
 *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *  See the License for the specific language governing permissions and
 *  *  limitations under the License.
 *  *
 *  * For more information: http://www.orientdb.com
 *
 */
package com.orientechnologies.orient.core.metadata.sequence;

import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

/**
 * @author Matan Shukry (matanshukry@gmail.com)
 * @since 3/3/2015
 */
public class OSequenceCached extends OSequence {
  private static final String FIELD_CACHE = "cache";
  private long    cacheStart;
  private long    cacheEnd;
  private boolean firstCache;
  private int     increment;
  private Long limitValue = null;
  private Long              startValue;
  private SequenceOrderType orderType;
  private boolean           recyclable;
  private String name = null;

  public OSequenceCached() {
    this(null, null);
  }

  public OSequenceCached(final ODocument iDocument) {
    this(iDocument, null);
  }

  public OSequenceCached(final ODocument iDocument, OSequence.CreateParams params) {
    super(iDocument, params);
    //this is extension of initSequence
    if (iDocument == null) {
      if (params == null) {
        params = new CreateParams().setDefaults();
      }
      setCacheSize(params.cacheSize);
      cacheStart = cacheEnd = 0L;
      allocateCache(getCacheSize(), getDatabase());
    }
    if (iDocument != null) {
      firstCache = true;
      cacheStart = cacheEnd = getValue(iDocument);
    }
  }

  @Override
  synchronized boolean updateParams(OSequence.CreateParams params, boolean executeViaDistributed) throws ODatabaseException {
    boolean any = super.updateParams(params, executeViaDistributed);
    if (!executeViaDistributed) {
      if (params.cacheSize != null && this.getCacheSize() != params.cacheSize) {
        this.setCacheSize(params.cacheSize);
        any = true;
      }

      firstCache = true;
      save();
    }
    return any;
  }

  private void doRecycle(ODatabaseDocumentInternal finalDb) {
    if (recyclable) {
      reloadSequence();
      setValue(getStart());
      allocateCache(getCacheSize(), finalDb);
    } else {
      throw new OSequenceLimitReachedException("Limit reached");
    }
  }

  private void reloadCrucialValues() {
    increment = getIncrement();
    limitValue = getLimitValue();
    orderType = getOrderType();
    recyclable = getRecyclable();
    startValue = getStart();
    if (name == null) {
      name = getName();
    }
  }

  private boolean signalToAllocateCache() {
    if (orderType == SequenceOrderType.ORDER_POSITIVE) {
      if (cacheStart + increment > cacheEnd && !(limitValue != null && cacheStart + increment > limitValue)) {
        return true;
      }
    } else {
      if (cacheStart - increment < cacheEnd && !(limitValue != null && cacheStart - increment < limitValue)) {
        return true;
      }
    }
    return false;
  }

  private <T> T sendSequenceActionSetAndNext(long value) throws ExecutionException, InterruptedException {
    OSequenceAction action = new OSequenceAction(getName(), value);
    return tlDocument.get().getDatabase().sendSequenceAction(action);
  }

  //want to be atomic
  //first set new current value then call next
  protected long nextWithNewCurrentValue(long currentValue, boolean executeViaDistributed)
      throws OSequenceLimitReachedException, ODatabaseException {
    if (!executeViaDistributed) {
      //we don't want synchronization on whole method, because called with executeViaDistributed == true
      //will later call nextWithNewCurrentValue with parameter executeViaDistributed == false
      //and that will cause deadlock
      synchronized (this) {
        cacheStart = currentValue;
        return nextWork();
      }
    } else {
      try{
        return sendSequenceActionSetAndNext(currentValue);
      }
      catch (InterruptedException | ExecutionException exc){
        OLogManager.instance().error(this, exc.getMessage(), exc, (Object[]) null);
        throw new ODatabaseException(exc.getMessage());
      }
    }
  }

  @Override
  protected boolean shouldGoOverDistrtibute() {
    return isOnDistributted() && (replicationProtocolVersion == 2) && signalToAllocateCache();
  }

  @Override
  public long next() throws OSequenceLimitReachedException, ODatabaseException {
    boolean shouldGoOverDistributted = shouldGoOverDistrtibute();
    if (shouldGoOverDistributted) {
      return nextWithNewCurrentValue(cacheStart, true);
    }
    return nextWork();
  }

  @Override
  public long nextWork() throws OSequenceLimitReachedException {
    ODatabaseDocumentInternal mainDb = getDatabase();
    boolean tx = mainDb.getTransaction().isActive();
    try {
      ODatabaseDocumentInternal db = mainDb;
      if (tx) {
        db = mainDb.copy();
        db.activateOnCurrentThread();
      }
      try {
        ODatabaseDocumentInternal finalDb = db;
        return callRetry(signalToAllocateCache() || getCrucialValueChanged(), new Callable<Long>() {
          @Override
          public Long call() throws Exception {
            synchronized (OSequenceCached.this) {

              boolean detectedCrucialValueChange = false;
              if (getCrucialValueChanged()) {
                reloadCrucialValues();
                detectedCrucialValueChange = true;
              }
              if (orderType == SequenceOrderType.ORDER_POSITIVE) {
                if (signalToAllocateCache()) {
                  boolean cachedbefore = !firstCache;
                  allocateCache(getCacheSize(), finalDb);
                  if (!cachedbefore) {
                    if (limitValue != null && cacheStart + increment > limitValue) {
                      doRecycle(finalDb);
                    } else {
                      cacheStart = cacheStart + increment;
                    }
                  }
                } else if (limitValue != null && cacheStart + increment > limitValue) {
                  doRecycle(finalDb);
                } else {
                  cacheStart = cacheStart + increment;
                }
              } else {
                if (signalToAllocateCache()) {
                  boolean cachedbefore = !firstCache;
                  allocateCache(getCacheSize(), finalDb);
                  if (!cachedbefore) {
                    if (limitValue != null && cacheStart - increment < limitValue) {
                      doRecycle(finalDb);
                    } else {
                      cacheStart = cacheStart - increment;
                    }
                  }
                } else if (limitValue != null && cacheStart - increment < limitValue) {
                  doRecycle(finalDb);
                } else {
                  cacheStart = cacheStart - increment;
                }
              }

              if (detectedCrucialValueChange) {
                setCrucialValueChanged(false);
              }

              if (limitValue != null && !recyclable) {
                float tillEnd = Math.abs(limitValue - cacheStart) / (float) increment;
                float delta = Math.abs(limitValue - startValue) / (float) increment;
                //warning on 1%
                if ((float) tillEnd <= ((float) delta / 100.f) || tillEnd <= 1) {
                  String warningMessage =
                      "Non-recyclable sequence: " + name + " reaching limt, current value: " + cacheStart + " limit value: "
                          + limitValue + " with step: " + increment;
                  OLogManager.instance().warn(this, warningMessage);
                }
              }

              firstCache = false;
              return cacheStart;
            }
          }
        }, "next");
      } finally {
        if (tx) {
          db.close();
        }
      }
    } finally {
      if (tx) {
        mainDb.activateOnCurrentThread();
      }
    }
  }

  @Override
  protected synchronized long currentWork() {
    return this.cacheStart;
  }

  @Override
  public long resetWork() {
    ODatabaseDocumentInternal mainDb = getDatabase();
    boolean tx = mainDb.getTransaction().isActive();
    try {
      ODatabaseDocumentInternal db = mainDb;
      if (tx) {
        db = mainDb.copy();
        db.activateOnCurrentThread();
      }
      try {
        ODatabaseDocumentInternal finalDb = db;
        return callRetry(true, new Callable<Long>() {
          @Override
          public Long call() throws Exception {
            synchronized (OSequenceCached.this) {
              long newValue = getStart();
              setValue(newValue);
              save(finalDb);
              firstCache = true;
              allocateCache(getCacheSize(), finalDb);
              return newValue;
            }
          }
        }, "reset");
      } finally {
        if (tx) {
          db.close();
        }
      }
    } finally {
      if (tx) {
        mainDb.activateOnCurrentThread();
      }
    }
  }

  @Override
  public SEQUENCE_TYPE getSequenceType() {
    return SEQUENCE_TYPE.CACHED;
  }

  public final int getCacheSize() {
    return getDocument().field(FIELD_CACHE, OType.INTEGER);
  }

  public final void setCacheSize(int cacheSize) {
    getDocument().field(FIELD_CACHE, cacheSize);
  }

  private final void allocateCache(int cacheSize, ODatabaseDocumentInternal db) {
    if (getCrucialValueChanged()) {
      reloadCrucialValues();
      setCrucialValueChanged(false);
    }
    SequenceOrderType orederType = getOrderType();
    long value = getValue();
    long newValue;
    if (orederType == SequenceOrderType.ORDER_POSITIVE) {
      newValue = value + (getIncrement() * cacheSize);
      if (limitValue != null && newValue > limitValue) {
        newValue = limitValue;
      }
    } else {
      newValue = value - (getIncrement() * cacheSize);
      if (limitValue != null && newValue < limitValue) {
        newValue = limitValue;
      }
    }
    setValue(newValue);
    save(db);

    this.cacheStart = value;
    if (orederType == SequenceOrderType.ORDER_POSITIVE) {
      this.cacheEnd = newValue - 1;
    } else {
      this.cacheEnd = newValue + 1;
    }
    firstCache = false;
  }

}


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

import com.orientechnologies.common.collection.OMultiCollectionIterator;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.tx.OTransactionIndexChanges.OPERATION;

import java.util.*;

/**
 * Collects the changes to an index for a certain key
 *
 * @author Luca Garulli (l.garulli--at--orientechnologies.com) - initial contribution
 * @author Sergey Sitnikov - index key changes interpretation support
 */
public class OTransactionIndexChangesPerKey {
  /* internal */ static final int SET_ADD_THRESHOLD = 8;

  public final Object                       key;
  public final List<OTransactionIndexEntry> entries;

  public boolean clientTrackOnly;

  public static class OTransactionIndexEntry {
    public OPERATION     operation;
    public OIdentifiable value;

    public OTransactionIndexEntry(final OIdentifiable iValue, final OPERATION iOperation) {
      value = iValue;
      operation = iOperation;
    }

    @Override
    public boolean equals(Object obj) {
      // equality intentionally established by the value only, operation is ignored, see interpretAs* methods for details

      if (this == obj)
        return true;

      if (obj == null || obj.getClass() != OTransactionIndexEntry.class)
        return false;
      final OTransactionIndexEntry other = (OTransactionIndexEntry) obj;

      if (this.value != null)
        return this.value.equals(other.value);

      return other.value == null;
    }

    @Override
    public int hashCode() {
      return value == null ? 0 : value.hashCode();
    }
  }

  public OTransactionIndexChangesPerKey(final Object iKey) {
    this.key = iKey;
    entries = new ArrayList<OTransactionIndexEntry>();
  }

  public void add(OIdentifiable iValue, final OPERATION iOperation) {
    synchronized (this) {
      Iterator<OTransactionIndexEntry> iter = entries.iterator();
      while (iter.hasNext()) {
        OTransactionIndexEntry entry = iter.next();
        if (((entry.value == iValue) || (entry.value != null && entry.value.equals(iValue))) && !entry.operation.equals(iOperation)) {
          iter.remove();
          return;
        }
      }
      entries.add(new OTransactionIndexEntry(iValue != null ? iValue.getIdentity() : null, iOperation));
    }
  }

  /**
   * Interprets this key changes using the given {@link Interpretation interpretation}.
   *
   * @param interpretation the interpretation to use.
   *
   * @return the interpreted changes.
   */
  public Iterable<OTransactionIndexEntry> interpret(Interpretation interpretation) {
    synchronized (this) {
      switch (interpretation) {
      case Unique:
        return interpretAsUnique();
      case Dictionary:
        return interpretAsDictionary();
      case NonUnique:
        return interpretAsNonUnique();
      default:
        throw new IllegalStateException("Unexpected interpretation '" + interpretation + "'");
      }
    }
  }

  public void clear() {
    synchronized (this) {
      entries.clear();
    }
  }

  @Override
  public String toString() {
    final StringBuilder builder = new StringBuilder(64);
    builder.append(key).append(" [");
    boolean first = true;
    for (OTransactionIndexEntry entry : entries) {
      if (first)
        first = false;
      else
        builder.append(',');

      builder.append(entry.value).append(" (").append(entry.operation).append(")");
    }
    builder.append("]");
    return builder.toString();
  }

  private Iterable<OTransactionIndexEntry> interpretAsUnique() {
    // 1. Handle common fast paths.

    if (entries.size() < 2)
      return new ArrayList<OTransactionIndexEntry>(entries);

    if (entries.size() == 2) {
      final OTransactionIndexEntry entryA = entries.get(0);
      final OTransactionIndexEntry entryB = entries.get(1);

      if (entryA.operation == OPERATION.REMOVE && entryB.operation == OPERATION.REMOVE)
        return Collections.singletonList(entryA); // only one removal is observed anyway

      final ORID ridA = entryA.value == null ? null : entryA.value.getIdentity();
      final ORID ridB = entryB.value == null ? null : entryB.value.getIdentity();

      if (ridA != null && ridA.equals(ridB)) {
        if (entryA.operation == entryB.operation) // both operations do the same on the same RID
          return Collections.singletonList(entryA);

        if (entryB.operation == OPERATION.REMOVE) // remove operation cancels put
          return Collections.emptyList();

        return new ArrayList<OTransactionIndexEntry>(entries); // don't optimize remove-put on the same RID for safety
      }

      if (entryB.operation == OPERATION.REMOVE)
        return entryB.value == null ?
            Collections.singletonList(entryB) /* latest key removal wins */ :
            swap(entries) /* reorder to remove-put */;

      return new ArrayList<OTransactionIndexEntry>(entries); // it's either remove-put or put-put
    }

    // 2. Calculate observable changes to index.

    final Set<OTransactionIndexEntry> interpretation = new HashSet<OTransactionIndexEntry>(entries.size());
    OTransactionIndexEntry firstExternalRemove = null;
    for (OTransactionIndexEntry entry : entries) {
      final OIdentifiable value = entry.value;

      switch (entry.operation) {
      case PUT:
        assert value != null;
        interpretation.add(entry);
        break;
      case REMOVE:
        if (!interpretation.remove(entry)) {
          if (firstExternalRemove == null)
            firstExternalRemove = entry; // record only first external removal, it makes key ready for any put anyway
          if (value == null)
            interpretation.clear();
        }
        break;
      case CLEAR:
      default:
        assert false;
        break;
      }
    }

    // 3. Build resulting equivalent operation sequence.

    if (interpretation.isEmpty()) { // no observable changes except maybe some removal
      if (firstExternalRemove != null)
        return Collections.singletonList(firstExternalRemove);

      return Collections.emptyList();
    }

    final List<OTransactionIndexEntry> changes = new ArrayList<OTransactionIndexEntry>(
        1 /* for removal, if any */ + 2 /* for puts */);
    if (firstExternalRemove != null)
      changes.add(firstExternalRemove);

    int counter = 0;
    for (OTransactionIndexEntry entry : interpretation) {
      changes.add(entry);

      if (++counter == 2)
        break; // unique constraint already violated, stop
    }

    return changes;
  }

  private Iterable<OTransactionIndexEntry> interpretAsDictionary() {
    // 1. Handle common fast paths.

    if (entries.size() < 2)
      return new ArrayList<OTransactionIndexEntry>(entries);

    if (entries.size() == 2) {
      final OTransactionIndexEntry entryA = entries.get(0);
      final OTransactionIndexEntry entryB = entries.get(1);

      if (entryA.operation == OPERATION.REMOVE && entryB.operation == OPERATION.REMOVE)
        return Collections.singletonList(entryA); // only one removal is observed anyway

      final ORID ridA = entryA.value == null ? null : entryA.value.getIdentity();
      final ORID ridB = entryB.value == null ? null : entryB.value.getIdentity();

      if (ridA != null && ridA.equals(ridB)) {
        if (entryA.operation == entryB.operation) // both operations do the same on the same RID
          return Collections.singletonList(entryA);

        if (entryB.operation == OPERATION.REMOVE) // remove operation cancels put
          return Collections.emptyList();

        return Collections.singletonList(entryB); // put wins
      }

      if (entryB.operation == OPERATION.REMOVE && entryB.value == null)
        return Collections.singletonList(entryB); // latest key removal wins

      return entryB.operation == OPERATION.PUT ?
          Collections.singletonList(entryB) /* latest put wins */ :
          Collections.singletonList(entryA) /* it's put-remove on different RIDs, put wins */;
    }

    // 2. Calculate observable changes to index.

    // XXX: We need to return only *latest observable* put, it always wins to other puts and removals. Unfortunately, there
    // is no lightweight way to find it out using standard Java data structures, thanks to Josh Bloch for not exposing the
    // LinkedHashMap's "doubly-linked list" interface to the public, but mentioning it in the clever javadoc. So we have to
    // maintain our own queue.
    final Deque<OTransactionIndexEntry> lastObservedPuts = new ArrayDeque<OTransactionIndexEntry>(entries.size());

    final Set<OTransactionIndexEntry> interpretation = new HashSet<OTransactionIndexEntry>(entries.size());
    OTransactionIndexEntry firstExternalRemove = null;
    for (OTransactionIndexEntry entry : entries) {
      final OIdentifiable value = entry.value;

      switch (entry.operation) {
      case PUT:
        assert value != null;

        interpretation.add(entry);
        lastObservedPuts.addLast(entry);
        break;
      case REMOVE:
        if (interpretation.remove(entry)) { // the put of this RID is no longer observable
          assert value != null;

          // Recalculate last visible put.

          if (entry.equals(lastObservedPuts.peekLast()))
            lastObservedPuts.removeLast();

          OTransactionIndexEntry last;
          while ((last = lastObservedPuts.peekLast()) != null && !interpretation.contains(last)) // prune all unobservable puts
            lastObservedPuts.removeLast();
        } else {
          if (firstExternalRemove == null) // save only first external remove
            firstExternalRemove = entry;
          if (value == null) { // start from the scratch
            interpretation.clear();
            lastObservedPuts.clear();
          }
        }
        break;

      case CLEAR:
      default:
        assert false;
        break;
      }
    }

    // 3. Build resulting equivalent operation sequence.

    if (interpretation.isEmpty()) { // no observable changes except maybe some removal
      if (firstExternalRemove != null)
        return Collections.singletonList(firstExternalRemove);

      return Collections.emptyList();
    }

    return Collections.singletonList(lastObservedPuts.getLast()); // last visible put
  }

  private Iterable<OTransactionIndexEntry> interpretAsNonUnique() {
    // 1. Handle common fast paths.

    if (entries.size() < 2)
      return new ArrayList<OTransactionIndexEntry>(entries);

    if (entries.size() == 2) {
      final OTransactionIndexEntry entryA = entries.get(0);
      final OTransactionIndexEntry entryB = entries.get(1);

      final ORID ridA = entryA.value == null ? null : entryA.value.getIdentity();
      final ORID ridB = entryB.value == null ? null : entryB.value.getIdentity();

      if (ridA == null) {
        assert entryA.operation == OPERATION.REMOVE;

        if (entryB.operation == OPERATION.REMOVE)
          return Collections.singletonList(entryA); // both are removals

        return new ArrayList<OTransactionIndexEntry>(entries); // second operation is a put
      }

      if (ridB == null) {
        assert entryB.operation == OPERATION.REMOVE;

        return Collections.singletonList(entryB); // the only observable change is a key removal
      }

      if (ridA.equals(ridB)) {
        if (entryA.operation == entryB.operation) // both operations do the same on the same RID
          return Collections.singletonList(entryA);

        if (entryB.operation == OPERATION.REMOVE) // remove operation cancels put
          return Collections.emptyList();

        return Collections.singletonList(entryB); // put wins
      }

      return new ArrayList<OTransactionIndexEntry>(entries); // it's put-put on different RIDs
    }

    // 2. Calculate observable changes to index.

    final Set<OTransactionIndexEntry> changes = new HashSet<OTransactionIndexEntry>(entries.size());

    final Set<OTransactionIndexEntry> interpretation = new HashSet<OTransactionIndexEntry>(entries.size());
    boolean seenKeyRemoval = false;
    for (OTransactionIndexEntry entry : entries) {
      final OIdentifiable value = entry.value;
      final ORID rid = value == null ? null : value.getIdentity();

      switch (entry.operation) {
      case PUT:
        assert rid != null;

        interpretation.add(entry);
        break;
      case REMOVE:
        if (rid == null) {
          interpretation.clear();

          changes.clear();
          changes.add(entry); // save key removal as it affects the key regardless of the RID
          seenKeyRemoval = true;
        } else {
          if (!interpretation.remove(entry))
            if (!seenKeyRemoval) // no point in removing a RID from the removed key
              changes.add(entry); // save that operation to remove the RID potentially created outside of a transaction
        }
        break;

      case CLEAR:
      default:
        assert false;
        break;
      }
    }

    // 3. Build resulting equivalent operation sequence.

    changes.removeAll(interpretation); // remove any removal which has a corresponding put, put is enough

    if (interpretation.isEmpty()) // no observable changes except maybe some removals
      return changes; // it's either single key removal or one or more RID removals

    if (!seenKeyRemoval /* since key removal is an ordered operation */ && interpretation.size() < SET_ADD_THRESHOLD) {
      changes.addAll(interpretation);
      return changes;
    } else {
      final OMultiCollectionIterator<OTransactionIndexEntry> result = new OMultiCollectionIterator<OTransactionIndexEntry>();
      result.setAutoConvertToRecord(false);
      result.add(changes);
      result.add(interpretation);
      return result;
    }
  }

  private static Iterable<OTransactionIndexEntry> swap(List<OTransactionIndexEntry> list) {
    assert list.size() == 2;
    final List<OTransactionIndexEntry> result = new ArrayList<OTransactionIndexEntry>(2);
    result.add(list.get(1));
    result.add(list.get(0));
    return result;
  }

  /**
   * Defines interpretations supported by {@link #interpret(Interpretation)}.
   */
  public enum Interpretation {
    /**
     * Interpret changes like they was done for unique index.
     */
    Unique,

    /**
     * Interpret changes like they was done for dictionary index.
     */
    Dictionary,

    /**
     * Interpret changes like they was done for non-unique index.
     */
    NonUnique
  }

}


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

import com.orientechnologies.common.types.OModifiableBoolean;
import com.orientechnologies.orient.core.command.*;
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.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.record.ODirection;
import com.orientechnologies.orient.core.record.OEdge;
import com.orientechnologies.orient.core.record.OElement;
import com.orientechnologies.orient.core.record.OVertex;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.filter.OSQLFilter;
import com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery;

import java.util.*;

/**
 * SQL DELETE EDGE command.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OCommandExecutorSQLDeleteEdge extends OCommandExecutorSQLSetAware
    implements OCommandDistributedReplicateRequest, OCommandResultListener {
  public static final  String NAME          = "DELETE EDGE";
  private static final String KEYWORD_BATCH = "BATCH";
  private List<ORecordId> rids;
  private String          fromExpr;
  private String          toExpr;
  private int removed = 0;
  private OCommandRequest query;
  private OSQLFilter      compiledFilter;
  //  private AtomicReference<OrientBaseGraph> currentGraph  = new AtomicReference<OrientBaseGraph>();
  private String          label;
  private OModifiableBoolean shutdownFlag = new OModifiableBoolean();
  private boolean txAlreadyBegun;
  private int batch = 100;

  @SuppressWarnings("unchecked")
  public OCommandExecutorSQLDeleteEdge parse(final OCommandRequest iRequest) {
    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

    String queryText = textRequest.getText();
    String originalQuery = queryText;

    try {
      // System.out.println("NEW PARSER FROM: " + queryText);
      queryText = preParse(queryText, iRequest);
      // System.out.println("NEW PARSER TO: " + queryText);
      textRequest.setText(queryText);

      init((OCommandRequestText) iRequest);

      parserRequiredKeyword("DELETE");
      parserRequiredKeyword("EDGE");

      OClass clazz = null;
      String where = null;

      String temp = parseOptionalWord(true);
      String originalTemp = null;

      int limit = -1;

      if (temp != null && !parserIsEnded())
        originalTemp = parserText.substring(parserGetPreviousPosition(), parserGetCurrentPosition()).trim();

      final OModifiableBoolean shutdownFlag = new OModifiableBoolean();
      ODatabaseDocumentInternal curDb = ODatabaseRecordThreadLocal.instance().get();
//      final OrientGraph graph = OGraphCommandExecutorSQLFactory.getGraph(false, shutdownFlag);
      try {
        while (temp != null) {

          if (temp.equals("FROM")) {
            fromExpr = parserRequiredWord(false, "Syntax error", " =><,\r\n");
            if (rids != null)
              throwSyntaxErrorException("FROM '" + fromExpr + "' is not allowed when specify a RIDs (" + rids + ")");

          } else if (temp.equals("TO")) {
            toExpr = parserRequiredWord(false, "Syntax error", " =><,\r\n");
            if (rids != null)
              throwSyntaxErrorException("TO '" + toExpr + "' is not allowed when specify a RID (" + rids + ")");

          } else if (temp.startsWith("#")) {
            rids = new ArrayList<ORecordId>();
            rids.add(new ORecordId(temp));
            if (fromExpr != null || toExpr != null)
              throwSyntaxErrorException("Specifying the RID " + rids + " is not allowed with FROM/TO");

          } else if (temp.startsWith("[") && temp.endsWith("]")) {
            temp = temp.substring(1, temp.length() - 1);
            rids = new ArrayList<ORecordId>();
            for (String rid : temp.split(",")) {
              rid = rid.trim();
              if (!rid.startsWith("#")) {
                throwSyntaxErrorException("Not a valid RID: " + rid);
              }
              rids.add(new ORecordId(rid));
            }
          } else if (temp.equals(KEYWORD_WHERE)) {
            if (clazz == null)
              // ASSIGN DEFAULT CLASS
              clazz = curDb.getMetadata().getSchema().getClass("E");

            where = parserGetCurrentPosition() > -1 ? " " + parserText.substring(parserGetCurrentPosition()) : "";

            compiledFilter = OSQLEngine.getInstance().parseCondition(where, getContext(), KEYWORD_WHERE);
            break;

          } else if (temp.equals(KEYWORD_BATCH)) {
            temp = parserNextWord(true);
            if (temp != null)
              batch = Integer.parseInt(temp);

          } else if (temp.equals(KEYWORD_LIMIT)) {
            temp = parserNextWord(true);
            if (temp != null)
              limit = Integer.parseInt(temp);

          } else if (temp.length() > 0) {
            // GET/CHECK CLASS NAME
            label = originalTemp;
            clazz = curDb.getMetadata().getSchema().getClass(temp);
            if (clazz == null)
              throw new OCommandSQLParsingException("Class '" + temp + "' was not found");
          }

          temp = parseOptionalWord(true);
          if (parserIsEnded())
            break;
        }

        if (where == null)
          if (limit > -1) {
            where = " LIMIT " + limit;
          } else {
            where = "";
          }
        else
          where = " WHERE " + where;

        if (fromExpr == null && toExpr == null && rids == null)
          if (clazz == null)
            // DELETE ALL THE EDGES
            query = curDb.command(new OSQLAsynchQuery<ODocument>("select from E" + where, this));
          else
            // DELETE EDGES OF CLASS X
            query = curDb.command(new OSQLAsynchQuery<ODocument>("select from `" + clazz.getName()+"` " + where, this));

        return this;
      } finally {
        ODatabaseRecordThreadLocal.instance().set(curDb);
      }
    } finally {
      textRequest.setText(originalQuery);
    }

  }

  /**
   * Execute the command and return the ODocument object created.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    if (fromExpr == null && toExpr == null && rids == null && query == null && compiledFilter == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");
    ODatabaseDocumentInternal db = getDatabase();
    txAlreadyBegun = db.getTransaction().isActive();

    if (rids != null) {
      // REMOVE PUNCTUAL RID
      db.begin();
      for (ORecordId rid : rids) {
        final OEdge e = toEdge(rid);
        if (e != null) {
          e.delete();
          removed++;
        }
      }
      db.commit();
      return removed;
    } else {
      // MULTIPLE EDGES
      final Set<OEdge> edges = new HashSet<OEdge>();
      if (query == null) {
        db.begin();
        Set<OIdentifiable> fromIds = null;
        if (fromExpr != null)
          fromIds = OSQLEngine.getInstance().parseRIDTarget(db, fromExpr, context, iArgs);
        Set<OIdentifiable> toIds = null;
        if (toExpr != null)
          toIds = OSQLEngine.getInstance().parseRIDTarget(db, toExpr, context, iArgs);
        if (label == null)
          label = "E";

        if (fromIds != null && toIds != null) {
          int fromCount = 0;
          int toCount = 0;
          for (OIdentifiable fromId : fromIds) {
            final OVertex v = toVertex(fromId);
            if (v != null)
              fromCount += count(v.getEdges(ODirection.OUT, label));
          }
          for (OIdentifiable toId : toIds) {
            final OVertex v = toVertex(toId);
            if (v != null)
              toCount += count(v.getEdges(ODirection.IN, label));
          }
          if (fromCount <= toCount) {
            // REMOVE ALL THE EDGES BETWEEN VERTICES
            for (OIdentifiable fromId : fromIds) {
              final OVertex v = toVertex(fromId);
              if (v != null)
                for (OEdge e : v.getEdges(ODirection.OUT, label)) {
                  final OIdentifiable inV = ((OEdge) e).getTo();
                  if (inV != null && toIds.contains(inV.getIdentity()))
                    edges.add(e);
                }
            }
          } else {
            for (OIdentifiable toId : toIds) {
              final OVertex v = toVertex(toId);
              if (v != null)
                for (OEdge e : v.getEdges(ODirection.IN, label)) {
                  final OIdentifiable outV = ((OEdge) e).getFrom();
                  if (outV != null && fromIds.contains(outV.getIdentity()))
                    edges.add(e);
                }
            }
          }
        } else if (fromIds != null) {
          // REMOVE ALL THE EDGES THAT START FROM A VERTEXES
          for (OIdentifiable fromId : fromIds) {

            final OVertex v = toVertex(fromId);
            if (v != null) {
              for (OEdge e : v.getEdges(ODirection.OUT, label)) {
                edges.add(e);
              }
            }
          }
        } else if (toIds != null) {
          // REMOVE ALL THE EDGES THAT ARRIVE TO A VERTEXES
          for (OIdentifiable toId : toIds) {
            final OVertex v = toVertex(toId);
            if (v != null) {
              for (OEdge e : v.getEdges(ODirection.IN, label)) {
                edges.add(e);
              }
            }
          }
        } else
          throw new OCommandExecutionException("Invalid target: " + toIds);

        if (compiledFilter != null) {
          // ADDITIONAL FILTERING
          for (Iterator<OEdge> it = edges.iterator(); it.hasNext(); ) {
            final OEdge edge = it.next();
            if (!(Boolean) compiledFilter.evaluate(edge.getRecord(), null, context))
              it.remove();
          }
        }

        // DELETE THE FOUND EDGES
        removed = edges.size();
        for (OEdge edge : edges)
          edge.delete();

        db.commit();
        return removed;

      } else {
        db.begin();
        // TARGET IS A CLASS + OPTIONAL CONDITION
        query.setContext(getContext());
        query.execute(iArgs);
        db.commit();
        return removed;
      }
    }
  }

  private int count(Iterable<OEdge> edges) {
    int result = 0;
    for (OEdge x : edges) {
      result++;
    }
    return result;
  }

  /**
   * Delete the current edge.
   */
  public boolean result(final Object iRecord) {
    final OIdentifiable id = (OIdentifiable) iRecord;

    if (compiledFilter != null) {
      // ADDITIONAL FILTERING
      if (!(Boolean) compiledFilter.evaluate(id.getRecord(), null, context))
        return true;
    }

    if (id.getIdentity().isValid()) {

      final OEdge e = toEdge(id);

      if (e != null) {
        e.delete();

        if (!txAlreadyBegun && batch > 0 && (removed + 1) % batch == 0) {
          getDatabase().commit();
          getDatabase().begin();
        }

        removed++;
      }
    }

    return true;
  }

  private OEdge toEdge(OIdentifiable item) {
    if (item != null && item instanceof OElement) {
      final OIdentifiable a = item;
      return ((OElement) item).asEdge().orElseThrow(() ->  new OCommandExecutionException(""+(a.getIdentity())+" is not an edge"));
    } else {
      item = getDatabase().load(item.getIdentity());
      if (item != null && item instanceof OElement) {
        final OIdentifiable a = item;
        return ((OElement) item).asEdge().orElseThrow(() ->  new OCommandExecutionException(""+(a.getIdentity())+" is not an edge"));
      }
    }
    return null;
  }

  private OVertex toVertex(OIdentifiable item) {
    if (item != null && item instanceof OElement) {
      return ((OElement) item).asVertex().orElse(null);
    } else {
      item = getDatabase().load(item.getIdentity());
      if (item != null && item instanceof OElement) {
        return ((OElement) item).asVertex().orElse(null);
      }
    }
    return null;
  }

  @Override
  public String getSyntax() {
    return "DELETE EDGE <rid>|FROM <rid>|TO <rid>|<[<class>] [WHERE <conditions>]> [BATCH <batch-size>]";
  }

  @Override
  public void end() {

  }

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

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

  public DISTRIBUTED_RESULT_MGMT getDistributedResultManagement() {
    return getDistributedExecutionMode() == DISTRIBUTED_EXECUTION_MODE.LOCAL ?
        DISTRIBUTED_RESULT_MGMT.CHECK_FOR_EQUALS :
        DISTRIBUTED_RESULT_MGMT.MERGE;
  }

  @Override
  public Object getResult() {
    return null;
  }

  public DISTRIBUTED_EXECUTION_MODE getDistributedExecutionMode() {
    return query != null && !getDatabase().getTransaction().isActive() ?
        DISTRIBUTED_EXECUTION_MODE.REPLICATE :
        DISTRIBUTED_EXECUTION_MODE.LOCAL;
  }

  @Override
  public Set<String> getInvolvedClusters() {
    final HashSet<String> result = new HashSet<String>();
    if (rids != null) {
      final ODatabaseDocumentInternal database = getDatabase();
      for (ORecordId rid : rids) {
        result.add(database.getClusterNameById(rid.getClusterId()));
      }
    } else if (query != null) {
      final OCommandExecutor executor = OCommandManager.instance().getExecutor((OCommandRequestInternal) query);
      // COPY THE CONTEXT FROM THE REQUEST
      executor.setContext(context);
      executor.parse(query);
      return executor.getInvolvedClusters();
    }
    return result;
  }

  /**
   * setLimit() for DELETE EDGE is ignored. Please use LIMIT keyword in the SQL statement
   */
  public <RET extends OCommandExecutor> RET setLimit(final int iLimit) {
    // do nothing
    return (RET) this;
  }
}