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

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.parser.OBaseParser;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.command.OCommandPredicate;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OQueryParsingException;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.sql.OCommandExecutorSQLSelect;
import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.OSQLEngine;
import com.orientechnologies.orient.core.sql.OSQLHelper;
import com.orientechnologies.orient.core.sql.operator.OQueryOperator;
import com.orientechnologies.orient.core.sql.operator.OQueryOperatorAnd;
import com.orientechnologies.orient.core.sql.operator.OQueryOperatorNot;
import com.orientechnologies.orient.core.sql.operator.OQueryOperatorOr;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;

import java.util.*;

/**
 * Parses text in SQL format and build a tree of conditions.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 *
 */
public class OSQLPredicate extends OBaseParser implements OCommandPredicate {
  protected Set<OProperty>                properties = new HashSet<OProperty>();
  protected OSQLFilterCondition           rootCondition;
  protected List<String>                  recordTransformed;
  protected List<OSQLFilterItemParameter> parameterItems;
  protected int                           braces;
  protected OCommandContext               context;

  public OSQLPredicate() {
  }

  public OSQLPredicate(final String iText) {
    text(iText);
  }

  protected void throwSyntaxErrorException(final String iText) {
    final String syntax = getSyntax();
    if (syntax.equals("?"))
      throw new OCommandSQLParsingException(iText, parserText, parserGetPreviousPosition());

    throw new OCommandSQLParsingException(iText + ". Use " + syntax, parserText, parserGetPreviousPosition());
  }

  protected String upperCase(String text) {
    // TODO remove and refactor (see same method in OCommandExecutorAbstract)
    StringBuilder result = new StringBuilder(text.length());
    for (char c : text.toCharArray()) {
      String upper = ("" + c).toUpperCase(Locale.ENGLISH);
      if (upper.length() > 1) {
        result.append(c);
      } else {
        result.append(upper);
      }
    }
    return result.toString();
  }

  public OSQLPredicate text(final String iText) {
    if (iText == null)
      throw new OCommandSQLParsingException("Query text is null");

    try {
      parserText = iText;
      parserTextUpperCase = upperCase(parserText);
      parserSetCurrentPosition(0);
      parserSkipWhiteSpaces();

      rootCondition = (OSQLFilterCondition) extractConditions(null);

      optimize();
    } catch (OQueryParsingException e) {
      if (e.getText() == null)
        // QUERY EXCEPTION BUT WITHOUT TEXT: NEST IT
        throw OException.wrapException(
            new OQueryParsingException("Error on parsing query", parserText, parserGetCurrentPosition()), e);

      throw e;
    } catch (Exception t) {
      throw OException.wrapException(new OQueryParsingException("Error on parsing query", parserText, parserGetCurrentPosition()),
          t);
    }
    return this;
  }

  public Object evaluate() {
    return evaluate(null, null, null);
  }

  public Object evaluate(final OCommandContext iContext) {
    return evaluate(null, null, iContext);
  }

  public Object evaluate(final OIdentifiable iRecord, ODocument iCurrentResult, final OCommandContext iContext) {
    if (rootCondition == null)
      return true;

    return rootCondition.evaluate(iRecord, iCurrentResult, iContext);
  }

  protected Object extractConditions(final OSQLFilterCondition iParentCondition) {
    final int oldPosition = parserGetCurrentPosition();
    parserNextWord(true, " )=><,\r\n");
    final String word = parserGetLastWord();

    boolean inBraces = word.length() > 0 && word.charAt(0) == OStringSerializerHelper.EMBEDDED_BEGIN;

    if (word.length() > 0 && (word.equalsIgnoreCase("SELECT") || word.equalsIgnoreCase("TRAVERSE"))) {
      // SUB QUERY
      final StringBuilder embedded = new StringBuilder(256);
      OStringSerializerHelper.getEmbedded(parserText, oldPosition - 1, -1, embedded);
      parserSetCurrentPosition(oldPosition + embedded.length() + 1);
      return new OSQLSynchQuery<Object>(embedded.toString());
    }

    parserSetCurrentPosition(oldPosition);
    OSQLFilterCondition currentCondition = extractCondition();

    // CHECK IF THERE IS ANOTHER CONDITION ON RIGHT
    while (parserSkipWhiteSpaces()) {

      if (!parserIsEnded() && parserGetCurrentChar() == ')')
        return currentCondition;

      final OQueryOperator nextOperator = extractConditionOperator();
      if (nextOperator == null)
        return currentCondition;

      if (nextOperator.precedence > currentCondition.getOperator().precedence) {
        // SWAP ITEMS
        final OSQLFilterCondition subCondition = new OSQLFilterCondition(currentCondition.right, nextOperator);
        currentCondition.right = subCondition;
        subCondition.right = extractConditionItem(false, 1);
      } else {
        final OSQLFilterCondition parentCondition = new OSQLFilterCondition(currentCondition, nextOperator);
        parentCondition.right = extractConditions(parentCondition);
        currentCondition = parentCondition;
      }
    }

    currentCondition.inBraces = inBraces;

    // END OF TEXT
    return currentCondition;
  }

  protected OSQLFilterCondition extractCondition() {

    if (!parserSkipWhiteSpaces())
      // END OF TEXT
      return null;

    // EXTRACT ITEMS
    Object left = extractConditionItem(true, 1);

    if (left != null && checkForEnd(left.toString()))
      return null;

    OQueryOperator oper;
    final Object right;

    if (left instanceof OQueryOperator && ((OQueryOperator) left).isUnary()) {
      oper = (OQueryOperator) left;
      left = extractConditionItem(false, 1);
      right = null;
    } else {
      oper = extractConditionOperator();

      if (oper instanceof OQueryOperatorNot)
        // SPECIAL CASE: READ NEXT OPERATOR
        oper = new OQueryOperatorNot(extractConditionOperator());

      if(oper instanceof OQueryOperatorAnd || oper instanceof OQueryOperatorOr){
        right = extractCondition();
      }else {
        right = oper != null ? extractConditionItem(false, oper.expectedRightWords) : null;
      }
    }

    // CREATE THE CONDITION OBJECT
    return new OSQLFilterCondition(left, oper, right);
  }

  protected boolean checkForEnd(final String iWord) {
    if (iWord != null
        && (iWord.equals(OCommandExecutorSQLSelect.KEYWORD_ORDER) || iWord.equals(OCommandExecutorSQLSelect.KEYWORD_LIMIT)
            || iWord.equals(OCommandExecutorSQLSelect.KEYWORD_SKIP) || iWord.equals(OCommandExecutorSQLSelect.KEYWORD_OFFSET))) {
      parserMoveCurrentPosition(iWord.length() * -1);
      return true;
    }
    return false;
  }

  private OQueryOperator extractConditionOperator() {
    if (!parserSkipWhiteSpaces())
      // END OF PARSING: JUST RETURN
      return null;

    if (parserGetCurrentChar() == ')')
      // FOUND ')': JUST RETURN
      return null;

    final OQueryOperator[] operators = OSQLEngine.getInstance().getRecordOperators();
    final String[] candidateOperators = new String[operators.length];
    for (int i = 0; i < candidateOperators.length; ++i)
      candidateOperators[i] = operators[i].keyword;

    final int operatorPos = parserNextChars(true, false, candidateOperators);

    if (operatorPos == -1) {
      parserGoBack();
      return null;
    }

    final OQueryOperator op = operators[operatorPos];
    if (op.expectsParameters) {
      // PARSE PARAMETERS IF ANY
      parserGoBack();

      parserNextWord(true, " 0123456789'\"");
      final String word = parserGetLastWord();

      final List<String> params = new ArrayList<String>();
      // CHECK FOR PARAMETERS
      if (word.length() > op.keyword.length() && word.charAt(op.keyword.length()) == OStringSerializerHelper.EMBEDDED_BEGIN) {
        int paramBeginPos = parserGetCurrentPosition() - (word.length() - op.keyword.length());
        parserSetCurrentPosition(OStringSerializerHelper.getParameters(parserText, paramBeginPos, -1, params));
      } else if (!word.equals(op.keyword))
        throw new OQueryParsingException("Malformed usage of operator '" + op.toString() + "'. Parsed operator is: " + word);

      try {
        // CONFIGURE COULD INSTANTIATE A NEW OBJECT: ACT AS A FACTORY
        return op.configure(params);
      } catch (Exception e) {
        throw OException.wrapException(new OQueryParsingException("Syntax error using the operator '" + op.toString()
            + "'. Syntax is: " + op.getSyntax()), e);
      }
    } else
      parserMoveCurrentPosition(+1);
    return op;
  }

  private Object extractConditionItem(final boolean iAllowOperator, final int iExpectedWords) {
    final Object[] result = new Object[iExpectedWords];

    for (int i = 0; i < iExpectedWords; ++i) {
      parserNextWord(false, " =><,\r\n");
      String word = parserGetLastWord();

      if (word.length() == 0)
        break;

      word = word.replaceAll("\\\\", "\\\\\\\\"); //see issue #5229

      final String uWord = word.toUpperCase(Locale.ENGLISH);

      final int lastPosition = parserIsEnded() ? parserText.length() : parserGetCurrentPosition();

      if (word.length() > 0 && word.charAt(0) == OStringSerializerHelper.EMBEDDED_BEGIN) {
        braces++;

        // SUB-CONDITION
        parserSetCurrentPosition(lastPosition - word.length() + 1);

        final Object subCondition = extractConditions(null);

        if (!parserSkipWhiteSpaces() || parserGetCurrentChar() == ')') {
          braces--;
          parserMoveCurrentPosition(+1);
        }
        if (subCondition instanceof OSQLFilterCondition) {
          ((OSQLFilterCondition) subCondition).inBraces = true;
        }
        result[i] = subCondition;
      } else if (word.charAt(0) == OStringSerializerHelper.LIST_BEGIN) {
        // COLLECTION OF ELEMENTS
        parserSetCurrentPosition(lastPosition - getLastWordLength());

        final List<String> stringItems = new ArrayList<String>();
        parserSetCurrentPosition(OStringSerializerHelper.getCollection(parserText, parserGetCurrentPosition(), stringItems));
        result[i] = convertCollectionItems(stringItems);

        parserMoveCurrentPosition(+1);

      } else if (uWord.startsWith(OSQLFilterItemFieldAll.NAME + OStringSerializerHelper.EMBEDDED_BEGIN)) {

        result[i] = new OSQLFilterItemFieldAll(this, word, null);

      } else if (uWord.startsWith(OSQLFilterItemFieldAny.NAME + OStringSerializerHelper.EMBEDDED_BEGIN)) {

        result[i] = new OSQLFilterItemFieldAny(this, word, null);

      } else {

        if (uWord.equals("NOT")) {
          if (iAllowOperator)
            return new OQueryOperatorNot();
          else {
            // GET THE NEXT VALUE
            parserNextWord(false, " )=><,\r\n");
            final String nextWord = parserGetLastWord();

            if (nextWord.length() > 0) {
              word += " " + nextWord;

              if (word.endsWith(")"))
                word = word.substring(0, word.length() - 1);
            }
          }
        } else if (uWord.equals("AND"))
          // SPECIAL CASE IN "BETWEEN X AND Y"
          result[i] = word;

        while (word.endsWith(")")) {
          final int openParenthesis = word.indexOf('(');
          if (openParenthesis == -1) {
            // DISCARD END PARENTHESIS
            word = word.substring(0, word.length() - 1);
            parserMoveCurrentPosition(-1);
          } else
            break;
        }

        word = word.replaceAll("\\\\\\\\", "\\\\"); //see issue #5229
        result[i] = OSQLHelper.parseValue(this, this, word, context);
      }
    }

    return iExpectedWords == 1 ? result[0] : result;
  }

  private List<Object> convertCollectionItems(List<String> stringItems) {
    List<Object> coll = new ArrayList<Object>();
    for (String s : stringItems) {
      coll.add(OSQLHelper.parseValue(this, this, s, context));
    }
    return coll;
  }

  public OSQLFilterCondition getRootCondition() {
    return rootCondition;
  }

  @Override
  public String toString() {
    if (rootCondition != null)
      return "Parsed: " + rootCondition.toString();
    return "Unparsed: " + parserText;
  }

  /**
   * Binds parameters.
   */
  public void bindParameters(final Map<Object, Object> iArgs) {
    if (parameterItems == null || iArgs == null || iArgs.size() == 0)
      return;

    for (int i = 0; i < parameterItems.size(); i++) {
      OSQLFilterItemParameter value = parameterItems.get(i);
      if ("?".equals(value.getName())) {
        value.setValue(iArgs.get(i));
      } else {
        value.setValue(iArgs.get(value.getName()));
      }
    }
  }

  public OSQLFilterItemParameter addParameter(final String iName) {
    final String name;
    if (iName.charAt(0) == OStringSerializerHelper.PARAMETER_NAMED) {
      name = iName.substring(1);

      // CHECK THE PARAMETER NAME IS CORRECT
      if (!OStringSerializerHelper.isAlphanumeric(name)) {
        throw new OQueryParsingException("Parameter name '" + name + "' is invalid, only alphanumeric characters are allowed");
      }
    } else
      name = iName;

    final OSQLFilterItemParameter param = new OSQLFilterItemParameter(name);

    if (parameterItems == null)
      parameterItems = new ArrayList<OSQLFilterItemParameter>();

    parameterItems.add(param);
    return param;
  }

  public void setRootCondition(final OSQLFilterCondition iCondition) {
    rootCondition = iCondition;
  }

  protected void optimize() {
    if (rootCondition != null)
      computePrefetchFieldList(rootCondition, new HashSet<String>());
  }

  protected Set<String> computePrefetchFieldList(final OSQLFilterCondition iCondition, final Set<String> iFields) {
    Object left = iCondition.getLeft();
    Object right = iCondition.getRight();
    if (left instanceof OSQLFilterItemField) {
      ((OSQLFilterItemField) left).setPreLoadedFields(iFields);
      iFields.add(((OSQLFilterItemField) left).getRoot());
    } else if (left instanceof OSQLFilterCondition)
      computePrefetchFieldList((OSQLFilterCondition) left, iFields);

    if (right instanceof OSQLFilterItemField) {
      ((OSQLFilterItemField) right).setPreLoadedFields(iFields);
      iFields.add(((OSQLFilterItemField) right).getRoot());
    } else if (right instanceof OSQLFilterCondition)
      computePrefetchFieldList((OSQLFilterCondition) right, iFields);

    return iFields;
  }
}


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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.object.enhancement.OObjectEntitySerializer;

@SuppressWarnings({ "unchecked" })
public class OObjectCustomSerializerList<TYPE> implements List<TYPE>, OObjectLazyCustomSerializer<List<TYPE>>, Serializable {
  private static final long       serialVersionUID = -8541477416577361792L;

  private ORecord              sourceRecord;
  private final List<Object>      serializedList;
  private final ArrayList<Object> list             = new ArrayList<Object>();
  private boolean                 converted        = false;
  private final Class<?>          deserializeClass;

  public OObjectCustomSerializerList(final Class<?> iDeserializeClass, final ORecord iSourceRecord,
      final List<Object> iRecordList) {
    this.sourceRecord = iSourceRecord;
    this.serializedList = iRecordList;
    this.deserializeClass = iDeserializeClass;
    for (int i = 0; i < iRecordList.size(); i++) {
      list.add(i, null);
    }
  }

  public OObjectCustomSerializerList(final Class<?> iDeserializeClass, final ORecord iSourceRecord,
      final List<Object> iRecordList, final Collection<? extends TYPE> iSourceList) {
    this.sourceRecord = iSourceRecord;
    this.serializedList = iRecordList;
    this.deserializeClass = iDeserializeClass;
    addAll(iSourceList);
    for (int i = iSourceList.size(); i < iRecordList.size(); i++) {
      list.add(i, null);
    }
  }

  public Iterator<TYPE> iterator() {
    return new OObjectCustomSerializerIterator<TYPE>(deserializeClass, sourceRecord, serializedList.iterator());
  }

  public boolean contains(final Object o) {
    boolean underlyingContains = serializedList.contains(OObjectEntitySerializer.serializeFieldValue(deserializeClass, o));
    return underlyingContains || list.contains(o);
  }

  public boolean add(TYPE element) {
    serializedList.add(OObjectEntitySerializer.serializeFieldValue(deserializeClass, element));
    return list.add(element);
  }

  public void add(int index, TYPE element) {
    setDirty();
    serializedList.add(index, OObjectEntitySerializer.serializeFieldValue(deserializeClass, element));
    list.add(index, element);
  }

  public TYPE get(final int index) {
    TYPE o = (TYPE) list.get(index);
    if (o == null) {
      Object toDeserialize = serializedList.get(index);
      o = (TYPE) OObjectEntitySerializer.deserializeFieldValue(deserializeClass, toDeserialize);
      list.set(index, o);
    }
    return o;
  }

  public int indexOf(final Object o) {
    return list.indexOf(o);
  }

  public int lastIndexOf(final Object o) {
    return list.lastIndexOf(o);
  }

  public Object[] toArray() {
    convertAll();
    return list.toArray();
  }

  public <T> T[] toArray(final T[] a) {
    convertAll();
    return list.toArray(a);
  }

  public int size() {
    return serializedList.size();
  }

  public boolean isEmpty() {
    return serializedList.isEmpty();
  }

  public boolean remove(Object o) {
    setDirty();
    int indexOfO = list.indexOf(o);
    serializedList.remove(indexOfO);
    return list.remove(o);
  }

  public boolean containsAll(Collection<?> c) {
    for (Object o : c) {
      if (!contains(o))
        return false;
    }
    return true;
  }

  public boolean addAll(Collection<? extends TYPE> c) {
    boolean dirty = false;
    for (TYPE element : c) {
      dirty = add(element) || dirty;
    }
    if (dirty)
      setDirty();
    return dirty;
  }

  public boolean addAll(int index, Collection<? extends TYPE> c) {
    for (TYPE element : c) {
      add(index, element);
      index++;
    }
    if (c.size() > 0)
      setDirty();
    return c.size() > 0;
  }

  public boolean removeAll(Collection<?> c) {
    boolean dirty = true;
    for (Object o : c) {
      dirty = dirty || remove(o);
    }
    if (dirty)
      setDirty();
    return dirty;
  }

  public boolean retainAll(Collection<?> c) {
    boolean modified = false;
    Iterator<TYPE> e = iterator();
    while (e.hasNext()) {
      if (!c.contains(e.next())) {
        remove(e);
        modified = true;
      }
    }
    return modified;
  }

  public void clear() {
    setDirty();
    serializedList.clear();
    list.clear();
  }

  public TYPE set(int index, TYPE element) {
    serializedList.set(index, OObjectEntitySerializer.serializeFieldValue(deserializeClass, element));
    return (TYPE) list.set(index, element);
  }

  public TYPE remove(int index) {
    serializedList.remove(index);
    return (TYPE) list.remove(index);
  }

  public ListIterator<TYPE> listIterator() {
    return (ListIterator<TYPE>) list.listIterator();
  }

  public ListIterator<TYPE> listIterator(int index) {
    return (ListIterator<TYPE>) list.listIterator(index);
  }

  public List<TYPE> subList(int fromIndex, int toIndex) {
    return (List<TYPE>) list.subList(fromIndex, toIndex);
  }

  public boolean isConverted() {
    return converted;
  }

  public void detach() {
    convertAll();
  }

  public void detach(boolean nonProxiedInstance) {
    convertAll();
  }

  public void detachAll(boolean nonProxiedInstance, Map<Object, Object> alreadyDetached, Map<Object, Object> lazyObjects) {
    convertAll();
  }

  protected void convertAll() {
    if (converted)
      return;

    for (int i = 0; i < size(); ++i)
      convert(i);

    converted = true;
  }

  public void setDirty() {
    if (sourceRecord != null)
      sourceRecord.setDirty();
  }

  @Override
  public List<TYPE> getNonOrientInstance() {
    List<TYPE> list = new ArrayList<TYPE>();
    list.addAll(this);
    return this;
  }

  /**
   * Convert the item requested.
   *
   * @param iIndex
   *          Position of the item to convert
   */
  private void convert(final int iIndex) {
    if (converted)
      return;

    Object o = list.get(iIndex);
    if (o == null) {
      o = serializedList.get(iIndex);
      list.set(iIndex, OObjectEntitySerializer.deserializeFieldValue(deserializeClass, o));
    }
  }

  protected boolean indexLoaded(int iIndex) {
    return list.get(iIndex) != null;
  }

  @Override
  public String toString() {
    return list.toString();
  }

  @Override
  public Object getUnderlying() {
    return serializedList;
  }
}


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

import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.record.OElement;
import com.orientechnologies.orient.core.sql.parser.OMatchPathItem;
import com.orientechnologies.orient.core.sql.parser.ORid;
import com.orientechnologies.orient.core.sql.parser.OWhereClause;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

/**
 * Created by luigidellaquila on 23/09/16.
 */
public class MatchEdgeTraverser {
  protected OResult        sourceRecord;
  protected EdgeTraversal  edge;
  protected OMatchPathItem item;

  Iterator<OResultInternal> downstream;

  public MatchEdgeTraverser(OResult lastUpstreamRecord, EdgeTraversal edge) {
    this.sourceRecord = lastUpstreamRecord;
    this.edge = edge;
    this.item = edge.edge.item;
  }

  public MatchEdgeTraverser(OResult lastUpstreamRecord, OMatchPathItem item) {
    this.sourceRecord = lastUpstreamRecord;
    this.item = item;
  }

  public boolean hasNext(OCommandContext ctx) {
    init(ctx);
    return downstream.hasNext();
  }

  public OResult next(OCommandContext ctx) {
    init(ctx);
    if (!downstream.hasNext()) {
      throw new IllegalStateException();
    }
    String endPointAlias = getEndpointAlias();
    OResultInternal nextR = downstream.next();
    OIdentifiable nextElement = nextR.getElement().get();
    Object prevValue = sourceRecord.getProperty(endPointAlias);
    if (prevValue != null && !equals(prevValue, nextElement)) {
      return null;
    }
    OResultInternal result = new OResultInternal();
    for (String prop : sourceRecord.getPropertyNames()) {
      result.setProperty(prop, sourceRecord.getProperty(prop));
    }
    result.setProperty(endPointAlias, toResult(nextElement));
    if (edge.edge.item.getFilter().getDepthAlias() != null) {
      result.setProperty(edge.edge.item.getFilter().getDepthAlias(), nextR.getMetadata("$depth"));
    }
    if (edge.edge.item.getFilter().getPathAlias() != null) {
      result.setProperty(edge.edge.item.getFilter().getPathAlias(), nextR.getMetadata("$matchPath"));
    }
    return result;
  }

  protected boolean equals(Object prevValue, OIdentifiable nextElement) {
    if (prevValue instanceof OResult) {
      prevValue = ((OResult) prevValue).getElement().orElse(null);
    }
    if (nextElement instanceof OResult) {
      nextElement = ((OResult) nextElement).getElement().orElse(null);
    }
    return prevValue != null && prevValue.equals(nextElement);
  }

  protected Object toResult(OIdentifiable nextElement) {
    OResultInternal result = new OResultInternal();
    result.setElement(nextElement);
    return result;
  }

  protected String getStartingPointAlias() {
    return this.edge.edge.out.alias;
  }

  protected String getEndpointAlias() {
    if (this.item != null) {
      return this.item.getFilter().getAlias();
    }
    return this.edge.edge.in.alias;
  }

  protected void init(OCommandContext ctx) {
    if (downstream == null) {
      Object startingElem = sourceRecord.getProperty(getStartingPointAlias());
      if (startingElem instanceof OResult) {
        startingElem = ((OResult) startingElem).getElement().orElse(null);
      }
      downstream = executeTraversal(ctx, this.item, (OIdentifiable) startingElem, 0, null).iterator();
    }
  }

  protected Iterable<OResultInternal> executeTraversal(OCommandContext iCommandContext, OMatchPathItem item,
      OIdentifiable startingPoint, int depth, List<OIdentifiable> pathToHere) {

    OWhereClause filter = null;
    OWhereClause whileCondition = null;
    Integer maxDepth = null;
    String className = null;
    Integer clusterId = null;
    ORid targetRid = null;
    if (item.getFilter() != null) {
      filter = getTargetFilter(item);
      whileCondition = item.getFilter().getWhileCondition();
      maxDepth = item.getFilter().getMaxDepth();
      className = targetClassName(item, iCommandContext);
      String clusterName = targetClusterName(item, iCommandContext);
      if (clusterName != null) {
        clusterId = iCommandContext.getDatabase().getClusterIdByName(clusterName);
      }
      targetRid = targetRid(item, iCommandContext);
    }

    Iterable<OResultInternal> result;

    if (whileCondition == null && maxDepth == null) {// in this case starting point is not returned and only one level depth is
      // evaluated

      Iterable<OResultInternal> queryResult = traversePatternEdge(startingPoint, iCommandContext);
      final OWhereClause theFilter = filter;
      final String theClassName = className;
      final Integer theClusterId = clusterId;
      final ORid theTargetRid = targetRid;
      result = () -> {
        Iterator<OResultInternal> iter = queryResult.iterator();

        return new Iterator() {

          OResultInternal nextElement = null;

          @Override
          public boolean hasNext() {
            if (nextElement == null) {
              fetchNext();
            }
            return nextElement != null;
          }

          @Override
          public Object next() {
            if (nextElement == null) {
              fetchNext();
            }
            if (nextElement == null) {
              throw new IllegalStateException();
            }
            OResultInternal res = nextElement;
            nextElement = null;
            return res;
          }

          public void fetchNext() {
            Object previousMatch = iCommandContext.getVariable("$currentMatch");
            while (iter.hasNext()) {
              OResultInternal next = iter.next();
              OElement elem = next.toElement();
              iCommandContext.setVariable("$currentMatch", elem);
              if (matchesFilters(iCommandContext, theFilter, elem) && matchesClass(iCommandContext, theClassName, elem)
                  && matchesCluster(iCommandContext, theClusterId, elem) && matchesRid(iCommandContext, theTargetRid, elem)) {
                nextElement = next;
                break;
              }
            }
            iCommandContext.setVariable("$currentMatch", previousMatch);
          }
        };
      };

    } else {// in this case also zero level (starting point) is considered and traversal depth is given by the while condition
      result = new ArrayList<>();
      iCommandContext.setVariable("$depth", depth);
      Object previousMatch = iCommandContext.getVariable("$currentMatch");
      iCommandContext.setVariable("$currentMatch", startingPoint);

      if (matchesFilters(iCommandContext, filter, startingPoint) && matchesClass(iCommandContext, className, startingPoint)
          && matchesCluster(iCommandContext, clusterId, startingPoint) && matchesRid(iCommandContext, targetRid, startingPoint)) {
        OResultInternal rs = new OResultInternal(startingPoint);
        // set traversal depth in the metadata
        rs.setMetadata("$depth", depth);
        // set traversal path in the metadata
        rs.setMetadata("$matchPath", pathToHere == null ? Collections.EMPTY_LIST : pathToHere);
        // add the result to the list
        ((List) result).add(rs);
      }

      if ((maxDepth == null || depth < maxDepth) && (whileCondition == null || whileCondition
          .matchesFilters(startingPoint, iCommandContext))) {

        Iterable<OResultInternal> queryResult = traversePatternEdge(startingPoint, iCommandContext);

        for (OResultInternal origin : queryResult) {
          //          if(origin.equals(startingPoint)){
          //            continue;
          //          }
          // TODO consider break strategies (eg. re-traverse nodes)

          List<OIdentifiable> newPath = new ArrayList<>();
          if (pathToHere != null) {
            newPath.addAll(pathToHere);
          }

          OElement elem = origin.toElement();
          newPath.add(elem.getIdentity());

          Iterable<OResultInternal> subResult = executeTraversal(iCommandContext, item, elem, depth + 1, newPath);
          if (subResult instanceof Collection) {
            ((List) result).addAll((Collection<? extends OResultInternal>) subResult);
          } else {
            for (OResultInternal i : subResult) {
              ((List) result).add(i);
            }
          }
        }
      }
      iCommandContext.setVariable("$currentMatch", previousMatch);
    }
    return result;
  }

  protected OWhereClause getTargetFilter(OMatchPathItem item) {
    return item.getFilter().getFilter();
  }

  protected String targetClassName(OMatchPathItem item, OCommandContext iCommandContext) {
    return item.getFilter().getClassName(iCommandContext);
  }

  protected String targetClusterName(OMatchPathItem item, OCommandContext iCommandContext) {
    return item.getFilter().getClusterName(iCommandContext);
  }

  protected ORid targetRid(OMatchPathItem item, OCommandContext iCommandContext) {
    return item.getFilter().getRid(iCommandContext);
  }

  private boolean matchesClass(OCommandContext iCommandContext, String className, OIdentifiable origin) {
    if (className == null) {
      return true;
    }
    OElement element = null;
    if (origin instanceof OElement) {
      element = (OElement) origin;
    } else if (origin != null) {
      Object record = origin.getRecord();
      if (record instanceof OElement) {
        element = (OElement) record;
      }
    }
    if (element != null) {
      Optional<OClass> clazz = element.getSchemaType();
      if (!clazz.isPresent()) {
        return false;
      }
      return clazz.get().isSubClassOf(className);
    }
    return false;
  }

  private boolean matchesCluster(OCommandContext iCommandContext, Integer clusterId, OIdentifiable origin) {
    if (clusterId == null) {
      return true;
    }
    if (origin == null) {
      return false;
    }

    if (origin.getIdentity() == null) {
      return false;
    }
    return clusterId.equals(origin.getIdentity().getClusterId());
  }

  private boolean matchesRid(OCommandContext iCommandContext, ORid rid, OIdentifiable origin) {
    if (rid == null) {
      return true;
    }
    if (origin == null) {
      return false;
    }

    if (origin.getIdentity() == null) {
      return false;
    }
    return origin.getIdentity().equals(rid.toRecordId(origin, iCommandContext));
  }

  protected boolean matchesFilters(OCommandContext iCommandContext, OWhereClause filter, OIdentifiable origin) {
    return filter == null || filter.matchesFilters(origin, iCommandContext);
  }

  //TODO refactor this method to receive the item.

  protected Iterable<OResultInternal> traversePatternEdge(OIdentifiable startingPoint, OCommandContext iCommandContext) {

    Iterable possibleResults = null;
    if (this.item.getFilter() != null) {
      String alias = getEndpointAlias();
      Object matchedNodes = iCommandContext.getVariable(MatchPrefetchStep.PREFETCHED_MATCH_ALIAS_PREFIX + alias);
      if (matchedNodes != null) {
        if (matchedNodes instanceof Iterable) {
          possibleResults = (Iterable) matchedNodes;
        } else {
          possibleResults = Collections.singleton(matchedNodes);
        }
      }
    }

    Object prevCurrent = iCommandContext.getVariable("$current");
    iCommandContext.setVariable("$current", startingPoint);
    Object qR;
    try {
      qR = this.item.getMethod().execute(startingPoint, possibleResults, iCommandContext);
    } finally {
      iCommandContext.setVariable("$current", prevCurrent);
    }

    if (qR == null) {
      return Collections.EMPTY_LIST;
    }
    if (qR instanceof OIdentifiable) {
      return Collections.singleton(new OResultInternal((OIdentifiable) qR));
    }
    if (qR instanceof Iterable) {
      final Iterator<Object> iter = ((Iterable) qR).iterator();
      Iterable<OResultInternal> result = () -> new Iterator<OResultInternal>() {
        private OResultInternal nextElement;

        @Override
        public boolean hasNext() {
          if (nextElement == null) {
            fetchNext();
          }
          return nextElement != null;
        }

        @Override
        public OResultInternal next() {
          if (nextElement == null) {
            fetchNext();
          }
          if (nextElement == null) {
            throw new IllegalStateException();
          }
          OResultInternal res = nextElement;
          nextElement = null;
          return res;
        }

        public void fetchNext() {
          while (iter.hasNext()) {
            Object o = iter.next();
            if (o instanceof OIdentifiable) {
              nextElement = new OResultInternal((OIdentifiable) o);
              break;
            } else if (o instanceof OResultInternal) {
              nextElement = (OResultInternal) o;
              break;
            } else if (o == null) {
              continue;
            } else {
              throw new UnsupportedOperationException();
            }
          }
        }
      };

      return result;
    }
    return Collections.EMPTY_LIST;
  }

}


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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import com.orientechnologies.orient.core.record.ORecord;

/**
 *
 * @author Luca Molino (molino.luca--at--gmail.com)
 *
 */
@SuppressWarnings({ "unchecked" })
public class OObjectEnumLazyList<TYPE extends Enum<?>> implements List<TYPE>, OObjectLazyEnumSerializer<List<TYPE>>, Serializable {
  private static final long     serialVersionUID = -8541477416577361792L;

  private ORecord            sourceRecord;
  private final List<Object>    serializedList;
  private final ArrayList<TYPE> list             = new ArrayList<TYPE>();
  private boolean               converted        = false;
  private final Class<Enum>     enumClass;

  public OObjectEnumLazyList(final Class<Enum> iEnumClass, final ORecord iSourceRecord, final List<Object> iRecordList) {
    this.sourceRecord = iSourceRecord;
    this.serializedList = iRecordList;
    this.enumClass = iEnumClass;
    for (int i = 0; i < iRecordList.size(); i++) {
      list.add(i, null);
    }
  }

  public OObjectEnumLazyList(final Class<Enum> iEnumClass, final ORecord iSourceRecord, final List<Object> iRecordList,
      final Collection<? extends TYPE> iSourceList) {
    this.sourceRecord = iSourceRecord;
    this.serializedList = iRecordList;
    this.enumClass = iEnumClass;
    addAll(iSourceList);
    for (int i = iSourceList.size(); i < iRecordList.size(); i++) {
      list.add(i, null);
    }
  }

  public Iterator<TYPE> iterator() {
    return new OObjectEnumLazyIterator<TYPE>(enumClass, sourceRecord, serializedList.iterator());
  }

  public boolean contains(final Object o) {
	  return this.indexOf(o) > -1;
  }

  public boolean add(TYPE element) {
    serializedList.add(element.name());
    return list.add(element);
  }

  public void add(int index, TYPE element) {
    setDirty();
    serializedList.add(index, element.name());
    list.add(index, element);
  }

  public TYPE get(final int index) {
    TYPE o = (TYPE) list.get(index);
    if (o == null) {
      Object toDeserialize = serializedList.get(index);
      if (toDeserialize instanceof Number)
        o = (TYPE) enumClass.getEnumConstants()[((Number) toDeserialize).intValue()];
      else
        o = (TYPE) Enum.valueOf(enumClass, toDeserialize.toString());
      list.set(index, o);
    }
    return o;
  }

  public int indexOf(final Object o) {
	  TYPE enumToCheck = objectToEnum(o);

	  if(enumToCheck != null)
		  return serializedList.indexOf(enumToCheck.name());
	  else
		  return -1;
  }

  public int lastIndexOf(final Object o) {
	  TYPE enumToCheck = objectToEnum(o);

	  if(enumToCheck != null)
		  return serializedList.lastIndexOf(enumToCheck.name());
	  else
		  return -1;
  }

  public Object[] toArray() {
    convertAll();
    return list.toArray();
  }

  public <T> T[] toArray(final T[] a) {
    convertAll();
    return list.toArray(a);
  }

  public int size() {
    return serializedList.size();
  }

  public boolean isEmpty() {
    return serializedList.isEmpty();
  }

  public boolean remove(Object o) {
    setDirty();
    int indexOfO = list.indexOf(o);
    serializedList.remove(indexOfO);
    return list.remove(o);
  }

  public boolean containsAll(Collection<?> c) {
    for (Object o : c) {
      if (!contains(o))
        return false;
    }
    return true;
  }

  public boolean addAll(Collection<? extends TYPE> c) {
    boolean dirty = false;
    for (TYPE element : c) {
      dirty = add(element) || dirty;
    }
    if (dirty)
      setDirty();
    return dirty;
  }

  public boolean addAll(int index, Collection<? extends TYPE> c) {
    for (TYPE element : c) {
      add(index, element);
      index++;
    }
    if (c.size() > 0)
      setDirty();
    return c.size() > 0;
  }

  public boolean removeAll(Collection<?> c) {
    boolean dirty = true;
    for (Object o : c) {
      dirty = dirty || remove(o);
    }
    if (dirty)
      setDirty();
    return dirty;
  }

  public boolean retainAll(Collection<?> c) {
    boolean modified = false;
    Iterator<TYPE> e = iterator();
    while (e.hasNext()) {
      if (!c.contains(e.next())) {
        remove(e);
        modified = true;
      }
    }
    return modified;
  }

  public void clear() {
    setDirty();
    serializedList.clear();
    list.clear();
  }

  public TYPE set(int index, TYPE element) {
    serializedList.set(index, element.name());
    return (TYPE) list.set(index, element);
  }

  public TYPE remove(int index) {
    serializedList.remove(index);
    return (TYPE) list.remove(index);
  }

  public ListIterator<TYPE> listIterator() {
    return (ListIterator<TYPE>) list.listIterator();
  }

  public ListIterator<TYPE> listIterator(int index) {
    return (ListIterator<TYPE>) list.listIterator(index);
  }

  public List<TYPE> subList(int fromIndex, int toIndex) {
    return (List<TYPE>) list.subList(fromIndex, toIndex);
  }

  public boolean isConverted() {
    return converted;
  }

  public void detach() {
    convertAll();
  }

  public void detach(boolean nonProxiedInstance) {
    convertAll();
  }

  public void detachAll(boolean nonProxiedInstance, Map<Object, Object> alreadyDetached, Map<Object, Object> lazyObjects) {
    convertAll();
  }

  @Override
  public List<TYPE> getNonOrientInstance() {
    List<TYPE> list = new ArrayList<TYPE>();
    list.addAll(this);
    return list;
  }

  protected void convertAll() {
    if (converted)
      return;

    for (int i = 0; i < size(); ++i)
      convert(i);

    converted = true;
  }

  public void setDirty() {
    if (sourceRecord != null)
      sourceRecord.setDirty();
  }

  @Override
  public Object getUnderlying() {
    return serializedList;
  }

  /**
   * Convert the item requested.
   *
   * @param iIndex
   *          Position of the item to convert
   */
  private void convert(final int iIndex) {
    if (converted)
      return;

    Object o = list.get(iIndex);
    if (o == null) {
      o = serializedList.get(iIndex);
      if (o instanceof Number)
        o = enumClass.getEnumConstants()[((Number) o).intValue()];
      else
        o = Enum.valueOf(enumClass, o.toString());
      list.set(iIndex, (TYPE) o);
    }
  }

  protected boolean indexLoaded(int iIndex) {
    return list.get(iIndex) != null;
  }

  @Override
  public String toString() {
    return list.toString();
  }

  private TYPE objectToEnum(Object o)
  {
	 if(o != null && (o.getClass() == this.enumClass))
	 {
		 return (TYPE) o;
	 }
	 else
		 return null;
  }
}


/*
 *
 *  *  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.serialization.serializer.record.string;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.io.OIOUtils;
import com.orientechnologies.common.profiler.OProfiler;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OSchemaException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentInternal;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.serialization.serializer.record.ORecordSerializer;
import com.orientechnologies.orient.core.serialization.serializer.string.OStringSerializerAnyStreamable;
import com.orientechnologies.orient.core.serialization.serializer.string.OStringSerializerEmbedded;
import com.orientechnologies.orient.core.util.ODateHelper;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.util.*;

@SuppressWarnings("serial")
public abstract class ORecordSerializerStringAbstract implements ORecordSerializer, Serializable {
  protected static final OProfiler PROFILER              = Orient.instance().getProfiler();
  private static final char        DECIMAL_SEPARATOR     = '.';
  private static final String      MAX_INTEGER_AS_STRING = String.valueOf(Integer.MAX_VALUE);
  private static final int         MAX_INTEGER_DIGITS    = MAX_INTEGER_AS_STRING.length();

  public static Object fieldTypeFromStream(final ODocument iDocument, OType iType, final Object iValue) {
    if (iValue == null)
      return null;

    if (iType == null)
      iType = OType.EMBEDDED;

    switch (iType) {
    case STRING:
    case INTEGER:
    case BOOLEAN:
    case FLOAT:
    case DECIMAL:
    case LONG:
    case DOUBLE:
    case SHORT:
    case BYTE:
    case BINARY:
    case DATE:
    case DATETIME:
    case LINK:
      return simpleValueFromStream(iValue, iType);

    case EMBEDDED: {
      // EMBEDED RECORD
      final Object embeddedObject = OStringSerializerEmbedded.INSTANCE.fromStream((String) iValue);
      if (embeddedObject instanceof ODocument)
        ODocumentInternal.addOwner((ODocument) embeddedObject, iDocument);

      // EMBEDDED OBJECT
      return embeddedObject;
    }

    case CUSTOM:
      // RECORD
      final Object result = OStringSerializerAnyStreamable.INSTANCE.fromStream((String) iValue);
      if (result instanceof ODocument)
        ODocumentInternal.addOwner((ODocument) result, iDocument);
      return result;

    case EMBEDDEDSET:
    case EMBEDDEDLIST: {
      final String value = (String) iValue;
      return ORecordSerializerSchemaAware2CSV.INSTANCE.embeddedCollectionFromStream(iDocument, iType, null, null, value);
    }

    case EMBEDDEDMAP: {
      final String value = (String) iValue;
      return ORecordSerializerSchemaAware2CSV.INSTANCE.embeddedMapFromStream(iDocument, null, value, null);
    }
    }

    throw new IllegalArgumentException("Type " + iType + " not supported to convert value: " + iValue);
  }

  public static Object convertValue(final String iValue, final OType iExpectedType) {
    final Object v = getTypeValue((String) iValue);
    return OType.convert(v, iExpectedType.getDefaultJavaType());
  }

  public static void fieldTypeToString(final StringBuilder iBuffer, OType iType, final Object iValue) {
    if (iValue == null)
      return;

    final long timer = PROFILER.startChrono();

    if (iType == null) {
      if (iValue instanceof ORID)
        iType = OType.LINK;
      else
        iType = OType.EMBEDDED;
    }

    switch (iType) {
    case STRING:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.string2string"), "Serialize string to string", timer);
      break;

    case BOOLEAN:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.bool2string"), "Serialize boolean to string", timer);
      break;

    case INTEGER:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.int2string"), "Serialize integer to string", timer);
      break;

    case FLOAT:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.float2string"), "Serialize float to string", timer);
      break;

    case DECIMAL:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.decimal2string"), "Serialize decimal to string",
          timer);
      break;

    case LONG:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.long2string"), "Serialize long to string", timer);
      break;

    case DOUBLE:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.double2string"), "Serialize double to string", timer);
      break;

    case SHORT:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.short2string"), "Serialize short to string", timer);
      break;

    case BYTE:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.byte2string"), "Serialize byte to string", timer);
      break;

    case BINARY:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.binary2string"), "Serialize binary to string", timer);
      break;

    case DATE:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.date2string"), "Serialize date to string", timer);
      break;

    case DATETIME:
      simpleValueToStream(iBuffer, iType, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.datetime2string"), "Serialize datetime to string",
          timer);
      break;

    case LINK:
      if (iValue instanceof ORecordId)
        ((ORecordId) iValue).toString(iBuffer);
      else
        ((OIdentifiable) iValue).getIdentity().toString(iBuffer);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.link2string"), "Serialize link to string", timer);
      break;

    case EMBEDDEDSET:
      ORecordSerializerSchemaAware2CSV.INSTANCE
          .embeddedCollectionToStream(ODatabaseRecordThreadLocal.instance().getIfDefined(), iBuffer, null, null, iValue, true,
              true);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.embedSet2string"), "Serialize embeddedset to string",
          timer);
      break;

    case EMBEDDEDLIST:
      ORecordSerializerSchemaAware2CSV.INSTANCE
          .embeddedCollectionToStream(ODatabaseRecordThreadLocal.instance().getIfDefined(), iBuffer, null, null, iValue, true,
              false);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.embedList2string"),
          "Serialize embeddedlist to string", timer);
      break;

    case EMBEDDEDMAP:
      ORecordSerializerSchemaAware2CSV.INSTANCE.embeddedMapToStream(ODatabaseRecordThreadLocal.instance().getIfDefined(), iBuffer, null, null, iValue, true);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.embedMap2string"), "Serialize embeddedmap to string",
          timer);
      break;

    case EMBEDDED:
      if (iValue instanceof ODocument) {
        ORecordSerializerSchemaAware2CSV.INSTANCE.toString((ODocument) iValue, iBuffer, null);
      } else
        OStringSerializerEmbedded.INSTANCE.toStream(iBuffer, iValue);
      PROFILER
          .stopChrono(PROFILER.getProcessMetric("serializer.record.string.embed2string"), "Serialize embedded to string", timer);
      break;

    case CUSTOM:
      OStringSerializerAnyStreamable.INSTANCE.toStream(iBuffer, iValue);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.custom2string"), "Serialize custom to string", timer);
      break;

    default:
      throw new IllegalArgumentException("Type " + iType + " not supported to convert value: " + iValue);
    }
  }

  /**
   * Parses a string returning the closer type. Numbers by default are INTEGER if haven't decimal separator, otherwise FLOAT. To
   * treat all the number types numbers are postponed with a character that tells the type: b=byte, s=short, l=long, f=float,
   * d=double, t=date.
   *
   * @param iValue
   *          Value to parse
   * @return The closest type recognized
   */
  public static OType getType(final String iValue) {
    if (iValue.length() == 0)
      return null;

    final char firstChar = iValue.charAt(0);

    if (firstChar == ORID.PREFIX)
      // RID
      return OType.LINK;
    else if (firstChar == '\'' || firstChar == '"')
      return OType.STRING;
    else if (firstChar == OStringSerializerHelper.BINARY_BEGINEND)
      return OType.BINARY;
    else if (firstChar == OStringSerializerHelper.EMBEDDED_BEGIN)
      return OType.EMBEDDED;
    else if (firstChar == OStringSerializerHelper.LIST_BEGIN)
      return OType.EMBEDDEDLIST;
    else if (firstChar == OStringSerializerHelper.SET_BEGIN)
      return OType.EMBEDDEDSET;
    else if (firstChar == OStringSerializerHelper.MAP_BEGIN)
      return OType.EMBEDDEDMAP;
    else if (firstChar == OStringSerializerHelper.CUSTOM_TYPE)
      return OType.CUSTOM;

    // BOOLEAN?
    if (iValue.equalsIgnoreCase("true") || iValue.equalsIgnoreCase("false"))
      return OType.BOOLEAN;

    // NUMBER OR STRING?
    boolean integer = true;
    for (int index = 0; index < iValue.length(); ++index) {
      final char c = iValue.charAt(index);
      if (c < '0' || c > '9')
        if ((index == 0 && (c == '+' || c == '-')))
          continue;
        else if (c == DECIMAL_SEPARATOR)
          integer = false;
        else {
          if (index > 0)
            if (!integer && c == 'E') {
              // CHECK FOR SCIENTIFIC NOTATION
              if (index < iValue.length()) {
                if (iValue.charAt(index + 1) == '-')
                  // JUMP THE DASH IF ANY (NOT MANDATORY)
                  index++;
                continue;
              }
            } else if (c == 'f')
              return index != (iValue.length() - 1) ? OType.STRING : OType.FLOAT;
            else if (c == 'c')
              return index != (iValue.length() - 1) ? OType.STRING : OType.DECIMAL;
            else if (c == 'l')
              return index != (iValue.length() - 1) ? OType.STRING : OType.LONG;
            else if (c == 'd')
              return index != (iValue.length() - 1) ? OType.STRING : OType.DOUBLE;
            else if (c == 'b')
              return index != (iValue.length() - 1) ? OType.STRING : OType.BYTE;
            else if (c == 'a')
              return index != (iValue.length() - 1) ? OType.STRING : OType.DATE;
            else if (c == 't')
              return index != (iValue.length() - 1) ? OType.STRING : OType.DATETIME;
            else if (c == 's')
              return index != (iValue.length() - 1) ? OType.STRING : OType.SHORT;
            else if (c == 'e') { //eg. 1e-06
              try{
                Double.parseDouble(iValue);
                return  OType.DOUBLE;
              } catch (Exception ignore) {
                return OType.STRING;
              }
            }

          return OType.STRING;
        }
    }

    if (integer) {
      // AUTO CONVERT TO LONG IF THE INTEGER IS TOO BIG
      final int numberLength = iValue.length();
      if (numberLength > MAX_INTEGER_DIGITS || (numberLength == MAX_INTEGER_DIGITS && iValue.compareTo(MAX_INTEGER_AS_STRING) > 0))
        return OType.LONG;

      return OType.INTEGER;
    }

    // CHECK IF THE DECIMAL NUMBER IS A FLOAT OR DOUBLE
    final double dou = Double.parseDouble(iValue);
    if (dou <= Float.MAX_VALUE && dou >= Float.MIN_VALUE && Double.toString(dou).equals(Float.toString((float) dou))
        && new Double(new Double(dou).floatValue()).doubleValue() == dou) {
      return OType.FLOAT;
    } else if (!new Double(dou).toString().equals(iValue)) {
      return OType.DECIMAL;
    }

    return OType.DOUBLE;
  }

  /**
   * Parses the field type char returning the closer type. Default is STRING. b=binary if iValue.length() >= 4 b=byte if
   * iValue.length() <= 3 s=short, l=long f=float d=double a=date t=datetime
   *
   * @param iValue
   *          Value to parse
   * @param iCharType
   *          Char value indicating the type
   * @return The closest type recognized
   */
  public static OType getType(final String iValue, final char iCharType) {
    if (iCharType == 'f')
      return OType.FLOAT;
    else if (iCharType == 'c')
      return OType.DECIMAL;
    else if (iCharType == 'l')
      return OType.LONG;
    else if (iCharType == 'd')
      return OType.DOUBLE;
    else if (iCharType == 'b') {
      if (iValue.length() >= 1 && iValue.length() <= 3)
        return OType.BYTE;
      else
        return OType.BINARY;
    } else if (iCharType == 'a')
      return OType.DATE;
    else if (iCharType == 't')
      return OType.DATETIME;
    else if (iCharType == 's')
      return OType.SHORT;
    else if (iCharType == 'e')
      return OType.EMBEDDEDSET;
    else if (iCharType == 'g')
      return OType.LINKBAG;
    else if (iCharType == 'z')
      return OType.LINKLIST;
    else if (iCharType == 'm')
      return OType.LINKMAP;
    else if (iCharType == 'x')
      return OType.LINK;
    else if (iCharType == 'n')
      return OType.LINKSET;
    else if (iCharType == 'x')
      return OType.LINK;
    else if (iCharType == 'u')
      return OType.CUSTOM;

    return OType.STRING;
  }

  /**
   * Parses a string returning the value with the closer type. Numbers by default are INTEGER if haven't decimal separator,
   * otherwise FLOAT. To treat all the number types numbers are postponed with a character that tells the type: b=byte, s=short,
   * l=long, f=float, d=double, t=date. If starts with # it's a RecordID. Most of the code is equals to getType() but has been
   * copied to speed-up it.
   *
   * @param iValue
   *          Value to parse
   * @return The closest type recognized
   */
  public static Object getTypeValue(final String iValue) {
    if (iValue == null || iValue.equalsIgnoreCase("NULL"))
      return null;

    if (iValue.length() == 0)
      return "";

    if (iValue.length() > 1)
      if (iValue.charAt(0) == '"' && iValue.charAt(iValue.length() - 1) == '"')
        // STRING
        return OStringSerializerHelper.decode(iValue.substring(1, iValue.length() - 1));
      else if (iValue.charAt(0) == OStringSerializerHelper.BINARY_BEGINEND
          && iValue.charAt(iValue.length() - 1) == OStringSerializerHelper.BINARY_BEGINEND)
        // STRING
        return OStringSerializerHelper.getBinaryContent(iValue);
      else if (iValue.charAt(0) == OStringSerializerHelper.LIST_BEGIN
          && iValue.charAt(iValue.length() - 1) == OStringSerializerHelper.LIST_END) {
        // LIST
        final ArrayList<String> coll = new ArrayList<String>();
        OStringSerializerHelper.getCollection(iValue, 0, coll, OStringSerializerHelper.LIST_BEGIN,
            OStringSerializerHelper.LIST_END, OStringSerializerHelper.COLLECTION_SEPARATOR);
        return coll;
      } else if (iValue.charAt(0) == OStringSerializerHelper.SET_BEGIN
          && iValue.charAt(iValue.length() - 1) == OStringSerializerHelper.SET_END) {
        // SET
        final Set<String> coll = new HashSet<String>();
        OStringSerializerHelper.getCollection(iValue, 0, coll, OStringSerializerHelper.SET_BEGIN, OStringSerializerHelper.SET_END,
            OStringSerializerHelper.COLLECTION_SEPARATOR);
        return coll;
      } else if (iValue.charAt(0) == OStringSerializerHelper.MAP_BEGIN
          && iValue.charAt(iValue.length() - 1) == OStringSerializerHelper.MAP_END) {
        // MAP
        return OStringSerializerHelper.getMap(iValue);
      }

    if (iValue.charAt(0) == ORID.PREFIX)
      // RID
      return new ORecordId(iValue);

    boolean integer = true;
    char c;

    boolean stringStarBySign = false;

    for (int index = 0; index < iValue.length(); ++index) {
      c = iValue.charAt(index);
      if (c < '0' || c > '9') {
        if ((index == 0 && (c == '+' || c == '-'))) {
          stringStarBySign = true;
          continue;
        } else if (c == DECIMAL_SEPARATOR)
          integer = false;
        else {
          if (index > 0) {
            if (!integer && c == 'E') {
              // CHECK FOR SCIENTIFIC NOTATION
              if (index < iValue.length())
                index++;
              if (iValue.charAt(index) == '-')
                continue;
            }

            final String v = iValue.substring(0, index);

            if (c == 'f')
              return new Float(v);
            else if (c == 'c')
              return new BigDecimal(v);
            else if (c == 'l')
              return new Long(v);
            else if (c == 'd')
              return new Double(v);
            else if (c == 'b')
              return new Byte(v);
            else if (c == 'a' || c == 't')
              return new Date(Long.parseLong(v));
            else if (c == 's')
              return new Short(v);
          }
          return iValue;
        }
      } else if (stringStarBySign) {
        stringStarBySign = false;
      }
    }
    if (stringStarBySign)
      return iValue;

    if (integer) {
      try {
        return new Integer(iValue);
      } catch (NumberFormatException ignore) {
        return new Long(iValue);
      }
    } else if ("NaN".equals(iValue) || "Infinity".equals(iValue))
      // NaN and Infinity CANNOT BE MANAGED BY BIG-DECIMAL TYPE
      return new Double(iValue);
    else
      return new BigDecimal(iValue);
  }

  public static Object simpleValueFromStream(final Object iValue, final OType iType) {
    switch (iType) {
    case STRING:
      if (iValue instanceof String) {
        final String s = OIOUtils.getStringContent(iValue);
        return OStringSerializerHelper.decode(s);
      }
      return iValue.toString();

    case INTEGER:
      if (iValue instanceof Integer)
        return iValue;
      return new Integer(iValue.toString());

    case BOOLEAN:
      if (iValue instanceof Boolean)
        return iValue;
      return new Boolean(iValue.toString());

    case FLOAT:
      if (iValue instanceof Float)
        return iValue;
      return convertValue((String) iValue, iType);

    case DECIMAL:
      if (iValue instanceof BigDecimal)
        return iValue;
      return convertValue((String) iValue, iType);

    case LONG:
      if (iValue instanceof Long)
        return iValue;
      return convertValue((String) iValue, iType);

    case DOUBLE:
      if (iValue instanceof Double)
        return iValue;
      return convertValue((String) iValue, iType);

    case SHORT:
      if (iValue instanceof Short)
        return iValue;
      return convertValue((String) iValue, iType);

    case BYTE:
      if (iValue instanceof Byte)
        return iValue;
      return convertValue((String) iValue, iType);

    case BINARY:
      return OStringSerializerHelper.getBinaryContent(iValue);

    case DATE:
    case DATETIME:
      if (iValue instanceof Date)
        return iValue;
      return convertValue((String) iValue, iType);

    case LINK:
      if (iValue instanceof ORID)
        return iValue.toString();
      else if (iValue instanceof String)
        return new ORecordId((String) iValue);
      else
        return ((ORecord) iValue).getIdentity().toString();
    }

    throw new IllegalArgumentException("Type " + iType + " is not simple type.");
  }

  public static void simpleValueToStream(final StringBuilder iBuffer, final OType iType, final Object iValue) {
    if (iValue == null || iType == null)
      return;
    switch (iType) {
    case STRING:
      iBuffer.append('"');
      iBuffer.append(OStringSerializerHelper.encode(iValue.toString()));
      iBuffer.append('"');
      break;

    case BOOLEAN:
      iBuffer.append(String.valueOf(iValue));
      break;

    case INTEGER:
      iBuffer.append(String.valueOf(iValue));
      break;

    case FLOAT:
      iBuffer.append(String.valueOf(iValue));
      iBuffer.append('f');
      break;

    case DECIMAL:
      if (iValue instanceof BigDecimal)
        iBuffer.append(((BigDecimal) iValue).toPlainString());
      else
        iBuffer.append(String.valueOf(iValue));
      iBuffer.append('c');
      break;

    case LONG:
      iBuffer.append(String.valueOf(iValue));
      iBuffer.append('l');
      break;

    case DOUBLE:
      iBuffer.append(String.valueOf(iValue));
      iBuffer.append('d');
      break;

    case SHORT:
      iBuffer.append(String.valueOf(iValue));
      iBuffer.append('s');
      break;

    case BYTE:
      if (iValue instanceof Character)
        iBuffer.append((int) ((Character) iValue).charValue());
      else if (iValue instanceof String)
        iBuffer.append(String.valueOf((int) ((String) iValue).charAt(0)));
      else
        iBuffer.append(String.valueOf(iValue));
      iBuffer.append('b');
      break;

    case BINARY:
      iBuffer.append(OStringSerializerHelper.BINARY_BEGINEND);
      if (iValue instanceof Byte)
        iBuffer.append(Base64.getEncoder().encodeToString(new byte[] { ((Byte) iValue).byteValue() }));
      else
        iBuffer.append(Base64.getEncoder().encodeToString((byte[]) iValue));
      iBuffer.append(OStringSerializerHelper.BINARY_BEGINEND);
      break;

    case DATE:
      if (iValue instanceof Date) {
        // RESET HOURS, MINUTES, SECONDS AND MILLISECONDS
        final Calendar calendar = ODateHelper.getDatabaseCalendar();
        calendar.setTime((Date) iValue);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);

        iBuffer.append(calendar.getTimeInMillis());
      } else
        iBuffer.append(iValue);
      iBuffer.append('a');
      break;

    case DATETIME:
      if (iValue instanceof Date)
        iBuffer.append(((Date) iValue).getTime());
      else
        iBuffer.append(iValue);
      iBuffer.append('t');
      break;
    }
  }

  public abstract ORecord fromString(String iContent, ORecord iRecord, String[] iFields);

  public StringBuilder toString(final ORecord iRecord, final StringBuilder iOutput, final String iFormat) {
    return toString(iRecord, iOutput, iFormat, false, true);
  }

  public ORecord fromString(final String iSource) {
    return fromString(iSource, (ORecord) ODatabaseRecordThreadLocal.instance().get().newInstance(), null);
  }

  @Override
  public String[] getFieldNamesRoot(ODocument reference, byte[] iSource) {
    return null;
  }

  @Override
  public ORecord fromStream(final byte[] iSource, final ORecord iRecord, final String[] iFields) {
    final long timer = PROFILER.startChrono();

    try {
      return fromString(new String(iSource,"UTF-8"), iRecord, iFields);
    } catch (UnsupportedEncodingException e) {
      throw OException.wrapException(new OSchemaException("Error reading record"),e);
    } finally {

      PROFILER
          .stopChrono(PROFILER.getProcessMetric("serializer.record.string.fromStream"), "Deserialize record from stream", timer);
    }
  }

  public byte[] toStream(final ORecord iRecord, boolean iOnlyDelta) {
    final long timer = PROFILER.startChrono();

    try {
      return toString(iRecord, new StringBuilder(2048), null, iOnlyDelta, true).toString().getBytes("UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw OException.wrapException(new OSchemaException("error encoding string"), e);
    } finally {

      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.toStream"), "Serialize record to stream", timer);
    }
  }

  protected abstract StringBuilder toString(final ORecord iRecord, final StringBuilder iOutput, final String iFormat, boolean iOnlyDelta, boolean autoDetectCollectionType);

  public boolean getSupportBinaryEvaluate() {
    return false;
  }

}


package com.orientechnologies.orient.core.storage.cache.chm;

import java.util.concurrent.ThreadLocalRandom;

/**
 * A probabilistic multiset for estimating the popularity of an element within a time window. The
 * maximum frequency of an element is limited to 15 (4-bits) and an aging process periodically
 * halves the popularity of all elements.
 *
 * @author ben.manes@gmail.com (Ben Manes)
 */
public final class FrequencySketch implements Admittor {

  /*
   * This class maintains a 4-bit CountMinSketch [1] with periodic aging to provide the popularity
   * history for the TinyLfu admission policy [2]. The time and space efficiency of the sketch
   * allows it to cheaply estimate the frequency of an entry in a stream of cache access events.
   *
   * The counter matrix is represented as a single dimensional array holding 16 counters per slot. A
   * fixed depth of four balances the accuracy and cost, resulting in a width of four times the
   * length of the array. To retain an accurate estimation the array's length equals the maximum
   * number of entries in the cache, increased to the closest power-of-two to exploit more efficient
   * bit masking. This configuration results in a confidence of 93.75% and error bound of e / width.
   *
   * The frequency of all entries is aged periodically using a sampling window based on the maximum
   * number of entries in the cache. This is referred to as the reset operation by TinyLfu and keeps
   * the sketch fresh by dividing all counters by two and subtracting based on the number of odd
   * counters found. The O(n) cost of aging is amortized, ideal for hardware prefetching, and uses
   * inexpensive bit manipulations per array location.
   *
   * A per instance smear is used to help protect against hash flooding [3], which would result
   * in the admission policy always rejecting new candidates. The use of a pseudo random hashing
   * function resolves the concern of a denial of service attack by exploiting the hash codes.
   *
   * [1] An Improved Data Stream Summary: The Count-Min Sketch and its Applications
   * http://dimacs.rutgers.edu/~graham/pubs/papers/cm-full.pdf
   * [2] TinyLFU: A Highly Efficient Cache Admission Policy
   * http://arxiv.org/pdf/1512.00727.pdf
   * [3] Denial of Service via Algorithmic Complexity Attack
   * https://www.usenix.org/legacy/events/sec03/tech/full_papers/crosby/crosby.pdf
   */

  private static final long[] SEED       = new long[] { // A mixture of seeds from FNV-1a, CityHash, and Murmur3
      0xc3a5c85c97cb3127L, 0xb492b66fbe98f273L, 0x9ae16a3b2f90404fL, 0xcbf29ce484222325L };
  private static final long   RESET_MASK = 0x7777777777777777L;
  private static final long   ONE_MASK   = 0x1111111111111111L;

  private final int randomSeed;

  private int    sampleSize;
  private int    tableMask;
  private long[] table;
  private int    size;

  /**
   * Creates a lazily initialized frequency sketch, requiring {@link #ensureCapacity} be called
   * when the maximum size of the cache has been determined.
   */
  @SuppressWarnings("NullAway.Init")
  FrequencySketch() {
    final int seed = ThreadLocalRandom.current().nextInt();
    this.randomSeed = ((seed & 1) == 0) ? seed + 1 : seed;
  }

  /**
   * Initializes and increases the capacity of this <tt>FrequencySketch</tt> instance, if necessary,
   * to ensure that it can accurately estimate the popularity of elements given the maximum size of
   * the cache. This operation forgets all previous counts when resizing.
   *
   * @param maximumSize the maximum size of the cache
   */
  public void ensureCapacity(final long maximumSize) {
    final int maximum = (int) Math.min(maximumSize, Integer.MAX_VALUE >>> 1);
    if ((table != null) && (table.length >= maximum)) {
      return;
    }

    table = new long[(maximum == 0) ? 1 : ceilingNextPowerOfTwo(maximum)];
    tableMask = Math.max(0, table.length - 1);
    sampleSize = (maximumSize == 0) ? 10 : (10 * maximum);
    if (sampleSize <= 0) {
      sampleSize = Integer.MAX_VALUE;
    }
    size = 0;
  }

  /**
   * Returns the estimated number of occurrences of an element, up to the maximum (15).
   *
   * @param hash the hash code of element to count occurrences of
   *
   * @return the estimated number of occurrences of the element; possibly zero but never negative
   */
  @Override
  public int frequency(int hash) {
    hash = spread(hash);
    final int start = (hash & 3) << 2;
    int frequency = Integer.MAX_VALUE;
    for (int i = 0; i < 4; i++) {
      final int index = indexOf(hash, i);
      final int count = (int) ((table[index] >>> ((start + i) << 2)) & 0xfL);
      frequency = Math.min(frequency, count);
    }
    return frequency;
  }

  /**
   * Increments the popularity of the element if it does not exceed the maximum (15). The popularity
   * of all elements will be periodically down sampled when the observed events exceeds a threshold.
   * This process provides a frequency aging to allow expired long term entries to fade away.
   *
   * @param hash the hash code of element
   */
  @Override
  public void increment(int hash) {
    hash = spread(hash);
    final int start = (hash & 3) << 2;

    // Loop unrolling improves throughput by 5m ops/s
    final int index0 = indexOf(hash, 0);
    final int index1 = indexOf(hash, 1);
    final int index2 = indexOf(hash, 2);
    final int index3 = indexOf(hash, 3);

    boolean added = incrementAt(index0, start);
    added |= incrementAt(index1, start + 1);
    added |= incrementAt(index2, start + 2);
    added |= incrementAt(index3, start + 3);

    if (added && (++size == sampleSize)) {
      reset();
    }
  }

  /**
   * Increments the specified counter by 1 if it is not already at the maximum value (15).
   *
   * @param i the table index (16 counters)
   * @param j the counter to increment
   *
   * @return if incremented
   */
  private boolean incrementAt(final int i, final int j) {
    final int offset = j << 2;
    final long mask = (0xfL << offset);
    if ((table[i] & mask) != mask) {
      table[i] += (1L << offset);
      return true;
    }
    return false;
  }

  /**
   * Reduces every counter by half of its original value.
   */
  private void reset() {
    int count = 0;
    for (int i = 0; i < table.length; i++) {
      count += Long.bitCount(table[i] & ONE_MASK);
      table[i] = (table[i] >>> 1) & RESET_MASK;
    }
    size = (size >>> 1) - (count >>> 2);
  }

  /**
   * Returns the table index for the counter at the specified depth.
   *
   * @param item the element's hash
   * @param i    the counter depth
   *
   * @return the table index
   */
  private int indexOf(final int item, final int i) {
    long hash = SEED[i] * item;
    hash += hash >> 32;
    return ((int) hash) & tableMask;
  }

  /**
   * Applies a supplemental hash function to a given hashCode, which defends against poor quality
   * hash functions.
   */
  private int spread(int x) {
    x = ((x >>> 16) ^ x) * 0x45d9f3b;
    x = ((x >>> 16) ^ x) * randomSeed;
    return (x >>> 16) ^ x;
  }

  private static int ceilingNextPowerOfTwo(final int x) {
    // From Hacker's Delight, Chapter 3, Harry S. Warren Jr.
    return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1));
  }
}


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

import com.orientechnologies.common.comparator.OCaseInsentiveComparator;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.util.OCollections;
import com.orientechnologies.orient.core.collate.OCollate;
import com.orientechnologies.orient.core.collate.ODefaultCollate;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.OScenarioThreadLocal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.exception.OSchemaException;
import com.orientechnologies.orient.core.index.*;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OSQLEngine;
import com.orientechnologies.orient.core.storage.OAutoshardedStorage;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;
import com.orientechnologies.orient.core.type.ODocumentWrapperNoClass;

import java.text.ParseException;
import java.util.*;

/**
 * Contains the description of a persistent class property.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public abstract class OPropertyImpl implements OProperty {
  protected final   OClassImpl owner;
  protected         OType      linkedType;
  protected         OClass     linkedClass;
  transient private String     linkedClassName;

  protected String              description;
  protected boolean             mandatory;
  protected boolean             notNull = false;
  protected String              min;
  protected String              max;
  protected String              defaultValue;
  protected String              regexp;
  protected boolean             readonly;
  protected Map<String, String> customFields;
  protected OCollate            collate = new ODefaultCollate();
  protected OGlobalProperty     globalRef;
  protected ODocument           document;

  private volatile int hashCode;

  OPropertyImpl(final OClassImpl owner) {
    document = new ODocument().setTrackingChanges(false);
    this.owner = owner;
  }

  OPropertyImpl(final OClassImpl owner, final ODocument document) {
    this(owner);
    this.document = document;
  }

  public OPropertyImpl(OClassImpl oClassImpl, OGlobalProperty global) {
    this(oClassImpl);
    this.globalRef = global;
  }

  public String getName() {
    acquireSchemaReadLock();
    try {
      return globalRef.getName();
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getFullName() {
    acquireSchemaReadLock();
    try {
      return owner.getName() + "." + globalRef.getName();
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getFullNameQuoted() {
    acquireSchemaReadLock();
    try {
      return "`" + owner.getName() + "`.`" + globalRef.getName() + "`";
    } finally {
      releaseSchemaReadLock();
    }
  }

  public OType getType() {
    acquireSchemaReadLock();
    try {
      return globalRef.getType();
    } finally {
      releaseSchemaReadLock();
    }
  }

  public int compareTo(final OProperty o) {
    acquireSchemaReadLock();
    try {
      return globalRef.getName().compareTo(o.getName());
    } finally {
      releaseSchemaReadLock();
    }
  }

  /**
   * Creates an index on this property. Indexes speed up queries but slow down insert and update operations. For massive inserts we
   * suggest to remove the index, make the massive insert and recreate it.
   *
   * @param iType One of types supported.

UNIQUE: Doesn't allow duplicates

NOTUNIQUE: Allow duplicates

   *
FULLTEXT: Indexes single word for full text search

   *
   * @return
   *
   * @see {@link OClass#createIndex(String, OClass.INDEX_TYPE, String...)} instead.
   */
  public OIndex<?> createIndex(final OClass.INDEX_TYPE iType) {
    return createIndex(iType.toString());
  }

  /**
   * Creates an index on this property. Indexes speed up queries but slow down insert and update operations. For massive inserts we
   * suggest to remove the index, make the massive insert and recreate it.
   *
   * @param iType
   *
   * @return
   *
   * @see {@link OClass#createIndex(String, OClass.INDEX_TYPE, String...)} instead.
   */
  public OIndex<?> createIndex(final String iType) {
    acquireSchemaReadLock();
    try {
      return owner.createIndex(getFullName(), iType, globalRef.getName());
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public OIndex<?> createIndex(OClass.INDEX_TYPE iType, ODocument metadata) {
    return createIndex(iType.name(), metadata);
  }

  @Override
  public OIndex<?> createIndex(String iType, ODocument metadata) {
    acquireSchemaReadLock();
    try {
      return owner.createIndex(getFullName(), iType, null, metadata, new String[] { globalRef.getName() });
    } finally {
      releaseSchemaReadLock();
    }
  }

  /**
   * Remove the index on property
   *
   * @deprecated Use {@link OIndexManager#dropIndex(String)} instead.
   */
  @Deprecated
  public OPropertyImpl dropIndexes() {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_DELETE);

    acquireSchemaReadLock();
    try {
      final OIndexManager indexManager = getDatabase().getMetadata().getIndexManager();

      final ArrayList<OIndex<?>> relatedIndexes = new ArrayList<OIndex<?>>();
      for (final OIndex<?> index : indexManager.getClassIndexes(owner.getName())) {
        final OIndexDefinition definition = index.getDefinition();

        if (OCollections.indexOf(definition.getFields(), globalRef.getName(), new OCaseInsentiveComparator()) > -1) {
          if (definition instanceof OPropertyIndexDefinition) {
            relatedIndexes.add(index);
          } else {
            throw new IllegalArgumentException(
                "This operation applicable only for property indexes. " + index.getName() + " is " + index.getDefinition());
          }
        }
      }

      for (final OIndex<?> index : relatedIndexes)
        getDatabase().getMetadata().getIndexManager().dropIndex(index.getName());

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

  /**
   * Remove the index on property
   *
   * @deprecated by {@link #dropIndexes()}
   */
  @Deprecated
  public void dropIndexesInternal() {
    dropIndexes();
  }

  /**
   * Returns the first index defined for the property.
   *
   * @deprecated Use {@link OClass#getInvolvedIndexes(String...)} instead.
   */
  @Deprecated
  public OIndex<?> getIndex() {
    acquireSchemaReadLock();
    try {
      Set<OIndex<?>> indexes = owner.getInvolvedIndexes(globalRef.getName());
      if (indexes != null && !indexes.isEmpty())
        return indexes.iterator().next();
      return null;
    } finally {
      releaseSchemaReadLock();
    }
  }

  /**
   * @deprecated Use {@link OClass#getInvolvedIndexes(String...)} instead.
   */
  @Deprecated
  public Set<OIndex<?>> getIndexes() {
    acquireSchemaReadLock();
    try {
      return owner.getInvolvedIndexes(globalRef.getName());
    } finally {
      releaseSchemaReadLock();
    }
  }

  /**
   * @deprecated Use {@link OClass#areIndexed(String...)} instead.
   */
  @Deprecated
  public boolean isIndexed() {
    acquireSchemaReadLock();
    try {
      return owner.areIndexed(globalRef.getName());
    } finally {
      releaseSchemaReadLock();
    }
  }

  public OClass getOwnerClass() {
    return owner;
  }

  /**
   * Returns the linked class in lazy mode because while unmarshalling the class could be not loaded yet.
   *
   * @return
   */
  public OClass getLinkedClass() {
    acquireSchemaReadLock();
    try {
      if (linkedClass == null && linkedClassName != null)
        linkedClass = owner.owner.getClass(linkedClassName);
      return linkedClass;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public static void checkSupportLinkedClass(OType type) {
    if (type != OType.LINK && type != OType.LINKSET && type != OType.LINKLIST && type != OType.LINKMAP && type != OType.EMBEDDED
        && type != OType.EMBEDDEDSET && type != OType.EMBEDDEDLIST && type != OType.EMBEDDEDMAP && type != OType.LINKBAG)
      throw new OSchemaException("Linked class is not supported for type: " + type);
  }

  public OType getLinkedType() {
    acquireSchemaReadLock();
    try {
      return linkedType;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public static void checkLinkTypeSupport(OType type) {
    if (type != OType.EMBEDDEDSET && type != OType.EMBEDDEDLIST && type != OType.EMBEDDEDMAP)
      throw new OSchemaException("Linked type is not supported for type: " + type);
  }

  public boolean isNotNull() {
    acquireSchemaReadLock();
    try {
      return notNull;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public boolean isMandatory() {
    acquireSchemaReadLock();
    try {
      return mandatory;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public boolean isReadonly() {
    acquireSchemaReadLock();
    try {
      return readonly;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getMin() {
    acquireSchemaReadLock();
    try {
      return min;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getMax() {
    acquireSchemaReadLock();
    try {
      return max;
    } finally {
      releaseSchemaReadLock();
    }
  }

  protected static Object quoteString(String s) {
    if (s == null) {
      return "null";
    }
    String result = "\"" + (s.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"")) + "\"";
    return result;
  }

  public String getDefaultValue() {
    acquireSchemaReadLock();
    try {
      return defaultValue;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getRegexp() {
    acquireSchemaReadLock();
    try {
      return regexp;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getCustom(final String iName) {
    acquireSchemaReadLock();
    try {
      if (customFields == null)
        return null;

      return customFields.get(iName);
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Map<String, String> getCustomInternal() {
    acquireSchemaReadLock();
    try {
      if (customFields != null)
        return Collections.unmodifiableMap(customFields);
      return null;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public void removeCustom(final String iName) {
    setCustom(iName, null);
  }

  public Set<String> getCustomKeys() {
    acquireSchemaReadLock();
    try {
      if (customFields != null)
        return customFields.keySet();

      return new HashSet<String>();
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Object get(final ATTRIBUTES attribute) {
    if (attribute == null)
      throw new IllegalArgumentException("attribute is null");

    switch (attribute) {
    case LINKEDCLASS:
      return getLinkedClass();
    case LINKEDTYPE:
      return getLinkedType();
    case MIN:
      return getMin();
    case MANDATORY:
      return isMandatory();
    case READONLY:
      return isReadonly();
    case MAX:
      return getMax();
    case DEFAULT:
      return getDefaultValue();
    case NAME:
      return getName();
    case NOTNULL:
      return isNotNull();
    case REGEXP:
      return getRegexp();
    case TYPE:
      return getType();
    case COLLATE:
      return getCollate();
    case DESCRIPTION:
      return getDescription();
    }

    throw new IllegalArgumentException("Cannot find attribute '" + attribute + "'");
  }

  public void set(final ATTRIBUTES attribute, final Object iValue) {
    if (attribute == null)
      throw new IllegalArgumentException("attribute is null");

    final String stringValue = iValue != null ? iValue.toString() : null;

    switch (attribute) {
    case LINKEDCLASS:
      setLinkedClass(getDatabase().getMetadata().getSchema().getClass(stringValue));
      break;
    case LINKEDTYPE:
      if (stringValue == null)
        setLinkedType(null);
      else
        setLinkedType(OType.valueOf(stringValue));
      break;
    case MIN:
      setMin(stringValue);
      break;
    case MANDATORY:
      setMandatory(Boolean.parseBoolean(stringValue));
      break;
    case READONLY:
      setReadonly(Boolean.parseBoolean(stringValue));
      break;
    case MAX:
      setMax(stringValue);
      break;
    case DEFAULT:
      setDefaultValue(stringValue);
      break;
    case NAME:
      setName(stringValue);
      break;
    case NOTNULL:
      setNotNull(Boolean.parseBoolean(stringValue));
      break;
    case REGEXP:
      setRegexp(stringValue);
      break;
    case TYPE:
      setType(OType.valueOf(stringValue.toUpperCase(Locale.ENGLISH)));
      break;
    case COLLATE:
      setCollate(stringValue);
      break;
    case CUSTOM:
      int indx = stringValue != null ? stringValue.indexOf('=') : -1;
      if (indx < 0) {
        if ("clear".equalsIgnoreCase(stringValue)) {
          clearCustom();
        } else
          throw new IllegalArgumentException("Syntax error: expected <name> = <value> or clear, instead found: " + iValue);
      } else {
        String customName = stringValue.substring(0, indx).trim();
        String customValue = stringValue.substring(indx + 1).trim();
        if (isQuoted(customValue)) {
          customValue = removeQuotes(customValue);
        }
        if (customValue.isEmpty())
          removeCustom(customName);
        else
          setCustom(customName, customValue);
      }
      break;
    case DESCRIPTION:
      setDescription(stringValue);
      break;
    }
  }

  private String removeQuotes(String s) {
    s = s.trim();
    return s.substring(1, s.length() - 1);
  }

  private boolean isQuoted(String s) {
    s = s.trim();
    if (s.startsWith("\"") && s.endsWith("\""))
      return true;
    if (s.startsWith("'") && s.endsWith("'"))
      return true;
    if (s.startsWith("`") && s.endsWith("`"))
      return true;

    return false;
  }

  public OCollate getCollate() {
    acquireSchemaReadLock();
    try {
      return collate;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public OProperty setCollate(final OCollate collate) {
    setCollate(collate.getName());
    return this;
  }

  @Override
  public String getDescription() {
    acquireSchemaReadLock();
    try {
      return description;
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public String toString() {
    acquireSchemaReadLock();
    try {
      return globalRef.getName() + " (type=" + globalRef.getType() + ")";
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public int hashCode() {
    int sh = hashCode;
    if (sh != 0)
      return sh;

    acquireSchemaReadLock();
    try {
      sh = hashCode;
      if (sh != 0)
        return sh;

      calculateHashCode();
      return hashCode;
    } finally {
      releaseSchemaReadLock();
    }
  }

  private void calculateHashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + ((owner == null) ? 0 : owner.hashCode());
    hashCode = result;
  }

  @Override
  public boolean equals(final Object obj) {
    acquireSchemaReadLock();
    try {
      if (this == obj)
        return true;
      if (obj == null || !OProperty.class.isAssignableFrom(obj.getClass()))
        return false;
      OProperty other = (OProperty) obj;
      if (owner == null) {
        if (other.getOwnerClass() != null)
          return false;
      } else if (!owner.equals(other.getOwnerClass()))
        return false;
      return this.getName().equals(other.getName());
    } finally {
      releaseSchemaReadLock();
    }
  }

  @SuppressWarnings("unchecked")
  public void fromStream() {

    String name = document.field("name");
    OType type = null;
    if (document.field("type") != null)
      type = OType.getById(((Integer) document.field("type")).byteValue());
    Integer globalId = document.field("globalId");
    if (globalId != null)
      globalRef = owner.owner.getGlobalPropertyById(globalId);
    else {
      if (type == null)
        type = OType.ANY;
      globalRef = owner.owner.findOrCreateGlobalProperty(name, type);
    }

    mandatory = document.containsField("mandatory") ? (Boolean) document.field("mandatory") : false;
    readonly = document.containsField("readonly") ? (Boolean) document.field("readonly") : false;
    notNull = document.containsField("notNull") ? (Boolean) document.field("notNull") : false;
    defaultValue = (String) (document.containsField("defaultValue") ? document.field("defaultValue") : null);
    if (document.containsField("collate"))
      collate = OSQLEngine.getCollate((String) document.field("collate"));

    min = (String) (document.containsField("min") ? document.field("min") : null);
    max = (String) (document.containsField("max") ? document.field("max") : null);
    regexp = (String) (document.containsField("regexp") ? document.field("regexp") : null);
    linkedClassName = (String) (document.containsField("linkedClass") ? document.field("linkedClass") : null);
    linkedType = document.field("linkedType") != null ? OType.getById(((Integer) document.field("linkedType")).byteValue()) : null;
    customFields = (Map<String, String>) (document.containsField("customFields") ?
        document.field("customFields", OType.EMBEDDEDMAP) :
        null);
    description = (String) (document.containsField("description") ? document.field("description") : null);
  }

  public Collection<OIndex<?>> getAllIndexes() {
    acquireSchemaReadLock();
    try {
      final Set<OIndex<?>> indexes = owner.getIndexes();
      final List<OIndex<?>> indexList = new LinkedList<OIndex<?>>();
      for (final OIndex<?> index : indexes) {
        final OIndexDefinition indexDefinition = index.getDefinition();
        if (indexDefinition.getFields().contains(globalRef.getName()))
          indexList.add(index);
      }

      return indexList;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public ODocument toStream() {
    document.setInternalStatus(ORecordElement.STATUS.UNMARSHALLING);

    try {
      document.field("name", getName());
      document.field("type", getType().id);
      document.field("globalId", globalRef.getId());
      document.field("mandatory", mandatory);
      document.field("readonly", readonly);
      document.field("notNull", notNull);
      document.field("defaultValue", defaultValue);

      document.field("min", min);
      document.field("max", max);
      if (regexp != null) {
        document.field("regexp", regexp);
      } else {
        document.removeField("regexp");
      }
      if (linkedType != null)
        document.field("linkedType", linkedType.id);
      if (linkedClass != null || linkedClassName != null)
        document.field("linkedClass", linkedClass != null ? linkedClass.getName() : linkedClassName);

      document.field("customFields", customFields != null && customFields.size() > 0 ? customFields : null, OType.EMBEDDEDMAP);
      if (collate != null) {
        document.field("collate", collate.getName());
      }
      document.field("description", description);

    } finally {
      document.setInternalStatus(ORecordElement.STATUS.LOADED);
    }
    return document;
  }

  public void acquireSchemaReadLock() {
    owner.acquireSchemaReadLock();
  }

  public void releaseSchemaReadLock() {
    owner.releaseSchemaReadLock();
  }

  public void acquireSchemaWriteLock() {
    owner.acquireSchemaWriteLock();
  }

  public void releaseSchemaWriteLock() {
    calculateHashCode();
    owner.releaseSchemaWriteLock();
  }

  public void checkEmbedded() {
    if (!(getDatabase().getStorage().getUnderlying() instanceof OAbstractPaginatedStorage))
      throw new OSchemaException("'Internal' schema modification methods can be used only inside of embedded database");
  }

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

  protected void checkForDateFormat(final String iDateAsString) {
    if (iDateAsString != null)
      if (globalRef.getType() == OType.DATE) {
        try {
          getDatabase().getStorage().getConfiguration().getDateFormatInstance().parse(iDateAsString);
        } catch (ParseException e) {
          throw OException
              .wrapException(new OSchemaException("Invalid date format while formatting date '" + iDateAsString + "'"), e);
        }
      } else if (globalRef.getType() == OType.DATETIME) {
        try {
          getDatabase().getStorage().getConfiguration().getDateTimeFormatInstance().parse(iDateAsString);
        } catch (ParseException e) {
          throw OException
              .wrapException(new OSchemaException("Invalid datetime format while formatting date '" + iDateAsString + "'"), e);
        }
      }
  }

  protected boolean isDistributedCommand() {
    return getDatabase().getStorage() instanceof OAutoshardedStorage && !((OAutoshardedStorage) getDatabase().getStorage())
        .isLocalEnv();
  }

  @Override
  public Integer getId() {
    return globalRef.getId();
  }

  public void fromStream(ODocument document) {
    this.document = document;
    fromStream();
  }

  public ODocument toNetworkStream() {
    ODocument document = new ODocument();
    document.setInternalStatus(ORecordElement.STATUS.UNMARSHALLING);

    try {
      document.field("name", getName());
      document.field("type", getType().id);
      document.field("globalId", globalRef.getId());
      document.field("mandatory", mandatory);
      document.field("readonly", readonly);
      document.field("notNull", notNull);
      document.field("defaultValue", defaultValue);

      document.field("min", min);
      document.field("max", max);
      if (regexp != null) {
        document.field("regexp", regexp);
      } else {
        document.removeField("regexp");
      }
      if (linkedType != null)
        document.field("linkedType", linkedType.id);
      if (linkedClass != null || linkedClassName != null)
        document.field("linkedClass", linkedClass != null ? linkedClass.getName() : linkedClassName);

      document.field("customFields", customFields != null && customFields.size() > 0 ? customFields : null, OType.EMBEDDEDMAP);
      if (collate != null) {
        document.field("collate", collate.getName());
      }
      document.field("description", description);

    } finally {
      document.setInternalStatus(ORecordElement.STATUS.LOADED);
    }
    return document;
  }

}