package org.ovirt.engine.core.bll.context;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.ovirt.engine.core.common.businessentities.BusinessEntity;
import org.ovirt.engine.core.common.businessentities.BusinessEntitySnapshot;
import org.ovirt.engine.core.common.businessentities.BusinessEntitySnapshot.EntityStatusSnapshot;
import org.ovirt.engine.core.common.businessentities.BusinessEntitySnapshot.SnapshotType;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.dao.BusinessEntitySnapshotDAO;
import org.ovirt.engine.core.utils.Serializer;

/**
 * Default context used to track entities that are changing during a command's execution and save the changes at each
 * state change to the DB.
 */
public class DefaultCompensationContext implements CompensationContext {

    /**
     * A set of all the entities which have been snapshotted ever in this context, since we only want to save the
     * initial snapshot of each entity the comand has changed/created.
     */
    private Set<CachedEntityEntry> cachedEntities = new HashSet<CachedEntityEntry>();

    /**
     * All the entities that have been changed/added since the state/command began.
     */
    private List<BusinessEntitySnapshot> entitiesToPersist = new LinkedList<BusinessEntitySnapshot>();

    /**
     * The serializer which is used to convert the entity to a snapshot.
     */
    private Serializer snapshotSerializer;

    /**
     * The DAO which is used to track all the changed business entities.
     */
    private BusinessEntitySnapshotDAO businessEntitySnapshotDAO;

    /**
     * The id of the command which this context is tracking.
     */
    private Guid commandId;

    /**
     * The type of the command which this context is tracking.
     */
    private String commandType;

    /**
     * @param snapshotSerializer
     *            the snapshotSerializer to set
     */
    public void setSnapshotSerializer(Serializer snapshotSerializer) {
        this.snapshotSerializer = snapshotSerializer;
    }

    /**
     * @param businessEntitySnapshotDAO
     *            the businessEntitySnapshotDAO to set
     */
    public void setBusinessEntitySnapshotDAO(BusinessEntitySnapshotDAO businessEntitySnapshotDAO) {
        this.businessEntitySnapshotDAO = businessEntitySnapshotDAO;
    }

    /**
     * @param commandId
     *            the commandId to set
     */
    public void setCommandId(Guid commandId) {
        this.commandId = commandId;
    }

    /**
     * @param commandType
     *            the commandType to set
     */
    public void setCommandType(String commandType) {
        this.commandType = commandType;
    }

    @Override
    public void snapshotEntity(BusinessEntity<?> entity) {
        snapshotEntityInMemory(entity, entity, SnapshotType.CHANGED_ENTITY);
    }

    @Override
    public void snapshotNewEntity(BusinessEntity<?> entity) {
        snapshotEntityInMemory(entity, entity.getId(), SnapshotType.NEW_ENTITY_ID);
    }

    @Override
    public void snapshotEntityStatus(BusinessEntity<?> entity, Enum<?> status) {
        EntityStatusSnapshot snapshot = new EntityStatusSnapshot();
        snapshot.setId(entity.getId());
        snapshot.setStatus(status);
        snapshotEntityInMemory(entity, snapshot, SnapshotType.CHANGED_STATUS_ONLY);
    }

    /**
     * Save a snapshot of the entity but only if it is new to this context.
     *
     * @param entity
     *            The entity to save a snapshot of.
     * @param payload
     *            The payload to be serialized and saved.
     * @param snapshotType
     *            The type of snapshot we're taking, so that in compensation we know what is the payload type, and how
     *            to use it to revert the entity state.
     */
    private void snapshotEntityInMemory(BusinessEntity<?> entity, Serializable payload, SnapshotType snapshotType) {
        CachedEntityEntry cachedEntityEntry = new CachedEntityEntry(entity, snapshotType);
        if (!cachedEntities.contains(cachedEntityEntry)) {
            cachedEntities.add(cachedEntityEntry);

            BusinessEntitySnapshot entitySnapshot = new BusinessEntitySnapshot();
            entitySnapshot.setCommandId(commandId);
            entitySnapshot.setCommandType(commandType);
            entitySnapshot.setEntityId(String.valueOf(entity.getId()));
            entitySnapshot.setEntityType(entity.getClass().getName());
            entitySnapshot.setEntitySnapshot((String) snapshotSerializer.serialize(payload));
            entitySnapshot.setSnapshotClass(payload.getClass().getName());
            entitySnapshot.setSnapshotType(snapshotType);
            entitySnapshot.setInsertionOrder(cachedEntities.size());

            entitiesToPersist.add(entitySnapshot);
        }
    }

    @Override
    public void stateChanged() {
        for (BusinessEntitySnapshot snapshot : entitiesToPersist) {
            businessEntitySnapshotDAO.save(snapshot);
        }

        entitiesToPersist.clear();
    }

    @Override
    public void resetCompensation() {
        businessEntitySnapshotDAO.removeAllForCommandId(commandId);
        cachedEntities.clear();
        entitiesToPersist.clear();
    }

    /* -- Inner types -- */

    /**
     * Represents a cached entity which is made of the snapshot type, the entity class and the id, so that we can track
     * which entities have already been recorded and which entities have not.
     */
    private class CachedEntityEntry {

        /**
         * The id of the cached entity.
         */
        private Object id;

        /**
         * The class of the cached entity.
         */
        private Class<?> entityClass;

        /**
         * The type of snapshot that is cached.
         */
        private SnapshotType snapshotType;

        /**
         * Construct a new cached entry for the given entity.
         *
         * @param entity
         *            The entity to construct a cache entry for.
         * @param snapshotType
         *            The type of snapshot.
         */
        public CachedEntityEntry(BusinessEntity<?> entity, SnapshotType snapshotType) {
            super();
            this.id = entity.getId();
            this.entityClass = entity.getClass();
            this.snapshotType = snapshotType;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime * result + ((entityClass == null) ? 0 : entityClass.hashCode());
            result = prime * result + ((id == null) ? 0 : id.hashCode());
            result = prime * result + ((snapshotType == null) ? 0 : snapshotType.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            CachedEntityEntry other = (CachedEntityEntry) obj;
            if (!getOuterType().equals(other.getOuterType())) {
                return false;
            }
            if (entityClass == null) {
                if (other.entityClass != null) {
                    return false;
                }
            } else if (!entityClass.equals(other.entityClass)) {
                return false;
            }
            if (id == null) {
                if (other.id != null) {
                    return false;
                }
            } else if (!id.equals(other.id)) {
                return false;
            }
            if (snapshotType != other.snapshotType) {
                return false;
            }
            return true;
        }

        private DefaultCompensationContext getOuterType() {
            return DefaultCompensationContext.this;
        }
    }

    @Override
    public void snapshotEntities(Collection<? extends BusinessEntity<?>> entities) {
        if (entities != null) {
            for (BusinessEntity<?> entity : entities) {
                snapshotEntity(entity);
            }
        }
    }

    @Override
    public void snapshotNewEntities(Collection<? extends BusinessEntity<?>> entities) {
        if (entities != null) {
            for (BusinessEntity<?> entity : entities) {
                snapshotNewEntity(entity);
            }
        }
    }
}
