/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "IdleTaskRunner.h"
#include "mozilla/TaskCategory.h"
#include "mozilla/TaskController.h"
#include "nsRefreshDriver.h"

namespace mozilla {

already_AddRefed<IdleTaskRunner> IdleTaskRunner::Create(
    const CallbackType& aCallback, const char* aRunnableName,
    TimeDuration aStartDelay, TimeDuration aMaxDelay,
    TimeDuration aMinimumUsefulBudget, bool aRepeating,
    const MayStopProcessingCallbackType& aMayStopProcessing,
    const RequestInterruptCallbackType& aRequestInterrupt) {
  if (aMayStopProcessing && aMayStopProcessing()) {
    return nullptr;
  }

  RefPtr<IdleTaskRunner> runner = new IdleTaskRunner(
      aCallback, aRunnableName, aStartDelay, aMaxDelay, aMinimumUsefulBudget,
      aRepeating, aMayStopProcessing, aRequestInterrupt);
  runner->Schedule(false);  // Initial scheduling shouldn't use idle dispatch.
  return runner.forget();
}

class IdleTaskRunnerTask : public Task {
 public:
  explicit IdleTaskRunnerTask(IdleTaskRunner* aRunner)
      : Task(true, EventQueuePriority::Idle),
        mRunner(aRunner),
        mRequestInterrupt(aRunner->mRequestInterrupt) {
    SetManager(TaskController::Get()->GetIdleTaskManager());
  }

  bool Run() override {
    if (mRunner) {
      // IdleTaskRunner::Run can actually trigger the destruction of the
      // IdleTaskRunner. Make sure it doesn't get destroyed before the method
      // finished.
      RefPtr<IdleTaskRunner> runner(mRunner);
      runner->Run();
    }
    return true;
  }

  void SetIdleDeadline(TimeStamp aDeadline) override {
    if (mRunner) {
      mRunner->SetIdleDeadline(aDeadline);
    }
  }

  void Cancel() { mRunner = nullptr; }

  bool GetName(nsACString& aName) override {
    if (mRunner) {
      aName.Assign(mRunner->GetName());
    } else {
      aName.Assign("ExpiredIdleTaskRunner");
    }
    return true;
  }

  void RequestInterrupt(uint32_t aInterruptPriority) override {
    if (mRequestInterrupt) {
      mRequestInterrupt(aInterruptPriority);
    }
  }

 private:
  IdleTaskRunner* mRunner;

  // Copied here and invoked even if there is no mRunner currently, to avoid
  // race conditions checking mRunner when an interrupt is requested.
  IdleTaskRunner::RequestInterruptCallbackType mRequestInterrupt;
};

IdleTaskRunner::IdleTaskRunner(
    const CallbackType& aCallback, const char* aRunnableName,
    TimeDuration aStartDelay, TimeDuration aMaxDelay,
    TimeDuration aMinimumUsefulBudget, bool aRepeating,
    const MayStopProcessingCallbackType& aMayStopProcessing,
    const RequestInterruptCallbackType& aRequestInterrupt)
    : mCallback(aCallback),
      mStartTime(TimeStamp::Now() + aStartDelay),
      mMaxDelay(aMaxDelay),
      mMinimumUsefulBudget(aMinimumUsefulBudget),
      mRepeating(aRepeating),
      mTimerActive(false),
      mMayStopProcessing(aMayStopProcessing),
      mRequestInterrupt(aRequestInterrupt),
      mName(aRunnableName) {}

void IdleTaskRunner::Run() {
  if (!mCallback) {
    return;
  }

  // Deadline is null when called from timer or RunNextCollectorTimer rather
  // than during idle time.
  TimeStamp now = TimeStamp::Now();

  // Note that if called from RunNextCollectorTimer, we may not have reached
  // mStartTime yet. Pretend we are overdue for idle time.
  bool overdueForIdle = mDeadline.IsNull();
  bool didRun = false;
  bool allowIdleDispatch = false;

  if (mTask) {
    // If we find ourselves here we should usually be running from this task,
    // but there are exceptions. In any case we're doing the work now and don't
    // need our task going forward unless we're re-scheduled.
    nsRefreshDriver::CancelIdleTask(mTask);
    // Extra safety, make sure a task can never have a dangling ptr.
    mTask->Cancel();
    mTask = nullptr;
  }

  if (overdueForIdle || ((now + mMinimumUsefulBudget) < mDeadline)) {
    CancelTimer();
    didRun = mCallback(mDeadline);
    // If we didn't do meaningful work, don't schedule using immediate
    // idle dispatch, since that could lead to a loop until the idle
    // period ends.
    allowIdleDispatch = didRun;
  } else if (now >= mDeadline) {
    allowIdleDispatch = true;
  }

  if (mCallback && (mRepeating || !didRun)) {
    Schedule(allowIdleDispatch);
  } else {
    mCallback = nullptr;
  }
}

static void TimedOut(nsITimer* aTimer, void* aClosure) {
  RefPtr<IdleTaskRunner> runner = static_cast<IdleTaskRunner*>(aClosure);
  runner->Run();
}

void IdleTaskRunner::SetIdleDeadline(mozilla::TimeStamp aDeadline) {
  mDeadline = aDeadline;
}

void IdleTaskRunner::SetMinimumUsefulBudget(int64_t aMinimumUsefulBudget) {
  mMinimumUsefulBudget = TimeDuration::FromMilliseconds(aMinimumUsefulBudget);
}

void IdleTaskRunner::SetTimer(TimeDuration aDelay, nsIEventTarget* aTarget) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aTarget->IsOnCurrentThread());
  // aTarget is always the main thread event target provided from
  // NS_DispatchToCurrentThreadQueue(). We ignore aTarget here to ensure that
  // CollectorRunner always run specifically the main thread.
  SetTimerInternal(aDelay);
}

void IdleTaskRunner::Cancel() {
  CancelTimer();
  mTimer = nullptr;
  mScheduleTimer = nullptr;
  mCallback = nullptr;
}

static void ScheduleTimedOut(nsITimer* aTimer, void* aClosure) {
  RefPtr<IdleTaskRunner> runnable = static_cast<IdleTaskRunner*>(aClosure);
  runnable->Schedule(true);
}

void IdleTaskRunner::Schedule(bool aAllowIdleDispatch) {
  if (!mCallback) {
    return;
  }

  if (mMayStopProcessing && mMayStopProcessing()) {
    Cancel();
    return;
  }

  mDeadline = TimeStamp();

  TimeStamp now = TimeStamp::Now();
  bool useRefreshDriver = false;
  if (now >= mStartTime) {
    // Detect whether the refresh driver is ticking by checking if
    // GetIdleDeadlineHint returns its input parameter.
    useRefreshDriver =
        (nsRefreshDriver::GetIdleDeadlineHint(
             now, nsRefreshDriver::IdleCheck::OnlyThisProcessRefreshDriver) !=
         now);
  } else {
    NS_WARNING_ASSERTION(!aAllowIdleDispatch,
                         "early callback, or time went backwards");
  }

  if (useRefreshDriver) {
    if (!mTask) {
      // If a task was already scheduled, no point rescheduling.
      mTask = new IdleTaskRunnerTask(this);
      // RefreshDriver is ticking, let it schedule the idle dispatch.
      nsRefreshDriver::DispatchIdleTaskAfterTickUnlessExists(mTask);
    }
    // Ensure we get called at some point, even if RefreshDriver is stopped.
    SetTimerInternal(mMaxDelay);
  } else {
    // RefreshDriver doesn't seem to be running.
    if (aAllowIdleDispatch) {
      SetTimerInternal(mMaxDelay);
      if (!mTask) {
        // If we have mTask we've already scheduled one, and the refresh driver
        // shouldn't be running if we hit this code path.
        mTask = new IdleTaskRunnerTask(this);
        RefPtr<Task> task(mTask);
        TaskController::Get()->AddTask(task.forget());
      }
    } else {
      if (!mScheduleTimer) {
        mScheduleTimer = NS_NewTimer();
        if (!mScheduleTimer) {
          return;
        }
      } else {
        mScheduleTimer->Cancel();
      }
      // We weren't allowed to do idle dispatch immediately, do it after a
      // short timeout. (Or wait for our start time if we haven't started yet.)
      uint32_t waitToSchedule = 16; /* ms */
      if (now < mStartTime) {
        // + 1 to round milliseconds up to be sure to wait until after
        // mStartTime.
        waitToSchedule = (mStartTime - now).ToMilliseconds() + 1;
      }
      mScheduleTimer->InitWithNamedFuncCallback(
          ScheduleTimedOut, this, waitToSchedule,
          nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, mName);
    }
  }
}

IdleTaskRunner::~IdleTaskRunner() { CancelTimer(); }

void IdleTaskRunner::CancelTimer() {
  if (mTask) {
    nsRefreshDriver::CancelIdleTask(mTask);
    mTask->Cancel();
    mTask = nullptr;
  }
  if (mTimer) {
    mTimer->Cancel();
  }
  if (mScheduleTimer) {
    mScheduleTimer->Cancel();
  }
  mTimerActive = false;
}

void IdleTaskRunner::SetTimerInternal(TimeDuration aDelay) {
  if (mTimerActive) {
    return;
  }

  if (!mTimer) {
    mTimer = NS_NewTimer();
  } else {
    mTimer->Cancel();
  }

  if (mTimer) {
    mTimer->InitWithNamedFuncCallback(TimedOut, this, aDelay.ToMilliseconds(),
                                      nsITimer::TYPE_ONE_SHOT, mName);
    mTimerActive = true;
  }
}

}  // end of namespace mozilla
