/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/KeyframeUtils.h"

#include "mozilla/AnimationUtils.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/Move.h"
#include "mozilla/RangedArray.h"
#include "mozilla/ServoBindings.h"
#include "mozilla/StyleAnimationValue.h"
#include "mozilla/TimingParams.h"
#include "mozilla/dom/BaseKeyframeTypesBinding.h" // For FastBaseKeyframe etc.
#include "mozilla/dom/Element.h"
#include "mozilla/dom/KeyframeEffectBinding.h"
#include "mozilla/dom/KeyframeEffectReadOnly.h" // For PropertyValuesPair etc.
#include "jsapi.h" // For ForOfIterator etc.
#include "nsClassHashtable.h"
#include "nsCSSParser.h"
#include "nsCSSPropertyIDSet.h"
#include "nsCSSProps.h"
#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
#include "nsTArray.h"
#include <algorithm> // For std::stable_sort

namespace mozilla {

// ------------------------------------------------------------------
//
// Internal data types
//
// ------------------------------------------------------------------

// This is used while calculating paced spacing. If the keyframe is not pacable,
// we set its cumulative distance to kNotPaceable, so we can use this to check.
const double kNotPaceable = -1.0;

// For the aAllowList parameter of AppendStringOrStringSequence and
// GetPropertyValuesPairs.
enum class ListAllowance { eDisallow, eAllow };

/**
 * A comparator to sort nsCSSPropertyID values such that longhands are sorted
 * before shorthands, and shorthands with fewer components are sorted before
 * shorthands with more components.
 *
 * Using this allows us to prioritize values specified by longhands (or smaller
 * shorthand subsets) when longhands and shorthands are both specified
 * on the one keyframe.
 *
 * Example orderings that result from this:
 *
 *   margin-left, margin
 *
 * and:
 *
 *   border-top-color, border-color, border-top, border
 */
class PropertyPriorityComparator
{
public:
  PropertyPriorityComparator()
    : mSubpropertyCountInitialized(false) {}

  bool Equals(nsCSSPropertyID aLhs, nsCSSPropertyID aRhs) const
  {
    return aLhs == aRhs;
  }

  bool LessThan(nsCSSPropertyID aLhs,
                nsCSSPropertyID aRhs) const
  {
    bool isShorthandLhs = nsCSSProps::IsShorthand(aLhs);
    bool isShorthandRhs = nsCSSProps::IsShorthand(aRhs);

    if (isShorthandLhs) {
      if (isShorthandRhs) {
        // First, sort shorthands by the number of longhands they have.
        uint32_t subpropCountLhs = SubpropertyCount(aLhs);
        uint32_t subpropCountRhs = SubpropertyCount(aRhs);
        if (subpropCountLhs != subpropCountRhs) {
          return subpropCountLhs < subpropCountRhs;
        }
        // Otherwise, sort by IDL name below.
      } else {
        // Put longhands before shorthands.
        return false;
      }
    } else {
      if (isShorthandRhs) {
        // Put longhands before shorthands.
        return true;
      }
    }
    // For two longhand properties, or two shorthand with the same number
    // of longhand components, sort by IDL name.
    return nsCSSProps::PropertyIDLNameSortPosition(aLhs) <
           nsCSSProps::PropertyIDLNameSortPosition(aRhs);
  }

  uint32_t SubpropertyCount(nsCSSPropertyID aProperty) const
  {
    if (!mSubpropertyCountInitialized) {
      PodZero(&mSubpropertyCount);
      mSubpropertyCountInitialized = true;
    }
    if (mSubpropertyCount[aProperty] == 0) {
      uint32_t count = 0;
      CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
          p, aProperty, CSSEnabledState::eForAllContent) {
        ++count;
      }
      mSubpropertyCount[aProperty] = count;
    }
    return mSubpropertyCount[aProperty];
  }

private:
  // Cache of shorthand subproperty counts.
  mutable RangedArray<
    uint32_t,
    eCSSProperty_COUNT_no_shorthands,
    eCSSProperty_COUNT - eCSSProperty_COUNT_no_shorthands> mSubpropertyCount;
  mutable bool mSubpropertyCountInitialized;
};

/**
 * Adaptor for PropertyPriorityComparator to sort objects which have
 * a mProperty member.
 */
template <typename T>
class TPropertyPriorityComparator : PropertyPriorityComparator
{
public:
  bool Equals(const T& aLhs, const T& aRhs) const
  {
    return PropertyPriorityComparator::Equals(aLhs.mProperty, aRhs.mProperty);
  }
  bool LessThan(const T& aLhs, const T& aRhs) const
  {
    return PropertyPriorityComparator::LessThan(aLhs.mProperty, aRhs.mProperty);
  }
};

/**
 * Iterator to walk through a PropertyValuePair array using the ordering
 * provided by PropertyPriorityComparator.
 */
class PropertyPriorityIterator
{
public:
  explicit PropertyPriorityIterator(
    const nsTArray<PropertyValuePair>& aProperties)
    : mProperties(aProperties)
  {
    mSortedPropertyIndices.SetCapacity(mProperties.Length());
    for (size_t i = 0, len = mProperties.Length(); i < len; ++i) {
      PropertyAndIndex propertyIndex = { mProperties[i].mProperty, i };
      mSortedPropertyIndices.AppendElement(propertyIndex);
    }
    mSortedPropertyIndices.Sort(PropertyAndIndex::Comparator());
  }

  class Iter
  {
  public:
    explicit Iter(const PropertyPriorityIterator& aParent)
      : mParent(aParent)
      , mIndex(0) { }

    static Iter EndIter(const PropertyPriorityIterator &aParent)
    {
      Iter iter(aParent);
      iter.mIndex = aParent.mSortedPropertyIndices.Length();
      return iter;
    }

    bool operator!=(const Iter& aOther) const
    {
      return mIndex != aOther.mIndex;
    }

    Iter& operator++()
    {
      MOZ_ASSERT(mIndex + 1 <= mParent.mSortedPropertyIndices.Length(),
                 "Should not seek past end iterator");
      mIndex++;
      return *this;
    }

    const PropertyValuePair& operator*()
    {
      MOZ_ASSERT(mIndex < mParent.mSortedPropertyIndices.Length(),
                 "Should not try to dereference an end iterator");
      return mParent.mProperties[mParent.mSortedPropertyIndices[mIndex].mIndex];
    }

  private:
    const PropertyPriorityIterator& mParent;
    size_t mIndex;
  };

  Iter begin() { return Iter(*this); }
  Iter end()   { return Iter::EndIter(*this); }

private:
  struct PropertyAndIndex
  {
    nsCSSPropertyID mProperty;
    size_t mIndex; // Index of mProperty within mProperties

    typedef TPropertyPriorityComparator<PropertyAndIndex> Comparator;
  };

  const nsTArray<PropertyValuePair>& mProperties;
  nsTArray<PropertyAndIndex> mSortedPropertyIndices;
};

/**
 * A property-values pair obtained from the open-ended properties
 * discovered on a regular keyframe or property-indexed keyframe object.
 *
 * Single values (as required by a regular keyframe, and as also supported
 * on property-indexed keyframes) are stored as the only element in
 * mValues.
 */
struct PropertyValuesPair
{
  nsCSSPropertyID mProperty;
  nsTArray<nsString> mValues;

  typedef TPropertyPriorityComparator<PropertyValuesPair> Comparator;
};

/**
 * An additional property (for a property-values pair) found on a
 * BaseKeyframe or BasePropertyIndexedKeyframe object.
 */
struct AdditionalProperty
{
  nsCSSPropertyID mProperty;
  size_t mJsidIndex;        // Index into |ids| in GetPropertyValuesPairs.

  struct PropertyComparator
  {
    bool Equals(const AdditionalProperty& aLhs,
                const AdditionalProperty& aRhs) const
    {
      return aLhs.mProperty == aRhs.mProperty;
    }
    bool LessThan(const AdditionalProperty& aLhs,
                  const AdditionalProperty& aRhs) const
    {
      return nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) <
             nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
    }
  };
};

/**
 * Data for a segment in a keyframe animation of a given property
 * whose value is a StyleAnimationValue.
 *
 * KeyframeValueEntry is used in GetAnimationPropertiesFromKeyframes
 * to gather data for each individual segment.
 */
struct KeyframeValueEntry
{
  nsCSSPropertyID mProperty;
  StyleAnimationValue mValue;
  float mOffset;
  Maybe<ComputedTimingFunction> mTimingFunction;

  struct PropertyOffsetComparator
  {
    static bool Equals(const KeyframeValueEntry& aLhs,
                       const KeyframeValueEntry& aRhs)
    {
      return aLhs.mProperty == aRhs.mProperty &&
             aLhs.mOffset == aRhs.mOffset;
    }
    static bool LessThan(const KeyframeValueEntry& aLhs,
                         const KeyframeValueEntry& aRhs)
    {
      // First, sort by property IDL name.
      int32_t order = nsCSSProps::PropertyIDLNameSortPosition(aLhs.mProperty) -
                      nsCSSProps::PropertyIDLNameSortPosition(aRhs.mProperty);
      if (order != 0) {
        return order < 0;
      }

      // Then, by offset.
      return aLhs.mOffset < aRhs.mOffset;
    }
  };
};

class ComputedOffsetComparator
{
public:
  static bool Equals(const Keyframe& aLhs, const Keyframe& aRhs)
  {
    return aLhs.mComputedOffset == aRhs.mComputedOffset;
  }

  static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs)
  {
    return aLhs.mComputedOffset < aRhs.mComputedOffset;
  }
};

// ------------------------------------------------------------------
//
// Inlined helper methods
//
// ------------------------------------------------------------------

inline bool
IsInvalidValuePair(const PropertyValuePair& aPair, StyleBackendType aBackend)
{
  if (aBackend == StyleBackendType::Servo) {
    return !aPair.mServoDeclarationBlock;
  }

  // There are three types of values we store as token streams:
  //
  // * Shorthand values (where we manually extract the token stream's string
  //   value) and pass that along to various parsing methods
  // * Longhand values with variable references
  // * Invalid values
  //
  // We can distinguish between the last two cases because for invalid values
  // we leave the token stream's mPropertyID as eCSSProperty_UNKNOWN.
  return !nsCSSProps::IsShorthand(aPair.mProperty) &&
         aPair.mValue.GetUnit() == eCSSUnit_TokenStream &&
         aPair.mValue.GetTokenStreamValue()->mPropertyID
           == eCSSProperty_UNKNOWN;
}


// ------------------------------------------------------------------
//
// Internal helper method declarations
//
// ------------------------------------------------------------------

static void
GetKeyframeListFromKeyframeSequence(JSContext* aCx,
                                    nsIDocument* aDocument,
                                    JS::ForOfIterator& aIterator,
                                    nsTArray<Keyframe>& aResult,
                                    ErrorResult& aRv);

static bool
ConvertKeyframeSequence(JSContext* aCx,
                        nsIDocument* aDocument,
                        JS::ForOfIterator& aIterator,
                        nsTArray<Keyframe>& aResult);

static bool
GetPropertyValuesPairs(JSContext* aCx,
                       JS::Handle<JSObject*> aObject,
                       ListAllowance aAllowLists,
                       nsTArray<PropertyValuesPair>& aResult);

static bool
AppendStringOrStringSequenceToArray(JSContext* aCx,
                                    JS::Handle<JS::Value> aValue,
                                    ListAllowance aAllowLists,
                                    nsTArray<nsString>& aValues);

static bool
AppendValueAsString(JSContext* aCx,
                    nsTArray<nsString>& aValues,
                    JS::Handle<JS::Value> aValue);

static PropertyValuePair
MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
                      nsCSSParser& aParser, nsIDocument* aDocument);

static bool
HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);

static void
MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);

static bool
IsComputeValuesFailureKey(const PropertyValuePair& aPair);

static void
BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
                              nsTArray<AnimationProperty>& aResult);

static void
GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
                                           nsIDocument* aDocument,
                                           JS::Handle<JS::Value> aValue,
                                           nsTArray<Keyframe>& aResult,
                                           ErrorResult& aRv);

static bool
RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
                          nsIDocument* aDocument);

static void
DistributeRange(const Range<Keyframe>& aSpacingRange,
                const Range<Keyframe>& aRangeToAdjust);

static void
DistributeRange(const Range<Keyframe>& aSpacingRange);

static void
PaceRange(const Range<Keyframe>& aKeyframes,
          const Range<double>& aCumulativeDistances);

static nsTArray<double>
GetCumulativeDistances(const nsTArray<ComputedKeyframeValues>& aValues,
                       nsCSSPropertyID aProperty,
                       nsStyleContext* aStyleContext);

// ------------------------------------------------------------------
//
// Public API
//
// ------------------------------------------------------------------

/* static */ nsTArray<Keyframe>
KeyframeUtils::GetKeyframesFromObject(JSContext* aCx,
                                      nsIDocument* aDocument,
                                      JS::Handle<JSObject*> aFrames,
                                      ErrorResult& aRv)
{
  MOZ_ASSERT(!aRv.Failed());

  nsTArray<Keyframe> keyframes;

  if (!aFrames) {
    // The argument was explicitly null meaning no keyframes.
    return keyframes;
  }

  // At this point we know we have an object. We try to convert it to a
  // sequence of keyframes first, and if that fails due to not being iterable,
  // we try to convert it to a property-indexed keyframe.
  JS::Rooted<JS::Value> objectValue(aCx, JS::ObjectValue(*aFrames));
  JS::ForOfIterator iter(aCx);
  if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return keyframes;
  }

  if (iter.valueIsIterable()) {
    GetKeyframeListFromKeyframeSequence(aCx, aDocument, iter, keyframes, aRv);
  } else {
    GetKeyframeListFromPropertyIndexedKeyframe(aCx, aDocument, objectValue,
                                               keyframes, aRv);
  }

  if (aRv.Failed()) {
    MOZ_ASSERT(keyframes.IsEmpty(),
               "Should not set any keyframes when there is an error");
    return keyframes;
  }

  // We currently don't support additive animation. However, Web Animations
  // says that if you don't have a keyframe at offset 0 or 1, then you should
  // synthesize one using an additive zero value when you go to compose style.
  // Until we implement additive animations we just throw if we encounter any
  // set of keyframes that would put us in that situation.

  if (RequiresAdditiveAnimation(keyframes, aDocument)) {
    aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
    keyframes.Clear();
  }

  return keyframes;
}

/* static */ void
KeyframeUtils::ApplySpacing(nsTArray<Keyframe>& aKeyframes,
                            SpacingMode aSpacingMode,
                            nsCSSPropertyID aProperty,
                            nsTArray<ComputedKeyframeValues>& aComputedValues,
                            nsStyleContext* aStyleContext)
{
  if (aKeyframes.IsEmpty()) {
    return;
  }

  nsTArray<double> cumulativeDistances;
  if (aSpacingMode == SpacingMode::paced) {
    MOZ_ASSERT(IsAnimatableProperty(aProperty),
               "Paced property should be animatable");

    cumulativeDistances = GetCumulativeDistances(aComputedValues, aProperty,
                                                 aStyleContext);
    // Reset the computed offsets if using paced spacing.
    for (Keyframe& keyframe : aKeyframes) {
      keyframe.mComputedOffset = Keyframe::kComputedOffsetNotSet;
    }
  }

  // If the first keyframe has an unspecified offset, fill it in with 0%.
  // If there is only a single keyframe, then it gets 100%.
  if (aKeyframes.Length() > 1) {
    Keyframe& firstElement = aKeyframes[0];
    firstElement.mComputedOffset = firstElement.mOffset.valueOr(0.0);
    // We will fill in the last keyframe's offset below
  } else {
    Keyframe& lastElement = aKeyframes.LastElement();
    lastElement.mComputedOffset = lastElement.mOffset.valueOr(1.0);
  }

  // Fill in remaining missing offsets.
  const Keyframe* const last = aKeyframes.cend() - 1;
  const RangedPtr<Keyframe> begin(aKeyframes.begin(), aKeyframes.Length());
  RangedPtr<Keyframe> keyframeA = begin;
  while (keyframeA != last) {
    // Find keyframe A and keyframe B *between* which we will apply spacing.
    RangedPtr<Keyframe> keyframeB = keyframeA + 1;
    while (keyframeB->mOffset.isNothing() && keyframeB != last) {
      ++keyframeB;
    }
    keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0);

    // Fill computed offsets in (keyframe A, keyframe B).
    if (aSpacingMode == SpacingMode::distribute) {
      DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1));
    } else {
      // a) Find Paced A (first paceable keyframe) and
      //    Paced B (last paceable keyframe) in [keyframe A, keyframe B].
      RangedPtr<Keyframe> pacedA = keyframeA;
      while (pacedA < keyframeB &&
             cumulativeDistances[pacedA - begin] == kNotPaceable) {
        ++pacedA;
      }
      RangedPtr<Keyframe> pacedB = keyframeB;
      while (pacedB > keyframeA &&
             cumulativeDistances[pacedB - begin] == kNotPaceable) {
        --pacedB;
      }
      // As spec says, if there is no paceable keyframe
      // in [keyframe A, keyframe B], we let Paced A and Paced B refer to
      // keyframe B.
      if (pacedA > pacedB) {
        pacedA = pacedB = keyframeB;
      }
      // b) Apply distributing offsets in (keyframe A, Paced A] and
      //    [Paced B, keyframe B).
      DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1),
                      Range<Keyframe>(keyframeA + 1, pacedA + 1));
      DistributeRange(Range<Keyframe>(keyframeA, keyframeB + 1),
                      Range<Keyframe>(pacedB, keyframeB));
      // c) Apply paced offsets to each paceable keyframe in (Paced A, Paced B).
      //    We pass the range [Paced A, Paced B] since PaceRange needs the end
      //    points of the range in order to calculate the correct offset.
      PaceRange(Range<Keyframe>(pacedA, pacedB + 1),
                Range<double>(&cumulativeDistances[pacedA - begin],
                              pacedB - pacedA + 1));
      // d) Fill in any computed offsets in (Paced A, Paced B) that are still
      //    not set (e.g. because the keyframe was not paceable, or because the
      //    cumulative distance between paceable properties was zero).
      for (RangedPtr<Keyframe> frame = pacedA + 1; frame < pacedB; ++frame) {
        if (frame->mComputedOffset != Keyframe::kComputedOffsetNotSet) {
          continue;
        }

        RangedPtr<Keyframe> start = frame - 1;
        RangedPtr<Keyframe> end = frame + 1;
        while (end < pacedB &&
               end->mComputedOffset == Keyframe::kComputedOffsetNotSet) {
          ++end;
        }
        DistributeRange(Range<Keyframe>(start, end + 1));
        frame = end;
      }
    }
    keyframeA = keyframeB;
  }
}

/* static */ void
KeyframeUtils::ApplyDistributeSpacing(nsTArray<Keyframe>& aKeyframes)
{
  nsTArray<ComputedKeyframeValues> emptyArray;
  ApplySpacing(aKeyframes, SpacingMode::distribute, eCSSProperty_UNKNOWN,
               emptyArray, nullptr);
}

/* static */ nsTArray<ComputedKeyframeValues>
KeyframeUtils::GetComputedKeyframeValues(const nsTArray<Keyframe>& aKeyframes,
                                         dom::Element* aElement,
                                         nsStyleContext* aStyleContext)
{
  MOZ_ASSERT(aStyleContext);
  MOZ_ASSERT(aElement);

  StyleBackendType styleBackend = aElement->OwnerDoc()->GetStyleBackendType();

  const size_t len = aKeyframes.Length();
  nsTArray<ComputedKeyframeValues> result(len);

  for (const Keyframe& frame : aKeyframes) {
    nsCSSPropertyIDSet propertiesOnThisKeyframe;
    ComputedKeyframeValues* computedValues = result.AppendElement();
    for (const PropertyValuePair& pair :
           PropertyPriorityIterator(frame.mPropertyValues)) {
      MOZ_ASSERT(!pair.mServoDeclarationBlock ||
                 styleBackend == StyleBackendType::Servo,
                 "Animation values were parsed using Servo backend but target"
                 " element is not using Servo backend?");

      if (IsInvalidValuePair(pair, styleBackend)) {
        continue;
      }

      // Expand each value into the set of longhands and produce
      // a KeyframeValueEntry for each value.
      nsTArray<PropertyStyleAnimationValuePair> values;

      if (styleBackend == StyleBackendType::Servo) {
        if (!StyleAnimationValue::ComputeValues(pair.mProperty,
              CSSEnabledState::eForAllContent, aStyleContext,
              *pair.mServoDeclarationBlock, values)) {
          continue;
        }
      } else {
        // For shorthands, we store the string as a token stream so we need to
        // extract that first.
        if (nsCSSProps::IsShorthand(pair.mProperty)) {
          nsCSSValueTokenStream* tokenStream = pair.mValue.GetTokenStreamValue();
          if (!StyleAnimationValue::ComputeValues(pair.mProperty,
                CSSEnabledState::eForAllContent, aElement, aStyleContext,
                tokenStream->mTokenStream, /* aUseSVGMode */ false, values) ||
              IsComputeValuesFailureKey(pair)) {
            continue;
          }
        } else {
          if (!StyleAnimationValue::ComputeValues(pair.mProperty,
                CSSEnabledState::eForAllContent, aElement, aStyleContext,
                pair.mValue, /* aUseSVGMode */ false, values)) {
            continue;
          }
          MOZ_ASSERT(values.Length() == 1,
                    "Longhand properties should produce a single"
                    " StyleAnimationValue");
        }
      }

      for (auto& value : values) {
        // If we already got a value for this property on the keyframe,
        // skip this one.
        if (propertiesOnThisKeyframe.HasProperty(value.mProperty)) {
          continue;
        }
        computedValues->AppendElement(value);
        propertiesOnThisKeyframe.AddProperty(value.mProperty);
      }
    }
  }

  MOZ_ASSERT(result.Length() == aKeyframes.Length(), "Array length mismatch");
  return result;
}

/* static */ nsTArray<AnimationProperty>
KeyframeUtils::GetAnimationPropertiesFromKeyframes(
  const nsTArray<Keyframe>& aKeyframes,
  const nsTArray<ComputedKeyframeValues>& aComputedValues,
  nsStyleContext* aStyleContext)
{
  MOZ_ASSERT(aKeyframes.Length() == aComputedValues.Length(),
             "Array length mismatch");

  nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());

  const size_t len = aKeyframes.Length();
  for (size_t i = 0; i < len; ++i) {
    const Keyframe& frame = aKeyframes[i];
    for (auto& value : aComputedValues[i]) {
      MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
                 "Invalid computed offset");
      KeyframeValueEntry* entry = entries.AppendElement();
      entry->mOffset = frame.mComputedOffset;
      entry->mProperty = value.mProperty;
      entry->mValue = value.mValue;
      entry->mTimingFunction = frame.mTimingFunction;
    }
  }

  nsTArray<AnimationProperty> result;
  BuildSegmentsFromValueEntries(entries, result);
  return result;
}

/* static */ bool
KeyframeUtils::IsAnimatableProperty(nsCSSPropertyID aProperty)
{
  if (aProperty == eCSSProperty_UNKNOWN) {
    return false;
  }

  if (!nsCSSProps::IsShorthand(aProperty)) {
    return nsCSSProps::kAnimTypeTable[aProperty] != eStyleAnimType_None;
  }

  CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, aProperty,
                                       CSSEnabledState::eForAllContent) {
    if (nsCSSProps::kAnimTypeTable[*subprop] != eStyleAnimType_None) {
      return true;
    }
  }

  return false;
}

// ------------------------------------------------------------------
//
// Internal helpers
//
// ------------------------------------------------------------------

/**
 * Converts a JS object to an IDL sequence<Keyframe>.
 *
 * @param aCx The JSContext corresponding to |aIterator|.
 * @param aDocument The document to use when parsing CSS properties.
 * @param aIterator An already-initialized ForOfIterator for the JS
 *   object to iterate over as a sequence.
 * @param aResult The array into which the resulting Keyframe objects will be
 *   appended.
 * @param aRv Out param to store any errors thrown by this function.
 */
static void
GetKeyframeListFromKeyframeSequence(JSContext* aCx,
                                    nsIDocument* aDocument,
                                    JS::ForOfIterator& aIterator,
                                    nsTArray<Keyframe>& aResult,
                                    ErrorResult& aRv)
{
  MOZ_ASSERT(!aRv.Failed());
  MOZ_ASSERT(aResult.IsEmpty());

  // Convert the object in aIterator to a sequence of keyframes producing
  // an array of Keyframe objects.
  if (!ConvertKeyframeSequence(aCx, aDocument, aIterator, aResult)) {
    aRv.Throw(NS_ERROR_FAILURE);
    aResult.Clear();
    return;
  }

  // If the sequence<> had zero elements, we won't generate any
  // keyframes.
  if (aResult.IsEmpty()) {
    return;
  }

  // Check that the keyframes are loosely sorted and with values all
  // between 0% and 100%.
  if (!HasValidOffsets(aResult)) {
    aRv.ThrowTypeError<dom::MSG_INVALID_KEYFRAME_OFFSETS>();
    aResult.Clear();
    return;
  }
}

/**
 * Converts a JS object wrapped by the given JS::ForIfIterator to an
 * IDL sequence<Keyframe> and stores the resulting Keyframe objects in
 * aResult.
 */
static bool
ConvertKeyframeSequence(JSContext* aCx,
                        nsIDocument* aDocument,
                        JS::ForOfIterator& aIterator,
                        nsTArray<Keyframe>& aResult)
{
  JS::Rooted<JS::Value> value(aCx);
  nsCSSParser parser(aDocument->CSSLoader());

  for (;;) {
    bool done;
    if (!aIterator.next(&value, &done)) {
      return false;
    }
    if (done) {
      break;
    }
    // Each value found when iterating the object must be an object
    // or null/undefined (which gets treated as a default {} dictionary
    // value).
    if (!value.isObject() && !value.isNullOrUndefined()) {
      dom::ThrowErrorMessage(aCx, dom::MSG_NOT_OBJECT,
                             "Element of sequence<Keyframe> argument");
      return false;
    }

    // Convert the JS value into a BaseKeyframe dictionary value.
    dom::binding_detail::FastBaseKeyframe keyframeDict;
    if (!keyframeDict.Init(aCx, value,
                           "Element of sequence<Keyframe> argument")) {
      return false;
    }

    Keyframe* keyframe = aResult.AppendElement(fallible);
    if (!keyframe) {
      return false;
    }
    if (!keyframeDict.mOffset.IsNull()) {
      keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
    }

    ErrorResult rv;
    keyframe->mTimingFunction =
      TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, rv);
    if (rv.MaybeSetPendingException(aCx)) {
      return false;
    }

    // Look for additional property-values pairs on the object.
    nsTArray<PropertyValuesPair> propertyValuePairs;
    if (value.isObject()) {
      JS::Rooted<JSObject*> object(aCx, &value.toObject());
      if (!GetPropertyValuesPairs(aCx, object,
                                  ListAllowance::eDisallow,
                                  propertyValuePairs)) {
        return false;
      }
    }

    for (PropertyValuesPair& pair : propertyValuePairs) {
      MOZ_ASSERT(pair.mValues.Length() == 1);
      keyframe->mPropertyValues.AppendElement(
        MakePropertyValuePair(pair.mProperty, pair.mValues[0], parser,
                              aDocument));

      // When we go to convert keyframes into arrays of property values we
      // call StyleAnimation::ComputeValues. This should normally return true
      // but in order to test the case where it does not, BaseKeyframeDict
      // includes a chrome-only member that can be set to indicate that
      // ComputeValues should fail for shorthand property values on that
      // keyframe.
      if (nsCSSProps::IsShorthand(pair.mProperty) &&
          keyframeDict.mSimulateComputeValuesFailure) {
        MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
      }
    }
  }

  return true;
}

/**
 * Reads the property-values pairs from the specified JS object.
 *
 * @param aObject The JS object to look at.
 * @param aAllowLists If eAllow, values will be converted to
 *   (DOMString or sequence<DOMString); if eDisallow, values
 *   will be converted to DOMString.
 * @param aResult The array into which the enumerated property-values
 *   pairs will be stored.
 * @return false on failure or JS exception thrown while interacting
 *   with aObject; true otherwise.
 */
static bool
GetPropertyValuesPairs(JSContext* aCx,
                       JS::Handle<JSObject*> aObject,
                       ListAllowance aAllowLists,
                       nsTArray<PropertyValuesPair>& aResult)
{
  nsTArray<AdditionalProperty> properties;

  // Iterate over all the properties on aObject and append an
  // entry to properties for them.
  //
  // We don't compare the jsids that we encounter with those for
  // the explicit dictionary members, since we know that none
  // of the CSS property IDL names clash with them.
  JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
  if (!JS_Enumerate(aCx, aObject, &ids)) {
    return false;
  }
  for (size_t i = 0, n = ids.length(); i < n; i++) {
    nsAutoJSString propName;
    if (!propName.init(aCx, ids[i])) {
      return false;
    }
    nsCSSPropertyID property =
      nsCSSProps::LookupPropertyByIDLName(propName,
                                          CSSEnabledState::eForAllContent);
    if (KeyframeUtils::IsAnimatableProperty(property)) {
      AdditionalProperty* p = properties.AppendElement();
      p->mProperty = property;
      p->mJsidIndex = i;
    }
  }

  // Sort the entries by IDL name and then get each value and
  // convert it either to a DOMString or to a
  // (DOMString or sequence<DOMString>), depending on aAllowLists,
  // and build up aResult.
  properties.Sort(AdditionalProperty::PropertyComparator());

  for (AdditionalProperty& p : properties) {
    JS::Rooted<JS::Value> value(aCx);
    if (!JS_GetPropertyById(aCx, aObject, ids[p.mJsidIndex], &value)) {
      return false;
    }
    PropertyValuesPair* pair = aResult.AppendElement();
    pair->mProperty = p.mProperty;
    if (!AppendStringOrStringSequenceToArray(aCx, value, aAllowLists,
                                             pair->mValues)) {
      return false;
    }
  }

  return true;
}

/**
 * Converts aValue to DOMString, if aAllowLists is eDisallow, or
 * to (DOMString or sequence<DOMString>) if aAllowLists is aAllow.
 * The resulting strings are appended to aValues.
 */
static bool
AppendStringOrStringSequenceToArray(JSContext* aCx,
                                    JS::Handle<JS::Value> aValue,
                                    ListAllowance aAllowLists,
                                    nsTArray<nsString>& aValues)
{
  if (aAllowLists == ListAllowance::eAllow && aValue.isObject()) {
    // The value is an object, and we want to allow lists; convert
    // aValue to (DOMString or sequence<DOMString>).
    JS::ForOfIterator iter(aCx);
    if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
      return false;
    }
    if (iter.valueIsIterable()) {
      // If the object is iterable, convert it to sequence<DOMString>.
      JS::Rooted<JS::Value> element(aCx);
      for (;;) {
        bool done;
        if (!iter.next(&element, &done)) {
          return false;
        }
        if (done) {
          break;
        }
        if (!AppendValueAsString(aCx, aValues, element)) {
          return false;
        }
      }
      return true;
    }
  }

  // Either the object is not iterable, or aAllowLists doesn't want
  // a list; convert it to DOMString.
  if (!AppendValueAsString(aCx, aValues, aValue)) {
    return false;
  }

  return true;
}

/**
 * Converts aValue to DOMString and appends it to aValues.
 */
static bool
AppendValueAsString(JSContext* aCx,
                    nsTArray<nsString>& aValues,
                    JS::Handle<JS::Value> aValue)
{
  return ConvertJSValueToString(aCx, aValue, dom::eStringify, dom::eStringify,
                                *aValues.AppendElement());
}

/**
 * Construct a PropertyValuePair parsing the given string into a suitable
 * nsCSSValue object.
 *
 * @param aProperty The CSS property.
 * @param aStringValue The property value to parse.
 * @param aParser The CSS parser object to use.
 * @param aDocument The document to use when parsing.
 * @return The constructed PropertyValuePair object.
 */
static PropertyValuePair
MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
                      nsCSSParser& aParser, nsIDocument* aDocument)
{
  MOZ_ASSERT(aDocument);
  PropertyValuePair result;

  result.mProperty = aProperty;

  if (aDocument->GetStyleBackendType() == StyleBackendType::Servo) {
    nsCString name = nsCSSProps::GetStringValue(aProperty);

    NS_ConvertUTF16toUTF8 value(aStringValue);
    RefPtr<ThreadSafeURIHolder> base =
      new ThreadSafeURIHolder(aDocument->GetDocumentURI());
    RefPtr<ThreadSafeURIHolder> referrer =
      new ThreadSafeURIHolder(aDocument->GetDocumentURI());
    RefPtr<ThreadSafePrincipalHolder> principal =
      new ThreadSafePrincipalHolder(aDocument->NodePrincipal());

    nsCString baseString;
    aDocument->GetDocumentURI()->GetSpec(baseString);

    RefPtr<RawServoDeclarationBlock> servoDeclarationBlock =
      Servo_ParseProperty(&name, &value, &baseString,
                          base, referrer, principal).Consume();

    if (servoDeclarationBlock) {
      result.mServoDeclarationBlock = servoDeclarationBlock.forget();
      return result;
    }
  }

  nsCSSValue value;
  if (!nsCSSProps::IsShorthand(aProperty)) {
    aParser.ParseLonghandProperty(aProperty,
                                  aStringValue,
                                  aDocument->GetDocumentURI(),
                                  aDocument->GetDocumentURI(),
                                  aDocument->NodePrincipal(),
                                  value);
  }

  if (value.GetUnit() == eCSSUnit_Null) {
    // Either we have a shorthand, or we failed to parse a longhand.
    // In either case, store the string value as a token stream.
    nsCSSValueTokenStream* tokenStream = new nsCSSValueTokenStream;
    tokenStream->mTokenStream = aStringValue;

    // We are about to convert a null value to a token stream value but
    // by leaving the mPropertyID as unknown, we will be able to
    // distinguish between invalid values and valid token stream values
    // (e.g. values with variable references).
    MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
               "The property of a token stream should be initialized"
               " to unknown");

    // By leaving mShorthandPropertyID as unknown, we ensure that when
    // we call nsCSSValue::AppendToString we get back the string stored
    // in mTokenStream.
    MOZ_ASSERT(tokenStream->mShorthandPropertyID == eCSSProperty_UNKNOWN,
               "The shorthand property of a token stream should be initialized"
               " to unknown");
    value.SetTokenStreamValue(tokenStream);
  } else {
    // If we succeeded in parsing with Gecko, but not Servo the animation is
    // not going to work since, for the purposes of animation, we're going to
    // ignore |mValue| when the backend is Servo.
    NS_WARNING_ASSERTION(aDocument->GetStyleBackendType() !=
                           StyleBackendType::Servo,
                         "Gecko succeeded in parsing where Servo failed");
  }

  result.mValue = value;

  return result;
}

/**
 * Checks that the given keyframes are loosely ordered (each keyframe's
 * offset that is not null is greater than or equal to the previous
 * non-null offset) and that all values are within the range [0.0, 1.0].
 *
 * @return true if the keyframes' offsets are correctly ordered and
 *   within range; false otherwise.
 */
static bool
HasValidOffsets(const nsTArray<Keyframe>& aKeyframes)
{
  double offset = 0.0;
  for (const Keyframe& keyframe : aKeyframes) {
    if (keyframe.mOffset) {
      double thisOffset = keyframe.mOffset.value();
      if (thisOffset < offset || thisOffset > 1.0f) {
        return false;
      }
      offset = thisOffset;
    }
  }
  return true;
}

/**
 * Takes a property-value pair for a shorthand property and modifies the
 * value to indicate that when we call StyleAnimationValue::ComputeValues on
 * that value we should behave as if that function had failed.
 *
 * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
 *              a shorthand property.
 */
static void
MarkAsComputeValuesFailureKey(PropertyValuePair& aPair)
{
  MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
             "Only shorthand property values can be marked as failure values");

  // We store shorthand values as nsCSSValueTokenStream objects whose mProperty
  // and mShorthandPropertyID are eCSSProperty_UNKNOWN and whose mTokenStream
  // member contains the shorthand property's value as a string.
  //
  // We need to leave mShorthandPropertyID as eCSSProperty_UNKNOWN so that
  // nsCSSValue::AppendToString returns the mTokenStream value, but we can
  // update mPropertyID to a special value to indicate that this is
  // a special failure sentinel.
  nsCSSValueTokenStream* tokenStream = aPair.mValue.GetTokenStreamValue();
  MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
             "Shorthand value should initially have an unknown property ID");
  tokenStream->mPropertyID = eCSSPropertyExtra_no_properties;
}

/**
 * Returns true if |aPair| is a property-value pair on which we have
 * previously called MarkAsComputeValuesFailureKey (and hence we should
 * simulate failure when calling StyleAnimationValue::ComputeValues using its
 * value).
 *
 * @param aPair The property-value pair to test.
 * @return True if |aPair| represents a failure value.
 */
static bool
IsComputeValuesFailureKey(const PropertyValuePair& aPair)
{
  return nsCSSProps::IsShorthand(aPair.mProperty) &&
         aPair.mValue.GetTokenStreamValue()->mPropertyID ==
           eCSSPropertyExtra_no_properties;
}

/**
 * Builds an array of AnimationProperty objects to represent the keyframe
 * animation segments in aEntries.
 */
static void
BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
                              nsTArray<AnimationProperty>& aResult)
{
  if (aEntries.IsEmpty()) {
    return;
  }

  // Sort the KeyframeValueEntry objects so that all entries for a given
  // property are together, and the entries are sorted by offset otherwise.
  std::stable_sort(aEntries.begin(), aEntries.end(),
                   &KeyframeValueEntry::PropertyOffsetComparator::LessThan);

  // For a given index i, we want to generate a segment from aEntries[i]
  // to aEntries[j], if:
  //
  //   * j > i,
  //   * aEntries[i + 1]'s offset/property is different from aEntries[i]'s, and
  //   * aEntries[j - 1]'s offset/property is different from aEntries[j]'s.
  //
  // That will eliminate runs of same offset/property values where there's no
  // point generating zero length segments in the middle of the animation.
  //
  // Additionally we need to generate a zero length segment at offset 0 and at
  // offset 1, if we have multiple values for a given property at that offset,
  // since we need to retain the very first and very last value so they can
  // be used for reverse and forward filling.
  //
  // Typically, for each property in |aEntries|, we expect there to be at least
  // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
  // However, since it is possible that when building |aEntries|, the call to
  // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
  // Furthermore, since we don't yet implement additive animation and hence
  // don't have sensible fallback behavior when these values are missing, the
  // following loop takes care to identify properties that lack a value at
  // offset 0.0/1.0 and drops those properties from |aResult|.

  nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN;
  AnimationProperty* animationProperty = nullptr;

  size_t i = 0, n = aEntries.Length();

  while (i < n) {
    // Check that the last property ends with an entry at offset 1.
    if (i + 1 == n) {
      if (aEntries[i].mOffset != 1.0f && animationProperty) {
        aResult.RemoveElementAt(aResult.Length() - 1);
        animationProperty = nullptr;
      }
      break;
    }

    MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN &&
               aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN,
               "Each entry should specify a valid property");

    // Skip properties that don't have an entry with offset 0.
    if (aEntries[i].mProperty != lastProperty &&
        aEntries[i].mOffset != 0.0f) {
      // Since the entries are sorted by offset for a given property, and
      // since we don't update |lastProperty|, we will keep hitting this
      // condition until we change property.
      ++i;
      continue;
    }

    // Drop properties that don't end with an entry with offset 1.
    if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
        aEntries[i].mOffset != 1.0f) {
      if (animationProperty) {
        aResult.RemoveElementAt(aResult.Length() - 1);
        animationProperty = nullptr;
      }
      ++i;
      continue;
    }

    // Starting from i, determine the next [i, j] interval from which to
    // generate a segment.
    size_t j;
    if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
      // We need to generate an initial zero-length segment.
      MOZ_ASSERT(aEntries[i].mProperty == aEntries[i + 1].mProperty);
      j = i + 1;
      while (aEntries[j + 1].mOffset == 0.0f &&
             aEntries[j + 1].mProperty == aEntries[j].mProperty) {
        ++j;
      }
    } else if (aEntries[i].mOffset == 1.0f) {
      if (aEntries[i + 1].mOffset == 1.0f &&
          aEntries[i + 1].mProperty == aEntries[i].mProperty) {
        // We need to generate a final zero-length segment.
        j = i + 1;
        while (j + 1 < n &&
               aEntries[j + 1].mOffset == 1.0f &&
               aEntries[j + 1].mProperty == aEntries[j].mProperty) {
          ++j;
        }
      } else {
        // New property.
        MOZ_ASSERT(aEntries[i].mProperty != aEntries[i + 1].mProperty);
        animationProperty = nullptr;
        ++i;
        continue;
      }
    } else {
      while (aEntries[i].mOffset == aEntries[i + 1].mOffset &&
             aEntries[i].mProperty == aEntries[i + 1].mProperty) {
        ++i;
      }
      j = i + 1;
    }

    // If we've moved on to a new property, create a new AnimationProperty
    // to insert segments into.
    if (aEntries[i].mProperty != lastProperty) {
      MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
      MOZ_ASSERT(!animationProperty);
      animationProperty = aResult.AppendElement();
      animationProperty->mProperty = aEntries[i].mProperty;
      lastProperty = aEntries[i].mProperty;
    }

    MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");

    // Now generate the segment.
    AnimationPropertySegment* segment =
      animationProperty->mSegments.AppendElement();
    segment->mFromKey   = aEntries[i].mOffset;
    segment->mToKey     = aEntries[j].mOffset;
    segment->mFromValue = aEntries[i].mValue;
    segment->mToValue   = aEntries[j].mValue;
    segment->mTimingFunction = aEntries[i].mTimingFunction;

    i = j;
  }
}

/**
 * Converts a JS object representing a property-indexed keyframe into
 * an array of Keyframe objects.
 *
 * @param aCx The JSContext for |aValue|.
 * @param aDocument The document to use when parsing CSS properties.
 * @param aValue The JS object.
 * @param aResult The array into which the resulting AnimationProperty
 *   objects will be appended.
 * @param aRv Out param to store any errors thrown by this function.
 */
static void
GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
                                           nsIDocument* aDocument,
                                           JS::Handle<JS::Value> aValue,
                                           nsTArray<Keyframe>& aResult,
                                           ErrorResult& aRv)
{
  MOZ_ASSERT(aValue.isObject());
  MOZ_ASSERT(aResult.IsEmpty());
  MOZ_ASSERT(!aRv.Failed());

  // Convert the object to a property-indexed keyframe dictionary to
  // get its explicit dictionary members.
  dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
  if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument",
                         false)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  Maybe<ComputedTimingFunction> easing =
    TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, aRv);
  if (aRv.Failed()) {
    return;
  }

  // Get all the property--value-list pairs off the object.
  JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
  nsTArray<PropertyValuesPair> propertyValuesPairs;
  if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
                              propertyValuesPairs)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  // Create a set of keyframes for each property.
  nsCSSParser parser(aDocument->CSSLoader());
  nsClassHashtable<nsFloatHashKey, Keyframe> processedKeyframes;
  for (const PropertyValuesPair& pair : propertyValuesPairs) {
    size_t count = pair.mValues.Length();
    if (count == 0) {
      // No animation values for this property.
      continue;
    }
    if (count == 1) {
      // We don't support additive values and so can't support an
      // animation that goes from the underlying value to this
      // specified value.  Throw an exception until we do support this.
      aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
      return;
    }

    size_t n = pair.mValues.Length() - 1;
    size_t i = 0;

    for (const nsString& stringValue : pair.mValues) {
      double offset = i++ / double(n);
      Keyframe* keyframe = processedKeyframes.LookupOrAdd(offset);
      if (keyframe->mPropertyValues.IsEmpty()) {
        keyframe->mTimingFunction = easing;
        keyframe->mComputedOffset = offset;
      }
      keyframe->mPropertyValues.AppendElement(
        MakePropertyValuePair(pair.mProperty, stringValue, parser, aDocument));
    }
  }

  aResult.SetCapacity(processedKeyframes.Count());
  for (auto iter = processedKeyframes.Iter(); !iter.Done(); iter.Next()) {
    aResult.AppendElement(Move(*iter.UserData()));
  }

  aResult.Sort(ComputedOffsetComparator());
}

/**
 * Returns true if the supplied set of keyframes has keyframe values for
 * any property for which it does not also supply a value for the 0% and 100%
 * offsets. In this case we are supposed to synthesize an additive zero value
 * but since we don't support additive animation yet we can't support this
 * case. We try to detect that here so we can throw an exception. The check is
 * not entirely accurate but should detect most common cases.
 *
 * @param aKeyframes The set of keyframes to analyze.
 * @param aDocument The document to use when parsing keyframes so we can
 *   try to detect where we have an invalid value at 0%/100%.
 */
static bool
RequiresAdditiveAnimation(const nsTArray<Keyframe>& aKeyframes,
                          nsIDocument* aDocument)
{
  // We are looking to see if that every property referenced in |aKeyframes|
  // has a valid property at offset 0.0 and 1.0. The check as to whether a
  // property is valid or not, however, is not precise. We only check if the
  // property can be parsed, NOT whether it can also be converted to a
  // StyleAnimationValue since doing that requires a target element bound to
  // a document which we might not always have at the point where we want to
  // perform this check.
  //
  // This is only a temporary measure until we implement additive animation.
  // So as long as this check catches most cases, and we don't do anything
  // horrible in one of the cases we can't detect, it should be sufficient.

  nsCSSPropertyIDSet properties;              // All properties encountered.
  nsCSSPropertyIDSet propertiesWithFromValue; // Those with a defined 0% value.
  nsCSSPropertyIDSet propertiesWithToValue;   // Those with a defined 100% value.

  auto addToPropertySets = [&](nsCSSPropertyID aProperty, double aOffset) {
    properties.AddProperty(aProperty);
    if (aOffset == 0.0) {
      propertiesWithFromValue.AddProperty(aProperty);
    } else if (aOffset == 1.0) {
      propertiesWithToValue.AddProperty(aProperty);
    }
  };

  StyleBackendType styleBackend = aDocument->GetStyleBackendType();

  for (size_t i = 0, len = aKeyframes.Length(); i < len; i++) {
    const Keyframe& frame = aKeyframes[i];

    // We won't have called ApplySpacing when this is called so
    // we can't use frame.mComputedOffset. Instead we do a rough version
    // of that algorithm that substitutes null offsets with 0.0 for the first
    // frame, 1.0 for the last frame, and 0.5 for everything else.
    double computedOffset = i == len - 1
                            ? 1.0
                            : i == 0 ? 0.0 : 0.5;
    double offsetToUse = frame.mOffset
                         ? frame.mOffset.value()
                         : computedOffset;

    for (const PropertyValuePair& pair : frame.mPropertyValues) {
      if (IsInvalidValuePair(pair, styleBackend)) {
        continue;
      }

      if (nsCSSProps::IsShorthand(pair.mProperty)) {
        if (styleBackend == StyleBackendType::Gecko) {
          nsCSSValueTokenStream* tokenStream =
            pair.mValue.GetTokenStreamValue();
          nsCSSParser parser(aDocument->CSSLoader());
          if (!parser.IsValueValidForProperty(pair.mProperty,
                                              tokenStream->mTokenStream)) {
            continue;
          }
        }
        // For the Servo backend, invalid shorthand values are represented by
        // a null mServoDeclarationBlock member which we skip above in
        // IsInvalidValuePair.
        MOZ_ASSERT(styleBackend != StyleBackendType::Servo ||
                   pair.mServoDeclarationBlock);
        CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(
            prop, pair.mProperty, CSSEnabledState::eForAllContent) {
          addToPropertySets(*prop, offsetToUse);
        }
      } else {
        addToPropertySets(pair.mProperty, offsetToUse);
      }
    }
  }

  return !propertiesWithFromValue.Equals(properties) ||
         !propertiesWithToValue.Equals(properties);
}

/**
 * Evenly distribute the computed offsets in (A, B).
 * We pass the range keyframes in [A, B] and use A, B to calculate distributing
 * computed offsets in (A, B). The second range, aRangeToAdjust, is passed, so
 * we can know which keyframe we want to apply to. aRangeToAdjust should be in
 * the range of aSpacingRange.
 *
 * @param aSpacingRange The sequence of keyframes between whose endpoints we
 *   should apply distribute spacing.
 * @param aRangeToAdjust The range of keyframes we want to apply to.
 */
static void
DistributeRange(const Range<Keyframe>& aSpacingRange,
                const Range<Keyframe>& aRangeToAdjust)
{
  MOZ_ASSERT(aRangeToAdjust.begin() >= aSpacingRange.begin() &&
             aRangeToAdjust.end() <= aSpacingRange.end(),
             "Out of range");
  const size_t n = aSpacingRange.length() - 1;
  const double startOffset = aSpacingRange[0].mComputedOffset;
  const double diffOffset = aSpacingRange[n].mComputedOffset - startOffset;
  for (auto iter = aRangeToAdjust.begin();
       iter != aRangeToAdjust.end();
       ++iter) {
    size_t index = iter - aSpacingRange.begin();
    iter->mComputedOffset = startOffset + double(index) / n * diffOffset;
  }
}

/**
 * Overload of DistributeRange to apply distribute spacing to all keyframes in
 * between the endpoints of the given range.
 *
 * @param aSpacingRange The sequence of keyframes between whose endpoints we
 *   should apply distribute spacing.
 */
static void
DistributeRange(const Range<Keyframe>& aSpacingRange)
{
  // We don't need to apply distribute spacing to keyframe A and keyframe B.
  DistributeRange(aSpacingRange,
                  Range<Keyframe>(aSpacingRange.begin() + 1,
                                  aSpacingRange.end() - 1));
}

/**
 * Apply paced spacing to all paceable keyframes in between the endpoints of the
 * given range.
 *
 * @param aKeyframes The range of keyframes between whose endpoints we should
 *   apply paced spacing. Both endpoints should be paceable, i.e. the
 *   corresponding elements in |aCumulativeDist| should not be kNotPaceable.
 *   Within this function, we refer to the start and end points of this range
 *   as Paced A and Paced B respectively in keeping with the notation used in
 *   the spec.
 * @param aCumulativeDistances The sequence of cumulative distances of the paced
 *   property as returned by GetCumulativeDistances(). This acts as a
 *   parallel range to |aKeyframes|.
 */
static void
PaceRange(const Range<Keyframe>& aKeyframes,
          const Range<double>& aCumulativeDistances)
{
  MOZ_ASSERT(aKeyframes.length() == aCumulativeDistances.length(),
             "Range length mismatch");

  const size_t len = aKeyframes.length();
  // If there is nothing between the end points, there is nothing to space.
  if (len < 3) {
    return;
  }

  const double distA = *(aCumulativeDistances.begin());
  const double distB = *(aCumulativeDistances.end() - 1);
  MOZ_ASSERT(distA != kNotPaceable && distB != kNotPaceable,
             "Both Paced A and Paced B should be paceable");

  // If the total distance is zero, we should fall back to distribute spacing.
  // The caller will fill-in any keyframes without a computed offset using
  // distribute spacing so we can just return here.
  if (distA == distB) {
    return;
  }

  const RangedPtr<Keyframe> pacedA = aKeyframes.begin();
  const RangedPtr<Keyframe> pacedB = aKeyframes.end() - 1;
  MOZ_ASSERT(pacedA->mComputedOffset != Keyframe::kComputedOffsetNotSet &&
             pacedB->mComputedOffset != Keyframe::kComputedOffsetNotSet,
             "Both Paced A and Paced B should have valid computed offsets");

  // Apply computed offset.
  const double offsetA     = pacedA->mComputedOffset;
  const double diffOffset  = pacedB->mComputedOffset - offsetA;
  const double initialDist = distA;
  const double totalDist   = distB - initialDist;
  for (auto iter = pacedA + 1; iter != pacedB; ++iter) {
    size_t k = iter - aKeyframes.begin();
    if (aCumulativeDistances[k] == kNotPaceable) {
      continue;
    }

    double dist = aCumulativeDistances[k] - initialDist;
    iter->mComputedOffset = offsetA + diffOffset * dist / totalDist;
  }
}

/**
 * Get cumulative distances for the paced property.
 *
 * @param aValues The computed values returned by GetComputedKeyframeValues.
 * @param aPacedProperty The paced property.
 * @param aStyleContext The style context for computing distance on transform.
 * @return The cumulative distances for the paced property. The length will be
 *   the same as aValues.
 */
static nsTArray<double>
GetCumulativeDistances(const nsTArray<ComputedKeyframeValues>& aValues,
                       nsCSSPropertyID aPacedProperty,
                       nsStyleContext* aStyleContext)
{
  // a) If aPacedProperty is a shorthand property, get its components.
  //    Otherwise, just add the longhand property into the set.
  size_t pacedPropertyCount = 0;
  nsCSSPropertyIDSet pacedPropertySet;
  bool isShorthand = nsCSSProps::IsShorthand(aPacedProperty);
  if (isShorthand) {
    CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(p, aPacedProperty,
                                         CSSEnabledState::eForAllContent) {
      pacedPropertySet.AddProperty(*p);
      ++pacedPropertyCount;
    }
  } else {
    pacedPropertySet.AddProperty(aPacedProperty);
    pacedPropertyCount = 1;
  }

  // b) Search each component (shorthand) or the longhand property, and
  //    calculate the cumulative distances of paceable keyframe pairs.
  const size_t len = aValues.Length();
  nsTArray<double> cumulativeDistances(len);
  // cumulativeDistances is a parallel array to |aValues|, so set its length to
  // the length of |aValues|.
  cumulativeDistances.SetLength(len);
  ComputedKeyframeValues prevPacedValues;
  size_t preIdx = 0;
  for (size_t i = 0; i < len; ++i) {
    // Find computed values of the paced property.
    ComputedKeyframeValues pacedValues;
    for (const PropertyStyleAnimationValuePair& pair : aValues[i]) {
      if (pacedPropertySet.HasProperty(pair.mProperty)) {
        pacedValues.AppendElement(pair);
      }
    }

    // Check we have values for all the paceable longhand components.
    if (pacedValues.Length() != pacedPropertyCount) {
      // This keyframe is not paceable, assign kNotPaceable and skip it.
      cumulativeDistances[i] = kNotPaceable;
      continue;
    }

    // Sort the pacedValues first, so the order of subproperties of
    // pacedValues is always the same as that of prevPacedValues.
    if (isShorthand) {
      pacedValues.Sort(
        TPropertyPriorityComparator<PropertyStyleAnimationValuePair>());
    }

    if (prevPacedValues.IsEmpty()) {
      // This is the first paceable keyframe so its cumulative distance is 0.0.
      cumulativeDistances[i] = 0.0;
    } else {
      double dist = 0.0;
      if (isShorthand) {
        // Apply the distance by the square root of the sum of squares of
        // longhand component distances.
        for (size_t propIdx = 0; propIdx < pacedPropertyCount; ++propIdx) {
          nsCSSPropertyID prop = prevPacedValues[propIdx].mProperty;
          MOZ_ASSERT(pacedValues[propIdx].mProperty == prop,
                     "Property mismatch");

          double componentDistance = 0.0;
          if (StyleAnimationValue::ComputeDistance(
                prop,
                prevPacedValues[propIdx].mValue,
                pacedValues[propIdx].mValue,
                aStyleContext,
                componentDistance)) {
            dist += componentDistance * componentDistance;
          }
        }
        dist = sqrt(dist);
      } else {
        // If the property is longhand, we just use the 1st value.
        // If ComputeDistance() fails, |dist| will remain zero so there will be
        // no distance between the previous paced value and this value.
        Unused <<
          StyleAnimationValue::ComputeDistance(aPacedProperty,
                                               prevPacedValues[0].mValue,
                                               pacedValues[0].mValue,
                                               aStyleContext,
                                               dist);
      }
      cumulativeDistances[i] = cumulativeDistances[preIdx] + dist;
    }
    prevPacedValues.SwapElements(pacedValues);
    preIdx = i;
  }
  return cumulativeDistances;
}

} // namespace mozilla