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

#include "AnimationUtils.h"
#include "mozAutoDocUpdate.h"
#include "mozilla/AnimationEventDispatcher.h"
#include "mozilla/AnimationTarget.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/CycleCollectedJSContext.h"
#include "mozilla/DeclarationBlock.h"
#include "mozilla/Likely.h"
#include "mozilla/Maybe.h"  // For Maybe
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/dom/AnimationBinding.h"
#include "mozilla/dom/CSSNumericValueBinding.h"
#include "mozilla/dom/CSSTransition.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/DocumentTimeline.h"
#include "mozilla/dom/MutationObservers.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/ScrollTimeline.h"  // For PROGRESS_TIMELINE_DURATION_MILLISEC
#include "nsAnimationManager.h"          // For CSSAnimation
#include "nsComputedDOMStyle.h"
#include "nsDOMCSSAttrDeclaration.h"  // For nsDOMCSSAttributeDeclaration
#include "nsDOMMutationObserver.h"    // For nsAutoAnimationMutationBatch
#include "nsIFrame.h"
#include "nsThreadUtils.h"  // For nsRunnableMethod and nsRevocableEventPtr
#include "nsTransitionManager.h"  // For CSSTransition

namespace mozilla::dom {

// Static members
uint64_t Animation::sNextAnimationIndex = 0;

NS_IMPL_CYCLE_COLLECTION_INHERITED(Animation, DOMEventTargetHelper, mTimeline,
                                   mEffect, mReady, mFinished)

NS_IMPL_ADDREF_INHERITED(Animation, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(Animation, DOMEventTargetHelper)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Animation)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

JSObject* Animation::WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) {
  return dom::Animation_Binding::Wrap(aCx, this, aGivenProto);
}

// ---------------------------------------------------------------------------
//
// Utility methods
//
// ---------------------------------------------------------------------------

namespace {
// A wrapper around nsAutoAnimationMutationBatch that looks up the
// appropriate document from the supplied animation.
class MOZ_RAII AutoMutationBatchForAnimation {
 public:
  explicit AutoMutationBatchForAnimation(const Animation& aAnimation) {
    NonOwningAnimationTarget target = aAnimation.GetTargetForAnimation();
    if (!target) {
      return;
    }

    // For mutation observers, we use the OwnerDoc.
    mAutoBatch.emplace(target.mElement->OwnerDoc());
  }

 private:
  Maybe<nsAutoAnimationMutationBatch> mAutoBatch;
};
}  // namespace

// ---------------------------------------------------------------------------
//
// Animation interface:
//
// ---------------------------------------------------------------------------

Animation::Animation(nsIGlobalObject* aGlobal)
    : DOMEventTargetHelper(aGlobal),
      mAnimationIndex(sNextAnimationIndex++),
      mRTPCallerType(aGlobal->GetRTPCallerType()) {}

Animation::~Animation() = default;

/* static */
already_AddRefed<Animation> Animation::ClonePausedAnimation(
    nsIGlobalObject* aGlobal, const Animation& aOther, AnimationEffect& aEffect,
    AnimationTimeline& aTimeline) {
  // FIXME: Bug 1805950: Support printing for scroll-timeline once we resolve
  // the spec issue.
  if (aOther.UsingScrollTimeline()) {
    return nullptr;
  }

  RefPtr<Animation> animation = new Animation(aGlobal);

  // Setup the timeline. We always use document-timeline of the new document,
  // even if the timeline of |aOther| is null.
  animation->mTimeline = &aTimeline;

  // Setup the playback rate.
  animation->mPlaybackRate = aOther.mPlaybackRate;

  // Setup the timing.
  const Nullable<TimeDuration> currentTime = aOther.GetCurrentTimeAsDuration();
  if (!aOther.GetTimeline()) {
    // This simulates what we do in SetTimelineNoUpdate(). It's possible to
    // preserve the progress if the previous timeline is a scroll-timeline.
    // So for null timeline, it may have a progress and the non-null current
    // time.
    if (!currentTime.IsNull()) {
      animation->SilentlySetCurrentTime(currentTime.Value());
    }
    animation->mPreviousCurrentTime = animation->GetCurrentTimeAsDuration();
  } else {
    animation->mHoldTime = currentTime;
    if (!currentTime.IsNull()) {
      // FIXME: Should we use |timelineTime| as previous current time here? It
      // seems we should use animation->GetCurrentTimeAsDuration(), per
      // UpdateFinishedState().
      const Nullable<TimeDuration> timelineTime =
          aTimeline.GetCurrentTimeAsDuration();
      MOZ_ASSERT(!timelineTime.IsNull(), "Timeline not yet set");
      animation->mPreviousCurrentTime = timelineTime;
    }
  }

  // Setup the effect's link to this.
  animation->mEffect = &aEffect;
  animation->mEffect->SetAnimation(animation);

  animation->mPendingState = PendingState::PausePending;

  // We expect our relevance to be the same as the orginal.
  animation->mIsRelevant = aOther.mIsRelevant;

  animation->PostUpdate();
  animation->mTimeline->NotifyAnimationUpdated(*animation);
  return animation.forget();
}

NonOwningAnimationTarget Animation::GetTargetForAnimation() const {
  AnimationEffect* effect = GetEffect();
  NonOwningAnimationTarget target;
  if (!effect || !effect->AsKeyframeEffect()) {
    return target;
  }
  return effect->AsKeyframeEffect()->GetAnimationTarget();
}

/* static */
already_AddRefed<Animation> Animation::Constructor(
    const GlobalObject& aGlobal, AnimationEffect* aEffect,
    const Optional<AnimationTimeline*>& aTimeline, ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());

  AnimationTimeline* timeline;
  Document* document =
      AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());

  if (aTimeline.WasPassed()) {
    timeline = aTimeline.Value();
  } else {
    if (!document) {
      aRv.Throw(NS_ERROR_FAILURE);
      return nullptr;
    }
    timeline = document->Timeline();
  }

  RefPtr<Animation> animation = new Animation(global);
  // JS side can't refer to timeline by name.
  animation->SetTimelineNoUpdate(timeline, nullptr, FromJS::Yes);
  animation->SetEffectNoUpdate(aEffect);

  return animation.forget();
}

void Animation::SetId(const nsAString& aId) {
  if (mId == aId) {
    return;
  }
  mId = aId;
  MutationObservers::NotifyAnimationChanged(this);
}

void Animation::SetEffect(AnimationEffect* aEffect) {
  SetEffectNoUpdate(aEffect);
  PostUpdate();
}

// https://drafts.csswg.org/web-animations/#setting-the-target-effect
void Animation::SetEffectNoUpdate(AnimationEffect* aEffect) {
  RefPtr<Animation> kungFuDeathGrip(this);

  if (mEffect == aEffect) {
    return;
  }

  AutoMutationBatchForAnimation mb(*this);
  bool wasRelevant = mIsRelevant;

  if (mEffect) {
    // We need to notify observers now because once we set mEffect to null
    // we won't be able to find the target element to notify.
    if (mIsRelevant) {
      MutationObservers::NotifyAnimationRemoved(this);
    }

    // Break links with the old effect and then drop it.
    RefPtr<AnimationEffect> oldEffect = mEffect;
    mEffect = nullptr;
    if (IsPartialPrerendered()) {
      if (KeyframeEffect* oldKeyframeEffect = oldEffect->AsKeyframeEffect()) {
        oldKeyframeEffect->ResetPartialPrerendered();
      }
    }
    oldEffect->SetAnimation(nullptr);

    // The following will not do any notification because mEffect is null.
    UpdateRelevance();
  }

  if (aEffect) {
    // Break links from the new effect to its previous animation, if any.
    RefPtr<AnimationEffect> newEffect = aEffect;
    Animation* prevAnim = aEffect->GetAnimation();
    if (prevAnim) {
      prevAnim->SetEffect(nullptr);
    }

    // Create links with the new effect. SetAnimation(this) will also update
    // mIsRelevant of this animation, and then notify mutation observer if
    // needed by calling Animation::UpdateRelevance(), so we don't need to
    // call it again.
    mEffect = newEffect;
    mEffect->SetAnimation(this);

    // Notify possible add or change.
    // If the target is different, the change notification will be ignored by
    // AutoMutationBatchForAnimation.
    if (wasRelevant && mIsRelevant) {
      MutationObservers::NotifyAnimationChanged(this);
    }
  }

  MaybeScheduleReplacementCheck();

  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
}

static TimeStamp EnsurePaintIsScheduled(Document& aDoc) {
  PresShell* presShell = aDoc.GetPresShell();
  if (!presShell) {
    return {};
  }
  nsIFrame* rootFrame = presShell->GetRootFrame();
  if (!rootFrame) {
    return {};
  }
  rootFrame->SchedulePaintWithoutInvalidatingObservers();
  auto* rd = rootFrame->PresContext()->RefreshDriver();
  if (!rd->IsInRefresh()) {
    return {};
  }
  return rd->MostRecentRefresh();
}

void Animation::RemovedNamedTimelineReferenceFromJS(const nsAtom* aName) {
  if (!AsCSSAnimation()) {
    MOZ_ASSERT_UNREACHABLE("How?");
    return;
  }
  auto* animationManager = [&]() -> nsAnimationManager* {
    auto* doc = GetRenderedDocument();
    if (!doc) {
      return nullptr;
    }
    auto* presContext = doc->GetPresContext();
    if (!presContext) {
      return nullptr;
    }
    return presContext->AnimationManager();
  }();
  if (!animationManager) {
    return;
  }
  animationManager->RemoveNamedTimelineAnimation(aName, AsCSSAnimation());
}

void Animation::SetTimelineFromJS(AnimationTimeline* aTimeline) {
  TimelineWillSetFromJS();
  // Can't refer to timeline by name from JS side.
  const auto prevTimelineName = GetTimelineName();
  SetTimeline(aTimeline, nullptr, FromJS::Yes);
  if (prevTimelineName) {
    RemovedNamedTimelineReferenceFromJS(prevTimelineName);
  }
}

bool Animation::SetTimeline(AnimationTimeline* aTimeline,
                            const nsAtom* aTimelineName, FromJS aFromJS) {
  const auto updated = SetTimelineNoUpdate(aTimeline, aTimelineName, aFromJS);
  PostUpdate();
  return updated;
}

// https://drafts.csswg.org/web-animations-2/#setting-the-timeline
bool Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline,
                                    const nsAtom* aTimelineName,
                                    FromJS aFromJS) {
  if (aFromJS == FromJS::No && TimelineOverridenByJS()) {
    return false;
  }
  // 1. Let old timeline be the current timeline of animation, if any.
  // 2. If new timeline is the same object as old timeline, abort this
  // procedure.
  if (mTimeline == aTimeline) {
    // nullptr -> nullptr but going from/to named timeline is significant.
    if (mTimelineName != aTimelineName) {
      mTimelineName = aTimelineName;
    }
    // Timeline still didn't update, so...
    return false;
  }

  // 3. Let previous play state be animation’s play state.
  const AnimationPlayState previousPlayState = PlayState();
  // 4. Let previous current time be the animation’s current time.
  const Nullable<TimeDuration> previousCurrentTime = GetCurrentTimeAsDuration();

  // 5. Set previous progress based in the first condition that applies:
  // If previous current time is unresolved:
  //   => Set previous progress to unresolved.
  // If end time is zero:
  //   => Set previous progress to zero.
  // Otherwise
  //   => Set previous progress = previous current time / end time
  Nullable<double> previousProgress;
  if (!previousCurrentTime.IsNull()) {
    const TimeDuration endTime = TimeDuration(EffectEnd());
    previousProgress.SetValue(endTime.IsZero()
                                  ? 0.0
                                  : previousCurrentTime.Value().ToSeconds() /
                                        endTime.ToSeconds());
  }

  // We compute the active time for the old timeline because we will use it to
  // queue the cancel event later.
  StickyTimeDuration activeTime =
      mEffect ? mEffect->GetComputedTiming().mActiveTime : StickyTimeDuration();

  // 6. Let from finite timeline be true if old timeline is not null and not
  // monotonically increasing.
  const bool fromFiniteTimeline =
      mTimeline && !mTimeline->IsMonotonicallyIncreasing();
  // 7. Let to finite timeline be true if timeline is not null and not
  // monotonically increasing.
  const bool toFiniteTimeline =
      aTimeline && !aTimeline->IsMonotonicallyIncreasing();

  // 8. Let the timeline of animation be new timeline.
  RefPtr<AnimationTimeline> oldTimeline = mTimeline;
  if (oldTimeline) {
    oldTimeline->RemoveAnimation(this);
  }
  mTimeline = aTimeline;
  mTimelineName = aTimelineName;
  // Update the normalized timing and keyframe timeline range ofset because we
  // are using the new timeline.
  if (mEffect) {
    mEffect->UpdateNormalizedTiming();
    MaybeUpdateKeyframeComputedOffsets();
  }

  // 9. Perform the steps corresponding to the first matching condition from the
  // following, if any:
  if (toFiniteTimeline) {
    // If to finite timeline,
    // Apply any pending playback rate on animation
    ApplyPendingPlaybackRate();
    // Set auto align start time to true.
    mAutoAlignStartTime = true;
    // Set start time to unresolved.
    mStartTime.SetNull();
    // Set hold time to unresolved.
    mHoldTime.SetNull();

    switch (previousPlayState) {
      case AnimationPlayState::Running:
      case AnimationPlayState::Finished:
        // If previous play state is "finished" or "running"
        // Schedule a pending play task.
        //
        // FIXME: It seems we may have to do more things than just play the
        // pending play task. In Chromium, it just calls Play(). However, in
        // WebKit, It re-creates the ready promise and play a pending play task.
        // We also call PlayNoUpdate() for now. However, this may be an
        // overshoot but we won't miss anything. If the spec issue gets
        // updated, we could refine this to just do the necessary things.
        // https://github.com/w3c/csswg-drafts/issues/11465
        PlayNoUpdate(IgnoredErrorResult(), LimitBehavior::AutoRewind);
        break;
      case AnimationPlayState::Paused:
        // If previous play state is "paused" and previous progress is resolved:
        if (!previousCurrentTime.IsNull()) {
          // Set hold time to previous progress * end time.
          mHoldTime.SetValue(
              TimeDuration(EffectEnd().MultDouble(previousProgress.Value())));
        }
        break;
      case AnimationPlayState::Idle:
        break;
    }
  } else if (fromFiniteTimeline) {
    // mAutoAlignStartTime is only meaningful for finite timelines; clear it
    // here. Transitioning into a new finite timeline is handled by the
    // toFiniteTimeline branch above. This clearing is a deviation from spec
    // [1], which only acts when previousProgress is resolved; without it the
    // flag's invariant (true only while the timeline is finite) is violated
    // and AutoAlignStartTime would later fire on a monotonic timeline.
    // [1] https://drafts.csswg.org/web-animations-2/#setting-the-timeline
    mAutoAlignStartTime = false;
    if (!previousProgress.IsNull()) {
      // If from finite timeline and previous progress is resolved, run the
      // procedure to set the current time to previous progress * end time.
      SetCurrentTimeNoUpdate(
          TimeDuration(EffectEnd().MultDouble(previousProgress.Value())));
    }
  }
  // 10. If the start time of animation is resolved, make animation’s hold time
  // unresolved.
  if (!mStartTime.IsNull()) {
    mHoldTime.SetNull();
  }

  if (!aTimeline) {
    MaybeQueueCancelEvent(activeTime);
  }

  // 11. Run the procedure to update an animation’s finished state for animation
  // with the did seek flag set to false, and the synchronously notify flag set
  // to false.
  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);

  // FIXME: Bug 1799071: Check if we need to add
  // MutationObservers::NotifyAnimationChanged(this) here.
  return true;
}

void Animation::SetTimelineRange(AnimationRange&& aRange) {
  SetTimelineRangeNoUpdate(std::move(aRange));
  PostUpdate();
}

void Animation::SetTimelineRangeNoUpdate(AnimationRange&& aRange) {
  if (mTimelineRange == aRange) {
    return;
  }

  // TODO: Bug 2006262. We may have to rewrite this when adding the attribute:
  // https://drafts.csswg.org/web-animations-2/#dom-animation-rangestart
  // https://drafts.csswg.org/web-animations-2/#dom-animation-rangeend
  //
  // For now, this is not exposed and is set during initialization of the CSS
  // Animations.
  mTimelineRange = std::move(aRange);

  if (mEffect) {
    mEffect->UpdateNormalizedTiming();
    MaybeUpdateKeyframeComputedOffsets();
  }
}

// https://drafts.csswg.org/web-animations/#set-the-animation-start-time
void Animation::SetStartTime(const Nullable<TimeDuration>& aNewStartTime) {
  // Return early if the start time will not change. However, if we
  // are pending, then setting the start time to any value
  // including the current value has the effect of aborting
  // pending tasks so we should not return early in that case.
  if (!Pending() && aNewStartTime == mStartTime) {
    return;
  }

  AutoMutationBatchForAnimation mb(*this);

  Nullable<TimeDuration> timelineTime;
  if (mTimeline) {
    // The spec says to check if the timeline is active (has a resolved time)
    // before using it here, but we don't need to since it's harmless to set
    // the already null time to null.
    timelineTime = mTimeline->GetCurrentTimeAsDuration();
  }
  if (timelineTime.IsNull() && !aNewStartTime.IsNull()) {
    mHoldTime.SetNull();
  }

  Nullable<TimeDuration> previousCurrentTime = GetCurrentTimeAsDuration();

  ApplyPendingPlaybackRate();
  mStartTime = aNewStartTime;

  mAutoAlignStartTime = false;

  if (!aNewStartTime.IsNull()) {
    if (PlaybackRateInternal() != 0.0) {
      mHoldTime.SetNull();
    }
  } else {
    mHoldTime = previousCurrentTime;
  }

  CancelPendingTasks();
  // We may have already resolved mReady, but in that case calling
  // MaybeResolve is a no-op, so that's okay.
  MaybeResolvePromiseWithThis(mReady);

  UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
  if (IsRelevant()) {
    MutationObservers::NotifyAnimationChanged(this);
  }
  PostUpdate();
}

// https://drafts.csswg.org/web-animations/#current-time
Nullable<TimeDuration> Animation::GetCurrentTimeForHoldTime(
    const Nullable<TimeDuration>& aHoldTime) const {
  Nullable<TimeDuration> result;
  if (!aHoldTime.IsNull()) {
    result = aHoldTime;
    return result;
  }

  if (mTimeline && !mStartTime.IsNull()) {
    Nullable<TimeDuration> timelineTime = mTimeline->GetCurrentTimeAsDuration();
    if (!timelineTime.IsNull()) {
      result = CurrentTimeFromTimelineTime(
          timelineTime.Value(), mStartTime.Value(), PlaybackRateInternal());
    }
  }
  return result;
}

// https://drafts.csswg.org/web-animations/#set-the-current-time
void Animation::SetCurrentTime(const TimeDuration& aSeekTime) {
  // Return early if the current time has not changed. However, if we
  // are pause-pending, then setting the current time to any value
  // including the current value has the effect of aborting the
  // pause so we should not return early in that case.
  if (mPendingState != PendingState::PausePending &&
      Nullable<TimeDuration>(aSeekTime) == GetCurrentTimeAsDuration()) {
    return;
  }

  AutoMutationBatchForAnimation mb(*this);

  SetCurrentTimeNoUpdate(aSeekTime);

  if (IsRelevant()) {
    MutationObservers::NotifyAnimationChanged(this);
  }
  PostUpdate();
}

void Animation::SetCurrentTimeNoUpdate(const TimeDuration& aSeekTime) {
  SilentlySetCurrentTime(aSeekTime);

  if (mPendingState == PendingState::PausePending) {
    // Finish the pause operation
    mHoldTime.SetValue(aSeekTime);

    ApplyPendingPlaybackRate();
    mStartTime.SetNull();

    MaybeResolvePromiseWithThis(mReady);
    CancelPendingTasks();
  }

  UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
}

// https://drafts.csswg.org/web-animations-2/#the-overall-progress-of-an-animation
Nullable<double> Animation::GetOverallProgress() const {
  Nullable<double> result;
  if (!mEffect) {
    return result;
  }
  const Nullable<TimeDuration> currentTime = GetCurrentTimeAsDuration();
  if (currentTime.IsNull()) {
    return result;
  }

  const StickyTimeDuration endTime = EffectEnd();
  if (endTime.IsZero()) {
    if (currentTime.Value() < TimeDuration()) {
      result.SetValue(0.0);
    } else {
      result.SetValue(1.0);
    }
    return result;
  }

  if (endTime == StickyTimeDuration::Forever()) {
    result.SetValue(0.0);
    return result;
  }

  auto overallProgress =
      std::min(std::max(currentTime.Value() / endTime, 0.0), 1.0);
  result.SetValue(overallProgress);
  return result;
}

// https://drafts.csswg.org/web-animations/#set-the-playback-rate
void Animation::SetPlaybackRate(double aPlaybackRate) {
  mPendingPlaybackRate.reset();

  if (aPlaybackRate == mPlaybackRate) {
    return;
  }

  AutoMutationBatchForAnimation mb(*this);

  Nullable<TimeDuration> previousTime = GetCurrentTimeAsDuration();
  mPlaybackRate = aPlaybackRate;
  if (!HasFiniteTimeline() && !previousTime.IsNull()) {
    SetCurrentTime(previousTime.Value());
  }

  // In the case where GetCurrentTimeAsDuration() returns the same result before
  // and after updating mPlaybackRate, SetCurrentTime will return early since,
  // as far as it can tell, nothing has changed.
  // As a result, we need to perform the following updates here:
  // - update timing (since, if the sign of the playback rate has changed, our
  //   finished state may have changed),
  // - dispatch a change notification for the changed playback rate, and
  // - update the playback rate on animations on layers.
  UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Async);
  if (IsRelevant()) {
    MutationObservers::NotifyAnimationChanged(this);
  }
  PostUpdate();
}

// https://drafts.csswg.org/web-animations/#seamlessly-update-the-playback-rate
void Animation::UpdatePlaybackRate(double aPlaybackRate) {
  if (mPendingPlaybackRate && mPendingPlaybackRate.value() == aPlaybackRate) {
    return;
  }

  // Calculate the play state using the existing playback rate since below we
  // want to know if the animation is _currently_ finished or not, not whether
  // it _will_ be finished.
  AnimationPlayState playState = PlayState();

  mPendingPlaybackRate = Some(aPlaybackRate);

  if (Pending()) {
    // If we already have a pending task, there is nothing more to do since the
    // playback rate will be applied then.
    //
    // However, as with the idle/paused case below, we still need to update the
    // relevance (and effect set to make sure it only contains relevant
    // animations) since the relevance is based on the Animation play state
    // which incorporates the _pending_ playback rate.
    UpdateEffect(PostRestyleMode::Never);
    return;
  }

  AutoMutationBatchForAnimation mb(*this);

  if (playState == AnimationPlayState::Idle ||
      playState == AnimationPlayState::Paused ||
      GetCurrentTimeAsDuration().IsNull()) {
    // If |previous play state| is idle or paused, or the current time is
    // unresolved, we apply any pending playback rate on animation immediately.
    ApplyPendingPlaybackRate();

    // We don't need to update timing or post an update here because:
    //
    // * the current time hasn't changed -- it's either unresolved or fixed
    //   with a hold time -- so the output won't have changed
    // * the finished state won't have changed even if the sign of the
    //   playback rate changed since we're not finished (we're paused or idle)
    // * the playback rate on layers doesn't need to be updated since we're not
    //   moving. Once we get a start time etc. we'll update the playback rate
    //   then.
    //
    // However we still need to update the relevance and effect set as well as
    // notifying observers.
    UpdateEffect(PostRestyleMode::Never);
    if (IsRelevant()) {
      MutationObservers::NotifyAnimationChanged(this);
    }
  } else if (playState == AnimationPlayState::Finished) {
    MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTimeAsDuration().IsNull(),
               "If we have no active timeline, we should be idle or paused");
    if (aPlaybackRate != 0) {
      // The unconstrained current time can only be unresolved if either we
      // don't have an active timeline (and we already asserted that is not
      // true) or we have an unresolved start time (in which case we should be
      // paused).
      MOZ_ASSERT(!GetUnconstrainedCurrentTime().IsNull(),
                 "Unconstrained current time should be resolved");
      TimeDuration unconstrainedCurrentTime =
          GetUnconstrainedCurrentTime().Value();
      TimeDuration timelineTime = mTimeline->GetCurrentTimeAsDuration().Value();
      mStartTime = StartTimeFromTimelineTime(
          timelineTime, unconstrainedCurrentTime, aPlaybackRate);
    } else {
      mStartTime = mTimeline->GetCurrentTimeAsDuration();
    }

    ApplyPendingPlaybackRate();

    // Even though we preserve the current time, we might now leave the finished
    // state (e.g. if the playback rate changes sign) so we need to update
    // timing.
    UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
    if (IsRelevant()) {
      MutationObservers::NotifyAnimationChanged(this);
    }
    PostUpdate();
  } else {
    ErrorResult rv;
    Play(rv, LimitBehavior::Continue);
    MOZ_ASSERT(!rv.Failed(),
               "We should only fail to play when using auto-rewind behavior");
  }
}

// https://drafts.csswg.org/web-animations/#play-state
AnimationPlayState Animation::PlayState() const {
  Nullable<TimeDuration> currentTime = GetCurrentTimeAsDuration();
  if (currentTime.IsNull() && mStartTime.IsNull() && !Pending()) {
    return AnimationPlayState::Idle;
  }

  if (mPendingState == PendingState::PausePending ||
      (mStartTime.IsNull() && !Pending())) {
    return AnimationPlayState::Paused;
  }

  double playbackRate = CurrentOrPendingPlaybackRate();
  if (!currentTime.IsNull() &&
      ((playbackRate > 0.0 && currentTime.Value() >= EffectEnd()) ||
       (playbackRate < 0.0 && currentTime.Value() <= TimeDuration()))) {
    return AnimationPlayState::Finished;
  }

  return AnimationPlayState::Running;
}

Promise* Animation::GetReady(ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = GetRelevantGlobal();
  if (!mReady && global) {
    mReady = Promise::Create(global, aRv);  // Lazily create on demand
  }
  if (!mReady) {
    aRv.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }
  if (!Pending()) {
    MaybeResolvePromiseWithThis(mReady);
  }
  return mReady;
}

void Animation::MaybeResolvePromiseWithThis(Promise* aPromise) {
  if (!aPromise) {
    return;
  }
  if (!nsContentUtils::IsSafeToRunScript()) [[unlikely]] {
    nsContentUtils::AddScriptRunner(NewRunnableMethod<RefPtr<Promise>>(
        "MaybeResolvePromiseWithThis", this,
        &Animation::MaybeResolvePromiseWithThis, aPromise));
    return;
  }
  RefPtr promise = aPromise;
  promise->MaybeResolve(this);
}

Promise* Animation::GetFinished(ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = GetRelevantGlobal();
  if (!mFinished && global) {
    mFinished = Promise::Create(global, aRv);  // Lazily create on demand
  }
  if (!mFinished) {
    aRv.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }
  if (mFinishedIsResolved) {
    MaybeResolveFinishedPromise();
  }
  return mFinished;
}

// https://drafts.csswg.org/web-animations/#cancel-an-animation
void Animation::Cancel(PostRestyleMode aPostRestyle) {
  bool newlyIdle = false;

  if (PlayState() != AnimationPlayState::Idle) {
    newlyIdle = true;

    ResetPendingTasks();

    if (mFinished) {
      mFinished->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
      // mFinished can already be resolved.
      MOZ_ALWAYS_TRUE(mFinished->SetAnyPromiseIsHandled());
    }
    ResetFinishedPromise();

    QueuePlaybackEvent(nsGkAtoms::oncancel,
                       GetTimelineCurrentTimeAsTimeStamp());
  }

  StickyTimeDuration activeTime =
      mEffect ? mEffect->GetComputedTiming().mActiveTime : StickyTimeDuration();

  mHoldTime.SetNull();
  mStartTime.SetNull();

  // Allow our effect to remove itself from the its target element's EffectSet.
  UpdateEffect(aPostRestyle);

  if (mTimeline) {
    mTimeline->RemoveAnimation(this);
  }
  MaybeQueueCancelEvent(activeTime);

  if (newlyIdle && aPostRestyle == PostRestyleMode::IfNeeded) {
    PostUpdate();
  }
}

// https://drafts.csswg.org/web-animations/#finish-an-animation
void Animation::Finish(ErrorResult& aRv) {
  double effectivePlaybackRate = CurrentOrPendingPlaybackRate();

  if (effectivePlaybackRate == 0) {
    return aRv.ThrowInvalidStateError(
        "Can't finish animation with zero playback rate");
  }
  if (effectivePlaybackRate > 0 && EffectEnd() == TimeDuration::Forever()) {
    return aRv.ThrowInvalidStateError("Can't finish infinite animation");
  }

  AutoMutationBatchForAnimation mb(*this);

  ApplyPendingPlaybackRate();

  // Seek to the end
  TimeDuration limit =
      PlaybackRateInternal() > 0 ? TimeDuration(EffectEnd()) : TimeDuration();
  bool didChange = GetCurrentTimeAsDuration() != Nullable<TimeDuration>(limit);
  SilentlySetCurrentTime(limit);

  // If we are paused or play-pending we need to fill in the start time in
  // order to transition to the finished state.
  //
  // We only do this, however, if we have an active timeline. If we have an
  // inactive timeline we can't transition into the finished state just like
  // we can't transition to the running state (this finished state is really
  // a substate of the running state).
  if (mStartTime.IsNull() && mTimeline &&
      !mTimeline->GetCurrentTimeAsDuration().IsNull()) {
    mStartTime =
        StartTimeFromTimelineTime(mTimeline->GetCurrentTimeAsDuration().Value(),
                                  limit, PlaybackRateInternal());
    didChange = true;
  }

  // If we just resolved the start time for a pause or play-pending
  // animation, we need to clear the task. We don't do this as a branch of
  // the above however since we can have a play-pending animation with a
  // resolved start time if we aborted a pause operation.
  if (!mStartTime.IsNull() && (mPendingState == PendingState::PlayPending ||
                               mPendingState == PendingState::PausePending)) {
    if (mPendingState == PendingState::PausePending) {
      mHoldTime.SetNull();
    }
    CancelPendingTasks();
    didChange = true;
    MaybeResolvePromiseWithThis(mReady);
  }
  UpdateTiming(SeekFlag::DidSeek, SyncNotifyFlag::Sync);
  if (didChange && IsRelevant()) {
    MutationObservers::NotifyAnimationChanged(this);
  }
  PostUpdate();
}

void Animation::Play(ErrorResult& aRv, LimitBehavior aLimitBehavior) {
  PlayNoUpdate(aRv, aLimitBehavior);
  PostUpdate();
}

// https://drafts.csswg.org/web-animations/#reverse-an-animation
void Animation::Reverse(ErrorResult& aRv) {
  if (!mTimeline) {
    return aRv.ThrowInvalidStateError(
        "Can't reverse an animation with no associated timeline");
  }
  if (mTimeline->GetCurrentTimeAsDuration().IsNull()) {
    return aRv.ThrowInvalidStateError(
        "Can't reverse an animation associated with an inactive timeline");
  }

  double effectivePlaybackRate = mPendingPlaybackRate.valueOr(mPlaybackRate);

  if (effectivePlaybackRate == 0.0) {
    return;
  }

  Maybe<double> originalPendingPlaybackRate = mPendingPlaybackRate;

  mPendingPlaybackRate = Some(-effectivePlaybackRate);

  Play(aRv, LimitBehavior::AutoRewind);

  // If Play() threw, restore state and don't report anything to mutation
  // observers.
  if (aRv.Failed()) {
    mPendingPlaybackRate = std::move(originalPendingPlaybackRate);
  }

  // Play(), above, unconditionally calls PostUpdate so we don't need to do
  // it here.
}

void Animation::Persist() {
  if (mReplaceState == AnimationReplaceState::Persisted) {
    return;
  }

  bool wasRemoved = mReplaceState == AnimationReplaceState::Removed;

  mReplaceState = AnimationReplaceState::Persisted;

  // If the animation is not (yet) removed, there should be no side effects of
  // persisting it.
  if (wasRemoved) {
    UpdateEffect(PostRestyleMode::IfNeeded);
    PostUpdate();
  }
}

DocGroup* Animation::GetDocGroup() {
  Document* doc = GetRenderedDocument();
  return doc ? doc->GetDocGroup() : nullptr;
}

// https://drafts.csswg.org/web-animations/#dom-animation-commitstyles
void Animation::CommitStyles(ErrorResult& aRv) {
  if (!mEffect) {
    return;
  }

  // Take an owning reference to the keyframe effect. This will ensure that
  // this Animation and the target element remain alive after flushing style.
  RefPtr<KeyframeEffect> keyframeEffect = mEffect->AsKeyframeEffect();
  if (!keyframeEffect) {
    return;
  }

  NonOwningAnimationTarget target = keyframeEffect->GetAnimationTarget();
  if (!target) {
    return;
  }

  if (!target.mPseudoRequest.IsNotPseudo()) {
    return aRv.ThrowNoModificationAllowedError(
        "Can't commit styles of a pseudo-element");
  }

  // Check it is an element with a style attribute
  RefPtr<nsStyledElement> styledElement =
      nsStyledElement::FromNodeOrNull(target.mElement);
  if (!styledElement) {
    return aRv.ThrowNoModificationAllowedError(
        "Target is not capable of having a style attribute");
  }

  // Hold onto a strong reference to the doc in case the flush destroys it.
  RefPtr<Document> doc = target.mElement->GetComposedDoc();

  // Flush frames before checking if the target element is rendered since the
  // result could depend on pending style changes, and IsRendered() looks at the
  // primary frame.
  if (doc) {
    doc->FlushPendingNotifications(FlushType::Frames);
  }
  if (!target.mElement->IsRendered()) {
    return aRv.ThrowInvalidStateError("Target is not rendered");
  }
  nsPresContext* presContext =
      nsContentUtils::GetContextForContent(target.mElement);
  if (!presContext) {
    return aRv.ThrowInvalidStateError("Target is not rendered");
  }

  // Get the computed animation values
  UniquePtr<StyleAnimationValueMap> animationValues(
      Servo_AnimationValueMap_Create());
  if (!presContext->EffectCompositor()->ComposeServoAnimationRuleForEffect(
          *keyframeEffect, CascadeLevel(), animationValues.get(),
          StaticPrefs::dom_animations_commit_styles_endpoint_inclusive()
              ? EndpointBehavior::Inclusive
              : EndpointBehavior::Exclusive)) {
    NS_WARNING("Failed to compose animation style to commit");
    return;
  }

  // Calling SetCSSDeclaration will trigger attribute setting code.
  // Start the update now so that the old rule doesn't get used
  // between when we mutate the declaration and when we set the new
  // rule.
  mozAutoDocUpdate autoUpdate(target.mElement->OwnerDoc(), true);

  // Get the inline style to append to
  RefPtr<StyleLockedDeclarationBlock> declarationBlock;
  if (auto* existing = target.mElement->GetInlineStyleDeclaration()) {
    declarationBlock = nsDOMCSSDeclaration::EnsureBlockMutable(existing);
  } else {
    declarationBlock = Servo_DeclarationBlock_CreateEmpty().Consume();
  }

  // Prepare the callback
  MutationClosureData closureData;
  closureData.mShouldBeCalled = true;
  closureData.mElement = target.mElement;
  DeclarationBlockMutationClosure beforeChangeClosure = {
      nsDOMCSSAttributeDeclaration::MutationClosureFunction,
      &closureData,
  };

  // Set the animated styles
  bool changed = false;
  const AnimatedPropertyIDSet& properties = keyframeEffect->GetPropertySet();
  for (const CSSPropertyId& property : properties) {
    RefPtr<StyleAnimationValue> computedValue =
        Servo_AnimationValueMap_GetValue(animationValues.get(), &property)
            .Consume();
    if (computedValue) {
      changed |= Servo_DeclarationBlock_SetPropertyToAnimationValue(
          declarationBlock.get(), computedValue, beforeChangeClosure);
    }
  }

  if (!changed) {
    MOZ_ASSERT(!closureData.mWasCalled);
    return;
  }

  MOZ_ASSERT(closureData.mWasCalled);
  // Update inline style declaration
  target.mElement->SetInlineStyleDeclaration(*declarationBlock, closureData);
}

// ---------------------------------------------------------------------------
//
// JS wrappers for Animation interface:
//
// ---------------------------------------------------------------------------

void Animation::GetStartTime(Nullable<OwningCSSNumberish>& aRetVal) const {
  AnimationUtils::DurationToCSSNumberish(
      GetStartTime(), AcceptsPercentageBasedTime(), mRTPCallerType,
      GetParentObject(), aRetVal);
}

void Animation::SetStartTime(const Nullable<CSSNumberish>& aStartTime,
                             ErrorResult& aRv) {
  if (aStartTime.IsNull()) {
    SetStartTime(Nullable<TimeDuration>());
    return;
  }

  const bool progressBased = AcceptsPercentageBasedTime();

  // Step 1: Run the validate a CSSNumberish time procedure; abort on failure.
  if (!AnimationUtils::ValidateCSSNumberishTime(aStartTime.Value(),
                                                progressBased, aRv)) {
    return;
  }

  Nullable<TimeDuration> time =
      AnimationUtils::CSSNumberishToDuration(aStartTime.Value(), progressBased);
  MOZ_ASSERT(!time.IsNull());
  SetStartTime(time);
}

bool Animation::AcceptsPercentageBasedTime() const {
  return StaticPrefs::layout_css_typed_om_enabled() && HasFiniteTimeline();
}

void Animation::GetCurrentTime(Nullable<OwningCSSNumberish>& aRetVal) const {
  AnimationUtils::DurationToCSSNumberish(
      GetCurrentTimeAsDuration(), AcceptsPercentageBasedTime(), mRTPCallerType,
      GetParentObject(), aRetVal);
}

// https://drafts.csswg.org/web-animations-2/#setting-the-current-time-of-an-animation
// https://drafts.csswg.org/web-animations-2/#set-the-current-time
void Animation::SetCurrentTime(const Nullable<CSSNumberish>& aCurrentTime,
                               ErrorResult& aRv) {
  // Step 1: If seek time is an unresolved time value.
  if (aCurrentTime.IsNull()) {
    if (!GetCurrentTimeAsDuration().IsNull()) {
      aRv.ThrowTypeError(
          "Current time is resolved but trying to set it to an unresolved "
          "time");
    }
    return;
  }

  const bool progressBased = AcceptsPercentageBasedTime();

  // Step 2+3: Run the validate a CSSNumberish time procedure; abort on failure.
  if (!AnimationUtils::ValidateCSSNumberishTime(aCurrentTime.Value(),
                                                progressBased, aRv)) {
    return;
  }

  Nullable<TimeDuration> seekTime = AnimationUtils::CSSNumberishToDuration(
      aCurrentTime.Value(), progressBased);
  MOZ_ASSERT(!seekTime.IsNull());
  SetCurrentTime(seekTime.Value());
}

// ---------------------------------------------------------------------------

void Animation::Tick(AnimationTimeline::TickState& aTickState) {
  MakeReadyAndMaybeTrigger();
  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Sync);

  // Check for changes to whether or not this animation is replaceable.
  bool isReplaceable = IsReplaceable();
  if (isReplaceable && !mWasReplaceableAtLastTick) {
    ScheduleReplacementCheck();
  }
  mWasReplaceableAtLastTick = isReplaceable;

  if (!mEffect) {
    return;
  }

  // Update layers if we are newly finished.
  KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
  if (keyframeEffect && !keyframeEffect->Properties().IsEmpty() &&
      !mFinishedAtLastComposeStyle &&
      PlayState() == AnimationPlayState::Finished) {
    PostUpdate();
  }
}

bool Animation::MakeReadyAndMaybeTrigger() {
  // FIXME: We probably need to do this only if the timeline data or range is
  // changed. For now we always call the procedure per spec.
  AutoAlignStartTime();
  if (!Pending()) {
    return false;
  }
  // mPendingReadyTime is only meaningful for monotonic timelines (see its
  // declaration comment). For finite timelines, trigger directly using the
  // timeline's current time.
  if (mPendingReadyTime.IsNull() && !HasFiniteTimeline()) {
    if (MOZ_LIKELY(mTimeline)) {
      // Monotonic timeline with no ready time yet — schedule the trigger for
      // the next tick, but with this tick's timestamp.
      mPendingReadyTime = mTimeline->GetCurrentTimeAsTimeStamp();
    }
    return false;
  }
  return TryTriggerNow();
}

bool Animation::TryTriggerNow() {
  if (!Pending()) {
    return true;
  }
  // If we don't have an active timeline we can't trigger the animation.
  if (NS_WARN_IF(!mTimeline)) {
    return false;
  }

  // Note(dshin): Don't try to trigger inactive timelines, since they won't
  // tick in any meaningful way. This has implications on fulfilling the ready
  // promise - See https://github.com/w3c/csswg-drafts/issues/9256
  if (mTimeline->IsInactiveTimeline()) {
    return false;
  }

  // FIXME: Bug 2017448. Force to use timeline current time for finite
  // timelines. We may have to figure out a more suitable way to handle it.
  auto currentTime = (mPendingReadyTime.IsNull() || HasFiniteTimeline())
                         ? mTimeline->GetCurrentTimeAsDuration()
                         : mTimeline->ToTimelineTime(mPendingReadyTime);
  mPendingReadyTime = {};
  if (currentTime.IsNull()) {
    (void)NS_WARN_IF(!HasFiniteTimeline());
    return false;
  }
  FinishPendingAt(currentTime.Value());
  return true;
}

double Animation::CurrentOrPendingPlaybackRate() const {
  if (mPendingPlaybackRate.isSome()) {
    return *mPendingPlaybackRate * AnimationsPlayBackRateMultiplier();
  }

  return PlaybackRateInternal();
}

TimeStamp Animation::AnimationTimeToTimeStamp(
    const StickyTimeDuration& aTime) const {
  // Initializes to null. Return the same object every time to benefit from
  // return-value-optimization.
  TimeStamp result;

  // We *don't* check for mTimeline->TracksWallclockTime() here because that
  // method only tells us if the timeline times can be converted to
  // TimeStamps that can be compared to TimeStamp::Now() or not, *not*
  // whether the timelines can be converted to TimeStamp values at all.
  //
  // Furthermore, we want to be able to use this method when the refresh driver
  // is under test control (in which case TracksWallclockTime() will return
  // false).
  //
  // Once we introduce timelines that are not time-based we will need to
  // differentiate between them here and determine how to sort their events.
  if (!mTimeline) {
    return result;
  }

  // Check the time is convertible to a timestamp
  if (aTime == TimeDuration::Forever() || PlaybackRateInternal() == 0.0 ||
      mStartTime.IsNull()) {
    return result;
  }

  // Invert the standard relation:
  //   current time = (timeline time - start time) * playback rate
  TimeDuration timelineTime =
      TimeDuration(aTime).MultDouble(1.0 / PlaybackRateInternal()) +
      mStartTime.Value();

  result = mTimeline->ToTimeStamp(timelineTime);
  return result;
}

TimeStamp Animation::ElapsedTimeToTimeStamp(
    const StickyTimeDuration& aElapsedTime) const {
  TimeDuration delay =
      mEffect ? mEffect->NormalizedTiming().Delay() : TimeDuration();
  return AnimationTimeToTimeStamp(aElapsedTime + delay);
}

// https://drafts.csswg.org/web-animations/#silently-set-the-current-time
void Animation::SilentlySetCurrentTime(const TimeDuration& aSeekTime) {
  // TODO: Bug 1762238: Introduce "valid seek time" after introducing
  // CSSNumberish time values.
  // https://drafts.csswg.org/web-animations-2/#silently-set-the-current-time

  // Check the time is convertible to a timestamp
  if (!mHoldTime.IsNull() || mStartTime.IsNull() || !mTimeline ||
      mTimeline->GetCurrentTimeAsDuration().IsNull() ||
      PlaybackRateInternal() == 0.0) {
    mHoldTime.SetValue(aSeekTime);
  } else {
    mStartTime =
        StartTimeFromTimelineTime(mTimeline->GetCurrentTimeAsDuration().Value(),
                                  aSeekTime, PlaybackRateInternal());
  }

  if (!mTimeline || mTimeline->GetCurrentTimeAsDuration().IsNull()) {
    mStartTime.SetNull();
  }

  mPreviousCurrentTime.SetNull();
  mAutoAlignStartTime = false;
}

bool Animation::ShouldBeSynchronizedWithMainThread(
    const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame,
    AnimationPerformanceWarning::Type& aPerformanceWarning) const {
  // Only synchronize playing animations
  if (!IsPlaying()) {
    return false;
  }

  // Currently only transform animations need to be synchronized
  if (!aPropertySet.Intersects(nsCSSPropertyIDSet::TransformLikeProperties())) {
    return false;
  }

  KeyframeEffect* keyframeEffect =
      mEffect ? mEffect->AsKeyframeEffect() : nullptr;
  if (!keyframeEffect) {
    return false;
  }

  return keyframeEffect->ShouldBlockAsyncTransformAnimations(
      aFrame, aPropertySet, aPerformanceWarning);
}

void Animation::UpdateRelevance() {
  bool wasRelevant = mIsRelevant;
  mIsRelevant = mReplaceState != AnimationReplaceState::Removed &&
                (HasCurrentEffect() || IsInEffect());

  // Notify animation observers.
  if (wasRelevant && !mIsRelevant) {
    MutationObservers::NotifyAnimationRemoved(this);
  } else if (!wasRelevant && mIsRelevant) {
    UpdateHiddenByContentVisibility();
    MutationObservers::NotifyAnimationAdded(this);
  }
}

template <class T>
bool IsMarkupAnimation(T* aAnimation) {
  return aAnimation && aAnimation->IsTiedToMarkup();
}

// https://drafts.csswg.org/web-animations/#replaceable-animation
bool Animation::IsReplaceable() const {
  // We never replace CSS animations or CSS transitions since they are managed
  // by CSS.
  if (IsMarkupAnimation(AsCSSAnimation()) ||
      IsMarkupAnimation(AsCSSTransition())) {
    return false;
  }

  // Only finished animations can be replaced.
  if (PlayState() != AnimationPlayState::Finished) {
    return false;
  }

  // Already removed animations cannot be replaced.
  if (ReplaceState() == AnimationReplaceState::Removed) {
    return false;
  }

  // We can only replace an animation if we know that, uninterfered, it would
  // never start playing again. That excludes any animations on timelines that
  // aren't monotonically increasing.
  //
  // If we don't have any timeline at all, then we can't be in the finished
  // state (since we need both a resolved start time and current time for that)
  // and will have already returned false above.
  //
  // (However, if it ever does become possible to be finished without a timeline
  // then we will want to return false here since it probably suggests an
  // animation being driven directly by script, in which case we can't assume
  // anything about how they will behave.)
  if (!GetTimeline() || !GetTimeline()->TracksWallclockTime()) {
    return false;
  }

  // If the animation doesn't have an effect then we can't determine if it is
  // filling or not so just leave it alone.
  if (!GetEffect()) {
    return false;
  }

  // At the time of writing we only know about KeyframeEffects. If we introduce
  // other types of effects we will need to decide if they are replaceable or
  // not.
  MOZ_ASSERT(GetEffect()->AsKeyframeEffect(),
             "Effect should be a keyframe effect");

  // We only replace animations that are filling.
  if (GetEffect()->GetComputedTiming().mProgress.IsNull()) {
    return false;
  }

  // We should only replace animations with a target element (since otherwise
  // what other effects would we consider when determining if they are covered
  // or not?).
  if (!GetEffect()->AsKeyframeEffect()->GetAnimationTarget()) {
    return false;
  }

  return true;
}

bool Animation::IsRemovable() const {
  return ReplaceState() == AnimationReplaceState::Active && IsReplaceable();
}

void Animation::ScheduleReplacementCheck() {
  MOZ_ASSERT(
      IsReplaceable(),
      "Should only schedule a replacement check for a replaceable animation");

  // If IsReplaceable() is true, the following should also hold
  MOZ_ASSERT(GetEffect());
  MOZ_ASSERT(GetEffect()->AsKeyframeEffect());

  NonOwningAnimationTarget target =
      GetEffect()->AsKeyframeEffect()->GetAnimationTarget();

  MOZ_ASSERT(target);

  nsPresContext* presContext =
      nsContentUtils::GetContextForContent(target.mElement);
  if (presContext) {
    presContext->EffectCompositor()->NoteElementForReducing(target);
  }
}

void Animation::MaybeScheduleReplacementCheck() {
  if (!IsReplaceable()) {
    return;
  }

  ScheduleReplacementCheck();
}

void Animation::Remove() {
  MOZ_ASSERT(IsRemovable(),
             "Should not be trying to remove an effect that is not removable");

  mReplaceState = AnimationReplaceState::Removed;

  UpdateEffect(PostRestyleMode::IfNeeded);
  PostUpdate();

  QueuePlaybackEvent(nsGkAtoms::onremove, GetTimelineCurrentTimeAsTimeStamp());
}

int32_t Animation::CompareCompositeOrder(
    const Maybe<EventContext>& aContext, const Animation& aOther,
    const Maybe<EventContext>& aOtherContext,
    nsContentUtils::NodeIndexCache& aCache) const {
  // 0. Object-equality case
  if (&aOther == this) {
    return 0;
  }

  // 1. CSS Transitions sort lowest
  {
    auto asCSSTransitionForSorting =
        [](const Animation& anim,
           const Maybe<EventContext>& aContext) -> const CSSTransition* {
      const CSSTransition* transition = anim.AsCSSTransition();
      return transition && (aContext || transition->IsTiedToMarkup())
                 ? transition
                 : nullptr;
    };
    const auto* const thisTransition =
        asCSSTransitionForSorting(*this, aContext);
    const auto* const otherTransition =
        asCSSTransitionForSorting(aOther, aOtherContext);
    if (thisTransition && otherTransition) {
      return thisTransition->CompareCompositeOrder(aContext, *otherTransition,
                                                   aOtherContext, aCache);
    }
    if (thisTransition || otherTransition) {
      // Cancelled transitions no longer have an owning element. To be strictly
      // correct we should store a strong reference to the owning element
      // so that if we arrive here while sorting cancel events, we can sort
      // them in the correct order.
      //
      // However, given that cancel events are almost always queued
      // synchronously in some deterministic manner, we can be fairly sure
      // that cancel events will be dispatched in a deterministic order
      // (which is our only hard requirement until specs say otherwise).
      // Furthermore, we only reach here when we have events with equal
      // timestamps so this is an edge case we can probably ignore for now.
      return thisTransition ? -1 : 1;
    }
  }

  // 2. CSS Animations sort next
  {
    auto asCSSAnimationForSorting =
        [](const Animation& anim) -> const CSSAnimation* {
      const CSSAnimation* animation = anim.AsCSSAnimation();
      return animation && animation->IsTiedToMarkup() ? animation : nullptr;
    };
    auto thisAnimation = asCSSAnimationForSorting(*this);
    auto otherAnimation = asCSSAnimationForSorting(aOther);
    if (thisAnimation && otherAnimation) {
      return thisAnimation->CompareCompositeOrder(*otherAnimation, aCache);
    }
    if (thisAnimation || otherAnimation) {
      return thisAnimation ? -1 : 1;
    }
  }

  // Subclasses of Animation repurpose mAnimationIndex to implement their
  // own brand of composite ordering. However, by this point we should have
  // handled any such custom composite ordering so we should now have unique
  // animation indices.
  MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex,
             "Animation indices should be unique");

  // 3. Finally, generic animations sort by their position in the global
  // animation array.
  return mAnimationIndex > aOther.mAnimationIndex ? 1 : -1;
}

void Animation::WillComposeStyle() {
  mFinishedAtLastComposeStyle = (PlayState() == AnimationPlayState::Finished);

  MOZ_ASSERT(mEffect);

  KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
  if (keyframeEffect) {
    keyframeEffect->WillComposeStyle();
  }
}

void Animation::ComposeStyle(
    StyleAnimationValueMap& aComposeResult,
    const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip,
    EndpointBehavior aEndpointBehavior) {
  if (!mEffect) {
    return;
  }

  // In order to prevent flicker, there are a few cases where we want to use
  // a different time for rendering that would otherwise be returned by
  // GetCurrentTimeAsDuration. These are:
  //
  // (a) For animations that are pausing but which are still running on the
  //     compositor. In this case we send a layer transaction that removes the
  //     animation but which also contains the animation values calculated on
  //     the main thread. To prevent flicker when this occurs we want to ensure
  //     the timeline time used to calculate the main thread animation values
  //     does not lag far behind the time used on the compositor. Ideally we
  //     would like to use the "animation ready time", but for now we just use
  //     the current wallclock time. TODO(emilio): After bug 1864425 it seems we
  //     could just use the ready time, or maybe we can remove this?
  //
  // (b) For animations that are pausing that we have already taken off the
  //     compositor. In this case we record a pending ready time but we don't
  //     apply it until the next tick. However, while waiting for the next tick,
  //     we should still use the pending ready time as the timeline time. If we
  //     use the regular timeline time the animation may appear jump backwards
  //     if the main thread's timeline time lags behind the compositor.
  //
  // (c) For animations that are play-pending due to an aborted pause operation
  //     (i.e. a pause operation that was interrupted before we entered the
  //     paused state). When we cancel a pending pause we might momentarily take
  //     the animation off the compositor, only to re-add it moments later. In
  //     that case the compositor might have been ahead of the main thread so we
  //     should use the current wallclock time to ensure the animation doesn't
  //     temporarily jump backwards.
  //
  // To address each of these cases we temporarily tweak the hold time
  // immediately before updating the style rule and then restore it immediately
  // afterwards. This is purely to prevent visual flicker. Other behavior
  // such as dispatching events continues to rely on the regular timeline time.
  const bool pending = Pending();
  {
    AutoRestore<Nullable<TimeDuration>> restoreHoldTime(mHoldTime);
    if (pending && mHoldTime.IsNull() && !mStartTime.IsNull()) {
      Nullable<TimeDuration> timeToUse;
      if (mTimeline && mTimeline->TracksWallclockTime()) {
        timeToUse = mTimeline->ToTimelineTime(TimeStamp::Now());
      }
      if (!timeToUse.IsNull()) {
        mHoldTime = CurrentTimeFromTimelineTime(
            timeToUse.Value(), mStartTime.Value(), PlaybackRateInternal());
      }
    }

    KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
    if (keyframeEffect) {
      keyframeEffect->ComposeStyle(aComposeResult, aPropertiesToSkip,
                                   aEndpointBehavior);
    }
  }

  MOZ_ASSERT(
      pending == Pending(),
      "Pending state should not change during the course of compositing");
}

void Animation::NotifyEffectTimingUpdated() {
  MOZ_ASSERT(mEffect,
             "We should only update effect timing when we have a target "
             "effect");
  UpdateTiming(Animation::SeekFlag::NoSeek, Animation::SyncNotifyFlag::Async);
}

void Animation::NotifyEffectPropertiesUpdated() {
  MOZ_ASSERT(mEffect,
             "We should only update effect properties when we have a target "
             "effect");

  MaybeScheduleReplacementCheck();
}

void Animation::NotifyEffectTargetUpdated() {
  MOZ_ASSERT(mEffect,
             "We should only update the effect target when we have a target "
             "effect");

  MaybeScheduleReplacementCheck();
}

// https://drafts.csswg.org/web-animations-2/#playing-an-animation-section
void Animation::PlayNoUpdate(ErrorResult& aRv, LimitBehavior aLimitBehavior) {
  AutoMutationBatchForAnimation mb(*this);

  // 1. Let aborted pause be a boolean flag that is true if animation has a
  //    pending pause task, and false otherwise.
  // 2. Let has pending ready promise be a boolean flag that is initially false.
  // 3. Let has finite timeline be true if animation has an associated timeline
  //    that is not monotonically increasing.
  // 4. Let previous current time be the animation’s current time.
  // 5. Let enable seek be true if the auto-rewind flag is true and has finite
  //    timeline is false. Otherwise, initialize to false.
  const bool abortedPause = mPendingState == PendingState::PausePending;
  bool hasPendingReadyPromise = false;
  const bool hasFiniteTimeline = HasFiniteTimeline();
  const Nullable<TimeDuration> prevCurrentTime = GetCurrentTimeAsDuration();
  const bool enableSeek =
      (aLimitBehavior == LimitBehavior::AutoRewind) && !hasFiniteTimeline;

  // 6. Perform the steps corresponding to the first matching condition from the
  // following, if any:
  const double effectivePlaybackRate = CurrentOrPendingPlaybackRate();
  const StickyTimeDuration associatedEffectEnd = EffectEnd();
  if (effectivePlaybackRate > 0.0 && enableSeek &&
      (prevCurrentTime.IsNull() || prevCurrentTime.Value() < TimeDuration() ||
       prevCurrentTime.Value() >= associatedEffectEnd)) {
    // If animation’s effective playback rate > 0, enable seek is true and
    // either animation’s:
    //   - previous current time is unresolved, or
    //   - previous current time < zero, or
    //   - previous current time ≥ associated effect end,
    // Set the animation’s hold time to zero.
    mHoldTime = TimeDuration();
  } else if (effectivePlaybackRate < 0.0 && enableSeek &&
             (prevCurrentTime.IsNull() ||
              prevCurrentTime.Value() <= TimeDuration() ||
              prevCurrentTime.Value() > associatedEffectEnd)) {
    // If animation’s effective playback rate < 0, enable seek is true and
    // either animation’s:
    //   - previous current time is unresolved, or
    //   - previous current time ≤ zero, or
    //   - previous current time > associated effect end,
    // If associated effect end is positive infinity,
    //   throw an "InvalidStateError" DOMException and abort these steps.
    // Otherwise,
    //   Set the animation’s hold time to the animation’s associated effect end.
    if (associatedEffectEnd == TimeDuration::Forever()) {
      return aRv.ThrowInvalidStateError(
          "Can't rewind animation with infinite effect end");
    }
    mHoldTime.SetValue(TimeDuration(associatedEffectEnd));
  } else if (effectivePlaybackRate == 0.0 && prevCurrentTime.IsNull()) {
    // If animation’s effective playback rate = 0 and animation’s current time
    // is unresolved,
    // Set the animation’s hold time to zero.
    mHoldTime = TimeDuration();
  }

  // 7. If has finite timeline and previous current time is unresolved:
  if (hasFiniteTimeline && prevCurrentTime.IsNull()) {
    // Set the flag auto align start time to true.
    mAutoAlignStartTime = true;
  }

  // Note: This is a special case mentioned in web-animations-1, but not in
  // web-animations-2. The Gecko's implementation of auto-rewind is slightly
  // different from other browsers when playing a new animation and updating
  // an existing animation. (See [1] for more details.) So we need to set
  // |mHoldTime| for this special case to make sure our behavior matches other
  // browsers, especially for a null timeline with the false auto-rewind flag.
  // [1] https://github.com/w3c/csswg-drafts/issues/7145
  if (!hasFiniteTimeline && prevCurrentTime.IsNull() && mHoldTime.IsNull()) {
    mHoldTime = TimeDuration();
  }

  const bool hasInactiveTimeline = mTimeline && mTimeline->IsInactiveTimeline();
  if (hasInactiveTimeline && mHoldTime.IsNull()) {
    // Note(dshin): If we're inactive state and trying to play, hold at zero.
    // This isn't part of the spec (Spec discusses inactive timelines very
    // little), but this falls out of inactive timeline behing a finite timeline
    // (See the class definition for why).
    mHoldTime = TimeDuration();
  }

  // 8. If animation’s hold time is resolved, let its start time be unresolved.
  //
  // Clear the start time until we resolve a new one. We do this except
  // for the case where we are aborting a pause and don't have a hold time.
  //
  // If we're aborting a pause and *do* have a hold time (e.g. because
  // the animation is finished or we just applied the auto-rewind behavior
  // above) we should respect it by clearing the start time. If we *don't*
  // have a hold time we should keep the current start time so that the
  // the animation continues moving uninterrupted by the aborted pause.
  if (!mHoldTime.IsNull()) {
    mStartTime.SetNull();
  }

  // 9. If animation has a pending play task or a pending pause task,
  if (mPendingState != PendingState::NotPending) {
    // Cancel that task.
    CancelPendingTasks();
    // Set has pending ready promise to true.
    hasPendingReadyPromise = true;
  }

  // 10. If the following three conditions are all satisfied:
  //   - animation’s hold time is unresolved, and
  //   - aborted pause is false, and
  //   - animation does not have a pending playback rate,
  // abort this procedure.
  //
  // Note:
  // If the hold time is null then we're already playing normally and,
  // typically, we can bail out here.
  //
  // However, there are two cases where we can't do that:
  //
  // (a) If we just aborted a pause. In this case, for consistency, we need to
  //     go through the motions of doing an asynchronous start.
  //
  // (b) If we have timing changes (specifically a change to the playbackRate)
  //     that should be applied asynchronously.
  //
  // FIXME: |pendingAutoAlignedStartTime| is a workaround because the spec
  // missed a condition for finite timelines. See the spec issue for details.
  // https://github.com/w3c/csswg-drafts/issues/10965#issuecomment-2413140700
  auto pendingAutoAlignedStartTime = mAutoAlignStartTime && mStartTime.IsNull();
  if (mHoldTime.IsNull() && !abortedPause && !mPendingPlaybackRate &&
      !pendingAutoAlignedStartTime) {
    return;
  }

  // 11. If has pending ready promise is false, let animation’s current ready
  // promise be a new promise in the relevant Realm of animation.
  if (!hasPendingReadyPromise) {
    // Clear ready promise. We'll create a new one lazily.
    mReady = nullptr;
  }

  // 12. Schedule a task to run as soon as animation is ready.
  // Note: See Animation::ResumeAt() for the details of this step.
  mPendingState = PendingState::PlayPending;
  mPendingReadyTime = {};
  if (Document* doc = GetRenderedDocument()) {
    mPendingReadyTime = EnsurePaintIsScheduled(*doc);
  }

  // 13. Run the procedure to update an animation’s finished state for animation
  // with the did seek flag set to false, and the synchronously notify flag set
  // to false.
  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
  if (IsRelevant()) {
    MutationObservers::NotifyAnimationChanged(this);
  }
}

// Note: The module level 2 defines some replacements, so we have to take both
// sections into account.
// https://drafts.csswg.org/web-animations-1/#pausing-an-animation-section
// https://drafts.csswg.org/web-animations-2/#pausing-an-animation-section
void Animation::Pause(ErrorResult& aRv) {
  // If animation has a pending pause task, abort these steps.
  // If the play state of animation is paused, abort these steps.
  if (IsPausedOrPausing()) {
    return;
  }

  AutoMutationBatchForAnimation mb(*this);

  // Let has finite timeline be true if animation has an associated timeline
  // that is not monotonically increasing.
  const bool hasFiniteTimeline = HasFiniteTimeline();

  if (GetCurrentTimeAsDuration().IsNull()) {
    if (!hasFiniteTimeline) {
      // If the animation’s current time is unresolved and has finite timeline
      // is false, perform the steps according to the first matching condition
      // below:
      if (PlaybackRateInternal() >= 0.0) {
        // If animation’s playback rate is ≥ 0, set hold time to zero.
        mHoldTime.SetValue(TimeDuration());
      } else {
        if (EffectEnd() == TimeDuration::Forever()) {
          // If associated effect end for animation is positive infinity, throw
          // an "InvalidStateError" DOMException and abort these steps.
          return aRv.ThrowInvalidStateError(
              "Can't seek to infinite effect end");
        }
        // Otherwise, set hold time to animation’s associated effect end.
        mHoldTime.SetValue(TimeDuration(EffectEnd()));
      }
    } else {
      // If has finite timeline is true, and the animation’s current time is
      // unresolved, set the auto align start time flag to true.
      mAutoAlignStartTime = true;
    }
  }

  // Let has pending ready promise be a boolean flag that is initially false.
  bool hasPendingReadyPromise = false;

  // If animation has a pending play task, cancel that task and let has pending
  // ready promise be true.
  if (mPendingState == PendingState::PlayPending) {
    CancelPendingTasks();
    hasPendingReadyPromise = true;
  }

  // If has pending ready promise is false, set animation’s current ready
  // promise to a new promise in the relevant Realm of animation.
  if (!hasPendingReadyPromise) {
    // Clear ready promise. We'll create a new one lazily.
    mReady = nullptr;
  }

  // Schedule the panding pause task. See Animation::PauseAt() for details.
  mPendingState = PendingState::PausePending;
  mPendingReadyTime = {};
  if (Document* doc = GetRenderedDocument()) {
    mPendingReadyTime = EnsurePaintIsScheduled(*doc);
  }

  // Run the procedure to update an animation’s finished state for animation
  // with the did seek flag set to false, and the synchronously notify flag set
  // to false.
  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
  if (IsRelevant()) {
    MutationObservers::NotifyAnimationChanged(this);
  }

  PostUpdate();
}

// https://drafts.csswg.org/web-animations-2/#playing-an-animation-section
void Animation::ResumeAt(const TimeDuration& aReadyTime) {
  // This method is only expected to be called for an animation that is
  // waiting to play. We can easily adapt it to handle other states
  // but it's currently not necessary.
  MOZ_ASSERT(mPendingState == PendingState::PlayPending,
             "Expected to resume a play-pending animation");
  MOZ_ASSERT(!mHoldTime.IsNull() || !mStartTime.IsNull(),
             "An animation in the play-pending state should have either a"
             " resolved hold time or resolved start time");

  AutoMutationBatchForAnimation mb(*this);
  bool hadPendingPlaybackRate = mPendingPlaybackRate.isSome();

  if (!mHoldTime.IsNull()) {
    // The hold time is set, so we don't need any special handling to preserve
    // the current time.
    ApplyPendingPlaybackRate();
    mStartTime = StartTimeFromTimelineTime(aReadyTime, mHoldTime.Value(),
                                           PlaybackRateInternal());
    if (PlaybackRateInternal() != 0) {
      mHoldTime.SetNull();
    }
  } else if (!mStartTime.IsNull() && mPendingPlaybackRate) {
    // Apply any pending playback rate, preserving the current time.
    TimeDuration currentTimeToMatch = CurrentTimeFromTimelineTime(
        aReadyTime, mStartTime.Value(), PlaybackRateInternal());
    ApplyPendingPlaybackRate();
    mStartTime = StartTimeFromTimelineTime(aReadyTime, currentTimeToMatch,
                                           PlaybackRateInternal());
    if (PlaybackRateInternal() == 0) {
      mHoldTime.SetValue(currentTimeToMatch);
    }
  }

  mPendingState = PendingState::NotPending;

  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Sync);

  // If we had a pending playback rate, we will have now applied it so we need
  // to notify observers.
  if (hadPendingPlaybackRate && IsRelevant()) {
    MutationObservers::NotifyAnimationChanged(this);
  }

  MaybeResolvePromiseWithThis(mReady);
}

void Animation::PauseAt(const TimeDuration& aReadyTime) {
  MOZ_ASSERT(mPendingState == PendingState::PausePending,
             "Expected to pause a pause-pending animation");

  if (!mStartTime.IsNull() && mHoldTime.IsNull()) {
    mHoldTime = CurrentTimeFromTimelineTime(aReadyTime, mStartTime.Value(),
                                            PlaybackRateInternal());
  }
  ApplyPendingPlaybackRate();
  mStartTime.SetNull();
  mPendingState = PendingState::NotPending;

  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);

  MaybeResolvePromiseWithThis(mReady);
}

void Animation::UpdateTiming(SeekFlag aSeekFlag,
                             SyncNotifyFlag aSyncNotifyFlag) {
  // We call UpdateFinishedState before UpdateEffect because the former
  // can change the current time, which is used by the latter.
  UpdateFinishedState(aSeekFlag, aSyncNotifyFlag);
  UpdateEffect(PostRestyleMode::IfNeeded);

  if (mTimeline) {
    mTimeline->NotifyAnimationUpdated(*this);
  }
}

// https://drafts.csswg.org/web-animations/#update-an-animations-finished-state
void Animation::UpdateFinishedState(SeekFlag aSeekFlag,
                                    SyncNotifyFlag aSyncNotifyFlag) {
  Nullable<TimeDuration> unconstrainedCurrentTime =
      aSeekFlag == SeekFlag::NoSeek ? GetUnconstrainedCurrentTime()
                                    : GetCurrentTimeAsDuration();
  TimeDuration effectEnd = TimeDuration(EffectEnd());

  if (!unconstrainedCurrentTime.IsNull() && !mStartTime.IsNull() &&
      mPendingState == PendingState::NotPending) {
    if (PlaybackRateInternal() > 0.0 &&
        unconstrainedCurrentTime.Value() >= effectEnd) {
      if (aSeekFlag == SeekFlag::DidSeek) {
        mHoldTime = unconstrainedCurrentTime;
      } else if (!mPreviousCurrentTime.IsNull()) {
        mHoldTime.SetValue(std::max(mPreviousCurrentTime.Value(), effectEnd));
      } else {
        mHoldTime.SetValue(effectEnd);
      }
    } else if (PlaybackRateInternal() < 0.0 &&
               unconstrainedCurrentTime.Value() <= TimeDuration()) {
      if (aSeekFlag == SeekFlag::DidSeek) {
        mHoldTime = unconstrainedCurrentTime;
      } else if (!mPreviousCurrentTime.IsNull()) {
        mHoldTime.SetValue(
            std::min(mPreviousCurrentTime.Value(), TimeDuration()));
      } else {
        mHoldTime.SetValue(TimeDuration());
      }
    } else if (PlaybackRateInternal() != 0.0 && mTimeline &&
               !mTimeline->GetCurrentTimeAsDuration().IsNull()) {
      if (aSeekFlag == SeekFlag::DidSeek && !mHoldTime.IsNull()) {
        mStartTime = StartTimeFromTimelineTime(
            mTimeline->GetCurrentTimeAsDuration().Value(), mHoldTime.Value(),
            PlaybackRateInternal());
      }
      mHoldTime.SetNull();
    }
  }

  // We must recalculate the current time to take account of any mHoldTime
  // changes the code above made.
  mPreviousCurrentTime = GetCurrentTimeAsDuration();

  bool currentFinishedState = PlayState() == AnimationPlayState::Finished;
  if (currentFinishedState && !mFinishedIsResolved) {
    DoFinishNotification(aSyncNotifyFlag);
  } else if (!currentFinishedState && mFinishedIsResolved) {
    ResetFinishedPromise();
  }
}

void Animation::UpdateEffect(PostRestyleMode aPostRestyle) {
  if (mEffect) {
    UpdateRelevance();

    KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
    if (keyframeEffect) {
      keyframeEffect->NotifyAnimationTimingUpdated(aPostRestyle);
    }
  }
}

void Animation::FlushUnanimatedStyle() const {
  if (Document* doc = GetRenderedDocument()) {
    doc->FlushPendingNotifications(
        ChangesToFlush(FlushType::Style, /* aFlushAnimations = */ false,
                       /* aUpdateRelevancy = */ false));
  }
}

void Animation::PostUpdate() {
  if (!mEffect) {
    return;
  }

  KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
  if (!keyframeEffect) {
    return;
  }
  keyframeEffect->RequestRestyle(EffectCompositor::RestyleType::Layer);
}

void Animation::CancelPendingTasks() {
  mPendingState = PendingState::NotPending;
}

// https://drafts.csswg.org/web-animations/#reset-an-animations-pending-tasks
void Animation::ResetPendingTasks() {
  if (!Pending()) {
    return;
  }

  CancelPendingTasks();
  ApplyPendingPlaybackRate();

  if (mReady) {
    mReady->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
    MOZ_ALWAYS_TRUE(mReady->SetAnyPromiseIsHandled());
    mReady = nullptr;
  }
}

// https://drafts.csswg.org/web-animations-2/#at-progress-timeline-boundary
/* static*/ Animation::ProgressTimelinePosition
Animation::AtProgressTimelineBoundary(
    const Nullable<TimeDuration>& aTimelineDuration,
    const Nullable<TimeDuration>& aCurrentTime,
    const TimeDuration& aEffectStartTime, const double aPlaybackRate) {
  // Based on changed defined in: https://github.com/w3c/csswg-drafts/pull/6702
  // 1.  If any of the following conditions are true:
  //     * the associated animation's timeline is not a progress-based timeline,
  //     or
  //     * the associated animation's timeline duration is unresolved or zero,
  //     or
  //     * the animation's playback rate is zero
  //     return false
  // Note: We can detect a progress-based timeline by relying on the fact that
  // monotonic timelines (i.e. non-progress-based timelines) have an unresolved
  // timeline duration.
  if (aTimelineDuration.IsNull() || aTimelineDuration.Value().IsZero() ||
      aPlaybackRate == 0.0) {
    return ProgressTimelinePosition::NotBoundary;
  }

  // 2.  Let effective start time be the animation's start time if resolved, or
  // zero otherwise.
  const TimeDuration& effectiveStartTime = aEffectStartTime;

  // 3.  Let effective timeline time be (animation's current time / animation's
  // playback rate) + effective start time.
  // Note: we use zero if the current time is unresolved. See the spec issue:
  // https://github.com/w3c/csswg-drafts/issues/7458
  const TimeDuration effectiveTimelineTime =
      (aCurrentTime.IsNull()
           ? TimeDuration()
           : aCurrentTime.Value().MultDouble(1.0 / aPlaybackRate)) +
      effectiveStartTime;

  // 4.  Let effective timeline progress be (effective timeline time / timeline
  // duration)
  // 5.  If effective timeline progress is 0 or 1, return true,
  // We avoid the division here but it is effectively the same as 4 & 5 above.
  return effectiveTimelineTime.IsZero() ||
                 (AnimationUtils::IsWithinAnimationTimeTolerance(
                     effectiveTimelineTime, aTimelineDuration.Value()))
             ? ProgressTimelinePosition::Boundary
             : ProgressTimelinePosition::NotBoundary;
}

void Animation::UpdateNormalizedTimingForTimelineDataChange() {
  if (!mEffect) {
    return;
  }

  mEffect->UpdateNormalizedTiming();
}

void Animation::MaybeUpdateKeyframeComputedOffsets() {
  if (!mEffect || !mEffect->AsKeyframeEffect()) {
    return;
  }

  mEffect->AsKeyframeEffect()->MaybeUpdateKeyframeComputedOffsets(
      mTimeline, mTimelineRange);
}

StickyTimeDuration Animation::EffectEnd() const {
  if (!mEffect) {
    return StickyTimeDuration();
  }

  // FIXME: The definition of end time in web-animation-1 is different from that
  // in web-animation-2, which includes the start time. We are still using the
  // definition in web-animation-1 here for now.
  // Note: It seems other browsers still use the definition in web-animation-1
  // as well. Perhaps the new definition is for some other new features?
  return mEffect->NormalizedTiming().EndTime();
}

Document* Animation::GetRenderedDocument() const {
  if (!mEffect || !mEffect->AsKeyframeEffect()) {
    return nullptr;
  }

  return mEffect->AsKeyframeEffect()->GetRenderedDocument();
}

Document* Animation::GetTimelineDocument() const {
  return mTimeline ? mTimeline->GetDocument() : nullptr;
}

class AsyncFinishNotification : public MicroTaskRunnable {
 public:
  explicit AsyncFinishNotification(Animation* aAnimation)
      : mAnimation(aAnimation) {}

  virtual void Run(AutoSlowOperation& aAso) override {
    mAnimation->DoFinishNotificationImmediately(this);
    mAnimation = nullptr;
  }

  virtual bool Suppressed() override {
    nsIGlobalObject* global = mAnimation->GetRelevantGlobal();
    return global && global->IsInSyncOperation();
  }

 private:
  RefPtr<Animation> mAnimation;
};

void Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag) {
  CycleCollectedJSContext* context = CycleCollectedJSContext::Get();

  if (aSyncNotifyFlag == SyncNotifyFlag::Sync) {
    DoFinishNotificationImmediately();
  } else if (!mFinishNotificationTask) {
    RefPtr<MicroTaskRunnable> runnable = new AsyncFinishNotification(this);
    context->DispatchToMicroTask(do_AddRef(runnable));
    mFinishNotificationTask = std::move(runnable);
  }
}

void Animation::ResetFinishedPromise() {
  mFinishedIsResolved = false;
  mFinished = nullptr;
}

void Animation::MaybeResolveFinishedPromise() {
  mFinishedIsResolved = true;
  MaybeResolvePromiseWithThis(mFinished);
}

void Animation::DoFinishNotificationImmediately(MicroTaskRunnable* aAsync) {
  if (aAsync && aAsync != mFinishNotificationTask) {
    return;
  }

  mFinishNotificationTask = nullptr;

  if (PlayState() != AnimationPlayState::Finished) {
    return;
  }

  MaybeResolveFinishedPromise();

  QueuePlaybackEvent(nsGkAtoms::onfinish,
                     AnimationTimeToTimeStamp(EffectEnd()));
}

void Animation::QueuePlaybackEvent(nsAtom* aOnEvent,
                                   TimeStamp&& aScheduledEventTime) {
  // Use document for timing.
  // https://drafts.csswg.org/web-animations-1/#document-for-timing
  Document* doc = GetTimelineDocument();
  if (!doc) {
    return;
  }

  nsPresContext* presContext = doc->GetPresContext();
  if (!presContext) {
    return;
  }

  Nullable<double> currentTime;
  if (aOnEvent == nsGkAtoms::onfinish || aOnEvent == nsGkAtoms::onremove) {
    currentTime = AnimationUtils::TimeDurationToDouble(
        GetCurrentTimeAsDuration(), mRTPCallerType);
  }

  Nullable<double> timelineTime;
  if (mTimeline) {
    timelineTime = mTimeline->GetCurrentTimeAsDouble();
  }

  presContext->AnimationEventDispatcher()->QueueEvent(
      AnimationEventInfo(aOnEvent, currentTime, timelineTime,
                         std::move(aScheduledEventTime), this));
}

bool Animation::IsRunningOnCompositor() const {
  return mEffect && mEffect->AsKeyframeEffect() &&
         mEffect->AsKeyframeEffect()->IsRunningOnCompositor();
}

bool Animation::HasCurrentEffect() const {
  return GetEffect() && GetEffect()->IsCurrent();
}

bool Animation::IsInEffect() const {
  return GetEffect() && GetEffect()->IsInEffect();
}

void Animation::SetHiddenByContentVisibility(bool hidden) {
  if (mHiddenByContentVisibility == hidden) {
    return;
  }

  mHiddenByContentVisibility = hidden;

  if (!GetTimeline()) {
    return;
  }

  GetTimeline()->NotifyAnimationContentVisibilityChanged(this, !hidden);
}

void Animation::UpdateHiddenByContentVisibility() {
  // To be consistent with nsIFrame::UpdateAnimationVisibility, here we only
  // deal with CSSAnimation and CSSTransition.
  if (!AsCSSAnimation() && !AsCSSTransition()) {
    return;
  }
  NonOwningAnimationTarget target = GetTargetForAnimation();
  if (!target) {
    return;
  }
  // If a CSS animation or CSS transition is no longer associated with an owning
  // element, it behaves like a programmatic web animation, c-v shouldn't hide
  // it.
  bool hasOwningElement = IsMarkupAnimation(AsCSSAnimation()) ||
                          IsMarkupAnimation(AsCSSTransition());
  if (auto* frame = target.mElement->GetPrimaryFrame()) {
    SetHiddenByContentVisibility(
        hasOwningElement && frame->IsHiddenByContentVisibilityOnAnyAncestor());
  }
}

// When updating timeline current time, the start time of any attached
// animation is conditionally updated. For each attached animation, run the
// procedure for calculating an auto-aligned start time.
// https://drafts.csswg.org/scroll-animations-1/#event-loop
// https://drafts.csswg.org/web-animations-2/#auto-aligning-start-time
void Animation::AutoAlignStartTime() {
  // If the auto-align start time flag is false, abort this procedure.
  if (!mAutoAlignStartTime) {
    return;
  }

  // If the timeline is inactive, abort this procedure.
  if (!mTimeline || mTimeline->GetCurrentTimeAsDuration().IsNull()) {
    return;
  }

  MOZ_ASSERT(!mTimeline->IsMonotonicallyIncreasing(),
             "We shouldn't come here for monotonically increasing timeline");
  // Bail out in release builds if we somehow get here with a monotonic
  // timeline, to avoid dereferencing AsScrollTimeline() below.
  if (mTimeline->IsMonotonicallyIncreasing()) {
    return;
  }

  // If play state is idle, abort this procedure.
  const AnimationPlayState playState = PlayState();
  if (playState == AnimationPlayState::Idle) {
    return;
  }

  // If play state is paused, and hold time is resolved, abort this procedure.
  if (playState == AnimationPlayState::Paused && !mHoldTime.IsNull()) {
    return;
  }

  // Let start offset be the resolved timeline time corresponding to the start
  // of the animation attachment range. In the case of view timelines, it
  // requires a calculation based on the proportion of the cover range.
  //
  // Let end offset be the resolved timeline time corresponding to the end of
  // the animation attachment range. In the case of view timelines, it requires
  // a calculation based on the proportion of the cover range.
  MOZ_ASSERT(mTimeline->IsScrollTimeline(),
             "Only the finite timeline sets this flag.");
  const auto [startOffset, endOffset] =
      mTimeline->AsScrollTimeline()->IntervalForAttachmentRange(mTimelineRange);

  // Set start time to start offset if effective playback rate ≥ 0, and end
  // offset otherwise.
  const double effectivePlaybackRate = CurrentOrPendingPlaybackRate();
  mStartTime.SetValue(TimeDuration::FromMilliseconds(
      (effectivePlaybackRate >= 0.0 ? startOffset : endOffset) *
      PROGRESS_TIMELINE_DURATION_MILLISEC));

  // Clear hold time.
  mHoldTime.SetNull();
}

StickyTimeDuration Animation::IntervalStartTime(
    const StickyTimeDuration& aActiveDuration) const {
  MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(),
             "Should be called for CSS animations or transitions");
  static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration();
  return std::max(
      std::min(StickyTimeDuration(-mEffect->NormalizedTiming().Delay()),
               aActiveDuration),
      zeroDuration);
}

// Later side of the elapsed time range reported in CSS Animations and CSS
// Transitions events.
//
// https://drafts.csswg.org/css-animations-2/#interval-end
// https://drafts.csswg.org/css-transitions-2/#interval-end
StickyTimeDuration Animation::IntervalEndTime(
    const StickyTimeDuration& aActiveDuration) const {
  MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(),
             "Should be called for CSS animations or transitions");

  static constexpr StickyTimeDuration zeroDuration = StickyTimeDuration();
  const StickyTimeDuration& effectEnd = EffectEnd();

  // If both "associated effect end" and "start delay" are Infinity, we skip it
  // because we will get NaN when computing "Infinity - Infinity", and
  // using NaN in std::min or std::max is undefined.
  if (MOZ_UNLIKELY(effectEnd == TimeDuration::Forever() &&
                   effectEnd == mEffect->NormalizedTiming().Delay())) {
    // Note: If we use TimeDuration::Forever(), within our animation event
    // handling, we'd end up turning that into a null TimeStamp which can causes
    // errors if we try to do any arithmetic with it. Given that we should never
    // end up _using_ the interval end time. So returning zeroDuration here is
    // probably fine.
    return zeroDuration;
  }

  return std::max(std::min(effectEnd - mEffect->NormalizedTiming().Delay(),
                           aActiveDuration),
                  zeroDuration);
}

double Animation::AnimationsPlayBackRateMultiplier() const {
  if (mEffect && mEffect->AsKeyframeEffect()) {
    return mEffect->AsKeyframeEffect()->AnimationsPlayBackRateMultiplier();
  }
  return 1.0;
}

double Animation::PlaybackRateInternal() const {
  return mPlaybackRate * AnimationsPlayBackRateMultiplier();
}

}  // namespace mozilla::dom
