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

#ifndef IOS_CHROME_BROWSER_FULLSCREEN_UI_BUNDLED_FULLSCREEN_MODEL_H_
#define IOS_CHROME_BROWSER_FULLSCREEN_UI_BUNDLED_FULLSCREEN_MODEL_H_

#include <CoreGraphics/CoreGraphics.h>

#include <cmath>

#import "base/observer_list.h"
#include "base/memory/weak_ptr.h"
#import "ios/chrome/browser/broadcaster/ui_bundled/chrome_broadcast_observer_bridge.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/scoped_fullscreen_disabler.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/toolbar/legacy/ui_bundled/fullscreen/toolbars_size_observer.h"
#import "ios/public/provider/chrome/browser/fullscreen/fullscreen_api.h"
#import "ios/web/common/features.h"

class FullscreenModelObserver;
@class ToolbarsSize;

// Represents the direction the user is scrolling.
enum class FullscreenModelScrollDirection : int { kUp, kDown, kNone };

// Model object used to calculate fullscreen state.
class FullscreenModel : public ChromeBroadcastObserverInterface,
                        ToolbarsSizeObserver {
 public:
  FullscreenModel();

  FullscreenModel(const FullscreenModel&) = delete;
  FullscreenModel& operator=(const FullscreenModel&) = delete;

  ~FullscreenModel() override;

  // Adds and removes FullscreenModelObservers.
  void AddObserver(FullscreenModelObserver* observer);
  void RemoveObserver(FullscreenModelObserver* observer);

  // The progress value calculated by the model.
  CGFloat progress() const { return progress_; }

  // Whether fullscreen is disabled.  When disabled, the toolbar is completely
  // visible.
  bool enabled() const { return disabled_counter_ == 0U; }

  // Whether the base offset has been recorded after state has been invalidated
  // by navigations or toolbar height changes.
  bool has_base_offset() const {
    CHECK(ios::provider::IsFullscreenSmoothScrollingSupported());
    return !std::isnan(base_offset_);
  }

  // The base offset against which the fullscreen progress is being calculated.
  CGFloat base_offset() const { return base_offset_; }

  // Returns the difference between the max and min toolbar heights.
  CGFloat get_toolbar_height_delta() const {
    CGFloat top_delta =
        GetExpandedTopToolbarHeight() - GetCollapsedTopToolbarHeight();
    if (top_delta < FLT_EPSILON &&
        GetCollapsedBottomToolbarHeight() >= FLT_EPSILON) {
      CGFloat bottom_delta =
          GetExpandedBottomToolbarHeight() - GetCollapsedBottomToolbarHeight();
      return bottom_delta;
    }
    return top_delta;
  }

  // Returns whether the page content is tall enough for the toolbar to be
  // scrolled to an entirely collapsed position.
  bool can_collapse_toolbar() const {
    return content_height_ > scroll_view_height_ + get_toolbar_height_delta();
  }

  // Whether the view is scrolled all the way to the top.
  bool is_scrolled_to_top() const {
    return y_content_offset_ <= -GetExpandedTopToolbarHeight();
  }

  // Whether the view is scrolled all the way to the bottom.
  bool is_scrolled_to_bottom() const {
    if (ios::provider::IsFullscreenSmoothScrollingSupported()) {
      return y_content_offset_ + scroll_view_height_ >= content_height_;
    } else {
      return y_content_offset_ -
                 (GetCollapsedTopToolbarHeight() +
                  GetCollapsedBottomToolbarHeight() + safe_area_insets_.bottom +
                  safe_area_insets_.top) +
                 (scroll_view_height_ + GetExpandedTopToolbarHeight() +
                  GetExpandedBottomToolbarHeight()) >=
             content_height_;
    }
  }

  // The min, max, and current insets caused by the toolbars.
  UIEdgeInsets min_toolbar_insets() const {
    return GetToolbarInsetsAtProgress(0.0);
  }
  UIEdgeInsets max_toolbar_insets() const {
    return GetToolbarInsetsAtProgress(1.0);
  }
  UIEdgeInsets current_toolbar_insets() const {
    return GetToolbarInsetsAtProgress(progress_);
  }
  // Setter for whether the force fullscreen mode was triggered manually.
  void set_manually_forced(bool manually_forced) {
    manually_forced_ = manually_forced;
  }
  // Returns the toolbar insets at `progress`.
  UIEdgeInsets GetToolbarInsetsAtProgress(CGFloat progress) const {
    return UIEdgeInsetsMake(GetCollapsedTopToolbarHeight() +
                                progress * (GetExpandedTopToolbarHeight() -
                                            GetCollapsedTopToolbarHeight()),
                            0,
                            GetCollapsedBottomToolbarHeight() +
                                progress * (GetExpandedBottomToolbarHeight() -
                                            GetCollapsedBottomToolbarHeight()),
                            0);
  }

  // Increments and decrements `disabled_counter_` for features that require the
  // toolbar be completely visible.
  void IncrementDisabledCounter();
  void DecrementDisabledCounter();

  // Force enter fullscreen without animation. Setting the progress to 0.0 even
  // when fullscreen is disabled.
  void ForceEnterFullscreen();

  // Recalculates the fullscreen progress for a new navigation.
  void ResetForNavigation();

  // Instructs the model to ignore broadcasted scroll updates for the remainder
  // of the current scroll.  Has no effect if not called while a scroll is
  // occurring.  The model will resume listening for scroll events when
  // `scrolling_` is reset to false.
  void IgnoreRemainderOfCurrentScroll();

  // Called when a scroll end animation finishes.  `progress` is the fullscreen
  // progress corresponding to the final state of the aniamtion.
  void AnimationEndedWithProgress(CGFloat progress);

  // TODO(crbug.com/397683326): Move these values to `ToolbarsSize`.
  // Getter for the minimum top toolbar height to use in calculations.
  CGFloat GetCollapsedTopToolbarHeight() const;

  // Getter for the maximum top toolbar height to use in calculations.
  CGFloat GetExpandedTopToolbarHeight() const;

  // Getter for the maximum bottom toolbar height to use in calculations.
  CGFloat GetExpandedBottomToolbarHeight() const;

  // Getter for the minimum bottom toolbar height to use in calculations.
  CGFloat GetCollapsedBottomToolbarHeight() const;

  // Whenever the height of the top or bottom toolbar changes.
  void ToolbarsHeightDidChange();

  // Setter for the height of the scroll view displaying the main content.
  void SetScrollViewHeight(CGFloat scroll_view_height);
  CGFloat GetScrollViewHeight() const;

  // Setter for the current height of the rendered page.
  void SetContentHeight(CGFloat content_height);
  CGFloat GetContentHeight() const;

  // Setter for the top content inset of the scroll view displaying the main
  // content.
  void SetTopContentInset(CGFloat top_inset);
  CGFloat GetTopContentInset() const;

  // Setter for the current vertical content offset. Setting this will
  // recalculate the progress value.
  void SetYContentOffset(CGFloat y_content_offset);
  CGFloat GetYContentOffset() const;

  // Setter for whether the scroll view is scrolling. If a scroll event ends
  // and the progress value is not 0.0 or 1.0, the model will round to the
  // nearest value.
  void SetScrollViewIsScrolling(bool scrolling);
  bool IsScrollViewScrolling() const;

  // Setter for whether the scroll view is zooming.
  void SetScrollViewIsZooming(bool zooming);
  bool IsScrollViewZooming() const;

  // Setter for whether the scroll view is being dragged.
  void SetScrollViewIsDragging(bool dragging);
  bool IsScrollViewDragging() const;

  // Setter for whether the scroll view is resized for fullscreen events.
  void SetResizesScrollView(bool resizes_scroll_view);
  bool ResizesScrollView() const;

  // Setter for the safe area insets for the current WebState's view.
  void SetWebViewSafeAreaInsets(UIEdgeInsets safe_area_insets);
  UIEdgeInsets GetWebViewSafeAreaInsets() const;

  // Setter for whether force fullscreen mode is active. The mode is used when
  // the bottom toolbar is collapsed above the keyboard.
  void SetForceFullscreenMode(bool force_fullscreen_mode);
  bool IsForceFullscreenMode() const;
  void SetInsetsUpdateEnabled(bool enabled);
  bool IsInsetsUpdateEnabled() const;

  FullscreenModelScrollDirection GetLastScrollDirection() const {
    return fullscreen_scroll_direction_;
  }

  // Sets the last scroll direction. If the direction has changed, the base
  // offset is updated.
  void SetLastScrollDirection(FullscreenModelScrollDirection direction);

  // Updates `speed_` of the fullscreen model accordingly to
  // fullscreen flag `fullscreen transition experiment`.
  void UpdateSpeed();

  // Getter for the speed of fullscren transition.
  CGFloat GetSpeed() { return speed_; }

  // Setter for the toolbar.
  void SetToolbarsSize(ToolbarsSize* toolbars_size);

 private:
  // Returns how a scroll to the current `y_content_offset_` from `from_offset`
  // should be handled.
  enum class ScrollAction : short {
    kIgnore,                       // Ignore the scroll.
    kUpdateBaseOffset,             // Update `base_offset_` only.
    kUpdateProgress,               // Update `progress_` only.
    kUpdateBaseOffsetAndProgress,  // Update `base_offset_` and `progress_`.
  };
  ScrollAction ActionForScrollFromOffset(CGFloat from_offset) const;

  // Updates the base offset given the current y content offset, progress, and
  // toolbar height.
  void UpdateBaseOffset();

  // Updates the progress value given the current y content offset, base offset,
  // and toolbar height.
  void UpdateProgress();

  // Updates the disabled counter depending on the current values of
  // `scroll_view_height_` and `content_height_`.
  void UpdateDisabledCounterForContentHeight();

  // Setter for `progress_`.  Notifies observers of the new value if
  // `notify_observers` is true.
  void SetProgress(CGFloat progress);

  // Returns true if the size of the scroll is more than the threshold to begin
  // entering or exiting fullscreen.
  bool ScrollThresholdExceeded() const;

  // ChromeBroadcastObserverInterface:
  void OnScrollViewSizeBroadcasted(CGSize scroll_view_size) override;
  void OnScrollViewContentSizeBroadcasted(CGSize content_size) override;
  void OnScrollViewContentInsetBroadcasted(UIEdgeInsets content_inset) override;
  void OnContentScrollOffsetBroadcasted(CGFloat offset) override;
  void OnScrollViewIsScrollingBroadcasted(bool scrolling) override;
  void OnScrollViewIsZoomingBroadcasted(bool zooming) override;
  void OnScrollViewIsDraggingBroadcasted(bool dragging) override;
  void OnCollapsedTopToolbarHeightBroadcasted(CGFloat height) override;
  void OnExpandedTopToolbarHeightBroadcasted(CGFloat height) override;
  void OnExpandedBottomToolbarHeightBroadcasted(CGFloat height) override;
  void OnCollapsedBottomToolbarHeightBroadcasted(CGFloat height) override;

  // ToolbarsSizeObserver implementation
  void OnTopToolbarHeightChanged() override;
  void OnBottomToolbarHeightChanged() override;
  // The observers for this model.
  // TODO(crbug.com/484371187): Investigate if reentrancy can be removed.
  base::ObserverList<
      FullscreenModelObserver,
      true,
      base::ObserverListReentrancyPolicy::kAllowReentrancyUntriaged>
      observers_;
  // The percentage of the toolbar that should be visible, where 1.0 denotes a
  // fully visible toolbar and 0.0 denotes a completely hidden one.
  CGFloat progress_ = 0.0;
  // The base offset from which to calculate fullscreen state.  When `locked_`
  // is false, it is reset to the current offset after each scroll event.
  CGFloat base_offset_ = 0.0;
  // Heights of the top and bottom toolbars.
  ToolbarsSize* toolbars_size_;
  // The current vertical content offset of the main content.
  CGFloat y_content_offset_ = 0.0;
  // The height of the scroll view displaying the current page.
  CGFloat scroll_view_height_ = 0.0;
  // The height of the current page's rendered content.
  CGFloat content_height_ = 0.0;
  // The top inset of the scroll view displaying the current page.
  CGFloat top_inset_ = 0.0;
  // How many currently-running features require the toolbar be visible.
  size_t disabled_counter_ = 0;
  // Counts the number of currently-running feature that require to force
  // fullscreen mode.
  size_t force_fullscreen_mode_counter_ = 0;
  // Whether fullscreen is disabled for short content.
  bool disabled_for_short_content_ = false;
  // Whether the main content is being scrolled.
  bool scrolling_ = false;
  // Whether the scroll view is zooming.
  bool zooming_ = false;
  // Whether the main content is being dragged.
  bool dragging_ = false;
  // Whether the in-progress scroll is being ignored.
  bool ignoring_current_scroll_ = false;
  // Whether the scroll view is resized for fullscreen events.
  bool resizes_scroll_view_ = false;
  // The WebState view's safe area insets.
  UIEdgeInsets safe_area_insets_ = UIEdgeInsetsZero;
  // The number of FullscreenModelObserver callbacks currently being executed.
  size_t observer_callback_count_ = 0;
  // Whether updating insets is enabled.
  bool insets_update_enabled_ = true;
  // Whether progress is currently being set.
  bool setting_progress_ = false;
  // Current direction of scrolling initiated by the user.
  FullscreenModelScrollDirection fullscreen_scroll_direction_ =
      FullscreenModelScrollDirection::kNone;
  // Speed of fullscreen transition.
  CGFloat speed_ = 1.0;
  // Time when scrolling started.
  std::optional<base::TimeTicks> start_scrolling_time_ = std::nullopt;
  // True is the scrolling time have been recorded.
  bool is_scrolling_time_recorded_ = false;
  // Time when fullscreen mode was entered.
  std::optional<base::TimeTicks> time_entered_fullscreen_ = std::nullopt;
  // Time when fullscreen mode was exited.
  std::optional<base::TimeTicks> time_exited_fullscreen_ = std::nullopt;
  // The minimum scroll amount that will result in beginning to enter or exit
  // fullscreen.
  CGFloat scroll_threshold_ = 0.0;
  // The content offset when the most recent drag event started.
  CGFloat offset_at_start_of_drag_ = 0;
  // Whether the force fullscreen mode was triggered manually.
  bool manually_forced_ = false;

  friend class FullscreenModelTest;

  base::WeakPtrFactory<FullscreenModel> weak_factory_{this};
};

#endif  // IOS_CHROME_BROWSER_FULLSCREEN_UI_BUNDLED_FULLSCREEN_MODEL_H_
