package com.atlassian.stash.internal.pull;

import com.atlassian.bitbucket.pull.PullRequestCommentAnchorDiffType;
import com.atlassian.stash.internal.comment.CommentCounts;
import com.atlassian.stash.internal.content.InternalChangeLocation;
import com.google.common.collect.Maps;
import org.hibernate.Criteria;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import javax.annotation.Nonnull;
import java.util.*;

/**
 * @since 1.3
 */
@Repository("pullRequestCommentAnchorDao")
public class HibernatePullRequestCommentAnchorDao
        extends AbstractHibernateDiffCommentAnchorDao<InternalPullRequest, InternalPullRequestDiffCommentAnchor>
        implements PullRequestCommentAnchorDao {

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

    @Nonnull
    @Override
    public Map<InternalChangeLocation, CommentCounts> getCountsByLocation(@Nonnull InternalPullRequest pullRequest) {
        @SuppressWarnings("unchecked")
        List<Object[]> results = session().createQuery(
                "select path, srcPath, orphaned, count(*) " +
                        "from InternalPullRequestDiffCommentAnchor " +
                        "where pullRequest.id = :pullRequestId " +
                        "and diffType = :diffType " +
                        "group by path, srcPath, orphaned " +
                        "order by path, srcPath")
                .setLong("pullRequestId", pullRequest.getGlobalId())
                .setParameter("diffType", PullRequestCommentAnchorDiffType.EFFECTIVE)
                .list();
        if (results.isEmpty()) {
            return Collections.emptyMap();
        }
        return mapCounts(results);
    }

    @Nonnull
    @Override
    @SuppressWarnings("unchecked")
    public Collection<InternalPullRequestDiffCommentAnchor> findByCommentIds(@Nonnull Set<Long> commentIds) {
        if (commentIds.isEmpty()) {
            return Collections.emptyList();
        }

        Criteria criteria = session().createCriteria(InternalPullRequestDiffCommentAnchor.class)
                .add(Restrictions.in("comment.id", commentIds));

        return criteria.list();
    }

    @Override
    @Nonnull
    public List<InternalPullRequestDiffCommentAnchor> search(@Nonnull PullRequestAnchorSearchCriteria searchCriteria) {
        Criteria criteria = session().createCriteria(InternalPullRequestDiffCommentAnchor.class)
                .add(Restrictions.eq("pullRequest.id", searchCriteria.getPullRequest().getGlobalId()))
                .add(Restrictions.eq("orphaned", false));

        searchCriteria.getDiffType()
                .ifPresent(diffType -> criteria.add(Restrictions.eq("diffType", diffType)));

        return search(criteria, searchCriteria);
    }

    private static Map<InternalChangeLocation, CommentCounts> mapCounts(List<Object[]> results) {
        Map<InternalChangeLocation, CommentCounts> countsByLocation = Maps.newHashMapWithExpectedSize(results.size());
        for (int i = 0; i < results.size(); ++i) {
            CountHelper helper = new CountHelper(results.get(i));

            //There may be a second row with the same path to merge counts from. Pull requests can return two
            //rows, where one is the active count and the other is the orphaned count
            int next = i + 1;
            if (next < results.size() && helper.merge(results.get(next))) {
                //The next row is the "other" set of counts for the same pair of paths, and has been collapsed
                //up onto the same CommentCounts instance as the current row, so the next iteration of the loop
                //should not process it
                i = next; //Will increment again as part of the for loop
            }

            countsByLocation.put(helper.getLocation(), helper.getCounts());
        }

        return countsByLocation;
    }

    private static class CountHelper {

        private final String path;
        private final String srcPath;

        private int activeCount;
        private int orphanedCount;

        private CountHelper(Object[] columns) {
            this.path = (String) columns[0];
            this.srcPath = (String) columns[1];

            setCountFrom(columns);
        }

        public CommentCounts getCounts() {
            return new CommentCounts(activeCount, orphanedCount);
        }

        public InternalChangeLocation getLocation() {
            return new InternalChangeLocation.Builder().path(path).srcPath(srcPath).build();
        }

        public boolean merge(Object[] columns) {
            if (path.equals(columns[0]) && Objects.equals(srcPath, columns[1])) {
                setCountFrom(columns);

                return true;
            }
            return false;
        }

        public void setCountFrom(Object[] columns) {
            int count = ((Number) columns[3]).intValue();
            if ((Boolean) columns[2]) {
                orphanedCount = count;
            } else {
                activeCount = count;
            }
        }
    }
}
