/*
 *
 * Copyright 2012 Luca Molino (molino.luca--AT--gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.orientechnologies.orient.object.enhancement;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.object.ODatabaseObject;
import com.orientechnologies.orient.core.entity.OEntityManager;
import com.orientechnologies.orient.core.exception.OSerializationException;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.object.serialization.OObjectCustomSerializerList;
import com.orientechnologies.orient.object.serialization.OObjectCustomSerializerMap;
import com.orientechnologies.orient.object.serialization.OObjectCustomSerializerSet;

/**
 * @author luca.molino
 * 
 */
public class OObjectEntityEnhancer {

  private static final OObjectEntityEnhancer       instance              = new OObjectEntityEnhancer();
  private final Map<Class<?>, OObjectMethodFilter> customMethodFilters   = new HashMap<Class<?>, OObjectMethodFilter>();
  private final OObjectMethodFilter                defaultMethodFilter   = new OObjectMethodFilter();

  public static final String                       ENHANCER_CLASS_PREFIX = "orientdb_";

  public OObjectEntityEnhancer() {
  }

  @SuppressWarnings("unchecked")
  public <T> T getProxiedInstance(final String iClass, final OEntityManager entityManager, final ODocument doc,
      final ProxyObject parent, Object... iArgs) {
    final Class<T> clazz = (Class<T>) entityManager.getEntityClass(iClass);
    return getProxiedInstance(clazz, null, doc, parent, iArgs);
  }

  @SuppressWarnings("unchecked")
  public <T> T getProxiedInstance(final String iClass, final Object iEnclosingInstance, final OEntityManager entityManager,
      final ODocument doc, final ProxyObject parent, Object... iArgs) {
    final Class<T> clazz = (Class<T>) entityManager.getEntityClass(iClass);
    return getProxiedInstance(clazz, iEnclosingInstance, doc, parent, iArgs);
  }

  public <T> T getProxiedInstance(final Class<T> iClass, final ODocument doc, Object... iArgs) {
    return getProxiedInstance(iClass, null, doc, null, iArgs);
  }

  @SuppressWarnings("unchecked")
  public <T> T getProxiedInstance(final Class<T> iClass, Object iEnclosingInstance, final ODocument doc, final ProxyObject parent,
      Object... iArgs) {
    if (iClass == null) {
      throw new OSerializationException("Type " + doc.getClassName()
          + " cannot be serialized because is not part of registered entities. To fix this error register this class");
    }
    final Class<T> c;
    boolean isInnerClass = iClass.getEnclosingClass() != null;
    if (Proxy.class.isAssignableFrom(iClass)) {
      c = iClass;
    } else {
      ProxyFactory f = new ProxyFactory();
      f.setSuperclass(iClass);
      if (customMethodFilters.get(iClass) != null) {
        f.setFilter(customMethodFilters.get(iClass));
      } else {
        f.setFilter(defaultMethodFilter);
      }
      c = f.createClass();
    }
    MethodHandler mi = new OObjectProxyMethodHandler(doc);
    ((OObjectProxyMethodHandler) mi).setParentObject(parent);
    try {
      T newEntity;
      if (iArgs != null && iArgs.length > 0) {
        if (isInnerClass) {
          if (iEnclosingInstance == null) {
            iEnclosingInstance = iClass.getEnclosingClass().newInstance();
          }
          Object[] newArgs = new Object[iArgs.length + 1];
          newArgs[0] = iEnclosingInstance;
          for (int i = 0; i < iArgs.length; i++) {
            newArgs[i + 1] = iArgs[i];
          }
          iArgs = newArgs;
        }
        Constructor<T> constructor = null;
        for (Constructor<?> constr : c.getConstructors()) {
          boolean found = true;
          if (constr.getParameterTypes().length == iArgs.length) {
            for (int i = 0; i < constr.getParameterTypes().length; i++) {
              Class<?> parameterType = constr.getParameterTypes()[i];
              if (parameterType.isPrimitive()) {
                if (!isPrimitiveParameterCorrect(parameterType, iArgs[i])) {
                  found = false;
                  break;
                }
              } else if (iArgs[i] != null && !parameterType.isAssignableFrom(iArgs[i].getClass())) {
                found = false;
                break;
              }
            }
          } else {
            continue;
          }
          if (found) {
            constructor = (Constructor<T>) constr;
            break;
          }
        }
        if (constructor != null) {
          newEntity = (T) constructor.newInstance(iArgs);
          initDocument(iClass, newEntity, doc, (ODatabaseObject) ODatabaseRecordThreadLocal.INSTANCE.get().getDatabaseOwner());
        } else {
          if (iEnclosingInstance != null)
            newEntity = createInstanceNoParameters(c, iEnclosingInstance);
          else
            newEntity = createInstanceNoParameters(c, iClass);
        }
      } else {
        if (iEnclosingInstance != null)
          newEntity = createInstanceNoParameters(c, iEnclosingInstance);
        else
          newEntity = createInstanceNoParameters(c, iClass);
      }
      ((Proxy) newEntity).setHandler(mi);
      if (OObjectEntitySerializer.hasBoundedDocumentField(iClass))
        OObjectEntitySerializer.setFieldValue(OObjectEntitySerializer.getBoundedDocumentField(iClass), newEntity, doc);
      return newEntity;
    } catch (InstantiationException ie) {
      OLogManager.instance().error(this, "Error creating proxied instance for class " + iClass.getName(), ie);
    } catch (IllegalAccessException iae) {
      OLogManager.instance().error(this, "Error creating proxied instance for class " + iClass.getName(), iae);
    } catch (IllegalArgumentException iae) {
      OLogManager.instance().error(this, "Error creating proxied instance for class " + iClass.getName(), iae);
    } catch (SecurityException se) {
      OLogManager.instance().error(this, "Error creating proxied instance for class " + iClass.getName(), se);
    } catch (InvocationTargetException ite) {
      OLogManager.instance().error(this, "Error creating proxied instance for class " + iClass.getName(), ite);
    } catch (NoSuchMethodException nsme) {
      OLogManager.instance().error(this, "Error creating proxied instance for class " + iClass.getName(), nsme);
    }
    return null;
  }

  public OObjectMethodFilter getMethodFilter(Class<?> iClass) {
    if (Proxy.class.isAssignableFrom(iClass))
      iClass = iClass.getSuperclass();
    OObjectMethodFilter filter = customMethodFilters.get(iClass);
    if (filter == null)
      filter = defaultMethodFilter;
    return filter;
  }

  public void registerClassMethodFilter(Class<?> iClass, OObjectMethodFilter iMethodFilter) {
    customMethodFilters.put(iClass, iMethodFilter);
  }

  public void deregisterClassMethodFilter(Class<?> iClass) {
    customMethodFilters.remove(iClass);
  }

  public static synchronized OObjectEntityEnhancer getInstance() {
    return instance;
  }

  private boolean isPrimitiveParameterCorrect(Class<?> primitiveClass, Object parameterValue) {
    if (parameterValue == null)
      return false;
    final Class<?> parameterClass = parameterValue.getClass();
    if (Integer.TYPE.isAssignableFrom(primitiveClass))
      return Integer.class.isAssignableFrom(parameterClass);
    else if (Double.TYPE.isAssignableFrom(primitiveClass))
      return Double.class.isAssignableFrom(parameterClass);
    else if (Float.TYPE.isAssignableFrom(primitiveClass))
      return Float.class.isAssignableFrom(parameterClass);
    else if (Long.TYPE.isAssignableFrom(primitiveClass))
      return Long.class.isAssignableFrom(parameterClass);
    else if (Short.TYPE.isAssignableFrom(primitiveClass))
      return Short.class.isAssignableFrom(parameterClass);
    else if (Byte.TYPE.isAssignableFrom(primitiveClass))
      return Byte.class.isAssignableFrom(parameterClass);
    return false;
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  protected void initDocument(Class<?> iClass, Object iInstance, ODocument iDocument, ODatabaseObject db)
      throws IllegalArgumentException, IllegalAccessException {
    for (Class<?> currentClass = iClass; currentClass != Object.class;) {
      for (Field f : currentClass.getDeclaredFields()) {
        if (f.getName().equals("this$0"))
          continue;
        if (!f.isAccessible()) {
          f.setAccessible(true);
        }
        Object o = f.get(iInstance);
        if (o != null) {
          if (OObjectEntitySerializer.isSerializedType(f)) {
            if (o instanceof List<?>) {
              List<?> list = new ArrayList();
              iDocument.field(f.getName(), list);
              o = new OObjectCustomSerializerList(OObjectEntitySerializer.getSerializedType(f), iDocument, list, (List<?>) o);
              f.set(iInstance, o);
            } else if (o instanceof Set<?>) {
              Set<?> set = new HashSet();
              iDocument.field(f.getName(), set);
              o = new OObjectCustomSerializerSet(OObjectEntitySerializer.getSerializedType(f), iDocument, set, (Set<?>) o);
              f.set(iInstance, o);
            } else if (o instanceof Map<?, ?>) {
              Map<?, ?> map = new HashMap();
              iDocument.field(f.getName(), map);
              o = new OObjectCustomSerializerMap(OObjectEntitySerializer.getSerializedType(f), iDocument, map, (Map<?, ?>) o);
              f.set(iInstance, o);
            } else {
              o = OObjectEntitySerializer.serializeFieldValue(o.getClass(), o);
              iDocument.field(f.getName(), o);
            }
          } else {
            iDocument.field(f.getName(), OObjectEntitySerializer.typeToStream(o, OType.getTypeByClass(f.getType()), db, iDocument));
          }
        }
      }
      currentClass = currentClass.getSuperclass();
    }
  }

  protected <T> T createInstanceNoParameters(Class<T> iProxiedClass, Class<?> iOriginalClass) throws SecurityException,
      NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
    T instanceToReturn = null;
    final Class<?> enclosingClass = iOriginalClass.getEnclosingClass();

    if (enclosingClass != null && !Modifier.isStatic(iOriginalClass.getModifiers())) {
      Object instanceOfEnclosingClass = createInstanceNoParameters(enclosingClass, enclosingClass);

      Constructor<T> ctor = iProxiedClass.getConstructor(enclosingClass);

      if (ctor != null) {
        instanceToReturn = ctor.newInstance(instanceOfEnclosingClass);
      }
    } else {
      try {
        instanceToReturn = iProxiedClass.newInstance();
      } catch (InstantiationException e) {
        OLogManager.instance().error(this, "Cannot create an instance of the enclosing class '%s'", iOriginalClass);
        throw e;
      }
    }

    return instanceToReturn;

  }

  protected <T> T createInstanceNoParameters(Class<T> iProxiedClass, Object iEnclosingInstance) throws SecurityException,
      NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
    T instanceToReturn = null;
    final Class<?> enclosingClass = iEnclosingInstance.getClass();

    if (enclosingClass != null) {

      Constructor<T> ctor = iProxiedClass.getConstructor(enclosingClass);

      if (ctor != null) {
        instanceToReturn = ctor.newInstance(iEnclosingInstance);
      }
    } else {
      instanceToReturn = iProxiedClass.newInstance();
    }

    return instanceToReturn;

  }
}
