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

#include "mozilla/AnimationEventDispatcher.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/CSSAnimationBinding.h"
#include "mozilla/dom/KeyframeEffectBinding.h"
#include "nsPresContext.h"

namespace mozilla::dom {

using AnimationPhase = ComputedTiming::AnimationPhase;

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

void CSSAnimation::SetEffect(AnimationEffect* aEffect) {
  Animation::SetEffect(aEffect);

  PropertiesWillSetFromJS(CSSAnimationProperties::Effect);
}

void CSSAnimation::SetStartTime(const Nullable<CSSNumberish>& aStartTime,
                                ErrorResult& aRv) {
  // Note that we always compare with the paused state since for the purposes
  // of determining if play control is being overridden or not, we want to
  // treat the finished state as running.
  bool wasPaused = PlayState() == AnimationPlayState::Paused;

  Animation::SetStartTime(aStartTime, aRv);
  if (aRv.Failed()) {
    return;
  }

  bool isPaused = PlayState() == AnimationPlayState::Paused;

  if (wasPaused != isPaused) {
    PropertiesWillSetFromJS(CSSAnimationProperties::PlayState);
  }
}

mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) {
  FlushUnanimatedStyle();
  return Animation::GetReady(aRv);
}

void CSSAnimation::Reverse(ErrorResult& aRv) {
  // As with CSSAnimation::SetStartTime, we're really only interested in the
  // paused state.
  bool wasPaused = PlayState() == AnimationPlayState::Paused;

  Animation::Reverse(aRv);
  if (aRv.Failed()) {
    return;
  }

  bool isPaused = PlayState() == AnimationPlayState::Paused;

  if (wasPaused != isPaused) {
    PropertiesWillSetFromJS(CSSAnimationProperties::PlayState);
  }
}

AnimationPlayState CSSAnimation::PlayStateFromJS() const {
  // Flush style to ensure that any properties controlling animation state
  // (e.g. animation-play-state) are fully updated.
  FlushUnanimatedStyle();
  return Animation::PlayStateFromJS();
}

bool CSSAnimation::PendingFromJS() const {
  // Flush style since, for example, if the animation-play-state was just
  // changed its possible we should now be pending.
  FlushUnanimatedStyle();
  return Animation::PendingFromJS();
}

void CSSAnimation::PlayFromJS(ErrorResult& aRv) {
  // Note that flushing style below might trigger calls to
  // PlayFromStyle()/PauseFromStyle() on this object.
  FlushUnanimatedStyle();
  Animation::PlayFromJS(aRv);
  if (aRv.Failed()) {
    return;
  }

  PropertiesWillSetFromJS(CSSAnimationProperties::PlayState);
}

void CSSAnimation::PauseFromJS(ErrorResult& aRv) {
  Animation::PauseFromJS(aRv);
  if (aRv.Failed()) {
    return;
  }

  PropertiesWillSetFromJS(CSSAnimationProperties::PlayState);
}

void CSSAnimation::PlayFromStyle() {
  ErrorResult rv;
  Animation::Play(rv, Animation::LimitBehavior::Continue);
  // play() should not throw when LimitBehavior is Continue
  MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing animation");
}

void CSSAnimation::PauseFromStyle() {
  ErrorResult rv;
  Animation::Pause(rv);
  // pause() should only throw when *all* of the following conditions are true:
  // - we are in the idle state, and
  // - we have a negative playback rate, and
  // - we have an infinitely repeating animation
  // The first two conditions will never happen under regular style processing
  // but could happen if an author made modifications to the Animation object
  // and then updated animation-play-state. It's an unusual case and there's
  // no obvious way to pass on the exception information so we just silently
  // fail for now.
  if (rv.Failed()) {
    NS_WARNING("Unexpected exception pausing animation - silently failing");
  }
}

void CSSAnimation::Tick(TickState& aState) {
  Animation::Tick(aState);
  QueueEvents();
}

int32_t CSSAnimation::CompareCompositeOrder(
    const CSSAnimation& aOther, nsContentUtils::NodeIndexCache& aCache) const {
  MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(),
             "Should only be called for CSS animations that are sorted "
             "as CSS animations (i.e. tied to CSS markup)");

  // 0. Object-equality case
  if (&aOther == this) {
    return 0;
  }

  // 1. Sort by document order
  if (!mOwningElement.Equals(aOther.mOwningElement)) {
    return mOwningElement.Compare(aOther.mOwningElement, aCache);
  }

  // 2. (Same element and pseudo): Sort by position in animation-name
  MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex);
  return mAnimationIndex < aOther.mAnimationIndex ? -1 : 1;
}

void CSSAnimation::QueueEvents(const StickyTimeDuration& aActiveTime) {
  // If the animation is pending, we ignore animation events until we finish
  // pending.
  if (mPendingState != PendingState::NotPending) {
    return;
  }

  // CSS animations dispatch events at their owning element. This allows
  // script to repurpose a CSS animation to target a different element,
  // to use a group effect (which has no obvious "target element"), or
  // to remove the animation effect altogether whilst still getting
  // animation events.
  //
  // It does mean, however, that for a CSS animation that has no owning
  // element (e.g. it was created using the CSSAnimation constructor or
  // disassociated from CSS) no events are fired. If it becomes desirable
  // for these animations to still fire events we should spec the concept
  // of the "original owning element" or "event target" and allow script
  // to set it when creating a CSSAnimation object.
  if (!mOwningElement.ShouldFireEvents()) {
    return;
  }

  nsPresContext* presContext = mOwningElement.GetPresContext();
  if (!presContext) {
    return;
  }

  uint64_t currentIteration = 0;
  ComputedTiming::AnimationPhase currentPhase;
  StickyTimeDuration intervalStartTime;
  StickyTimeDuration intervalEndTime;
  StickyTimeDuration iterationStartTime;

  if (!mEffect) {
    currentPhase =
        GetAnimationPhaseWithoutEffect<ComputedTiming::AnimationPhase>(*this);
    if (currentPhase == mPreviousPhase) {
      return;
    }
  } else {
    ComputedTiming computedTiming = mEffect->GetComputedTiming();
    currentPhase = computedTiming.mPhase;
    currentIteration = computedTiming.mCurrentIteration;
    if (currentPhase == mPreviousPhase &&
        currentIteration == mPreviousIteration) {
      return;
    }
    intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration);
    intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration);

    uint64_t iterationBoundary = mPreviousIteration > currentIteration
                                     ? currentIteration + 1
                                     : currentIteration;
    double multiplier = iterationBoundary - computedTiming.mIterationStart;
    if (multiplier != 0.0) {
      iterationStartTime = computedTiming.mDuration.MultDouble(multiplier);
    }
  }

  TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
  TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
  TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime);

  AutoTArray<AnimationEventInfo, 2> events;

  auto appendAnimationEvent = [&](EventMessage aMessage,
                                  const StickyTimeDuration& aElapsedTime,
                                  const TimeStamp& aScheduledEventTimeStamp) {
    double elapsedTime = aElapsedTime.ToSeconds();
    if (aMessage == eAnimationCancel) {
      // 0 is an inappropriate value for this callsite. What we need to do is
      // use a single random value for all increasing times reportable.
      // That is to say, whenever elapsedTime goes negative (because an
      // animation restarts, something rewinds the animation, or otherwise)
      // a new random value for the mix-in must be generated.
      elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(
          elapsedTime, 0, mRTPCallerType);
    }
    events.AppendElement(AnimationEventInfo(
        mAnimationName, mOwningElement.Target(), aMessage, elapsedTime,
        mAnimationIndex, aScheduledEventTimeStamp, this));
  };

  // Handle cancel event first
  if ((mPreviousPhase != AnimationPhase::Idle &&
       mPreviousPhase != AnimationPhase::After) &&
      currentPhase == AnimationPhase::Idle) {
    appendAnimationEvent(eAnimationCancel, aActiveTime,
                         GetTimelineCurrentTimeAsTimeStamp());
  }

  switch (mPreviousPhase) {
    case AnimationPhase::Idle:
    case AnimationPhase::Before:
      if (currentPhase == AnimationPhase::Active) {
        appendAnimationEvent(eAnimationStart, intervalStartTime,
                             startTimeStamp);
      } else if (currentPhase == AnimationPhase::After) {
        appendAnimationEvent(eAnimationStart, intervalStartTime,
                             startTimeStamp);
        appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
      }
      break;
    case AnimationPhase::Active:
      if (currentPhase == AnimationPhase::Before) {
        appendAnimationEvent(eAnimationEnd, intervalStartTime, startTimeStamp);
      } else if (currentPhase == AnimationPhase::Active) {
        // The currentIteration must have changed or element we would have
        // returned early above.
        MOZ_ASSERT(currentIteration != mPreviousIteration);
        appendAnimationEvent(eAnimationIteration, iterationStartTime,
                             iterationTimeStamp);
      } else if (currentPhase == AnimationPhase::After) {
        appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
      }
      break;
    case AnimationPhase::After:
      if (currentPhase == AnimationPhase::Before) {
        appendAnimationEvent(eAnimationStart, intervalEndTime, startTimeStamp);
        appendAnimationEvent(eAnimationEnd, intervalStartTime, endTimeStamp);
      } else if (currentPhase == AnimationPhase::Active) {
        appendAnimationEvent(eAnimationStart, intervalEndTime, endTimeStamp);
      }
      break;
  }
  mPreviousPhase = currentPhase;
  mPreviousIteration = currentIteration;

  if (!events.IsEmpty()) {
    presContext->AnimationEventDispatcher()->QueueEvents(std::move(events));
  }
}

void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag,
                                SyncNotifyFlag aSyncNotifyFlag) {
  if (mNeedsNewAnimationIndexWhenRun &&
      PlayState() != AnimationPlayState::Idle) {
    mAnimationIndex = sNextAnimationIndex++;
    mNeedsNewAnimationIndexWhenRun = false;
  }

  Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag);
}

/////////////////////// CSSAnimationKeyframeEffect ////////////////////////

void CSSAnimationKeyframeEffect::GetTiming(EffectTiming& aRetVal) const {
  MaybeFlushUnanimatedStyle();
  KeyframeEffect::GetTiming(aRetVal);
}

void CSSAnimationKeyframeEffect::GetComputedTimingAsDict(
    ComputedEffectTiming& aRetVal) const {
  MaybeFlushUnanimatedStyle();
  KeyframeEffect::GetComputedTimingAsDict(aRetVal);
}

void CSSAnimationKeyframeEffect::UpdateTiming(
    const OptionalEffectTiming& aTiming, ErrorResult& aRv) {
  KeyframeEffect::UpdateTiming(aTiming, aRv);

  if (aRv.Failed()) {
    return;
  }

  if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
    CSSAnimationProperties updatedProperties = CSSAnimationProperties::None;
    if (aTiming.mDuration.WasPassed()) {
      updatedProperties |= CSSAnimationProperties::Duration;
    }
    if (aTiming.mIterations.WasPassed()) {
      updatedProperties |= CSSAnimationProperties::IterationCount;
    }
    if (aTiming.mDirection.WasPassed()) {
      updatedProperties |= CSSAnimationProperties::Direction;
    }
    if (aTiming.mDelay.WasPassed()) {
      updatedProperties |= CSSAnimationProperties::Delay;
    }
    if (aTiming.mFill.WasPassed()) {
      updatedProperties |= CSSAnimationProperties::FillMode;
    }

    cssAnimation->PropertiesWillSetFromJS(updatedProperties);
  }
}

void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext,
                                              JS::Handle<JSObject*> aKeyframes,
                                              ErrorResult& aRv) {
  // SetKeyframes will trigger the keyframe generation (for missing 0% and
  // 100%), so we set this flag to avoid generating the keyframes if they come
  // from JS.
  mIgnoreKeyframesGeneration = true;
  KeyframeEffect::SetKeyframes(aContext, aKeyframes, aRv);

  if (aRv.Failed()) {
    mIgnoreKeyframesGeneration = false;
    return;
  }

  if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
    cssAnimation->PropertiesWillSetFromJS(CSSAnimationProperties::Keyframes);
  }
}

void CSSAnimationKeyframeEffect::SetComposite(
    const CompositeOperation& aComposite) {
  KeyframeEffect::SetComposite(aComposite);

  if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
    cssAnimation->PropertiesWillSetFromJS(CSSAnimationProperties::Composition);
  }
}

class ComputedOffsetComparator {
 public:
  static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) {
    // Keep the specified order if any of the keyframes have no computed offset.
    return !std::isnan(aLhs.mComputedOffset) &&
           !std::isnan(aRhs.mComputedOffset) &&
           aLhs.mComputedOffset < aRhs.mComputedOffset;
  }
};

enum class TargetKeyframe : uint8_t {
  Initial,
  Final,
};
static Keyframe* GetOrCreateInitialOrFinalComputedKeyframe(
    nsTArray<Keyframe>& aPercentageKeyframes,
    nsTArray<Keyframe>& aRangeKeyframes, const TargetKeyframe aTargetKeyframe,
    const StyleComputedTimingFunction& aDefaultTimingFunction,
    const CompositeOperationOrAuto aDefaultComposite) {
  const double targetOffset =
      aTargetKeyframe == TargetKeyframe::Initial ? 0.0 : 1.0;
  auto IsKeyframeMatched = [&](const Keyframe& aKeyframe) {
    return aKeyframe.mComputedOffset == targetOffset &&
           (aKeyframe.mComposite == CompositeOperationOrAuto::Auto ||
            aKeyframe.mComposite == aDefaultComposite) &&
           (aKeyframe.mTimingFunction
                ? *aKeyframe.mTimingFunction == aDefaultTimingFunction
                : aDefaultTimingFunction.IsLinearKeyword());
  };

  // 1. Search the offset in |aRangeKeyframes| first, in the reverse order. We
  //    search |aRangeKeyframes| first because we perfer the later keyframe in
  //    the used order, which breaks the tie with the computed order. And range
  //    offsets are always put after percentage offsets in the computed order.
  //
  // Note: We intentionally search in the reverse order because we prefer the
  // later keyframe if mulitple keyframes have 0% or 100% (with the matched
  // easing and composite). It should happen only in keyframes with
  // <timeline-range-offset> offsets because we don't group them when converting
  // them from Servo |KeyframesStep| into Gecko |Keyframe|, in
  // Servo_StyleSet_GetKeyframesForName(), and intentionally keep those
  // keyframes unsorted (i.e. the specified order).
  // e.g.
  // cover 0% {...}
  // entry 0% {...}
  // Both of them have computed offset 0%. Since we keep |mKeyframes| with
  // <timeline-range-offset> offsets unsorted, so we can simply choose the last
  // one when breaking a tie.
  //
  // This is spec'ed especially for 100%. For 0%, the spec doesnt' mention that
  // so we use the same rule for now.
  // https://github.com/w3c/csswg-drafts/issues/14281
  for (auto& keyframe : Reversed(aRangeKeyframes)) {
    MOZ_ASSERT(keyframe.mOffset);

    // Skip the keyframe with unresolved computed offset.
    if (std::isnan(keyframe.mComputedOffset)) {
      continue;
    }

    if (IsKeyframeMatched(keyframe)) {
      return &keyframe;
    }

    // |aRangeKeyframes| is sorted by |keyframe.mComputedOffset|, so we can
    // break early.
    if (keyframe.mComputedOffset < targetOffset) {
      break;
    }
  }

  // 2. Search the offset in |aPercentageKeyframes|. We group the keyframes with
  //    <percentage> offsets already so we shouldn't have mulitple matches.
  //
  // Note: |insertPosition| is used to find the last 0% or 100% keyframe. We
  // would like to insert the missing keyframe after it.
  // e.g.
  // 0% { animation-timing-function: linear; }
  // 50% { width: 100px; }
  // 100% { animation-composition: add; }
  // We have to generate the keyframes like this:
  // [0] { offset: 0.0, easing: 'linear', composite: 'auto' }
  // [1] { offset: 0.0, easing: 'ease', composite: 'replace' } // generated 0%
  // [2] { offset: 0.5, easing: 'ease', composite: 'auto' }
  // [3] { offset: 1.0, easing: 'ease', composite: 'add' }
  // [4] { offset: 1.0, easing: 'ease', composite: 'replace' } // generated 100%
  // So |insertPosition| for 0% is 1, and for 100% is 4.
  size_t insertPosition = 0;
  for (auto& keyframe : aPercentageKeyframes) {
    MOZ_ASSERT(keyframe.mOffset);
    MOZ_ASSERT(!std::isnan(keyframe.mComputedOffset));

    if (IsKeyframeMatched(keyframe)) {
      return &keyframe;
    }

    if (keyframe.mComputedOffset > targetOffset) {
      break;
    }
    ++insertPosition;
  }

  // Now this is a missing 0%/100% keyframe. Let's generate it.
  Keyframe* newKeyframe = aPercentageKeyframes.InsertElementAt(insertPosition);
  newKeyframe->mOffset.emplace(
      Keyframe::OffsetType::PercentageOffset(targetOffset));
  newKeyframe->mComputedOffset = targetOffset;
  if (!aDefaultTimingFunction.IsLinearKeyword()) {
    newKeyframe->mTimingFunction.emplace(aDefaultTimingFunction);
  }
  newKeyframe->mComposite = aDefaultComposite;
  return newKeyframe;
}

// The step 3 (Generate Initial and Final Frames) in
// https://drafts.csswg.org/css-animations-2/#keyframe-processing
//
// Note: we use "computed keyframes" here because we expect those keyframe
// offsets have been resolved (so we have resolved computed offsets here for the
// active timeline). The only difference from spec is that we don't compute the
// property values here since the caller will do that. Perhaps we could merge
// them together in the future.
// https://drafts.csswg.org/web-animations-1/#computed-keyframes
bool CSSAnimationKeyframeEffect::GetComputedKeyframes(
    nsTArray<Keyframe>& aKeyframes) const {
  // Check if we do need to do this. If it is overrideen by JS, we should skip
  // this function and the caller should just use the specified keyframes.
  if (mIgnoreKeyframesGeneration) {
    return false;
  }

  // FIXME: Perhaps we could cache |allProperties| for keyframes with
  // <percentage> offsets. (The properties of keyframes with
  // <timeline-range-offset> offsets need to be checked everytime
  // unfortunately.)
  AnimatedPropertyIDSet allProperties;
  AnimatedPropertyIDSet fromProperties;
  AnimatedPropertyIDSet toProperties;

  // Add 2 more elements for the insertion of 0% and 100% if any.
  nsTArray<Keyframe> percentageKeyframes(mKeyframes.Length() + 2);
  nsTArray<Keyframe> rangeKeyframes(mKeyframes.Length());

  // Iterate keyframes and collect properties first. We have to append the
  // propeties to the 0% or 100% keyframes if needed. We merged keyframes and
  // converted the properties into physical longhands for the specified
  // keyframes already, so we can just collect the propertis information here.
  for (const Keyframe& keyframe : mKeyframes) {
    MOZ_ASSERT(keyframe.mOffset);
    if (keyframe.mOffset->IsPercentageOffset()) {
      percentageKeyframes.AppendElement(keyframe);
    } else {
      rangeKeyframes.AppendElement(keyframe);
    }

    if (std::isnan(keyframe.mComputedOffset)) {
      continue;
    }

    AnimatedPropertyIDSet currentProperties;
    for (const auto& pair : keyframe.mPropertyValues) {
      currentProperties.AddProperty(pair.mProperty);
    }
    allProperties.AddProperties(currentProperties);

    // Note that the automatic from (0%) and to (100%) keyframes are only
    // generated for properties that don’t have keyframes at or earlier than 0%
    // or at or after 100% (respectively).
    // https://drafts.csswg.org/scroll-animations-1/#named-range-keyframes
    //
    // So we have to collect the properties with offsets <= 0 and offsets >= 1,
    // no matter what keyframe-specific timing function and keyframe-specific
    // composite are used.
    if (keyframe.mComputedOffset <= 0.0) {
      fromProperties.AddProperties(currentProperties);
    } else if (keyframe.mComputedOffset >= 1.0) {
      toProperties.AddProperties(currentProperties);
    }
  }

  // We keep the specified order if all of them are unresolved. Otherwise, sort
  // them with computed offsets to get the used order. We do this for finding
  // and generating the initial and final keyframes (since we need the used
  // order to search the keyframes below).
  //
  // Also, in getKeyframes(), other browsers show the keyframes with
  // <timeline-range-offset> offsets in the order of the computed offsets. This
  // is vague in the spec after we introduced <timeline-range-offset>, so we
  // just match the behavior of other browsers here.
  rangeKeyframes.StableSort(ComputedOffsetComparator());

  // The default composite used to search to 0% and 100% keyframes below.
  const dom::CompositeOperationOrAuto defaultComposite = [&]() {
    switch (mDefaultComposite) {
      case CompositeOperation::Replace:
        return dom::CompositeOperationOrAuto::Replace;
      case CompositeOperation::Add:
        return dom::CompositeOperationOrAuto::Add;
      case CompositeOperation::Accumulate:
        return dom::CompositeOperationOrAuto::Accumulate;
    }
    // The initial value is replace.
    return dom::CompositeOperationOrAuto::Replace;
  }();

  // Generate Initial and Final Frames:
  // 1. Find or create the initial keyframe, a keyframe with a keyframe offset
  //    of 0%, default timing function as its keyframe timing function, and
  //    default composite as its keyframe composite.
  // 2. For any property in animated properties that is not otherwise present
  //    in a keyframe with an offset of 0% or one that would be positioned
  //    earlier in the used keyframe order, add the computed value of that
  //    property on element to initial keyframe’s keyframe values.
  // 3. If initial keyframe’s keyframe values is not empty, prepend initial
  //    keyframe to keyframes. (Note: We add it to keyframes after the last
  //    keyframe with offset 0%.)

  const bool isEmptyKeyframes =
      percentageKeyframes.IsEmpty() && rangeKeyframes.IsEmpty();
  auto appendProperties = [&](Keyframe& aKeyframe,
                              const AnimatedPropertyIDSet& aCurrentProperties) {
    auto& propertyValues = aKeyframe.mPropertyValues;
    for (const auto& property : allProperties) {
      if (aCurrentProperties.HasProperty(property)) {
        continue;
      }
      // The nullptr of |mServoDeclarationBlock| means we use the base values,
      // i.e. the value generated for that property by finding the computed
      // value for that property in the absence of animations.
      propertyValues.AppendElement(PropertyValuePair{property, nullptr});
    }
  };
  // Check the equality of two property sets. Note that |fromProperties| must be
  // a subset of |allProperties|.
  if (!allProperties.IsSubsetOf(fromProperties) && !isEmptyKeyframes) {
    Keyframe* fromKeyframe = GetOrCreateInitialOrFinalComputedKeyframe(
        percentageKeyframes, rangeKeyframes, TargetKeyframe::Initial,
        mDefaultTimingFunction, defaultComposite);
    MOZ_ASSERT(fromKeyframe);
    appendProperties(*fromKeyframe, fromProperties);
  }

  // 4. Repeat for final keyframe, using an offset of 100%, considering
  // keyframes positioned later in the used keyframe order, and appending to
  // keyframes. Note that |toProperties| must be a subset of |allProperties|.
  if (!allProperties.IsSubsetOf(toProperties) && !isEmptyKeyframes) {
    Keyframe* toKeyframe = GetOrCreateInitialOrFinalComputedKeyframe(
        percentageKeyframes, rangeKeyframes, TargetKeyframe::Final,
        mDefaultTimingFunction, defaultComposite);
    MOZ_ASSERT(toKeyframe);
    appendProperties(*toKeyframe, toProperties);
  }

  aKeyframes = std::move(percentageKeyframes);
  if (!rangeKeyframes.IsEmpty()) {
    aKeyframes.AppendElements(std::move(rangeKeyframes));
  }
  return true;
}

void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const {
  if (!GetOwningCSSAnimation()) {
    return;
  }

  if (dom::Document* doc = GetRenderedDocument()) {
    doc->FlushPendingNotifications(
        ChangesToFlush(FlushType::Style, false /* flush animations */, false));
  }
}

}  // namespace mozilla::dom
