package com.atlassian.stash.internal.repository.sync.auto;

import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.google.common.collect.ImmutableList;

import javax.annotation.Nonnull;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.List;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

/**
 * Describes a request to apply a given {@link AutoRefSyncOperation operation} to one or more {@link RefChange changes}
 * to synchronize them from a given {@link #getUpstreamId() upstream} to another {@link #getRepositoryId() repository}.
 *
 * @since 3.3
 */
public class AutoRefSyncRequest implements Externalizable {

    private AutoRefSyncOperation operation;
    private List<RefChange> refChanges;
    private int repositoryId;
    private int upstreamId;
    private int userId;

    //Required for Externalizable
    public AutoRefSyncRequest() {
    }

    private AutoRefSyncRequest(Builder builder) {
        operation = builder.operation;
        refChanges = builder.refChanges;
        repositoryId = builder.repositoryId;
        upstreamId = builder.upstreamId;
        userId = builder.userId;
    }

    public AutoRefSyncOperation getOperation() {
        return operation;
    }

    public List<RefChange> getRefChanges() {
        return refChanges;
    }

    public int getRepositoryId() {
        return repositoryId;
    }

    public int getUpstreamId() {
        return upstreamId;
    }

    public int getUserId() {
        return userId;
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        repositoryId = in.readInt();
        upstreamId = in.readInt();
        userId = in.readInt();
        operation = AutoRefSyncOperation.fromId(in.readInt());
        refChanges = readRefChanges(in, in.readInt());
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(repositoryId);
        out.writeInt(upstreamId);
        out.writeInt(userId);
        out.writeInt(operation.getId());
        writeRefChanges(out);
    }

    private MinimalRef readRef(ObjectInput in) throws IOException {
        SimpleMinimalRef.Builder builder = new SimpleMinimalRef.Builder()
                .id(in.readUTF())
                .displayId(in.readUTF());
        try {
            builder.type((RefType) in.readObject());
        } catch (ClassNotFoundException e) {
            // fall back to BRANCH type
            builder.type(StandardRefType.BRANCH);
        }
        return builder.build();
    }

    private List<RefChange> readRefChanges(ObjectInput in, int count) throws IOException {
        ImmutableList.Builder<RefChange> builder = ImmutableList.builder();
        for (int i = 0; i < count; ++i) {
            builder.add(new SimpleRefChange.Builder()
                    .ref(readRef(in))
                    .fromHash(in.readUTF())
                    .toHash(in.readUTF())
                    .type(RefChangeType.fromId(in.readInt()))
                    .build());
        }

        return builder.build();
    }

    private void writeRef(ObjectOutput out, MinimalRef ref) throws IOException {
        out.writeUTF(ref.getId());
        out.writeUTF(ref.getDisplayId());
        out.writeObject(ref.getType());
    }

    private void writeRefChanges(ObjectOutput out) throws IOException {
        out.writeInt(refChanges.size());
        for (RefChange refChange : refChanges) {
            writeRef(out, refChange.getRef());
            out.writeUTF(refChange.getFromHash());
            out.writeUTF(refChange.getToHash());
            out.writeInt(refChange.getType().getId());
        }
    }

    public static class Builder {

        private final List<RefChange> refChanges;
        private final int userId;

        private AutoRefSyncOperation operation;
        private int repositoryId;
        private int upstreamId;

        @SuppressWarnings("ConstantConditions")
        public Builder(@Nonnull List<RefChange> refChanges, @Nonnull ApplicationUser user) {
            this.refChanges = checkNotNull(refChanges, "refChanges");
            this.userId = checkNotNull(user, "user").getId();
        }

        @Nonnull
        public AutoRefSyncRequest build() {
            checkState(repositoryId != 0, "A repository ID is required");
            checkState(upstreamId != 0, "An upstream repository ID is required");
            checkState(operation != null, "A RefSyncOperation is required");

            return new AutoRefSyncRequest(this);
        }

        @Nonnull
        public Builder operation(@Nonnull AutoRefSyncOperation value) {
            operation = checkNotNull(value, "operation");

            return this;
        }

        @Nonnull
        public Builder repository(@Nonnull Repository value) {
            repositoryId = extractId(value, "repository");

            return this;
        }

        @Nonnull
        public Builder upstream(@Nonnull Repository value) {
            upstreamId = extractId(value, "upstream");

            return this;
        }

        private static int extractId(Repository repository, String name) {
            return checkNotNull(checkNotNull(repository, name).getId(), "%s.id", name);
        }
    }
}
