/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "mozilla/dom/SpeculationRulesManager.h"

#include "mozilla/Maybe.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/PrefetchIPCTypes.h"
#include "mozilla/dom/PrefetchLog.h"
#include "mozilla/dom/PrefetchTypes.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/dom/WindowGlobalChild.h"
#include "mozilla/dom/speculationrules_ffi_generated.h"
#include "nsNetUtil.h"
#include "nsTHashSet.h"

namespace mozilla::dom {

void SpeculationRulesManager::RemoveRecord(PrefetchRecordChild* aRecord) {
  mPrefetchRecords.RemoveElement(aRecord);
}

void SpeculationRulesManager::StartPrefetch(Document* aDocument,
                                            const PrefetchCandidate& aCand) {
  WindowGlobalChild* wgc = aDocument->GetWindowGlobalChild();
  if (!wgc || wgc->IsClosed()) {
    return;
  }

  nsCOMPtr<nsIURI> uri;
  if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), aCand.url))) {
    LOG_SPECRULES_WARN(
        ("StartPrefetch: invalid candidate URL '%s'", aCand.url.get()));
    return;
  }

  // A candidate can be enacted repeatedly, for instance every time the user
  // hovers a moderate eagerness link, so don't prefetch a URL that this
  // document is already prefetching.
  for (const RefPtr<PrefetchRecordChild>& record : mPrefetchRecords) {
    bool equals = false;
    if (record->URL() && NS_SUCCEEDED(record->URL()->Equals(uri, &equals)) &&
        equals) {
      LOG_SPECRULES(("StartPrefetch: already prefetching %s", aCand.url.get()));
      return;
    }
  }

  nsCOMPtr<nsIReferrerInfo> referrerInfo =
      new ReferrerInfo(aDocument->GetDocumentURIAsReferrer(),
                       static_cast<ReferrerPolicy>(aCand.referrer_policy));

  nsString noVarySearchHint;
  if (!aCand.no_vary_search_hint.IsVoid()) {
    CopyUTF8toUTF16(aCand.no_vary_search_hint, noVarySearchHint);
  }

  nsTArray<Maybe<nsString>> tags;
  for (const nsCString& tag : aCand.tags) {
    tags.AppendElement(tag.IsVoid() ? Nothing()
                                    : Some(NS_ConvertUTF8toUTF16(tag)));
  }

  const bool anonymizeClientIP = false;

  SpeculativePrefetchArgs args(WrapNotNull(uri.get()), referrerInfo,
                               noVarySearchHint, anonymizeClientIP,
                               std::move(tags));

  auto actor =
      MakeRefPtr<PrefetchRecordChild>(uri, PrefetchAnonymizationPolicy::None);

  if (!wgc->SendPPrefetchRecordConstructor(actor.get(), args)) {
    LOG_SPECRULES(("StartPrefetch: IPC send failed"));
    return;
  }

  mPrefetchRecords.AppendElement(std::move(actor));
}

void SpeculationRulesManager::CancelStalePrefetches(
    const nsTArray<PrefetchCandidate>& aStillSpeculated) {
  nsTHashSet<nsCString> stillSpeculatedUrls;
  for (const PrefetchCandidate& cand : aStillSpeculated) {
    stillSpeculatedUrls.Insert(cand.url);
  }

  nsTArray<RefPtr<PrefetchRecordChild>> stale;
  mPrefetchRecords.RemoveElementsBy(
      [&](const RefPtr<PrefetchRecordChild>& aRecord) {
        nsAutoCString spec;
        if (aRecord->URL()) {
          aRecord->URL()->GetSpec(spec);
        }
        if (stillSpeculatedUrls.Contains(spec)) {
          return false;
        }
        stale.AppendElement(aRecord);
        return true;
      });

  // Cancel and destroy the stale actors only after mPrefetchRecords has been
  // fully updated above: Send__delete__ synchronously runs ActorDestroy,
  // which re-enters SpeculationRulesManager::RemoveRecord and mutates
  // mPrefetchRecords, which RemoveElementsBy above does not tolerate while
  // it's still iterating.
  for (const RefPtr<PrefetchRecordChild>& record : stale) {
    nsAutoCString spec;
    if (record->URL()) {
      record->URL()->GetSpec(spec);
    }
    LOG_SPECRULES(("CancelStalePrefetches: canceling %s", spec.get()));
    record->SendCancel();
    PrefetchRecordChild::Send__delete__(record);
  }
}

}  // namespace mozilla::dom
