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

import java.lang.reflect.Field;
import java.util.*;
import java.util.Map.Entry;

import com.orientechnologies.orient.core.exception.OTransactionException;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyObject;

import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.db.ODatabaseComplex;
import com.orientechnologies.orient.core.db.ODatabaseSchemaAware;
import com.orientechnologies.orient.core.db.ODatabaseWrapperAbstract;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.db.object.OObjectLazyMultivalueElement;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.hook.ORecordHook;
import com.orientechnologies.orient.core.hook.ORecordHook.RESULT;
import com.orientechnologies.orient.core.hook.ORecordHook.TYPE;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.metadata.OMetadata;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.metadata.security.OUser;
import com.orientechnologies.orient.core.query.OQuery;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordInternal;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.tx.OTransaction;
import com.orientechnologies.orient.core.tx.OTransaction.TXTYPE;
import com.orientechnologies.orient.core.version.ORecordVersion;
import com.orientechnologies.orient.object.enhancement.OObjectProxyMethodHandler;
import com.orientechnologies.orient.object.serialization.OObjectSerializerHelper;

@SuppressWarnings("unchecked")
public abstract class ODatabasePojoAbstract<T extends Object> extends ODatabaseWrapperAbstract<ODatabaseDocumentTx> implements
    ODatabaseSchemaAware<T> {
  protected IdentityHashMap<Object, ODocument> objects2Records = new IdentityHashMap<Object, ODocument>();
  protected IdentityHashMap<ODocument, T>      records2Objects = new IdentityHashMap<ODocument, T>();
  protected HashMap<ORID, ODocument>           rid2Records     = new HashMap<ORID, ODocument>();
  protected boolean                            retainObjects   = true;

  public ODatabasePojoAbstract(final ODatabaseDocumentTx iDatabase) {
    super(iDatabase);
    iDatabase.setDatabaseOwner(this);
  }

  public abstract ODocument pojo2Stream(final T iPojo, final ODocument record);

  public abstract Object stream2pojo(final ODocument record, final Object iPojo, final String iFetchPlan);

  @Override
  public void close() {
    objects2Records.clear();
    records2Objects.clear();
    rid2Records.clear();
    super.close();
  }

  public OTransaction getTransaction() {
    return underlying.getTransaction();
  }

  public ODatabaseComplex<T> begin() {
    return (ODatabaseComplex<T>) underlying.begin();
  }

  public ODatabaseComplex<T> begin(final TXTYPE iType) {
    return (ODatabaseComplex<T>) underlying.begin(iType);
  }

  public ODatabaseComplex<T> begin(final OTransaction iTx) {
    return (ODatabaseComplex<T>) underlying.begin(iTx);
  }

  public ODatabaseComplex<T> commit() {
    return commit(false);
  }

  @Override
  public ODatabaseComplex<T> commit(boolean force) throws OTransactionException {
    underlying.commit(force);

    if (!underlying.getTransaction().isActive())
      clearNewEntriesFromCache();

    return this;
  }

  public ODatabaseComplex<T> rollback() {
    return rollback(false);
  }

  @Override
  public ODatabaseComplex<T> rollback(boolean force) throws OTransactionException {
    underlying.rollback(force);

    if (!underlying.getTransaction().isActive()) {
      clearNewEntriesFromCache();

      final Set<ORID> rids = new HashSet<ORID>(rid2Records.keySet());

      ORecord<?> record;
      Object object;
      for (ORID rid : rids) {
        if (rid.isTemporary()) {
          record = rid2Records.remove(rid);
          if (record != null) {
            object = records2Objects.remove(record);
            if (object != null) {
              objects2Records.remove(object);
            }
          }
        }
      }
    }

    return this;
  }

  /**
   * Sets as dirty a POJO. This is useful when you change the object and need to tell to the engine to treat as dirty.
   * 
   * @param iPojo
   *          User object
   */
  public void setDirty(final Object iPojo) {
    if (iPojo == null)
      return;

    final ODocument record = getRecordByUserObject(iPojo, false);
    if (record == null)
      throw new OObjectNotManagedException("The object " + iPojo + " is not managed by current database");

    record.setDirty();
  }

  /**
   * Sets as not dirty a POJO. This is useful when you change some other object and need to tell to the engine to treat this one as
   * not dirty.
   * 
   * @param iPojo
   *          User object
   */
  public void unsetDirty(final Object iPojo) {
    if (iPojo == null)
      return;

    final ODocument record = getRecordByUserObject(iPojo, false);
    if (record == null)
      return;

    record.unsetDirty();
  }

  public void setInternal(final ATTRIBUTES attribute, final Object iValue) {
    underlying.setInternal(attribute, iValue);
  }

  /**
   * Returns the version number of the object.
   * 
   * @param iPojo
   *          User object
   */
  public ORecordVersion getVersion(final Object iPojo) {
    final ODocument record = getRecordByUserObject(iPojo, false);

    if (record == null)
      throw new OObjectNotManagedException("The object " + iPojo + " is not managed by current database");

    return record.getRecordVersion();
  }

  /**
   * Returns the object unique identity.
   * 
   * @param iPojo
   *          User object
   */
  public ORID getIdentity(final Object iPojo) {
    final ODocument record = getRecordByUserObject(iPojo, false);
    if (record == null)
      throw new OObjectNotManagedException("The object " + iPojo + " is not managed by current database");

    return record.getIdentity();
  }

  public OUser getUser() {
    return underlying.getUser();
  }

  public void setUser(OUser user) {
    underlying.setUser(user);
  }

  public OMetadata getMetadata() {
    return underlying.getMetadata();
  }

  /**
   * Returns a wrapped OCommandRequest instance to catch the result-set by converting it before to return to the user application.
   */
  public <RET extends OCommandRequest> RET command(final OCommandRequest iCommand) {
    return (RET) new OCommandSQLPojoWrapper(this, underlying.command(iCommand));
  }

  public <RET extends List<?>> RET query(final OQuery<?> iCommand, final Object... iArgs) {
    checkOpeness();

    convertParameters(iArgs);

    final List<ODocument> result = underlying.query(iCommand, iArgs);

    if (result == null)
      return null;

    final List<Object> resultPojo = new ArrayList<Object>();
    Object obj;
    for (OIdentifiable doc : result) {
      if (doc instanceof ODocument) {
        // GET THE ASSOCIATED DOCUMENT
        if (((ODocument) doc).getClassName() == null)
          obj = doc;
        else
          obj = getUserObjectByRecord(((ODocument) doc), iCommand.getFetchPlan(), true);

        resultPojo.add(obj);
      } else {
        resultPojo.add(doc);
      }

    }

    return (RET) resultPojo;
  }

  public ODatabaseComplex<T> delete(final ORecordInternal<?> iRecord) {
    underlying.delete(iRecord);
    return this;
  }

  public ODatabaseComplex<T> delete(final ORID iRID) {
    underlying.delete(iRID);
    return this;
  }

  public ODatabaseComplex<T> delete(final ORID iRID, final ORecordVersion iVersion) {
    underlying.delete(iRID, iVersion);
    return this;
  }

  public ODatabaseComplex<T> cleanOutRecord(final ORID iRID, final ORecordVersion iVersion) {
    underlying.cleanOutRecord(iRID, iVersion);
    return this;
  }

  public <DBTYPE extends ODatabaseComplex<?>> DBTYPE registerHook(final ORecordHook iHookImpl) {
    underlying.registerHook(iHookImpl);
    return (DBTYPE) this;
  }

  public <DBTYPE extends ODatabaseComplex<?>> DBTYPE registerHook(final ORecordHook iHookImpl, ORecordHook.HOOK_POSITION iPosition) {
    underlying.registerHook(iHookImpl, iPosition);
    return (DBTYPE) this;
  }

  public RESULT callbackHooks(final TYPE iType, final OIdentifiable iObject) {
    return underlying.callbackHooks(iType, iObject);
  }

  public Map<ORecordHook, ORecordHook.HOOK_POSITION> getHooks() {
    return underlying.getHooks();
  }

  public <DBTYPE extends ODatabaseComplex<?>> DBTYPE unregisterHook(final ORecordHook iHookImpl) {
    underlying.unregisterHook(iHookImpl);
    return (DBTYPE) this;
  }

  public boolean isMVCC() {
    return underlying.isMVCC();
  }

  public <DBTYPE extends ODatabaseComplex<?>> DBTYPE setMVCC(final boolean iMvcc) {
    underlying.setMVCC(iMvcc);
    return (DBTYPE) this;
  }

  /**
   * Specifies if retain handled objects in memory or not. Setting it to false can improve performance on large inserts. Default is
   * enabled.
   * 
   * @param iValue
   *          True to enable, false to disable it.
   * @see #isRetainObjects()
   */
  public ODatabasePojoAbstract<T> setRetainObjects(final boolean iValue) {
    retainObjects = iValue;
    return this;
  }

  /**
   * Returns true if current configuration retains objects, otherwise false
   * 
   * @see #setRetainObjects(boolean)
   */
  public boolean isRetainObjects() {
    return retainObjects;
  }

  public ODocument getRecordByUserObject(final Object iPojo, final boolean iCreateIfNotAvailable) {
    if (iPojo instanceof ODocument)
      return (ODocument) iPojo;
    else if (iPojo instanceof Proxy)
      return ((OObjectProxyMethodHandler) ((ProxyObject) iPojo).getHandler()).getDoc();

    ODocument record = objects2Records.get(iPojo);
    if (record == null) {
      // SEARCH BY RID
      final ORID rid = OObjectSerializerHelper.getObjectID(this, iPojo);
      if (rid != null && rid.isValid()) {
        record = rid2Records.get(rid);
        if (record == null)
          // LOAD IT
          record = underlying.load(rid);
      } else if (iCreateIfNotAvailable) {
        record = underlying.newInstance(iPojo.getClass().getSimpleName());
      } else {
        return null;
      }

      registerUserObject(iPojo, record);
    }

    return record;
  }

  public boolean existsUserObjectByRID(ORID iRID) {
    return rid2Records.containsKey(iRID);
  }

  public ODocument getRecordById(final ORID iRecordId) {
    return iRecordId.isValid() ? rid2Records.get(iRecordId) : null;
  }

  public boolean isManaged(final Object iEntity) {
    return objects2Records.containsKey(iEntity);
  }

  public T getUserObjectByRecord(final OIdentifiable iRecord, final String iFetchPlan) {
    return getUserObjectByRecord(iRecord, iFetchPlan, true);
  }

  public T getUserObjectByRecord(final OIdentifiable iRecord, final String iFetchPlan, final boolean iCreate) {
    if (!(iRecord instanceof ODocument))
      return null;

    // PASS FOR rid2Records MAP BECAUSE IDENTITY COULD BE CHANGED IF WAS NEW AND IN TX
    ODocument record = rid2Records.get(iRecord.getIdentity());

    if (record == null)
      record = (ODocument) iRecord;

    Object pojo = records2Objects.get(record);

    if (pojo == null && iCreate) {
      checkOpeness();

      try {
        if (iRecord.getRecord().getInternalStatus() == ORecordElement.STATUS.NOT_LOADED)
          record = (ODocument) record.load();

        pojo = newInstance(record.getClassName());
        registerUserObject(pojo, record);

        stream2pojo(record, pojo, iFetchPlan);

      } catch (Exception e) {
        throw new OConfigurationException("Cannot retrieve pojo from record " + record, e);
      }
    }

    return (T) pojo;
  }

  public void attach(final Object iPojo) {
    checkOpeness();

    final ODocument record = objects2Records.get(iPojo);
    if (record != null)
      return;

    if (OObjectSerializerHelper.hasObjectID(iPojo)) {
    } else {
      throw new OObjectNotDetachedException("Cannot attach a non-detached object");
    }
  }

  public <RET> RET detach(final Object iPojo) {
    checkOpeness();

    for (Field field : iPojo.getClass().getDeclaredFields()) {
      final Object value = OObjectSerializerHelper.getFieldValue(iPojo, field.getName());
      if (value instanceof OObjectLazyMultivalueElement)
        ((OObjectLazyMultivalueElement<?>) value).detach(false);
    }

    return (RET) iPojo;
  }

  /**
   * Register a new POJO
   */
  public void registerUserObject(final Object iObject, final ORecordInternal<?> iRecord) {
    if (!(iRecord instanceof ODocument))
      return;

    final ODocument doc = (ODocument) iRecord;
    final boolean isTombstone = doc.getRecordVersion().isTombstone();
    if (retainObjects) {
      if (iObject != null) {
        if (!isTombstone) {
          objects2Records.put(iObject, doc);
          records2Objects.put(doc, (T) iObject);
        }

        OObjectSerializerHelper.setObjectID(iRecord.getIdentity(), iObject);
        OObjectSerializerHelper.setObjectVersion(iRecord.getRecordVersion().copy(), iObject);
      }

      final ORID rid = iRecord.getIdentity();
      if (rid.isValid() && !isTombstone)
        rid2Records.put(rid, doc);
    }
  }

  public void unregisterPojo(final T iObject, final ODocument iRecord) {
    if (iObject != null)
      objects2Records.remove(iObject);

    if (iRecord != null) {
      records2Objects.remove(iRecord);

      final ORID rid = iRecord.getIdentity();
      if (rid.isValid())
        rid2Records.remove(rid);
    }
  }

  protected void clearNewEntriesFromCache() {
    for (Iterator<Entry<ORID, ODocument>> it = rid2Records.entrySet().iterator(); it.hasNext();) {
      Entry<ORID, ODocument> entry = it.next();
      if (entry.getKey().isNew()) {
        it.remove();
      }
    }

    for (Iterator<Entry<Object, ODocument>> it = objects2Records.entrySet().iterator(); it.hasNext();) {
      Entry<Object, ODocument> entry = it.next();
      if (entry.getValue().getIdentity().isNew()) {
        it.remove();
      }
    }

    for (Iterator<Entry<ODocument, T>> it = records2Objects.entrySet().iterator(); it.hasNext();) {
      Entry<ODocument, T> entry = it.next();
      if (entry.getKey().getIdentity().isNew()) {
        it.remove();
      }
    }
  }

  /**
   * Converts an array of parameters: if a POJO is used, then replace it with its record id.
   * 
   * @param iArgs
   *          Array of parameters as Object
   * @see #convertParameter(Object)
   */
  protected void convertParameters(final Object... iArgs) {
    if (iArgs == null)
      return;

    // FILTER PARAMETERS
    for (int i = 0; i < iArgs.length; ++i)
      iArgs[i] = convertParameter(iArgs[i]);
  }

  /**
   * Convert a parameter: if a POJO is used, then replace it with its record id.
   * 
   * @param iParameter
   *          Parameter to convert, if applicable
   * @see #convertParameters(Object...)
   */
  protected Object convertParameter(final Object iParameter) {
    if (iParameter != null)
      // FILTER PARAMETERS
      if (iParameter instanceof Map<?, ?>) {
        Map<String, Object> map = (Map<String, Object>) iParameter;

        for (Entry<String, Object> e : map.entrySet()) {
          map.put(e.getKey(), convertParameter(e.getValue()));
        }

        return map;
      } else if (iParameter instanceof Collection<?>) {
        List<Object> result = new ArrayList<Object>();
        for (Object object : (Collection<Object>) iParameter) {
          result.add(convertParameter(object));
        }
        return result;
      } else if (iParameter.getClass().isEnum()) {
        return ((Enum<?>) iParameter).name();
      } else if (!OType.isSimpleType(iParameter)) {
        final ORID rid = getIdentity(iParameter);
        if (rid != null && rid.isValid())
          // REPLACE OBJECT INSTANCE WITH ITS RECORD ID
          return rid;
      }

    return iParameter;
  }
}
