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

#include <algorithm>

#include "mozilla/AutoRestore.h"
#include "mozilla/PresShell.h"
#include "mozilla/PresShellInlines.h"
#include "mozilla/RestyleManager.h"
#include "mozilla/SMILTimedElement.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/SVGAnimationElement.h"
#include "nsContentUtils.h"
#include "nsCSSProps.h"
#include "nsRefreshDriver.h"
#include "mozilla/dom/Document.h"
#include "SMILCompositor.h"
#include "SMILCSSProperty.h"

using namespace mozilla::dom;

namespace mozilla {

//----------------------------------------------------------------------
// SMILAnimationController implementation

//----------------------------------------------------------------------
// ctors, dtors, factory methods

SMILAnimationController::SMILAnimationController(Document* aDoc)
    : mDocument(aDoc) {
  MOZ_ASSERT(aDoc, "need a non-null document");

  if (nsRefreshDriver* refreshDriver = GetRefreshDriver()) {
    mStartTime = refreshDriver->MostRecentRefresh();
  } else {
    mStartTime = mozilla::TimeStamp::Now();
  }
  mCurrentSampleTime = mStartTime;

  Begin();
}

SMILAnimationController::~SMILAnimationController() {
  NS_ASSERTION(mAnimationElementTable.Count() == 0,
               "Animation controller shouldn't be tracking any animation"
               " elements when it dies");
  NS_ASSERTION(!mRegisteredWithRefreshDriver,
               "Leaving stale entry in refresh driver's observer list");
}

void SMILAnimationController::Disconnect() {
  MOZ_ASSERT(mDocument, "disconnecting when we weren't connected...?");
  MOZ_ASSERT(mRefCnt.get() == 1,
             "Expecting to disconnect when doc is sole remaining owner");
  NS_ASSERTION(mPauseState & SMILTimeContainer::PAUSE_PAGEHIDE,
               "Expecting to be paused for pagehide before disconnect");

  StopSampling(GetRefreshDriver());

  mDocument = nullptr;  // (raw pointer)
}

//----------------------------------------------------------------------
// SMILTimeContainer methods:

void SMILAnimationController::Pause(uint32_t aType) {
  SMILTimeContainer::Pause(aType);
  UpdateSampling();
}

void SMILAnimationController::Resume(uint32_t aType) {
  bool wasPaused = !!mPauseState;
  // Update mCurrentSampleTime so that calls to GetParentTime--used for
  // calculating parent offsets--are accurate
  mCurrentSampleTime = mozilla::TimeStamp::Now();

  SMILTimeContainer::Resume(aType);

  if (wasPaused && !mPauseState) {
    UpdateSampling();
  }
}

SMILTime SMILAnimationController::GetParentTime() const {
  return (SMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds();
}

//----------------------------------------------------------------------
// nsARefreshObserver methods:
NS_IMPL_ADDREF(SMILAnimationController)
NS_IMPL_RELEASE(SMILAnimationController)

// nsRefreshDriver Callback function
void SMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) {
  // Although we never expect aTime to go backwards, when we initialise the
  // animation controller, if we can't get hold of a refresh driver we
  // initialise mCurrentSampleTime to Now(). It may be possible that after
  // doing so we get sampled by a refresh driver whose most recent refresh time
  // predates when we were initialised, so to be safe we make sure to take the
  // most recent time here.
  aTime = std::max(mCurrentSampleTime, aTime);

  // Sleep detection: If the time between samples is a whole lot greater than we
  // were expecting then we assume the computer went to sleep or someone's
  // messing with the clock. In that case, fiddle our parent offset and use our
  // average time between samples to calculate the new sample time. This
  // prevents us from hanging while trying to catch up on all the missed time.

  // Smoothing of coefficient for the average function. 0.2 should let us track
  // the sample rate reasonably tightly without being overly affected by
  // occasional delays.
  static const double SAMPLE_DUR_WEIGHTING = 0.2;
  // If the elapsed time exceeds our expectation by this number of times we'll
  // initiate special behaviour to basically ignore the intervening time.
  static const double SAMPLE_DEV_THRESHOLD = 200.0;

  SMILTime elapsedTime =
      (SMILTime)(aTime - mCurrentSampleTime).ToMilliseconds();
  if (mAvgTimeBetweenSamples == 0) {
    // First sample.
    mAvgTimeBetweenSamples = elapsedTime;
  } else {
    if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) {
      // Unexpectedly long delay between samples.
      NS_WARNING(
          "Detected really long delay between samples, continuing from "
          "previous sample");
      mParentOffset += elapsedTime - mAvgTimeBetweenSamples;
    }
    // Update the moving average. Due to truncation here the average will
    // normally be a little less than it should be but that's probably ok.
    mAvgTimeBetweenSamples =
        (SMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING +
                   mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING));
  }
  mCurrentSampleTime = aTime;

  Sample();
}

//----------------------------------------------------------------------
// Animation element registration methods:

void SMILAnimationController::RegisterAnimationElement(
    SVGAnimationElement* aAnimationElement) {
  const bool wasEmpty = mAnimationElementTable.IsEmpty();
  mAnimationElementTable.PutEntry(aAnimationElement);
  if (wasEmpty) {
    UpdateSampling();
  }
}

void SMILAnimationController::UnregisterAnimationElement(
    SVGAnimationElement* aAnimationElement) {
  mAnimationElementTable.RemoveEntry(aAnimationElement);
  if (mAnimationElementTable.IsEmpty()) {
    UpdateSampling();
  }
}

//----------------------------------------------------------------------
// Page show/hide

void SMILAnimationController::OnPageShow() {
  Resume(SMILTimeContainer::PAUSE_PAGEHIDE);
}

void SMILAnimationController::OnPageHide() {
  Pause(SMILTimeContainer::PAUSE_PAGEHIDE);
}

//----------------------------------------------------------------------
// Cycle-collection support

void SMILAnimationController::Traverse(
    nsCycleCollectionTraversalCallback* aCallback) {
  // Traverse last compositor table
  if (mLastCompositorTable) {
    for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) {
      SMILCompositor* compositor = iter.Get();
      compositor->Traverse(aCallback);
    }
  }
}

void SMILAnimationController::Unlink() { mLastCompositorTable = nullptr; }

//----------------------------------------------------------------------
// Refresh driver lifecycle related methods

void SMILAnimationController::NotifyRefreshDriverCreated(
    nsRefreshDriver* aRefreshDriver) {
  UpdateSampling();
}

void SMILAnimationController::NotifyRefreshDriverDestroying(
    nsRefreshDriver* aRefreshDriver) {
  StopSampling(aRefreshDriver);
}

//----------------------------------------------------------------------
// Timer-related implementation helpers

bool SMILAnimationController::ShouldSample() const {
  return !mPauseState && !mAnimationElementTable.IsEmpty() &&
         !mChildContainerTable.IsEmpty();
}

void SMILAnimationController::UpdateSampling() {
  const bool shouldSample = ShouldSample();
  const bool isSampling = mRegisteredWithRefreshDriver;
  if (shouldSample == isSampling) {
    return;
  }

  nsRefreshDriver* driver = GetRefreshDriver();
  if (!driver) {
    return;
  }

  if (shouldSample) {
    // We're effectively resuming from a pause so update our current sample time
    // or else it will confuse our "average time between samples" calculations.
    mCurrentSampleTime = mozilla::TimeStamp::Now();
    driver->AddRefreshObserver(this, FlushType::Style, "SMIL animations");
    mRegisteredWithRefreshDriver = true;
    Sample();  // Run the first sample manually.
  } else {
    StopSampling(driver);
  }
}

void SMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) {
  if (aRefreshDriver && mRegisteredWithRefreshDriver) {
    // NOTE: The document might already have been detached from its PresContext
    // (and RefreshDriver), which would make GetRefreshDriver() return null.
    MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(),
               "Stopping sampling with wrong refresh driver");
    aRefreshDriver->RemoveRefreshObserver(this, FlushType::Style);
    mRegisteredWithRefreshDriver = false;
  }
}

//----------------------------------------------------------------------
// Sample-related methods and callbacks

void SMILAnimationController::DoSample() {
  DoSample(true);  // Skip unchanged time containers
}

void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) {
  if (!mDocument) {
    NS_ERROR("Shouldn't be sampling after document has disconnected");
    return;
  }
  if (mRunningSample) {
    NS_ERROR("Shouldn't be recursively sampling");
    return;
  }

  bool isStyleFlushNeeded = mResampleNeeded;
  mResampleNeeded = false;

  nsCOMPtr<Document> document(mDocument);  // keeps 'this' alive too

  // Set running sample flag -- do this before flushing styles so that when we
  // flush styles we don't end up requesting extra samples
  AutoRestore<bool> autoRestoreRunningSample(mRunningSample);
  mRunningSample = true;

  // STEP 1: Bring model up to date
  // (i)  Rewind elements where necessary
  // (ii) Run milestone samples
  RewindElements();
  DoMilestoneSamples();

  // STEP 2: Sample the child time containers
  //
  // When we sample the child time containers they will simply record the sample
  // time in document time.
  TimeContainerHashtable activeContainers(mChildContainerTable.Count());
  for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
    if (!container) {
      continue;
    }

    if (!container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN) &&
        (container->NeedsSample() || !aSkipUnchangedContainers)) {
      container->ClearMilestones();
      container->Sample();
      container->MarkSeekFinished();
      activeContainers.PutEntry(container);
    }
  }

  // STEP 3: (i)  Sample the timed elements AND
  //         (ii) Create a table of compositors
  //
  // (i) Here we sample the timed elements (fetched from the
  // SVGAnimationElements) which determine from the active time if the
  // element is active and what its simple time etc. is. This information is
  // then passed to its time client (SMILAnimationFunction).
  //
  // (ii) During the same loop we also build up a table that contains one
  // compositor for each animated attribute and which maps animated elements to
  // the corresponding compositor for their target attribute.
  //
  // Note that this compositor table needs to be allocated on the heap so we can
  // store it until the next sample. This lets us find out which elements were
  // animated in sample 'n-1' but not in sample 'n' (and hence need to have
  // their animation effects removed in sample 'n').
  //
  // Parts (i) and (ii) are not functionally related but we combine them here to
  // save iterating over the animation elements twice.

  // Create the compositor table
  UniquePtr<SMILCompositorTable> currentCompositorTable(
      new SMILCompositorTable(0));
  nsTArray<RefPtr<SVGAnimationElement>> animElems(
      mAnimationElementTable.Count());

  for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) {
    SampleTimedElement(animElem, &activeContainers);
    AddAnimationToCompositorTable(animElem, currentCompositorTable.get(),
                                  isStyleFlushNeeded);
    animElems.AppendElement(animElem);
  }
  activeContainers.Clear();

  // STEP 4: Compare previous sample's compositors against this sample's.
  // (Transfer cached base values across, & remove animation effects from
  // no-longer-animated targets.)
  if (mLastCompositorTable) {
    // * Transfer over cached base values, from last sample's compositors
    for (auto iter = currentCompositorTable->Iter(); !iter.Done();
         iter.Next()) {
      SMILCompositor* compositor = iter.Get();
      SMILCompositor* lastCompositor =
          mLastCompositorTable->GetEntry(compositor->GetKey());

      if (lastCompositor) {
        compositor->StealCachedBaseValue(lastCompositor);
      }
    }

    // * For each compositor in current sample's hash table, remove entry from
    // prev sample's hash table -- we don't need to clear animation
    // effects of those compositors, since they're still being animated.
    for (const auto& key : currentCompositorTable->Keys()) {
      mLastCompositorTable->RemoveEntry(key);
    }

    // * For each entry that remains in prev sample's hash table (i.e. for
    // every target that's no longer animated), clear animation effects.
    for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) {
      iter.Get()->ClearAnimationEffects();
    }
  }

  // return early if there are no active animations to avoid a style flush
  if (currentCompositorTable->Count() == 0) {
    mLastCompositorTable = nullptr;
    return;
  }

  if (isStyleFlushNeeded) {
    document->FlushPendingNotifications(FlushType::Style);
  }

  // WARNING:
  // WARNING: the above flush may have destroyed the pres shell and/or
  // WARNING: frames and other layout related objects.
  // WARNING:

  // STEP 5: Compose currently-animated attributes.
  // XXXdholbert: This step traverses our animation targets in an effectively
  // random order. For animation from/to 'inherit' values to work correctly
  // when the inherited value is *also* being animated, we really should be
  // traversing our animated nodes in an ancestors-first order (bug 501183)
  bool mightHavePendingStyleUpdates = false;
  for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) {
    iter.Get()->ComposeAttribute(mightHavePendingStyleUpdates);
  }

  // Update last compositor table
  mLastCompositorTable = std::move(currentCompositorTable);
  mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates;

  NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!");
}

void SMILAnimationController::RewindElements() {
  const bool rewindNeeded = std::any_of(
      mChildContainerTable.Keys().cbegin(), mChildContainerTable.Keys().cend(),
      [](SMILTimeContainer* container) { return container->NeedsRewind(); });

  if (!rewindNeeded) return;

  for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) {
    SMILTimeContainer* timeContainer = animElem->GetTimeContainer();
    if (timeContainer && timeContainer->NeedsRewind()) {
      animElem->TimedElement().Rewind();
    }
  }

  for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
    container->ClearNeedsRewind();
  }
}

void SMILAnimationController::DoMilestoneSamples() {
  // We need to sample the timing model but because SMIL operates independently
  // of the frame-rate, we can get one sample at t=0s and the next at t=10min.
  //
  // In between those two sample times a whole string of significant events
  // might be expected to take place: events firing, new interdependencies
  // between animations resolved and dissolved, etc.
  //
  // Furthermore, at any given time, we want to sample all the intervals that
  // end at that time BEFORE any that begin. This behaviour is implied by SMIL's
  // endpoint-exclusive timing model.
  //
  // So we have the animations (specifically the timed elements) register the
  // next significant moment (called a milestone) in their lifetime and then we
  // step through the model at each of these moments and sample those animations
  // registered for those times. This way events can fire in the correct order,
  // dependencies can be resolved etc.

  SMILTime sampleTime = INT64_MIN;

  while (true) {
    // We want to find any milestones AT OR BEFORE the current sample time so we
    // initialise the next milestone to the moment after (1ms after, to be
    // precise) the current sample time and see if there are any milestones
    // before that. Any other milestones will be dealt with in a subsequent
    // sample.
    SMILMilestone nextMilestone(GetCurrentTimeAsSMILTime() + 1, true);
    for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
      if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) {
        continue;
      }
      SMILMilestone thisMilestone;
      bool didGetMilestone =
          container->GetNextMilestoneInParentTime(thisMilestone);
      if (didGetMilestone && thisMilestone < nextMilestone) {
        nextMilestone = thisMilestone;
      }
    }

    if (nextMilestone.mTime > GetCurrentTimeAsSMILTime()) {
      break;
    }

    nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elements;
    for (SMILTimeContainer* container : mChildContainerTable.Keys()) {
      if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) {
        continue;
      }
      container->PopMilestoneElementsAtMilestone(nextMilestone, elements);
    }

    uint32_t length = elements.Length();

    // During the course of a sampling we don't want to actually go backwards.
    // Due to negative offsets, early ends and the like, a timed element might
    // register a milestone that is actually in the past. That's fine, but it's
    // still only going to get *sampled* with whatever time we're up to and no
    // earlier.
    //
    // Because we're only performing this clamping at the last moment, the
    // animations will still all get sampled in the correct order and
    // dependencies will be appropriately resolved.
    sampleTime = std::max(nextMilestone.mTime, sampleTime);

    for (uint32_t i = 0; i < length; ++i) {
      SVGAnimationElement* elem = elements[i].get();
      MOZ_ASSERT(elem, "nullptr animation element in list");
      SMILTimeContainer* container = elem->GetTimeContainer();
      if (!container)
        // The container may be nullptr if the element has been detached from
        // its parent since registering a milestone.
        continue;

      SMILTimeValue containerTimeValue =
          container->ParentToContainerTime(sampleTime);
      if (!containerTimeValue.IsDefinite()) continue;

      // Clamp the converted container time to non-negative values.
      SMILTime containerTime =
          std::max<SMILTime>(0, containerTimeValue.GetMillis());

      if (nextMilestone.mIsEnd) {
        elem->TimedElement().SampleEndAt(containerTime);
      } else {
        elem->TimedElement().SampleAt(containerTime);
      }
    }
  }
}

/*static*/
void SMILAnimationController::SampleTimedElement(
    SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) {
  SMILTimeContainer* timeContainer = aElement->GetTimeContainer();
  if (!timeContainer) return;

  // We'd like to call timeContainer->NeedsSample() here and skip all timed
  // elements that belong to paused time containers that don't need a sample,
  // but that doesn't work because we've already called Sample() on all the time
  // containers so the paused ones don't need a sample any more and they'll
  // return false.
  //
  // Instead we build up a hashmap of active time containers during the previous
  // step (SampleTimeContainer) and then test here if the container for this
  // timed element is in the list.
  if (!aActiveContainers->GetEntry(timeContainer)) return;

  SMILTime containerTime = timeContainer->GetCurrentTimeAsSMILTime();

  MOZ_ASSERT(!timeContainer->IsSeeking(),
             "Doing a regular sample but the time container is still seeking");
  aElement->TimedElement().SampleAt(containerTime);
}

/*static*/
void SMILAnimationController::AddAnimationToCompositorTable(
    SVGAnimationElement* aElement, SMILCompositorTable* aCompositorTable,
    bool& aStyleFlushNeeded) {
  // Add a compositor to the hash table if there's not already one there
  SMILTargetIdentifier key;
  if (!GetTargetIdentifierForAnimation(aElement, key))
    // Something's wrong/missing about animation's target; skip this animation
    return;

  SMILAnimationFunction& func = aElement->AnimationFunction();

  // Only add active animation functions. If there are no active animations
  // targeting an attribute, no compositor will be created and any previously
  // applied animations will be cleared.
  if (func.IsActiveOrFrozen()) {
    // Look up the compositor for our target, & add our animation function
    // to its list of animation functions.
    SMILCompositor* result = aCompositorTable->PutEntry(key);
    result->AddAnimationFunction(&func);

  } else if (func.HasChanged()) {
    // Look up the compositor for our target, and force it to skip the
    // "nothing's changed so don't bother compositing" optimization for this
    // sample. |func| is inactive, but it's probably *newly* inactive (since
    // it's got HasChanged() == true), so we need to make sure to recompose
    // its target.
    SMILCompositor* result = aCompositorTable->PutEntry(key);
    result->ToggleForceCompositing();

    // We've now made sure that |func|'s inactivity will be reflected as of
    // this sample. We need to clear its HasChanged() flag so that it won't
    // trigger this same clause in future samples (until it changes again).
    func.ClearHasChanged();
  }
  aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample();
}

static inline bool IsTransformAttribute(int32_t aNamespaceID,
                                        nsAtom* aAttributeName) {
  return aNamespaceID == kNameSpaceID_None &&
         (aAttributeName == nsGkAtoms::transform ||
          aAttributeName == nsGkAtoms::patternTransform ||
          aAttributeName == nsGkAtoms::gradientTransform);
}

// Helper function that, given a SVGAnimationElement, looks up its target
// element & target attribute and populates a SMILTargetIdentifier
// for this target.
/*static*/
bool SMILAnimationController::GetTargetIdentifierForAnimation(
    SVGAnimationElement* aAnimElem, SMILTargetIdentifier& aResult) {
  // Look up target (animated) element
  Element* targetElem = aAnimElem->GetTargetElementContent();
  if (!targetElem)
    // Animation has no target elem -- skip it.
    return false;

  // Look up target (animated) attribute
  // SMILANIM section 3.1, attributeName may
  // have an XMLNS prefix to indicate the XML namespace.
  RefPtr<nsAtom> attributeName;
  int32_t attributeNamespaceID;
  if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID,
                                         getter_AddRefs(attributeName)))
    // Animation has no target attr -- skip it.
    return false;

  // animateTransform can only animate transforms, conversely transforms
  // can only be animated by animateTransform
  if (IsTransformAttribute(attributeNamespaceID, attributeName) !=
      (aAnimElem->IsSVGElement(nsGkAtoms::animateTransform)))
    return false;

  // Construct the key
  aResult.mElement = targetElem;
  aResult.mAttributeName = attributeName;
  aResult.mAttributeNamespaceID = attributeNamespaceID;

  return true;
}

bool SMILAnimationController::PreTraverse() {
  return PreTraverseInSubtree(nullptr);
}

bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) {
  MOZ_ASSERT(NS_IsMainThread());

  if (!mMightHavePendingStyleUpdates) {
    return false;
  }

  nsPresContext* context = mDocument->GetPresContext();
  if (!context) {
    return false;
  }

  bool foundElementsNeedingRestyle = false;
  for (SVGAnimationElement* animElement : mAnimationElementTable.Keys()) {
    SMILTargetIdentifier key;
    if (!GetTargetIdentifierForAnimation(animElement, key)) {
      // Something's wrong/missing about animation's target; skip this animation
      continue;
    }

    // Ignore restyles that aren't in the flattened tree subtree rooted at
    // aRoot.
    if (aRoot && !nsContentUtils::ContentIsFlattenedTreeDescendantOf(
                     key.mElement, aRoot)) {
      continue;
    }

    context->RestyleManager()->PostRestyleEventForAnimations(
        key.mElement, PseudoStyleType::NotPseudo, RestyleHint::RESTYLE_SMIL);

    foundElementsNeedingRestyle = true;
  }

  // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted
  // all restyles.
  if (!aRoot) {
    mMightHavePendingStyleUpdates = false;
  }

  return foundElementsNeedingRestyle;
}

//----------------------------------------------------------------------
// Add/remove child time containers

nsresult SMILAnimationController::AddChild(SMILTimeContainer& aChild) {
  const bool wasEmpty = mChildContainerTable.IsEmpty();
  TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild);
  NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY);
  if (wasEmpty) {
    UpdateSampling();
  }
  return NS_OK;
}

void SMILAnimationController::RemoveChild(SMILTimeContainer& aChild) {
  mChildContainerTable.RemoveEntry(&aChild);
  if (mChildContainerTable.IsEmpty()) {
    UpdateSampling();
  }
}

// Helper method
nsRefreshDriver* SMILAnimationController::GetRefreshDriver() {
  if (!mDocument) {
    NS_ERROR("Requesting refresh driver after document has disconnected!");
    return nullptr;
  }

  nsPresContext* context = mDocument->GetPresContext();
  return context ? context->RefreshDriver() : nullptr;
}

void SMILAnimationController::FlagDocumentNeedsFlush() {
  if (PresShell* presShell = mDocument->GetPresShell()) {
    presShell->SetNeedStyleFlush();
  }
}

}  // namespace mozilla
