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

#include "gfxFontUtils.h"
#include "gfxTextRun.h"
#include "mozilla/Services.h"
#include "mozilla/ServoUtils.h"
#include "nsCRT.h"

#include "mozilla/dom/Document.h"
#include "nsPresContext.h"

using mozilla::services::GetObserverService;

NS_IMPL_ISUPPORTS(nsFontCache, nsIObserver)

static mozilla::LazyLogModule gFingerprinterDetection("FingerprinterDetection");

// The Init and Destroy methods are necessary because it's not
// safe to call AddObserver from a constructor or RemoveObserver
// from a destructor.  That should be fixed.
void nsFontCache::Init(nsPresContext* aContext) {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
  mContext = aContext;
  // register as a memory-pressure observer to free font resources
  // in low-memory situations.
  nsCOMPtr<nsIObserverService> obs = GetObserverService();
  if (obs) {
    obs->AddObserver(this, "memory-pressure", false);
  }

  mLocaleLanguage = nsLanguageAtomService::GetService()->GetLocaleLanguage();
  if (!mLocaleLanguage) {
    mLocaleLanguage = NS_Atomize("x-western");
  }
}

void nsFontCache::Destroy() {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIObserverService> obs = GetObserverService();
  if (obs) {
    obs->RemoveObserver(this, "memory-pressure");
  }
  Flush();
}

NS_IMETHODIMP
nsFontCache::Observe(nsISupports*, const char* aTopic, const char16_t*) {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
  if (!nsCRT::strcmp(aTopic, "memory-pressure")) {
    Compact();
  }
  return NS_OK;
}

already_AddRefed<nsFontMetrics> nsFontCache::GetMetricsFor(
    const nsFont& aFont, const nsFontMetrics::Params& aParams) {
  // We may eventually want to put an nsFontCache on canvas2d workers, but for
  // now it is only used by the main-thread layout code and stylo.
  mozilla::AssertIsMainThreadOrServoFontMetricsLocked();

  nsAtom* language = aParams.language && !aParams.language->IsEmpty()
                         ? aParams.language
                         : mLocaleLanguage.get();

  // First check our cache
  // start from the end, which is where we put the most-recent-used element
  const int32_t n = mFontMetrics.Length() - 1;
  for (int32_t i = n; i >= 0; --i) {
    nsFontMetrics* fm = mFontMetrics.Elements()[i];
    if (fm->Font().Equals(aFont) &&
        fm->GetUserFontSet() == aParams.userFontSet &&
        fm->Language() == language &&
        fm->Orientation() == aParams.orientation &&
#ifdef XP_WIN
        fm->AllowForceGDIClassic() == aParams.allowForceGDIClassic &&
#endif
        fm->ExplicitLanguage() == aParams.explicitLanguage) {
      if (i != n) {
        // promote it to the end of the cache
        mFontMetrics.RemoveElementAtUnsafe(i);
        mFontMetrics.AppendElement(fm);
      }
      fm->GetThebesFontGroup()->UpdateUserFonts();
      return do_AddRef(fm);
    }
  }

  DetectFontFingerprinting(aFont);

  // It's not in the cache. Get font metrics and then cache them.
  // If the cache has reached its size limit, drop the older half of the
  // entries; but if Servo traversal is running (the usual case), we have
  // to post a task back to the main thread to do the flush.
  if (n >= kMaxCacheEntries - 1 && !mFlushPending) {
    if (gfxFontUtils::IsInServoTraversal()) {
      mFlushPending = true;
      nsCOMPtr<nsIRunnable> flushTask = new FlushFontMetricsTask(this);
      MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(flushTask));
    } else {
      Flush(mFontMetrics.Length() - kMaxCacheEntries / 2);
    }
  }

  nsFontMetrics::Params params = aParams;
  params.language = language;
  RefPtr<nsFontMetrics> fm = new nsFontMetrics(aFont, params, mContext);
  // the mFontMetrics list has the "head" at the end, because append
  // is cheaper than insert
  mFontMetrics.AppendElement(do_AddRef(fm).take());
  return fm.forget();
}

void nsFontCache::DetectFontFingerprinting(const nsFont& aFont) {
  // We try to detect font fingerprinting attempts by recognizing a large
  // number of cache misses in a short amount of time, which indicates the
  // usage of an unreasonable amount of different fonts by the web page.

  if (aFont.family.families.list.IsEmpty()) {
    return;
  }

  if (!MOZ_LOG_TEST(gFingerprinterDetection, mozilla::LogLevel::Info) &&
      mReportedProbableFingerprinting) {
    // We've already reported fingerprinting for this document, and logging
    // isn't enabled, so skip the rest of the detection logic.
    return;
  }

  PRTime now = PR_Now();
  nsAutoString key;
  for (const auto& family : aFont.family.families.list.AsSpan()) {
    if (family.IsGeneric()) {
      continue;
    }
    key.Append(family.AsFamilyName().name.AsAtom()->GetUTF16String());
  }
  if (key.IsEmpty()) {
    return;
  }

  auto missedFonts = mMissedFontFamilyNames.Lock();
  missedFonts->InsertOrUpdate(key, now);
  // Don't bother checking for fingerprinting attempts if we haven't seen
  // enough cache misses yet.
  if (missedFonts->Count() <= kFingerprintingCacheMissThreshold) {
    return;
  }
  uint16_t fontsMissedRecently = 0;

  bool clearMissedFonts = false;
  if (!mReportedProbableFingerprinting) {
    for (auto iter = missedFonts->Iter(); !iter.Done(); iter.Next()) {
      if (now - kFingerprintingLastNSec <= iter.Data()) {
        if (++fontsMissedRecently > kFingerprintingCacheMissThreshold) {
          mContext->Document()->RecordFontFingerprinting();
          mReportedProbableFingerprinting = true;
          clearMissedFonts = true;
          break;
        }
      } else {
        // Remove the old entries from missed cache list.
        iter.Remove();
      }
    }
    if (mReportedProbableFingerprinting) {
      // We detected a font fingerprinter, so print out all the fonts they
      // queries and missed
      for (auto iter = missedFonts->Iter(); !iter.Done(); iter.Next()) {
        MOZ_LOG(
            gFingerprinterDetection, mozilla::LogLevel::Info,
            ("Font Fingerprinting Tripped | Document %p | Font Family | %s",
             mContext->Document(), NS_ConvertUTF16toUTF8(iter.Key()).get()));
      }
    }
  } else {
    // I've already reported the font fingerprinting, but I want to report each
    // additional font
    MOZ_LOG(gFingerprinterDetection, mozilla::LogLevel::Info,
            ("Font Fingerprinting Tripped | Document %p | Font Family | %s",
             mContext->Document(), NS_ConvertUTF16toUTF8(key).get()));
  }
  if (!MOZ_LOG_TEST(gFingerprinterDetection, mozilla::LogLevel::Info) &&
      clearMissedFonts) {
    missedFonts->Clear();
  }
}

void nsFontCache::UpdateUserFonts(gfxUserFontSet* aUserFontSet) {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
  for (nsFontMetrics* fm : mFontMetrics) {
    gfxFontGroup* fg = fm->GetThebesFontGroup();
    if (fg->GetUserFontSet() == aUserFontSet) {
      fg->UpdateUserFonts();
    }
  }
}

void nsFontCache::FontMetricsDeleted(const nsFontMetrics* aFontMetrics) {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
  mFontMetrics.RemoveElement(aFontMetrics);
}

void nsFontCache::Compact() {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
  // Need to loop backward because the running element can be removed on
  // the way
  for (int32_t i = mFontMetrics.Length() - 1; i >= 0; --i) {
    nsFontMetrics* fm = mFontMetrics[i];
    nsFontMetrics* oldfm = fm;
    // Destroy() isn't here because we want our device context to be
    // notified
    NS_RELEASE(fm);  // this will reset fm to nullptr
    // if the font is really gone, it would have called back in
    // FontMetricsDeleted() and would have removed itself
    if (mFontMetrics.IndexOf(oldfm) != mFontMetrics.NoIndex) {
      // nope, the font is still there, so let's hold onto it too
      NS_ADDREF(oldfm);
    }
  }
  auto missedFonts = mMissedFontFamilyNames.Lock();
  missedFonts->Clear();
}

// Flush the aFlushCount oldest entries, or all if (aFlushCount < 0)
void nsFontCache::Flush(int32_t aFlushCount) {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
  int32_t n = aFlushCount < 0
                  ? mFontMetrics.Length()
                  : std::min<int32_t>(aFlushCount, mFontMetrics.Length());
  for (int32_t i = n - 1; i >= 0; --i) {
    nsFontMetrics* fm = mFontMetrics[i];
    // Destroy() will unhook our device context from the fm so that we
    // won't waste time in triggering the notification of
    // FontMetricsDeleted() in the subsequent release
    fm->Destroy();
    NS_RELEASE(fm);
  }
  mFontMetrics.RemoveElementsAt(0, n);
}
