/* 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 "AnimatedPropertyIDSet.h"
#include "CSSPropertyId.h"
#include "mozilla/ComputedStyle.h"
#include "mozilla/EffectSet.h"
#include "mozilla/ElementAnimationData.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/RestyleManager.h"
#include "mozilla/ServoBindings.h"
#include "mozilla/StyleAnimationValue.h"
#include "mozilla/dom/CSSTransition.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentTimeline.h"
#include "mozilla/dom/Element.h"
#include "nsAnimationManager.h"
#include "nsCSSPropertyIDSet.h"
#include "nsCSSProps.h"
#include "nsDisplayList.h"
#include "nsIContent.h"
#include "nsIFrame.h"
#include "nsRFPService.h"
#include "nsStyleChangeList.h"

using mozilla::dom::CSSTransition;
using mozilla::dom::DocumentTimeline;
using mozilla::dom::KeyframeEffect;

using namespace mozilla;
using namespace mozilla::css;

bool nsTransitionManager::UpdateTransitions(
    dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest,
    const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) {
  if (mPresContext->Medium() == nsGkAtoms::print) {
    // For print or print preview, ignore transitions.
    return false;
  }

  MOZ_ASSERT(mPresContext->IsDynamic());
  if (aNewStyle.StyleDisplay()->mDisplay == StyleDisplay::None) {
    StopAnimationsForElement(aElement, aPseudoRequest);
    return false;
  }

  auto* collection = CSSTransitionCollection::Get(aElement, aPseudoRequest);
  return DoUpdateTransitions(*aNewStyle.StyleUIReset(), aElement,
                             aPseudoRequest, collection, aOldStyle, aNewStyle);
}

// This function expands the shorthands and "all" keyword specified in
// transition-property, and then execute |aHandler| on the expanded longhand.
// |aHandler| should be a lamda function which accepts NonCustomCSSPropertyId.
template <typename T>
static void ExpandTransitionProperty(const StyleTransitionProperty& aProperty,
                                     T aHandler) {
  switch (aProperty.tag) {
    case StyleTransitionProperty::Tag::Unsupported:
      break;
    case StyleTransitionProperty::Tag::Custom: {
      auto property =
          CSSPropertyId::FromCustomName(aProperty.AsCustom().AsAtom());
      aHandler(property);
      break;
    }
    case StyleTransitionProperty::Tag::NonCustom: {
      NonCustomCSSPropertyId id =
          NonCustomCSSPropertyId(aProperty.AsNonCustom()._0);
      if (nsCSSProps::IsShorthand(id)) {
        CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, id,
                                             CSSEnabledState::ForAllContent) {
          CSSPropertyId property(*subprop);
          aHandler(property);
        }
      } else {
        CSSPropertyId property(id);
        aHandler(property);
      }
      break;
    }
  }
}

bool nsTransitionManager::DoUpdateTransitions(
    const nsStyleUIReset& aStyle, dom::Element* aElement,
    const PseudoStyleRequest& aPseudoRequest,
    CSSTransitionCollection*& aElementTransitions,
    const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle) {
  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 |propertiesChecked|).
  bool startedAny = false;
  AnimatedPropertyIDSet propertiesChecked;
  for (uint32_t i = aStyle.mTransitionPropertyCount; i--;) {
    const float delay = aStyle.GetTransitionDelay(i).ToMilliseconds();

    // The spec says a negative duration is treated as zero.
    const float duration =
        std::max(aStyle.GetTransitionDuration(i).ToMilliseconds(), 0.0f);

    // If the combined duration of this transition is 0 or less we won't start a
    // transition, we can avoid even looking at transition-property if we're the
    // last one.
    if (i == 0 && delay + duration <= 0.0f) {
      continue;
    }

    const auto behavior = aStyle.GetTransitionBehavior(i);
    ExpandTransitionProperty(
        aStyle.GetTransitionProperty(i), [&](const CSSPropertyId& aProperty) {
          // We might have something to transition.  See if
          // any of the properties in question changed and
          // are animatable.
          startedAny |= ConsiderInitiatingTransition(
              aProperty, aStyle, i, delay, duration, behavior, aElement,
              aPseudoRequest, aElementTransitions, aOldStyle, aNewStyle,
              propertiesChecked);
        });
  }

  // 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.
  if (aElementTransitions) {
    const bool checkProperties = !aStyle.GetTransitionProperty(0).IsAll();
    AnimatedPropertyIDSet allTransitionProperties;
    if (checkProperties) {
      for (uint32_t i = aStyle.mTransitionPropertyCount; i-- != 0;) {
        ExpandTransitionProperty(aStyle.GetTransitionProperty(i),
                                 [&](const CSSPropertyId& aProperty) {
                                   allTransitionProperties.AddProperty(
                                       aProperty.ToPhysical(aNewStyle));
                                 });
      }
    }

    OwningCSSTransitionPtrArray& animations = aElementTransitions->mAnimations;
    size_t i = animations.Length();
    MOZ_ASSERT(i != 0, "empty transitions list?");
    AnimationValue currentValue;
    do {
      --i;
      CSSTransition* anim = animations[i];
      const CSSPropertyId& property = anim->TransitionProperty();
      if (
          // Properties no longer in `transition-property`.
          (checkProperties && !allTransitionProperties.HasProperty(property)) ||
          // 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.
          !Servo_ComputedValues_TransitionValueMatches(
              &aNewStyle, &property, anim->ToValue().mServo.get())) {
        // Stop the transition.
        DoCancelTransition(aElement, aPseudoRequest, aElementTransitions, i);
      }
    } while (i != 0);
  }

  return startedAny;
}

static Keyframe& AppendKeyframe(double aOffset, const CSSPropertyId& aProperty,
                                AnimationValue&& aValue,
                                nsTArray<Keyframe>& aKeyframes) {
  Keyframe& frame = *aKeyframes.AppendElement();
  frame.mOffset.emplace(Keyframe::OffsetType::PercentageOffset(aOffset));
  MOZ_ASSERT(aValue.mServo);
  RefPtr<StyleLockedDeclarationBlock> decl =
      Servo_AnimationValue_Uncompute(aValue.mServo).Consume();
  frame.mPropertyValues.AppendElement(
      PropertyValuePair(aProperty, std::move(decl)));
  return frame;
}

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

  AppendKeyframe(0.0, aProperty, std::move(aStartValue), keyframes);
  AppendKeyframe(1.0, aProperty, std::move(aEndValue), keyframes);

  return keyframes;
}

using ReplacedTransitionProperties =
    CSSTransition::ReplacedTransitionProperties;
static Maybe<ReplacedTransitionProperties> GetReplacedTransitionProperties(
    const CSSTransition& aTransition,
    const DocumentTimeline* aTimelineToMatch) {
  Maybe<ReplacedTransitionProperties> result;

  if (!aTransition.HasCurrentEffect()) {
    return result;
  }

  // Transition needs to be running on the same timeline.
  if (aTransition.GetTimeline() != aTimelineToMatch) {
    return result;
  }

  auto startTime = aTransition.GetStartTime();
  if (startTime.IsNull() && !aTransition.GetPendingReadyTime().IsNull()) {
    startTime =
        aTimelineToMatch->ToTimelineTime(aTransition.GetPendingReadyTime());
  }

  if (startTime.IsNull()) {
    return result;
  }

  // The transition needs to have a keyframe effect.
  const KeyframeEffect* keyframeEffect =
      aTransition.GetEffect() ? aTransition.GetEffect()->AsKeyframeEffect()
                              : nullptr;
  if (!keyframeEffect) {
    return result;
  }

  // The keyframe effect needs to be a simple transition of the original
  // transition property (i.e. not replaced with something else).
  if (keyframeEffect->Properties().Length() != 1 ||
      keyframeEffect->Properties()[0].mSegments.Length() != 1 ||
      keyframeEffect->Properties()[0].mProperty !=
          aTransition.TransitionProperty()) {
    return result;
  }

  const AnimationPropertySegment& segment =
      keyframeEffect->Properties()[0].mSegments[0];

  result.emplace(ReplacedTransitionProperties(
      {startTime.Value(), aTransition.PlaybackRateInternal(),
       keyframeEffect->SpecifiedTiming(), segment.mTimingFunction,
       segment.mFromValue, segment.mToValue}));

  return result;
}

bool nsTransitionManager::ConsiderInitiatingTransition(
    const CSSPropertyId& aProperty, const nsStyleUIReset& aStyle,
    uint32_t aTransitionIndex, float aDelay, float aDuration,
    mozilla::StyleTransitionBehavior aBehavior, dom::Element* aElement,
    const PseudoStyleRequest& aPseudoRequest,
    CSSTransitionCollection*& aElementTransitions,
    const ComputedStyle& aOldStyle, const ComputedStyle& aNewStyle,
    AnimatedPropertyIDSet& aPropertiesChecked) {
  // IsShorthand itself will assert if aProperty is not a property.
  MOZ_ASSERT(aProperty.IsCustom() || !nsCSSProps::IsShorthand(aProperty.mId),
             "property out of range");
  NS_ASSERTION(
      !aElementTransitions || &aElementTransitions->mElement == aElement,
      "Element mismatch");

  CSSPropertyId property = aProperty.ToPhysical(aNewStyle);

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

  aPropertiesChecked.AddProperty(property);

  if (aDuration + aDelay <= 0.0f) {
    return false;
  }

  size_t currentIndex = nsTArray<KeyframeEffect>::NoIndex;
  const auto* oldTransition = [&]() -> const CSSTransition* {
    if (!aElementTransitions) {
      return nullptr;
    }
    const OwningCSSTransitionPtrArray& animations =
        aElementTransitions->mAnimations;
    for (size_t i = 0, i_end = animations.Length(); i < i_end; ++i) {
      if (animations[i]->TransitionProperty() == property) {
        currentIndex = i;
        return animations[i];
      }
    }
    return nullptr;
  }();

  // For compositor animations, |aOldStyle| may have out-of-date transition
  // rules, and it may be equal to the |endValue| of a reversing transition by
  // accidentally. This causes Servo_ComputedValues_ShouldTransition() to return
  // an incorrect result. Therefore, we have to recompute the current value if
  // this transition is running on the compositor, to make sure we create the
  // transition properly. Here, we pre-compute the progress and collect the
  // necessary info, so Servo_ComputedValues_ShouldTransition() could compute
  // the current value if needed.
  // FIXME: Bug 1634945. We should use the last value from the compositor as the
  // current value.
  Maybe<ReplacedTransitionProperties> replacedTransitionProperties;
  Maybe<double> progress;
  if (oldTransition) {
    // If this new transition is replacing an existing transition, 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.
    //
    // Note: We compute this before calling
    // Servo_ComputedValues_ShouldTransition() so we can reuse it for computing
    // the current value and setting the replaced transition properties later in
    // this function.
    const dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline();
    replacedTransitionProperties =
        GetReplacedTransitionProperties(*oldTransition, timeline);
    progress = replacedTransitionProperties.andThen(
        [&](const ReplacedTransitionProperties& aProperties) {
          const dom::AnimationTimeline* timeline = oldTransition->GetTimeline();
          MOZ_ASSERT(timeline);
          return CSSTransition::ComputeTransformedProgress(*timeline,
                                                           aProperties);
        });
  }

  AnimationValue startValue, endValue;
  const StyleShouldTransitionResult result =
      Servo_ComputedValues_ShouldTransition(
          &aOldStyle, &aNewStyle, &property, aBehavior,
          oldTransition ? oldTransition->ToValue().mServo.get() : nullptr,
          replacedTransitionProperties
              ? replacedTransitionProperties->mFromValue.mServo.get()
              : nullptr,
          // Note: It's possible to replace the keyframes by Web Animations API,
          // so we have to pass the mToValue from the keyframe segment, to make
          // sure this value is aligned with mFromValue.
          replacedTransitionProperties
              ? replacedTransitionProperties->mToValue.mServo.get()
              : nullptr,
          progress.ptrOr(nullptr), &startValue.mServo, &endValue.mServo);

  // 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 (result.old_transition_value_matches) {
    // GetAnimationRule already called RestyleForAnimation.
    return false;
  }

  if (!result.should_animate) {
    if (oldTransition) {
      // 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.
      DoCancelTransition(aElement, aPseudoRequest, aElementTransitions,
                         currentIndex);
    }
    return false;
  }

  AnimationValue startForReversingTest = startValue;
  double reversePortion = 1.0;

  // If the new transition reverses an existing one, we'll need to
  // handle the timing differently.
  if (oldTransition && oldTransition->HasCurrentEffect() &&
      oldTransition->StartForReversingTest() == endValue) {
    // Compute the appropriate negative transition-delay such that right
    // now we'd end up at the current position.
    double valuePortion =
        oldTransition->CurrentValuePortion() * oldTransition->ReversePortion() +
        (1.0 - oldTransition->ReversePortion());
    // 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 (aDelay < 0.0f && std::isfinite(aDelay)) {
      aDelay *= valuePortion;
    }

    if (std::isfinite(aDuration)) {
      aDuration *= valuePortion;
    }

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

  TimingParams timing = TimingParamsFromCSSParams(
      Some(aDuration), aDelay, 1.0 /* iteration count */,
      StyleAnimationDirection::Normal, StyleAnimationFillMode::Backwards);

  const StyleComputedTimingFunction& tf =
      aStyle.GetTransitionTimingFunction(aTransitionIndex);
  if (!tf.IsLinearKeyword()) {
    timing.SetTimingFunction(Some(tf));
  }

  RefPtr<CSSTransition> transition = DoCreateTransition(
      property, aElement, aPseudoRequest, aNewStyle, aElementTransitions,
      std::move(timing), std::move(startValue), std::move(endValue),
      std::move(startForReversingTest), reversePortion);
  if (!transition) {
    return false;
  }

  OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations;
#ifdef DEBUG
  for (size_t i = 0, i_end = transitions.Length(); i < i_end; ++i) {
    MOZ_ASSERT(
        i == currentIndex || transitions[i]->TransitionProperty() != property,
        "duplicate transitions for property");
  }
#endif
  if (oldTransition) {
    if (replacedTransitionProperties) {
      transition->SetReplacedTransition(
          std::move(replacedTransitionProperties.ref()));
    }

    transitions[currentIndex]->CancelFromStyle(PostRestyleMode::IfNeeded);
    oldTransition = nullptr;  // Clear pointer so it doesn't dangle
    transitions[currentIndex] = transition;
  } else {
    // XXX(Bug 1631371) Check if this should use a fallible operation as it
    // pretended earlier.
    transitions.AppendElement(transition);
  }

  if (auto* effectSet = EffectSet::Get(aElement, aPseudoRequest)) {
    effectSet->UpdateAnimationGeneration(mPresContext);
  }

  return true;
}

already_AddRefed<CSSTransition> nsTransitionManager::DoCreateTransition(
    const CSSPropertyId& aProperty, dom::Element* aElement,
    const PseudoStyleRequest& aPseudoRequest,
    const mozilla::ComputedStyle& aNewStyle,
    CSSTransitionCollection*& aElementTransitions, TimingParams&& aTiming,
    AnimationValue&& aStartValue, AnimationValue&& aEndValue,
    AnimationValue&& aStartForReversingTest, double aReversePortion) {
  dom::DocumentTimeline* timeline = aElement->OwnerDoc()->Timeline();
  KeyframeEffectParams effectOptions;
  auto keyframeEffect = MakeRefPtr<KeyframeEffect>(
      aElement->OwnerDoc(), OwningAnimationTarget(aElement, aPseudoRequest),
      std::move(aTiming), effectOptions);

  keyframeEffect->SetKeyframes(
      GetTransitionKeyframes(aProperty, std::move(aStartValue),
                             std::move(aEndValue)),
      &aNewStyle, timeline, nullptr /* No AnimationRange for transitions */);

  if (NS_WARN_IF(MOZ_UNLIKELY(!keyframeEffect->IsValidTransition()))) {
    return nullptr;
  }

  auto animation = MakeRefPtr<CSSTransition>(
      mPresContext->Document()->GetScopeObject(), aProperty);
  animation->SetOwningElement(OwningElementRef(*aElement, aPseudoRequest));
  animation->SetTimelineNoUpdate(timeline, nullptr,
                                 mozilla::dom::Animation::FromJS::No);
  animation->SetCreationSequence(
      mPresContext->RestyleManager()->GetAnimationGeneration());
  animation->SetEffectFromStyle(keyframeEffect);
  animation->SetReverseParameters(std::move(aStartForReversingTest),
                                  aReversePortion);
  animation->PlayFromStyle();

  if (!aElementTransitions) {
    aElementTransitions =
        &aElement->EnsureAnimationData().EnsureTransitionCollection(
            *aElement, aPseudoRequest);
    if (!aElementTransitions->isInList()) {
      AddElementCollection(aElementTransitions);
    }
  }
  return animation.forget();
}

void nsTransitionManager::DoCancelTransition(
    dom::Element* aElement, const PseudoStyleRequest& aPseudoRequest,
    CSSTransitionCollection*& aElementTransitions, size_t aIndex) {
  MOZ_ASSERT(aElementTransitions);
  OwningCSSTransitionPtrArray& transitions = aElementTransitions->mAnimations;
  CSSTransition* transition = transitions[aIndex];

  if (transition->HasCurrentEffect()) {
    if (auto* effectSet = EffectSet::Get(aElement, aPseudoRequest)) {
      effectSet->UpdateAnimationGeneration(mPresContext);
    }
  }
  transition->CancelFromStyle(PostRestyleMode::IfNeeded);
  transitions.RemoveElementAt(aIndex);

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