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

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.orientechnologies.common.io.OUtils;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.reflection.OReflectionHelper;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.annotation.OAccess;
import com.orientechnologies.orient.core.annotation.OAfterDeserialization;
import com.orientechnologies.orient.core.annotation.OAfterSerialization;
import com.orientechnologies.orient.core.annotation.OBeforeDeserialization;
import com.orientechnologies.orient.core.annotation.OBeforeSerialization;
import com.orientechnologies.orient.core.annotation.ODocumentInstance;
import com.orientechnologies.orient.core.annotation.OId;
import com.orientechnologies.orient.core.annotation.OVersion;
import com.orientechnologies.orient.core.db.OUserObject2RecordHandler;
import com.orientechnologies.orient.core.db.object.ODatabaseObject;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.db.record.OTrackedList;
import com.orientechnologies.orient.core.db.record.OTrackedMap;
import com.orientechnologies.orient.core.db.record.OTrackedMultiValue;
import com.orientechnologies.orient.core.db.record.OTrackedSet;
import com.orientechnologies.orient.core.entity.OEntityManager;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.exception.OSchemaException;
import com.orientechnologies.orient.core.exception.OSerializationException;
import com.orientechnologies.orient.core.exception.OTransactionException;
import com.orientechnologies.orient.core.fetch.OFetchContext;
import com.orientechnologies.orient.core.fetch.OFetchHelper;
import com.orientechnologies.orient.core.fetch.OFetchListener;
import com.orientechnologies.orient.core.id.OClusterPositionFactory;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.object.OObjectSerializerHelperManager;
import com.orientechnologies.orient.core.serialization.serializer.record.OSerializationThreadLocal;
import com.orientechnologies.orient.core.tx.OTransactionOptimistic;
import com.orientechnologies.orient.core.version.ORecordVersion;
import com.orientechnologies.orient.core.version.OSimpleVersion;
import com.orientechnologies.orient.core.version.OVersionFactory;
import com.orientechnologies.orient.object.db.ODatabasePojoAbstract;
import com.orientechnologies.orient.object.db.OObjectLazyList;
import com.orientechnologies.orient.object.db.OObjectLazyMap;
import com.orientechnologies.orient.object.db.OObjectNotDetachedException;
import com.orientechnologies.orient.object.fetch.OObjectFetchContext;
import com.orientechnologies.orient.object.fetch.OObjectFetchListener;

@SuppressWarnings("unchecked")
/**
 * Helper class to manage POJO by using the reflection.
 *  
 * @author Luca Garulli
 * @author Luca Molino
 * @author Jacques Desodt
 */
public class OObjectSerializerHelper {
  public static final Class<?>[]                            callbackAnnotationClasses = new Class[] { OBeforeDeserialization.class,
      OAfterDeserialization.class, OBeforeSerialization.class, OAfterSerialization.class };
  private static final Class<?>[]                           NO_ARGS                   = new Class<?>[] {};

  public static HashMap<Class<?>, OObjectSerializerContext> serializerContexts        = new LinkedHashMap<Class<?>, OObjectSerializerContext>();

  private static final HashMap<String, List<Field>>         classes                   = new HashMap<String, List<Field>>();
  private static final HashMap<String, Method>              callbacks                 = new HashMap<String, Method>();
  private static final HashMap<String, Object>              getters                   = new HashMap<String, Object>();
  private static final HashMap<String, Object>              setters                   = new HashMap<String, Object>();
  private static final HashMap<Class<?>, Field>             boundDocumentFields       = new HashMap<Class<?>, Field>();
  private static final HashMap<Class<?>, Field>             fieldIds                  = new HashMap<Class<?>, Field>();
  private static final HashMap<Class<?>, Field>             fieldVersions             = new HashMap<Class<?>, Field>();
  private static final HashMap<Class<?>, List<String>>      embeddedFields            = new HashMap<Class<?>, List<String>>();
  @SuppressWarnings("rawtypes")
  public static Class                                       jpaIdClass;
  @SuppressWarnings("rawtypes")
  public static Class                                       jpaVersionClass;
  @SuppressWarnings("rawtypes")
  public static Class                                       jpaAccessClass;
  @SuppressWarnings("rawtypes")
  public static Class                                       jpaEmbeddedClass;
  @SuppressWarnings("rawtypes")
  public static Class                                       jpaTransientClass;
  @SuppressWarnings("rawtypes")
  public static Class                                       jpaOneToOneClass;
  @SuppressWarnings("rawtypes")
  public static Class                                       jpaOneToManyClass;
  @SuppressWarnings("rawtypes")
  public static Class                                       jpaManyToManyClass;

  static {
    try {
      // DETERMINE IF THERE IS AVAILABLE JPA 1
      jpaIdClass = Class.forName("javax.persistence.Id");
      jpaVersionClass = Class.forName("javax.persistence.Version");
      jpaEmbeddedClass = Class.forName("javax.persistence.Embedded");
      jpaTransientClass = Class.forName("javax.persistence.Transient");
      jpaOneToOneClass = Class.forName("javax.persistence.OneToOne");
      jpaOneToManyClass = Class.forName("javax.persistence.OneToMany");
      jpaManyToManyClass = Class.forName("javax.persistence.ManyToMany");
      // DETERMINE IF THERE IS AVAILABLE JPA 2
      jpaAccessClass = Class.forName("javax.persistence.Access");

    } catch (Exception e) {
      // IGNORE THE EXCEPTION: JPA NOT FOUND
    }
  }

  public static void register() {
    OObjectSerializerHelperManager.getInstance().registerHelper(OObjectSerializerManager.getInstance());
  }

  public static boolean hasField(final Object iPojo, final String iProperty) {
    final Class<?> c = iPojo.getClass();
    final String className = c.getName();

    getClassFields(c);

    return getters.get(className + "." + iProperty) != null;
  }

  public static String getDocumentBoundField(final Class<?> iClass) {
    getClassFields(iClass);
    final Field f = boundDocumentFields.get(iClass);
    return f != null ? f.getName() : null;
  }

  public static Class<?> getFieldType(final Object iPojo, final String iProperty) {
    final Class<?> c = iPojo.getClass();
    final String className = c.getName();

    getClassFields(c);

    try {
      final Object o = getters.get(className + "." + iProperty);

      if (o == null)
        return null;
      else if (o instanceof Field)
        return ((Field) o).getType();
      else
        return ((Method) o).getReturnType();
    } catch (Exception e) {
      throw new OSchemaException("Cannot get the value of the property: " + iProperty, e);
    }
  }

  public static Class<?> getFieldType(ODocument iDocument, final OEntityManager iEntityManager) {
    if (iDocument.getInternalStatus() == ORecordElement.STATUS.NOT_LOADED)
      iDocument = (ODocument) iDocument.load();

    if (iDocument.getClassName() == null) {
      return null;
    } else {
      return iEntityManager.getEntityClass(iDocument.getClassName());
    }
  }

  public static Object getFieldValue(final Object iPojo, final String iProperty) {
    final Class<?> c = iPojo.getClass();
    final String className = c.getName();

    getClassFields(c);

    try {
      Object o = getters.get(className + "." + iProperty);

      if (o instanceof Method)
        return ((Method) o).invoke(iPojo);
      else if (o instanceof Field)
        return ((Field) o).get(iPojo);
      return null;
    } catch (Exception e) {
      throw new OSchemaException("Cannot get the value of the property: " + iProperty, e);
    }
  }

  public static void setFieldValue(final Object iPojo, final String iProperty, final Object iValue) {
    final Class<?> c = iPojo.getClass();
    final String className = c.getName();

    getClassFields(c);

    try {
      Object o = setters.get(className + "." + iProperty);

      if (o instanceof Method) {
        ((Method) o).invoke(iPojo,
            OObjectSerializerHelper.convertInObject(iPojo, iProperty, iValue, ((Method) o).getParameterTypes()[0]));
      } else if (o instanceof Field) {
        ((Field) o).set(iPojo, OType.convert(iValue, ((Field) o).getType()));
      }

    } catch (Exception e) {

      throw new OSchemaException(
          "Cannot set the value '" + iValue + "' to the property '" + iProperty + "' for the pojo: " + iPojo, e);
    }
  }

  @SuppressWarnings("rawtypes")
  public static Object fromStream(final ODocument iRecord, final Object iPojo, final OEntityManager iEntityManager,
      final OUserObject2RecordHandler iObj2RecHandler, final String iFetchPlan, final boolean iLazyLoading) {
    OFetchHelper.checkFetchPlanValid(iFetchPlan);
    final long timer = Orient.instance().getProfiler().startChrono();

    final Class<?> pojoClass = iPojo.getClass();

    final List<Field> properties = getClassFields(pojoClass);

    String fieldName;
    Object fieldValue;

    final String idFieldName = setObjectID(iRecord.getIdentity(), iPojo);
    final String vFieldName = setObjectVersion(iRecord.getRecordVersion(), iPojo);

    // CALL BEFORE UNMARSHALLING
    invokeCallback(iPojo, iRecord, OBeforeDeserialization.class);

    final String[] fieldNames = new String[properties.size()];

    // BIND BASIC FIELDS, LINKS WILL BE BOUND BY THE FETCH API
    int f = 0;
    for (Field p : properties) {
      fieldName = p.getName();
      fieldNames[f++] = fieldName;

      if (fieldName.equals(idFieldName) || fieldName.equals(vFieldName))
        continue;

      if (iRecord.containsField(fieldName)) {
        // BIND ONLY THE SPECIFIED FIELDS
        fieldValue = iRecord.field(fieldName);

        Object value = fieldValue;
        Type type = null;

        if (fieldValue == null
            || !(fieldValue instanceof ODocument)
            || (fieldValue instanceof Collection<?> && (((Collection<?>) fieldValue).size() == 0 || !(((Collection<?>) fieldValue)
                .iterator().next() instanceof ODocument)))
            || (!(fieldValue instanceof Map<?, ?>) || ((Map<?, ?>) fieldValue).size() == 0 || !(((Map<?, ?>) fieldValue).values()
                .iterator().next() instanceof ODocument))) {

          final Class<?> genericTypeClass = OReflectionHelper.getGenericMultivalueType(p);

          if (genericTypeClass != null)
            if (genericTypeClass.isEnum()) {
              // TRANSFORM THE MULTI-VALUE
              if (fieldValue instanceof List) {
                // LIST: TRANSFORM EACH SINGLE ITEM
                final List<Object> list = (List<Object>) fieldValue;
                Object v;
                for (int i = 0; i < list.size(); ++i) {
                  v = list.get(i);
                  if (v != null) {
                    v = Enum.valueOf((Class<Enum>) genericTypeClass, v.toString());
                    list.set(i, v);
                  }
                }
                value = list;
                type = List.class;
              } else if (fieldValue instanceof Set) {
                // SET: CREATE A TEMP SET TO WORK WITH ITEMS
                final Set<Object> newColl = new HashSet<Object>();
                final Set<Object> set = (Set<Object>) fieldValue;
                for (Object v : set) {
                  if (v != null) {
                    v = Enum.valueOf((Class<Enum>) genericTypeClass, v.toString());
                    newColl.add(v);
                  }
                }

                value = newColl;
                type = Set.class;
              } else if (fieldValue instanceof Map) {
                // MAP: TRANSFORM EACH SINGLE ITEM
                final Map<String, Object> map = (Map<String, Object>) fieldValue;
                Object v;
                for (Entry<String, ?> entry : map.entrySet()) {
                  v = entry.getValue();
                  if (v != null) {
                    v = Enum.valueOf((Class<Enum>) genericTypeClass, v.toString());
                    map.put(entry.getKey(), v);
                  }
                }
                type = Map.class;
                value = map;
              }

            } else {
              // TRANSFORM THE MULTI-VALUE
              if (fieldValue instanceof List) {
                // LIST: TRANSFORM EACH SINGLE ITEM
                final List<Object> list = (List<Object>) fieldValue;
                Object v;
                for (int i = 0; i < list.size(); ++i) {
                  v = list.get(i);
                  if (v != null)
                    list.set(i, unserializeFieldValue(genericTypeClass, v));
                }
                value = list;
                type = List.class;
              } else if (fieldValue instanceof Set) {
                // SET: CREATE A TEMP SET TO WORK WITH ITEMS
                final Set<Object> newColl = new HashSet<Object>();
                final Set<Object> set = (Set<Object>) fieldValue;
                for (Object v : set)
                  if (v != null)
                    newColl.add(unserializeFieldValue(genericTypeClass, v));

                value = newColl;
                type = Set.class;
              } else if (fieldValue instanceof Map) {
                // MAP: TRANSFORM EACH SINGLE ITEM
                final Map<String, Object> map = (Map<String, Object>) fieldValue;
                Object v;
                for (Entry<String, ?> entry : map.entrySet()) {
                  v = entry.getValue();
                  if (v != null)
                    map.put(entry.getKey(), unserializeFieldValue(genericTypeClass, v));

                }
                value = map;
                type = Map.class;
              }
            }

          if (type == null) {
            type = p.getGenericType();
            value = unserializeFieldValue((Class<?>) (type != null && type instanceof Class<?> ? type : null), fieldValue);
          }

          setFieldValue(iPojo, fieldName, value);
        }
      }

    }

    final OFetchListener listener = new OObjectFetchListener();
    final OFetchContext context = new OObjectFetchContext(iFetchPlan, iLazyLoading, iEntityManager, iObj2RecHandler);
    // BIND LINKS FOLLOWING THE FETCHING PLAN
    OFetchHelper.fetch(iRecord, iPojo, OFetchHelper.buildFetchPlan(iFetchPlan), listener, context, "");

    // CALL AFTER UNMARSHALLING
    invokeCallback(iPojo, iRecord, OAfterDeserialization.class);

    Orient.instance().getProfiler().stopChrono("Object.fromStream", "Deserialize object from stream", timer);

    return iPojo;
  }

  public static String setObjectID(final ORID iIdentity, final Object iPojo) {
    if (iPojo == null)
      return null;

    final Class<?> pojoClass = iPojo.getClass();

    getClassFields(pojoClass);

    final Field idField = fieldIds.get(pojoClass);
    if (idField != null) {
      Class<?> fieldType = idField.getType();

      final String idFieldName = idField.getName();

      if (ORID.class.isAssignableFrom(fieldType))
        setFieldValue(iPojo, idFieldName, iIdentity);
      else if (Number.class.isAssignableFrom(fieldType))
        setFieldValue(iPojo, idFieldName, iIdentity != null ? iIdentity.getClusterPosition() : null);
      else if (fieldType.equals(String.class))
        setFieldValue(iPojo, idFieldName, iIdentity != null ? iIdentity.toString() : null);
      else if (fieldType.equals(Object.class))
        setFieldValue(iPojo, idFieldName, iIdentity);
      else
        OLogManager.instance().warn(OObjectSerializerHelper.class,
            "@Id field has been declared as %s while the supported are: ORID, Number, String, Object", fieldType);
      return idFieldName;
    }
    return null;
  }

  public static ORecordId getObjectID(final ODatabasePojoAbstract<?> iDb, final Object iPojo) {
    getClassFields(iPojo.getClass());

    final Field idField = fieldIds.get(iPojo.getClass());
    if (idField != null) {
      final Object id = getFieldValue(iPojo, idField.getName());

      if (id != null) {
        // FOUND
        if (id instanceof ORecordId) {
          return (ORecordId) id;
        } else if (id instanceof Number) {
          // TREATS AS CLUSTER POSITION
          final OClass cls = iDb.getMetadata().getSchema().getClass(iPojo.getClass());
          if (cls == null)
            throw new OConfigurationException("Class " + iPojo.getClass() + " is not managed by current database");

          return new ORecordId(cls.getDefaultClusterId(), OClusterPositionFactory.INSTANCE.valueOf(((Number) id).longValue()));
        } else if (id instanceof String)
          return new ORecordId((String) id);
      }
    }
    return null;
  }

  public static String getObjectIDFieldName(final Object iPojo) {
    getClassFields(iPojo.getClass());

    final Field idField = fieldIds.get(iPojo.getClass());
    if (idField != null) {
      return idField.getName();
    }
    return null;
  }

  public static boolean hasObjectID(final Object iPojo) {
    getClassFields(iPojo.getClass());
    return fieldIds.get(iPojo.getClass()) != null;
  }

  public static String setObjectVersion(final ORecordVersion iVersion, final Object iPojo) {
    if (iPojo == null)
      return null;

    final Class<?> pojoClass = iPojo.getClass();
    getClassFields(pojoClass);

    final Field vField = fieldVersions.get(pojoClass);
    if (vField != null) {
      Class<?> fieldType = vField.getType();

      final String vFieldName = vField.getName();

      if (Number.class.isAssignableFrom(fieldType)) {
        if (iVersion instanceof OSimpleVersion)
          setFieldValue(iPojo, vFieldName, iVersion.getCounter());
        else
          OLogManager
              .instance()
              .warn(OObjectSerializerHelper.class,
                  "@Version field can't be declared as Number in distributed mode. Should be one of following: String, Object, ORecordVersion");
      } else if (fieldType.equals(String.class))
        setFieldValue(iPojo, vFieldName, String.valueOf(iVersion));
      else if (fieldType.equals(Object.class) || ORecordVersion.class.isAssignableFrom(fieldType))
        setFieldValue(iPojo, vFieldName, iVersion);
      else
        OLogManager.instance().warn(OObjectSerializerHelper.class,
            "@Version field has been declared as %s while the supported are: Number, String, Object", fieldType);
      return vFieldName;
    }
    return null;
  }

  public static ORecordVersion getObjectVersion(final Object iPojo) {
    getClassFields(iPojo.getClass());
    final Field idField = fieldVersions.get(iPojo.getClass());
    if (idField != null) {
      final Object ver = getFieldValue(iPojo, idField.getName());

      final ORecordVersion version = convertVersion(ver);
      if (version != null)
        return version;
    }
    throw new OObjectNotDetachedException("Cannot retrieve the object's VERSION for '" + iPojo + "' because has not been detached");
  }

  private static ORecordVersion convertVersion(final Object ver) {
    if (ver != null) {
      if (ver instanceof ORecordVersion) {
        return (ORecordVersion) ver;
      } else if (ver instanceof Number) {
        final ORecordVersion version = OVersionFactory.instance().createVersion();
        if (version instanceof OSimpleVersion) {
          // TREATS AS CLUSTER POSITION
          version.setCounter(((Number) ver).intValue());
          return version;
        }
      } else if (ver instanceof String) {
        final ORecordVersion version = OVersionFactory.instance().createVersion();
        version.getSerializer().fromString((String) ver, version);
        return version;
      } else
        OLogManager.instance().warn(OObjectSerializerHelper.class,
            "@Version field has been declared as %s while the supported are: Number, String, Object", ver.getClass());
    }
    return null;
  }

  public static String getObjectVersionFieldName(final Object iPojo) {
    getClassFields(iPojo.getClass());

    final Field idField = fieldVersions.get(iPojo.getClass());
    if (idField != null) {
      return idField.getName();
    }
    return null;
  }

  public static boolean hasObjectVersion(final Object iPojo) {
    getClassFields(iPojo.getClass());
    return fieldVersions.get(iPojo.getClass()) != null;
  }

  /**
   * Serialize the user POJO to a ORecordDocument instance.
   * 
   * @param iPojo
   *          User pojo to serialize
   * @param iRecord
   *          Record where to update
   * @param iObj2RecHandler
   */
  public static ODocument toStream(final Object iPojo, final ODocument iRecord, final OEntityManager iEntityManager,
      final OClass schemaClass, final OUserObject2RecordHandler iObj2RecHandler, final ODatabaseObject db,
      final boolean iSaveOnlyDirty) {
    if (iSaveOnlyDirty && !iRecord.isDirty())
      return iRecord;

    final long timer = Orient.instance().getProfiler().startChrono();

    final Integer identityRecord = System.identityHashCode(iRecord);

    if (OSerializationThreadLocal.INSTANCE.get().contains(identityRecord))
      return iRecord;

    OSerializationThreadLocal.INSTANCE.get().add(identityRecord);

    OProperty schemaProperty;

    final Class<?> pojoClass = iPojo.getClass();

    final List<Field> properties = getClassFields(pojoClass);

    // CHECK FOR ID BINDING
    final Field idField = fieldIds.get(pojoClass);
    if (idField != null) {
      Object id = getFieldValue(iPojo, idField.getName());
      if (id != null) {
        // FOUND
        if (id instanceof ORecordId) {
          iRecord.setIdentity((ORecordId) id);
        } else if (id instanceof Number) {
          // TREATS AS CLUSTER POSITION
          ((ORecordId) iRecord.getIdentity()).clusterId = schemaClass.getDefaultClusterId();
          ((ORecordId) iRecord.getIdentity()).clusterPosition = OClusterPositionFactory.INSTANCE.valueOf(((Number) id).longValue());
        } else if (id instanceof String)
          ((ORecordId) iRecord.getIdentity()).fromString((String) id);
        else if (id.getClass().equals(Object.class))
          iRecord.setIdentity((ORecordId) id);
        else
          OLogManager.instance().warn(OObjectSerializerHelper.class,
              "@Id field has been declared as %s while the supported are: ORID, Number, String, Object", id.getClass());
      }
    }

    // CHECK FOR VERSION BINDING
    final Field vField = fieldVersions.get(pojoClass);
    boolean versionConfigured = false;
    if (vField != null) {
      versionConfigured = true;
      Object ver = getFieldValue(iPojo, vField.getName());

      final ORecordVersion version = convertVersion(ver);
      if (version != null)
        iRecord.getRecordVersion().copyFrom(version);
    }

    if (db.isMVCC() && !versionConfigured && db.getTransaction() instanceof OTransactionOptimistic)
      throw new OTransactionException(
          "Cannot involve an object of class '"
              + pojoClass
              + "' in an Optimistic Transaction commit because it does not define @Version or @OVersion and therefore cannot handle MVCC");

    // SET OBJECT CLASS
    iRecord.setClassName(schemaClass != null ? schemaClass.getName() : null);

    String fieldName;
    Object fieldValue;

    // CALL BEFORE MARSHALLING
    invokeCallback(iPojo, iRecord, OBeforeSerialization.class);

    for (Field p : properties) {
      fieldName = p.getName();

      if (idField != null && fieldName.equals(idField.getName()))
        continue;

      if (vField != null && fieldName.equals(vField.getName()))
        continue;

      fieldValue = serializeFieldValue(getFieldType(iPojo, fieldName), getFieldValue(iPojo, fieldName));

      schemaProperty = schemaClass != null ? schemaClass.getProperty(fieldName) : null;

      if (fieldValue != null) {
        if (isEmbeddedObject(iPojo.getClass(), fieldValue.getClass(), fieldName, iEntityManager)) {
          // AUTO CREATE SCHEMA PROPERTY
          if (schemaClass == null) {
            db.getMetadata().getSchema().createClass(iPojo.getClass());
            iRecord.setClassNameIfExists(iPojo.getClass().getSimpleName());
          }

          if (schemaProperty == null) {
            OType t = OType.getTypeByClass(fieldValue.getClass());
            if (t == null)
              t = OType.EMBEDDED;
            schemaProperty = iRecord.getSchemaClass().createProperty(fieldName, t);
          }
        }
      }

      fieldValue = typeToStream(fieldValue, schemaProperty != null ? schemaProperty.getType() : null, iEntityManager,
          iObj2RecHandler, db, iRecord, iSaveOnlyDirty);

      iRecord.field(fieldName, fieldValue);
    }

    iObj2RecHandler.registerUserObject(iPojo, iRecord);

    // CALL AFTER MARSHALLING
    invokeCallback(iPojo, iRecord, OAfterSerialization.class);

    OSerializationThreadLocal.INSTANCE.get().remove(identityRecord);

    Orient.instance().getProfiler().stopChrono("Object.toStream", "Serialize object to stream", timer);

    return iRecord;
  }

  public static Object serializeFieldValue(final Class<?> type, final Object iFieldValue) {
    for (Class<?> classContext : serializerContexts.keySet()) {
      if (classContext != null && classContext.isAssignableFrom(type)) {
        return serializerContexts.get(classContext).serializeFieldValue(type, iFieldValue);
      }
    }

    if (serializerContexts.get(null) != null)
      return serializerContexts.get(null).serializeFieldValue(type, iFieldValue);

    return iFieldValue;
  }

  public static Object unserializeFieldValue(final Class<?> type, final Object iFieldValue) {
    for (Class<?> classContext : serializerContexts.keySet()) {
      if (classContext != null && classContext.isAssignableFrom(type)) {
        return serializerContexts.get(classContext).unserializeFieldValue(type, iFieldValue);
      }
    }

    if (serializerContexts.get(null) != null)
      return serializerContexts.get(null).unserializeFieldValue(type, iFieldValue);

    return iFieldValue;
  }

  private static Object typeToStream(Object iFieldValue, OType iType, final OEntityManager iEntityManager,
      final OUserObject2RecordHandler iObj2RecHandler, final ODatabaseObject db, final ODocument iRecord,
      final boolean iSaveOnlyDirty) {
    if (iFieldValue == null)
      return null;

    if (!OType.isSimpleType(iFieldValue)) {
      Class<?> fieldClass = iFieldValue.getClass();

      if (fieldClass.isArray()) {
        // ARRAY
        iFieldValue = multiValueToStream(Arrays.asList(iFieldValue), iType, iEntityManager, iObj2RecHandler, db, iRecord,
            iSaveOnlyDirty);
      } else if (Collection.class.isAssignableFrom(fieldClass)) {
        // COLLECTION (LIST OR SET)
        iFieldValue = multiValueToStream(iFieldValue, iType, iEntityManager, iObj2RecHandler, db, iRecord, iSaveOnlyDirty);
      } else if (Map.class.isAssignableFrom(fieldClass)) {
        // MAP
        iFieldValue = multiValueToStream(iFieldValue, iType, iEntityManager, iObj2RecHandler, db, iRecord, iSaveOnlyDirty);
      } else if (fieldClass.isEnum()) {
        // ENUM
        iFieldValue = ((Enum<?>) iFieldValue).name();
      } else {
        // LINK OR EMBEDDED
        fieldClass = iEntityManager.getEntityClass(fieldClass.getSimpleName());
        if (fieldClass != null) {
          // RECOGNIZED TYPE, SERIALIZE IT
          final ODocument linkedDocument = (ODocument) iObj2RecHandler.getRecordByUserObject(iFieldValue, true);

          final Object pojo = iFieldValue;
          iFieldValue = toStream(pojo, linkedDocument, iEntityManager, linkedDocument.getSchemaClass(), iObj2RecHandler, db,
              iSaveOnlyDirty);

          iObj2RecHandler.registerUserObject(pojo, linkedDocument);

        } else {
          final Object result = serializeFieldValue(null, iFieldValue);
          if (iFieldValue == result)
            throw new OSerializationException("Linked type [" + iFieldValue.getClass() + ":" + iFieldValue
                + "] cannot be serialized because is not part of registered entities. To fix this error register this class");

          iFieldValue = result;
        }
      }
    }
    return iFieldValue;
  }

  private static Object multiValueToStream(final Object iMultiValue, OType iType, final OEntityManager iEntityManager,
      final OUserObject2RecordHandler iObj2RecHandler, final ODatabaseObject db, final ODocument iRecord,
      final boolean iSaveOnlyDirty) {
    if (iMultiValue == null)
      return null;

    final Collection<Object> sourceValues;
    if (iMultiValue instanceof Collection<?>) {
      sourceValues = (Collection<Object>) iMultiValue;
    } else {
      sourceValues = (Collection<Object>) ((Map<?, ?>) iMultiValue).values();
    }

    if (iType == null) {
      if (sourceValues.size() == 0)
        return iMultiValue;

      // TRY TO UNDERSTAND THE COLLECTION TYPE BY ITS CONTENT
      final Object firstValue = sourceValues.iterator().next();

      if (firstValue == null)
        return iMultiValue;

      // DETERMINE THE RIGHT TYPE BASED ON SOURCE MULTI VALUE OBJECT
      if (OType.isSimpleType(firstValue)) {
        if (iMultiValue instanceof List)
          iType = OType.EMBEDDEDLIST;
        else if (iMultiValue instanceof Set)
          iType = OType.EMBEDDEDSET;
        else
          iType = OType.EMBEDDEDMAP;
      } else {
        if (iMultiValue instanceof List)
          iType = OType.LINKLIST;
        else if (iMultiValue instanceof Set)
          iType = OType.LINKSET;
        else
          iType = OType.LINKMAP;
      }
    }

    Object result = iMultiValue;
    final OType linkedType;

    // CREATE THE RETURN MULTI VALUE OBJECT BASED ON DISCOVERED TYPE
    if (iType.equals(OType.EMBEDDEDSET) || iType.equals(OType.LINKSET)) {
      if (iRecord != null && iType.equals(OType.EMBEDDEDSET))
        result = new OTrackedSet<Object>(iRecord);
      else
        result = new HashSet<Object>();
    } else if (iType.equals(OType.EMBEDDEDLIST) || iType.equals(OType.LINKLIST)) {
      if (iRecord != null && iType.equals(OType.EMBEDDEDLIST))
        result = new OTrackedList<Object>(iRecord);
      else
        result = new ArrayList<Object>();
    }
    // } else if (iType.equals(OType.EMBEDDEDLIST) ||
    // iType.equals(OType.LINKLIST)) {
    // result = new ArrayList<Object>();
    // } else if (iType.equals(OType.EMBEDDEDMAP) ||
    // iType.equals(OType.LINKMAP)) {
    // result = new HashMap<String, Object>();
    // } else
    // throw new IllegalArgumentException("Type " + iType +
    // " must be a collection");

    if (iType.equals(OType.LINKLIST) || iType.equals(OType.LINKSET) || iType.equals(OType.LINKMAP))
      linkedType = OType.LINK;
    else if (iType.equals(OType.EMBEDDEDLIST) || iType.equals(OType.EMBEDDEDSET) || iType.equals(OType.EMBEDDEDMAP))
      linkedType = OType.EMBEDDED;
    else
      throw new IllegalArgumentException("Type " + iType + " must be a multi value type (collection or map)");

    if (iMultiValue instanceof Set<?>) {
      for (Object o : sourceValues) {
        ((Collection<Object>) result).add(typeToStream(o, linkedType, iEntityManager, iObj2RecHandler, db, null, iSaveOnlyDirty));
      }
    } else if (iMultiValue instanceof List<?>) {
      for (int i = 0; i < sourceValues.size(); i++) {
        ((List<Object>) result).add(typeToStream(((List<?>) sourceValues).get(i), linkedType, iEntityManager, iObj2RecHandler, db,
            null, iSaveOnlyDirty));
      }
    } else {
      if (iMultiValue instanceof OObjectLazyMap<?>) {
        result = ((OObjectLazyMap<?>) iMultiValue).getUnderlying();
      } else {
        if (iRecord != null && iType.equals(OType.EMBEDDEDMAP))
          result = new OTrackedMap<Object>(iRecord);
        else
          result = new HashMap<Object, Object>();
        for (Entry<Object, Object> entry : ((Map<Object, Object>) iMultiValue).entrySet()) {
          ((Map<Object, Object>) result).put(entry.getKey(),
              typeToStream(entry.getValue(), linkedType, iEntityManager, iObj2RecHandler, db, null, iSaveOnlyDirty));
        }
      }
    }

    return result;
  }

  public static List<Field> getClassFields(final Class<?> iClass) {
    if (iClass.getName().startsWith("java.lang"))
      return null;

    synchronized (classes) {
      if (classes.containsKey(iClass.getName()))
        return classes.get(iClass.getName());

      return analyzeClass(iClass);
    }
  }

  /**
   * Returns the declared generic types of a class.
   * 
   * @param iObject
   *          Class to examine
   * @return The array of Type if any, otherwise null
   */
  public static Type[] getGenericTypes(final Object iObject) {
    if (iObject instanceof OTrackedMultiValue) {
      final Class<?> cls = ((OTrackedMultiValue<?, ?>) iObject).getGenericClass();
      if (cls != null)
        return new Type[] { cls };
    }

    return OReflectionHelper.getGenericTypes(iObject.getClass());
  }

  public static void invokeCallback(final Object iPojo, final ODocument iDocument, final Class<?> iAnnotation) {
    final Method m = callbacks.get(iPojo.getClass().getSimpleName() + "." + iAnnotation.getSimpleName());

    if (m != null)

      try {
        if (m.getParameterTypes().length > 0)
          m.invoke(iPojo, iDocument);
        else
          m.invoke(iPojo);
      } catch (Exception e) {
        throw new OConfigurationException("Error on executing user callback '" + m.getName() + "' annotated with '"
            + iAnnotation.getSimpleName() + "'", e);
      }
  }

  public static void bindSerializerContext(final Class<?> iClassContext, final OObjectSerializerContext iSerializerContext) {
    serializerContexts.put(iClassContext, iSerializerContext);
  }

  public static void unbindSerializerContext(final Class<?> iClassContext) {
    serializerContexts.remove(iClassContext);
  }

  protected static List<Field> analyzeClass(final Class<?> iClass) {

    final List<Field> properties = new ArrayList<Field>();
    classes.put(iClass.getName(), properties);

    String fieldName;
    Class<?> fieldType;
    int fieldModifier;
    boolean autoBinding;

    for (Class<?> currentClass = iClass; currentClass != Object.class;) {
      for (Field f : currentClass.getDeclaredFields()) {
        fieldModifier = f.getModifiers();
        if (Modifier.isStatic(fieldModifier) || Modifier.isNative(fieldModifier) || Modifier.isTransient(fieldModifier))
          continue;

        if (f.getName().equals("this$0"))
          continue;

        if (jpaTransientClass != null) {
          Annotation ann = f.getAnnotation(jpaTransientClass);
          if (ann != null)
            // @Transient DEFINED, JUMP IT
            continue;
        }

        fieldName = f.getName();
        fieldType = f.getType();
        properties.add(f);

        // CHECK FOR AUTO-BINDING
        autoBinding = true;
        if (f.getAnnotation(OAccess.class) == null || f.getAnnotation(OAccess.class).value() == OAccess.OAccessType.PROPERTY)
          autoBinding = true;
        // JPA 2+ AVAILABLE?
        else if (jpaAccessClass != null) {
          Annotation ann = f.getAnnotation(jpaAccessClass);
          if (ann != null) {
            // TODO: CHECK IF CONTAINS VALUE=FIELD
            autoBinding = true;
          }
        }

        if (f.getAnnotation(ODocumentInstance.class) != null)
          // BOUND DOCUMENT ON IT
          boundDocumentFields.put(iClass, f);

        boolean idFound = false;
        if (f.getAnnotation(OId.class) != null) {
          // RECORD ID
          fieldIds.put(iClass, f);
          idFound = true;
        }
        // JPA 1+ AVAILABLE?
        else if (jpaIdClass != null && f.getAnnotation(jpaIdClass) != null) {
          // RECORD ID
          fieldIds.put(iClass, f);
          idFound = true;
        }
        if (idFound) {
          // CHECK FOR TYPE
          if (fieldType.isPrimitive())
            OLogManager.instance().warn(OObjectSerializerHelper.class, "Field '%s' cannot be a literal to manage the Record Id",
                f.toString());
          else if (!ORID.class.isAssignableFrom(fieldType) && fieldType != String.class && fieldType != Object.class
              && !Number.class.isAssignableFrom(fieldType))
            OLogManager.instance().warn(OObjectSerializerHelper.class, "Field '%s' cannot be managed as type: %s", f.toString(),
                fieldType);
        }

        boolean vFound = false;
        if (f.getAnnotation(OVersion.class) != null) {
          // RECORD ID
          fieldVersions.put(iClass, f);
          vFound = true;
        }
        // JPA 1+ AVAILABLE?
        else if (jpaVersionClass != null && f.getAnnotation(jpaVersionClass) != null) {
          // RECORD ID
          fieldVersions.put(iClass, f);
          vFound = true;
        }
        if (vFound) {
          // CHECK FOR TYPE
          if (fieldType.isPrimitive())
            OLogManager.instance().warn(OObjectSerializerHelper.class, "Field '%s' cannot be a literal to manage the Version",
                f.toString());
          else if (fieldType != String.class && fieldType != Object.class && !ORecordVersion.class.isAssignableFrom(fieldType)
              && !Number.class.isAssignableFrom(fieldType))
            OLogManager.instance().warn(OObjectSerializerHelper.class, "Field '%s' cannot be managed as type: %s", f.toString(),
                fieldType);
        }

        // JPA 1+ AVAILABLE?
        if (jpaEmbeddedClass != null && f.getAnnotation(jpaEmbeddedClass) != null) {
          if (embeddedFields.get(iClass) == null)
            embeddedFields.put(iClass, new ArrayList<String>());
          embeddedFields.get(iClass).add(fieldName);
        }

        if (autoBinding)
          // TRY TO GET THE VALUE BY THE GETTER (IF ANY)
          try {
            String getterName = "get" + OUtils.camelCase(fieldName);
            Method m = currentClass.getMethod(getterName, NO_ARGS);
            getters.put(iClass.getName() + "." + fieldName, m);
          } catch (Exception e) {
            registerFieldGetter(iClass, fieldName, f);
          }
        else
          registerFieldGetter(iClass, fieldName, f);

        if (autoBinding)
          // TRY TO GET THE VALUE BY THE SETTER (IF ANY)
          try {
            String getterName = "set" + OUtils.camelCase(fieldName);
            Method m = currentClass.getMethod(getterName, f.getType());
            setters.put(iClass.getName() + "." + fieldName, m);
          } catch (Exception e) {
            registerFieldSetter(iClass, fieldName, f);
          }
        else
          registerFieldSetter(iClass, fieldName, f);
      }

      registerCallbacks(iClass, currentClass);

      currentClass = currentClass.getSuperclass();

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

  @SuppressWarnings("rawtypes")
  private static void registerCallbacks(final Class<?> iRootClass, final Class<?> iCurrentClass) {
    // FIND KEY METHODS
    for (Method m : iCurrentClass.getDeclaredMethods()) {
      // SEARCH FOR CALLBACK ANNOTATIONS
      for (Class annotationClass : callbackAnnotationClasses) {
        if (m.getAnnotation(annotationClass) != null)
          callbacks.put(iRootClass.getSimpleName() + "." + annotationClass.getSimpleName(), m);
      }
    }
  }

  private static void registerFieldSetter(final Class<?> iClass, String fieldName, Field f) {
    // TRY TO GET THE VALUE BY ACCESSING DIRECTLY TO THE PROPERTY
    if (!f.isAccessible())
      f.setAccessible(true);

    setters.put(iClass.getName() + "." + fieldName, f);
  }

  private static void registerFieldGetter(final Class<?> iClass, String fieldName, Field f) {
    // TRY TO GET THE VALUE BY ACCESSING DIRECTLY TO THE PROPERTY
    if (!f.isAccessible())
      f.setAccessible(true);

    getters.put(iClass.getName() + "." + fieldName, f);
  }

  private static boolean isEmbeddedObject(final Class<?> iPojoClass, final Class<?> iFieldClass, final String iFieldName,
      final OEntityManager iEntityManager) {
    return embeddedFields.get(iPojoClass) != null && embeddedFields.get(iPojoClass).contains(iFieldName);
  }

  public static Object convertDocumentInType(final ODocument oDocument, final Class<?> type) {
    Object pojo = null;
    try {
      pojo = type.newInstance();
      final List<Field> fields = OObjectSerializerHelper.analyzeClass(type);
      for (Field aField : fields) {
        OObjectSerializerHelper.setFieldFromDocument(oDocument, pojo, aField);
      }
    } catch (Exception e) {
      OLogManager.instance().error(null, "Error on converting document in object", e);
    }
    return pojo;
  }

  private static void setFieldFromDocument(final ODocument iDocument, final Object iPojo, final Field iField) throws Exception {
    final String idFieldName = OObjectSerializerHelper.setObjectID(iDocument.getIdentity(), iPojo);
    final String vFieldName = OObjectSerializerHelper.setObjectVersion(iDocument.getRecordVersion(), iPojo);
    final String fieldName = iField.getName();
    // Don't assign id and version fields, used by Orient
    if (!fieldName.equals(idFieldName) && !fieldName.equals(vFieldName)) {
      // Assign only fields that are in the document
      if (iDocument.containsField(fieldName)) {
        Class<?> aClass = (Class<?>) iField.getGenericType();
        Object fieldValue = iDocument.field(fieldName);
        Object realValue = OObjectSerializerHelper.getObject(fieldValue, aClass);
        String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        final Method m = iPojo.getClass().getMethod(setterName, aClass);
        m.invoke(iPojo, realValue);
      }
    }
  }

  private static Object getObject(final Object fieldValue, final Class<?> aClass) {
    if (fieldValue instanceof ODocument)
      return OObjectSerializerHelper.convertDocumentInType((ODocument) fieldValue, aClass);
    return fieldValue;
  }

  public static Object convertInObject(final Object iPojo, final String iField, final Object iValue, final Class<?> parameterType) {
    // New conversion method working with OLazyObjectList
    if (!(iValue instanceof OObjectLazyList<?>))
      return OType.convert(iValue, parameterType);

    List<Object> aSubList = null;
    try {
      final Field aField = OObjectSerializerHelper.getField(iPojo, iField);
      final Class<?> listClass = aField.getType();
      final ParameterizedType aType = (ParameterizedType) aField.getGenericType();
      final Class<?> objectClass = (Class<?>) aType.getActualTypeArguments()[0];
      final OObjectLazyList<?> aList = (OObjectLazyList<?>) iValue;
      // Instantiation of the list
      if (listClass.isInterface()) {
        aSubList = new ArrayList<Object>();
      } else {
        aSubList = (List<Object>) listClass.newInstance();
      }
      for (final Object value : aList) {
        if (value instanceof ODocument) {
          final ODocument aDocument = (ODocument) value;
          aSubList.add(OObjectSerializerHelper.convertDocumentInType(aDocument, objectClass));
        } else {
          aSubList.add(value);
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return aSubList;
  }

  private static Field getField(final Object iPojo, final String iField) {
    final List<Field> fields = OObjectSerializerHelper.getClassFields(iPojo.getClass());
    if (fields != null) {
      for (Field f : fields)
        if (f.getName().equals(iField))
          return f;
    }
    return null;
  }
}
