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

import com.atlassian.bitbucket.mesh.rpc.v1.RpcProposedRefUpdate;
import com.atlassian.bitbucket.mesh.util.BuilderSupport;
import com.atlassian.bitbucket.mesh.util.ShaUtils;
import com.google.protobuf.ByteString;

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

import static com.atlassian.bitbucket.mesh.util.ShaUtils.NULL_SHA1;
import static java.util.Objects.requireNonNull;

/**
 * Describes a <i>proposed</i> {@link RefUpdate}. This is a separate type to allow for different rules on how
 * hashes are provided.
 * <ul>
 *     <li>{@link RefUpdate#getNewHash} and {@link RefUpdate#getOldHash} are both required to be set, and will
 *     be {@link ShaUtils#NULL_SHA1} for certain types of update</li>
 *     <li>{@link #getNewHash} and {@link #getOldHash} allow {@code null} because, when using {@code git update-ref},
 *     <i>not specifying</i> a hash has different semantics than specifying {@link ShaUtils#NULL_SHA1} when creating
 *     and deleting refs</li>
 * </ul>
 * Using {@code null} for the {@link #getOldHash} in particular allows for ref updates to be applied regardless of
 * the target ref's current hash.
 */
public class ProposedRefUpdate {

    private final String newHash;
    private final String oldHash;
    private final String refId;
    private final RefUpdateType type;

    private ProposedRefUpdate(Builder builder) {
        newHash = builder.newHash;
        oldHash = builder.oldHash;
        refId = builder.refId;

        if (newHash == null || ShaUtils.isNull(newHash)) {
            type = RefUpdateType.DELETE;
        } else if (oldHash == null || ShaUtils.isNull(oldHash)) {
            type = RefUpdateType.ADD;
        } else {
            type = RefUpdateType.UPDATE;
        }
    }

    @Nonnull
    public static ProposedRefUpdate from(@Nonnull RpcProposedRefUpdate refUpdate) {
        return new Builder(requireNonNull(refUpdate, "refUpdate").getRefId().toStringUtf8())
                .newHash(refUpdate.hasNewHash() ? refUpdate.getNewHash() : null)
                .oldHash(refUpdate.hasOldHash() ? refUpdate.getOldHash() : null)
                .build();
    }

    @Nonnull
    public static ProposedRefUpdate of(@Nonnull String refId, @Nullable String oldHash, @Nullable String newHash) {
        return new Builder(refId)
                .newHash(newHash)
                .oldHash(oldHash)
                .build();
    }

    @Nonnull
    public static ProposedRefUpdate toAdd(@Nonnull String refId, @Nonnull String newHash) {
        return new Builder(refId)
                .newHash(requireNonNull(newHash, "newHash"))
                .oldHash(NULL_SHA1)
                .build();
    }

    @Nonnull
    public static ProposedRefUpdate toDelete(@Nonnull String refId, @Nullable String oldHash) {
        return new Builder(refId)
                .newHash(NULL_SHA1)
                .oldHash(oldHash)
                .build();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        ProposedRefUpdate that = (ProposedRefUpdate) o;
        return Objects.equals(refId, that.refId) &&
                Objects.equals(newHash, that.newHash) &&
                Objects.equals(oldHash, that.oldHash);
    }

    //Nullability is intentionally not documented here because it depends on the update type
    public String getOldHash() {
        return oldHash;
    }

    //Nullability is intentionally not documented here because it depends on the update type
    public String getNewHash() {
        return newHash;
    }

    @Nonnull
    public String getRefId() {
        return refId;
    }

    @Nonnull
    public RefUpdateType getType() {
        return type;
    }

    @Override
    public int hashCode() {
        return Objects.hash(refId, newHash, oldHash);
    }

    public boolean isDark() {
        return Ref.isDark(refId);
    }

    @Nonnull
    public RpcProposedRefUpdate toRefUpdate() {
        RpcProposedRefUpdate.Builder builder = RpcProposedRefUpdate.newBuilder()
                .setRefId(ByteString.copyFromUtf8(refId));
        if (oldHash != null) {
            builder.setOldHash(oldHash);
        }
        if (newHash != null) {
            builder.setNewHash(newHash);
        }
        return builder.build();
    }

    @Override
    public String toString() {
        return refId + ": " + oldHash + " -> " + newHash;
    }

    public static class Builder extends BuilderSupport {

        private final String refId;

        private String newHash;
        private String oldHash;

        public Builder(@Nonnull String refId) {
            this.refId = requireNonNull(refId, "refId");
        }

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

        @Nonnull
        public Builder newHash(@Nullable String value) {
            if (value != null) {
                requireNonBlank(value, "newHash");
            }
            newHash = value;

            return this;
        }

        @Nonnull
        public Builder oldHash(@Nullable String value) {
            if (value != null) {
                requireNonBlank(value, "oldHash");
            }
            oldHash = value;

            return this;
        }
    }
}
