/* 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 "APZTaskRunnable.h"

#include "mozilla/PresShell.h"
#include "nsRefreshDriver.h"

namespace mozilla::layers {

NS_IMETHODIMP
APZTaskRunnable::Run() {
  if (!mController) {
    mRegisteredPresShellId = 0;
    return NS_OK;
  }

  // Move these variables first since below RequestContentPaint and
  // NotifyFlushComplete might spin event loop so that any new incoming requests
  // will be properly queued and run in the next refresh driver's tick.
  const bool needsFlushCompleteNotification = mNeedsFlushCompleteNotification;
  auto requests = std::move(mPendingRequestQueue);
  mPendingRepaintRequestMap.clear();
  mNeedsFlushCompleteNotification = false;
  mRegisteredPresShellId = 0;
  RefPtr<GeckoContentController> controller = mController;

  // We need to process pending RepaintRequests first.
  while (!requests.empty()) {
    struct RequestProcessor {
      GeckoContentController* mController;
      void operator()(const RepaintRequest& aRequest) {
        mController->RequestContentRepaint(aRequest);
      }
      void operator()(const APZStateChangeRequest& aRequest) {
        mController->NotifyAPZStateChange(aRequest.mGuid, aRequest.mChange,
                                          aRequest.mArg,
                                          aRequest.mInputBlockId);
      }
    };
    requests.front().match(RequestProcessor{controller.get()});
    requests.pop_front();
  }

  if (needsFlushCompleteNotification) {
    // Then notify "apz-repaints-flushed" so that we can ensure that all pending
    // scroll position updates have finished when the "apz-repaints-flushed"
    // arrives.
    controller->NotifyFlushComplete();
  }

  return NS_OK;
}

void APZTaskRunnable::QueueRequest(const RepaintRequest& aRequest) {
  // If we are in test-controlled refreshes mode, process this |aRequest|
  // synchronously.
  if (IsTestControllingRefreshesEnabled()) {
    // Flush all pending requests and notification just in case the refresh
    // driver mode was changed before flushing them.
    RefPtr<GeckoContentController> controller = mController;
    Run();
    controller->RequestContentRepaint(aRequest);
    return;
  }
  EnsureRegisterAsEarlyRunner();

  RepaintRequestKey key{aRequest.GetScrollId(), aRequest.GetScrollUpdateType()};

  auto lastDiscardableRequest = mPendingRepaintRequestMap.find(key);
  // If there's an existing request with the same key, we can discard it and we
  // push the incoming one into the queue's tail so that we can ensure the order
  // of processing requests.
  if (lastDiscardableRequest != mPendingRepaintRequestMap.end()) {
    for (auto it = mPendingRequestQueue.begin();
         it != mPendingRequestQueue.end(); it++) {
      if (it->is<RepaintRequest>()) {
        const RepaintRequest& request = it->as<RepaintRequest>();
        if (RepaintRequestKey{request.GetScrollId(),
                              request.GetScrollUpdateType()} == key) {
          mPendingRequestQueue.erase(it);
          break;
        }
      }
    }
  }
  mPendingRepaintRequestMap.insert(key);
  mPendingRequestQueue.push_back(AsVariant(aRequest));
}

void APZTaskRunnable::QueueAPZStateChange(const ScrollableLayerGuid& aGuid,
                                          const APZStateChange& aChange,
                                          const int& aArg,
                                          Maybe<uint64_t> aInputBlockId) {
  // If we are in test-controlled refreshes mode, process this |aRequest|
  // synchronously.
  if (IsTestControllingRefreshesEnabled()) {
    // Flush all pending requests and notification just in case the refresh
    // driver mode was changed before flushing them.
    RefPtr<GeckoContentController> controller = mController;
    Run();
    controller->NotifyAPZStateChange(aGuid, aChange, aArg, aInputBlockId);
    return;
  }
  EnsureRegisterAsEarlyRunner();

  mPendingRequestQueue.push_back(
      AsVariant(APZStateChangeRequest{aGuid, aChange, aArg, aInputBlockId}));
}

void APZTaskRunnable::QueueFlushCompleteNotification() {
  // If we are in test-controlled refreshes mode, notify apz-repaints-flushed
  // synchronously.
  if (IsTestControllingRefreshesEnabled()) {
    // Flush all pending requests and notification just in case the refresh
    // driver mode was changed before flushing them.
    RefPtr<GeckoContentController> controller = mController;
    Run();
    controller->NotifyFlushComplete();
    return;
  }

  EnsureRegisterAsEarlyRunner();

  mNeedsFlushCompleteNotification = true;
}

bool APZTaskRunnable::IsRegisteredWithCurrentPresShell() const {
  MOZ_ASSERT(mController);

  uint32_t current = 0;
  if (PresShell* presShell = mController->GetTopLevelPresShell()) {
    current = presShell->GetPresShellId();
  }
  return mRegisteredPresShellId == current;
}

void APZTaskRunnable::EnsureRegisterAsEarlyRunner() {
  if (IsRegisteredWithCurrentPresShell()) {
    return;
  }

  // If the registered presshell id has been changed, we need to discard pending
  // requests and notification since all of them are for documents which
  // have been torn down.
  if (mRegisteredPresShellId) {
    mPendingRepaintRequestMap.clear();
    mPendingRequestQueue.clear();
    mNeedsFlushCompleteNotification = false;
  }

  if (PresShell* presShell = mController->GetTopLevelPresShell()) {
    if (nsRefreshDriver* driver = presShell->GetRefreshDriver()) {
      driver->AddEarlyRunner(this);
      mRegisteredPresShellId = presShell->GetPresShellId();
    }
  }
}

bool APZTaskRunnable::IsTestControllingRefreshesEnabled() const {
  if (!mController) {
    return false;
  }

  if (PresShell* presShell = mController->GetTopLevelPresShell()) {
    if (nsRefreshDriver* driver = presShell->GetRefreshDriver()) {
      return driver->IsTestControllingRefreshesEnabled();
    }
  }
  return false;
}

}  // namespace mozilla::layers
