// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef UI_LOTTIE_ANIMATION_H_
#define UI_LOTTIE_ANIMATION_H_

#include <functional>
#include <memory>
#include <optional>
#include <vector>

#include "base/component_export.h"
#include "base/containers/flat_map.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/observer_list.h"
#include "base/time/time.h"
#include "cc/paint/skottie_color_map.h"
#include "cc/paint/skottie_frame_data.h"
#include "cc/paint/skottie_frame_data_provider.h"
#include "cc/paint/skottie_resource_metadata.h"
#include "cc/paint/skottie_text_property_value.h"
#include "cc/paint/skottie_wrapper.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkStream.h"
#include "third_party/skia/modules/skottie/include/Skottie.h"
#include "ui/gfx/geometry/size.h"

class SkImage;
struct SkSamplingOptions;

namespace gfx {
class Canvas;
}  // namespace gfx

namespace lottie {
class AnimationTest;
class AnimationObserver;

// This class is a wrapper over the Skia object for lottie vector graphic
// animations. It has its own timeline manager for the animation controls. The
// framerate of the animation and the animation ticks are controlled externally
// and hence the consumer must manage the timer and call paint at the desired
// frame per second.
// This helps keep multiple animations be synchronized by having a common
// external tick clock.
//
// Usage example:
//   1. Rendering a single frame on the canvas:
//        Animation animation_ = Animation(data);
//        animation_.Paint(canvas, t);
//
//   2. Playing the animation and rendering each frame:
//      void SampleClient::Init() {
//        Animation animation_ = Animation(data);
//        animation_.Start(Animation::PlaybackConfig::CreateWithStyle(
//            Animation::Style::kLinear, *animation_));
//      }
//
//      // overrides cc::CompositorAnimationObserver
//      void SampleClient::OnAnimationStep(TimeTicks* timestamp) {
//        timestamp_ = timestamp;
//        SchedulePaint();
//      }
//
//      void SampleClient::OnPaint(Canvas* canvas) {
//        animation_.Paint(canvas, timestamp_);
//      }
//
//   2. If you only want to play a subsection of the animation:
//      void SampleClient::Init() {
//        // This will seek to the 1st second of the animation and from there
//        // play it for 5 seconds.
//        Animation animation_ = Animation(data);
//        animation_.Start(Animation::PlaybackConfig({
//            Seconds(1), Seconds(5), Animation::Style::kLinear}));
//      }
//
//      // overrides cc::CompositorAnimationObserver
//      void SampleClient::OnAnimationStep(TimeTicks*) {
//        timestamp_ = timestamp;
//        SchedulePaint();
//      }
//
//      void SampleClient::OnPaint(Canvas* canvas) {
//        animation_.Paint(canvas, timestamp_, gfx::Size(10, 10));
//      }
//
class COMPONENT_EXPORT(UI_LOTTIE) Animation final {
 public:
  enum class Style {
    kLinear = 0,  // The animation plays from one time instant to another.
    kThrobbing,   // The animation plays from one time instant to another and
                  // then back. The animation plays in loop until stopped.
    kLoop         // Same as LINEAR, except the animation repeats after it ends.
  };

  // An animation goes through a single "cycle" when it's played from one
  // timestamp to another. After reaching the final timestamp, it may either
  // loop back to the initial timestamp again, or even play in reverse depending
  // on the style described above.
  struct COMPONENT_EXPORT(UI_LOTTIE) CycleBoundaries {
    // Returns the range [0, animation.GetAnimationDuration()).
    static CycleBoundaries FullCycle(const Animation& animation);

    // The cycle's range is [start_offset, end_offset). |start_offset| must be
    // < |end_offset|, and both must be in the range
    // [0, GetAnimationDuration()]. They represent non-normalized timestamps in
    // the animation.
    base::TimeDelta start_offset;
    base::TimeDelta end_offset;
  };

  struct COMPONENT_EXPORT(UI_LOTTIE) PlaybackConfig {
    // By default, loop from the beginning of the animation to the end.
    static PlaybackConfig CreateDefault(const Animation& animation);
    // Play from the beginning of the animation to the end with the provided
    // |style|.
    static PlaybackConfig CreateWithStyle(Style style,
                                          const Animation& animation);

    PlaybackConfig();
    PlaybackConfig(std::vector<CycleBoundaries> scheduled_cycles,
                   base::TimeDelta initial_offset,
                   int initial_completed_cycles,
                   Style style,
                   bool ignore_reduced_motion = false);
    PlaybackConfig(const PlaybackConfig& other);
    PlaybackConfig& operator=(const PlaybackConfig& other);
    ~PlaybackConfig();

    // Set of cycles that the animation will iterate through in the order they
    // appear. Must not be empty. After reaching the last entry in
    // |scheduled_cycles|, the animation will continue re-using the last entry's
    // boundaries in all future cycles.
    //
    // Example: {[0, T), [T/2, 3T/4)}. In the first cycle, the animation will
    // play starting at time 0 until it reaches timestamp T. After that, it will
    // loop back to timestamp T/2 and play until 3T/4. The [T/2, 3T/4) cycle
    // repeats indefinitely until the animation is stopped.
    //
    // If |style| is kLinear, |scheduled_cycles| must have exactly one entry.
    std::vector<CycleBoundaries> scheduled_cycles;

    // |initial_offset| and |initial_completed_cycles| combined dictate
    // where to start playing the animation from within the |scheduled_cycles|
    // above. The most common thing is to start playing from the very beginning
    // (|initial_offset| is the |start_offset| of the first scheduled cycle
    // and |initial_completed_cycles| is 0). But this allows the caller to
    // specify an arbitrary starting point.
    base::TimeDelta initial_offset;
    // The animation will start playing as if it has already completed the
    // number of cycles specified below. Note this not only dictates which
    // scheduled cycle the animation starts within, but also the initial
    // direction of the animation for throbbing animations.
    int initial_completed_cycles = 0;

    Style style = Style::kLoop;

    // If true, the animation will play even if the user prefers reduced motion.
    // This is defaulted to false.
    bool ignore_reduced_motion = false;
  };

  // |frame_data_provider| may be null if it's known that the incoming skottie
  // animation does not contain any image assets.
  explicit Animation(
      scoped_refptr<cc::SkottieWrapper> skottie,
      cc::SkottieColorMap color_map = cc::SkottieColorMap(),
      cc::SkottieFrameDataProvider* frame_data_provider = nullptr);
  Animation(const Animation&) = delete;
  Animation& operator=(const Animation&) = delete;
  ~Animation();

  void AddObserver(AnimationObserver* observer);
  void RemoveObserver(AnimationObserver* observer);

  // Animation properties ------------------------------------------------------
  // Returns the total duration of the animation as reported by |animation_|.
  base::TimeDelta GetAnimationDuration() const;

  // Returns the size of the vector graphic as reported by |animation_|. This is
  // constant for a given |animation_|.
  gfx::Size GetOriginalSize() const;

  // Animation controls --------------------------------------------------------
  // This is an asynchronous call that would start playing the animation on the
  // next animation step. On a successful start the |observer_| would be
  // notified.
  //
  // If a null |playback_config| is provided, the default one is used.
  void Start(std::optional<PlaybackConfig> playback_config = std::nullopt);

  // Pauses the animation.
  void Pause();

  // This is an asynchronous call that would resume playing a paused animation
  // on the next animation step.
  void ResumePlaying();

  // Resets the animation to the first frame and stops.
  void Stop();

  // Returns the current normalized [0..1] value at which the animation frame
  // is.
  // 0 -> first frame and 1 -> last frame.
  //
  // Returns nullopt if a timestamp is currently is unavailable. This is the
  // case if:
  // * The animation is currently Stop()ed.
  // * The animation has been Start()ed but a single frame has not been painted
  //   yet.
  std::optional<float> GetCurrentProgress() const;

  // Returns the currently playing cycle within the PlaybackConfig's
  // |scheduled_cycles|. Returns nullopt under the same circumstances as
  // GetCurrentProgress().
  std::optional<CycleBoundaries> GetCurrentCycleBoundaries() const;

  // Returns the number of animation cycles that have been completed since
  // Play() was called, or nullopt if the animation is currently Stop()ed.
  std::optional<int> GetNumCompletedCycles() const;

  // Returns the currently active PlaybackConfig, or nullopt if the animation
  // is currently Stop()ed.
  std::optional<PlaybackConfig> GetPlaybackConfig() const;

  // Paint operations ----------------------------------------------------------
  // Paints the frame of the animation for the given |timestamp| at the given
  // |size|.
  void Paint(gfx::Canvas* canvas,
             const base::TimeTicks& timestamp,
             const gfx::Size& size);

  // Paints the frame of the animation for the normalized time instance |t|. Use
  // this for special cases when you want to manually manage which frame to
  // paint.
  void PaintFrame(gfx::Canvas* canvas, float t, const gfx::Size& size);

  // Returns the skottie object that contins the animation data.
  scoped_refptr<cc::SkottieWrapper> skottie() const { return skottie_; }

  // Returns the text nodes in the animation and their corresponding current
  // property values. The text nodes' initial property values reflect those
  // embedded in the Lottie animation file. A mutable reference is returned
  // so that the caller may modify the text map with its own custom values
  // before calling Paint(). The caller may do so as many times as desired.
  cc::SkottieTextPropertyValueMap& text_map() { return text_map_; }

  // Sets the rate at which the animation will be played. A |playback_speed| of
  // 1 renders exactly in real time, 0.5 is half as fast, 2 is twice as fast,
  // etc. This may be called at any time, and the |timestamp| passed to Paint()
  // is automatically adjusted internally to account for the playback speed.
  //
  // Defaults to 1 if not called.
  void SetPlaybackSpeed(float playback_speed);

 private:
  friend class AnimationTest;

  enum class PlayState {
    kStopped = 0,     // Animation is stopped.
    kSchedulePlay,    // Animation will start playing on the next animatin step.
    kPlaying,         // Animation is playing.
    kPaused,          // Animation is paused.
    kScheduleResume,  // Animation will resume playing on the next animation
                      // step
    kEnded            // Animation has ended.
  };

  // Class to manage the timeline when playing the animation. Manages the
  // normalized progress [0..1] between the given start and end offset. If the
  // reverse flag is set, the progress runs in reverse.
  class COMPONENT_EXPORT(UI_LOTTIE) TimerControl final {
   public:
    TimerControl(std::vector<CycleBoundaries> scheduled_cycles,
                 base::TimeDelta initial_offset,
                 int initial_completed_cycles,
                 const base::TimeDelta& total_duration,
                 const base::TimeTicks& start_timestamp,
                 bool should_reverse,
                 float playback_speed);
    ~TimerControl();
    TimerControl(const TimerControl&) = delete;
    TimerControl& operator=(const TimerControl&) = delete;

    // Update timeline progress based on the new timetick |timestamp|.
    void Step(const base::TimeTicks& timestamp);

    // Resumes the timer.
    void Resume(const base::TimeTicks& timestamp);

    void SetPlaybackSpeed(float playback_speed);

    double GetNormalizedCurrentCycleProgress() const;
    double GetNormalizedStartOffset() const;
    double GetNormalizedEndOffset() const;
    int completed_cycles() const { return completed_cycles_; }
    CycleBoundaries current_cycle() const { return current_cycle_; }

   private:
    friend class AnimationTest;

    // Only applies to throbbing animations, for which every even numbered
    // cycle plays forwards, and every odd numbered cycle plays reversed.
    bool IsPlayingInReverse() const;

    // See comments in |PlaybackConfig::scheduled_cycles|.
    const std::vector<CycleBoundaries> scheduled_cycles_;

    // Total duration of all cycles.
    const base::TimeDelta total_duration_;

    // The timetick at which |progress_| was updated last.
    base::TimeTicks previous_tick_;

    // This is the progress of the timer in the current cycle.
    base::TimeDelta current_cycle_progress_;

    // If true, the progress will go into reverse after each cycle. This is used
    // for throbbing animations.
    const bool should_reverse_ = false;

    // The number of times each |cycle_duration_| is covered by the timer.
    int completed_cycles_ = 0;

    // See comments above SetPlaybackSpeed().
    float playback_speed_ = 1.f;

    // The boundaries of the current cycle. This is a copy of one of the entries
    // in |scheduled_cycles_|.
    CycleBoundaries current_cycle_;
  };

  void InitTimer(const base::TimeTicks& timestamp);
  void TryNotifyAnimationCycleEnded();
  cc::SkottieWrapper::FrameDataFetchResult LoadImageForAsset(
      gfx::Canvas* canvas,
      cc::SkottieFrameDataMap& all_frame_data,
      cc::SkottieResourceIdHash asset_id,
      float t,
      sk_sp<SkImage>&,
      SkSamplingOptions&);
  void VerifyPlaybackConfigIsValid(const PlaybackConfig& playback_config) const;

  // Manages the timeline for the current playing animation.
  std::unique_ptr<TimerControl> timer_control_;

  // The current state of animation.
  PlayState state_ = PlayState::kStopped;

  // The config from the most recent call to Start().
  PlaybackConfig playback_config_;

  // TODO(crbug.com/484371187): Investigate if reentrancy can be removed.
  base::ObserverList<
      AnimationObserver,
      /*check_empty=*/false,
      /*reentrancy=*/
      base::ObserverListReentrancyPolicy::kAllowReentrancyUntriaged>
      observers_;

  scoped_refptr<cc::SkottieWrapper> skottie_;
  cc::SkottieColorMap color_map_;
  cc::SkottieTextPropertyValueMap text_map_;
  base::flat_map<cc::SkottieResourceIdHash,
                 scoped_refptr<cc::SkottieFrameDataProvider::ImageAsset>>
      image_assets_;

  float playback_speed_ = 1.f;
};

}  // namespace lottie

#endif  // UI_LOTTIE_ANIMATION_H_
