/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.bookkeeper.client;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.bookkeeper.meta.LedgerMetadataSerDe.CURRENT_METADATA_FORMAT_VERSION;
import static org.apache.bookkeeper.meta.LedgerMetadataSerDe.METADATA_FORMAT_VERSION_1;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.apache.bookkeeper.client.api.DigestType;
import org.apache.bookkeeper.client.api.LedgerMetadata;
import org.apache.bookkeeper.client.api.LedgerMetadata.State;
import org.apache.bookkeeper.common.annotation.InterfaceAudience.LimitedPrivate;
import org.apache.bookkeeper.common.annotation.InterfaceStability.Unstable;
import org.apache.bookkeeper.net.BookieId;

/**
 * Builder for building LedgerMetadata objects.
 */
@LimitedPrivate
@Unstable
@VisibleForTesting
public class LedgerMetadataBuilder {
    private long ledgerId = -1L;
    private int metadataFormatVersion = CURRENT_METADATA_FORMAT_VERSION;
    private int ensembleSize = 3;
    private int writeQuorumSize = 3;
    private int ackQuorumSize = 2;

    private State state = State.OPEN;
    private Optional<Long> lastEntryId = Optional.empty();
    private Optional<Long> length = Optional.empty();

    private TreeMap<Long, List<BookieId>> ensembles = new TreeMap<>();

    private Optional<DigestType> digestType = Optional.empty();
    private Optional<byte[]> password = Optional.empty();

    private long ctime = -1;
    private boolean storeCtime = false;
    private Map<String, byte[]> customMetadata = Collections.emptyMap();

    private static final long BLANK_CTOKEN = 0;
    private long cToken = BLANK_CTOKEN;

    public static LedgerMetadataBuilder create() {
        return new LedgerMetadataBuilder();
    }

    public static LedgerMetadataBuilder from(LedgerMetadata other) {
        LedgerMetadataBuilder builder = new LedgerMetadataBuilder();
        builder.ledgerId = other.getLedgerId();
        builder.metadataFormatVersion = other.getMetadataFormatVersion();
        builder.ensembleSize = other.getEnsembleSize();
        builder.writeQuorumSize = other.getWriteQuorumSize();
        builder.ackQuorumSize = other.getAckQuorumSize();

        builder.state = other.getState();
        if (builder.state == State.CLOSED) {
            builder.lastEntryId = Optional.of(other.getLastEntryId());
            builder.length = Optional.of(other.getLength());
        }

        builder.ensembles.putAll(other.getAllEnsembles());

        if (other.hasPassword()) {
            builder.password = Optional.of(other.getPassword());
            builder.digestType = Optional.of(other.getDigestType());
        }

        builder.ctime = other.getCtime();

        /** Hack to get around fact that ctime was never versioned correctly */
        builder.storeCtime = LedgerMetadataUtils.shouldStoreCtime(other);

        builder.customMetadata = ImmutableMap.copyOf(other.getCustomMetadata());

        return builder;
    }

    public LedgerMetadataBuilder withId(long ledgerId) {
        this.ledgerId = ledgerId;
        return this;
    }

    public LedgerMetadataBuilder withMetadataFormatVersion(int version) {
        if (version < METADATA_FORMAT_VERSION_1 || version > CURRENT_METADATA_FORMAT_VERSION) {
            return this;
        }
        this.metadataFormatVersion = version;
        return this;
    }

    public LedgerMetadataBuilder withPassword(byte[] password) {
        this.password = Optional.of(Arrays.copyOf(password, password.length));
        return this;
    }

    public LedgerMetadataBuilder withDigestType(DigestType digestType) {
        this.digestType = Optional.of(digestType);
        return this;
    }

    public LedgerMetadataBuilder withEnsembleSize(int ensembleSize) {
        checkState(ensembles.size() == 0, "Can only set ensemble size before adding ensembles to the builder");
        this.ensembleSize = ensembleSize;
        return this;
    }

    public LedgerMetadataBuilder withWriteQuorumSize(int writeQuorumSize) {
        this.writeQuorumSize = writeQuorumSize;
        return this;
    }

    public LedgerMetadataBuilder withAckQuorumSize(int ackQuorumSize) {
        this.ackQuorumSize = ackQuorumSize;
        return this;
    }

    public LedgerMetadataBuilder newEnsembleEntry(long firstEntry, List<BookieId> ensemble) {
        checkArgument(ensemble.size() == ensembleSize,
                      "Size of passed in ensemble must match the ensembleSize of the builder");
        checkArgument(ensembles.isEmpty() || firstEntry > ensembles.lastKey(),
                      "New entry must have a first entry greater than any existing ensemble key");
        ensembles.put(firstEntry, ensemble);
        return this;
    }

    public LedgerMetadataBuilder replaceEnsembleEntry(long firstEntry, List<BookieId> ensemble) {
        checkArgument(ensemble.size() == ensembleSize,
                      "Size of passed in ensemble must match the ensembleSize of the builder");
        checkArgument(ensembles.containsKey(firstEntry),
                      "Ensemble must replace an existing ensemble in the ensemble map");
        ensembles.put(firstEntry, ensemble);
        return this;
    }

    public LedgerMetadataBuilder withInRecoveryState() {
        this.state = State.IN_RECOVERY;
        return this;
    }

    public LedgerMetadataBuilder withClosedState() {
        this.state = State.CLOSED;
        return this;
    }

    public LedgerMetadataBuilder withLastEntryId(long lastEntryId) {
        this.lastEntryId = Optional.of(lastEntryId);
        return this;
    }

    public LedgerMetadataBuilder withLength(long length) {
        this.length = Optional.of(length);
        return this;
    }

    public LedgerMetadataBuilder withCustomMetadata(Map<String, byte[]> customMetadata) {
        this.customMetadata = ImmutableMap.copyOf(customMetadata);
        return this;
    }

    public LedgerMetadataBuilder withCreationTime(long ctime) {
        this.ctime = ctime;
        return this;
    }

    public LedgerMetadataBuilder storingCreationTime(boolean storing) {
        this.storeCtime = storing;
        return this;
    }

    public LedgerMetadataBuilder withCToken(long cToken) {
        this.cToken = cToken;
        return this;
    }

    public LedgerMetadata build() {
        checkArgument(ledgerId >= 0, "Ledger id must be set");
        checkArgument(ensembleSize >= writeQuorumSize, "Write quorum must be less or equal to ensemble size");
        checkArgument(writeQuorumSize >= ackQuorumSize, "Write quorum must be greater or equal to ack quorum");

        return new LedgerMetadataImpl(ledgerId, metadataFormatVersion,
                                      ensembleSize, writeQuorumSize, ackQuorumSize,
                                      state, lastEntryId, length, ensembles,
                                      digestType, password, ctime, storeCtime,
                                      cToken,
                                      customMetadata);
    }

}
