package com.atlassian.stash.internal.project;

import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.project.ProjectSearchRequest;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.stash.internal.AbstractHibernateDao;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.repository.InternalRepository;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

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

import static com.atlassian.bitbucket.util.Chainable.chain;
import static com.atlassian.stash.internal.HibernateUtils.*;

@Repository("projectDao")
public class HibernateProjectDao extends AbstractHibernateDao<Integer, InternalProject> implements ProjectDao {

    @Autowired
    public HibernateProjectDao(SessionFactory sessionFactory) {
        super(sessionFactory);
    }

    @Override
    public long countAll() {
        return count(session().createCriteria(InternalProject.class));
    }

    @Override
    public InternalPersonalProject createPersonal(@Nonnull InternalPersonalProject project) {
        create(project);

        return project;
    }

    @Nonnull
    @Override
    public Page<InternalProject> findAll(PageRequest pageRequest) {
        return findAll(pageRequest, null);
    }

    @Nonnull
    @Override
    public Page<InternalProject> findAll(PageRequest pageRequest, Predicate<? super InternalProject> predicate) {
        Criteria criteria = createBaseCriteria();

        //These pages are not initialized because they return only InternalNormalProjects, which don't need it
        if (predicate == null) {
            return pageCriteria(criteria, pageRequest);
        }
        return pageCriteria(criteria, pageRequest, predicate);
    }

    @Nonnull
    @Override
    @SuppressWarnings("unchecked")
    public Iterable<String> findAllKeys(@Nonnull Predicate<? super InternalProject> predicate) {
        Query query = session().createQuery(
                "select new com.atlassian.stash.internal.project.ProjectIdKeyWrapper(p.id, p.key, p.publiclyAccessible) " +
                        "from InternalNormalProject p " +
                        "order by p.key");
        List<InternalProject> projects = query.list();
        return chain(projects).filter(predicate).transform(Project::getKey);
    }

    @Override
    public InternalProject getById(Integer id) {
        return initialize(super.getById(id));
    }

    @Nonnull
    @Override
    public List<InternalProject> getByIds(@Nonnull Collection<Integer> ids) {
        return initializeList(super.getByIds(ids));
    }

    @Override
    public InternalProject getByKey(String namespace, @Nonnull String key) {
        return initialize((InternalProject) session().createQuery(
                "from InternalProject where namespace = :namespace AND upper(key) = upper(:projectKey)")
                .setParameter("namespace", InternalProject.namespaceOf(namespace))
                .setParameter("projectKey", key)
                .setCacheable(true)
                .setCacheRegion(ApplicationConstants.QUERY_CACHE_PROJECTS)
                .uniqueResult());
    }

    @Override
    public InternalProject getByName(String namespace, @Nonnull String name) {
        return initialize((InternalProject) session().createQuery(
                "from InternalProject where namespace = :namespace AND upper(name) = upper(:name)")
                .setParameter("namespace", InternalProject.namespaceOf(namespace))
                .setParameter("name", name)
                .uniqueResult());
    }

    @Override
    public InternalPersonalProject getByOwner(int ownerId) {
        return initialize((InternalPersonalProject) session().createCriteria(InternalPersonalProject.class)
                .add(Restrictions.eq("owner.id", ownerId))
                .uniqueResult());
    }

    @Override
    public InternalPersonalProject getByUsername(String username) {
        return initialize((InternalPersonalProject) session().createCriteria(InternalPersonalProject.class)
                .createAlias("owner", "o")
                .add(Restrictions.eq("o.username", IdentifierUtils.toLowerCase(username)))
                .uniqueResult());
    }

    @Override
    public boolean hasRepositories(int projectId) {
        return count(session().createCriteria(InternalRepository.class)
                .add(Restrictions.eq("project.id", projectId))) != 0L;
    }

    @Nonnull
    @Override
    public Page<InternalProject> search(@Nonnull ProjectSearchRequest searchRequest,
                                        @Nonnull PageRequest pageRequest,
                                        @Nonnull Predicate<? super InternalProject> predicate) {
        Criteria criteria = createBaseCriteria();
        if (searchRequest.hasName()) {
            //noinspection ConstantConditions
            criteria.add(Restrictions.ilike("name", searchRequest.getName(), MatchMode.ANYWHERE));
        }

        return pageCriteria(criteria, pageRequest, predicate);
    }

    @Override
    protected Iterable<Order> getImplicitOrder() {
        return Collections.singleton(Order.asc("name").ignoreCase());
    }

    private Criteria createBaseCriteria() {
        return applyImplicitOrder(session().createCriteria(InternalNormalProject.class));
    }
}
