package com.atlassian.stash.internal.scm.git.command.diff;

import com.atlassian.bitbucket.content.ContentTreeNode;
import com.atlassian.bitbucket.content.SimpleChange;
import com.atlassian.bitbucket.io.LineReaderOutputHandler;
import com.atlassian.bitbucket.scm.git.GitUtils;
import com.atlassian.bitbucket.scm.git.command.diff.GitChangeType;

import java.nio.charset.StandardCharsets;

/**
 * A {@link DiffTreeOutputHandler} implementation derived from {@code LineOutputHandler} providing functionality for
 * parsing {@link SimpleChange}s.
 */
public abstract class AbstractChangeDiffTreeOutputHandler<T>
        extends LineReaderOutputHandler
        implements DiffTreeOutputHandler<T> {

    /**
     * Constructs a new {@code AbstractChangeDiffTreeOutputHandler}
     */
    public AbstractChangeDiffTreeOutputHandler() {
        super(StandardCharsets.UTF_8);
    }

    protected SimpleChange parseChange(String line) {
        int index = line.indexOf("\t");
        if (index == -1) {
            throw new IllegalArgumentException("Line [" + line + "] does not contain a tab");
        }

        String metadata = line.substring(0, index); //File modes, content hashes, change type
        String paths = line.substring(index + 1); //At least 1 path

        int parents = metadata.lastIndexOf(':') + 1; //Count the colons--they tell how many parents there are
        if (parents == 0) { //-1 for not found, +1
            throw new IllegalStateException("Leading colons indicating the number of parents were not found");
        }
        metadata = metadata.substring(parents); //Drop the colons from the first mode before the split
        
        //Array length will vary, but we can use the parent count produced from the leading colons to know how
        //long the array should be
        String[] meta = metadata.split("\\s");
        if (meta.length != ((2 * (parents + 1)) + 1)) {
            //There are (parents + 1) file modes and hashes (where the +1 is the destination mode/hash)
            //There is the additional +1 for the change type
            throw new IllegalStateException("Metadata count [" + meta.length + "] does not match expectations");
        }

        SimpleChange.Builder builder = new SimpleChange.Builder();

        String rawType = meta[meta.length - 1]; //Last entry is always the change type
        GitChangeType type = GitChangeType.fromStatus(rawType.substring(0, 1));
        if (parents == 1 && type.isScored()) {
            //When only a single parent exists, certain change types (copy, rename) are scored. The percent score is
            //appended to the type string, like "R099" for 99% match on a rename.
            builder.percentUnchanged(Integer.parseInt(rawType.substring(1)));

            //Scored types have two paths in src/dst order if there is only one parent
            String[] rawPaths = paths.split("\\t");
            builder.path(GitUtils.unescapeQuoted(rawPaths[1]))
                    .srcPath(GitUtils.unescapeQuoted(rawPaths[0]));
        } else {
            //All other types have no score and only 1 path. Note that if there is more than 1 parent, even renames and
            //copies have no score and only one path as well
            builder.path(GitUtils.unescapeQuoted(paths));
        }
        builder.executable(GitUtils.isExecutable(meta[parents]))
                .srcExecutable(GitUtils.isExecutable(meta[0]))
                // If the path is or used to be a submodule, then we treat it as such
                // This covers the cases of modifying a submodule ('160000 160000'),
                // adding a submodule ('000000 160000'), removing a submodule ('160000 000000')
                // and changing the submodule to a file ('160000 100644'). This last case is particularly
                // important as it's our current strategy for submodule conflict resolution
                // (see MergeConflictHandler.visit(SubmoduleGitMergeConflict))
                .nodeType(GitUtils.isSubmodule(meta[parents]) || GitUtils.isSubmodule(meta[parents - 1]) ?
                        ContentTreeNode.Type.SUBMODULE : ContentTreeNode.Type.FILE)
                .type(type.getType());

        if (type == GitChangeType.DELETE) {
            //Deletion, use the source (first) content hash
            builder.contentId(meta[parents + 1]);
        } else {
            //For all other operations, use the last content hash
            builder.contentId(meta[meta.length - 2]);
        }
        builder.fromContentId(meta[parents + 1]);

        return builder.build();
    }
}
