/*
 * Copyright 2010-2012 Luca Garulli (l.garulli--at--orientechnologies.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.core.record.impl;

import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.util.OPair;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.config.OStorageConfiguration;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.record.*;
import com.orientechnologies.orient.core.db.record.ORecordElement.STATUS;
import com.orientechnologies.orient.core.db.record.ridbag.ORidBag;
import com.orientechnologies.orient.core.exception.OQueryParsingException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordInternal;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.serialization.serializer.record.string.ORecordSerializerStringAbstract;
import com.orientechnologies.orient.core.sql.OSQLHelper;
import com.orientechnologies.orient.core.sql.functions.OSQLFunctionRuntime;
import com.orientechnologies.orient.core.type.tree.OMVRBTreeRIDSet;
import com.orientechnologies.orient.core.version.ODistributedVersion;

import java.lang.reflect.Array;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;

/**
 * Helper class to manage documents.
 * 
 * @author Luca Garulli
 */
public class ODocumentHelper {
  public static final String ATTRIBUTE_THIS               = "@this";
  public static final String ATTRIBUTE_RID                = "@rid";
  public static final String ATTRIBUTE_RID_ID             = "@rid_id";
  public static final String ATTRIBUTE_RID_POS            = "@rid_pos";
  public static final String ATTRIBUTE_VERSION            = "@version";
  public static final String ATTRIBUTE_VERSION_TIMESTAMP  = "@version_time";
  public static final String ATTRIBUTE_VERSION_MACADDRESS = "@version_mac";
  public static final String ATTRIBUTE_CLASS              = "@class";
  public static final String ATTRIBUTE_TYPE               = "@type";
  public static final String ATTRIBUTE_SIZE               = "@size";
  public static final String ATTRIBUTE_FIELDS             = "@fields";
  public static final String ATTRIBUTE_RAW                = "@raw";

  public static void sort(List<? extends OIdentifiable> ioResultSet, List<OPair<String, String>> iOrderCriteria) {
    if (ioResultSet != null)
      Collections.sort(ioResultSet, new ODocumentComparator(iOrderCriteria));
  }

  @SuppressWarnings("unchecked")
  public static <RET> RET convertField(final ODocument iDocument, final String iFieldName, final Class<?> iFieldType, Object iValue) {
    if (iFieldType == null)
      return (RET) iValue;

    if (ORID.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof ORID) {
        return (RET) iValue;
      } else if (iValue instanceof String) {
        return (RET) new ORecordId((String) iValue);
      } else if (iValue instanceof ORecord<?>) {
        return (RET) ((ORecord<?>) iValue).getIdentity();
      }
    } else if (ORecord.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof ORID || iValue instanceof ORecord<?>) {
        return (RET) iValue;
      } else if (iValue instanceof String) {
        return (RET) new ORecordId((String) iValue);
      }
    } else if (Set.class.isAssignableFrom(iFieldType)) {
      if (!(iValue instanceof Set)) {
        // CONVERT IT TO SET
        final Collection<?> newValue;

        if (iValue instanceof ORecordLazyList || iValue instanceof ORecordLazyMap)
          newValue = new OMVRBTreeRIDSet(iDocument);
        else
          newValue = new OTrackedSet<Object>(iDocument);

        if (iValue instanceof Collection<?>) {
          ((Collection<Object>) newValue).addAll((Collection<Object>) iValue);
          return (RET) newValue;
        } else if (iValue instanceof Map) {
          ((Collection<Object>) newValue).addAll(((Map<String, Object>) iValue).values());
          return (RET) newValue;
        } else if (iValue instanceof String) {
          final String stringValue = (String) iValue;

          if (stringValue != null && !stringValue.isEmpty()) {
            final String[] items = stringValue.split(",");
            for (String s : items) {
              ((Collection<Object>) newValue).add(s);
            }
          }
          return (RET) newValue;
        } else if (OMultiValue.isMultiValue(iValue)) {
          // GENERIC MULTI VALUE
          for (Object s : OMultiValue.getMultiValueIterable(iValue)) {
            ((Collection<Object>) newValue).add(s);
          }
          return (RET) newValue;
        }
      } else {
        return (RET) iValue;
      }
    } else if (List.class.isAssignableFrom(iFieldType)) {
      if (!(iValue instanceof List)) {
        // CONVERT IT TO LIST
        final Collection<?> newValue;

        if (iValue instanceof OMVRBTreeRIDSet || iValue instanceof ORecordLazyMap)
          newValue = new ORecordLazyList(iDocument);
        else
          newValue = new OTrackedList<Object>(iDocument);

        if (iValue instanceof Collection) {
          ((Collection<Object>) newValue).addAll((Collection<Object>) iValue);
          return (RET) newValue;
        } else if (iValue instanceof Map) {
          ((Collection<Object>) newValue).addAll(((Map<String, Object>) iValue).values());
          return (RET) newValue;
        } else if (iValue instanceof String) {
          final String stringValue = (String) iValue;

          if (stringValue != null && !stringValue.isEmpty()) {
            final String[] items = stringValue.split(",");
            for (String s : items) {
              ((Collection<Object>) newValue).add(s);
            }
          }
          return (RET) newValue;
        } else if (OMultiValue.isMultiValue(iValue)) {
          // GENERIC MULTI VALUE
          for (Object s : OMultiValue.getMultiValueIterable(iValue)) {
            ((Collection<Object>) newValue).add(s);
          }
          return (RET) newValue;
        }
      } else {
        return (RET) iValue;
      }
    } else if (iValue instanceof Enum) {
      // ENUM
      if (Number.class.isAssignableFrom(iFieldType))
        iValue = ((Enum<?>) iValue).ordinal();
      else
        iValue = iValue.toString();
      if (!(iValue instanceof String) && !iFieldType.isAssignableFrom(iValue.getClass()))
        throw new IllegalArgumentException("Property '" + iFieldName + "' of type '" + iFieldType
            + "' cannot accept value of type: " + iValue.getClass());
    } else if (Date.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof String && ODatabaseRecordThreadLocal.INSTANCE.isDefined()) {
        final OStorageConfiguration config = iDocument.getDatabase().getStorage().getConfiguration();

        DateFormat formatter = config.getDateFormatInstance();

        if (((String) iValue).length() > config.dateFormat.length()) {
          // ASSUMES YOU'RE USING THE DATE-TIME FORMATTE
          formatter = config.getDateTimeFormatInstance();
        }

        try {
          Date newValue = formatter.parse((String) iValue);
          // _fieldValues.put(iFieldName, newValue);
          return (RET) newValue;
        } catch (ParseException pe) {
          final String dateFormat = ((String) iValue).length() > config.dateFormat.length() ? config.dateTimeFormat
              : config.dateFormat;
          throw new OQueryParsingException("Error on conversion of date '" + iValue + "' using the format: " + dateFormat);
        }
      }
    }

    iValue = OType.convert(iValue, iFieldType);

    return (RET) iValue;
  }

  @SuppressWarnings("unchecked")
  public static <RET> RET getFieldValue(Object value, final String iFieldName) {
    return (RET) getFieldValue(value, iFieldName, null);
  }

  @SuppressWarnings("unchecked")
  public static <RET> RET getFieldValue(Object value, final String iFieldName, final OCommandContext iContext) {
    if (value == null)
      return null;

    final int fieldNameLength = iFieldName.length();
    if (fieldNameLength == 0)
      return (RET) value;

    OIdentifiable currentRecord = value instanceof OIdentifiable ? (OIdentifiable) value : null;

    int beginPos = iFieldName.charAt(0) == '.' ? 1 : 0;
    int nextSeparatorPos = iFieldName.charAt(0) == '.' ? 1 : 0;
    do {
      char nextSeparator = ' ';
      for (; nextSeparatorPos < fieldNameLength; ++nextSeparatorPos) {
        nextSeparator = iFieldName.charAt(nextSeparatorPos);
        if (nextSeparator == '.' || nextSeparator == '[')
          break;
      }

      final String fieldName;
      if (nextSeparatorPos < fieldNameLength)
        fieldName = iFieldName.substring(beginPos, nextSeparatorPos);
      else {
        nextSeparator = ' ';
        if (beginPos > 0)
          fieldName = iFieldName.substring(beginPos);
        else
          fieldName = iFieldName;
      }

      if (nextSeparator == '[') {
        if (fieldName != null && fieldName.length() > 0) {
          if (currentRecord != null)
            value = getIdentifiableValue(currentRecord, fieldName);
          else if (value instanceof Map<?, ?>)
            value = getMapEntry((Map<String, ?>) value, fieldName);
          else if (OMultiValue.isMultiValue(value)) {
            final HashSet<Object> temp = new HashSet<Object>();
            for (Object o : OMultiValue.getMultiValueIterable(value)) {
              if (o instanceof OIdentifiable) {
                Object r = getFieldValue(o, iFieldName);
                if (r != null)
                  OMultiValue.add(temp, r);
              }
            }
            value = temp;
          }
        }

        if (value == null)
          return null;
        else if (value instanceof OIdentifiable)
          currentRecord = (OIdentifiable) value;

        final int end = iFieldName.indexOf(']', nextSeparatorPos);
        if (end == -1)
          throw new IllegalArgumentException("Missed closed ']'");

        String indexPart = iFieldName.substring(nextSeparatorPos + 1, end);
        if (indexPart.length() == 0)
          return null;

        nextSeparatorPos = end;

        if (value instanceof OCommandContext)
          value = ((OCommandContext) value).getVariables();

        if (value instanceof OIdentifiable) {
          final ORecord<?> record = currentRecord != null && currentRecord instanceof OIdentifiable ? ((OIdentifiable) currentRecord)
              .getRecord() : null;

          final Object index = getIndexPart(iContext, indexPart);
          final String indexAsString = index != null ? index.toString() : null;

          final List<String> indexParts = OStringSerializerHelper.smartSplit(indexAsString, ',');
          final List<String> indexRanges = OStringSerializerHelper.smartSplit(indexAsString, '-', ' ');
          final List<String> indexCondition = OStringSerializerHelper.smartSplit(indexAsString, '=', ' ');

          if (indexParts.size() == 1 && indexCondition.size() == 1 && indexRanges.size() == 1)
            // SINGLE VALUE
            value = ((ODocument) record).field(indexAsString);
          else if (indexParts.size() > 1) {
            // MULTI VALUE
            final Object[] values = new Object[indexParts.size()];
            for (int i = 0; i < indexParts.size(); ++i) {
              values[i] = ((ODocument) record).field(indexParts.get(i));
            }
            value = values;
          } else if (indexRanges.size() > 1) {

            // MULTI VALUES RANGE
            String from = indexRanges.get(0);
            String to = indexRanges.get(1);

            final ODocument doc = ((ODocument) ((OIdentifiable) value).getRecord());

            final String[] fieldNames = doc.fieldNames();
            final int rangeFrom = from != null && !from.isEmpty() ? Integer.parseInt(from) : 0;
            final int rangeTo = to != null && !to.isEmpty() ? Math.min(Integer.parseInt(to), fieldNames.length - 1)
                : fieldNames.length - 1;

            final Object[] values = new Object[rangeTo - rangeFrom + 1];

            for (int i = rangeFrom; i <= rangeTo; ++i)
              values[i - rangeFrom] = doc.field(fieldNames[i]);

            value = values;

          } else if (!indexCondition.isEmpty()) {
            // CONDITION
            final String conditionFieldName = indexCondition.get(0);
            Object conditionFieldValue = ORecordSerializerStringAbstract.getTypeValue(indexCondition.get(1));

            if (conditionFieldValue instanceof String)
              conditionFieldValue = OStringSerializerHelper.getStringContent(conditionFieldValue);

            final Object fieldValue = getFieldValue(currentRecord, conditionFieldName);

            if (conditionFieldValue != null && fieldValue != null)
              conditionFieldValue = OType.convert(conditionFieldValue, fieldValue.getClass());

            if (fieldValue == null && !conditionFieldValue.equals("null") || fieldValue != null
                & !fieldValue.equals(conditionFieldValue))
              value = null;
          }
        } else if (value instanceof Map<?, ?>) {
          final List<String> indexParts = OStringSerializerHelper.smartSplit(indexPart, ',',
              OStringSerializerHelper.DEFAULT_IGNORE_CHARS);
          if (indexParts.size() == 1) {
            // SINGLE VALUE
            final Object index = getIndexPart(iContext, indexPart);
            value = ((Map<?, ?>) value).get(index);
          } else {
            // MULTI VALUE
            final Object[] values = new Object[indexParts.size()];
            for (int i = 0; i < indexParts.size(); ++i) {
              final Object index = getIndexPart(iContext, indexParts.get(i));
              values[i] = ((Map<?, ?>) value).get(index);
            }
            value = values;
          }
        } else if (OMultiValue.isMultiValue(value)) {
          // MULTI VALUE
          final Object index = getIndexPart(iContext, indexPart);
          final String indexAsString = index != null ? index.toString() : null;

          final List<String> indexParts = OStringSerializerHelper.smartSplit(indexAsString, ',');
          final List<String> indexRanges = OStringSerializerHelper.smartSplit(indexAsString, '-');
          final List<String> indexCondition = OStringSerializerHelper.smartSplit(indexAsString, '=', ' ');

          if (indexParts.size() == 1 && indexRanges.size() == 1 && indexCondition.size() == 1) {
            // SINGLE VALUE
            if (value instanceof Map<?, ?>)
              value = getMapEntry((Map<String, ?>) value, index);
            else if (Character.isDigit(indexAsString.charAt(0)))
              value = OMultiValue.getValue(value, Integer.parseInt(indexAsString));
            else
              // FILTER BY FIELD
              value = getFieldValue(value, indexAsString, iContext);

          } else if (indexParts.size() > 1) {

            // MULTI VALUES
            final Object[] values = new Object[indexParts.size()];
            for (int i = 0; i < indexParts.size(); ++i)
              values[i] = OMultiValue.getValue(value, Integer.parseInt(indexParts.get(i)));
            value = values;

          } else if (indexRanges.size() > 1) {

            // MULTI VALUES RANGE
            String from = indexRanges.get(0);
            String to = indexRanges.get(1);

            final int rangeFrom = from != null && !from.isEmpty() ? Integer.parseInt(from) : 0;
            final int rangeTo = to != null && !to.isEmpty() ? Math.min(Integer.parseInt(to), OMultiValue.getSize(value) - 1)
                : OMultiValue.getSize(value) - 1;

            final Object[] values = new Object[rangeTo - rangeFrom + 1];
            for (int i = rangeFrom; i <= rangeTo; ++i)
              values[i - rangeFrom] = OMultiValue.getValue(value, i);
            value = values;

          } else if (!indexCondition.isEmpty()) {
            // CONDITION
            final String conditionFieldName = indexCondition.get(0);
            Object conditionFieldValue = ORecordSerializerStringAbstract.getTypeValue(indexCondition.get(1));

            if (conditionFieldValue instanceof String)
              conditionFieldValue = OStringSerializerHelper.getStringContent(conditionFieldValue);

            final HashSet<Object> values = new HashSet<Object>();
            for (Object v : OMultiValue.getMultiValueIterable(value)) {
              Object filtered = filterItem(conditionFieldName, conditionFieldValue, v);
              if (filtered != null)
                if (filtered instanceof Collection<?>)
                  values.addAll((Collection<? extends Object>) filtered);
                else
                  values.add(filtered);
            }

            if (values.isEmpty())
              // RETURNS NULL
              value = null;
            else if (values.size() == 1)
              // RETURNS THE SINGLE ODOCUMENT
              value = values.iterator().next();
            else
              // RETURNS THE FILTERED COLLECTION
              value = values;
          }
        }
      } else {
        if (fieldName.length() == 0) {
          // NO FIELD NAME: THIS IS THE CASE OF NOT USEFUL . AFTER A ] OR .
          beginPos = ++nextSeparatorPos;
          continue;
        }

        if (fieldName.startsWith("$"))
          value = iContext.getVariable(fieldName);
        else if (fieldName.contains("("))
          value = evaluateFunction(value, fieldName, iContext);
        else {
          final List<String> indexCondition = OStringSerializerHelper.smartSplit(fieldName, '=', ' ');

          if (indexCondition.size() == 2) {
            final String conditionFieldName = indexCondition.get(0);
            Object conditionFieldValue = ORecordSerializerStringAbstract.getTypeValue(indexCondition.get(1));

            if (conditionFieldValue instanceof String)
              conditionFieldValue = OStringSerializerHelper.getStringContent(conditionFieldValue);

            value = filterItem(conditionFieldName, conditionFieldValue, value);

          } else if (currentRecord != null) {
            // GET THE LINKED OBJECT IF ANY
            value = getIdentifiableValue(currentRecord, fieldName);
            if (value != null && value instanceof ORecord<?> && ((ORecord<?>) value).getInternalStatus() == STATUS.NOT_LOADED)
              // RELOAD IT
              ((ORecord<?>) value).reload();
          } else if (value instanceof Map<?, ?>)
            value = getMapEntry((Map<String, ?>) value, fieldName);
          else if (OMultiValue.isMultiValue(value)) {
            final Set<Object> values = new HashSet<Object>();
            for (Object v : OMultiValue.getMultiValueIterable(value)) {
              final Object item;

              if (v instanceof OIdentifiable)
                item = getIdentifiableValue((OIdentifiable) v, fieldName);
              else if (v instanceof Map)
                item = ((Map<?, ?>) v).get(fieldName);
              else
                item = null;

              if (item != null)
                if (item instanceof Collection<?>)
                  values.addAll((Collection<? extends Object>) item);
                else
                  values.add(item);
            }

            if (values.isEmpty())
              value = null;
            else
              value = values;
          } else
            return null;
        }
      }

      if (value instanceof OIdentifiable)
        currentRecord = (OIdentifiable) value;
      else
        currentRecord = null;

      beginPos = ++nextSeparatorPos;
    } while (nextSeparatorPos < fieldNameLength && value != null);

    return (RET) value;
  }

  protected static Object getIndexPart(final OCommandContext iContext, final String indexPart) {
    Object index = indexPart;
    if (indexPart.charAt(0) == '"' || indexPart.charAt(0) == '\'')
      index = OStringSerializerHelper.getStringContent(indexPart);
    else if (indexPart.charAt(0) == '$') {
      final Object ctxValue = iContext.getVariable(indexPart);
      if (ctxValue == null)
        return null;
      index = ctxValue;
    } else if (!Character.isDigit(indexPart.charAt(0)))
      // GET FROM CURRENT VALUE
      index = indexPart;
    return index;
  }

  @SuppressWarnings("unchecked")
  protected static Object filterItem(final String iConditionFieldName, final Object iConditionFieldValue, final Object iValue) {
    if (iValue instanceof OIdentifiable) {
      final ORecord<?> rec = ((OIdentifiable) iValue).getRecord();
      if (rec instanceof ODocument) {
        final ODocument doc = (ODocument) rec;

        Object fieldValue = doc.field(iConditionFieldName);

        if (iConditionFieldValue == null)
          return fieldValue == null ? doc : null;

        fieldValue = OType.convert(fieldValue, iConditionFieldValue.getClass());
        if (fieldValue != null && fieldValue.equals(iConditionFieldValue))
          return doc;
      }
    } else if (iValue instanceof Map<?, ?>) {
      final Map<String, ?> map = (Map<String, ?>) iValue;
      Object fieldValue = getMapEntry(map, iConditionFieldName);

      fieldValue = OType.convert(fieldValue, iConditionFieldValue.getClass());
      if (fieldValue != null && fieldValue.equals(iConditionFieldValue))
        return map;
    }
    return null;
  }

  /**
   * Retrieves the value crossing the map with the dotted notation
   * 
   * @param iKey
   *          Field(s) to retrieve. If are multiple fields, then the dot must be used as separator
   * @return
   */
  @SuppressWarnings("unchecked")
  public static Object getMapEntry(final Map<String, ?> iMap, final Object iKey) {
    if (iMap == null || iKey == null)
      return null;

    if (iKey instanceof String) {
      String iName = (String) iKey;
      int pos = iName.indexOf('.');
      if (pos > -1)
        iName = iName.substring(0, pos);

      final Object value = iMap.get(iName);
      if (value == null)
        return null;

      if (pos > -1) {
        final String restFieldName = iName.substring(pos + 1);
        if (value instanceof ODocument)
          return getFieldValue(value, restFieldName);
        else if (value instanceof Map<?, ?>)
          return getMapEntry((Map<String, ?>) value, restFieldName);
      }

      return value;
    } else
      return iMap.get(iKey);
  }

  public static Object getIdentifiableValue(final OIdentifiable iCurrent, final String iFieldName) {
    if (iFieldName == null || iFieldName.length() == 0)
      return null;

    final char begin = iFieldName.charAt(0);
    if (begin == '@') {
      // RETURN AN ATTRIBUTE
      if (iFieldName.equalsIgnoreCase(ATTRIBUTE_THIS))
        return iCurrent.getRecord();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_RID))
        return iCurrent.getIdentity();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_RID_ID))
        return iCurrent.getIdentity().getClusterId();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_RID_POS))
        return iCurrent.getIdentity().getClusterPosition();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_VERSION))
        return iCurrent.getRecord().getRecordVersion().getCounter();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_VERSION_TIMESTAMP)
          && OGlobalConfiguration.DB_USE_DISTRIBUTED_VERSION.getValueAsBoolean())
        return ((ODistributedVersion) iCurrent.getRecord().getRecordVersion()).getTimestamp();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_VERSION_MACADDRESS)
          && OGlobalConfiguration.DB_USE_DISTRIBUTED_VERSION.getValueAsBoolean())
        return ((ODistributedVersion) iCurrent.getRecord().getRecordVersion()).getMacAddress();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_CLASS))
        return ((ODocument) iCurrent.getRecord()).getClassName();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_TYPE))
        return Orient.instance().getRecordFactoryManager()
            .getRecordTypeName(((ORecordInternal<?>) iCurrent.getRecord()).getRecordType());
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_SIZE)) {
        final byte[] stream = ((ORecordInternal<?>) iCurrent.getRecord()).toStream();
        return stream != null ? stream.length : 0;
      } else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_FIELDS))
        return ((ODocument) iCurrent.getRecord()).fieldNames();
      else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_RAW))
        return new String(((ORecordInternal<?>) iCurrent.getRecord()).toStream());
    }

    if (iCurrent == null)
      return null;

    final ODocument doc = ((ODocument) iCurrent.getRecord());
    doc.checkForFields(iFieldName);
    return doc._fieldValues.get(iFieldName);
  }

  public static Object evaluateFunction(final Object currentValue, final String iFunction, final OCommandContext iContext) {
    if (currentValue == null)
      return null;

    Object result = null;

    final String function = iFunction.toUpperCase();

    if (function.startsWith("SIZE("))
      result = currentValue instanceof ORecord<?> ? 1 : OMultiValue.getSize(currentValue);
    else if (function.startsWith("LENGTH("))
      result = currentValue.toString().length();
    else if (function.startsWith("TOUPPERCASE("))
      result = currentValue.toString().toUpperCase();
    else if (function.startsWith("TOLOWERCASE("))
      result = currentValue.toString().toLowerCase();
    else if (function.startsWith("TRIM("))
      result = currentValue.toString().trim();
    else if (function.startsWith("TOJSON("))
      result = currentValue instanceof ODocument ? ((ODocument) currentValue).toJSON() : null;
    else if (function.startsWith("KEYS("))
      result = currentValue instanceof Map<?, ?> ? ((Map<?, ?>) currentValue).keySet() : null;
    else if (function.startsWith("VALUES("))
      result = currentValue instanceof Map<?, ?> ? ((Map<?, ?>) currentValue).values() : null;
    else if (function.startsWith("ASSTRING("))
      result = currentValue.toString();
    else if (function.startsWith("ASINTEGER("))
      result = new Integer(currentValue.toString());
    else if (function.startsWith("ASFLOAT("))
      result = new Float(currentValue.toString());
    else if (function.startsWith("ASBOOLEAN(")) {
      if (currentValue instanceof String)
        result = new Boolean((String) currentValue);
      else if (currentValue instanceof Number) {
        final int bValue = ((Number) currentValue).intValue();
        if (bValue == 0)
          result = Boolean.FALSE;
        else if (bValue == 1)
          result = Boolean.TRUE;
      }
    } else if (function.startsWith("ASDATE("))
      if (currentValue instanceof Date)
        result = currentValue;
      else if (currentValue instanceof Number)
        result = new Date(((Number) currentValue).longValue());
      else
        try {
          result = ODatabaseRecordThreadLocal.INSTANCE.get().getStorage().getConfiguration().getDateFormatInstance()
              .parse(currentValue.toString());
        } catch (ParseException e) {
        }
    else if (function.startsWith("ASDATETIME("))
      if (currentValue instanceof Date)
        result = currentValue;
      else if (currentValue instanceof Number)
        result = new Date(((Number) currentValue).longValue());
      else
        try {
          result = ODatabaseRecordThreadLocal.INSTANCE.get().getStorage().getConfiguration().getDateTimeFormatInstance()
              .parse(currentValue.toString());
        } catch (ParseException e) {
        }
    else {
      // EXTRACT ARGUMENTS
      final List<String> args = OStringSerializerHelper.getParameters(iFunction.substring(iFunction.indexOf('(')));

      final ORecordInternal<?> currentRecord = iContext != null ? (ORecordInternal<?>) iContext.getVariable("$current") : null;
      for (int i = 0; i < args.size(); ++i) {
        final String arg = args.get(i);
        final Object o = OSQLHelper.getValue(arg, currentRecord, iContext);
        if (o != null)
          args.set(i, o.toString());
      }

      if (function.startsWith("CHARAT("))
        result = currentValue.toString().charAt(Integer.parseInt(args.get(0)));
      else if (function.startsWith("INDEXOF("))
        if (args.size() == 1)
          result = currentValue.toString().indexOf(OStringSerializerHelper.getStringContent(args.get(0)));
        else
          result = currentValue.toString().indexOf(OStringSerializerHelper.getStringContent(args.get(0)),
              Integer.parseInt(args.get(1)));
      else if (function.startsWith("SUBSTRING("))
        if (args.size() == 1)
          result = currentValue.toString().substring(Integer.parseInt(args.get(0)));
        else
          result = currentValue.toString().substring(Integer.parseInt(args.get(0)), Integer.parseInt(args.get(1)));
      else if (function.startsWith("APPEND("))
        result = currentValue.toString() + OStringSerializerHelper.getStringContent(args.get(0));
      else if (function.startsWith("PREFIX("))
        result = OStringSerializerHelper.getStringContent(args.get(0)) + currentValue.toString();
      else if (function.startsWith("FORMAT("))
        if (currentValue instanceof Date)
          result = new SimpleDateFormat(OStringSerializerHelper.getStringContent(args.get(0))).format(currentValue);
        else
          result = String.format(OStringSerializerHelper.getStringContent(args.get(0)), currentValue.toString());
      else if (function.startsWith("LEFT(")) {
        final int len = Integer.parseInt(args.get(0));
        final String stringValue = currentValue.toString();
        result = stringValue.substring(0, len <= stringValue.length() ? len : stringValue.length());
      } else if (function.startsWith("RIGHT(")) {
        final int offset = Integer.parseInt(args.get(0));
        final String stringValue = currentValue.toString();
        result = stringValue.substring(offset < stringValue.length() ? stringValue.length() - offset : 0);
      } else {
        final OSQLFunctionRuntime f = OSQLHelper.getFunction(null, iFunction);
        if (f != null)
          result = f.execute(currentRecord, currentRecord, null, iContext);
      }
    }

    return result;
  }

  @SuppressWarnings("unchecked")
  public static void copyFieldValue(final ODocument iCloned, final Entry<String, Object> iEntry) {
    final Object fieldValue = iEntry.getValue();

    if (fieldValue != null) {
      if (fieldValue instanceof ODocument && !((ODocument) fieldValue).getIdentity().isValid()) {
        // EMBEDDED DOCUMENT
        iCloned._fieldValues.put(iEntry.getKey(), ((ODocument) fieldValue).copy());

        // LISTS
      } else if (fieldValue instanceof ORecordLazyList) {
        iCloned._fieldValues.put(iEntry.getKey(), ((ORecordLazyList) fieldValue).copy(iCloned));

      } else if (fieldValue instanceof ORecordTrackedList) {
        final ORecordTrackedList newList = new ORecordTrackedList(iCloned);
        newList.addAll((ORecordTrackedList) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newList);

      } else if (fieldValue instanceof OTrackedList<?>) {
        final OTrackedList<Object> newList = new OTrackedList<Object>(iCloned);
        newList.addAll((OTrackedList<Object>) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newList);

      } else if (fieldValue instanceof List<?>) {
        iCloned._fieldValues.put(iEntry.getKey(), new ArrayList<Object>((List<Object>) fieldValue));

        // SETS
      } else if (fieldValue instanceof OMVRBTreeRIDSet) {
        iCloned._fieldValues.put(iEntry.getKey(), ((OMVRBTreeRIDSet) fieldValue).copy(iCloned));

      } else if (fieldValue instanceof ORecordTrackedSet) {
        final ORecordTrackedSet newList = new ORecordTrackedSet(iCloned);
        newList.addAll((ORecordTrackedSet) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newList);

      } else if (fieldValue instanceof OTrackedSet<?>) {
        final OTrackedSet<Object> newList = new OTrackedSet<Object>(iCloned);
        newList.addAll((OTrackedSet<Object>) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newList);

      } else if (fieldValue instanceof Set<?>) {
        iCloned._fieldValues.put(iEntry.getKey(), new HashSet<Object>((Set<Object>) fieldValue));

        // MAPS
      } else if (fieldValue instanceof ORecordLazyMap) {
        final ORecordLazyMap newMap = new ORecordLazyMap(iCloned, ((ORecordLazyMap) fieldValue).getRecordType());
        newMap.putAll((ORecordLazyMap) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newMap);

      } else if (fieldValue instanceof OTrackedMap) {
        final OTrackedMap<Object> newMap = new OTrackedMap<Object>(iCloned);
        newMap.putAll((OTrackedMap<Object>) fieldValue);
        iCloned._fieldValues.put(iEntry.getKey(), newMap);

      } else if (fieldValue instanceof Map<?, ?>) {
        iCloned._fieldValues.put(iEntry.getKey(), new LinkedHashMap<String, Object>((Map<String, Object>) fieldValue));
      } else
        iCloned._fieldValues.put(iEntry.getKey(), fieldValue);
    } else if (iCloned.getSchemaClass() != null) {
      final OProperty prop = iCloned.getSchemaClass().getProperty(iEntry.getKey());
      if (prop != null && prop.isMandatory())
        iCloned._fieldValues.put(iEntry.getKey(), fieldValue);
    }
  }

  public static boolean hasSameContentItem(final Object iCurrent, ODatabaseRecord iMyDb, final Object iOther,
      final ODatabaseRecord iOtherDb, RIDMapper ridMapper) {
    if (iCurrent instanceof ODocument) {
      final ODocument current = (ODocument) iCurrent;
      if (iOther instanceof ORID) {
        if (!current.isDirty()) {
          if (!current.getIdentity().equals(iOther))
            return false;
        } else {
          final ODocument otherDoc = iOtherDb.load((ORID) iOther);
          if (!ODocumentHelper.hasSameContentOf(current, iMyDb, otherDoc, iOtherDb, ridMapper))
            return false;
        }
      } else if (!ODocumentHelper.hasSameContentOf(current, iMyDb, (ODocument) iOther, iOtherDb, ridMapper))
        return false;
    } else if (!compareScalarValues(iCurrent, iOther, ridMapper))
      return false;
    return true;
  }

  /**
   * Makes a deep comparison field by field to check if the passed ODocument instance is identical in the content to the current
   * one. Instead equals() just checks if the RID are the same.
   * 
   * @param iOther
   *          ODocument instance
   * @return true if the two document are identical, otherwise false
   * @see #equals(Object);
   */
  @SuppressWarnings("unchecked")
  public static boolean hasSameContentOf(final ODocument iCurrent, final ODatabaseRecord iMyDb, final ODocument iOther,
      final ODatabaseRecord iOtherDb, RIDMapper ridMapper) {
    if (iOther == null)
      return false;

    if (!iCurrent.equals(iOther) && iCurrent.getIdentity().isValid())
      return false;

    if (iMyDb != null)
      makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
        public Object call() {
          if (iCurrent.getInternalStatus() == STATUS.NOT_LOADED)
            iCurrent.reload();
          return null;
        }
      });

    if (iOtherDb != null)
      makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
        public Object call() {
          if (iOther.getInternalStatus() == STATUS.NOT_LOADED)
            iOther.reload();
          return null;
        }
      });

    if (iMyDb != null)
      makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
        public Object call() {
          iCurrent.checkForFields();
          return null;
        }
      });
    else
      iCurrent.checkForFields();

    if (iOtherDb != null)
      makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
        public Object call() {
          iOther.checkForFields();
          return null;
        }
      });
    else
      iOther.checkForFields();

    if (iCurrent._fieldValues.size() != iOther._fieldValues.size())
      return false;

    // CHECK FIELD-BY-FIELD
    Object myFieldValue;
    Object otherFieldValue;
    for (Entry<String, Object> f : iCurrent._fieldValues.entrySet()) {
      myFieldValue = f.getValue();
      otherFieldValue = iOther._fieldValues.get(f.getKey());

      if (myFieldValue == otherFieldValue)
        continue;

      // CHECK FOR NULLS
      if (myFieldValue == null) {
        if (otherFieldValue != null)
          return false;
      } else if (otherFieldValue == null)
        return false;

      if (myFieldValue != null)
        if (myFieldValue instanceof Set && otherFieldValue instanceof Set) {
          if (!compareSets(iMyDb, (Set<?>) myFieldValue, iOtherDb, (Set<?>) otherFieldValue, ridMapper))
            return false;
        } else if (myFieldValue instanceof Collection && otherFieldValue instanceof Collection) {
          if (!compareCollections(iMyDb, (Collection<?>) myFieldValue, iOtherDb, (Collection<?>) otherFieldValue, ridMapper))
            return false;
        } else if (myFieldValue instanceof ORidBag && otherFieldValue instanceof ORidBag) {
          if (!compareBags(iMyDb, (ORidBag) myFieldValue, iOtherDb, (ORidBag) otherFieldValue, ridMapper))
            return false;
        } else if (myFieldValue instanceof Map && otherFieldValue instanceof Map) {
          if (!compareMaps(iMyDb, (Map<Object, Object>) myFieldValue, iOtherDb, (Map<Object, Object>) otherFieldValue, ridMapper))
            return false;
        } else if (myFieldValue instanceof ODocument && otherFieldValue instanceof ODocument) {
          if (!hasSameContentOf((ODocument) myFieldValue, iMyDb, (ODocument) otherFieldValue, iOtherDb, ridMapper))
            return false;
        } else {
          if (!compareScalarValues(myFieldValue, otherFieldValue, ridMapper))
            return false;
        }
    }

    return true;
  }

  public static boolean compareMaps(ODatabaseRecord iMyDb, Map<Object, Object> myFieldValue, ODatabaseRecord iOtherDb,
      Map<Object, Object> otherFieldValue, RIDMapper ridMapper) {
    // CHECK IF THE ORDER IS RESPECTED
    final Map<Object, Object> myMap = myFieldValue;
    final Map<Object, Object> otherMap = otherFieldValue;

    if (myMap.size() != otherMap.size())
      return false;

    boolean oldMyAutoConvert = false;
    boolean oldOtherAutoConvert = false;

    if (myMap instanceof ORecordLazyMultiValue) {
      oldMyAutoConvert = ((ORecordLazyMultiValue) myMap).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) myMap).setAutoConvertToRecord(false);
    }

    if (otherMap instanceof ORecordLazyMultiValue) {
      oldOtherAutoConvert = ((ORecordLazyMultiValue) otherMap).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) otherMap).setAutoConvertToRecord(false);
    }

    try {
      final Iterator<Entry<Object, Object>> myEntryIterator = makeDbCall(iMyDb,
          new ODbRelatedCall<Iterator<Entry<Object, Object>>>() {
            public Iterator<Entry<Object, Object>> call() {
              return myMap.entrySet().iterator();
            }
          });

      while (makeDbCall(iMyDb, new ODbRelatedCall<Boolean>() {
        public Boolean call() {
          return myEntryIterator.hasNext();
        }
      })) {
        final Entry<Object, Object> myEntry = makeDbCall(iMyDb, new ODbRelatedCall<Entry<Object, Object>>() {
          public Entry<Object, Object> call() {
            return myEntryIterator.next();
          }
        });
        final Object myKey = makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
          public Object call() {
            return myEntry.getKey();
          }
        });

        if (makeDbCall(iOtherDb, new ODbRelatedCall<Boolean>() {
          public Boolean call() {
            return !otherMap.containsKey(myKey);
          }
        }))
          return false;

        if (myEntry.getValue() instanceof ODocument) {
          if (!hasSameContentOf(makeDbCall(iMyDb, new ODbRelatedCall<ODocument>() {
            public ODocument call() {
              return (ODocument) myEntry.getValue();
            }
          }), iMyDb, makeDbCall(iOtherDb, new ODbRelatedCall<ODocument>() {
            public ODocument call() {
              return (ODocument) otherMap.get(myEntry.getKey());
            }
          }), iOtherDb, ridMapper))
            return false;
        } else {
          final Object myValue = makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
            public Object call() {
              return myEntry.getValue();
            }
          });

          final Object otherValue = makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
            public Object call() {
              return otherMap.get(myEntry.getKey());
            }
          });

          if (!compareScalarValues(myValue, otherValue, ridMapper))
            return false;
        }
      }
      return true;
    } finally {
      if (myMap instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) myMap).setAutoConvertToRecord(oldMyAutoConvert);

      if (otherMap instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) otherMap).setAutoConvertToRecord(oldOtherAutoConvert);
    }
  }

  public static boolean compareCollections(ODatabaseRecord iMyDb, Collection<?> myFieldValue, ODatabaseRecord iOtherDb,
      Collection<?> otherFieldValue, RIDMapper ridMapper) {
    final Collection<?> myCollection = myFieldValue;
    final Collection<?> otherCollection = otherFieldValue;

    if (myCollection.size() != otherCollection.size())
      return false;

    boolean oldMyAutoConvert = false;
    boolean oldOtherAutoConvert = false;

    if (myCollection instanceof ORecordLazyMultiValue) {
      oldMyAutoConvert = ((ORecordLazyMultiValue) myCollection).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) myCollection).setAutoConvertToRecord(false);
    }

    if (otherCollection instanceof ORecordLazyMultiValue) {
      oldOtherAutoConvert = ((ORecordLazyMultiValue) otherCollection).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) otherCollection).setAutoConvertToRecord(false);
    }

    try {
      final Iterator<?> myIterator = makeDbCall(iMyDb, new ODbRelatedCall<Iterator<?>>() {
        public Iterator<?> call() {
          return myCollection.iterator();
        }
      });

      final Iterator<?> otherIterator = makeDbCall(iOtherDb, new ODbRelatedCall<Iterator<?>>() {
        public Iterator<?> call() {
          return otherCollection.iterator();
        }
      });

      while (makeDbCall(iMyDb, new ODbRelatedCall<Boolean>() {
        public Boolean call() {
          return myIterator.hasNext();
        }
      })) {
        final Object myNextVal = makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
          public Object call() {
            return myIterator.next();
          }
        });

        final Object otherNextVal = makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
          public Object call() {
            return otherIterator.next();
          }
        });

        if (!hasSameContentItem(myNextVal, iMyDb, otherNextVal, iOtherDb, ridMapper))
          return false;
      }
      return true;
    } finally {
      if (myCollection instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) myCollection).setAutoConvertToRecord(oldMyAutoConvert);

      if (otherCollection instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) otherCollection).setAutoConvertToRecord(oldOtherAutoConvert);
    }
  }

  public static boolean compareSets(ODatabaseRecord iMyDb, Set<?> myFieldValue, ODatabaseRecord iOtherDb, Set<?> otherFieldValue,
      RIDMapper ridMapper) {
    final Set<?> mySet = myFieldValue;
    final Set<?> otherSet = otherFieldValue;

    final int mySize = makeDbCall(iMyDb, new ODbRelatedCall<Integer>() {
      public Integer call() {
        return mySet.size();
      }
    });

    final int otherSize = makeDbCall(iOtherDb, new ODbRelatedCall<Integer>() {
      public Integer call() {
        return otherSet.size();
      }
    });

    if (mySize != otherSize)
      return false;

    boolean oldMyAutoConvert = false;
    boolean oldOtherAutoConvert = false;

    if (mySet instanceof ORecordLazyMultiValue) {
      oldMyAutoConvert = ((ORecordLazyMultiValue) mySet).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) mySet).setAutoConvertToRecord(false);
    }

    if (otherSet instanceof ORecordLazyMultiValue) {
      oldOtherAutoConvert = ((ORecordLazyMultiValue) otherSet).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) otherSet).setAutoConvertToRecord(false);
    }

    try {
      final Iterator<?> myIterator = makeDbCall(iMyDb, new ODbRelatedCall<Iterator<?>>() {
        public Iterator<?> call() {
          return mySet.iterator();
        }
      });

      while (makeDbCall(iMyDb, new ODbRelatedCall<Boolean>() {
        public Boolean call() {
          return myIterator.hasNext();
        }
      })) {

        final Iterator<?> otherIterator = makeDbCall(iOtherDb, new ODbRelatedCall<Iterator<?>>() {
          public Iterator<?> call() {
            return otherSet.iterator();
          }
        });

        final Object myNextVal = makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
          public Object call() {
            return myIterator.next();
          }
        });

        boolean found = false;
        while (!found && makeDbCall(iOtherDb, new ODbRelatedCall<Boolean>() {
          public Boolean call() {
            return otherIterator.hasNext();
          }
        })) {
          final Object otherNextVal = makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
            public Object call() {
              return otherIterator.next();
            }
          });

          found = hasSameContentItem(myNextVal, iMyDb, otherNextVal, iOtherDb, ridMapper);
        }

        if (!found)
          return false;
      }
      return true;
    } finally {
      if (mySet instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) mySet).setAutoConvertToRecord(oldMyAutoConvert);

      if (otherSet instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) otherSet).setAutoConvertToRecord(oldOtherAutoConvert);
    }
  }

  public static boolean compareBags(ODatabaseRecord iMyDb, ORidBag myFieldValue, ODatabaseRecord iOtherDb, ORidBag otherFieldValue,
      RIDMapper ridMapper) {
    final ORidBag myBag = myFieldValue;
    final ORidBag otherBag = otherFieldValue;

    final int mySize = makeDbCall(iMyDb, new ODbRelatedCall<Integer>() {
      public Integer call() {
        return myBag.size();
      }
    });

    final int otherSize = makeDbCall(iOtherDb, new ODbRelatedCall<Integer>() {
      public Integer call() {
        return otherBag.size();
      }
    });

    if (mySize != otherSize)
      return false;

    boolean oldMyAutoConvert;
    boolean oldOtherAutoConvert;

    oldMyAutoConvert = myBag.isAutoConvertToRecord();
    myBag.setAutoConvertToRecord(false);

    oldOtherAutoConvert = otherBag.isAutoConvertToRecord();
    otherBag.setAutoConvertToRecord(false);

    final ORidBag otherBagCopy = makeDbCall(iOtherDb, new ODbRelatedCall<ORidBag>() {
      @Override
      public ORidBag call() {
        final ORidBag otherRidBag = new ORidBag();
        otherRidBag.setAutoConvertToRecord(false);

        for (OIdentifiable identifiable : otherBag)
          otherRidBag.add(identifiable);

        return otherRidBag;
      }
    });

    try {
      final Iterator<OIdentifiable> myIterator = makeDbCall(iMyDb, new ODbRelatedCall<Iterator<OIdentifiable>>() {
        public Iterator<OIdentifiable> call() {
          return myBag.iterator();
        }
      });

      while (makeDbCall(iMyDb, new ODbRelatedCall<Boolean>() {
        public Boolean call() {
          return myIterator.hasNext();
        }
      })) {
        final OIdentifiable myIdentifiable = makeDbCall(iMyDb, new ODbRelatedCall<OIdentifiable>() {
          @Override
          public OIdentifiable call() {
            return myIterator.next();
          }
        });

        final ORID otherRid;
        if (ridMapper != null) {
          ORID convertedRid = ridMapper.map(myIdentifiable.getIdentity());
          if (convertedRid != null)
            otherRid = convertedRid;
          else
            otherRid = myIdentifiable.getIdentity();
        } else
          otherRid = myIdentifiable.getIdentity();

        makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
          @Override
          public Object call() {
            otherBagCopy.remove(otherRid);
            return null;
          }
        });

      }

      return makeDbCall(iOtherDb, new ODbRelatedCall<Boolean>() {
        @Override
        public Boolean call() {
          return otherBagCopy.isEmpty();
        }
      });
    } finally {
      myBag.setAutoConvertToRecord(oldMyAutoConvert);
      otherBag.setAutoConvertToRecord(oldOtherAutoConvert);
    }
  }

  private static boolean compareScalarValues(Object myValue, Object otherValue, RIDMapper ridMapper) {
    if (myValue == null && otherValue != null || myValue != null && otherValue == null)
      return false;

    if (myValue == null)
      return true;

    if (myValue.getClass().isArray() && !otherValue.getClass().isArray() || !myValue.getClass().isArray()
        && otherValue.getClass().isArray())
      return false;

    if (myValue.getClass().isArray() && otherValue.getClass().isArray()) {
      final int myArraySize = Array.getLength(myValue);
      final int otherArraySize = Array.getLength(otherValue);

      if (myArraySize != otherArraySize)
        return false;

      for (int i = 0; i < myArraySize; i++)
        if (!Array.get(myValue, i).equals(Array.get(otherValue, i)))
          return false;

      return true;
    }

    if (myValue instanceof Number && otherValue instanceof Number) {
      final Number myNumberValue = (Number) myValue;
      final Number otherNumberValue = (Number) otherValue;

      if (isInteger(myNumberValue) && isInteger(otherNumberValue))
        return myNumberValue.longValue() == otherNumberValue.longValue();
      else if (isFloat(myNumberValue) && isFloat(otherNumberValue))
        return myNumberValue.doubleValue() == otherNumberValue.doubleValue();
    }

    if (ridMapper != null && myValue instanceof ORID && otherValue instanceof ORID && ((ORID) myValue).isPersistent()) {
      ORID convertedValue = ridMapper.map((ORID) myValue);
      if (convertedValue != null)
        myValue = convertedValue;
    }

    return myValue.equals(otherValue);
  }

  private static boolean isInteger(Number value) {
    return value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long;

  }

  private static boolean isFloat(Number value) {
    return value instanceof Float || value instanceof Double;
  }

  public static void deleteCrossRefs(final ORID iRid, final ODocument iContent) {
    for (String fieldName : iContent.fieldNames()) {
      final Object fieldValue = iContent.field(fieldName);
      if (fieldValue != null) {
        if (fieldValue.equals(iRid)) {
          // REMOVE THE LINK
          iContent.field(fieldName, (ORID) null);
          iContent.save();
        } else if (fieldValue instanceof ODocument && ((ODocument) fieldValue).isEmbedded()) {
          // EMBEDDED DOCUMENT: GO RECURSIVELY
          deleteCrossRefs(iRid, (ODocument) fieldValue);
        } else if (OMultiValue.isMultiValue(fieldValue)) {
          // MULTI-VALUE (COLLECTION, ARRAY OR MAP), CHECK THE CONTENT
          for (final Iterator<?> it = OMultiValue.getMultiValueIterator(fieldValue); it.hasNext();) {
            final Object item = it.next();

            if (fieldValue.equals(iRid)) {
              // DELETE ITEM
              it.remove();
            } else if (item instanceof ODocument && ((ODocument) item).isEmbedded()) {
              // EMBEDDED DOCUMENT: GO RECURSIVELY
              deleteCrossRefs(iRid, (ODocument) item);
            }
          }
        }
      }
    }
  }

  public static <T> T makeDbCall(final ODatabaseRecord databaseRecord, final ODbRelatedCall<T> function) {
    ODatabaseRecordThreadLocal.INSTANCE.set(databaseRecord);
    return function.call();
  }

  public static interface ODbRelatedCall<T> {
    public T call();
  }

  public static interface RIDMapper {
    ORID map(ORID rid);
  }
}
