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

import com.atlassian.bitbucket.mesh.git.GitObjectType;
import com.atlassian.bitbucket.mesh.git.GitUtils;
import com.atlassian.bitbucket.mesh.git.content.DirectoryCallback;
import com.atlassian.bitbucket.mesh.git.content.File;
import com.atlassian.bitbucket.mesh.git.submodule.Submodule;
import com.atlassian.bitbucket.mesh.git.submodule.Submodules;
import com.atlassian.bitbucket.mesh.process.handler.AbstractLineStdioHandler;
import com.atlassian.bitbucket.mesh.process.handler.LineDecoder;
import com.atlassian.bitbucket.mesh.util.PageRange;

import javax.annotation.Nonnull;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.Objects.requireNonNull;

public class DirectoryLsTreeStdioHandler extends AbstractLineStdioHandler<Void> {

    //When -l is passed and sizes are displayed, Git leads them with _spaces_ rather than a tab, so we can
    //optionally detect a size simply by looking for one or more spaces followed by numbers. For directories,
    //Git outputs a - instead of a size, so we allow that too
    static final Pattern DIRECTORY_TREE_PATTERN =
            Pattern.compile("(\\d+) (tree|blob|commit) ([0-9a-fA-F]{40})(?:\\s+([-\\d]+))?\\t(.*)");
    private static final int GROUP_CONTENT_ID = 3;
    private static final int GROUP_MODE = 1;
    private static final int GROUP_PATH = 5;
    private static final int GROUP_SIZE = 4;
    private static final int GROUP_TYPE = 2;

    private final DirectoryCallback callback;
    private final DirectoryCreator directoryCreator;
    private final PageRange pageRange;
    private final Submodules submodules;

    private int added;
    private long matches;

    public DirectoryLsTreeStdioHandler(@Nonnull DirectoryCallback callback, @Nonnull DirectoryCreator directoryCreator,
                                       @Nonnull PageRange pageRange, @Nonnull Submodules submodules) {
        super(StandardCharsets.UTF_8, LineDecoder.Mode.NULL);

        this.callback = requireNonNull(callback, "callback");
        this.directoryCreator = requireNonNull(directoryCreator, "directoryCreator");
        this.pageRange = requireNonNull(pageRange, "pageRequest");
        this.submodules = requireNonNull(submodules, "submodules");
    }

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

    @Override
    public void onExit(int exitCode) {
        directoryCreator.close();
    }

    @Override
    protected boolean onStdout(@Nonnull String line, boolean truncated) {
        Matcher matcher = DIRECTORY_TREE_PATTERN.matcher(line);
        if (matcher.matches() && matches++ >= pageRange.getStart()) {
            if (parse(matcher)) {
                //Even if the callback still wants more entries, if we've filled the requested
                //page, stop processing
                return ++added < pageRange.getLimit();
            }

            //If the callback has indicated it doesn't want any more entries, stop processing
            return false;
        }

        return true;
    }

    private boolean parse(Matcher matcher) {
        //These three matches are guaranteed
        String contentId = matcher.group(GROUP_CONTENT_ID);
        String path = matcher.group(GROUP_PATH); //With -z, the
        GitObjectType type = GitObjectType.fromObjectType(matcher.group(GROUP_TYPE));

        switch (type) {
            case COMMIT:
                //Commits in trees represent submodules
                Submodule submodule = submodules.get(path, contentId);
                if (submodule == null) {
                    //If we can't find the configuration for the submodule, we can't render it
                    return true;
                }

                return callback.onSubmodule(submodule);
            case TREE:
                //Trees in trees represent subdirectories
                return callback.onDirectory(directoryCreator.create(path, contentId));
            default:
                //Blobs in trees represent files. The regex filters to "blob", "commit" and "tree", so we can
                //safely assume file here. If we get an unexpected type, the regex won't match
                File.Builder fileBuilder = new File.Builder(path, contentId)
                        //Parse the executable flag, if the mode is understood
                        .executable(GitUtils.isExecutable(matcher.group(GROUP_MODE)));

                //Sizes will only be returned for files, but the group will be null if sizes weren't requested
                String maybeSize = matcher.group(GROUP_SIZE);
                if (!(maybeSize == null || "-".equals(maybeSize))) {
                    fileBuilder.size(Long.valueOf(maybeSize));
                }

                return callback.onFile(fileBuilder.build());
        }
    }
}
