package com.breeze.hib;

import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;


import org.hibernate.FlushMode;
import org.hibernate.JDBCException;
import org.hibernate.PropertyValueException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.metadata.ClassMetadata;

import com.breeze.metadata.DataType;
import com.breeze.metadata.Metadata;
import com.breeze.save.*;

public class HibernateSaveProcessor extends SaveProcessor {

    private SessionFactory _sessionFactory;
    private Session _session;
    private RelationshipFixer _fixer;
    private List<String> _possibleErrors = new ArrayList<String>();
   

    /**
     * @param metadata
     * @param sessionFactory 
     */
    public HibernateSaveProcessor(Metadata metadata, SessionFactory sessionFactory) {
        super(metadata);
        _sessionFactory = sessionFactory;
    }

    /**
     * Persist the changes to the entities in the SaveWorkState that is passed in.
     * This implements the abstract method in SaveProcessor.
     * After the completion of this method the saveWorkState.toSaveResult method may be called the will
     * return a SaveResult instance.
     * This method both persists the entities and creates the collection of KeyMappings, 
     * which map the temporary keys to their real generated keys.
     * Note that this method sets session.FlushMode = FlushMode.MANUAL, so manual flushes are required.
     * @param saveWorkState
     */
    @Override
    protected void saveChangesCore(SaveWorkState saveWorkState) {
        _session = _sessionFactory.openSession();
        _session.setFlushMode(FlushMode.MANUAL);
        Transaction tx = _session.getTransaction();
        boolean hasExistingTransaction = tx.isActive();
        if (!hasExistingTransaction)  tx.begin();
        try {
            // Relate entities in the saveMap to other entities, so Hibernate can save the FK values.
            _fixer = new RelationshipFixer(saveWorkState, _session);
            _fixer.fixupRelationships();
            // At this point all entities are hooked up but are not yet in the session.
            setSaveState(SaveState.AfterFixup);
            // Allow subclass to process entities before we save them
            saveWorkState.beforeSaveEntities();
            List<EntityInfo> saveOrder = _fixer.sortDependencies();
            processSaves(saveOrder);

            // At this point all entities are hooked up and in the session, and
            // all tempIds have been replaced with real ids.

            // Final chance to process entities before we save them - all entities 
            // have been added to the session.
            setSaveState(SaveState.BeforeCommit);
            saveWorkState.beforeCommit(_session);

            _session.flush();
            refreshFromSession(saveWorkState);
            if (!hasExistingTransaction) tx.commit();
            // so that serialization of saveResult doesn't have issues.
            _fixer.removeRelationships();
        } catch (EntityErrorsException eee) {
            if (tx.isActive()) tx.rollback();
            throw eee;
        } catch (PropertyValueException pve) {
            // Hibernate can throw this
            if (tx.isActive()) tx.rollback();
            EntityError entityError = new EntityError("PropertyValueException", 
                    pve.getEntityName(), null,
                    pve.getPropertyName(), pve.getMessage());
            throw new EntityErrorsException(entityError);
        } catch (Exception ex) {
            if (tx.isActive()) tx.rollback();
            String msg = "Save exception: ";
            if (ex instanceof JDBCException) {
                msg += "SQLException: " + ((JDBCException) ex).getSQLException().getMessage();
            } else {
                msg += ex.getMessage(); // most hibernate exceptions appear here
            }
            if (_possibleErrors.size() > 0) {
                msg += ". Possible errors: " + _possibleErrors.toString() + " ";
            }
            throw new RuntimeException(msg, ex);
        } finally {
            //          if (!hasExistingTransaction) tx.Dispose();
            _session.close();
        }

    }
    
    @Override
    public void processRelationships(EntityInfo entityInfo, boolean removeMode) {
        // _fixer will not be initialized until just before beforeSaveEntities is called
        // so it will not be available for beforeSaveEntity calls.
        SaveState saveState = getSaveState();
        if (saveState == SaveState.BeforeFixup) return;
        _fixer.processRelationships(entityInfo, removeMode);
        if (saveState == SaveState.BeforeCommit) {
            processEntity(entityInfo);
        }
    }
    
    // TODO: determine why this is different from getIdentifier commented out below. 
    // May have to do with this method only getting called for single part keys.
    @Override
    public Object getIdentifier(Object entity) {
        Object id = getClassMetadata(entity.getClass()).getIdentifier(entity, null);
        return id != null ? id : null;
    }


    /**
     * Persist the changes to the entities in the saveOrder.
     * @param saveOrder
     */
    protected void processSaves(List<EntityInfo> saveOrder) {
        for (EntityInfo entityInfo : saveOrder) {
            processEntity(entityInfo);
        }
    }

    /**
     * Add, update, or delete the entity according to its EntityState.
     * @param entityInfo
     * @param classMeta
     */
    protected void processEntity(EntityInfo entityInfo) {
        Object entity = entityInfo.entity;
        ClassMetadata classMeta = getClassMetadata(entity.getClass());
        EntityState state = entityInfo.entityState;

        // Restore the old value of the concurrency column so Hibernate will be able to save the entity
        if (classMeta.isVersioned()) {
            restoreOldVersionValue(entityInfo, classMeta);
        }

        if (state == EntityState.Modified) {
            _session.update(entity);
        } else if (state == EntityState.Added) {
            _session.save(entity);
        } else if (state == EntityState.Deleted) {
            _session.delete(entity);
        } else {
            // Ignore EntityState.Unchanged.  Too many problems using session.Lock or session.Merge
            //session.Lock(entity, LockMode.None);
        }
    }


    /**
     * Restore the old value of the concurrency column so Hibernate will save the entity.
     * Otherwise it will complain because Breeze has already changed the value.
     * @param entityInfo
     * @param classMeta
     */
    protected void restoreOldVersionValue(EntityInfo entityInfo, ClassMetadata classMeta) {
        if (entityInfo.originalValuesMap == null || entityInfo.originalValuesMap.size() == 0)
            return;
        int vcol = classMeta.getVersionProperty();
        String vname = classMeta.getPropertyNames()[vcol];
        if (entityInfo.originalValuesMap.containsKey(vname)) {
            Object oldVersion = entityInfo.originalValuesMap.get(vname);
            Object entity = entityInfo.entity;
            if (oldVersion == null) {
                _possibleErrors.add("Hibernate does not support 'null' version properties. " +
                        "Entity: " + entity + ", Property: " + vname);
            }
            Class versionClazz = classMeta.getPropertyTypes()[vcol].getReturnedClass();
            DataType dataType = DataType.fromClass(versionClazz);
            Object oldValue = DataType.coerceData(oldVersion, dataType);
            classMeta.setPropertyValue(entity, vname, oldValue);
        }
    }


    /**
     * Refresh the entities from the database.  This picks up changes due to triggers, etc.
     * and makes Hibernate update the foreign keys.
     * @param saveMap
     */
    protected void refreshFromSession(SaveWorkState saveWorkState) {
        for (Entry<Class, List<EntityInfo>> entry : saveWorkState.entrySet()) {
            for (EntityInfo entityInfo : entry.getValue()) {
                if (entityInfo.entityState == EntityState.Added || entityInfo.entityState == EntityState.Modified) {
                    _session.refresh(entityInfo.entity);
                }
            }
        }

    }
    
    // perf 
    private Class _classCached;
    private ClassMetadata _classMetadataCached;

    private ClassMetadata getClassMetadata(Class clazz) {
        // perf enhancement - this method gets called a lot in loops.
        if (clazz != _classCached) {
            _classCached = clazz;
            _classMetadataCached = _sessionFactory.getClassMetadata(clazz);
        }
        return _classMetadataCached;
    }
    
//  /**
//  * Get the identifier value for the entity.  If the entity does not have an
//  * identifier property, or natural identifiers defined, then the entity itself is returned.
//  * @param entity
//  * @param meta
//  * @return
//  */
// protected Object getIdentifier(Object entity) {
//     Class type = entity.getClass();
//     meta = getClassMetadata(type);
//
//     Type idType = meta.getIdentifierType();
//     if (idType != null) {
//         Serializable id = meta.getIdentifier(entity, null);
//         if (idType.isComponentType()) {
//             ComponentType compType = (ComponentType) idType;
//             return compType.getPropertyValues(id, EntityMode.POJO);
//         } else {
//             return id;
//         }
//     } else if (meta.hasNaturalIdentifier()) {
//         int[] idprops = meta.getNaturalIdentifierProperties();
//         Object[] values = meta.getPropertyValues(entity);
//         Object[] idvalues = new Object[idprops.length];
//         for (int i = 0; i < idprops.length; i++) {
//             idvalues[i] = values[idprops[i]];
//         }
//         return idvalues;
//     }
//     return entity;
// }

    
}