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

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;

/**
 * Defines headers which may appear in {@code git diff} output. These will appear after the {@code diff --git} banner
 * and before any hunks.
*/
public enum GitDiffHeader {

    /**
     * Defines the source path for a copied file.
     */
    COPY_FROM("copy from", GitDiffHeaderType.PATH, GitDiffTarget.SOURCE),
    /**
     * Defines the destination path for a copied file.
     */
    COPY_TO("copy to", GitDiffHeaderType.PATH, GitDiffTarget.DESTINATION),
    /**
     * Defines the file mode a source file had before it was deleted.
     */
    DELETED_FILE_MODE("deleted file mode", GitDiffHeaderType.MODE, GitDiffTarget.SOURCE),
    /**
     * Defines the percentage of dissimilarity between a source file and a copied/renamed/moved destination file.
     */
    DISSIMILARITY_INDEX("dissimilarity index", GitDiffHeaderType.PERCENTAGE),
    /**
     * Defines git index data, such as content hashes, for the source and destination files.
     */
    INDEX("index", GitDiffHeaderType.INDEX),
    /**
     * Defines the file mode for a newly-added destination file.
     */
    NEW_FILE_MODE("new file mode", GitDiffHeaderType.MODE, GitDiffTarget.DESTINATION),
    /**
     * Defines the new mode for a destination file.
     */
    NEW_MODE("new mode", GitDiffHeaderType.MODE, GitDiffTarget.DESTINATION),
    /**
     * Defines the original mode for a source file.
     */
    OLD_MODE("old mode", GitDiffHeaderType.MODE, GitDiffTarget.SOURCE),
    /**
     * Defines the source path for a renamed/moved file.
     */
    RENAME_FROM("rename from", GitDiffHeaderType.PATH, GitDiffTarget.SOURCE),
    /**
     * Defines the destination path for a renamed/moved file.
     */
    RENAME_TO("rename to", GitDiffHeaderType.PATH, GitDiffTarget.DESTINATION),
    /**
     * Defines the percentage of similarity between a source file and a copied/renamed/moved destination file.
     */
    SIMILARITY_INDEX("similarity index", GitDiffHeaderType.PERCENTAGE);

    private static final Map<String, Object> HEADERS_BY_WORD = mapHeadersByWord();

    private final String key;
    private final GitDiffTarget target;
    private final GitDiffHeaderType type;

    GitDiffHeader(String key, GitDiffHeaderType type) {
        this(key, type, GitDiffTarget.NONE);
    }
    
    GitDiffHeader(String key, GitDiffHeaderType type, GitDiffTarget target) {
        type.verify(target);

        this.key = key;
        this.target = target;
        this.type = type;
    }

    @Nullable
    @SuppressWarnings({"rawtypes", "unchecked"})
    public static GitDiffHeader parseToHeader(@Nonnull String line) {
        String[] words = line.split("\\s", 4);

        Map<String, Object> map = HEADERS_BY_WORD;
        for (String word : words) {
            Object value = map.get(word);
            if (value == null) {
                break;
            }

            if (value instanceof Map) {
                //Word fork, there may still be a header
                map = (Map) value;
            } else {
                //If the value isn't null or a map, it has to be a header
                return (GitDiffHeader) value;
            }
        }

        //There's no header for this type of line--it's either a hunk or a new diff
        return null;
    }

    /**
     * Retrieves the key identifying this header in an output line.
     * <p>
     * Note: Header keys may contain one or more spaces, so {@code indexOf}-based splitting should not be used.
     * 
     * @return the header key
     */
    @Nonnull
    public String getKey() {
        return key;
    }

    /**
     * Retrieves the target to which the value of this header applies. The {@link #getType() header type} will indicate
     * whether a specific target is available.
     * 
     * @return the header target
     */
    @Nonnull
    public GitDiffTarget getTarget() {
        return target;
    }

    /**
     * Retrieves the type of data provided by the {@link #getValue(String) header value}. The type also indicates
     * whether a specific {@link #getTarget() target} is related to this header.
     *
     * @return the header type
     */
    @Nonnull
    public GitDiffHeaderType getType() {
        return type;
    }

    /**
     * Removes the leading {@link #getKey() key} from the provided line and returns the value.
     *
     * @param line the line to split
     * @return the value part of the line
     */
    @Nonnull
    public String getValue(@Nonnull String line) {
        return line.substring(key.length() + 1); //Drop the space after the header
    }

    private static Map<String, Object> mapHeadersByWord() {
        Map<String, Object> headersByWord = new HashMap<>();
        for (GitDiffHeader header : GitDiffHeader.values()) {
            List<String> words = Arrays.asList(header.getKey().split("\\s"));
            Iterator<String> iterator = words.iterator();

            Map<String, Object> map = headersByWord;
            while (true) {
                String word = iterator.next();
                if (iterator.hasNext()) {
                    //There are more words, so build the graph down further
                    @SuppressWarnings({"rawtypes", "unchecked"})
                    Map<String, Object> next = (Map) map.get(word);
                    if (next == null) {
                        next = new HashMap<>();

                        map.put(word, next);
                    }
                    map = next;
                } else {
                    //This is the last word in the key, so the value is this instance
                    map.put(word, header);

                    break;
                }
            }
        }

        return Collections.unmodifiableMap(headersByWord);
    }
}
