package com.atlassian.stash.internal;

import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.plugin.util.ClassUtils;
import com.atlassian.plugin.util.collect.Consumer;
import com.atlassian.stash.internal.hibernate.HibernatePageUtils;
import org.hibernate.*;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.dialect.HSQLDialect;
import org.hibernate.engine.spi.SessionFactoryImplementor;

import javax.annotation.Nonnull;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

public abstract class AbstractHibernateDao<K extends Serializable, E> implements Dao<K, E> {

    /**
     * Defines the maximum number of entities to retrieve in a single pass when deleting entities one-by-one on HSQL.
     *
     * @see #maybeBulkDelete(String)
     * @see #maybeBulkDelete(String, Map)
     */
    protected static final int DELETE_PAGE_SIZE = 500;
    
    protected final Class<E> entityClass;

    private final SessionFactory sessionFactory;

    @SuppressWarnings("unchecked")
    public AbstractHibernateDao(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
        entityClass = (Class<E>) ClassUtils.getTypeArguments(AbstractHibernateDao.class, getClass()).get(1);
    }

    @Override
    public E create(E entity) {
        session().saveOrUpdate(entity);
        return entity;
    }

    @Override
    public void delete(E entity) {
        session().delete(entity);
    }

    @Override
    public void deleteById(K id) {
        //Note: If an entity defines an explicit "id" property (as most of ours do), "id" will refer to that. If there
        //      _isn't_ an "id" field, "id" in HQL refers to the @Id (JPA or hbm.xml, doesn't matter) field or fields
        //      of the entity. For entities with a composite @Id, _this still works_. That means there is never a need
        //      to loadById to delete an object; if we have the @Id we can always delete with HQL directly
        //
        //noinspection JpaQlInspection
        session().createQuery("delete from " + entityClass.getSimpleName() + " where id = :id")
                .setParameter("id", id)
                .executeUpdate();
    }

    @Nonnull
    @Override
    public Page<E> findAll(PageRequest pageRequest) {
        Criteria criteria = session().createCriteria(entityClass);

        return pageCriteria(applyImplicitOrder(criteria), pageRequest);
    }

    @Nonnull
    @Override
    public Page<E> findAll(PageRequest pageRequest, Predicate<? super E> predicate) {
        Criteria criteria = session().createCriteria(entityClass);

        return pageCriteria(applyImplicitOrder(criteria), pageRequest, predicate);
    }

    @Override
    public E getById(K id) {
        return entityClass.cast(session().get(entityClass, id));
    }

    @Nonnull
    @Override
    @SuppressWarnings("unchecked")
    public List<E> getByIds(@Nonnull Collection<K> ids) {
        //"id" here functions the same it does in HQL and will use the entity's @Id mapping regardless of the
        //name of the actual field on the entity. See HibernateIndexerStateDaoTest
        return applyImplicitOrder(session().createCriteria(entityClass))
                .add(Restrictions.in("id", ids))
                .list();
    }

    @Override
    public E loadById(K id) {
        return entityClass.cast(session().load(entityClass, id));
    }

    @Override
    public void refresh(E entity) {
        session().refresh(entity);
    }

    @Override
    public E update(E entity) {
        return entityClass.cast(session().merge(entity));
    }

    protected Criteria applyImplicitOrder(Criteria criteria) {
        for (Order order : getImplicitOrder()) {
            criteria.addOrder(order);
        }

        return criteria;
    }

    protected Criteria createCriteria() {
        return session().createCriteria(entityClass);
    }

    protected abstract Iterable<Order> getImplicitOrder();

    protected boolean isHsql() {
        return ((SessionFactoryImplementor) sessionFactory).getDialect() instanceof HSQLDialect;
    }

    /**
     * Deletes the entities matching the provided HQL, using a bulk delete if possible and individual deletions on
     * HSQL. HSQL has quirks that result in partially-deleted or missed entities when performing bulk deletions on
     * hierarchical entities.
     * <p>
     * <b>Note</b>: This method is <i>only</i> necessary for hierarchical entities like activities. Straight entities
     * can <i>always</i> be bulk deleted using simple HQL.
     *
     * @param hql <i>base</i> HQL that can be run as a <i>query</i> to select entities to delete
     * @return the number of entities deleted
     * @since 3.0
     * @since 2.12.1
     * @since 2.11.6
     * @since 2.10.4
     */
    protected int maybeBulkDelete(String hql) {
        return maybeBulkDelete(hql, Collections.<String, Object>emptyMap());
    }

    /**
     * Deletes the entities matching the provided HQL, using a bulk delete if possible and individual deletions on
     * HSQL. HSQL has quirks that result in partially-deleted or missed entities when performing bulk deletions on
     * hierarchical entities.
     * <p>
     * <b>Note</b>: This method is <i>only</i> necessary for hierarchical entities like activities. Straight entities
     * can <i>always</i> be bulk deleted using simple HQL.
     *
     * @param hql        <i>base</i> HQL that can be run as a <i>query</i> to select entities to delete
     * @param parameters a map containing zero or more parameters to set on the query prior to executing it
     * @return the number of entities deleted
     * @since 3.0
     * @since 2.12.1
     * @since 2.11.6
     * @since 2.10.4
     */
    protected int maybeBulkDelete(String hql, Map<String, Object> parameters) {
        Session session = session();
        if (isHsql()) {
            //HSQL seems to have issues with partially-deleting hierarchical entities when using a bulk delete. To
            //work around that, delete entities individually
            int deleted = 0;

            Query query = setParameters(session.createQuery(hql).setMaxResults(DELETE_PAGE_SIZE), parameters);
            while (true) {
                List<?> entities = query.list();
                if (entities.isEmpty()) {
                    break;
                }

                for (Object entity : entities) {
                    session.delete(entity);
                    ++deleted;
                }
                session.flush(); //Flush the session before clearing to ensure updates are not lost
                session.clear(); //After each "page", clear the session to keep the L1 cache from bloating

                if (entities.size() < DELETE_PAGE_SIZE) {
                    //If the page wasn't full, there won't be any more entities to delete; skip the query
                    break;
                }
            }

            return deleted;
        }

        return setParameters(session.createQuery("delete " + hql), parameters).executeUpdate();
    }

    protected Page<E> pageCriteria(Criteria criteria, PageRequest request, Predicate<? super E> predicate) {
        return HibernatePageUtils.pageCriteria(criteria, request, predicate);
    }

    protected Page<E> pageCriteria(Criteria criteria, PageRequest pageRequest) {
        return HibernatePageUtils.pageCriteria(criteria, pageRequest);
    }

    protected Page<E> pageQuery(Query query, PageRequest request, Predicate<? super E> predicate) {
        return HibernatePageUtils.pageQuery(query, request, predicate);
    }

    protected Page<E> pageQuery(Query query, PageRequest request) {
        return HibernatePageUtils.pageQuery(query, request);
    }

    protected void scroll(Query query, Consumer<ScrollableResults> resultsConsumer) {
        ScrollableResults result = query.scroll();
        try {
            while (result.next()) {
                resultsConsumer.consume(result);
            }
        } finally {
            result.close();
        }
    }

    protected Session session() {
        return sessionFactory.getCurrentSession();
    }

    protected Query setParameters(Query query, Map<String, Object> parameters) {
        parameters.forEach(query::setParameter);

        return query;
    }
}
