/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:expandtab:shiftwidth=2:tabstop=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
 * Gijs Kruitbosch <gijskruitbosch@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Gijs Kruitbosch <gijskruitbosch@gmail.com>
 *  Edward Lee <edward.lee@engineering.uiuc.edu>
 *  Mike Kristoffersen <moz@mikek.dk>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either 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 ***** */

#include "nsIdleService.h"
#include "nsString.h"
#include "nsIObserverService.h"
#include "nsIServiceManager.h"
#include "nsDebug.h"
#include "nsCOMArray.h"
#include "prinrval.h"
#include "mozilla/Services.h"
#include "mozilla/Preferences.h"

using namespace mozilla;

// observer topics used:
#define OBSERVER_TOPIC_IDLE "idle"
#define OBSERVER_TOPIC_BACK "back"
#define OBSERVER_TOPIC_IDLE_DAILY "idle-daily"
// interval in seconds between internal idle time requests.
#define MIN_IDLE_POLL_INTERVAL 5
#define MAX_IDLE_POLL_INTERVAL 300 /* 5 min */
// Pref for last time (seconds since epoch) daily notification was sent.
#define PREF_LAST_DAILY "idle.lastDailyNotification"
// Number of seconds in a day.
#define SECONDS_PER_DAY 86400

// Use this to find previously added observers in our array:
class IdleListenerComparator
{
public:
  bool Equals(IdleListener a, IdleListener b) const
  {
    return (a.observer == b.observer) &&
           (a.reqIdleTime == b.reqIdleTime);
  }
};

////////////////////////////////////////////////////////////////////////////////
//// nsIdleServiceDaily

NS_IMPL_ISUPPORTS1(nsIdleServiceDaily, nsIObserver)

NS_IMETHODIMP
nsIdleServiceDaily::Observe(nsISupports *,
                            const char *,
                            const PRUnichar *)
{
  // Notify anyone who cares.
  nsCOMPtr<nsIObserverService> observerService =
    mozilla::services::GetObserverService();
  NS_ENSURE_STATE(observerService);
  (void)observerService->NotifyObservers(nsnull,
                                         OBSERVER_TOPIC_IDLE_DAILY,
                                         nsnull);

  // Notify the category observers.
  const nsCOMArray<nsIObserver> &entries = mCategoryObservers.GetEntries();
  for (PRInt32 i = 0; i < entries.Count(); ++i) {
    (void)entries[i]->Observe(nsnull, OBSERVER_TOPIC_IDLE_DAILY, nsnull);
  }

  // Stop observing idle for today.
  (void)mIdleService->RemoveIdleObserver(this, MAX_IDLE_POLL_INTERVAL);

  // Set the last idle-daily time pref.
  PRInt32 nowSec = static_cast<PRInt32>(PR_Now() / PR_USEC_PER_SEC);
  Preferences::SetInt(PREF_LAST_DAILY, nowSec);

  // Start timer for the next check in one day.
  (void)mTimer->InitWithFuncCallback(DailyCallback, this, SECONDS_PER_DAY * 1000,
                                     nsITimer::TYPE_ONE_SHOT);

  return NS_OK;
}

nsIdleServiceDaily::nsIdleServiceDaily(nsIIdleService* aIdleService)
  : mIdleService(aIdleService)
  , mTimer(do_CreateInstance(NS_TIMER_CONTRACTID))
  , mCategoryObservers(OBSERVER_TOPIC_IDLE_DAILY)
{
}

void
nsIdleServiceDaily::Init()
{
  // Check time of the last idle-daily notification.  If it was more than 24
  // hours ago listen for idle, otherwise set a timer for 24 hours from now.
  PRInt32 nowSec = static_cast<PRInt32>(PR_Now() / PR_USEC_PER_SEC);
  PRInt32 lastDaily = Preferences::GetInt(PREF_LAST_DAILY, 0);
  if (lastDaily < 0 || lastDaily > nowSec) {
    // The time is bogus, use default.
    lastDaily = 0;
  }

  // Check if it has been a day since the last notification.
  if (nowSec - lastDaily > SECONDS_PER_DAY) {
    // Wait for the user to become idle, so we can do todays idle tasks.
    DailyCallback(nsnull, this);
  }
  else {
    // Start timer for the next check in one day.
    (void)mTimer->InitWithFuncCallback(DailyCallback, this, SECONDS_PER_DAY * 1000,
                                       nsITimer::TYPE_ONE_SHOT);
  }
}

nsIdleServiceDaily::~nsIdleServiceDaily()
{
  if (mTimer) {
    mTimer->Cancel();
    mTimer = nsnull;
  }
}

// static
void
nsIdleServiceDaily::DailyCallback(nsITimer* aTimer, void* aClosure)
{
  nsIdleServiceDaily* me = static_cast<nsIdleServiceDaily*>(aClosure);

  // The one thing we do every day is to start waiting for the user to "have
  // a significant idle time".
  (void)me->mIdleService->AddIdleObserver(me, MAX_IDLE_POLL_INTERVAL);
}

////////////////////////////////////////////////////////////////////////////////
//// nsIdleService

nsIdleService::nsIdleService() : mLastIdleReset(0)
                               , mLastHandledActivity(0)
                               , mPolledIdleTimeIsValid(false)
{
  mDailyIdle = new nsIdleServiceDaily(this);
  mDailyIdle->Init();
}

nsIdleService::~nsIdleService()
{
  StopTimer();
}

NS_IMETHODIMP
nsIdleService::AddIdleObserver(nsIObserver* aObserver, PRUint32 aIdleTime)
{
  NS_ENSURE_ARG_POINTER(aObserver);
  NS_ENSURE_ARG(aIdleTime);

  // Put the time + observer in a struct we can keep:
  IdleListener listener(aObserver, aIdleTime);

  if (!mArrayListeners.AppendElement(listener)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  // Create our timer callback if it's not there already.
  if (!mTimer) {
    nsresult rv;
    mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Make sure our observer goes into 'idle' immediately if applicable.
  CheckAwayState(true);

  return NS_OK;
}

NS_IMETHODIMP
nsIdleService::RemoveIdleObserver(nsIObserver* aObserver, PRUint32 aTime)
{
  NS_ENSURE_ARG_POINTER(aObserver);
  NS_ENSURE_ARG(aTime);
  IdleListener listener(aObserver, aTime);

  // Find the entry and remove it:
  IdleListenerComparator c;
  if (mArrayListeners.RemoveElement(listener, c)) {
    if (mArrayListeners.IsEmpty()) {
      StopTimer();
    }
    return NS_OK;
  }

  // If we get here, we haven't removed anything:
  return NS_ERROR_FAILURE;
}

void
nsIdleService::ResetIdleTimeOut()
{
  mLastIdleReset = PR_IntervalToSeconds(PR_IntervalNow());
  if (!mLastIdleReset) mLastIdleReset = 1;

  // Now check if this changes anything
  CheckAwayState(false);
}

NS_IMETHODIMP
nsIdleService::GetIdleTime(PRUint32* idleTime)
{
  // Check sanity of in parameter.
  if (!idleTime) {
    return NS_ERROR_NULL_POINTER;
  }

  // Polled idle time in ms
  PRUint32 polledIdleTimeMS;

  mPolledIdleTimeIsValid = PollIdleTime(&polledIdleTimeMS);

  // If we don't have any valid data, then we are not in idle - pr. definition.
  if (!mPolledIdleTimeIsValid && 0 == mLastIdleReset) {
    *idleTime = 0;
    return NS_OK;
  }

  // If we never got a reset, just return the pulled time.
  if (0 == mLastIdleReset) {
    *idleTime = polledIdleTimeMS;
    return NS_OK;
  }

  // timeSinceReset is in seconds.
  PRUint32 timeSinceReset =
    PR_IntervalToSeconds(PR_IntervalNow()) - mLastIdleReset;

  // If we did't get pulled data, return the time since last idle reset.
  if (!mPolledIdleTimeIsValid) {
    // We need to convert to ms before returning the time.
    *idleTime = timeSinceReset * 1000;
    return NS_OK;
  }

  // Otherwise return the shortest time detected (in ms).
  *idleTime = NS_MIN(timeSinceReset * 1000, polledIdleTimeMS);

  return NS_OK;
}


bool
nsIdleService::PollIdleTime(PRUint32* /*aIdleTime*/)
{
  // Default behavior is not to have the ability to poll an idle time.
  return false;
}

bool
nsIdleService::UsePollMode()
{
  PRUint32 dummy;
  return PollIdleTime(&dummy);
}

void
nsIdleService::IdleTimerCallback(nsITimer* aTimer, void* aClosure)
{
  static_cast<nsIdleService*>(aClosure)->CheckAwayState(false);
}

void
nsIdleService::CheckAwayState(bool aNewObserver)
{
  /**
   * Find our last detected idle time (it's important this happens before the
   * call below to GetIdleTime, as we use the two values to detect if there
   * has been user activity since the last time we were here).
   */
  PRUint32 curTime = static_cast<PRUint32>(PR_Now() / PR_USEC_PER_SEC);
  PRUint32 lastTime = curTime - mLastHandledActivity;
  bool bootstrapTimer =  mLastHandledActivity == 0;

  /**
   * Too short since last check.
   * Bail out
   */
  if (!lastTime && !aNewObserver) {
    return;
  }

  // Get the idle time (in seconds).
  PRUint32 idleTime;
  if (NS_FAILED(GetIdleTime(&idleTime))) {
    return;
  }

  // If we have no valid data about the idle time, stop
  if (!mPolledIdleTimeIsValid && 0 == mLastIdleReset) {
    return;
  }

  // GetIdleTime returns the time in ms, internally we only calculate in s.
  idleTime /= 1000;

  // Set the time for last user activity.
  mLastHandledActivity = curTime - idleTime;

  /**
   * Now, if the idle time, is less than what we expect, it means the
   * user was active since last time that we checked.
   */
  bool userActivity = lastTime > idleTime;

  if (userActivity) {
    if (TryNotifyBackState(idleTime) || idleTime) {
      RescheduleIdleTimer(idleTime);
    }
  }

  if (!userActivity || aNewObserver || bootstrapTimer) {
    TryNotifyIdleState(idleTime);
    RescheduleIdleTimer(idleTime);
  }
}

bool
nsIdleService::TryNotifyBackState(PRUint32 aIdleTime)
{
  nsCOMArray<nsIObserver> notifyList;

  // Loop trough all listeners, and find any that have detected idle.
  for (PRUint32 i = 0; i < mArrayListeners.Length(); i++) {
    IdleListener& curListener = mArrayListeners.ElementAt(i);

    if (curListener.isIdle) {
      notifyList.AppendObject(curListener.observer);
      curListener.isIdle = false;
    }
  }

  PRInt32 numberOfPendingNotifications = notifyList.Count();

  // Bail id nothing to do
  if(!numberOfPendingNotifications) {
    return false;
  }

  // We need a text string to send with any state change events.
  nsAutoString timeStr;
  timeStr.AppendInt(aIdleTime);

  // Send the "non-idle" events.
  while(numberOfPendingNotifications--) {
    notifyList[numberOfPendingNotifications]->Observe(this,
                                                      OBSERVER_TOPIC_BACK,
                                                      timeStr.get());
  }

  // We found something so return true
  return true;
}

bool
nsIdleService::TryNotifyIdleState(PRUint32 aIdleTime)
{
  /**
   * Now we need to check for listeners that have expired, and while we are
   * looping through all the elements, we will also calculate when, if ever
   * the next one will need to be notified.
   */

  nsCOMArray<nsIObserver> notifyList;

  for (PRUint32 i = 0; i < mArrayListeners.Length(); i++) {
    IdleListener& curListener = mArrayListeners.ElementAt(i);

    // We are only interested in items, that are not in the idle state.
    if (!curListener.isIdle) {
      // If they have an idle time smaller than the actual idle time.
      if (curListener.reqIdleTime <= aIdleTime) {
        // then add the listener to the list of listeners that should be
        // notified.
        notifyList.AppendObject(curListener.observer);
        // This listener is now idle.
        curListener.isIdle = true;
      }
    }
  }

  PRInt32 numberOfPendingNotifications = notifyList.Count();

  // Bail if nothing to do
  if(!numberOfPendingNotifications) {
    return false;
  }

  // We need a text string to send with any state change events.
  nsAutoString timeStr;
  timeStr.AppendInt(aIdleTime);

  // Notify all listeners that just timed out.
  while(numberOfPendingNotifications--) {
    notifyList[numberOfPendingNotifications]->Observe(this,
                                                      OBSERVER_TOPIC_IDLE,
                                                      timeStr.get());
  }

  return true;
}

void
nsIdleService::RescheduleIdleTimer(PRUint32 aIdleTime)
{
  /**
   * Placet to store the wait time to the next notification, note that
   * PR_UINT32_MAX means no-one are listening (or that they have such a big
   * delay that it doesn't matter).
   */
  PRUint32 nextWaitTime = PR_UINT32_MAX;

  /**
   * Place to remember if there are any listeners that are in the idle state,
   * if there are, we need to poll frequently in a polling environment to detect
   * when the user becomes active again.
   */
  bool anyOneIdle = false;

  for (PRUint32 i = 0; i < mArrayListeners.Length(); i++) {
    IdleListener& curListener = mArrayListeners.ElementAt(i);

    // We are only interested in items, that are not in the idle state.
    if (!curListener.isIdle) {
        // If it hasn't expired yet, then we should note the time when it should
        // expire.
        nextWaitTime = NS_MIN(nextWaitTime, curListener.reqIdleTime);
    }

    // Remember if anyone becomes idle (it's safe to do this as a binary compare
    // as we are or'ing).
    anyOneIdle |= curListener.isIdle;
  }

  // In order to find when the next idle event should time out, we need to
  // subtract the time we should wait, from the time that has already passed.
  if (PR_UINT32_MAX != nextWaitTime) {
    nextWaitTime -= aIdleTime;
  }

  // If we are in poll mode, we need to poll for activity if anyone are idle,
  // otherwise we can wait polling until they would expire.
  if (UsePollMode() &&
      anyOneIdle &&
      nextWaitTime > MIN_IDLE_POLL_INTERVAL) {
    nextWaitTime = MIN_IDLE_POLL_INTERVAL;
  }

  // Start the timer if there is anything to wait for.
  if (PR_UINT32_MAX != nextWaitTime) {
    StartTimer(nextWaitTime);
  }
}

void
nsIdleService::StartTimer(PRUint32 aDelay)
{
  if (mTimer) {
    StopTimer();
    if (aDelay) {
      mTimer->InitWithFuncCallback(IdleTimerCallback, this, aDelay*1000,
                                   nsITimer::TYPE_ONE_SHOT);
    }
  }
}

void
nsIdleService::StopTimer()
{
  if (mTimer) {
    mTimer->Cancel();
  }
}

