/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 ;*; */
/* vim: set sw=2 ts=8 et 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 "nsAutoPtr.h"
#include "nsIDocShell.h"
#include "nsIDocument.h"
#include "nsIDocumentLoader.h"
#include "nsIObserverService.h"
#include "nsIXULRuntime.h"
#include "nsServiceManagerUtils.h"
#include "nsThreadUtils.h"
#include "RequestContextService.h"

#include "mozilla/Atomics.h"
#include "mozilla/Logging.h"
#include "mozilla/Services.h"
#include "mozilla/TimeStamp.h"

#include "mozilla/net/NeckoChild.h"
#include "mozilla/net/NeckoCommon.h"
#include "mozilla/net/PSpdyPush.h"

#include "../protocol/http/nsHttpHandler.h"

namespace mozilla {
namespace net {

LazyLogModule gRequestContextLog("RequestContext");
#undef LOG
#define LOG(args) MOZ_LOG(gRequestContextLog, LogLevel::Info, args)

// This is used to prevent adding tail pending requests after shutdown
static bool sShutdown = false;

// nsIRequestContext
class RequestContext final : public nsIRequestContext
                           , public nsITimerCallback
{
public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIREQUESTCONTEXT
  NS_DECL_NSITIMERCALLBACK

  explicit RequestContext(const uint64_t id);
private:
  virtual ~RequestContext();

  void ProcessTailQueue(nsresult aResult);
  // Reschedules the timer if needed
  void ScheduleUnblock();
  // Hard-reschedules the timer
  void RescheduleUntailTimer(TimeStamp const& now);

  uint64_t mID;
  Atomic<uint32_t>       mBlockingTransactionCount;
  nsAutoPtr<SpdyPushCache> mSpdyCache;
  nsCString mUserAgentOverride;

  typedef nsCOMPtr<nsIRequestTailUnblockCallback> PendingTailRequest;
  // Number of known opened non-tailed requets
  uint32_t mNonTailRequests;
  // Queue of requests that have been tailed, when conditions are met
  // we call each of them to unblock and drop the reference
  nsTArray<PendingTailRequest> mTailQueue;
  // Loosly scheduled timer, never scheduled further to the future than
  // mUntailAt time
  nsCOMPtr<nsITimer> mUntailTimer;
  // Timestamp when the timer is expected to fire,
  // always less than or equal to mUntailAt
  TimeStamp mTimerScheduledAt;
  // Timestamp when we want to actually untail queued requets based on
  // the number of request count change in the past; iff this timestamp
  // is set, we tail requests
  TimeStamp mUntailAt;

  // Timestamp of the navigation start time, set to Now() in BeginLoad().
  // This is used to progressively lower the maximum delay time so that
  // we can't get to a situation when a number of repetitive requests
  // on the page causes forever tailing.
  TimeStamp mBeginLoadTime;

  // This member is true only between DOMContentLoaded notification and
  // next document load beginning for this request context.
  // Top level request contexts are recycled.
  bool mAfterDOMContentLoaded;
};

NS_IMPL_ISUPPORTS(RequestContext, nsIRequestContext, nsITimerCallback)

RequestContext::RequestContext(const uint64_t aID)
  : mID(aID)
  , mBlockingTransactionCount(0)
  , mNonTailRequests(0)
  , mAfterDOMContentLoaded(false)
{
  LOG(("RequestContext::RequestContext this=%p id=%" PRIx64, this, mID));
}

RequestContext::~RequestContext()
{
  MOZ_ASSERT(mTailQueue.Length() == 0);

  LOG(("RequestContext::~RequestContext this=%p blockers=%u",
       this, static_cast<uint32_t>(mBlockingTransactionCount)));
}

NS_IMETHODIMP
RequestContext::BeginLoad()
{
  MOZ_ASSERT(NS_IsMainThread());

  LOG(("RequestContext::BeginLoad %p", this));

  if (IsNeckoChild()) {
    // Tailing is not supported on the child process
    if (gNeckoChild) {
      gNeckoChild->SendRequestContextLoadBegin(mID);
    }
    return NS_OK;
  }

  mAfterDOMContentLoaded = false;
  mBeginLoadTime = TimeStamp::NowLoRes();
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::DOMContentLoaded()
{
  MOZ_ASSERT(NS_IsMainThread());

  LOG(("RequestContext::DOMContentLoaded %p", this));

  if (IsNeckoChild()) {
    // Tailing is not supported on the child process
    if (gNeckoChild) {
      gNeckoChild->SendRequestContextAfterDOMContentLoaded(mID);
    }
    return NS_OK;
  }

  if (mAfterDOMContentLoaded) {
    // There is a possibility of a duplicate notification
    return NS_OK;
  }

  mAfterDOMContentLoaded = true;

  // Conditions for the delay calculation has changed.
  ScheduleUnblock();
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::GetBlockingTransactionCount(uint32_t *aBlockingTransactionCount)
{
  NS_ENSURE_ARG_POINTER(aBlockingTransactionCount);
  *aBlockingTransactionCount = mBlockingTransactionCount;
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::AddBlockingTransaction()
{
  mBlockingTransactionCount++;
  LOG(("RequestContext::AddBlockingTransaction this=%p blockers=%u",
       this, static_cast<uint32_t>(mBlockingTransactionCount)));
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::RemoveBlockingTransaction(uint32_t *outval)
{
  NS_ENSURE_ARG_POINTER(outval);
  mBlockingTransactionCount--;
  LOG(("RequestContext::RemoveBlockingTransaction this=%p blockers=%u",
       this, static_cast<uint32_t>(mBlockingTransactionCount)));
  *outval = mBlockingTransactionCount;
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::GetSpdyPushCache(mozilla::net::SpdyPushCache **aSpdyPushCache)
{
  *aSpdyPushCache = mSpdyCache.get();
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::SetSpdyPushCache(mozilla::net::SpdyPushCache *aSpdyPushCache)
{
  mSpdyCache = aSpdyPushCache;
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::GetID(uint64_t *outval)
{
  NS_ENSURE_ARG_POINTER(outval);
  *outval = mID;
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::GetUserAgentOverride(nsACString& aUserAgentOverride)
{
  aUserAgentOverride = mUserAgentOverride;
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::SetUserAgentOverride(const nsACString& aUserAgentOverride)
{
  mUserAgentOverride = aUserAgentOverride;
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::AddNonTailRequest()
{
  MOZ_ASSERT(NS_IsMainThread());

  ++mNonTailRequests;
  LOG(("RequestContext::AddNonTailRequest this=%p, cnt=%u",
       this, mNonTailRequests));

  ScheduleUnblock();
  return NS_OK;
}

NS_IMETHODIMP
RequestContext::RemoveNonTailRequest()
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mNonTailRequests > 0);

  LOG(("RequestContext::RemoveNonTailRequest this=%p, cnt=%u",
       this, mNonTailRequests - 1));

  --mNonTailRequests;

  ScheduleUnblock();
  return NS_OK;
}

void
RequestContext::ScheduleUnblock()
{
  MOZ_ASSERT(!IsNeckoChild());
  MOZ_ASSERT(NS_IsMainThread());

  if (!gHttpHandler) {
    return;
  }

  uint32_t quantum = gHttpHandler->TailBlockingDelayQuantum(mAfterDOMContentLoaded);
  uint32_t delayMax = gHttpHandler->TailBlockingDelayMax();
  uint32_t totalMax = gHttpHandler->TailBlockingTotalMax();

  if (!mBeginLoadTime.IsNull()) {
    // We decrease the maximum delay progressively with the time since the page load
    // begin.  This seems like a reasonable and clear heuristic allowing us to start
    // loading tailed requests in a deterministic time after the load has started.

    uint32_t sinceBeginLoad = static_cast<uint32_t>(
      (TimeStamp::NowLoRes() - mBeginLoadTime).ToMilliseconds());
    uint32_t tillTotal = totalMax - std::min(sinceBeginLoad, totalMax);
    uint32_t proportion = totalMax // values clamped between 0 and 60'000
      ? (delayMax * tillTotal) / totalMax
      : 0;
    delayMax = std::min(delayMax, proportion);
  }

  CheckedInt<uint32_t> delay = quantum * mNonTailRequests;

  if (!mAfterDOMContentLoaded) {
    // Before DOMContentLoaded notification we want to make sure that tailed
    // requests don't start when there is a short delay during which we may
    // not have any active requests on the page happening.
    delay += quantum;
  }

  if (!delay.isValid() || delay.value() > delayMax) {
    delay = delayMax;
  }

  LOG(("RequestContext::ScheduleUnblock this=%p non-tails=%u tail-queue=%zu delay=%u after-DCL=%d",
       this, mNonTailRequests, mTailQueue.Length(), delay.value(), mAfterDOMContentLoaded));

  TimeStamp now = TimeStamp::NowLoRes();
  mUntailAt = now + TimeDuration::FromMilliseconds(delay.value());

  if (mTimerScheduledAt.IsNull() || mUntailAt < mTimerScheduledAt) {
    LOG(("RequestContext %p timer would fire too late, rescheduling", this));
    RescheduleUntailTimer(now);
  }
}

void
RequestContext::RescheduleUntailTimer(TimeStamp const& now)
{
  MOZ_ASSERT(mUntailAt >= now);

  if (mUntailTimer) {
    mUntailTimer->Cancel();
  }

  if (!mTailQueue.Length()) {
    mUntailTimer = nullptr;
    mTimerScheduledAt = TimeStamp();
    return;
  }

  TimeDuration interval = mUntailAt - now;
  if (!mTimerScheduledAt.IsNull() && mUntailAt < mTimerScheduledAt) {
    // When the number of untailed requests goes down,
    // let's half the interval, since it's likely we would
    // reschedule for a shorter time again very soon.
    // This will likely save rescheduling this timer.
    interval = interval / int64_t(2);
    mTimerScheduledAt = mUntailAt - interval;
  } else {
    mTimerScheduledAt = mUntailAt;
  }

  uint32_t delay = interval.ToMilliseconds();
  mUntailTimer = do_CreateInstance("@mozilla.org/timer;1");
  mUntailTimer->InitWithCallback(this, delay, nsITimer::TYPE_ONE_SHOT);

  LOG(("RequestContext::RescheduleUntailTimer %p in %d", this, delay));
}

NS_IMETHODIMP
RequestContext::Notify(nsITimer *timer)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(timer == mUntailTimer);
  MOZ_ASSERT(!mTimerScheduledAt.IsNull());
  MOZ_ASSERT(mTailQueue.Length());

  mUntailTimer = nullptr;

  TimeStamp now = TimeStamp::NowLoRes();
  if (mUntailAt > mTimerScheduledAt && mUntailAt > now) {
    LOG(("RequestContext %p timer fired too soon, rescheduling", this));
    RescheduleUntailTimer(now);
    return NS_OK;
  }

  // Must drop to allow re-engage of the timer
  mTimerScheduledAt = TimeStamp();

  ProcessTailQueue(NS_OK);

  return NS_OK;
}

NS_IMETHODIMP
RequestContext::IsContextTailBlocked(nsIRequestTailUnblockCallback * aRequest,
                                     bool *aBlocked)
{
  MOZ_ASSERT(NS_IsMainThread());

  LOG(("RequestContext::IsContextTailBlocked this=%p, request=%p, queued=%zu",
       this, aRequest, mTailQueue.Length()));

  *aBlocked = false;

  if (sShutdown) {
    return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
  }

  if (mUntailAt.IsNull()) {
    LOG(("  untail time passed"));
    return NS_OK;
  }

  if (mAfterDOMContentLoaded && !mNonTailRequests) {
    LOG(("  after DOMContentLoaded and no untailed requests"));
    return NS_OK;
  }

  if (!gHttpHandler) {
    // Xpcshell tests may not have http handler
    LOG(("  missing gHttpHandler?"));
    return NS_OK;
  }

  *aBlocked = true;
  mTailQueue.AppendElement(aRequest);

  LOG(("  request queued"));

  if (!mUntailTimer) {
    ScheduleUnblock();
  }

  return NS_OK;
}

NS_IMETHODIMP
RequestContext::CancelTailedRequest(nsIRequestTailUnblockCallback * aRequest)
{
  MOZ_ASSERT(NS_IsMainThread());

  bool removed = mTailQueue.RemoveElement(aRequest);

  LOG(("RequestContext::CancelTailedRequest %p req=%p removed=%d",
       this, aRequest, removed));

  // Stop untail timer if all tail requests are canceled.
  if (removed && mTailQueue.IsEmpty()) {
    if (mUntailTimer) {
      mUntailTimer->Cancel();
      mUntailTimer = nullptr;
    }

    // Must drop to allow re-engage of the timer
    mTimerScheduledAt = TimeStamp();
  }

  return NS_OK;
}

void
RequestContext::ProcessTailQueue(nsresult aResult)
{
  LOG(("RequestContext::ProcessTailQueue this=%p, queued=%zu, rv=%" PRIx32,
       this, mTailQueue.Length(), static_cast<uint32_t>(aResult)));

  if (mUntailTimer) {
    mUntailTimer->Cancel();
    mUntailTimer = nullptr;
  }

  // Must drop to stop tailing requests
  mUntailAt = TimeStamp();

  nsTArray<PendingTailRequest> queue;
  queue.SwapElements(mTailQueue);

  for (auto request : queue) {
    LOG(("  untailing %p", request.get()));
    request->OnTailUnblock(aResult);
  }
}

NS_IMETHODIMP
RequestContext::CancelTailPendingRequests(nsresult aResult)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(NS_FAILED(aResult));

  ProcessTailQueue(aResult);
  return NS_OK;
}

//nsIRequestContextService
RequestContextService *RequestContextService::sSelf = nullptr;

NS_IMPL_ISUPPORTS(RequestContextService, nsIRequestContextService, nsIObserver)

RequestContextService::RequestContextService()
  : mNextRCID(1)
{
  MOZ_ASSERT(!sSelf, "multiple rcs instances!");
  MOZ_ASSERT(NS_IsMainThread());
  sSelf = this;

  nsCOMPtr<nsIXULRuntime> runtime = do_GetService("@mozilla.org/xre/runtime;1");
  runtime->GetProcessID(&mRCIDNamespace);
}

RequestContextService::~RequestContextService()
{
  MOZ_ASSERT(NS_IsMainThread());
  Shutdown();
  sSelf = nullptr;
}

nsresult
RequestContextService::Init()
{
  nsresult rv;

  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (!obs) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
  if (NS_FAILED(rv)) {
    return rv;
  }
  obs->AddObserver(this, "content-document-interactive", false);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return NS_OK;
}

void
RequestContextService::Shutdown()
{
  MOZ_ASSERT(NS_IsMainThread());
  // We need to do this to prevent the requests from being scheduled after
  // shutdown.
  for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) {
    iter.Data()->CancelTailPendingRequests(NS_ERROR_ABORT);
  }
  mTable.Clear();
  sShutdown = true;
}

/* static */ nsresult
RequestContextService::Create(nsISupports *aOuter, const nsIID& aIID, void **aResult)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (aOuter != nullptr) {
    return NS_ERROR_NO_AGGREGATION;
  }

  RefPtr<RequestContextService> svc = new RequestContextService();
  nsresult rv = svc->Init();
  NS_ENSURE_SUCCESS(rv, rv);

  return svc->QueryInterface(aIID, aResult);
}

NS_IMETHODIMP
RequestContextService::GetRequestContext(const uint64_t rcID, nsIRequestContext **rc)
{
  MOZ_ASSERT(NS_IsMainThread());
  NS_ENSURE_ARG_POINTER(rc);
  *rc = nullptr;

  if (sShutdown) {
    return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
  }

  if (!mTable.Get(rcID, rc)) {
    nsCOMPtr<nsIRequestContext> newSC = new RequestContext(rcID);
    mTable.Put(rcID, newSC);
    newSC.swap(*rc);
  }

  return NS_OK;
}

NS_IMETHODIMP
RequestContextService::GetRequestContextFromLoadGroup(nsILoadGroup *aLoadGroup, nsIRequestContext **rc)
{
  nsresult rv;

  uint64_t rcID;
  rv = aLoadGroup->GetRequestContextID(&rcID);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return GetRequestContext(rcID, rc);
}

NS_IMETHODIMP
RequestContextService::NewRequestContext(nsIRequestContext **rc)
{
  MOZ_ASSERT(NS_IsMainThread());
  NS_ENSURE_ARG_POINTER(rc);
  *rc = nullptr;

  if (sShutdown) {
    return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
  }

  uint64_t rcID = ((static_cast<uint64_t>(mRCIDNamespace) << 32) & 0xFFFFFFFF00000000LL) | mNextRCID++;

  nsCOMPtr<nsIRequestContext> newSC = new RequestContext(rcID);
  mTable.Put(rcID, newSC);
  newSC.swap(*rc);

  return NS_OK;
}

NS_IMETHODIMP
RequestContextService::RemoveRequestContext(const uint64_t rcID)
{
  if (IsNeckoChild() && gNeckoChild) {
    gNeckoChild->SendRemoveRequestContext(rcID);
  }

  MOZ_ASSERT(NS_IsMainThread());
  mTable.Remove(rcID);
  return NS_OK;
}

NS_IMETHODIMP
RequestContextService::Observe(nsISupports *subject, const char *topic,
                                  const char16_t *data_unicode)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (!strcmp(NS_XPCOM_SHUTDOWN_OBSERVER_ID, topic)) {
    Shutdown();
    return NS_OK;
  }

  if (!strcmp("content-document-interactive", topic)) {
    nsCOMPtr<nsIDocument> document(do_QueryInterface(subject));
    MOZ_ASSERT(document);
    // We want this be triggered also for iframes, since those track their
    // own request context ids.
    if (!document) {
      return NS_OK;
    }
    nsIDocShell* ds = document->GetDocShell();
    // XML documents don't always have a docshell assigned
    if (!ds) {
      return NS_OK;
    }
    nsCOMPtr<nsIDocumentLoader> dl(do_QueryInterface(ds));
    if (!dl) {
      return NS_OK;
    }
    nsCOMPtr<nsILoadGroup> lg;
    dl->GetLoadGroup(getter_AddRefs(lg));
    if (!lg) {
      return NS_OK;
    }
    nsCOMPtr<nsIRequestContext> rc;
    GetRequestContextFromLoadGroup(lg, getter_AddRefs(rc));
    if (rc) {
      rc->DOMContentLoaded();
    }

    return NS_OK;
  }

  MOZ_ASSERT(false, "Unexpected observer topic");
  return NS_OK;
}

} // ::mozilla::net
} // ::mozilla

#undef LOG
