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

import com.atlassian.bitbucket.mesh.git.content.PorcelainBlameCallback;
import com.atlassian.bitbucket.mesh.process.handler.BinaryDisposition;
import com.atlassian.bitbucket.mesh.util.PageRange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A {@link com.atlassian.bitbucket.mesh.process.ProcessStdioHandler} implementation which parses porcelain output
 * from {@code git blame} and passes the parsed data to a {@link PorcelainBlameCallback}. Because blame output is
 * forwarded to the callback as it is processed, this handler returns {@code null} for {@link #getResult()}.
 */
public class PorcelainBlameStdioHandler extends AbstractBlameStdioHandler<Void> {

    public static final Pattern LINE_HEADER_PATTERN = Pattern.compile("([0-9a-f]{40})\\s(\\d+)\\s(\\d+)\\s*");

    private static final Logger log = LoggerFactory.getLogger(PorcelainBlameStdioHandler.class);

    private final PorcelainBlameCallback callback;
    private final PageRange pageRange;
    private final List<Span> spans;
    private final int maxLineLength;

    private int added;
    private int lineNumber;
    private Span span;
    private State state;

    public PorcelainBlameStdioHandler(PorcelainBlameCallback callback, PageRange pageRange, int maxLineLength) {
        this.callback = callback;
        this.maxLineLength = maxLineLength + 1; //+1 for the leading tab output before line contents
        this.pageRange = pageRange;
        this.spans = new ArrayList<>();

        lineNumber = -1;
    }

    @Override
    public Void getResult() {
        return null;
    }

    @Nonnull
    @Override
    protected BinaryDisposition onBinary() {
        callback.onBinary();

        return BinaryDisposition.CANCEL;
    }

    @Override
    protected boolean onStdout(@Nonnull String line, boolean truncated) {
        Matcher matcher = SPAN_HEADER_PATTERN.matcher(line);
        if (matcher.matches()) {
            // if the line is a span header, reset write state and start a new span
            state = null;
            span = createSpan(matcher);

            // if this is a new commit, the next line will be metadata and shouldn't be truncated
            // otherwise, it will be content and should be truncated
            setMaxLineLength(isNewCommit(span.getHash()) ? Integer.MAX_VALUE : maxLineLength);

            return !(spans.isEmpty() && span.isEmpty());
        }

        if (span == null) {
            // if the line is not a span header and we have no span currently
            // then it is an unexpected output from git
            throw new IllegalStateException("Line [" + line + "] is not a valid span header");
        }

        if (isNewCommit(span.getHash())) {
            // if it is not a span header and we have no commit for current span
            // then we're still reading this commit.
            if (readCommit(span.getHash(), line)) {
                setMaxLineLength(maxLineLength);
            }
        } else if (state == null) { // once we done reading the commit, next line is content of the file
            lineNumber = lineNumber == -1 ? span.getLineNumber() : ++lineNumber;

            state = writeContent(lineNumber, line.substring(1), truncated);
            setMaxLineLength(Integer.MAX_VALUE);

            if (spans.contains(span)) {
                if (state == State.TRIM) {
                    // handle the case where our span includes one line too many because of pagination
                    span.trimLastPage();
                }
            } else if (state == State.CONTINUE) {
                spans.add(span);
            }

            return state != State.STOP;
        } else if ((matcher = LINE_HEADER_PATTERN.matcher(line)).matches()) {
            // next line after file content should be a line header, which starts with commit hash.
            String hash = matcher.group(1);

            // the line header must relate to the same commit as current span
            if (span.getHash().equals(hash)) {
                // we reset the state because the line after line header is content of the file again, so we want to write it
                state = null;
                setMaxLineLength(maxLineLength);
            } else {
                throw new IllegalStateException("Encountered unexpected hash [" + hash +
                        "] while processing spanned lines for [" + span.getHash() + "]");
            }
        } else {
            log.debug("Encountered unexpected line [{}]; expected line header (Working line: {})",
                    line, lineNumber);
            throw new IllegalStateException("Line [" + line + "] is not a valid line header");
        }

        return true;
    }

    @Override
    protected void onStdoutClosed() {
        if (!spans.isEmpty()) {
            generateBlame(spans).forEach(callback::onBlame);
        }
    }

    private State writeContent(int lineNumber, String content, boolean truncated) {
        if (++added > pageRange.getLimit()) {
            return State.TRIM; //Don't pass this line to the callback
        }

        return callback.onLine(content, lineNumber, truncated) ? State.CONTINUE : State.STOP;
    }

    private enum State {

        /**
         * The requested page has not been filled and the callback has not requested a stop, so keep processing.
         */
        CONTINUE,
        /**
         * The page <i>may</i> be full, but regardless the callback has requested a stop.
         * <p>
         * Note: In this state, there will <i>always</i> be at least one line left to read on the process's output
         * stream because {@link #TRIM} detection is handled first.
         */
        STOP,
        /**
         * The page is full and the line that has just been read should not be include.
         * <p>
         * Note: This state should only be hit <i>once</i>, and should mean there is no further output.
         */
        TRIM,
    }
}
