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

import com.atlassian.bitbucket.mesh.git.GitTreeNode;

import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Queue;

import static com.atlassian.bitbucket.mesh.git.GitConstants.SPARSE_FILTER;
import static com.atlassian.bitbucket.mesh.git.GitUtils.NULL_TREE;
import static org.apache.commons.lang3.StringUtils.containsAny;

public class AnyFileBatchCatFileStdioHandler extends AbstractBatchCatFileStdioHandler<String> {

    private final StringBuilder fileBuilder;
    private final Queue<String> nextObjectId;

    private GitTreeNode directory;
    private String file;

    AnyFileBatchCatFileStdioHandler(String commit) {
        fileBuilder = new StringBuilder();

        nextObjectId = new ArrayDeque<>(1);
        nextObjectId.offer(commit + ":"); //Trailing : to request the commit's tree
    }

    @Override
    public String getResult() {
        return file;
    }

    @Override
    protected boolean onContent(@Nonnull ByteBuffer content, boolean closed) {
        GitTreeNode node;
        while ((node = GitTreeNode.parse(content)) != null) {
            //Filter directories or files that contain any characters known to result in "git checkout"
            //failing to check out at least one file
            String path = node.getPath();
            if (containsAny(path, SPARSE_FILTER)) {
                continue;
            }

            if (node.isFile()) {
                file = fileBuilder.append(path).toString();

                //We've found a file; no need to read any more trees
                return false;
            }
            if (directory == null && node.isDirectory()) {
                directory = node;
            }
        }

        if (closed && directory != null) {
            //If we make it here we didn't find any files in this tree, but we did find a directory. List
            //that directory's contents next and continue searching.
            //
            //Since we only list the contents of the first directory we find, it's possible a *hand-crafted*
            //tree which somehow includes a reference to an *empty* subtree (something porcelain commands
            //would never do), might result in this handler failing to return a file even though there are
            //files in other directories. Since such a tree would have to be manually created, we've chosen
            //not to try and handle such a case since the end result is "only" that every file will end up
            //being checked out (hurting performance), but overall processing should still succeed
            fileBuilder.append(directory.getPath())
                    .append("/");
            nextObjectId.offer(directory.getContentId());
            directory = null;
        }

        return true;
    }

    @Override
    protected void onMissing(@Nonnull String objectId) {
        //The requested object doesn't exist (shouldn't actually be possible). Rather than throwing,
        //let the process complete normally; checkout(...) will fall back on doing a full checkout
    }

    @Override
    protected boolean onObject(@Nonnull BatchHeader header) {
        //If the requested object references the null tree, it's an empty commit. In that case, since
        //there aren't any tree entries, any object content can be ignored. Otherwise, stream it so we
        //can look for a file to use to create a sparse checkout
        return !NULL_TREE.equals(header.getObjectId());
    }

    @Override
    protected String getNextObjectId() {
        return nextObjectId.poll();
    }
}