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

import com.atlassian.bitbucket.comment.DiffCommentAnchor;
import com.atlassian.bitbucket.content.DiffSegmentType;
import com.atlassian.stash.internal.pull.InternalPullRequestDiffCommentAnchor;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.util.MoreCollectors.toImmutableList;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;

/**
 * Represents a drift result. Facilitates complex, state-dependent drift processing by allowing to record processed
 * anchors as:
 * <ul>
 *     <li>{@link #drift(int, Iterable)} drifted}</li>
 *     <li>{@link #orphan(Stream)} orphaned}</li>
 *     <li>{@link #retain(Iterable)} retained}</li>
 * </ul>
 * All the recorded changes and updates can then be {@link #applyTo(DriftContext) applied to} the current
 * {@code DriftContext}.
 * <p>
 * To merge multiple drift results, use {@link #merge(DriftResult)}.
 *
 * @since 2.11
 * @see DriftContext
 */
public class DriftResult {

    private final Map<Long, ProcessedAnchor> processedAnchors;

    public DriftResult() {
        processedAnchors = Maps.newHashMap();
    }

    private DriftResult(Map<Long, ProcessedAnchor> processedAnchors) {
        this.processedAnchors = processedAnchors;
    }

    public static DriftResult forRetained(@Nonnull Iterable<InternalPullRequestDiffCommentAnchor> retainedAnchors) {
        return new DriftResult().retain(retainedAnchors);
    }

    /**
     * Apply all the recorded anchors to {@code driftContext}.
     *
     * @param driftContext the drift context to apply to
     * @see ProcessedAnchor#applyTo(DriftContext)
     */
    public void applyTo(@Nonnull DriftContext driftContext) {
        Iterator<InternalPullRequestDiffCommentAnchor> anchorIterator = driftContext.iterator();
        while (anchorIterator.hasNext()) {
            InternalPullRequestDiffCommentAnchor anchor = anchorIterator.next();

            ProcessedAnchor processed = processedAnchors.get(anchor.getId());
            if (processed != null) {
                processed.applyTo(driftContext);
                anchorIterator.remove();
            }
        }
    }

    /**
     * Record all the {@code anchors} as drifted by the number of lines equal to {@code drift}.
     *
     * @param drift number of lines to drift by
     * @param anchors the anchors to record as drifted
     * @return this drift result
     */
    @Nonnull
    public DriftResult drift(int drift, @Nonnull Iterable<InternalPullRequestDiffCommentAnchor> anchors) {
        for (InternalPullRequestDiffCommentAnchor anchor : anchors) {
            record(anchor, new DriftedAnchor(anchor, drift));
        }

        return this;
    }

    /**
     * @param drift number of lines to drift by
     * @param anchor anchor to drift
     * @param more more anchors to drift
     * @return this drift result
     * @see #drift(int, Iterable)
     */
    @Nonnull
    public DriftResult drift(int drift, @Nonnull InternalPullRequestDiffCommentAnchor anchor,
                             @Nonnull InternalPullRequestDiffCommentAnchor... more) {
        return drift(drift, ImmutableList.<InternalPullRequestDiffCommentAnchor>builder()
                .add(anchor)
                .addAll(ImmutableList.copyOf(more))
                .build());
    }

    /**
     * @return all processed anchors that are not {@link OrphanedAnchor#FILTER orphaned}
     */
    @Nonnull
    public List<ProcessedAnchor> getReachableAnchors() {
        return getProcessedAnchors().stream()
                .filter(OrphanedAnchor.FILTER.negate())
                .collect(toImmutableList());
    }

    @Nonnull
    public DriftResult merge(@Nonnull DriftResult other) {
        checkNotNull(other, "other");

        Map<Long, ProcessedAnchor> merged = Maps.newHashMap(processedAnchors);
        for (Map.Entry<Long, ProcessedAnchor> otherAnchor : other.processedAnchors.entrySet()) {
            ProcessedAnchor displaced = merged.put(otherAnchor.getKey(), otherAnchor.getValue());
            if (displaced != null) {
                throw new IllegalStateException(
                        format("Anchor [%d] has been mapped to two different processing outcomes: %s and %s",
                                otherAnchor.getKey(),
                                otherAnchor.getValue(),
                                displaced));
            }
        }

        return new DriftResult(merged);
    }

    /**
     * Record all the {@code anchors} as orphaned.
     *
     * @param anchors anchors to orphan
     * @return this drift result
     */
    @Nonnull
    public DriftResult orphan(@Nonnull Stream<InternalPullRequestDiffCommentAnchor> anchors) {
        anchors.forEach(anchor -> record(anchor, new OrphanedAnchor(anchor)));

        return this;
    }

    /**
     * @param anchor anchor to orphan
     * @param more more anchors to orphan
     * @return this drift result
     * @see #orphan(Stream)
     */
    @Nonnull
    public DriftResult orphan(@Nonnull InternalPullRequestDiffCommentAnchor anchor,
                              @Nonnull InternalPullRequestDiffCommentAnchor... more) {
        return orphan(Stream.concat(Stream.of(anchor), Stream.of(more)));
    }

    /**
     * Record all the {@code anchors} as retained.
     *
     * @param anchors anchors to retain
     * @return this drift result
     */
    @Nonnull
    public DriftResult retain(@Nonnull Iterable<InternalPullRequestDiffCommentAnchor> anchors) {
        for (InternalPullRequestDiffCommentAnchor anchor : anchors) {
            record(anchor, new RetainedAnchor(anchor));
        }

        return this;
    }

    /**
     * @param anchor anchor to retain
     * @param more more anchors to retain
     * @return this drift result
     * @see #retain(Iterable)
     */
    @Nonnull
    public DriftResult retain(@Nonnull InternalPullRequestDiffCommentAnchor anchor,
                              @Nonnull InternalPullRequestDiffCommentAnchor... more) {
        return retain(ImmutableList.<InternalPullRequestDiffCommentAnchor>builder()
                .add(anchor)
                .addAll(ImmutableList.copyOf(more))
                .build());
    }

    /**
     * Record the {@code anchor} as indicated by {@code processedAnchor}. The IDs of {@code anchor} and
     * {@code processedAnchor} must match.
     *
     * @param anchor anchor to record
     * @param processedAnchor represents the state of the recorded anchor: drifted, orphaned or retained
     * @return this drift result
     * @throws java.lang.IllegalArgumentException if the IDs of {@code anchor} and  {@code processedAnchor} do not match
     */
    @Nonnull
    public DriftResult record(@Nonnull InternalPullRequestDiffCommentAnchor anchor,
                              @Nonnull ProcessedAnchor processedAnchor) {
        checkNotNull(anchor, "anchor");
        checkNotNull(processedAnchor, "processedAnchor");
        checkNotNull(anchor.getId(), "anchor.id");
        checkArgument(anchor.getId() == processedAnchor.getAnchor().getId(), "Cannot map processed anchor to an"
                + " anchor with a different ID (was: anchor.id=" + anchor.getId() + ",processedAnchor.id="
                + processedAnchor.getAnchor().getId());

        processedAnchors.put(anchor.getId(), processedAnchor);

        return this;
    }

    private List<ProcessedAnchor> getProcessedAnchors() {
        return ImmutableList.copyOf(processedAnchors.values());
    }


    /**
     * A {@link ProcessedAnchor} that should be {@link DriftContext#drift(InternalPullRequestDiffCommentAnchor, int)
     * drifted}.
     */
    public static class DriftedAnchor extends ProcessedAnchor {

        private final int drift;

        private DriftedAnchor(@Nonnull InternalPullRequestDiffCommentAnchor anchor, int drift) {
            super(anchor);

            this.drift = drift;
        }

        public int getLine() {
            return super.getLine() + drift;
        }

        /**
         * {@link DriftContext#drift(InternalPullRequestDiffCommentAnchor, int) Drifts} the anchor.
         *
         * @param context the drift context
         */
        @Override
        public void applyTo(@Nonnull DriftContext context) {
            context.drift(anchor, drift);
        }
    }

    /**
     * A {@link ProcessedAnchor} that should be {@link DriftContext#orphan(InternalPullRequestDiffCommentAnchor)
     * orphaned}.
     */
    public static class OrphanedAnchor extends ProcessedAnchor {

        public static final Predicate<ProcessedAnchor> FILTER = OrphanedAnchor.class::isInstance;

        private OrphanedAnchor(@Nonnull InternalPullRequestDiffCommentAnchor anchor) {
            super(anchor);
        }

        /**
         * {@link DriftContext#orphan(InternalPullRequestDiffCommentAnchor) Orphans} the anchor.
         *
         * @param context the drift context
         */
        @Override
        public void applyTo(@Nonnull DriftContext context) {
            context.orphan(anchor);
        }
    }

    /**
     * Encapsulates the processing that should be applied to an {@link InternalPullRequestDiffCommentAnchor anchor}.
     * <p>
     * Derived implementations must implement {@link #applyTo(DriftContext)} to apply the correct processing to the
     * anchor in the context.
     */
    public abstract static class ProcessedAnchor {

        protected final InternalPullRequestDiffCommentAnchor anchor;

        protected ProcessedAnchor(@Nonnull InternalPullRequestDiffCommentAnchor anchor) {
            this.anchor = checkNotNull(anchor, "anchor");
        }

        /**
         * Applies the processing for this anchor to the {@link DriftContext context}.
         * <p>
         * Note: This method should be called exactly <i>once</i>.
         *
         * @param context the context to apply the processing to
         */
        public abstract void applyTo(@Nonnull DriftContext context);

        @Nonnull
        public InternalPullRequestDiffCommentAnchor getAnchor() {
            return anchor;
        }

        public int getLine() {
            return anchor.getLine();
        }

        /**
         * @return anchor line type
         * @see DiffCommentAnchor#getLineType()
         */
        @Nullable
        public DiffSegmentType getLineType() {
            return anchor.getLineType();
        }

        /**
         * @return anchor path
         * @see DiffCommentAnchor#getPath()
         */
        @Nonnull
        public String getPath() {
            return anchor.getPath();
        }

        /**
         * @return anchor {@code srcPath}
         * @see DiffCommentAnchor#getSrcPath()
         */
        @Nullable
        public String getSrcPath() {
            return anchor.getSrcPath();
        }
    }

    /**
     * A {@link ProcessedAnchor} that should be {@link DriftContext#retain(InternalPullRequestDiffCommentAnchor)
     * retained}.
     */
    public static class RetainedAnchor extends ProcessedAnchor {

        private RetainedAnchor(@Nonnull InternalPullRequestDiffCommentAnchor anchor) {
            super(anchor);
        }

        /**
         * {@link DriftContext#retain(InternalPullRequestDiffCommentAnchor) Retains} the anchor.
         *
         * @param context the context for processing comment drift
         */
        @Override
        public void applyTo(@Nonnull DriftContext context) {
            context.retain(anchor);
        }
    }
}
