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

#ifndef mozilla_ElementAnimationData_h
#define mozilla_ElementAnimationData_h

#include "mozilla/PseudoStyleRequest.h"
#include "mozilla/UniquePtr.h"
#include "nsTHashMap.h"

class nsCycleCollectionTraversalCallback;

namespace mozilla {
class EffectSet;
template <typename Animation>
class AnimationCollection;
template <typename TimelineType>
class TimelineCollection;
namespace dom {
class Element;
class CSSAnimation;
class CSSTransition;
class ScrollTimeline;
class ViewTimeline;
}  // namespace dom
using CSSAnimationCollection = AnimationCollection<dom::CSSAnimation>;
using CSSTransitionCollection = AnimationCollection<dom::CSSTransition>;
using ScrollTimelineCollection = TimelineCollection<dom::ScrollTimeline>;
using ViewTimelineCollection = TimelineCollection<dom::ViewTimeline>;

// The animation data for a given element (and its pseudo-elements).
class ElementAnimationData {
  struct PerElementOrPseudoData {
    UniquePtr<EffectSet> mEffectSet;
    UniquePtr<CSSAnimationCollection> mAnimations;
    UniquePtr<CSSTransitionCollection> mTransitions;

    // Note: scroll-timeline-name is applied to elements which could be
    // scroll containers, or replaced elements. view-timeline-name is applied to
    // all elements. However, the named timeline is referenceable in
    // animation-timeline by the tree order scope.
    // Spec: https://drafts.csswg.org/scroll-animations-1/#timeline-scope.
    //
    // So it should be fine to create timeline objects only on the elements and
    // pseudo elements which support animations.
    //
    // Note: TimelineCollection owns and manages the named progress timeline
    // generated by specifying scroll-timeline-name property and
    // view-timeline-name property on this element. However, the anonymous
    // progress timelines (e.g. animation-timeline:scroll()) are owned by
    // Animation objects only.
    UniquePtr<ScrollTimelineCollection> mScrollTimelines;
    UniquePtr<ViewTimelineCollection> mViewTimelines;

    PerElementOrPseudoData();
    ~PerElementOrPseudoData();

    EffectSet& DoEnsureEffectSet();
    CSSTransitionCollection& DoEnsureTransitions(dom::Element&,
                                                 const PseudoStyleRequest&);
    CSSAnimationCollection& DoEnsureAnimations(dom::Element&,
                                               const PseudoStyleRequest&);
    ScrollTimelineCollection& DoEnsureScrollTimelines(
        dom::Element&, const PseudoStyleRequest&);
    ViewTimelineCollection& DoEnsureViewTimelines(dom::Element&,
                                                  const PseudoStyleRequest&);

    bool IsEmpty() const {
      return !mEffectSet && !mAnimations && !mTransitions &&
             !mScrollTimelines && !mViewTimelines;
    }

    void Traverse(nsCycleCollectionTraversalCallback&);
  };

  PerElementOrPseudoData mElementData;

  using PseudoData =
      nsTHashMap<PseudoStyleRequestHashKey, UniquePtr<PerElementOrPseudoData>>;
  PseudoData mPseudoData;
  // Avoid remove hash entry while other people are still using it.
  bool mIsClearingPseudoData = false;

  const PerElementOrPseudoData* GetData(
      const PseudoStyleRequest& aRequest) const {
    if (aRequest.mType != PseudoStyleType::NotPseudo) {
      return GetPseudoData(aRequest);
    }
    return &mElementData;
  }

  PerElementOrPseudoData& GetOrCreateData(const PseudoStyleRequest& aRequest) {
    if (aRequest.mType != PseudoStyleType::NotPseudo) {
      return GetOrCreatePseudoData(aRequest);
    }
    return mElementData;
  }

  const PerElementOrPseudoData* GetPseudoData(
      const PseudoStyleRequest& aRequest) const;
  PerElementOrPseudoData& GetOrCreatePseudoData(
      const PseudoStyleRequest& aRequest);
  void MaybeClearEntry(PseudoData::LookupResult<PseudoData&>&& aEntry);

  // |aFn| is the removal function which accepts only |PerElementOrPseudoData&|
  // as the parameter.
  template <typename Fn>
  void WithDataForRemoval(const PseudoStyleRequest& aRequest, Fn&& aFn);

 public:
  void Traverse(nsCycleCollectionTraversalCallback&);

  void ClearAllAnimationCollections();
  void ClearAllPseudos(bool aOnlyViewTransitions);
  void ClearViewTransitionPseudos() { ClearAllPseudos(true); }

  EffectSet* GetEffectSetFor(const PseudoStyleRequest& aRequest) const {
    if (auto* data = GetData(aRequest)) {
      return data->mEffectSet.get();
    }
    return nullptr;
  }

  void ClearEffectSetFor(const PseudoStyleRequest& aRequest);

  EffectSet& EnsureEffectSetFor(const PseudoStyleRequest& aRequest) {
    auto& data = GetOrCreateData(aRequest);
    if (auto* set = data.mEffectSet.get()) {
      return *set;
    }
    return data.DoEnsureEffectSet();
  }

  CSSTransitionCollection* GetTransitionCollection(
      const PseudoStyleRequest& aRequest) const {
    if (auto* data = GetData(aRequest)) {
      return data->mTransitions.get();
    }
    return nullptr;
  }

  void ClearTransitionCollectionFor(const PseudoStyleRequest& aRequest);

  CSSTransitionCollection& EnsureTransitionCollection(
      dom::Element& aOwner, const PseudoStyleRequest& aRequest) {
    auto& data = GetOrCreateData(aRequest);
    if (auto* collection = data.mTransitions.get()) {
      return *collection;
    }
    return data.DoEnsureTransitions(aOwner, aRequest);
  }

  CSSAnimationCollection* GetAnimationCollection(
      const PseudoStyleRequest& aRequest) const {
    if (auto* data = GetData(aRequest)) {
      return data->mAnimations.get();
    }
    return nullptr;
  }

  void ClearAnimationCollectionFor(const PseudoStyleRequest& aRequest);

  CSSAnimationCollection& EnsureAnimationCollection(
      dom::Element& aOwner, const PseudoStyleRequest& aRequest) {
    auto& data = GetOrCreateData(aRequest);
    if (auto* collection = data.mAnimations.get()) {
      return *collection;
    }
    return data.DoEnsureAnimations(aOwner, aRequest);
  }

  ScrollTimelineCollection* GetScrollTimelineCollection(
      const PseudoStyleRequest& aRequest) const {
    if (auto* data = GetData(aRequest)) {
      return data->mScrollTimelines.get();
    }
    return nullptr;
  }

  void ClearScrollTimelineCollectionFor(const PseudoStyleRequest& aRequest);

  ScrollTimelineCollection& EnsureScrollTimelineCollection(
      dom::Element& aOwner, const PseudoStyleRequest& aRequest) {
    auto& data = GetOrCreateData(aRequest);
    if (auto* collection = data.mScrollTimelines.get()) {
      return *collection;
    }
    return data.DoEnsureScrollTimelines(aOwner, aRequest);
  }

  ViewTimelineCollection* GetViewTimelineCollection(
      const PseudoStyleRequest& aRequest) const {
    if (auto* data = GetData(aRequest)) {
      return data->mViewTimelines.get();
    }
    return nullptr;
  }

  void ClearViewTimelineCollectionFor(const PseudoStyleRequest& aRequest);

  ViewTimelineCollection& EnsureViewTimelineCollection(
      dom::Element& aOwner, const PseudoStyleRequest& aRequest) {
    auto& data = GetOrCreateData(aRequest);
    if (auto* collection = data.mViewTimelines.get()) {
      return *collection;
    }
    return data.DoEnsureViewTimelines(aOwner, aRequest);
  }

  ElementAnimationData() = default;
};

}  // namespace mozilla

#endif
