package com.atlassian.stash.internal.crowd;

import com.atlassian.crowd.dao.group.GroupDAOHibernate;
import com.atlassian.crowd.dao.group.InternalGroupDao;
import com.atlassian.crowd.embedded.spi.GroupDao;
import com.atlassian.crowd.exception.GroupNotFoundException;
import com.atlassian.crowd.exception.ObjectNotFoundException;
import com.atlassian.crowd.model.directory.DirectoryImpl;
import com.atlassian.crowd.model.group.InternalGroup;
import com.atlassian.crowd.model.group.InternalGroupAttribute;
import com.atlassian.crowd.model.group.InternalGroupWithAttributes;
import com.atlassian.crowd.search.query.entity.EntityQuery;
import org.hibernate.Session;

import java.util.Collection;
import java.util.List;
import java.util.Set;

/**
 * Extension of Crowd's {@link GroupDAOHibernate} that walks all lazy loaded references on {@link InternalGroup} 
 * instances before returning them. Since Stash does not use the open-session-in-view pattern, without this code 
 * {@link org.hibernate.LazyInitializationException} would occur when trying access one of the lazy loaded fields 
 * outside of a transaction.
 *
 * @since 1.3
 */
public class HibernateGroupDao extends GroupDAOHibernate implements GroupDao, InternalGroupDao {

    @Override
    public InternalGroup findByName(long directoryId, String groupName) throws GroupNotFoundException {
        return initialize(super.findByName(directoryId, groupName));
    }

    @Override
    public Collection<InternalGroup> findByNames(long directoryID, Collection<String> groupnames) {
        return detach(super.findByNames(directoryID, groupnames));
    }

    @Override
    public InternalGroupWithAttributes findByNameWithAttributes(long directoryId, String groupName) throws GroupNotFoundException {
        InternalGroupWithAttributes result = super.findByNameWithAttributes(directoryId, groupName);
        initialize(result.getInternalGroup());

        return result;
    }

    @Override
    public Set<InternalGroupAttribute> findGroupAttributes(long groupId) {
        Set<InternalGroupAttribute> result =  super.findGroupAttributes(groupId);
        for (InternalGroupAttribute attribute : result) {
            HibernateDirectoryDao.initialize(DirectoryImpl.class.cast(attribute.getDirectory()));
            initialize(attribute.getGroup());
        }

        return result;
    }

    @Override
    public <T> List<T> search(long directoryId, EntityQuery<T> query) {
        List<T> result = super.search(directoryId, query);
        for (T item : result) {
            if (item instanceof InternalGroup) {
                initialize(InternalGroup.class.cast(item));
            } else if (item instanceof InternalGroupWithAttributes) {
                initialize(InternalGroupWithAttributes.class.cast(item).getInternalGroup());
            }
        }
        return result;
    }

    @Override
    public Object load(long ID) throws ObjectNotFoundException {
        return initialize(InternalGroup.class.cast(super.load(ID)));
    }

    @Override
    public Object loadReference(long id) {
        return initialize(InternalGroup.class.cast(super.loadReference(id)));
    }

    /**
     * Initializes all lazy loaded fields on an {@link InternalGroup} instance 
     * 
     * @param group the group to initialize
     * @return the provided group, returned for convenience.
     */
    static InternalGroup initialize(InternalGroup group) {
        if (group != null) {
            HibernateDirectoryDao.initialize((DirectoryImpl) group.getDirectory());
        }
        return group;
    }

    /**
     * Initializes all lazy loaded fields on collection of groups and then evicts them from the {@code Session}.
     * Evicting the groups mimics the behavior of the default Crowd {@code Hibernate4BatchFinder}, which creates
     * a session, finds groups and then closes that session--effectively detaching all of the returned groups.
     *
     * @param groups the groups to initialize and evict
     * @return the provided groups, returned for convenience
     * @since 3.5
     */
    Collection<InternalGroup> detach(Collection<InternalGroup> groups) {
        if (groups != null) {
            Session session = session();
            for (InternalGroup group : groups) {
                initialize(group);

                session.evict(group);
            }
        }
        return groups;
    }
}
