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

import static org.opends.messages.BackendMessages.*;
import static org.opends.server.backends.pluggable.EntryIDSet.*;
import static org.opends.server.backends.pluggable.IndexFilter.*;

import java.util.ArrayList;
import java.util.Collection;

import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
import org.forgerock.opendj.ldap.spi.IndexingOptions;
import org.forgerock.util.Utils;
import org.opends.server.backends.pluggable.AttributeIndex.IndexFilterType;
import org.opends.server.backends.pluggable.spi.Cursor;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
import org.opends.server.types.AttributeType;

/**
 * This class is an implementation of IndexQueryFactory which creates
 * IndexQuery objects as part of the query to the index.
 */
final class IndexQueryFactoryImpl implements IndexQueryFactory<IndexQuery>
{
  /**
   * This class creates a Null IndexQuery. It is used when there is no
   * record in the index. It may also be used when the index contains
   * all the records but an empty EntryIDSet should be returned as part
   * of the optimization.
   */
  private static final class NullIndexQuery implements IndexQuery
  {
    @Override
    public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage, StringBuilder indexNameOut)
    {
      return newUndefinedSet();
    }

    @Override
    public String toString()
    {
      return "Null";
    }
  }

  /** This class creates an intersection IndexQuery from a collection of IndexQuery objects. */
  private static final class IntersectionIndexQuery implements IndexQuery
  {
    /** Collection of IndexQuery objects. */
    private final Collection<IndexQuery> subIndexQueries;

    /**
     * Creates an instance of IntersectionIndexQuery.
     *
     * @param subIndexQueries
     *          Collection of IndexQuery objects.
     */
    private IntersectionIndexQuery(Collection<IndexQuery> subIndexQueries)
    {
      this.subIndexQueries = subIndexQueries;
    }

    @Override
    public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage, StringBuilder indexNameOut)
    {
      final EntryIDSet entryIDs = newUndefinedSet();
      for (IndexQuery query : subIndexQueries)
      {
        entryIDs.retainAll(query.evaluate(debugMessage, indexNameOut));
        if (isBelowFilterThreshold(entryIDs))
        {
          break;
        }
      }
      return entryIDs;
    }

    @Override
    public String toString()
    {
      return "Intersection(" + SEPARATOR + Utils.joinAsString(SEPARATOR, subIndexQueries) + ")";
    }
  }

  /** This class creates a union of IndexQuery objects. */
  private static final class UnionIndexQuery implements IndexQuery
  {
    /** Collection containing IndexQuery objects. */
    private final Collection<IndexQuery> subIndexQueries;

    /**
     * Creates an instance of UnionIndexQuery.
     *
     * @param subIndexQueries
     *          The Collection of IndexQuery objects.
     */
    private UnionIndexQuery(Collection<IndexQuery> subIndexQueries)
    {
      this.subIndexQueries = subIndexQueries;
    }

    @Override
    public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage, StringBuilder indexNameOut)
    {
      final EntryIDSet entryIDs = newDefinedSet();
      for (IndexQuery query : subIndexQueries)
      {
        entryIDs.addAll(query.evaluate(debugMessage, indexNameOut));
        if (entryIDs.isDefined() && entryIDs.size() >= CURSOR_ENTRY_LIMIT)
        {
          break;
        }
      }
      return entryIDs;
    }

    @Override
    public String toString()
    {
      return "Union(" + SEPARATOR + Utils.joinAsString(SEPARATOR, subIndexQueries) + ")";
    }
  }

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private static final String PRESENCE_INDEX_KEY = "presence";
  private static final String SEPARATOR = "\n  ";

  private final ReadableTransaction txn;
  /** The Map containing the string type identifier and the corresponding index. */
  private final AttributeIndex attributeIndex;

  /**
   * Creates a new IndexQueryFactoryImpl object.
   *
   * @param txn
   *          The readable storage
   * @param attributeIndex
   *          The targeted attribute index
   */
  IndexQueryFactoryImpl(ReadableTransaction txn, AttributeIndex attributeIndex)
  {
    this.txn = txn;
    this.attributeIndex = attributeIndex;
  }

  @Override
  public IndexQuery createExactMatchQuery(final String indexID, final ByteSequence key)
  {
    return new IndexQuery()
      {
        @Override
        public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage, StringBuilder indexNameOut)
        {
          // Read the tree and get Record for the key.
          // Select the right index to be used.
          final Index index = attributeIndex.getNameToIndexes().get(indexID);
          if (index == null)
          {
            appendDisabledIndexType(debugMessage, indexID, attributeIndex.getAttributeType());
            return createMatchAllQuery().evaluate(debugMessage, indexNameOut);
          }

          final EntryIDSet entrySet = index.get(txn, key);
          updateStatsForUndefinedResults(debugMessage, entrySet, index);
          return entrySet;
        }

        @Override
        public String toString()
        {
          return "ExactMatch(" + indexID + "=" + key + ")";
        }
      };
  }

  @Override
  public IndexQuery createRangeMatchQuery(final String indexID, final ByteSequence lowerBound,
      final ByteSequence upperBound, final boolean includeLowerBound, final boolean includeUpperBound)
  {
    return new IndexQuery()
    {
      @Override
      public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage, StringBuilder indexNameOut)
      {
        final Index index = attributeIndex.getNameToIndexes().get(indexID);
        if (index == null)
        {
          appendDisabledIndexType(debugMessage, indexID, attributeIndex.getAttributeType());
          return createMatchAllQuery().evaluate(debugMessage, indexNameOut);
        }

        final EntryIDSet entrySet = readRange(index, txn, lowerBound, upperBound, includeLowerBound, includeUpperBound);
        updateStatsForUndefinedResults(debugMessage, entrySet, index);
        return entrySet;
      }

      private final EntryIDSet readRange(Index index, ReadableTransaction txn, ByteSequence lower, ByteSequence upper,
          boolean lowerIncluded, boolean upperIncluded)
      {
        // If this index is not trusted, then just return an undefined id set.
        if (!index.isTrusted())
        {
          return newUndefinedSet();
        }

        try
        {
          // Total number of IDs found so far.
          int totalIDCount = 0;
          ArrayList<EntryIDSet> sets = new ArrayList<>();
          Cursor<ByteString, EntryIDSet> cursor = index.openCursor(txn);
          try
          {
            boolean success;
            // Set the lower bound if necessary.
            if (lower.length() > 0)
            {
              // Initialize the cursor to the lower bound.
              success = cursor.positionToKeyOrNext(lower);

              // Advance past the lower bound if necessary.
              if (success && !lowerIncluded && cursor.getKey().equals(lower))
              {
                // Do not include the lower value.
                success = cursor.next();
              }
            }
            else
            {
              success = cursor.next();
            }

            if (!success)
            {
              // There are no values.
              return EntryIDSet.newDefinedSet();
            }

            // Step through the keys until we hit the upper bound or the last key.
            while (success)
            {
              // Check against the upper bound if necessary
              if (upper.length() > 0)
              {
                int cmp = cursor.getKey().compareTo(upper);
                if (cmp > 0 || (cmp == 0 && !upperIncluded))
                {
                  break;
                }
              }

              EntryIDSet set = cursor.getValue();
              if (!set.isDefined())
              {
                // There is no point continuing.
                return set;
              }
              totalIDCount += set.size();
              if (totalIDCount > IndexFilter.CURSOR_ENTRY_LIMIT)
              {
                // There are too many. Give up and return an undefined list.
                // Use any key to have debugsearchindex return LIMIT-EXCEEDED instead of NOT-INDEXED.
                return newUndefinedSetWithKey(cursor.getKey());
              }
              sets.add(set);
              success = cursor.next();
            }

            return EntryIDSet.newSetFromUnion(sets);
          }
          finally
          {
            cursor.close();
          }
        }
        catch (StorageRuntimeException e)
        {
          logger.traceException(e);
          return newUndefinedSet();
        }
      }

        @Override
        public String toString()
        {
          final StringBuilder sb = new StringBuilder("RangeMatch(");
          sb.append(lowerBound).append(" ");
          sb.append(includeLowerBound ? "<=" : "<").append(" ");
          sb.append(indexID).append(" ");
          sb.append(includeUpperBound ? ">=" : ">").append(" ");
          sb.append(upperBound);
          sb.append(")");
          return sb.toString();
        }
      };
  }

  @Override
  public IndexQuery createIntersectionQuery(Collection<IndexQuery> subqueries)
  {
    return new IntersectionIndexQuery(subqueries);
  }

  @Override
  public IndexQuery createUnionQuery(Collection<IndexQuery> subqueries)
  {
    return new UnionIndexQuery(subqueries);
  }

  /**
   * {@inheritDoc}
   * <p>
   * It returns an empty EntryIDSet object when either all or no record
   * sets are requested.
   */
  @Override
  public IndexQuery createMatchAllQuery()
  {
    return new IndexQuery()
      {
        @Override
        public EntryIDSet evaluate(LocalizableMessageBuilder debugMessage, StringBuilder indexNameOut)
        {
          final String indexID = PRESENCE_INDEX_KEY;
          final Index index = attributeIndex.getNameToIndexes().get(indexID);
          if (index == null)
          {
            appendDisabledIndexType(debugMessage, indexID, attributeIndex.getAttributeType());
            return newUndefinedSet();
          }

          final EntryIDSet entrySet = index.get(txn, AttributeIndex.PRESENCE_KEY);
          updateStatsForUndefinedResults(debugMessage, entrySet, index);
          if (indexNameOut != null)
          {
            indexNameOut.append(IndexFilterType.PRESENCE);
          }
          return entrySet;
        }

        @Override
        public String toString()
        {
          return "MatchAll(" + PRESENCE_INDEX_KEY + ")";
        }
      };
  }

  private static void appendDisabledIndexType(LocalizableMessageBuilder debugMessage, String indexID,
      AttributeType attrType)
  {
    if (debugMessage != null)
    {
      debugMessage.append(INFO_INDEX_FILTER_INDEX_TYPE_DISABLED.get(indexID, attrType.getNameOrOID()));
    }
  }

  private static void updateStatsForUndefinedResults(
      LocalizableMessageBuilder debugMessage, EntryIDSet idSet, Index index)
  {
    if (debugMessage != null && !idSet.isDefined())
    {
      if (!index.isTrusted())
      {
        debugMessage.append(INFO_INDEX_FILTER_INDEX_NOT_TRUSTED.get(index.getName()));
      }
      else
      {
        debugMessage.append(INFO_INDEX_FILTER_INDEX_LIMIT_EXCEEDED.get(index.getName()));
      }
    }
  }

  @Override
  public IndexingOptions getIndexingOptions()
  {
    return attributeIndex.getIndexingOptions();
  }

  /**
   * Creates an empty IndexQuery object.
   *
   * @return A NullIndexQuery object.
   */
  static IndexQuery createNullIndexQuery()
  {
    return new NullIndexQuery();
  }
}
