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

#ifndef AutoplayExperimentHelper_h
#define AutoplayExperimentHelper_h

#include "core/page/PageVisibilityState.h"
#include "platform/Timer.h"
#include "platform/geometry/IntRect.h"

namespace blink {

class Document;
class AutoplayExperimentTest;

// These values are used for a histogram. Do not reorder.
enum AutoplayMetrics {
  // Media element with autoplay seen.
  AutoplayMediaFound = 0,
  // Autoplay enabled and user stopped media play at any point.
  AutoplayPaused = 1,
  // Autoplay enabled but user bailed out on media play early.
  AutoplayBailout = 2,
  // Autoplay disabled but user manually started media.
  AutoplayManualStart = 3,
  // Autoplay was (re)enabled through a user-gesture triggered load()
  AutoplayEnabledThroughLoad = 4,
  // Autoplay disabled by sandbox flags.
  AutoplayDisabledBySandbox = 5,

  // These metrics indicate "no gesture detected, but the gesture
  // requirement was overridden by experiment".  They do not include cases
  // where no user gesture is required (!m_userGestureRequiredForPlay).

  // User gesture requirement was bypassed, and playback started, during
  // initial load via the autoplay flag.  If playback was deferred due to
  // visibility requirements, then this does not apply.
  GesturelessPlaybackStartedByAutoplayFlagImmediately = 6,

  // User gesture requirement was bypassed, and playback started, when
  // the play() method was called.  If playback is deferred due to
  // visibility requirements, then this does not apply.
  GesturelessPlaybackStartedByPlayMethodImmediately = 7,

  // User gesture requirement was bypassed, and playback started, after
  // an element with the autoplay flag moved into the viewport.  Playback
  // was deferred earlier due to visibility requirements.
  GesturelessPlaybackStartedByAutoplayFlagAfterScroll = 8,

  // User gesture requirement was bypassed, and playback started, after
  // an element on which the play() method was called was moved into the
  // viewport.  Playback had been deferred due to visibility requirements.
  GesturelessPlaybackStartedByPlayMethodAfterScroll = 9,

  // play() failed to play due to gesture requirement.
  PlayMethodFailed = 10,

  // Some play, whether user initiated or not, started.
  AnyPlaybackStarted = 11,
  // Some play, whether user initiated or not, paused.
  AnyPlaybackPaused = 12,
  // Some playback, whether user initiated or not, bailed out early.
  AnyPlaybackBailout = 13,
  // Some playback, whether user initiated or not, played to completion.
  AnyPlaybackComplete = 14,

  // Number of audio elements detected that reach the resource fetch algorithm.
  AnyAudioElement = 15,
  // Numer of video elements detected that reach the resource fetch algorithm.
  AnyVideoElement = 16,

  // User gesture was bypassed, and playback started, and media played to
  // completion without a user-initiated pause.
  AutoplayComplete = 17,

  // Autoplay started after the gesture requirement was removed by a
  // user gesture load().
  GesturelessPlaybackEnabledByLoad = 18,

  // Gestureless playback started after the gesture requirement was removed
  // because src is media stream.
  GesturelessPlaybackEnabledByStream = 19,

  // Gestureless playback was started, but was never overridden.  This
  // includes the case in which no gesture was ever required.
  GesturelessPlaybackNotOverridden = 20,

  // Gestureless playback was enabled by a user gesture play() call.
  GesturelessPlaybackEnabledByPlayMethod = 21,

  // This enum value must be last.
  NumberOfAutoplayMetrics,
};

class CORE_EXPORT AutoplayExperimentHelper final
    : public GarbageCollectedFinalized<AutoplayExperimentHelper> {
  friend class AutoplayExperimentTest;

 public:
  // For easier testing, collect all the things we care about here.
  class Client : public GarbageCollectedFinalized<Client> {
   public:
    virtual ~Client() {}

    // HTMLMediaElement
    virtual double currentTime() const = 0;
    virtual double duration() const = 0;
    virtual bool paused() const = 0;
    virtual bool ended() const = 0;
    virtual bool muted() const = 0;
    virtual void setMuted(bool) = 0;
    virtual void playInternal() = 0;
    virtual void pauseInternal() = 0;
    virtual bool isLockedPendingUserGesture() const = 0;
    virtual void unlockUserGesture() = 0;
    virtual void recordAutoplayMetric(AutoplayMetrics) = 0;
    virtual bool shouldAutoplay() = 0;
    virtual bool isHTMLVideoElement() const = 0;
    virtual bool isHTMLAudioElement() const = 0;

    // Document
    virtual bool isLegacyViewportType() = 0;
    virtual PageVisibilityState pageVisibilityState() const = 0;
    virtual String autoplayExperimentMode() const = 0;

    // Frame
    virtual bool isCrossOrigin() const = 0;
    virtual bool isAutoplayAllowedPerSettings() const = 0;

    // LayoutObject
    virtual void setRequestPositionUpdates(bool) = 0;
    virtual IntRect absoluteBoundingBoxRect() const = 0;

    DEFINE_INLINE_VIRTUAL_TRACE() {}
  };

  static AutoplayExperimentHelper* create(Client* client) {
    return new AutoplayExperimentHelper(client);
  }

  ~AutoplayExperimentHelper();

  void becameReadyToPlay();
  void playMethodCalled();
  void pauseMethodCalled();
  void loadMethodCalled();
  void mutedChanged();
  void positionChanged(const IntRect&);
  void updatePositionNotificationRegistration();
  void recordSandboxFailure();
  void loadingStarted();
  void playbackStarted();
  void playbackStopped();
  void initialPlayWithUserGesture();

  // Returns true if and only if any experiment is enabled (i.e., |m_mode|
  // is not ExperimentOff).
  bool isExperimentEnabled();

  // Remove the user gesture requirement, and record why.  If there is no
  // gesture requirement, then this does nothing.
  void unlockUserGesture(AutoplayMetrics);

  // Set the reason that we're overridding the user gesture.  If there is no
  // gesture requirement, then this does nothing.
  void setDeferredOverrideReason(AutoplayMetrics);

  // Return true if and only if the user gesture requirement is currently
  // overridden by the experiment, permitting playback.
  bool isGestureRequirementOverridden() const;

  // Return true if and only if playback is queued but hasn't started yet,
  // such as if the element doesn't meet visibility requirements.
  bool isPlaybackDeferred() const;

  // Set the position to the current view's position, and
  void triggerAutoplayViewportCheckForTesting();

  enum Mode {
    // Do not enable the autoplay experiment.
    ExperimentOff = 0,
    // Enable gestureless autoplay for video elements.
    ForVideo = 1 << 0,
    // Enable gestureless autoplay for audio elements.
    ForAudio = 1 << 1,
    // Restrict gestureless autoplay to media that is in a visible page.
    IfPageVisible = 1 << 2,
    // Restrict gestureless autoplay to media that is entirely visible in
    // the viewport.
    IfViewport = 1 << 3,
    // Restrict gestureless autoplay to media that is partially visible in
    // the viewport.
    IfPartialViewport = 1 << 4,
    // Restrict gestureless autoplay to audio-less or muted media.
    IfMuted = 1 << 5,
    // Restrict gestureless autoplay to sites which contain the
    // viewport tag.
    IfMobile = 1 << 6,
    // Restrict gestureless autoplay to sites which are from the same origin
    // as the top-level frame.
    IfSameOrigin = 1 << 7,
    // Extend IfSameOrigin to allow autoplay of cross-origin elements if
    // they're muted.  This has no effect on same-origin or if IfSameOrigin
    // isn't enabled.
    OrMuted = 1 << 8,
    // If gestureless autoplay is allowed, then mute the media before
    // starting to play.
    PlayMuted = 1 << 9,
  };

  DEFINE_INLINE_TRACE() { visitor->trace(m_client); }

 private:
  explicit AutoplayExperimentHelper(Client*);

  // Register to receive position updates, if we haven't already.  If we
  // have, then this does nothing.
  void registerForPositionUpdatesIfNeeded();

  // Un-register for position updates, if we are currently registered.
  void unregisterForPositionUpdatesIfNeeded();

  // Modifiers for checking isEligible().
  enum EligibilityMode {
    // Perform all normal eligibility checks.
    Normal = 0,

    // Perform normal eligibility checks, but skip checking if autoplay has
    // actually been requested.  In other words, don't fail just becase
    // nobody has called play() and/or set the autoplay attribute.
    IgnorePendingPlayback = 1
  };

  // Return true if any only if this player meets (most) of the eligibility
  // requirements for the experiment to override the need for a user
  // gesture.  This includes everything except the visibility test.
  // |mode| modifies the eligibility check, as described above.
  bool isEligible(EligibilityMode = Normal) const;

  // Return false if and only if m_element is not visible, and we care
  // that it must be visible.
  bool meetsVisibilityRequirements() const;

  // Set the muted flag on the media if we're in an experiment mode that
  // requires it, else do nothing.
  void muteIfNeeded();

  // Maybe override the requirement for a user gesture, and start playing
  // autoplay media.  Returns true if only if it starts playback.
  bool maybeStartPlaying();

  // Configure internal state to record that the autoplay experiment is
  // going to start playback.  This doesn't actually start playback, since
  // there are several different cases.
  void prepareToAutoplay(AutoplayMetrics);

  // Record that an attempt to play without a user gesture has happened.
  // This does not assume whether or not the play attempt will succeed.
  // This method takes no action after it is called once.
  void autoplayMediaEncountered();

  // If we are about to enter a paused state, call this to record
  // autoplay metrics.
  void recordMetricsBeforePause();

  // Process a timer for checking visibility.
  void viewportTimerFired(TimerBase*);

  Client& client() const { return *m_client; }

  bool isLockedPendingUserGesture() const;

  inline bool enabled(Mode mode) const {
    return static_cast<int>(m_mode) & static_cast<int>(mode);
  }

  Mode fromString(const String& mode);

  void recordAutoplayMetric(AutoplayMetrics);

  // Could stopping at this point be considered a bailout of playback?
  // (as in, "The user really didn't want to play this").
  bool isBailout() const;

  // Returns true if and only if the experiment requires some sort of viewport
  // visibility check for autoplay.
  bool requiresViewportVisibility() const;

  Member<Client> m_client;

  Mode m_mode;

  // Autoplay experiment state.
  // True if we've received a play() without a pause().
  bool m_playPending : 1;

  // Are we registered with the view for position updates?
  bool m_registeredWithLayoutObject : 1;

  // According to our last position update, are we in the viewport?
  bool m_wasInViewport : 1;

  // Have we counted this autoplay media in the metrics yet?  This is set when
  // a media element tries to autoplay, and we record that via the
  // AutoplayMediaFound metric.
  bool m_autoplayMediaEncountered : 1;

  // Have we recorded a metric about the cause of the initial playback of
  // this media yet?
  bool m_playbackStartedMetricRecorded : 1;

  // Is the current playback the result of autoplay?  If so, then this flag
  // records that the pause / stop should be counted in the autoplay metrics.
  bool m_waitingForAutoplayPlaybackStop : 1;

  // Did we record that this media element exists in the metrics yet?  This is
  // independent of whether it autoplays; we just want to know how many
  // elements exist for the Any{Audio|Video}Element metrics.
  bool m_recordedElement : 1;

  // According to our last position update, where was our element?
  IntRect m_lastLocation;
  IntRect m_lastVisibleRect;

  // When was m_lastLocation set?
  double m_lastLocationUpdateTime;

  Timer<AutoplayExperimentHelper> m_viewportTimer;

  // Reason that autoplay would be allowed to proceed without a user gesture.
  AutoplayMetrics m_autoplayDeferredMetric;
};

inline AutoplayExperimentHelper::Mode& operator|=(
    AutoplayExperimentHelper::Mode& a,
    const AutoplayExperimentHelper::Mode& b) {
  a = static_cast<AutoplayExperimentHelper::Mode>(static_cast<int>(a) |
                                                  static_cast<int>(b));
  return a;
}

}  // namespace blink

#endif  // AutoplayExperimentHelper_h
