/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.replication.common;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;

import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.replication.protocol.ProtocolVersion;

/**
 * This class is used to associate serverIds with {@link CSN}s.
 * <p>
 * For example, it is exchanged with the replication servers at connection
 * establishment time to communicate "which CSNs was last seen by a serverId".
 */
public class ServerState implements Iterable<CSN>
{

  /** Associates a serverId with a CSN. */
  private final ConcurrentMap<Integer, CSN> serverIdToCSN = new ConcurrentSkipListMap<>();
  /**
   * Whether the state has been saved to persistent storage. It starts at true,
   * and moves to false when an update is made to the current object.
   */
  private volatile boolean saved = true;

  /**
   * Creates a new empty ServerState.
   */
  public ServerState()
  {
    super();
  }

  /**
   * Empty the ServerState.
   * After this call the Server State will be in the same state
   * as if it was just created.
   */
  public void clear()
  {
    serverIdToCSN.clear();
  }

  /**
   * Forward update the Server State with a CSN. The provided CSN will be put on
   * the current object only if it is newer than the existing CSN for the same
   * serverId or if there is no existing CSN.
   *
   * @param csn
   *          The committed CSN.
   * @return a boolean indicating if the update was meaningful.
   */
  public boolean update(CSN csn)
  {
    if (csn == null)
    {
      return false;
    }

    saved = false;

    final int serverId = csn.getServerId();
    while (true)
    {
      final CSN existingCSN = serverIdToCSN.get(serverId);
      if (existingCSN == null)
      {
        if (serverIdToCSN.putIfAbsent(serverId, csn) == null)
        {
          return true;
        }
        // oops, a concurrent modification happened, run the same process again
        continue;
      }
      else if (csn.isNewerThan(existingCSN))
      {
        if (serverIdToCSN.replace(serverId, existingCSN, csn))
        {
          return true;
        }
        // oops, a concurrent modification happened, run the same process again
        continue;
      }
      return false;
    }
  }

  /**
   * Update the Server State with a Server State. Every CSN of this object is
   * updated with the CSN of the passed server state if it is newer.
   *
   * @param serverState the server state to use for the update.
   * @return a boolean indicating if the update was meaningful.
   */
  public boolean update(ServerState serverState)
  {
    if (serverState == null)
    {
      return false;
    }

    boolean updated = false;
    for (CSN csn : serverState.serverIdToCSN.values())
    {
      if (update(csn))
      {
        updated = true;
      }
    }
    return updated;
  }

  /**
   * Removes the mapping to the provided CSN if it is present in this
   * ServerState.
   *
   * @param expectedCSN
   *          the CSN to be removed
   * @return true if the CSN could be removed, false otherwise.
   */
  public boolean removeCSN(CSN expectedCSN)
  {
    if (expectedCSN == null)
    {
      return false;
    }

    if (serverIdToCSN.remove(expectedCSN.getServerId(), expectedCSN))
    {
      saved = false;
      return true;
    }
    return false;
  }

  /**
   * Replace the Server State with another ServerState.
   *
   * @param serverState The ServerState.
   *
   * @return a boolean indicating if the update was meaningful.
   */
  public boolean reload(ServerState serverState) {
    if (serverState == null) {
      return false;
    }

    clear();
    return update(serverState);
  }

  /**
   * Return a Set of String usable as a textual representation of
   * a Server state.
   * format : time seqnum id
   *
   * example :
   *  1 00000109e4666da600220001
   *  2 00000109e44567a600220002
   *
   * @return the representation of the Server state
   */
  public Set<String> toStringSet()
  {
    final Set<String> result = new HashSet<>();
    for (CSN change : serverIdToCSN.values())
    {
      Date date = new Date(change.getTime());
      result.add(change + " " + date + " " + change.getTime());
    }
    return result;
  }

  /**
   * Return an ArrayList of ANS1OctetString encoding the CSNs
   * contained in the ServerState.
   * @return an ArrayList of ANS1OctetString encoding the CSNs
   * contained in the ServerState.
   */
  public ArrayList<ByteString> toASN1ArrayList()
  {
    final ArrayList<ByteString> values = new ArrayList<>(0);
    for (CSN csn : serverIdToCSN.values())
    {
      values.add(ByteString.valueOfUtf8(csn.toString()));
    }
    return values;
  }



  /**
   * Encodes this server state to the provided ASN1 writer.
   *
   * @param writer
   *          The ASN1 writer.
   * @param protocolVersion
   *          The replication protocol version.
   * @throws IOException
   *           If an error occurred during encoding.
   */
  public void writeTo(ASN1Writer writer, short protocolVersion)
      throws IOException
  {
    if (protocolVersion >= ProtocolVersion.REPLICATION_PROTOCOL_V7)
    {
      for (CSN csn : serverIdToCSN.values())
      {
        writer.writeOctetString(csn.toByteString());
      }
    }
    else
    {
      for (CSN csn : serverIdToCSN.values())
      {
        writer.writeOctetString(csn.toString());
      }
    }
  }

  /**
   * Returns a snapshot of this object.
   *
   * @return an unmodifiable List representing a snapshot of this object.
   */
  public List<CSN> getSnapshot()
  {
    if (serverIdToCSN.isEmpty())
    {
      return Collections.emptyList();
    }
    return Collections.unmodifiableList(new ArrayList<CSN>(serverIdToCSN.values()));
  }

  /**
   * Return the text representation of ServerState.
   * @return the text representation of ServerState
   */
  @Override
  public String toString()
  {
    final StringBuilder buffer = new StringBuilder();
    toString(buffer);
    return buffer.toString();
  }

  /**
   * Appends the text representation of ServerState.
   * @param buffer The buffer to which the information should be appended.
   */
  void toString(final StringBuilder buffer)
  {
    boolean first = true;
    for (CSN csn : serverIdToCSN.values())
    {
      if (!first)
      {
        buffer.append(" ");
      }
      csn.toString(buffer);
      first = false;
    }
  }

  /**
   * Returns the {@code CSN} contained in this server state which corresponds to
   * the provided server ID.
   *
   * @param serverId
   *          The server ID.
   * @return The {@code CSN} contained in this server state which
   *         corresponds to the provided server ID.
   */
  public CSN getCSN(int serverId)
  {
    return serverIdToCSN.get(serverId);
  }

  /**
   * Returns a copy of this ServerState's content as a Map of serverId => CSN.
   *
   * @return a copy of this ServerState's content as a Map of serverId => CSN.
   */
  public Map<Integer, CSN> getServerIdToCSNMap()
  {
    // copy to protect from concurrent updates
    // that could change the number of elements in the Map
    return new HashMap<>(serverIdToCSN);
  }

  /** {@inheritDoc} */
  @Override
  public Iterator<CSN> iterator()
  {
    return serverIdToCSN.values().iterator();
  }

  /**
   * Check that all the CSNs in the covered serverState are also in this
   * serverState.
   *
   * @param covered The ServerState that needs to be checked.
   * @return A boolean indicating if this ServerState covers the ServerState
   *         given in parameter.
   */
  public boolean cover(ServerState covered)
  {
    for (CSN coveredChange : covered.serverIdToCSN.values())
    {
      if (!cover(coveredChange))
      {
        return false;
      }
    }
    return true;
  }

  /**
   * Checks that the CSN given as a parameter is in this ServerState.
   *
   * @param   covered The CSN that should be checked.
   * @return  A boolean indicating if this ServerState contains the CSN given in
   *          parameter.
   */
  public boolean cover(CSN covered)
  {
    final CSN csn = this.serverIdToCSN.get(covered.getServerId());
    return csn != null && !csn.isOlderThan(covered);
  }

  /**
   * Tests if the state is empty.
   *
   * @return True if the state is empty.
   */
  public boolean isEmpty()
  {
    return serverIdToCSN.isEmpty();
  }

  /**
   * Make a duplicate of this state.
   * @return The duplicate of this state.
   */
  public ServerState duplicate()
  {
    final ServerState newState = new ServerState();
    newState.serverIdToCSN.putAll(serverIdToCSN);
    return newState;
  }

  /**
   * Computes the number of changes a first server state has in advance
   * compared to a second server state.
   * @param ss1 The server state supposed to be newer than the second one
   * @param ss2 The server state supposed to be older than the first one
   * @return The difference of changes (sum of the differences for each server
   * id changes). 0 If no gap between 2 states.
   * @throws IllegalArgumentException If one of the passed state is null
   */
  public static int diffChanges(ServerState ss1, ServerState ss2)
      throws IllegalArgumentException
  {
    if (ss1 == null || ss2 == null)
    {
      throw new IllegalArgumentException("Null server state(s)");
    }

    int diff = 0;
    for (Integer serverId : ss1.serverIdToCSN.keySet())
    {
      CSN csn1 = ss1.serverIdToCSN.get(serverId);
      if (csn1 != null)
      {
        CSN csn2 = ss2.serverIdToCSN.get(serverId);
        if (csn2 != null)
        {
          diff += CSN.diffSeqNum(csn1, csn2);
        }
        else
        {
          // ss2 does not have a change for this server id but ss1, so the
          // server holding ss1 has every changes represented in csn1 in advance
          // compared to server holding ss2, add this amount
          diff += csn1.getSeqnum();
        }
      }
    }

    return diff;
  }

  /**
   * Set the saved status of this ServerState.
   *
   * @param b A boolean indicating if the State has been safely stored.
   */
  public void setSaved(boolean b)
  {
    saved = b;
  }

  /**
   * Get the saved status of this ServerState.
   *
   * @return The saved status of this ServerState.
   */
  public boolean isSaved()
  {
    return saved;
  }

}
