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

#include "mozilla/ElementAnimationData.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/ScrollTimeline.h"
#include "mozilla/dom/ViewTimeline.h"
#include "nsPresContext.h"

namespace mozilla {
using dom::Element;
using dom::ScrollTimeline;
using dom::ViewTimeline;

template <typename TimelineType>
static void TryDestroyTimeline(Element* aElement,
                               const PseudoStyleRequest& aPseudoRequest) {
  auto* collection =
      TimelineCollection<TimelineType>::Get(aElement, aPseudoRequest);
  if (!collection) {
    return;
  }
  collection->Destroy();
}

void TimelineManager::UpdateTimelines(Element* aElement,
                                      const PseudoStyleRequest& aPseudoRequest,
                                      const ComputedStyle* aComputedStyle,
                                      ProgressTimelineType aType) {
  MOZ_ASSERT(
      aElement->IsInComposedDoc(),
      "No need to update timelines that are not attached to the document tree");

  // If we are in a display:none subtree we will have no computed values.
  // However, if we are on the root of display:none subtree, the computed values
  // might not have been cleared yet. In either case, since CSS animations
  // should not run in display:none subtrees, so we don't need timeline, either.
  const bool shouldDestroyTimelines =
      !aComputedStyle ||
      aComputedStyle->StyleDisplay()->mDisplay == StyleDisplay::None;

  switch (aType) {
    case ProgressTimelineType::Scroll:
      if (shouldDestroyTimelines) {
        TryDestroyTimeline<ScrollTimeline>(aElement, aPseudoRequest);
        return;
      }
      DoUpdateTimelines<StyleScrollTimeline, ScrollTimeline>(
          mPresContext, aElement, aPseudoRequest,
          aComputedStyle->StyleUIReset()->mScrollTimelines,
          aComputedStyle->StyleUIReset()->mScrollTimelineNameCount);
      break;

    case ProgressTimelineType::View:
      if (shouldDestroyTimelines) {
        TryDestroyTimeline<ViewTimeline>(aElement, aPseudoRequest);
        return;
      }
      DoUpdateTimelines<StyleViewTimeline, ViewTimeline>(
          mPresContext, aElement, aPseudoRequest,
          aComputedStyle->StyleUIReset()->mViewTimelines,
          aComputedStyle->StyleUIReset()->mViewTimelineNameCount);
      break;
  }
}

template <typename TimelineType>
static already_AddRefed<TimelineType> PopExistingTimeline(
    nsAtom* aName, TimelineCollection<TimelineType>* aCollection) {
  if (!aCollection) {
    return nullptr;
  }
  return aCollection->Extract(aName);
}

template <typename StyleType, typename TimelineType>
static auto BuildTimelines(nsPresContext* aPresContext, Element* aElement,
                           const PseudoStyleRequest& aPseudoRequest,
                           const nsStyleAutoArray<StyleType>& aTimelines,
                           size_t aTimelineCount,
                           TimelineCollection<TimelineType>* aCollection) {
  typename TimelineCollection<TimelineType>::TimelineMap result;
  // If multiple timelines are attempting to modify the same property, then the
  // timeline closest to the end of the list of names wins.
  // The spec doesn't mention this specifically for scroll-timeline-name and
  // view-timeline-name, so we follow the same rule with animation-name.
  for (size_t idx = 0; idx < aTimelineCount; ++idx) {
    const StyleType& timeline = aTimelines[idx];
    if (timeline.GetName() == nsGkAtoms::_empty) {
      continue;
    }

    RefPtr<TimelineType> dest =
        PopExistingTimeline(timeline.GetName(), aCollection);
    if (dest) {
      dest->ReplacePropertiesWith(aElement, aPseudoRequest, timeline);
    } else {
      dest = TimelineType::MakeNamed(aPresContext->Document(), aElement,
                                     aPseudoRequest, timeline);
    }
    MOZ_ASSERT(dest);

    // Override the previous one if it is duplicated.
    Unused << result.InsertOrUpdate(timeline.GetName(), dest);
  }
  return result;
}

template <typename TimelineType>
static TimelineCollection<TimelineType>& EnsureTimelineCollection(
    Element& aElement, const PseudoStyleRequest& aPseudoRequest);

template <>
ScrollTimelineCollection& EnsureTimelineCollection<ScrollTimeline>(
    Element& aElement, const PseudoStyleRequest& aPseudoRequest) {
  return aElement.EnsureAnimationData().EnsureScrollTimelineCollection(
      aElement, aPseudoRequest);
}

template <>
ViewTimelineCollection& EnsureTimelineCollection<ViewTimeline>(
    Element& aElement, const PseudoStyleRequest& aPseudoRequest) {
  return aElement.EnsureAnimationData().EnsureViewTimelineCollection(
      aElement, aPseudoRequest);
}

template <typename StyleType, typename TimelineType>
void TimelineManager::DoUpdateTimelines(
    nsPresContext* aPresContext, Element* aElement,
    const PseudoStyleRequest& aPseudoRequest,
    const nsStyleAutoArray<StyleType>& aStyleTimelines, size_t aTimelineCount) {
  auto* collection =
      TimelineCollection<TimelineType>::Get(aElement, aPseudoRequest);
  if (!collection && aTimelineCount == 1 &&
      aStyleTimelines[0].GetName() == nsGkAtoms::_empty) {
    return;
  }

  // We create a new timeline list based on its computed style and the existing
  // timelines.
  auto newTimelines = BuildTimelines<StyleType, TimelineType>(
      aPresContext, aElement, aPseudoRequest, aStyleTimelines, aTimelineCount,
      collection);

  if (newTimelines.IsEmpty()) {
    if (collection) {
      collection->Destroy();
    }
    return;
  }

  if (!collection) {
    collection =
        &EnsureTimelineCollection<TimelineType>(*aElement, aPseudoRequest);
    if (!collection->isInList()) {
      AddTimelineCollection(collection);
    }
  }

  // Replace unused timeline with new ones.
  collection->Swap(newTimelines);

  // FIXME: Bug 1774060. We may have to restyle the animations which use the
  // dropped timelines. Or rely on restyling the subtree and the following
  // siblings when mutating {scroll|view}-timeline-name.
}

void TimelineManager::UpdateHiddenByContentVisibilityForAnimations() {
  for (auto* scrollTimelineCollection : mScrollTimelineCollections) {
    for (ScrollTimeline* timeline :
         scrollTimelineCollection->Timelines().Values()) {
      timeline->UpdateHiddenByContentVisibility();
    }
  }

  for (auto* viewTimelineCollection : mViewTimelineCollections) {
    for (ViewTimeline* timeline :
         viewTimelineCollection->Timelines().Values()) {
      timeline->UpdateHiddenByContentVisibility();
    }
  }
}

}  // namespace mozilla
