/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* Code to start and animate CSS transitions. */

#include "nsTransitionManager.h"
#include "nsAnimationManager.h"
#include "mozilla/dom/CSSTransitionBinding.h"

#include "nsIContent.h"
#include "nsContentUtils.h"
#include "nsStyleContext.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/TimeStamp.h"
#include "nsRefreshDriver.h"
#include "nsRuleProcessorData.h"
#include "nsRuleWalker.h"
#include "nsCSSPropertyIDSet.h"
#include "mozilla/EffectCompositor.h"
#include "mozilla/EffectSet.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/ServoBindings.h"
#include "mozilla/StyleAnimationValue.h"
#include "mozilla/dom/DocumentTimeline.h"
#include "mozilla/dom/Element.h"
#include "nsIFrame.h"
#include "Layers.h"
#include "FrameLayerBuilder.h"
#include "nsCSSProps.h"
#include "nsCSSPseudoElements.h"
#include "nsDisplayList.h"
#include "nsStyleChangeList.h"
#include "nsStyleSet.h"
#include "mozilla/RestyleManager.h"
#include "mozilla/RestyleManagerInlines.h"
#include "nsDOMMutationObserver.h"

using mozilla::TimeStamp;
using mozilla::TimeDuration;
using mozilla::dom::Animation;
using mozilla::dom::AnimationPlayState;
using mozilla::dom::CSSTransition;
using mozilla::dom::KeyframeEffectReadOnly;

using namespace mozilla;
using namespace mozilla::css;

namespace {
struct TransitionEventParams {
  EventMessage mMessage;
  StickyTimeDuration mElapsedTime;
  TimeStamp mTimeStamp;
};
} // anonymous namespace

double
ElementPropertyTransition::CurrentValuePortion() const
{
  MOZ_ASSERT(!GetLocalTime().IsNull(),
             "Getting the value portion of an animation that's not being "
             "sampled");

  // Transitions use a fill mode of 'backwards' so GetComputedTiming will
  // never return a null time progress due to being *before* the animation
  // interval. However, it might be possible that we're behind on flushing
  // causing us to get called *after* the animation interval. So, just in
  // case, we override the fill mode to 'both' to ensure the progress
  // is never null.
  TimingParams timingToUse = SpecifiedTiming();
  timingToUse.SetFill(dom::FillMode::Both);
  ComputedTiming computedTiming = GetComputedTiming(&timingToUse);

  MOZ_ASSERT(!computedTiming.mProgress.IsNull(),
             "Got a null progress for a fill mode of 'both'");
  MOZ_ASSERT(mKeyframes.Length() == 2,
             "Should have two animation keyframes for a transition");
  return ComputedTimingFunction::GetPortion(mKeyframes[0].mTimingFunction,
                                            computedTiming.mProgress.Value(),
                                            computedTiming.mBeforeFlag);
}

void
ElementPropertyTransition::UpdateStartValueFromReplacedTransition()
{
  if (!mReplacedTransition) {
    return;
  }
  MOZ_ASSERT(nsCSSProps::PropHasFlags(TransitionProperty(),
                                      CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR),
             "The transition property should be able to be run on the "
             "compositor");
  MOZ_ASSERT(mTarget && mTarget->mElement->OwnerDoc(),
             "We should have a valid document at this moment");

  dom::DocumentTimeline* timeline = mTarget->mElement->OwnerDoc()->Timeline();
  ComputedTiming computedTiming = GetComputedTimingAt(
    dom::CSSTransition::GetCurrentTimeAt(*timeline,
                                         TimeStamp::Now(),
                                         mReplacedTransition->mStartTime,
                                         mReplacedTransition->mPlaybackRate),
    mReplacedTransition->mTiming,
    mReplacedTransition->mPlaybackRate);

  if (!computedTiming.mProgress.IsNull()) {
    double valuePosition =
      ComputedTimingFunction::GetPortion(mReplacedTransition->mTimingFunction,
                                         computedTiming.mProgress.Value(),
                                         computedTiming.mBeforeFlag);

    MOZ_ASSERT(mProperties.Length() == 1 &&
               mProperties[0].mSegments.Length() == 1,
               "The transition should have one property and one segment");
    MOZ_ASSERT(mKeyframes.Length() == 2,
               "Transitions should have exactly two animation keyframes");
    MOZ_ASSERT(mKeyframes[0].mPropertyValues.Length() == 1,
               "Transitions should have exactly one property in their first "
               "frame");

    const AnimationValue& replacedFrom = mReplacedTransition->mFromValue;
    const AnimationValue& replacedTo = mReplacedTransition->mToValue;
    AnimationValue startValue;
    if (mDocument->IsStyledByServo()) {
      startValue.mServo =
        Servo_AnimationValues_Interpolate(replacedFrom.mServo,
                                          replacedTo.mServo,
                                          valuePosition).Consume();
      if (startValue.mServo) {
        mKeyframes[0].mPropertyValues[0].mServoDeclarationBlock =
          Servo_AnimationValue_Uncompute(startValue.mServo).Consume();
        mProperties[0].mSegments[0].mFromValue = Move(startValue);
      }
    } else if (StyleAnimationValue::Interpolate(mProperties[0].mProperty,
                                                replacedFrom.mGecko,
                                                replacedTo.mGecko,
                                                valuePosition,
                                                startValue.mGecko)) {
      nsCSSValue cssValue;
      DebugOnly<bool> uncomputeResult =
        StyleAnimationValue::UncomputeValue(mProperties[0].mProperty,
                                            startValue.mGecko,
                                            cssValue);
      MOZ_ASSERT(uncomputeResult, "UncomputeValue should not fail");
      mKeyframes[0].mPropertyValues[0].mValue = cssValue;

      mProperties[0].mSegments[0].mFromValue = Move(startValue);
    }
  }

  mReplacedTransition.reset();
}

////////////////////////// CSSTransition ////////////////////////////

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

void
CSSTransition::GetTransitionProperty(nsString& aRetVal) const
{
  MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty,
             "Transition Property should be initialized");
  aRetVal =
    NS_ConvertUTF8toUTF16(nsCSSProps::GetStringValue(mTransitionProperty));
}

AnimationPlayState
CSSTransition::PlayStateFromJS() const
{
  FlushStyle();
  return Animation::PlayStateFromJS();
}

void
CSSTransition::PlayFromJS(ErrorResult& aRv)
{
  FlushStyle();
  Animation::PlayFromJS(aRv);
}

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

  Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag);
}

void
CSSTransition::QueueEvents(StickyTimeDuration aActiveTime)
{
  if (!mOwningElement.IsSet()) {
    return;
  }

  dom::Element* owningElement;
  CSSPseudoElementType owningPseudoType;
  mOwningElement.GetElement(owningElement, owningPseudoType);
  MOZ_ASSERT(owningElement, "Owning element should be set");

  nsPresContext* presContext =
    nsContentUtils::GetContextForContent(owningElement);
  if (!presContext) {
    return;
  }

  const StickyTimeDuration zeroDuration = StickyTimeDuration();

  TransitionPhase currentPhase;
  StickyTimeDuration intervalStartTime;
  StickyTimeDuration intervalEndTime;

  if (!mEffect) {
    currentPhase = GetAnimationPhaseWithoutEffect<TransitionPhase>(*this);
  } else {
    ComputedTiming computedTiming = mEffect->GetComputedTiming();

    currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase);
    intervalStartTime =
      std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().Delay()),
                        computedTiming.mActiveDuration), zeroDuration);
    intervalEndTime =
      std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().Delay()),
                        computedTiming.mActiveDuration), zeroDuration);
  }

  // TimeStamps to use for ordering the events when they are dispatched. We
  // use a TimeStamp so we can compare events produced by different elements,
  // perhaps even with different timelines.
  // The zero timestamp is for transitionrun events where we ignore the delay
  // for the purpose of ordering events.
  TimeStamp zeroTimeStamp  = AnimationTimeToTimeStamp(zeroDuration);
  TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
  TimeStamp endTimeStamp   = ElapsedTimeToTimeStamp(intervalEndTime);

  if (mPendingState != PendingState::NotPending &&
      (mPreviousTransitionPhase == TransitionPhase::Idle ||
       mPreviousTransitionPhase == TransitionPhase::Pending))
  {
    currentPhase = TransitionPhase::Pending;
  }

  AutoTArray<TransitionEventParams, 3> events;

  // Handle cancel events first
  if ((mPreviousTransitionPhase != TransitionPhase::Idle &&
       mPreviousTransitionPhase != TransitionPhase::After) &&
      currentPhase == TransitionPhase::Idle) {
    TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(aActiveTime);
    events.AppendElement(TransitionEventParams{ eTransitionCancel,
                                                aActiveTime,
                                                activeTimeStamp });
  }

  // All other events
  switch (mPreviousTransitionPhase) {
    case TransitionPhase::Idle:
      if (currentPhase == TransitionPhase::Pending ||
          currentPhase == TransitionPhase::Before) {
        events.AppendElement(TransitionEventParams{ eTransitionRun,
                                                    intervalStartTime,
                                                    zeroTimeStamp });
      } else if (currentPhase == TransitionPhase::Active) {
        events.AppendElement(TransitionEventParams{ eTransitionRun,
                                                    intervalStartTime,
                                                    zeroTimeStamp });
        events.AppendElement(TransitionEventParams{ eTransitionStart,
                                                    intervalStartTime,
                                                    startTimeStamp });
      } else if (currentPhase == TransitionPhase::After) {
        events.AppendElement(TransitionEventParams{ eTransitionRun,
                                                    intervalStartTime,
                                                    zeroTimeStamp });
        events.AppendElement(TransitionEventParams{ eTransitionStart,
                                                    intervalStartTime,
                                                    startTimeStamp });
        events.AppendElement(TransitionEventParams{ eTransitionEnd,
                                                    intervalEndTime,
                                                    endTimeStamp });
      }
      break;

    case TransitionPhase::Pending:
    case TransitionPhase::Before:
      if (currentPhase == TransitionPhase::Active) {
        events.AppendElement(TransitionEventParams{ eTransitionStart,
                                                    intervalStartTime,
                                                    startTimeStamp });
      } else if (currentPhase == TransitionPhase::After) {
        events.AppendElement(TransitionEventParams{ eTransitionStart,
                                                    intervalStartTime,
                                                    startTimeStamp });
        events.AppendElement(TransitionEventParams{ eTransitionEnd,
                                                    intervalEndTime,
                                                    endTimeStamp });
      }
      break;

    case TransitionPhase::Active:
      if (currentPhase == TransitionPhase::After) {
        events.AppendElement(TransitionEventParams{ eTransitionEnd,
                                                    intervalEndTime,
                                                    endTimeStamp });
      } else if (currentPhase == TransitionPhase::Before) {
        events.AppendElement(TransitionEventParams{ eTransitionEnd,
                                                    intervalStartTime,
                                                    startTimeStamp });
      }
      break;

    case TransitionPhase::After:
      if (currentPhase == TransitionPhase::Active) {
        events.AppendElement(TransitionEventParams{ eTransitionStart,
                                                    intervalEndTime,
                                                    startTimeStamp });
      } else if (currentPhase == TransitionPhase::Before) {
        events.AppendElement(TransitionEventParams{ eTransitionStart,
                                                    intervalEndTime,
                                                    startTimeStamp });
        events.AppendElement(TransitionEventParams{ eTransitionEnd,
                                                    intervalStartTime,
                                                    endTimeStamp });
      }
      break;
  }
  mPreviousTransitionPhase = currentPhase;

  nsTransitionManager* manager = presContext->TransitionManager();
  for (const TransitionEventParams& evt : events) {
    manager->QueueEvent(TransitionEventInfo(owningElement, owningPseudoType,
                                            evt.mMessage,
                                            TransitionProperty(),
                                            evt.mElapsedTime,
                                            evt.mTimeStamp,
                                            this));
  }
}

void
CSSTransition::Tick()
{
  Animation::Tick();
  QueueEvents();
}

nsCSSPropertyID
CSSTransition::TransitionProperty() const
{
  MOZ_ASSERT(eCSSProperty_UNKNOWN != mTransitionProperty,
             "Transition property should be initialized");
  return mTransitionProperty;
}

AnimationValue
CSSTransition::ToValue() const
{
  MOZ_ASSERT(!mTransitionToValue.IsNull(),
             "Transition ToValue should be initialized");
  return mTransitionToValue;
}

bool
CSSTransition::HasLowerCompositeOrderThan(const CSSTransition& aOther) const
{
  MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(),
             "Should only be called for CSS transitions that are sorted "
             "as CSS transitions (i.e. tied to CSS markup)");

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

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

  // 2. (Same element and pseudo): Sort by transition generation
  if (mAnimationIndex != aOther.mAnimationIndex) {
    return mAnimationIndex < aOther.mAnimationIndex;
  }

  // 3. (Same transition generation): Sort by transition property
  return nsCSSProps::GetStringValue(TransitionProperty()) <
         nsCSSProps::GetStringValue(aOther.TransitionProperty());
}

/* static */ Nullable<TimeDuration>
CSSTransition::GetCurrentTimeAt(const dom::DocumentTimeline& aTimeline,
                                const TimeStamp& aBaseTime,
                                const TimeDuration& aStartTime,
                                double aPlaybackRate)
{
  Nullable<TimeDuration> result;

  Nullable<TimeDuration> timelineTime = aTimeline.ToTimelineTime(aBaseTime);
  if (!timelineTime.IsNull()) {
    result.SetValue((timelineTime.Value() - aStartTime)
                      .MultDouble(aPlaybackRate));
  }

  return result;
}

void
CSSTransition::SetEffectFromStyle(dom::AnimationEffectReadOnly* aEffect)
{
  Animation::SetEffectNoUpdate(aEffect);

  // Initialize transition property.
  ElementPropertyTransition* pt = aEffect ? aEffect->AsTransition() : nullptr;
  if (eCSSProperty_UNKNOWN == mTransitionProperty && pt) {
    mTransitionProperty = pt->TransitionProperty();
    mTransitionToValue = pt->ToValue();
  }
}

////////////////////////// nsTransitionManager ////////////////////////////

NS_IMPL_CYCLE_COLLECTION(nsTransitionManager, mEventDispatcher)

NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsTransitionManager, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsTransitionManager, Release)

static inline bool
ExtractNonDiscreteComputedValue(nsCSSPropertyID aProperty,
                                GeckoStyleContext* aStyleContext,
                                AnimationValue& aAnimationValue)
{
  return (nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_Discrete ||
          aProperty == eCSSProperty_visibility) &&
         StyleAnimationValue::ExtractComputedValue(aProperty, aStyleContext,
                                                   aAnimationValue.mGecko);
}

static inline bool
ExtractNonDiscreteComputedValue(nsCSSPropertyID aProperty,
                                const ServoStyleContext* aComputedStyle,
                                AnimationValue& aAnimationValue)
{
  if (Servo_Property_IsDiscreteAnimatable(aProperty) &&
      aProperty != eCSSProperty_visibility) {
    return false;
  }

  aAnimationValue.mServo =
    Servo_ComputedValues_ExtractAnimationValue(aComputedStyle,
                                               aProperty).Consume();
  return !!aAnimationValue.mServo;
}

void
nsTransitionManager::StyleContextChanged(dom::Element *aElement,
                                         GeckoStyleContext* aOldStyleContext,
                                         RefPtr<GeckoStyleContext>* aNewStyleContext /* inout */)
{
  GeckoStyleContext* newStyleContext = *aNewStyleContext;

  NS_PRECONDITION(aOldStyleContext->GetPseudo() == newStyleContext->GetPseudo(),
                  "pseudo type mismatch");

  if (mInAnimationOnlyStyleUpdate) {
    // If we're doing an animation-only style update, return, since the
    // purpose of an animation-only style update is to update only the
    // animation styles so that we don't consider style changes
    // resulting from changes in the animation time for starting a
    // transition.
    return;
  }

  if (!mPresContext->IsDynamic()) {
    // For print or print preview, ignore transitions.
    return;
  }

  if (aOldStyleContext->HasPseudoElementData() !=
      newStyleContext->HasPseudoElementData()) {
    // If the old style context and new style context differ in terms of
    // whether they're inside ::first-letter, ::first-line, or similar,
    // bail.  We can't hit this codepath for normal style changes
    // involving moving frames around the boundaries of these
    // pseudo-elements since we don't call StyleContextChanged from
    // ReparentStyleContext.  However, we can hit this codepath during
    // the handling of transitions that start across reframes.
    //
    // While there isn't an easy *perfect* way to handle this case, err
    // on the side of missing some transitions that we ought to have
    // rather than having bogus transitions that we shouldn't.
    //
    // We could consider changing this handling, although it's worth
    // thinking about whether the code below could do anything weird in
    // this case.
    return;
  }

  // NOTE: Things in this function (and ConsiderInitiatingTransition)
  // should never call PeekStyleData because we don't preserve gotten
  // structs across reframes.

  // Return sooner (before the startedAny check below) for the most
  // common case: no transitions specified or running.
  const nsStyleDisplay *disp = newStyleContext->StyleDisplay();
  CSSPseudoElementType pseudoType = newStyleContext->GetPseudoType();
  if (pseudoType != CSSPseudoElementType::NotPseudo) {
    if (pseudoType != CSSPseudoElementType::before &&
        pseudoType != CSSPseudoElementType::after) {
      return;
    }

    NS_ASSERTION((pseudoType == CSSPseudoElementType::before &&
                  aElement->IsGeneratedContentContainerForBefore()) ||
                 (pseudoType == CSSPseudoElementType::after &&
                  aElement->IsGeneratedContentContainerForAfter()),
                 "Unexpected aElement coming through");

    // Else the element we want to use from now on is the element the
    // :before or :after is attached to.
    aElement = aElement->GetParent()->AsElement();
  }

  CSSTransitionCollection* collection =
    CSSTransitionCollection::GetAnimationCollection(aElement, pseudoType);
  if (!collection &&
      disp->mTransitionPropertyCount == 1 &&
      disp->mTransitions[0].GetCombinedDuration() <= 0.0f) {
    return;
  }

  MOZ_ASSERT(mPresContext->RestyleManager()->IsGecko(),
             "ServoRestyleManager should not use nsTransitionManager "
             "for transitions");
  if (collection &&
      collection->mCheckGeneration ==
        mPresContext->RestyleManager()->GetAnimationGeneration()) {
    // When we start a new transition, we immediately post a restyle.
    // If the animation generation on the collection is current, that
    // means *this* is that restyle, since we bump the animation
    // generation on the restyle manager whenever there's a real style
    // change (i.e., one where mInAnimationOnlyStyleUpdate isn't true,
    // which causes us to return above).  Thus we shouldn't do anything.
    return;
  }
  if (newStyleContext->GetParent() &&
      newStyleContext->GetParent()->HasPseudoElementData()) {
    // Ignore transitions on things that inherit properties from
    // pseudo-elements.
    // FIXME (Bug 522599): Add tests for this.
    return;
  }

  NS_WARNING_ASSERTION(
    !mPresContext->EffectCompositor()->HasThrottledStyleUpdates(),
    "throttled animations not up to date");

  // Compute what the css-transitions spec calls the "after-change
  // style", which is the new style without any data from transitions,
  // but still inheriting from data that contains transitions that are
  // not stopping or starting right now.
  RefPtr<GeckoStyleContext> afterChangeStyle;
  if (collection) {
    MOZ_ASSERT(mPresContext->StyleSet()->IsGecko(),
               "ServoStyleSets should not use nsTransitionManager "
               "for transitions");
    nsStyleSet* styleSet = mPresContext->StyleSet()->AsGecko();
    afterChangeStyle =
      styleSet->ResolveStyleByRemovingAnimation(aElement, newStyleContext,
                                                eRestyle_CSSTransitions);
  } else {
    afterChangeStyle = newStyleContext;
  }

  nsAutoAnimationMutationBatch mb(aElement->OwnerDoc());

  DebugOnly<bool> startedAny = false;
  // We don't have to update transitions if display:none, although we will
  // cancel them after restyling.
  if (!afterChangeStyle->IsInDisplayNoneSubtree()) {
    startedAny = DoUpdateTransitions(disp,
                                     aElement,
                                     afterChangeStyle->GetPseudoType(),
                                     collection,
                                     aOldStyleContext->AsGecko(),
                                     afterChangeStyle->AsGecko());
  }

  MOZ_ASSERT(!startedAny || collection,
             "must have element transitions if we started any transitions");

  EffectCompositor::CascadeLevel cascadeLevel =
    EffectCompositor::CascadeLevel::Transitions;

  if (collection) {
    collection->UpdateCheckGeneration(mPresContext);
    mPresContext->EffectCompositor()->MaybeUpdateAnimationRule(aElement,
                                                               pseudoType,
                                                               cascadeLevel,
                                                               newStyleContext);
  }

  // We want to replace the new style context with the after-change style.
  *aNewStyleContext = afterChangeStyle;
  if (collection) {
    // Since we have transition styles, we have to undo this replacement.
    // The check of collection->mCheckGeneration against the restyle
    // manager's GetAnimationGeneration() will ensure that we don't go
    // through the rest of this function again when we do.
    mPresContext->EffectCompositor()->PostRestyleForAnimation(aElement,
                                                              pseudoType,
                                                              cascadeLevel);
  }
}

bool
nsTransitionManager::UpdateTransitions(
  dom::Element *aElement,
  CSSPseudoElementType aPseudoType,
  const ServoStyleContext* aOldStyle,
  const ServoStyleContext* aNewStyle)
{
  if (!mPresContext->IsDynamic()) {
    // For print or print preview, ignore transitions.
    return false;
  }

  CSSTransitionCollection* collection =
    CSSTransitionCollection::GetAnimationCollection(aElement, aPseudoType);
  const nsStyleDisplay *disp =
      aNewStyle->ComputedData()->GetStyleDisplay();
  return DoUpdateTransitions(disp,
                             aElement, aPseudoType,
                             collection,
                             aOldStyle, aNewStyle);
}

template<typename StyleType>
bool
nsTransitionManager::DoUpdateTransitions(
  const nsStyleDisplay* aDisp,
  dom::Element* aElement,
  CSSPseudoElementType aPseudoType,
  CSSTransitionCollection*& aElementTransitions,
  StyleType aOldStyle,
  StyleType aNewStyle)
{
  MOZ_ASSERT(aDisp, "Null nsStyleDisplay");
  MOZ_ASSERT(!aElementTransitions ||
             aElementTransitions->mElement == aElement, "Element mismatch");

  // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
  // I'll consider only the transitions from the number of items in
  // 'transition-property' on down, and later ones will override earlier
  // ones (tracked using |whichStarted|).
  bool startedAny = false;
  nsCSSPropertyIDSet whichStarted;
  for (uint32_t i = aDisp->mTransitionPropertyCount; i-- != 0; ) {
    const StyleTransition& t = aDisp->mTransitions[i];
    // Check the combined duration (combination of delay and duration)
    // first, since it defaults to zero, which means we can ignore the
    // transition.
    if (t.GetCombinedDuration() > 0.0f) {
      // We might have something to transition.  See if any of the
      // properties in question changed and are animatable.
      // FIXME: Would be good to find a way to share code between this
      // interpretation of transition-property and the one below.
      nsCSSPropertyID property = t.GetProperty();
      if (property == eCSSPropertyExtra_no_properties ||
          property == eCSSPropertyExtra_variable ||
          property == eCSSProperty_UNKNOWN) {
        // Nothing to do, but need to exclude this from cases below.
      } else if (property == eCSSPropertyExtra_all_properties) {
        for (nsCSSPropertyID p = nsCSSPropertyID(0);
             p < eCSSProperty_COUNT_no_shorthands;
             p = nsCSSPropertyID(p + 1)) {
          ConsiderInitiatingTransition(p, t, aElement, aPseudoType,
                                       aElementTransitions,
                                       aOldStyle, aNewStyle,
                                       &startedAny, &whichStarted);
        }
      } else if (nsCSSProps::IsShorthand(property)) {
        CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, property,
                                             CSSEnabledState::eForAllContent)
        {
          ConsiderInitiatingTransition(*subprop, t, aElement, aPseudoType,
                                       aElementTransitions,
                                       aOldStyle, aNewStyle,
                                       &startedAny, &whichStarted);
        }
      } else {
        ConsiderInitiatingTransition(property, t, aElement, aPseudoType,
                                     aElementTransitions,
                                     aOldStyle, aNewStyle,
                                     &startedAny, &whichStarted);
      }
    }
  }

  // Stop any transitions for properties that are no longer in
  // 'transition-property', including finished transitions.
  // Also stop any transitions (and remove any finished transitions)
  // for properties that just changed (and are still in the set of
  // properties to transition), but for which we didn't just start the
  // transition.  This can happen delay and duration are both zero, or
  // because the new value is not interpolable.
  // Note that we also do the latter set of work in
  // nsTransitionManager::PruneCompletedTransitions.
  if (aElementTransitions) {
    bool checkProperties =
      aDisp->mTransitions[0].GetProperty() != eCSSPropertyExtra_all_properties;
    nsCSSPropertyIDSet allTransitionProperties;
    if (checkProperties) {
      for (uint32_t i = aDisp->mTransitionPropertyCount; i-- != 0; ) {
        const StyleTransition& t = aDisp->mTransitions[i];
        // FIXME: Would be good to find a way to share code between this
        // interpretation of transition-property and the one above.
        nsCSSPropertyID property = t.GetProperty();
        if (property == eCSSPropertyExtra_no_properties ||
            property == eCSSPropertyExtra_variable ||
            property == eCSSProperty_UNKNOWN) {
          // Nothing to do, but need to exclude this from cases below.
        } else if (property == eCSSPropertyExtra_all_properties) {
          for (nsCSSPropertyID p = nsCSSPropertyID(0);
               p < eCSSProperty_COUNT_no_shorthands;
               p = nsCSSPropertyID(p + 1)) {
            allTransitionProperties.AddProperty(p);
          }
        } else if (nsCSSProps::IsShorthand(property)) {
          CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
              subprop, property, CSSEnabledState::eForAllContent) {
            allTransitionProperties.AddProperty(*subprop);
          }
        } else {
          allTransitionProperties.AddProperty(property);
        }
      }
    }

    OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
    size_t i = animations.Length();
    MOZ_ASSERT(i != 0, "empty transitions list?");
    AnimationValue currentValue;
    do {
      --i;
      CSSTransition* anim = animations[i];
          // properties no longer in 'transition-property'
      if ((checkProperties &&
           !allTransitionProperties.HasProperty(anim->TransitionProperty())) ||
          // properties whose computed values changed but for which we
          // did not start a new transition (because delay and
          // duration are both zero, or because the new value is not
          // interpolable); a new transition would have anim->ToValue()
          // matching currentValue
          !ExtractNonDiscreteComputedValue(anim->TransitionProperty(),
                                           aNewStyle, currentValue) ||
          currentValue != anim->ToValue()) {
        // stop the transition
        if (anim->HasCurrentEffect()) {
          EffectSet* effectSet =
            EffectSet::GetEffectSet(aElement, aPseudoType);
          if (effectSet) {
            effectSet->UpdateAnimationGeneration(mPresContext);
          }
        }
        anim->CancelFromStyle();
        animations.RemoveElementAt(i);
      }
    } while (i != 0);

    if (animations.IsEmpty()) {
      aElementTransitions->Destroy();
      aElementTransitions = nullptr;
    }
  }

  return startedAny;
}

static Keyframe&
AppendKeyframe(double aOffset,
               nsCSSPropertyID aProperty,
               AnimationValue&& aValue,
               nsTArray<Keyframe>& aKeyframes)
{
  Keyframe& frame = *aKeyframes.AppendElement();
  frame.mOffset.emplace(aOffset);

  if (aValue.mServo) {
    RefPtr<RawServoDeclarationBlock> decl =
      Servo_AnimationValue_Uncompute(aValue.mServo).Consume();
    frame.mPropertyValues.AppendElement(
      Move(PropertyValuePair(aProperty, Move(decl))));
  } else {
    nsCSSValue propertyValue;
    DebugOnly<bool> uncomputeResult =
      StyleAnimationValue::UncomputeValue(aProperty, Move(aValue.mGecko),
                                          propertyValue);
    MOZ_ASSERT(uncomputeResult,
               "Unable to get specified value from computed value");
    frame.mPropertyValues.AppendElement(
      Move(PropertyValuePair(aProperty, Move(propertyValue))));
  }
  return frame;
}

static nsTArray<Keyframe>
GetTransitionKeyframes(nsCSSPropertyID aProperty,
                       AnimationValue&& aStartValue,
                       AnimationValue&& aEndValue,
                       const nsTimingFunction& aTimingFunction)
{
  nsTArray<Keyframe> keyframes(2);

  Keyframe& fromFrame = AppendKeyframe(0.0, aProperty, Move(aStartValue),
                                       keyframes);
  if (aTimingFunction.mType != nsTimingFunction::Type::Linear) {
    fromFrame.mTimingFunction.emplace();
    fromFrame.mTimingFunction->Init(aTimingFunction);
  }

  AppendKeyframe(1.0, aProperty, Move(aEndValue), keyframes);

  return keyframes;
}

static bool
IsTransitionable(nsCSSPropertyID aProperty, bool aIsServo)
{
  if (aIsServo) {
    return Servo_Property_IsTransitionable(aProperty);
  }

  // FIXME: This should also exclude discretely-animated properties.
  return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None;
}

template<typename StyleType>
void
nsTransitionManager::ConsiderInitiatingTransition(
  nsCSSPropertyID aProperty,
  const StyleTransition& aTransition,
  dom::Element* aElement,
  CSSPseudoElementType aPseudoType,
  CSSTransitionCollection*& aElementTransitions,
  StyleType aOldStyle,
  StyleType aNewStyle,
  bool* aStartedAny,
  nsCSSPropertyIDSet* aWhichStarted)
{
  // IsShorthand itself will assert if aProperty is not a property.
  MOZ_ASSERT(!nsCSSProps::IsShorthand(aProperty),
             "property out of range");
  NS_ASSERTION(!aElementTransitions ||
               aElementTransitions->mElement == aElement, "Element mismatch");

  // Ignore disabled properties. We can arrive here if the transition-property
  // is 'all' and the disabled property has a default value which derives value
  // from another property, e.g. color.
  if (!nsCSSProps::IsEnabled(aProperty, CSSEnabledState::eForAllContent)) {
    return;
  }

  if (aWhichStarted->HasProperty(aProperty)) {
    // A later item in transition-property already started a
    // transition for this property, so we ignore this one.
    // See comment above and
    // http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html .
    return;
  }

  if (!IsTransitionable(aProperty, aElement->IsStyledByServo())) {
    return;
  }

  dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline();

  AnimationValue startValue, endValue;
  bool haveValues =
    ExtractNonDiscreteComputedValue(aProperty, aOldStyle, startValue) &&
    ExtractNonDiscreteComputedValue(aProperty, aNewStyle, endValue);

  bool haveChange = startValue != endValue;

  bool shouldAnimate =
    haveValues &&
    haveChange &&
    startValue.IsInterpolableWith(aProperty, endValue);

  bool haveCurrentTransition = false;
  size_t currentIndex = nsTArray<ElementPropertyTransition>::NoIndex;
  const ElementPropertyTransition *oldPT = nullptr;
  if (aElementTransitions) {
    OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
    for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) {
      if (animations[i]->TransitionProperty() == aProperty) {
        haveCurrentTransition = true;
        currentIndex = i;
        oldPT = animations[i]->GetEffect()
                ? animations[i]->GetEffect()->AsTransition()
                : nullptr;
        break;
      }
    }
  }

  // If we got a style change that changed the value to the endpoint
  // of the currently running transition, we don't want to interrupt
  // its timing function.
  // This needs to be before the !shouldAnimate && haveCurrentTransition
  // case below because we might be close enough to the end of the
  // transition that the current value rounds to the final value.  In
  // this case, we'll end up with shouldAnimate as false (because
  // there's no value change), but we need to return early here rather
  // than cancel the running transition because shouldAnimate is false!
  //
  // Likewise, if we got a style change that changed the value to the
  // endpoint of our finished transition, we also don't want to start
  // a new transition for the reasons described in
  // https://lists.w3.org/Archives/Public/www-style/2015Jan/0444.html .
  if (haveCurrentTransition && haveValues &&
      aElementTransitions->mAnimations[currentIndex]->ToValue() == endValue) {
    // GetAnimationRule already called RestyleForAnimation.
    return;
  }

  if (!shouldAnimate) {
    if (haveCurrentTransition) {
      // We're in the middle of a transition, and just got a non-transition
      // style change to something that we can't animate.  This might happen
      // because we got a non-transition style change changing to the current
      // in-progress value (which is particularly easy to cause when we're
      // currently in the 'transition-delay').  It also might happen because we
      // just got a style change to a value that can't be interpolated.
      OwningCSSTransitionPtrArray& animations =
        aElementTransitions->mAnimations;
      animations[currentIndex]->CancelFromStyle();
      oldPT = nullptr; // Clear pointer so it doesn't dangle
      animations.RemoveElementAt(currentIndex);
      EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType);
      if (effectSet) {
        effectSet->UpdateAnimationGeneration(mPresContext);
      }

      if (animations.IsEmpty()) {
        aElementTransitions->Destroy();
        // |aElementTransitions| is now a dangling pointer!
        aElementTransitions = nullptr;
      }
      // GetAnimationRule already called RestyleForAnimation.
    }
    return;
  }

  const nsTimingFunction &tf = aTransition.GetTimingFunction();
  float delay = aTransition.GetDelay();
  float duration = aTransition.GetDuration();
  if (duration < 0.0) {
    // The spec says a negative duration is treated as zero.
    duration = 0.0;
  }

  AnimationValue startForReversingTest = startValue;
  double reversePortion = 1.0;

  // If the new transition reverses an existing one, we'll need to
  // handle the timing differently.
  // FIXME: Move mStartForReversingTest, mReversePortion to CSSTransition,
  //        and set the timing function on transitions as an effect-level
  //        easing (rather than keyframe-level easing). (Bug 1292001)
  if (haveCurrentTransition &&
      aElementTransitions->mAnimations[currentIndex]->HasCurrentEffect() &&
      oldPT &&
      oldPT->mStartForReversingTest == endValue) {
    // Compute the appropriate negative transition-delay such that right
    // now we'd end up at the current position.
    double valuePortion =
      oldPT->CurrentValuePortion() * oldPT->mReversePortion +
      (1.0 - oldPT->mReversePortion);
    // A timing function with negative y1 (or y2!) might make
    // valuePortion negative.  In this case, we still want to apply our
    // reversing logic based on relative distances, not make duration
    // negative.
    if (valuePortion < 0.0) {
      valuePortion = -valuePortion;
    }
    // A timing function with y2 (or y1!) greater than one might
    // advance past its terminal value.  It's probably a good idea to
    // clamp valuePortion to be at most one to preserve the invariant
    // that a transition will complete within at most its specified
    // time.
    if (valuePortion > 1.0) {
      valuePortion = 1.0;
    }

    // Negative delays are essentially part of the transition
    // function, so reduce them along with the duration, but don't
    // reduce positive delays.
    if (delay < 0.0f) {
      delay *= valuePortion;
    }

    duration *= valuePortion;

    startForReversingTest = oldPT->ToValue();
    reversePortion = valuePortion;
  }

  TimingParams timing =
    TimingParamsFromCSSParams(duration, delay,
                              1.0 /* iteration count */,
                              dom::PlaybackDirection::Normal,
                              dom::FillMode::Backwards);

  // aElement is non-null here, so we emplace it directly.
  Maybe<OwningAnimationTarget> target;
  target.emplace(aElement, aPseudoType);
  KeyframeEffectParams effectOptions;
  RefPtr<ElementPropertyTransition> pt =
    new ElementPropertyTransition(aElement->OwnerDoc(), target, timing,
                                  startForReversingTest, reversePortion,
                                  effectOptions);

  pt->SetKeyframes(GetTransitionKeyframes(aProperty,
                                          Move(startValue), Move(endValue), tf),
                   aNewStyle);

  RefPtr<CSSTransition> animation =
    new CSSTransition(mPresContext->Document()->GetScopeObject());
  animation->SetOwningElement(OwningElementRef(*aElement, aPseudoType));
  animation->SetTimelineNoUpdate(timeline);
  animation->SetCreationSequence(
    mPresContext->RestyleManager()->GetAnimationGeneration());
  animation->SetEffectFromStyle(pt);
  animation->PlayFromStyle();

  if (!aElementTransitions) {
    bool createdCollection = false;
    aElementTransitions =
      CSSTransitionCollection::GetOrCreateAnimationCollection(
        aElement, aPseudoType, &createdCollection);
    if (!aElementTransitions) {
      MOZ_ASSERT(!createdCollection, "outparam should agree with return value");
      NS_WARNING("allocating collection failed");
      return;
    }

    if (createdCollection) {
      AddElementCollection(aElementTransitions);
    }
  }

  OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
#ifdef DEBUG
  for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) {
    MOZ_ASSERT(
      i == currentIndex || animations[i]->TransitionProperty() != aProperty,
      "duplicate transitions for property");
  }
#endif
  if (haveCurrentTransition) {
    // If this new transition is replacing an existing transition that is running
    // on the compositor, we store select parameters from the replaced transition
    // so that later, once all scripts have run, we can update the start value
    // of the transition using TimeStamp::Now(). This allows us to avoid a
    // large jump when starting a new transition when the main thread lags behind
    // the compositor.
    if (oldPT &&
        oldPT->IsCurrent() &&
        oldPT->IsRunningOnCompositor() &&
        !oldPT->GetAnimation()->GetStartTime().IsNull() &&
        timeline == oldPT->GetAnimation()->GetTimeline()) {
      const AnimationPropertySegment& segment =
        oldPT->Properties()[0].mSegments[0];
      pt->mReplacedTransition.emplace(
        ElementPropertyTransition::ReplacedTransitionProperties({
          oldPT->GetAnimation()->GetStartTime().Value(),
          oldPT->GetAnimation()->PlaybackRate(),
          oldPT->SpecifiedTiming(),
          segment.mTimingFunction,
          segment.mFromValue,
          segment.mToValue
        })
      );
    }
    animations[currentIndex]->CancelFromStyle();
    oldPT = nullptr; // Clear pointer so it doesn't dangle
    animations[currentIndex] = animation;
  } else {
    if (!animations.AppendElement(animation)) {
      NS_WARNING("out of memory");
      return;
    }
  }

  EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType);
  if (effectSet) {
    effectSet->UpdateAnimationGeneration(mPresContext);
  }

  *aStartedAny = true;
  aWhichStarted->AddProperty(aProperty);
}

void
nsTransitionManager::PruneCompletedTransitions(mozilla::dom::Element* aElement,
                                               CSSPseudoElementType aPseudoType,
                                               GeckoStyleContext* aNewStyleContext)
{
  MOZ_ASSERT(!aElement->IsGeneratedContentContainerForBefore() &&
             !aElement->IsGeneratedContentContainerForAfter());

  CSSTransitionCollection* collection =
    CSSTransitionCollection::GetAnimationCollection(aElement, aPseudoType);
  if (!collection) {
    return;
  }

  // Remove any finished transitions whose style doesn't match the new
  // style.
  // This is similar to some of the work that happens near the end of
  // nsTransitionManager::StyleContextChanged.
  // FIXME (bug 1158431): Really, we should also cancel running
  // transitions whose destination doesn't match as well.
  OwningCSSTransitionPtrArray& animations = collection->mAnimations;
  size_t i = animations.Length();
  MOZ_ASSERT(i != 0, "empty transitions list?");
  do {
    --i;
    CSSTransition* anim = animations[i];

    if (anim->HasCurrentEffect()) {
      continue;
    }

    // Since effect is a finished transition, we know it didn't
    // influence style.
    AnimationValue currentValue;
    if (!ExtractNonDiscreteComputedValue(anim->TransitionProperty(),
                                         aNewStyleContext, currentValue) ||
        currentValue != anim->ToValue()) {
      anim->CancelFromStyle();
      animations.RemoveElementAt(i);
    }
  } while (i != 0);

  if (collection->mAnimations.IsEmpty()) {
    collection->Destroy();
    // |collection| is now a dangling pointer!
    collection = nullptr;
  }
}
