/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is mozilla.org code.
 *
 * The Initial Developer of the Original Code is
 * Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   robert@ocallahan.org
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#ifndef NSEXPIRATIONTRACKER_H_
#define NSEXPIRATIONTRACKER_H_

#include "prlog.h"
#include "nsTArray.h"
#include "nsITimer.h"
#include "nsCOMPtr.h"
#include "nsComponentManagerUtils.h"

/**
 * Data used to track the expiration state of an object. We promise that this
 * is 32 bits so that objects that includes this as a field can pad and align
 * efficiently.
 */
struct nsExpirationState {
  enum { NOT_TRACKED = (1U << 4) - 1,
         MAX_INDEX_IN_GENERATION = (1U << 28) - 1 };

  nsExpirationState() : mGeneration(NOT_TRACKED) {}
  PRBool IsTracked() { return mGeneration != NOT_TRACKED; }

  /**
   * The generation that this object belongs to, or NOT_TRACKED.
   */
  PRUint32 mGeneration:4;
  PRUint32 mIndexInGeneration:28;
};

/**
 * nsExpirationTracker can track the lifetimes and usage of a large number of
 * objects, and send a notification some window of time after a live object was
 * last used. This is very useful when you manage a large number of objects
 * and want to flush some after they haven't been used for a while.
 * nsExpirationTracker is designed to be very space and time efficient.
 * 
 * The type parameter T is the object type that we will track pointers to. T
 * must include an accessible method GetExpirationState() that returns a
 * pointer to an nsExpirationState associated with the object (preferably,
 * stored in a field of the object).
 * 
 * The parameter K is the number of generations that will be used. Increasing
 * the number of generations narrows the window within which we promise
 * to fire notifications, at a slight increase in space cost for the tracker.
 * We require 2 <= K <= nsExpirationState::NOT_TRACKED (currently 15).
 * 
 * To use this class, you need to inherit from it and override the
 * NotifyExpired() method.
 * 
 * The approach is to track objects in K generations. When an object is accessed
 * it moves from its current generation to the newest generation. Generations
 * are stored in a cyclic array; when a timer interrupt fires, we advance
 * the current generation pointer to effectively age all objects very efficiently.
 * By storing information in each object about its generation and index within its
 * generation array, we make removal of objects from a generation very cheap.
 * 
 * Future work:
 * -- Add a method to change the timer period?
 */
template <class T, PRUint32 K> class nsExpirationTracker {
  public:
    /**
     * Initialize the tracker.
     * @param aTimerPeriod the timer period in milliseconds. The guarantees
     * provided by the tracker are defined in terms of this period. If the
     * period is zero, then we don't use a timer and rely on someone calling
     * AgeOneGeneration explicitly.
     */
    nsExpirationTracker(PRUint32 aTimerPeriod)
      : mTimerPeriod(aTimerPeriod), mNewestGeneration(0),
        mInAgeOneGeneration(PR_FALSE) {
      PR_STATIC_ASSERT(K >= 2 && K <= nsExpirationState::NOT_TRACKED);
    }
    ~nsExpirationTracker() {
      if (mTimer) {
        mTimer->Cancel();
      }
    }

    /**
     * Add an object to be tracked. It must not already be tracked. It will
     * be added to the newest generation, i.e., as if it was just used.
     * @return an error on out-of-memory
     */
    nsresult AddObject(T* aObj) {
      nsExpirationState* state = aObj->GetExpirationState();
      NS_ASSERTION(!state->IsTracked(), "Tried to add an object that's already tracked");
      nsTArray<T*>& generation = mGenerations[mNewestGeneration];
      PRUint32 index = generation.Length();
      if (index > nsExpirationState::MAX_INDEX_IN_GENERATION) {
        NS_WARNING("More than 256M elements tracked, this is probably a problem");
        return NS_ERROR_OUT_OF_MEMORY;
      }
      if (index == 0) {
        // We might need to start the timer
        nsresult rv = CheckStartTimer();
        if (NS_FAILED(rv))
          return rv;
      }
      if (!generation.AppendElement(aObj))
        return NS_ERROR_OUT_OF_MEMORY;
      state->mGeneration = mNewestGeneration;
      state->mIndexInGeneration = index;
      return NS_OK;
    }

    /**
     * Remove an object from the tracker. It must currently be tracked.
     */
    void RemoveObject(T* aObj) {
      nsExpirationState* state = aObj->GetExpirationState();
      NS_ASSERTION(state->IsTracked(), "Tried to remove an object that's not tracked");
      nsTArray<T*>& generation = mGenerations[state->mGeneration];
      PRUint32 index = state->mIndexInGeneration;
      NS_ASSERTION(generation.Length() > index &&
                   generation[index] == aObj, "Object is lying about its index");
      // Move the last object to fill the hole created by removing aObj
      PRUint32 last = generation.Length() - 1;
      T* lastObj = generation[last];
      generation[index] = lastObj;
      lastObj->GetExpirationState()->mIndexInGeneration = index;
      generation.RemoveElementAt(last);
      state->mGeneration = nsExpirationState::NOT_TRACKED;
      // We do not check whether we need to stop the timer here. The timer
      // will check that itself next time it fires. Checking here would not
      // be efficient since we'd need to track all generations. Also we could
      // thrash by incessantly creating and destroying timers if someone
      // kept adding and removing an object from the tracker.
    }

    /**
     * Notify that an object has been used.
     * @return an error if we lost the object from the tracker...
     */
    nsresult MarkUsed(T* aObj) {
      nsExpirationState* state = aObj->GetExpirationState();
      if (mNewestGeneration == state->mGeneration)
        return NS_OK;
      RemoveObject(aObj);
      return AddObject(aObj);
    }

    /**
     * The timer calls this, but it can also be manually called if you want
     * to age objects "artifically". This can result in calls to NotifyExpired.
     */
    void AgeOneGeneration() {
      if (mInAgeOneGeneration) {
        NS_WARNING("Can't reenter AgeOneGeneration from NotifyExpired");
        return;
      }
      
      mInAgeOneGeneration = PR_TRUE;
      PRUint32 reapGeneration = 
        mNewestGeneration > 0 ? mNewestGeneration - 1 : K - 1;
      nsTArray<T*>& generation = mGenerations[reapGeneration];
      // The following is rather tricky. We have to cope with objects being
      // removed from this generation either because of a call to RemoveObject
      // (or indirectly via MarkUsed) inside NotifyExpired. Fortunately no
      // objects can be added to this generation because it's not the newest
      // generation. We depend on the fact that RemoveObject can only cause
      // the indexes of objects in this generation to *decrease*, not increase.
      // So if we start from the end and work our way backwards we are guaranteed
      // to see each object at least once.
      PRUint32 index = generation.Length();
      for (;;) {
        // Objects could have been removed so index could be outside
        // the array
        index = PR_MIN(index, generation.Length());
        if (index == 0)
          break;
        --index;
        NotifyExpired(generation[index]);
      }
      // Any leftover objects from reapGeneration just end up in the new
      // newest-generation. This is bad form, though, so warn if there are any.
      if (!generation.IsEmpty()) {
        NS_WARNING("Expired objects were not removed or marked used");
      }
      // Free excess memory used by the generation array, since we probably
      // just removed most or all of its elements.
      generation.Compact();
      mNewestGeneration = reapGeneration;
      mInAgeOneGeneration = PR_FALSE;
    }

    /**
     * This just calls AgeOneGeneration K times. Under normal circumstances this
     * will result in all objects getting NotifyExpired called on them, but
     * if NotifyExpired itself marks some objects as used, then those objects
     * might not expire. This would be a good thing to call if we get into
     * a critically-low memory situation.
     */
    void AgeAllGenerations() {
      PRUint32 i;
      for (i = 0; i < K; ++i) {
        AgeOneGeneration();
      }
    }

  protected:
    /**
     * This must be overridden to catch notifications. It is called whenever
     * we detect that an object has not been used for at least (K-1)*mTimerPeriod
     * seconds. If timer events are not delayed, it will be called within
     * roughly K*mTimerPeriod seconds after the last use. (Unless AgeOneGeneration
     * or AgeAllGenerations have been called to accelerate the aging process.)
     * 
     * NOTE: These bounds ignore delays in timer firings due to actual work being
     * performed by the browser. We use a slack timer so there is always at least
     * mTimerPeriod milliseconds between firings, which gives us (K-1)*mTimerPeriod
     * as a pretty solid lower bound. The upper bound is rather loose, however.
     * If the maximum amount by which any given timer firing is delayed is D, then
     * the upper bound before NotifyExpired is called is K*(mTimerPeriod + D).
     * 
     * The NotifyExpired call is expected to remove the object from the tracker,
     * but it need not. The object (or other objects) could be "resurrected"
     * by calling MarkUsed() on them, or they might just not be removed.
     * Any objects left over that have not been resurrected or removed
     * are placed in the new newest-generation, but this is considered "bad form"
     * and should be avoided (we'll issue a warning). (This recycling counts
     * as "a use" for the purposes of the expiry guarantee above...)
     * 
     * For robustness and simplicity, we allow objects to be notified more than
     * once here in the same timer tick.
     */
    virtual void NotifyExpired(T* aObj) = 0;

  private:
    nsTArray<T*>       mGenerations[K];
    nsCOMPtr<nsITimer> mTimer;
    PRUint32           mTimerPeriod;
    PRUint32           mNewestGeneration;
    PRPackedBool       mInAgeOneGeneration;

    static void TimerCallback(nsITimer* aTimer, void* aThis) {
      nsExpirationTracker* tracker = static_cast<nsExpirationTracker*>(aThis);
      tracker->AgeOneGeneration();
      // Cancel the timer if we have no objects to track
      PRUint32 i;
      for (i = 0; i < K; ++i) {
        if (!tracker->mGenerations[i].IsEmpty())
          return;
      }
      tracker->mTimer->Cancel();
      tracker->mTimer = nsnull;
    }

    nsresult CheckStartTimer() {
      if (mTimer || !mTimerPeriod)
        return NS_OK;
      mTimer = do_CreateInstance("@mozilla.org/timer;1");
      if (!mTimer)
        return NS_ERROR_OUT_OF_MEMORY;
      mTimer->InitWithFuncCallback(TimerCallback, this, mTimerPeriod,
                                   nsITimer::TYPE_REPEATING_SLACK);
      return NS_OK;
    }
};

#endif /*NSEXPIRATIONTRACKER_H_*/
