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

import static org.opends.server.util.CollectionUtils.*;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.LDAPURL;
import org.opends.server.types.MemberList;
import org.opends.server.types.MembershipException;
import org.opends.server.types.SearchFilter;

/**
 * This class defines a mechanism that may be used to iterate over the
 * members of a dynamic group, optionally using an additional set of
 * criteria to further filter the results.
 */
public class DynamicGroupMemberList
       extends MemberList
{
  /** Indicates whether the search thread has completed its processing. */
  private boolean searchesCompleted;

  /** The base DN to use when filtering the set of group members. */
  private final DN baseDN;

  /** The DN of the entry containing the group definition. */
  private final DN groupDN;

  /**
   * The queue into which results will be placed while they are waiting to be
   * returned.  The types of objects that may be placed in this queue are Entry
   * objects to return or MembershipException objects to throw.
   */
  private final LinkedBlockingQueue<Object> resultQueue;

  /** The search filter to use when filtering the set of group members. */
  private final SearchFilter filter;

  /** The search scope to use when filtering the set of group members. */
  private final SearchScope scope;

  /** The set of LDAP URLs that define the membership criteria. */
  private final Set<LDAPURL> memberURLs;



  /**
   * Creates a new dynamic group member list with the provided information.
   *
   * @param  groupDN     The DN of the entry containing the group definition.
   * @param  memberURLs  The set of LDAP URLs that define the membership
   *                     criteria for the associated group.
   *
   * @throws  DirectoryException  If a problem occurs while creating the member
   *                              list.
   */
  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs)
         throws DirectoryException
  {
    this(groupDN, memberURLs, null, null, null);
  }



  /**
   * Creates a new dynamic group member list with the provided information.
   *
   * @param  groupDN     The DN of the entry containing the group definition.
   * @param  memberURLs  The set of LDAP URLs that define the membership
   *                     criteria for the associated group.
   * @param  baseDN      The base DN that should be enforced for all entries to
   *                     return.
   * @param  scope       The scope that should be enforced for all entries to
   *                     return.
   * @param  filter      The filter that should be enforced for all entries to
   *                     return.
   *
   * @throws  DirectoryException  If a problem occurs while creating the member
   *                              list.
   */
  public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs,
                                DN baseDN, SearchScope scope,
                                SearchFilter filter)
         throws DirectoryException
  {
    this.groupDN    = groupDN;
    this.memberURLs = memberURLs;
    this.baseDN     = baseDN;
    this.filter     = filter;

    if (scope == null)
    {
      this.scope = SearchScope.WHOLE_SUBTREE;
    }
    else
    {
      this.scope = scope;
    }

    searchesCompleted = false;
    resultQueue = new LinkedBlockingQueue<>(10);


    // We're going to have to perform one or more internal searches in order to
    // get the results.  We need to be careful about the way that we construct
    // them in order to avoid the possibility of getting duplicate results, so
    // searches with overlapping bases will need to be combined.
    LinkedHashMap<DN,LinkedList<LDAPURL>> baseDNs = new LinkedHashMap<>();
    for (LDAPURL memberURL : memberURLs)
    {
      // First, determine the base DN for the search.  It needs to be evaluated
      // as relative to both the overall base DN specified in the set of
      // criteria, as well as any other existing base DNs in the same hierarchy.
      DN urlBaseDN = memberURL.getBaseDN();
      if (baseDN != null)
      {
        if (baseDN.isDescendantOf(urlBaseDN))
        {
          // The base DN requested by the user is below the base DN for this
          // URL, so we'll use the base DN requested by the user.
          urlBaseDN = baseDN;
        }
        else if (! urlBaseDN.isDescendantOf(baseDN))
        {
          // The base DN from the URL is outside the base requested by the user,
          // so we can skip this URL altogether.
          continue;
        }
      }

      // If this is the first URL, then we can just add it with the base DN.
      // Otherwise, we need to see if it needs to be merged with other URLs in
      // the same hierarchy.
      if (baseDNs.isEmpty())
      {
        baseDNs.put(urlBaseDN, newLinkedList(memberURL));
      }
      else
      {
        // See if the specified base DN is already in the map.  If so, then
        // just add the new URL to the existing list.
        LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
        if (urlList == null)
        {
          // There's no existing list for the same base DN, but there might be
          // DNs in an overlapping hierarchy.  If so, then use the base DN that
          // is closest to the naming context.  If not, then add a new list with
          // the current base DN.
          boolean found = false;
          Iterator<DN> iterator = baseDNs.keySet().iterator();
          while (iterator.hasNext())
          {
            DN existingBaseDN = iterator.next();
            if (urlBaseDN.isDescendantOf(existingBaseDN))
            {
              // The base DN for the current URL is below an existing base DN,
              // so we can just add this URL to the existing list and be done.
              urlList = baseDNs.get(existingBaseDN);
              urlList.add(memberURL);
              found = true;
              break;
            }
            else if (existingBaseDN.isDescendantOf(urlBaseDN))
            {
              // The base DN for the current URL is above the existing base DN,
              // so we should use the base DN for the current URL instead of the
              // existing one.
              urlList = baseDNs.get(existingBaseDN);
              urlList.add(memberURL);
              iterator.remove();
              baseDNs.put(urlBaseDN, urlList);
              found = true;
              break;
            }
          }

          if (! found)
          {
            baseDNs.put(urlBaseDN, newLinkedList(memberURL));
          }
        }
        else
        {
          // There was already a list with the same base DN, so just add the URL.
          urlList.add(memberURL);
        }
      }
    }


    // At this point, we should know what base DN(s) we need to use, so we can
    // create the filter to use with that base DN.  There are some special-case
    // optimizations that we can do here, but in general the filter will look
    // like "(&(filter)(|(urlFilters)))".
    LinkedHashMap<DN,SearchFilter> searchMap = new LinkedHashMap<>();
    for (DN urlBaseDN : baseDNs.keySet())
    {
      LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN);
      LinkedHashSet<SearchFilter> urlFilters = new LinkedHashSet<>();
      for (LDAPURL url : urlList)
      {
        urlFilters.add(url.getFilter());
      }

      SearchFilter combinedFilter;
      if (filter == null)
      {
        if (urlFilters.size() == 1)
        {
          combinedFilter = urlFilters.iterator().next();
        }
        else
        {
          combinedFilter = SearchFilter.createORFilter(urlFilters);
        }
      }
      else
      {
        if (urlFilters.size() == 1)
        {
          SearchFilter urlFilter = urlFilters.iterator().next();
          if (urlFilter.equals(filter))
          {
            combinedFilter = filter;
          }
          else
          {
            LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>();
            filterSet.add(filter);
            filterSet.add(urlFilter);
            combinedFilter = SearchFilter.createANDFilter(filterSet);
          }
        }
        else
        {
          if (urlFilters.contains(filter))
          {
            combinedFilter = filter;
          }
          else
          {
            LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>();
            filterSet.add(filter);
            filterSet.add(SearchFilter.createORFilter(urlFilters));
            combinedFilter = SearchFilter.createANDFilter(filterSet);
          }
        }
      }

      searchMap.put(urlBaseDN, combinedFilter);
    }


    // At this point, we should have all the information we need to perform the
    // searches.  Create arrays of the elements for each.
    DN[]           baseDNArray = new DN[baseDNs.size()];
    SearchFilter[] filterArray = new SearchFilter[baseDNArray.length];
    LDAPURL[][]    urlArray    = new LDAPURL[baseDNArray.length][];
    Iterator<DN> iterator = baseDNs.keySet().iterator();
    for (int i=0; i < baseDNArray.length; i++)
    {
      baseDNArray[i] = iterator.next();
      filterArray[i] = searchMap.get(baseDNArray[i]);

      LinkedList<LDAPURL> urlList = baseDNs.get(baseDNArray[i]);
      urlArray[i] = new LDAPURL[urlList.size()];
      int j=0;
      for (LDAPURL url : urlList)
      {
        urlArray[i][j++] = url;
      }
    }


    DynamicGroupSearchThread searchThread =
         new DynamicGroupSearchThread(this, baseDNArray, filterArray, urlArray);
    searchThread.start();
  }



  /**
   * Retrieves the DN of the dynamic group with which this dynamic group member
   * list is associated.
   *
   * @return  The DN of the dynamic group with which this dynamic group member
   *          list is associated.
   */
  public final DN getDynamicGroupDN()
  {
    return groupDN;
  }



  /**
   * Indicates that all of the searches needed to iterate across the member list
   * have completed and there will not be any more results provided.
   */
  final void setSearchesCompleted()
  {
    searchesCompleted = true;
  }



  /**
   * Adds the provided entry to the set of results that should be returned for
   * this member list.
   *
   * @param  entry  The entry to add to the set of results that should be
   *                returned for this member list.
   *
   * @return  {@code true} if the entry was added to the result set, or
   *          {@code false} if it was not (either because a timeout expired or
   *          the attempt was interrupted).  If this method returns
   *          {@code false}, then the search thread should terminate
   *          immediately.
   */
  final boolean addResult(Entry entry)
  {
    try
    {
      return resultQueue.offer(entry, 10, TimeUnit.SECONDS);
    }
    catch (InterruptedException ie)
    {
      return false;
    }
  }



  /**
   * Adds the provided membership exception so that it will be thrown along with
   * the set of results for this member list.
   *
   * @param  membershipException  The membership exception to be thrown.
   *
   * @return  {@code true} if the exception was added to the result set, or
   *          {@code false} if it was not (either because a timeout expired or
   *          the attempt was interrupted).  If this method returns
   *          {@code false}, then the search thread should terminate
   *          immediately.
   */
  final boolean addResult(MembershipException membershipException)
  {
    try
    {
      return resultQueue.offer(membershipException, 10, TimeUnit.SECONDS);
    }
    catch (InterruptedException ie)
    {
      return false;
    }
  }



  /** {@inheritDoc} */
  @Override
  public boolean hasMoreMembers()
  {
    while (! searchesCompleted)
    {
      if (resultQueue.peek() != null)
      {
        return true;
      }

      try
      {
        Thread.sleep(0, 1000);
      } catch (Exception e) {}
    }

    return resultQueue.peek() != null;
  }



  /** {@inheritDoc} */
  @Override
  public Entry nextMemberEntry()
         throws MembershipException
  {
    if (! hasMoreMembers())
    {
      return null;
    }

    Object result = resultQueue.poll();
    if (result == null)
    {
      close();
      return null;
    }
    else if (result instanceof Entry)
    {
      return (Entry) result;
    }
    else if (result instanceof MembershipException)
    {
      MembershipException me = (MembershipException) result;
      if (! me.continueIterating())
      {
        close();
      }

      throw me;
    }

    // We should never get here.
    close();
    return null;
  }



  /** {@inheritDoc} */
  @Override
  public void close()
  {
    searchesCompleted = true;
    resultQueue.clear();
  }
}

