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

import com.atlassian.bitbucket.mesh.git.GitUtils;
import com.atlassian.bitbucket.mesh.git.builder.revlist.CommitMessageMode;
import com.atlassian.bitbucket.mesh.git.builder.revlist.CommitPump;
import com.atlassian.bitbucket.mesh.git.builder.revlist.CommitPumpSettings;
import com.atlassian.bitbucket.mesh.git.commit.Commit;
import com.atlassian.bitbucket.mesh.git.commit.LastModifiedCallback;
import com.atlassian.bitbucket.mesh.process.OsProcess;
import com.atlassian.bitbucket.mesh.process.handler.AbstractLineStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.LineEncoder;

import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;

import static org.apache.commons.lang3.StringUtils.appendIfMissing;
import static org.apache.commons.lang3.StringUtils.isEmpty;

public class LastModifiedLogStdioHandler extends AbstractLineStdioHandler<Void> {

    private static final String FORMAT_START = "%x03START%x04%ncommit %H%n"; //commit %H is to mimic rev-list
    private static final String OBJECT_START = "\u0003START\u0004";

    private final String basePath;
    private final LastModifiedCallback callback;
    private final CommitPump commitPump;
    private final List<String> files;
    private final LineEncoder stdinEncoder;

    private Commit commit;

    public LastModifiedLogStdioHandler(LastModifiedCallback callback, String commitId, String basePath,
                                       List<String> files, int maxMessageLength) {
        super(StandardCharsets.UTF_8);

        this.basePath = isEmpty(basePath) ? "" : appendIfMissing(basePath, "/");
        this.callback = callback;
        this.files = files;

        commitPump = new CommitPump(new CommitPumpSettings.Builder()
                .maxMessageLength(maxMessageLength)
                .messageMode(CommitMessageMode.SUBJECT)
                .withMessages(true)
                .build(), newCommit -> commit = newCommit);
        stdinEncoder = new LineEncoder(Stream.concat(
                Stream.of(commitId + "\n--"), //First write the commit ID, followed by "--", to stdin
                files.stream().map(file -> this.basePath + file)), //followed by the _fully qualified_ path of each file
                StandardCharsets.UTF_8);
    }

    @Nonnull
    public String getFormat() {
        return FORMAT_START + commitPump.getFormat();
    }

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

    @Override
    public void onStart(@Nonnull OsProcess process) {
        process.wantWrite();
    }

    @Override
    public boolean onStdinReady(@Nonnull ByteBuffer buffer) {
        if (stdinEncoder.encodeTo(buffer)) {
            return true;
        }
        closeStdin();
        return false;
    }

    @Override
    protected boolean onStdout(@Nonnull String line, boolean truncated) {
        if (line.equals(OBJECT_START)) {
            commit = null;
            return true;
        }

        if (commit == null) {
            commitPump.onStdout(line);
            return true;
        }

        //After the commit there should be an empty line. If it's not empty, try to parse it as a status and path
        if (line.isEmpty()) {
            return true;
        }

        //Then the diff-tree, indicating which files were modified in this commit. We use this to decide
        //if _this_ was the newest commit to modify any paths
        int split = line.indexOf('\t');
        String path = GitUtils.unescapeQuoted(line.substring(split + 1));

        Iterator<String> iterator = files.iterator();
        while (iterator.hasNext()) {
            String file = iterator.next();
            if (path.equals(basePath + file)) { //Test using the qualified path
                //We've found the most recent commit to modify this file. No further commits modifying
                //this file should be considered, so we remove it from the list
                iterator.remove();

                callback.onFileModified(file, commit);
            }
        }

        //If we've found all the files, we're done traversing commits
        return !files.isEmpty();
    }
}
