/**
 * 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.ratis.server.impl;

import org.apache.ratis.util.Preconditions;
import org.apache.ratis.util.StringUtils;

import java.util.*;

/**
 * Maintain the mappings between log index and corresponding raft configuration.
 * Initialized when starting the raft peer. The mappings are loaded from the
 * raft log, and updated while appending/truncating configuration related log
 * entries.
 */
public class ConfigurationManager {
  private final RaftConfiguration initialConf;
  private final NavigableMap<Long, RaftConfiguration> configurations =
      new TreeMap<>();
  /**
   * The current raft configuration. If configurations is not empty, should be
   * the last entry of the map. Otherwise is initialConf.
   */
  private RaftConfiguration currentConf;

  ConfigurationManager(RaftConfiguration initialConf) {
    this.initialConf = initialConf;
    this.currentConf = initialConf;
  }

  synchronized void addConfiguration(long logIndex, RaftConfiguration conf) {
    final RaftConfiguration found = configurations.get(logIndex);
    if (found != null) {
      Preconditions.assertTrue(found.equals(conf));
      return;
    }
    if (!configurations.isEmpty()) {
      final Map.Entry<Long, RaftConfiguration> lastEntry = configurations.lastEntry();
      Preconditions.assertTrue(lastEntry.getKey() < logIndex,
          () -> "lastEntry = " + lastEntry + ", lastEntry.index >= logIndex = " + logIndex);
    }
    configurations.put(logIndex, conf);
    this.currentConf = conf;
  }

  synchronized RaftConfiguration getCurrent() {
    return currentConf;
  }

  /**
   * Remove all the configurations whose log index is >= the given index.
   * @param index The given index. All the configurations whose log index is >=
   *              this value will be removed.
   * @return The configuration with largest log index < the given index.
   */
  synchronized RaftConfiguration removeConfigurations(long index) {
    SortedMap<Long, RaftConfiguration> toRemove = configurations.tailMap(index);
    for (Iterator<Map.Entry<Long, RaftConfiguration>> iter =
         toRemove.entrySet().iterator(); iter.hasNext();) {
      iter.next();
      iter.remove();
    }
    currentConf = configurations.isEmpty() ? initialConf :
        configurations.lastEntry().getValue();
    return currentConf;
  }

  synchronized int numOfConf() {
    return 1 + configurations.size();
  }

  @Override
  public synchronized String toString() {
    return getClass().getSimpleName() + ", init=" + initialConf + ", confs=" + StringUtils.map2String(configurations);
  }

  // TODO: remove Configuration entries after they are committed
}