package com.atlassian.bitbucket.mesh.git.builder.revlist;

import com.atlassian.bitbucket.mesh.git.GitChangeType;
import com.atlassian.bitbucket.mesh.git.commit.Commit;
import com.atlassian.bitbucket.mesh.git.content.MinimalChange;
import com.atlassian.bitbucket.mesh.process.ProcessStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.AbstractLineStdioHandler;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.nio.charset.StandardCharsets;

import static com.atlassian.bitbucket.mesh.git.GitUtils.unescapeQuoted;
import static com.google.common.base.Preconditions.checkArgument;

/**
 * A base class for building {@link ProcessStdioHandler} implementations which work with {@link Commit}s.
 */
public abstract class AbstractCommitRevListStdioHandler<T>
        extends AbstractLineStdioHandler<T> {

    private final CommitPump commitPump;
    private final boolean withPaths;

    private Commit commit;
    private MinimalChange commitRenameChange;
    private LineType nextLineType;
    private String trackingPath;

    protected AbstractCommitRevListStdioHandler(boolean withMessages, boolean withPaths, int maxMessageLength,
                                                String path) {
        this(new CommitPumpSettings.Builder()
                .withMessages(withMessages)
                .maxMessageLength(maxMessageLength)
                .build(), withPaths, path);
    }

    private AbstractCommitRevListStdioHandler(CommitPumpSettings settings, boolean withPaths, String path) {
        super(StandardCharsets.UTF_8);

        //If we use --name-status, we need a path; otherwise the result would be discarded
        checkArgument(!withPaths || path != null);

        nextLineType = LineType.COMMIT;
        trackingPath = path;
        this.withPaths = withPaths;

        commitPump = new CommitPump(settings, new CommitSink() {

            @Override
            public void onCommit(@Nonnull Commit commit) {
                AbstractCommitRevListStdioHandler.this.commit = commit;
            }

            @Override
            public void onPartialMessage(int maxLength) {
                setMaxLineLength(maxLength);
            }
        });
    }

    /**
     * Retrieves the format which should be provided to {@code git rev-list --format=} to produce output which can
     * be consumed by this output handler.
     *
     * @return a format string, which <i>may not</i> be {@code null}
     */
    @Nonnull
    public String getFormat() {
        return commitPump.getFormat();
    }

    @Override
    protected boolean onStdout(@Nonnull String line, boolean ignored) {
        if (nextLineType != LineType.COMMIT && line.startsWith("commit")) {
            if (!flushCommit(LineType.COMMIT)) {
                return false;
            }
        }

        if (nextLineType == LineType.COMMIT) {
            commitPump.onStdout(line);

            if (commit != null && withPaths) {
                nextLineType = LineType.NAME_STATUS;
                return true;
            }

            return flushCommit();
        }

        if (nextLineType == LineType.NAME_STATUS && StringUtils.isNotEmpty(line) && maybeParseNameStatus(line)) {
            return flushCommit(LineType.SKIP);
        }

        return true;
    }

    @Override
    protected void onStdoutClosed() {
        if (commit != null) {
            onCommit(commit, commitRenameChange);
        }
    }

    protected abstract boolean onCommit(Commit commit, @Nullable MinimalChange rename);

    private boolean flushCommit(LineType to) {
        try {
            return flushCommit();
        } finally {
            nextLineType = to;
        }
    }

    private boolean flushCommit() {
        Commit current = commit;
        MinimalChange currentRename = commitRenameChange;
        commit = null;
        commitRenameChange = null;

        return current == null || onCommit(current, currentRename);
    }

    private boolean maybeParseNameStatus(String line) {
        int firstTab = line.indexOf('\t');
        if (firstTab == -1) {
            throw new IllegalArgumentException("[" + line + "] does not contain a tab");
        }

        String metadata = line.substring(0, firstTab);
        String paths = line.substring(firstTab + 1); //Drop the tab
        String changeType = metadata.substring(0, 1);

        String afterPath;
        String beforePath;
        if ("C".equals(changeType) /* copy */ || "R".equals(changeType) /* rename */) {
            int secondTab = paths.indexOf('\t');
            if (secondTab == -1) {
                throw new IllegalStateException("[" + line +
                        "] indicates a copy or rename, but only a single path was provided");
            }

            afterPath = unescapeQuoted(paths.substring(secondTab + 1)); //Second path is the afterPath
            beforePath = unescapeQuoted(paths.substring(0, secondTab)); //First path is the beforePath
        } else {
            afterPath = unescapeQuoted(paths); //Only one path, the afterPath
            beforePath = null;
        }

        if (trackingPath.equals(afterPath)) { //If this change modified the tracking path, it's the one we care about
            MinimalChange.Builder builder = new MinimalChange.Builder()
                    .type(GitChangeType.fromStatus(changeType))
                    .path(afterPath);
            if (beforePath != null) {
                builder.srcPath(beforePath)
                        .percentUnchanged(Integer.parseInt(metadata.substring(1, firstTab)));

                trackingPath = beforePath; //If there was a rename, update the tracking path accordingly
            }

            commitRenameChange = builder.build();
            return true;
        }

        return false;
    }

    /**
     * Type of the line we're expecting to receive <i>next</i>
     */
    private enum LineType {

        COMMIT, // part of the commit object. Pass it to CommitPump
        NAME_STATUS, // part of name status, try to parse file name from it
        SKIP // skip this line
    }
}