/* -*- 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/. */

#include "AnimationHelper.h"
#include "mozilla/ComputedTimingFunction.h" // for ComputedTimingFunction
#include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // for dom::FillMode
#include "mozilla/dom/KeyframeEffectBinding.h" // for dom::IterationComposite
#include "mozilla/dom/KeyframeEffectReadOnly.h" // for dom::KeyFrameEffectReadOnly
#include "mozilla/gfx/gfxVars.h"        // for USE_STYLO_ON_COMPOSITOR
#include "mozilla/layers/CompositorThread.h" // for CompositorThreadHolder
#include "mozilla/layers/LayerAnimationUtils.h" // for TimingFunctionToComputedTimingFunction
#include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc
#include "nsDeviceContext.h"            // for AppUnitsPerCSSPixel
#include "nsDisplayList.h"              // for nsDisplayTransform, etc

namespace mozilla {
namespace layers {

void
CompositorAnimationStorage::Clear()
{
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());

  mAnimatedValues.Clear();
  mAnimations.Clear();
}

void
CompositorAnimationStorage::ClearById(const uint64_t& aId)
{
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());

  mAnimatedValues.Remove(aId);
  mAnimations.Remove(aId);
}

AnimatedValue*
CompositorAnimationStorage::GetAnimatedValue(const uint64_t& aId) const
{
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
  return mAnimatedValues.Get(aId);
}

Maybe<float>
CompositorAnimationStorage::GetAnimationOpacity(const uint64_t& aId) const
{
  auto value = GetAnimatedValue(aId);
  if (!value || value->mType != AnimatedValue::OPACITY) {
    return Nothing();
  }

  return Some(value->mOpacity);
}

Maybe<gfx::Matrix4x4>
CompositorAnimationStorage::GetAnimationTransform(const uint64_t& aId) const
{
  auto value = GetAnimatedValue(aId);
  if (!value || value->mType != AnimatedValue::TRANSFORM) {
    return Nothing();
  }

  gfx::Matrix4x4 transform = value->mTransform.mFrameTransform;
  const TransformData& data = value->mTransform.mData;
  float scale = data.appUnitsPerDevPixel();
  gfx::Point3D transformOrigin = data.transformOrigin();

  // Undo the rebasing applied by
  // nsDisplayTransform::GetResultingTransformMatrixInternal
  transform.ChangeBasis(-transformOrigin);

  // Convert to CSS pixels (this undoes the operations performed by
  // nsStyleTransformMatrix::ProcessTranslatePart which is called from
  // nsDisplayTransform::GetResultingTransformMatrix)
  double devPerCss =
    double(scale) / double(nsDeviceContext::AppUnitsPerCSSPixel());
  transform._41 *= devPerCss;
  transform._42 *= devPerCss;
  transform._43 *= devPerCss;

  return Some(transform);
}

void
CompositorAnimationStorage::SetAnimatedValue(uint64_t aId,
                                             gfx::Matrix4x4&& aTransformInDevSpace,
                                             gfx::Matrix4x4&& aFrameTransform,
                                             const TransformData& aData)
{
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
  AnimatedValue* value = new AnimatedValue(Move(aTransformInDevSpace),
                                           Move(aFrameTransform),
                                           aData);
  mAnimatedValues.Put(aId, value);
}

void
CompositorAnimationStorage::SetAnimatedValue(uint64_t aId,
                                             gfx::Matrix4x4&& aTransformInDevSpace)
{
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
  const TransformData dontCare = {};
  AnimatedValue* value = new AnimatedValue(Move(aTransformInDevSpace),
                                           gfx::Matrix4x4(),
                                           dontCare);
  mAnimatedValues.Put(aId, value);
}

void
CompositorAnimationStorage::SetAnimatedValue(uint64_t aId,
                                             const float& aOpacity)
{
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
  AnimatedValue* value = new AnimatedValue(aOpacity);
  mAnimatedValues.Put(aId, value);
}

AnimationArray*
CompositorAnimationStorage::GetAnimations(const uint64_t& aId) const
{
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
  return mAnimations.Get(aId);
}

void
CompositorAnimationStorage::SetAnimations(uint64_t aId, const AnimationArray& aValue)
{
  MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
  AnimationArray* value = new AnimationArray(aValue);
  mAnimations.Put(aId, value);
}

static StyleAnimationValue
SampleValue(double aPortion, const layers::Animation& aAnimation,
            const AnimationPropertySegment&& aSegment,
            const StyleAnimationValue& aLastValue,
            uint64_t aCurrentIteration,
            const StyleAnimationValue& aUnderlyingValue)
{
  NS_ASSERTION(aSegment.mFromValue.mGecko.IsNull() ||
               aSegment.mToValue.mGecko.IsNull() ||
               aSegment.mFromValue.mGecko.GetUnit() ==
                 aSegment.mToValue.mGecko.GetUnit(),
               "Must have same unit");

  StyleAnimationValue startValue =
    dom::KeyframeEffectReadOnly::CompositeValue(aAnimation.property(),
                                                aSegment.mFromValue.mGecko,
                                                aUnderlyingValue,
                                                aSegment.mFromComposite);
  StyleAnimationValue endValue =
    dom::KeyframeEffectReadOnly::CompositeValue(aAnimation.property(),
                                                aSegment.mToValue.mGecko,
                                                aUnderlyingValue,
                                                aSegment.mToComposite);

  // Iteration composition for accumulate
  if (static_cast<dom::IterationCompositeOperation>
        (aAnimation.iterationComposite()) ==
          dom::IterationCompositeOperation::Accumulate &&
      aCurrentIteration > 0) {
    // FIXME: Bug 1293492: Add a utility function to calculate both of
    // below StyleAnimationValues.
    startValue =
      StyleAnimationValue::Accumulate(aAnimation.property(),
                                      aLastValue.IsNull()
                                        ? aUnderlyingValue
                                        : aLastValue,
                                      Move(startValue),
                                      aCurrentIteration);
    endValue =
      StyleAnimationValue::Accumulate(aAnimation.property(),
                                      aLastValue.IsNull()
                                        ? aUnderlyingValue
                                        : aLastValue,
                                      Move(endValue),
                                      aCurrentIteration);
  }

  StyleAnimationValue interpolatedValue;
  // This should never fail because we only pass transform and opacity values
  // to the compositor and they should never fail to interpolate.
  DebugOnly<bool> uncomputeResult =
    StyleAnimationValue::Interpolate(aAnimation.property(),
                                     startValue, endValue,
                                     aPortion, interpolatedValue);
  MOZ_ASSERT(uncomputeResult, "could not uncompute value");
  return interpolatedValue;
}

bool
AnimationHelper::SampleAnimationForEachNode(
  TimeStamp aTime,
  AnimationArray& aAnimations,
  InfallibleTArray<AnimData>& aAnimationData,
  AnimationValue& aAnimationValue,
  bool& aHasInEffectAnimations)
{
  bool activeAnimations = false;

  if (aAnimations.IsEmpty()) {
    return activeAnimations;
  }

  // Process in order, since later aAnimations override earlier ones.
  for (size_t i = 0, iEnd = aAnimations.Length(); i < iEnd; ++i) {
    Animation& animation = aAnimations[i];
    AnimData& animData = aAnimationData[i];

    activeAnimations = true;

    MOZ_ASSERT((!animation.originTime().IsNull() &&
                animation.startTime().type() ==
                  MaybeTimeDuration::TTimeDuration) ||
               animation.isNotPlaying(),
               "If we are playing, we should have an origin time and a start"
               " time");
    // If the animation is not currently playing, e.g. paused or
    // finished, then use the hold time to stay at the same position.
    TimeDuration elapsedDuration =
      animation.isNotPlaying() ||
      animation.startTime().type() != MaybeTimeDuration::TTimeDuration
      ? animation.holdTime()
      : (aTime - animation.originTime() -
         animation.startTime().get_TimeDuration())
        .MultDouble(animation.playbackRate());
    TimingParams timing {
      animation.duration(),
      animation.delay(),
      animation.endDelay(),
      animation.iterations(),
      animation.iterationStart(),
      static_cast<dom::PlaybackDirection>(animation.direction()),
      static_cast<dom::FillMode>(animation.fillMode()),
      Move(AnimationUtils::TimingFunctionToComputedTimingFunction(
           animation.easingFunction()))
    };

    ComputedTiming computedTiming =
      dom::AnimationEffectReadOnly::GetComputedTimingAt(
        Nullable<TimeDuration>(elapsedDuration), timing,
        animation.playbackRate());

    if (computedTiming.mProgress.IsNull()) {
      continue;
    }

    uint32_t segmentIndex = 0;
    size_t segmentSize = animation.segments().Length();
    AnimationSegment* segment = animation.segments().Elements();
    while (segment->endPortion() < computedTiming.mProgress.Value() &&
           segmentIndex < segmentSize - 1) {
      ++segment;
      ++segmentIndex;
    }

    double positionInSegment =
      (computedTiming.mProgress.Value() - segment->startPortion()) /
      (segment->endPortion() - segment->startPortion());

    double portion =
      ComputedTimingFunction::GetPortion(animData.mFunctions[segmentIndex],
                                         positionInSegment,
                                     computedTiming.mBeforeFlag);

    AnimationPropertySegment animSegment;
    animSegment.mFromKey = 0.0;
    animSegment.mToKey = 1.0;
    animSegment.mFromValue = animData.mStartValues[segmentIndex];
    animSegment.mToValue = animData.mEndValues[segmentIndex];
    animSegment.mFromComposite =
      static_cast<dom::CompositeOperation>(segment->startComposite());
    animSegment.mToComposite =
      static_cast<dom::CompositeOperation>(segment->endComposite());

    // interpolate the property
    if (USE_STYLO_ON_COMPOSITOR) {
      dom::IterationCompositeOperation iterCompositeOperation =
          static_cast<dom::IterationCompositeOperation>(
            animation.iterationComposite());

      aAnimationValue.mServo =
        Servo_ComposeAnimationSegment(
          &animSegment,
          aAnimationValue.mServo,
          animData.mEndValues.LastElement().mServo,
          iterCompositeOperation,
          portion,
          computedTiming.mCurrentIteration).Consume();
    } else {
      aAnimationValue.mGecko =
        SampleValue(portion,
                    animation,
                    Move(animSegment),
                    animData.mEndValues.LastElement().mGecko,
                    computedTiming.mCurrentIteration,
                    aAnimationValue.mGecko);
    }
    aHasInEffectAnimations = true;
  }

#ifdef DEBUG
  // Sanity check that all of animation data are the same.
  const AnimationData& lastData = aAnimations.LastElement().data();
  for (const Animation& animation : aAnimations) {
    const AnimationData& data = animation.data();
    MOZ_ASSERT(data.type() == lastData.type(),
               "The type of AnimationData should be the same");
    if (data.type() == AnimationData::Tnull_t) {
      continue;
    }

    MOZ_ASSERT(data.type() == AnimationData::TTransformData);
    const TransformData& transformData = data.get_TransformData();
    const TransformData& lastTransformData = lastData.get_TransformData();
    MOZ_ASSERT(transformData.origin() == lastTransformData.origin() &&
               transformData.transformOrigin() ==
                 lastTransformData.transformOrigin() &&
               transformData.bounds() == lastTransformData.bounds() &&
               transformData.appUnitsPerDevPixel() ==
                 lastTransformData.appUnitsPerDevPixel(),
               "All of members of TransformData should be the same");
  }
#endif
  return activeAnimations;
}

static inline void
SetCSSAngle(const CSSAngle& aAngle, nsCSSValue& aValue)
{
  aValue.SetFloatValue(aAngle.value(), nsCSSUnit(aAngle.unit()));
}

static nsCSSValueSharedList*
CreateCSSValueList(const InfallibleTArray<TransformFunction>& aFunctions)
{
  nsAutoPtr<nsCSSValueList> result;
  nsCSSValueList** resultTail = getter_Transfers(result);
  for (uint32_t i = 0; i < aFunctions.Length(); i++) {
    RefPtr<nsCSSValue::Array> arr;
    switch (aFunctions[i].type()) {
      case TransformFunction::TRotationX:
      {
        const CSSAngle& angle = aFunctions[i].get_RotationX().angle();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_rotatex,
                                                    resultTail);
        SetCSSAngle(angle, arr->Item(1));
        break;
      }
      case TransformFunction::TRotationY:
      {
        const CSSAngle& angle = aFunctions[i].get_RotationY().angle();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_rotatey,
                                                      resultTail);
        SetCSSAngle(angle, arr->Item(1));
        break;
      }
      case TransformFunction::TRotationZ:
      {
        const CSSAngle& angle = aFunctions[i].get_RotationZ().angle();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_rotatez,
                                                      resultTail);
        SetCSSAngle(angle, arr->Item(1));
        break;
      }
      case TransformFunction::TRotation:
      {
        const CSSAngle& angle = aFunctions[i].get_Rotation().angle();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_rotate,
                                                      resultTail);
        SetCSSAngle(angle, arr->Item(1));
        break;
      }
      case TransformFunction::TRotation3D:
      {
        float x = aFunctions[i].get_Rotation3D().x();
        float y = aFunctions[i].get_Rotation3D().y();
        float z = aFunctions[i].get_Rotation3D().z();
        const CSSAngle& angle = aFunctions[i].get_Rotation3D().angle();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_rotate3d,
                                                      resultTail);
        arr->Item(1).SetFloatValue(x, eCSSUnit_Number);
        arr->Item(2).SetFloatValue(y, eCSSUnit_Number);
        arr->Item(3).SetFloatValue(z, eCSSUnit_Number);
        SetCSSAngle(angle, arr->Item(4));
        break;
      }
      case TransformFunction::TScale:
      {
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_scale3d,
                                                      resultTail);
        arr->Item(1).SetFloatValue(aFunctions[i].get_Scale().x(), eCSSUnit_Number);
        arr->Item(2).SetFloatValue(aFunctions[i].get_Scale().y(), eCSSUnit_Number);
        arr->Item(3).SetFloatValue(aFunctions[i].get_Scale().z(), eCSSUnit_Number);
        break;
      }
      case TransformFunction::TTranslation:
      {
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_translate3d,
                                                      resultTail);
        arr->Item(1).SetFloatValue(aFunctions[i].get_Translation().x(), eCSSUnit_Pixel);
        arr->Item(2).SetFloatValue(aFunctions[i].get_Translation().y(), eCSSUnit_Pixel);
        arr->Item(3).SetFloatValue(aFunctions[i].get_Translation().z(), eCSSUnit_Pixel);
        break;
      }
      case TransformFunction::TSkewX:
      {
        const CSSAngle& x = aFunctions[i].get_SkewX().x();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_skewx,
                                                      resultTail);
        SetCSSAngle(x, arr->Item(1));
        break;
      }
      case TransformFunction::TSkewY:
      {
        const CSSAngle& y = aFunctions[i].get_SkewY().y();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_skewy,
                                                      resultTail);
        SetCSSAngle(y, arr->Item(1));
        break;
      }
      case TransformFunction::TSkew:
      {
        const CSSAngle& x = aFunctions[i].get_Skew().x();
        const CSSAngle& y = aFunctions[i].get_Skew().y();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_skew,
                                                      resultTail);
        SetCSSAngle(x, arr->Item(1));
        SetCSSAngle(y, arr->Item(2));
        break;
      }
      case TransformFunction::TTransformMatrix:
      {
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_matrix3d,
                                                      resultTail);
        const gfx::Matrix4x4& matrix = aFunctions[i].get_TransformMatrix().value();
        arr->Item(1).SetFloatValue(matrix._11, eCSSUnit_Number);
        arr->Item(2).SetFloatValue(matrix._12, eCSSUnit_Number);
        arr->Item(3).SetFloatValue(matrix._13, eCSSUnit_Number);
        arr->Item(4).SetFloatValue(matrix._14, eCSSUnit_Number);
        arr->Item(5).SetFloatValue(matrix._21, eCSSUnit_Number);
        arr->Item(6).SetFloatValue(matrix._22, eCSSUnit_Number);
        arr->Item(7).SetFloatValue(matrix._23, eCSSUnit_Number);
        arr->Item(8).SetFloatValue(matrix._24, eCSSUnit_Number);
        arr->Item(9).SetFloatValue(matrix._31, eCSSUnit_Number);
        arr->Item(10).SetFloatValue(matrix._32, eCSSUnit_Number);
        arr->Item(11).SetFloatValue(matrix._33, eCSSUnit_Number);
        arr->Item(12).SetFloatValue(matrix._34, eCSSUnit_Number);
        arr->Item(13).SetFloatValue(matrix._41, eCSSUnit_Number);
        arr->Item(14).SetFloatValue(matrix._42, eCSSUnit_Number);
        arr->Item(15).SetFloatValue(matrix._43, eCSSUnit_Number);
        arr->Item(16).SetFloatValue(matrix._44, eCSSUnit_Number);
        break;
      }
      case TransformFunction::TPerspective:
      {
        float perspective = aFunctions[i].get_Perspective().value();
        arr = AnimationValue::AppendTransformFunction(eCSSKeyword_perspective,
                                                      resultTail);
        arr->Item(1).SetFloatValue(perspective, eCSSUnit_Pixel);
        break;
      }
      default:
        NS_ASSERTION(false, "All functions should be implemented?");
    }
  }
  if (aFunctions.Length() == 0) {
    result = new nsCSSValueList();
    result->mValue.SetNoneValue();
  }
  return new nsCSSValueSharedList(result.forget());
}

static AnimationValue
ToAnimationValue(const Animatable& aAnimatable)
{
  StyleBackendType backend = USE_STYLO_ON_COMPOSITOR
                             ? StyleBackendType::Servo
                             : StyleBackendType::Gecko;
  AnimationValue result;

  switch (aAnimatable.type()) {
    case Animatable::Tnull_t:
      break;
    case Animatable::TArrayOfTransformFunction: {
        const InfallibleTArray<TransformFunction>& transforms =
          aAnimatable.get_ArrayOfTransformFunction();
        RefPtr<nsCSSValueSharedList> list(CreateCSSValueList(transforms));
        MOZ_ASSERT(list, "Transform list should be non null");
        result = AnimationValue::Transform(backend, *list);
      }
      break;
    case Animatable::Tfloat:
      result = AnimationValue::Opacity(backend, aAnimatable.get_float());
      break;
    default:
      MOZ_ASSERT_UNREACHABLE("Unsupported type");
  }

  return result;
}

void
AnimationHelper::SetAnimations(AnimationArray& aAnimations,
                               InfallibleTArray<AnimData>& aAnimData,
                               AnimationValue& aBaseAnimationStyle)
{
  for (uint32_t i = 0; i < aAnimations.Length(); i++) {
    Animation& animation = aAnimations[i];
    // Adjust fill mode to fill forwards so that if the main thread is delayed
    // in clearing this animation we don't introduce flicker by jumping back to
    // the old underlying value
    switch (static_cast<dom::FillMode>(animation.fillMode())) {
      case dom::FillMode::None:
        animation.fillMode() = static_cast<uint8_t>(dom::FillMode::Forwards);
        break;
      case dom::FillMode::Backwards:
        animation.fillMode() = static_cast<uint8_t>(dom::FillMode::Both);
        break;
      default:
        break;
    }

    if (animation.baseStyle().type() != Animatable::Tnull_t) {
      aBaseAnimationStyle = ToAnimationValue(animation.baseStyle());
    }

    AnimData* data = aAnimData.AppendElement();
    InfallibleTArray<Maybe<ComputedTimingFunction>>& functions =
      data->mFunctions;
    InfallibleTArray<AnimationValue>& startValues = data->mStartValues;
    InfallibleTArray<AnimationValue>& endValues = data->mEndValues;

    const InfallibleTArray<AnimationSegment>& segments = animation.segments();
    for (const AnimationSegment& segment : segments) {
      startValues.AppendElement(ToAnimationValue(segment.startState()));
      endValues.AppendElement(ToAnimationValue(segment.endState()));

      TimingFunction tf = segment.sampleFn();
      Maybe<ComputedTimingFunction> ctf =
        AnimationUtils::TimingFunctionToComputedTimingFunction(tf);
      functions.AppendElement(ctf);
    }
  }
}

uint64_t
AnimationHelper::GetNextCompositorAnimationsId()
{
  static uint32_t sNextId = 0;
  ++sNextId;

  uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
  uint64_t nextId = procId;
  nextId = nextId << 32 | sNextId;
  return nextId;
}

void
AnimationHelper::SampleAnimations(CompositorAnimationStorage* aStorage,
                                  TimeStamp aTime)
{
  MOZ_ASSERT(aStorage);

  // Do nothing if there are no compositor animations
  if (!aStorage->AnimationsCount()) {
    return;
  }

  //Sample the animations in CompositorAnimationStorage
  for (auto iter = aStorage->ConstAnimationsTableIter();
       !iter.Done(); iter.Next()) {
    bool hasInEffectAnimations = false;
    AnimationArray* animations = iter.UserData();
    AnimationValue animationValue;
    InfallibleTArray<AnimData> animationData;
    AnimationHelper::SetAnimations(*animations,
                                   animationData,
                                   animationValue);
    AnimationHelper::SampleAnimationForEachNode(aTime,
                                                *animations,
                                                animationData,
                                                animationValue,
                                                hasInEffectAnimations);

    if (!hasInEffectAnimations) {
      continue;
    }

    // Store the AnimatedValue
    Animation& animation = animations->LastElement();
    switch (animation.property()) {
      case eCSSProperty_opacity: {
        aStorage->SetAnimatedValue(iter.Key(), animationValue.GetOpacity());
        break;
      }
      case eCSSProperty_transform: {
        RefPtr<const nsCSSValueSharedList> list =
          animationValue.GetTransformList();
        const TransformData& transformData = animation.data().get_TransformData();
        nsPoint origin = transformData.origin();
        // we expect all our transform data to arrive in device pixels
        gfx::Point3D transformOrigin = transformData.transformOrigin();
        nsDisplayTransform::FrameTransformProperties props(Move(list),
                                                           transformOrigin);

        gfx::Matrix4x4 transform =
          nsDisplayTransform::GetResultingTransformMatrix(props, origin,
                                                          transformData.appUnitsPerDevPixel(),
                                                          0, &transformData.bounds());
        gfx::Matrix4x4 frameTransform = transform;
        // If the parent has perspective transform, then the offset into reference
        // frame coordinates is already on this transform. If not, then we need to ask
        // for it to be added here.
        if (!transformData.hasPerspectiveParent()) {
           nsLayoutUtils::PostTranslate(transform, origin,
                                        transformData.appUnitsPerDevPixel(),
                                        true);
        }

        transform.PostScale(transformData.inheritedXScale(),
                            transformData.inheritedYScale(),
                            1);

        aStorage->SetAnimatedValue(iter.Key(),
                                   Move(transform), Move(frameTransform),
                                   transformData);
        break;
      }
      default:
        MOZ_ASSERT_UNREACHABLE("Unhandled animated property");
    }
  }
}

} // namespace layers
} // namespace mozilla
