package com.atlassian.bitbucket.mesh.git.content;

import com.atlassian.bitbucket.mesh.git.GitChangeType;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcChange;
import com.atlassian.bitbucket.mesh.util.BuilderSupport;
import com.google.common.base.Objects;
import com.google.protobuf.ByteString;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;

public class MinimalChange {

    private final String path;
    private final int percentUnchanged;
    private final String srcPath;
    private final GitChangeType type;

    protected MinimalChange(AbstractBuilder<?> builder) {
        path = builder.path;
        percentUnchanged = builder.percentUnchanged;
        srcPath = builder.srcPath;
        type = builder.type;

        checkState(path != null, "A path is required");
        checkState(type != null, "A type is required");
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o instanceof MinimalChange) {
            MinimalChange that = (MinimalChange) o;

            return Objects.equal(getPath(), that.getPath()) &&
                    Objects.equal(getSrcPath(), that.getSrcPath());
        }
        return false;
    }

    /**
     * The path to the changed content.
     * <ul>
     * <li>For {@link GitChangeType#COPY copies}, this is the path of the content that was created</li>
     * <li>For {@link GitChangeType#DELETE deletions}, this is the path of the content before it was deleted</li>
     * <li>For {@link GitChangeType#RENAME moves}, this is the path of content after the move</li>
     * <li>For all other {@link GitChangeType types}, this is the path of the content to which the change occurred</li>
     * </ul>
     *
     * @return the changed path
     */
    @Nonnull
    public String getPath() {
        return path;
    }

    /**
     * The percentage of content that matches before and after a change. This is useful, for example, in determining
     * the likelihood that an identified {@link GitChangeType#COPY copy} or {@link GitChangeType#RENAME move} is correct.
     * <p>
     * Note: This feature may not be supported by all SCM implementations. Where it is not supported, it will always
     * return {@code -1}.
     *
     * @return a percentage, if the change is scored, or {@code -1} if it is not
     */
    public int getPercentUnchanged() {
        return percentUnchanged;
    }

    /**
     * The path at which the changed content originated.
     * <ul>
     * <li>For {@link GitChangeType#COPY copies}, this is the path to the content that was copied</li>
     * <li>For {@link GitChangeType#RENAME moves}, this is the path to the content before it was moved</li>
     * <li>For all other {@link GitChangeType types}, this value is not defined</li>
     * </ul>
     *
     * @return the originating path, if any
     */
    @Nullable
    public String getSrcPath() {
        return srcPath;
    }

    /**
     * The type of change that was applied.
     *
     * @return the change type
     */
    @Nonnull
    public GitChangeType getType() {
        return type;
    }

    @Nonnull
    public RpcChange toChange() {
        return prepareRpcBuilder().build();
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{path='" + getPath() + "', srcPath='" + getSrcPath() +
                "', type=" + getType() + "}";
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getPath(), getSrcPath());
    }

    protected RpcChange.Builder prepareRpcBuilder() {
        RpcChange.Builder builder = RpcChange.newBuilder()
                .setPath(ByteString.copyFromUtf8(path))
                .setType(type.toRpc());
        if (percentUnchanged != -1) {
            builder.setPercentUnchanged(percentUnchanged);
        }
        if (srcPath != null) {
            builder.setSrcPath(ByteString.copyFromUtf8(srcPath));
        }

        return builder;
    }

    public static class Builder extends AbstractBuilder<Builder> {

        @Nonnull
        public MinimalChange build() {
            return new MinimalChange(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    abstract static class AbstractBuilder<B extends AbstractBuilder<B>> extends BuilderSupport {

        private String path;
        private int percentUnchanged;
        private String srcPath;
        private GitChangeType type;

        protected AbstractBuilder() {
            percentUnchanged = -1;
        }

        protected AbstractBuilder(@Nonnull MinimalChange change) {
            requireNonNull(change, "change");

            path = change.getPath();
            percentUnchanged = change.getPercentUnchanged();
            srcPath = change.getSrcPath();
            type = change.getType();
        }

        @Nonnull
        public B path(@Nonnull String value) {
            path = requireNonNull(value, "path");

            return self();
        }

        @Nonnull
        public B percentUnchanged(int value) {
            percentUnchanged = value;

            return self();
        }

        @Nonnull
        public B srcPath(@Nonnull String value) {
            srcPath = requireNonNull(value, "srcPath");

            return self();
        }

        @Nonnull
        public B type(@Nonnull GitChangeType value) {
            type = requireNonNull(value, "type");

            return self();
        }

        protected abstract B self();
    }
}
