package com.atlassian.stash.internal.pull.comment.diff;

import com.atlassian.bitbucket.comment.DiffCommentAnchor;
import com.atlassian.bitbucket.content.*;
import com.atlassian.stash.internal.pull.InternalPullRequestCommentActivity;
import com.atlassian.bitbucket.property.PropertyMap;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import java.util.Map;
import java.util.Set;

/**
 * An implementation of {@link DiffContentCallback} for generating {@link DiffContext diff context} for a set of
 * {@link com.atlassian.stash.internal.pull.InternalPullRequestCommentActivity comment activities}.
 * <p>
 * Activities are divided into {@link DiffContextBatch batches} based on the path that will produce their diff. This
 * callback allows multiple diffs to be streamed through it and chooses the in-progress batch from the paths provided
 * when a new diff starts.
 */
public class CommentContextDiffContentCallback extends AbstractDiffContentCallback {

    private final Map<String, DiffContextBatch> batchesByPath;

    /**
     * The working batch, which may be {@code null} if no batch matches the diff that is currently streaming.
     */
    private DiffContextBatch batch;

    public CommentContextDiffContentCallback(@Nonnull Set<InternalPullRequestCommentActivity> activities,
                                             @Nonnull PropertyMap properties) {
        batchesByPath = mapBatches(activities, properties);
    }

    @Override
    public void onDiffEnd(boolean truncated) {
        if (batch != null) {
            batch.endDiff();
            batch = null;
        }
    }

    @Override
    public void onDiffStart(Path src, Path dst) {
        //If the destination is null, it means this is a deletion and every line will show as removed. That's the only
        //time when the path on the comment will be the source's.
        String path = dst == null ? src.toString() : dst.toString();

        batch = batchesByPath.get(path);
    }

    @Override
    public void onHunkEnd(boolean truncated) {
        if (batch != null) {
            batch.endHunk();
        }
    }

    @Override
    public void onHunkStart(int srcLine, int srcSpan, int dstLine, int dstSpan) {
        if (batch != null) {
            batch.startHunk(srcLine, dstLine);
        }
    }

    @Override
    public void onSegmentLine(@Nonnull String line, ConflictMarker marker, boolean truncated) {
        if (batch != null) {
            batch.processLine(line, marker, truncated);
        }
    }

    @Override
    public void onSegmentStart(@Nonnull DiffSegmentType type) {
        if (batch != null) {
            batch.startSegment(type);
        }
    }

    /**
     * Maps {@link DiffContextBatch batches} based on the {@link DiffCommentAnchor#getPath() paths} on the anchors
     * for the provided {@link InternalPullRequestCommentActivity comment activities}.
     * <p>
     * It is assumed that calling code has preemptively sifted the activities and is only including activities that:
     * <ul>
     *     <li>Have anchors</li>
     *     <li>Are not {@link DiffCommentAnchor#isFileComment() file comments}</li>
     *     <li>Have {@link DiffCommentAnchor#getFromHash() from hashes}</li>
     * </ul>
     * As a result, the returned map is never {@code null} <i>or empty</i>.
     *
     * @param activities the activities to map into batches
     * @return a map from a diff path to a batch of comments which will draw context from that diff
     * @throws IllegalStateException if no batches can be produced from the provided activities
     */
    @Nonnull
    private static Map<String, DiffContextBatch> mapBatches(Set<InternalPullRequestCommentActivity> activities,
                                                            final PropertyMap properties) {
        Map<String, Set<InternalPullRequestCommentActivity>> activitiesByPath = Maps.newHashMap();
        for (InternalPullRequestCommentActivity activity : activities) {
            DiffCommentAnchor anchor = activity.getCommentAnchor();
            if (anchor == null || anchor.isFileComment() || StringUtils.isBlank(anchor.getFromHash())) {
                //General comments and file comments do not need diff context, so they are not considered.
                //Anchors without a from hash do not need a diff to generate their context, so they are not considered.
                continue;
            }

            Set<InternalPullRequestCommentActivity> byPath = activitiesByPath.get(anchor.getPath());
            if (byPath == null) {
                byPath = Sets.newHashSet();

                activitiesByPath.put(anchor.getPath(), byPath);
            }
            byPath.add(activity);
        }

        if (activitiesByPath.isEmpty()) {
            throw new IllegalStateException("No line-anchored comments were found in the provided activities. " +
                    "Performing a diff to gather context is not necessary");
        }
        return ImmutableMap.copyOf(
                Maps.transformValues(activitiesByPath,
                        new Function<Set<InternalPullRequestCommentActivity>, DiffContextBatch>() {

                            @Override
                            public DiffContextBatch apply(Set<InternalPullRequestCommentActivity> activities) {
                                return new DiffContextBatch(activities, properties);
                            }
                        }));
    }
}
