/*
 * 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 2007-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.server.core;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.api.Backend;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;

import static org.forgerock.util.Reject.*;
import static org.opends.messages.CoreMessages.*;

/**
 * Registry for maintaining the set of registered base DN's, associated backends
 * and naming context information.
 */
public class BaseDnRegistry {

  /** The set of base DNs registered with the server. */
  private final TreeMap<DN, Backend> baseDNs = new TreeMap<>();
  /** The set of private naming contexts registered with the server. */
  private final TreeMap<DN, Backend> privateNamingContexts = new TreeMap<>();
  /** The set of public naming contexts registered with the server. */
  private final TreeMap<DN, Backend> publicNamingContexts = new TreeMap<>();

  /**
   * Indicates whether or not this base DN registry is in test mode.
   * A registry instance that is in test mode will not modify backend
   * objects referred to in the above maps.
   */
  private boolean testOnly;

  /**
   * Registers a base DN with this registry.
   *
   * @param  baseDN to register
   * @param  backend with which the base DN is associated
   * @param  isPrivate indicates whether or not this base DN is private
   * @return list of error messages generated by registering the base DN
   *         that should be logged if the changes to this registry are
   *         committed to the server
   * @throws DirectoryException if the base DN cannot be registered
   */
  public List<LocalizableMessage> registerBaseDN(DN baseDN, Backend<?> backend, boolean isPrivate)
      throws DirectoryException
  {
    // Check to see if the base DN is already registered with the server.
    Backend<?> existingBackend = baseDNs.get(baseDN);
    if (existingBackend != null)
    {
      LocalizableMessage message = ERR_REGISTER_BASEDN_ALREADY_EXISTS.
          get(baseDN, backend.getBackendID(), existingBackend.getBackendID());
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
    }


    // Check to see if the backend is already registered with the server for
    // any other base DN(s).  The new base DN must not have any hierarchical
    // relationship with any other base Dns for the same backend.
    LinkedList<DN> otherBaseDNs = new LinkedList<>();
    for (DN dn : baseDNs.keySet())
    {
      Backend<?> b = baseDNs.get(dn);
      if (b.equals(backend))
      {
        otherBaseDNs.add(dn);

        if (baseDN.isAncestorOf(dn) || baseDN.isDescendantOf(dn))
        {
          LocalizableMessage message = ERR_REGISTER_BASEDN_HIERARCHY_CONFLICT.
              get(baseDN, backend.getBackendID(), dn);
          throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
        }
      }
    }


    // Check to see if the new base DN is subordinate to any other base DN
    // already defined.  If it is, then any other base DN(s) for the same
    // backend must also be subordinate to the same base DN.
    final Backend<?> superiorBackend = getSuperiorBackend(baseDN, otherBaseDNs, backend.getBackendID());
    if (superiorBackend == null && backend.getParentBackend() != null)
    {
      LocalizableMessage message = ERR_REGISTER_BASEDN_NEW_BASE_NOT_SUBORDINATE.
          get(baseDN, backend.getBackendID(), backend.getParentBackend().getBackendID());
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
    }


    // Check to see if the new base DN should be the superior base DN for any
    // other base DN(s) already defined.
    LinkedList<Backend<?>> subordinateBackends = new LinkedList<>();
    LinkedList<DN>      subordinateBaseDNs  = new LinkedList<>();
    for (DN dn : baseDNs.keySet())
    {
      Backend<?> b = baseDNs.get(dn);
      DN parentDN = dn.parent();
      while (parentDN != null)
      {
        if (parentDN.equals(baseDN))
        {
          subordinateBaseDNs.add(dn);
          subordinateBackends.add(b);
          break;
        }
        else if (baseDNs.containsKey(parentDN))
        {
          break;
        }

        parentDN = parentDN.parent();
      }
    }


    // If we've gotten here, then the new base DN is acceptable.  If we should
    // actually apply the changes then do so now.
    final List<LocalizableMessage> errors = new LinkedList<>();

    // Check to see if any of the registered backends already contain an
    // entry with the DN specified as the base DN.  This could happen if
    // we're creating a new subordinate backend in an existing directory
    // (e.g., moving the "ou=People,dc=example,dc=com" branch to its own
    // backend when that data already exists under the "dc=example,dc=com"
    // backend).  This condition shouldn't prevent the new base DN from
    // being registered, but it's definitely important enough that we let
    // the administrator know about it and remind them that the existing
    // backend will need to be reinitialized.
    if (superiorBackend != null && superiorBackend.entryExists(baseDN))
    {
      errors.add(WARN_REGISTER_BASEDN_ENTRIES_IN_MULTIPLE_BACKENDS.
          get(superiorBackend.getBackendID(), baseDN, backend.getBackendID()));
    }


    baseDNs.put(baseDN, backend);

    if (superiorBackend == null)
    {
      if (!testOnly)
      {
        backend.setPrivateBackend(isPrivate);
      }

      if (isPrivate)
      {
        privateNamingContexts.put(baseDN, backend);
      }
      else
      {
        publicNamingContexts.put(baseDN, backend);
      }
    }
    else if (otherBaseDNs.isEmpty() && !testOnly)
    {
      backend.setParentBackend(superiorBackend);
      superiorBackend.addSubordinateBackend(backend);
    }

    if (!testOnly)
    {
      for (Backend<?> b : subordinateBackends)
      {
        Backend<?> oldParentBackend = b.getParentBackend();
        if (oldParentBackend != null)
        {
          oldParentBackend.removeSubordinateBackend(b);
        }

        b.setParentBackend(backend);
        backend.addSubordinateBackend(b);
      }
    }

    for (DN dn : subordinateBaseDNs)
    {
      publicNamingContexts.remove(dn);
      privateNamingContexts.remove(dn);
    }

    return errors;
  }

  private Backend<?> getSuperiorBackend(DN baseDN, LinkedList<DN> otherBaseDNs, String backendID)
      throws DirectoryException
  {
    Backend<?> superiorBackend = null;
    DN parentDN = baseDN.parent();
    while (parentDN != null)
    {
      if (baseDNs.containsKey(parentDN))
      {
        superiorBackend = baseDNs.get(parentDN);

        for (DN dn : otherBaseDNs)
        {
          if (!dn.isDescendantOf(parentDN))
          {
            LocalizableMessage msg = ERR_REGISTER_BASEDN_DIFFERENT_PARENT_BASES.get(baseDN, backendID, dn);
            throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, msg);
          }
        }
        break;
      }

      parentDN = parentDN.parent();
    }
    return superiorBackend;
  }


  /**
   * Deregisters a base DN with this registry.
   *
   * @param  baseDN to deregister
   * @return list of error messages generated by deregistering the base DN
   *         that should be logged if the changes to this registry are
   *         committed to the server
   * @throws DirectoryException if the base DN could not be deregistered
   */
  public List<LocalizableMessage> deregisterBaseDN(DN baseDN)
         throws DirectoryException
  {
    ifNull(baseDN);

    // Make sure that the Directory Server actually contains a backend with
    // the specified base DN.
    Backend<?> backend = baseDNs.get(baseDN);
    if (backend == null)
    {
      LocalizableMessage message =
          ERR_DEREGISTER_BASEDN_NOT_REGISTERED.get(baseDN);
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
    }


    // Check to see if the backend has a parent backend, and whether it has
    // any subordinates with base DNs that are below the base DN to remove.
    Backend<?>             superiorBackend     = backend.getParentBackend();
    LinkedList<Backend<?>> subordinateBackends = new LinkedList<>();
    if (backend.getSubordinateBackends() != null)
    {
      for (Backend<?> b : backend.getSubordinateBackends())
      {
        for (DN dn : b.getBaseDNs())
        {
          if (dn.isDescendantOf(baseDN))
          {
            subordinateBackends.add(b);
            break;
          }
        }
      }
    }


    // See if there are any other base DNs registered within the same backend.
    LinkedList<DN> otherBaseDNs = new LinkedList<>();
    for (DN dn : baseDNs.keySet())
    {
      if (dn.equals(baseDN))
      {
        continue;
      }

      Backend<?> b = baseDNs.get(dn);
      if (backend.equals(b))
      {
        otherBaseDNs.add(dn);
      }
    }


    // If we've gotten here, then it's OK to make the changes.

    // Get rid of the references to this base DN in the mapping tree
    // information.
    baseDNs.remove(baseDN);
    publicNamingContexts.remove(baseDN);
    privateNamingContexts.remove(baseDN);

    final LinkedList<LocalizableMessage> errors = new LinkedList<>();
    if (superiorBackend == null)
    {
      // If there were any subordinate backends, then all of their base DNs
      // will now be promoted to naming contexts.
      for (Backend<?> b : subordinateBackends)
      {
        if (!testOnly)
        {
          b.setParentBackend(null);
          backend.removeSubordinateBackend(b);
        }

        for (DN dn : b.getBaseDNs())
        {
          if (b.isPrivateBackend())
          {
            privateNamingContexts.put(dn, b);
          }
          else
          {
            publicNamingContexts.put(dn, b);
          }
        }
      }
    }
    else
    {
      // If there are no other base DNs for the associated backend, then
      // remove this backend as a subordinate of the parent backend.
      if (otherBaseDNs.isEmpty() && !testOnly)
      {
        superiorBackend.removeSubordinateBackend(backend);
      }


      // If there are any subordinate backends, then they need to be made
      // subordinate to the parent backend.  Also, we should log a warning
      // message indicating that there may be inconsistent search results
      // because some of the structural entries will be missing.
      if (! subordinateBackends.isEmpty())
      {
        // Suppress this warning message on server shutdown.
        if (!DirectoryServer.getInstance().isShuttingDown()) {
          errors.add(WARN_DEREGISTER_BASEDN_MISSING_HIERARCHY.get(
              baseDN, backend.getBackendID()));
        }

        if (!testOnly)
        {
          for (Backend<?> b : subordinateBackends)
          {
            backend.removeSubordinateBackend(b);
            superiorBackend.addSubordinateBackend(b);
            b.setParentBackend(superiorBackend);
          }
        }
      }
    }
    return errors;
  }


  /**
   * Creates a default instance.
   */
  BaseDnRegistry()
  {
    this(false);
  }

  /**
   * Returns a copy of this registry.
   *
   * @return copy of this registry
   */
  BaseDnRegistry copy()
  {
    final BaseDnRegistry registry = new BaseDnRegistry(true);
    registry.baseDNs.putAll(baseDNs);
    registry.publicNamingContexts.putAll(publicNamingContexts);
    registry.privateNamingContexts.putAll(privateNamingContexts);
    return registry;
  }


  /**
   * Creates a parameterized instance.
   *
   * @param testOnly indicates whether this registry will be used for testing;
   *        when <code>true</code> this registry will not modify backends
   */
  private BaseDnRegistry(boolean testOnly)
  {
    this.testOnly = testOnly;
  }


  /**
   * Gets the mapping of registered base DNs to their associated backend.
   *
   * @return mapping from base DN to backend
   */
  Map<DN,Backend> getBaseDnMap() {
    return this.baseDNs;
  }


  /**
   * Gets the mapping of registered public naming contexts to their
   * associated backend.
   *
   * @return mapping from naming context to backend
   */
  Map<DN,Backend> getPublicNamingContextsMap() {
    return this.publicNamingContexts;
  }


  /**
   * Gets the mapping of registered private naming contexts to their
   * associated backend.
   *
   * @return mapping from naming context to backend
   */
  Map<DN,Backend> getPrivateNamingContextsMap() {
    return this.privateNamingContexts;
  }




  /**
   * Indicates whether the specified DN is contained in this registry as
   * a naming contexts.
   *
   * @param  dn  The DN for which to make the determination.
   *
   * @return  {@code true} if the specified DN is a naming context in this
   *          registry, or {@code false} if it is not.
   */
  boolean containsNamingContext(DN dn)
  {
    return privateNamingContexts.containsKey(dn) || publicNamingContexts.containsKey(dn);
  }


  /**
   * Clear and nullify this registry's internal state.
   */
  void clear() {
    baseDNs.clear();
    privateNamingContexts.clear();
    publicNamingContexts.clear();
  }

}
