/*
 * 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 2014-2015 ForgeRock AS
 */
package org.opends.server.backends.pluggable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.admin.std.server.MonitorProviderCfg;
import org.opends.server.api.MonitorProvider;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.Attributes;
import org.opends.server.types.SearchFilter;
import org.opends.server.util.TimeThread;

/**
 * A monitor provider for high level backend statistics, such as filter stats and search counters.
 */
class BackendMonitor extends MonitorProvider<MonitorProviderCfg>
{
  /** Represents the statistical information kept for each search filter. */
  private static final class FilterStats implements Comparable<FilterStats>
  {
    private volatile LocalizableMessage failureReason = LocalizableMessage.EMPTY;
    private long maxMatchingEntries = -1;
    private final AtomicInteger hits = new AtomicInteger();

    @Override
    public int compareTo(FilterStats that)
    {
      return this.hits.get() - that.hits.get();
    }

    private void update(int hitCount, LocalizableMessage failureReason)
    {
      this.hits.getAndAdd(hitCount);
      this.failureReason = failureReason;
    }

    private void update(int hitCount, long matchingEntries)
    {
      this.hits.getAndAdd(hitCount);
      this.failureReason = LocalizableMessage.EMPTY;
      synchronized (this)
      {
        if (matchingEntries > maxMatchingEntries)
        {
          maxMatchingEntries = matchingEntries;
        }
      }
    }
  }

  /** The name of this monitor instance. */
  private final String name;
  /** The root container to be monitored. */
  private final RootContainer rootContainer;

  private int maxEntries = 1024;
  private boolean filterUseEnabled;
  private String startTimeStamp;
  private final HashMap<SearchFilter, FilterStats> filterToStats = new HashMap<>();
  private final AtomicInteger indexedSearchCount = new AtomicInteger();
  private final AtomicInteger unindexedSearchCount = new AtomicInteger();

  /**
   * Creates a new backend monitor.
   * @param name The monitor instance name.
   * @param rootContainer A root container handle for the backend to be
   * monitored.
   */
  BackendMonitor(String name, RootContainer rootContainer)
  {
    this.name = name;
    this.rootContainer = rootContainer;
  }

  @Override
  public String getMonitorInstanceName()
  {
    return name;
  }

  /**
   * Retrieves a set of attributes containing monitor data that should be
   * returned to the client if the corresponding monitor entry is requested.
   *
   * @return A set of attributes containing monitor data that should be
   *         returned to the client if the corresponding monitor entry is requested.
   */
  @Override
  public List<Attribute> getMonitorData()
  {
    List<Attribute> monitorAttrs = new ArrayList<>();

    AttributeBuilder needReindex = createNeedReindex("need-reindex");
    if (needReindex.size() > 0)
    {
      monitorAttrs.add(needReindex.toAttribute());
    }

    if (filterUseEnabled)
    {
      monitorAttrs.add(createAttribute("filter-use-startTime", startTimeStamp));
      monitorAttrs.add(createFilterUse("filter-use"));
      monitorAttrs.add(createAttribute("filter-use-indexed", indexedSearchCount));
      monitorAttrs.add(createAttribute("filter-use-unindexed", unindexedSearchCount));
    }

    return monitorAttrs;
  }

  private AttributeBuilder createNeedReindex(String attrName)
  {
    AttributeBuilder needReindex = new AttributeBuilder(attrName);
    for (EntryContainer ec : rootContainer.getEntryContainers())
    {
      for (Tree tree : ec.listTrees())
      {
        if (tree instanceof Index && !((Index) tree).isTrusted())
        {
          needReindex.add(tree.getName().toString());
        }
      }
    }
    return needReindex;
  }

  private Attribute createFilterUse(String attrName)
  {
    AttributeBuilder builder = new AttributeBuilder(attrName);

    StringBuilder value = new StringBuilder();
    synchronized (filterToStats)
    {
      for (Map.Entry<SearchFilter, FilterStats> entry : filterToStats.entrySet())
      {
        entry.getKey().toString(value);
        value.append(" hits:").append(entry.getValue().hits.get());
        value.append(" maxmatches:").append(entry.getValue().maxMatchingEntries);
        value.append(" message:").append(entry.getValue().failureReason);
        builder.add(value.toString());
        value.setLength(0);
      }
    }
    return builder.toAttribute();
  }

  private Attribute createAttribute(String attrName, Object value)
  {
    return Attributes.create(attrName, String.valueOf(value));
  }

  /**
   * Updates the index filter statistics with this latest search filter
   * and the reason why an index was not used.
   *
   * @param searchFilter The search filter that was evaluated.
   * @param failureMessage The reason why an index was not used.
   */
  void updateStats(SearchFilter searchFilter, LocalizableMessage failureMessage)
  {
    if (!filterUseEnabled)
    {
      return;
    }

    FilterStats stats;
    synchronized (filterToStats)
    {
      stats = filterToStats.get(searchFilter);

      if (stats != null)
      {
        stats.update(1, failureMessage);
      }
      else
      {
        stats = new FilterStats();
        stats.update(1, failureMessage);
        removeLowestHit();
        filterToStats.put(searchFilter, stats);
      }
    }
  }

  /**
   * Updates the index filter statistics with this latest search filter
   * and the number of entries matched by the index lookup.
   *
   * @param searchFilter The search filter that was evaluated.
   * @param matchingEntries The number of entries matched by the successful
   *                        index lookup.
   */
  void updateStats(SearchFilter searchFilter, long matchingEntries)
  {
    if (!filterUseEnabled)
    {
      return;
    }

    FilterStats stats;
    synchronized (filterToStats)
    {
      stats = filterToStats.get(searchFilter);

      if (stats != null)
      {
        stats.update(1, matchingEntries);
      }
      else
      {
        stats = new FilterStats();
        stats.update(1, matchingEntries);
        removeLowestHit();
        filterToStats.put(searchFilter, stats);
      }
    }
  }

  /**
   * Enable or disable index filter statistics gathering.
   *
   * @param enabled <code>true></code> to enable index filter statics gathering.
   */
  void enableFilterUseStats(boolean enabled)
  {
    if (enabled && !filterUseEnabled)
    {
      startTimeStamp = TimeThread.getGMTTime();
      indexedSearchCount.set(0);
      unindexedSearchCount.set(0);
    }
    else if (!enabled)
    {
      filterToStats.clear();
    }
    filterUseEnabled = enabled;
  }

  /**
   * Indicates if index filter statistics gathering is enabled.
   *
   * @return <code>true</code> If index filter statistics gathering is enabled.
   */
  boolean isFilterUseEnabled()
  {
    return filterUseEnabled;
  }

  /**
   * Sets the maximum number of search filters statistics entries to keep
   * before ones with the least hits will be removed.
   *
   * @param maxEntries The maximum number of search filters statistics
   * entries to keep
   */
  void setMaxEntries(int maxEntries)
  {
    this.maxEntries = maxEntries;
  }

  /** Increments the statistics counter to include an indexed search. */
  void incrementIndexedSearchCount()
  {
    if (filterUseEnabled)
    {
      indexedSearchCount.getAndIncrement();
    }
  }

  /** Increments the statistics counter to include an unindexed search. */
  void incrementUnindexedSearchCount()
  {
    if (filterUseEnabled)
    {
      unindexedSearchCount.getAndIncrement();
    }
  }

  private void removeLowestHit()
  {
    while (!filterToStats.isEmpty() && filterToStats.size() > maxEntries)
    {
      Iterator<Map.Entry<SearchFilter, FilterStats>> it = filterToStats.entrySet().iterator();
      Map.Entry<SearchFilter, FilterStats> lowest = it.next();
      Map.Entry<SearchFilter, FilterStats> entry;
      while (lowest.getValue().hits.get() > 1 && it.hasNext())
      {
        entry = it.next();
        if (entry.getValue().hits.get() < lowest.getValue().hits.get())
        {
          lowest = entry;
        }
      }

      filterToStats.remove(lowest.getKey());
    }
  }
}
