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

#import "ios/chrome/browser/fullscreen/ui_bundled/legacy_fullscreen_mediator.h"

#import "base/check_op.h"
#import "base/memory/ptr_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "ios/chrome/browser/fullscreen/public/fullscreen_metrics.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_animator.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_content_adjustment_util.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_controller_observer.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_model.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_web_view_resizer.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/public/provider/chrome/browser/fullscreen/fullscreen_api.h"
#import "ios/web/public/web_state.h"

LegacyFullscreenMediator::LegacyFullscreenMediator(
    FullscreenController* controller,
    FullscreenModel* model)
    : controller_(controller),
      model_(model),
      resizer_([[FullscreenWebViewResizer alloc] initWithModel:model]) {
  DCHECK(controller_);
  DCHECK(model_);
  model_->AddObserver(this);
}

LegacyFullscreenMediator::~LegacyFullscreenMediator() {
  // Disconnect() is expected to be called before deallocation.
  DCHECK(!controller_);
  DCHECK(!model_);
}

void LegacyFullscreenMediator::SetWebState(web::WebState* webState) {
  resizer_.webState = webState;
}

void LegacyFullscreenMediator::SetIsBrowserTraitCollectionUpdating(
    bool updating) {
  if (updating_browser_trait_collection_ == updating) {
    return;
  }
  updating_browser_trait_collection_ = updating;
  if (updating_browser_trait_collection_) {
    resizer_.compensateFrameChangeByOffset = NO;
    scrolled_to_top_during_trait_collection_updates_ =
        model_->is_scrolled_to_top();
  } else {
    resizer_.compensateFrameChangeByOffset = YES;
    if (scrolled_to_top_during_trait_collection_updates_) {
      // If the content was scrolled to the top when the trait collection began
      // updating, changes in toolbar heights may cause the top of the page to
      // become hidden.  Ensure that the page remains scrolled to the top after
      // the trait collection finishes updating.
      web::WebState* web_state = resizer_.webState;
      if (web_state) {
        MoveContentBelowHeader(web_state->GetWebViewProxy(), model_);
      }
      scrolled_to_top_during_trait_collection_updates_ = false;
    }
  }
}

void LegacyFullscreenMediator::EnterFullscreen() {
  if (model_->enabled()) {
    fullscreen_enter_trigger_ =
        FullscreenModeTransitionTrigger::kUserControlled;
    AnimateWithStyle(FullscreenAnimatorStyle::ENTER_FULLSCREEN);
  }
}

void LegacyFullscreenMediator::ExitFullscreen(
    FullscreenModeTransitionTrigger trigger) {
  if (model_->IsForceFullscreenMode()) {
    return;
  }
  // Instruct the model to ignore the remainder of the current scroll when
  // starting this animator.  This prevents the toolbar from immediately being
  // hidden if AnimateModelReset() is called while a scroll view is
  // decelerating.
  model_->IgnoreRemainderOfCurrentScroll();
  fullscreen_exit_trigger_ = trigger;
  AnimateWithStyle(FullscreenAnimatorStyle::EXIT_FULLSCREEN);
}

void LegacyFullscreenMediator::ForceEnterFullscreen(
    bool insets_update_enabled,
    FullscreenModeTransitionTrigger trigger) {
  fullscreen_enter_trigger_ = trigger;
  if (trigger == FullscreenModeTransitionTrigger::kForcedByUser) {
    model_->set_manually_forced(true);
  }
  model_->SetForceFullscreenMode(true);
  model_->SetInsetsUpdateEnabled(insets_update_enabled);
  // Disable fullscreen because:
  // - It interfers with the animation when moving the secondary toolbar above
  // the keyboard.
  // - Fullscreen should not resize the toolbar it's above the keyboard.
  model_->IncrementDisabledCounter();
  model_->ForceEnterFullscreen();
}

void LegacyFullscreenMediator::ForceExitFullscreen(
    FullscreenModeTransitionTrigger trigger) {
  fullscreen_exit_trigger_ = trigger;
  model_->set_manually_forced(false);
  model_->SetForceFullscreenMode(false);
  model_->SetInsetsUpdateEnabled(true);
  model_->DecrementDisabledCounter();
  ExitFullscreenWithoutAnimation();
}

void LegacyFullscreenMediator::ExitFullscreenWithoutAnimation() {
  model_->ResetForNavigation();
}

void LegacyFullscreenMediator::Disconnect() {
  base::WeakPtr<LegacyFullscreenMediator> weak_this =
      weak_factory_.GetWeakPtr();
  for (auto& observer : observers_) {
    observer.FullscreenControllerWillShutDown(controller_);
    if (!weak_this) {
      return;
    }
  }
  resizer_.webState = nullptr;
  resizer_ = nil;
  [animator_ stopAnimation:YES];
  animator_ = nil;
  model_->RemoveObserver(this);
  model_ = nullptr;
  controller_ = nullptr;
  fullscreen_enter_trigger_ = std::nullopt;
}

void LegacyFullscreenMediator::FullscreenModelToolbarHeightsUpdated(
    FullscreenModel* model) {
  if (IsDisconnected()) {
    return;
  }
  base::WeakPtr<LegacyFullscreenMediator> weak_this =
      weak_factory_.GetWeakPtr();
  for (auto& observer : observers_) {
    observer.FullscreenViewportInsetRangeChanged(controller_,
                                                 model_->min_toolbar_insets(),
                                                 model_->max_toolbar_insets());
    if (!weak_this) {
      return;
    }
  }
  BOOL compensateFrameChangeByOffset = resizer_.compensateFrameChangeByOffset;
  // The compensateFrameChangeByOffset property determines whether the webview's
  // contentOffset should be adjusted to visually "pin" the web content during
  // fullscreen transitions. This makes the content appear to move as a whole
  // rather than scrolling internally. However, when toolbar heights are updated
  // (as in FullscreenModelToolbarHeightsUpdated), setting
  // compensateFrameChangeByOffset to YES would incorrectly shift the webview's
  // contentOffset, leading to unexpected content positioning. To prevent this,
  // compensateFrameChangeByOffset is set to NO here, ensuring the contentOffset
  // remains unchanged during the update.
  resizer_.compensateFrameChangeByOffset = NO;
  // Changes in the toolbar heights modifies the visible viewport so the WebView
  // needs to be resized as needed.
  [resizer_ updateForCurrentState];
  // Restore the compensateFrameChangeByOffset property to its original value.
  resizer_.compensateFrameChangeByOffset = compensateFrameChangeByOffset;
}

void LegacyFullscreenMediator::FullscreenModelProgressUpdated(
    FullscreenModel* model) {
  if (IsDisconnected()) {
    return;
  }
  DCHECK_EQ(model_, model);
  // Stops the animation only if there is a current animation running.
  if (animator_ && animator_.state == UIViewAnimatingStateActive) {
    StopAnimating(true /* update_model */);
  }
  base::WeakPtr<LegacyFullscreenMediator> weak_this =
      weak_factory_.GetWeakPtr();
  for (auto& observer : observers_) {
    observer.FullscreenProgressUpdated(controller_, model_->progress());
    if (!weak_this) {
      return;
    }
  }

  [resizer_ updateForCurrentState];
}

void LegacyFullscreenMediator::FullscreenModelEnabledStateChanged(
    FullscreenModel* model) {
  if (IsDisconnected()) {
    return;
  }
  DCHECK_EQ(model_, model);
  // Stops the animation only if there is a current animation running.
  if (animator_ && animator_.state == UIViewAnimatingStateActive) {
    StopAnimating(true /* update_model */);
  }
  base::WeakPtr<LegacyFullscreenMediator> weak_this =
      weak_factory_.GetWeakPtr();
  for (auto& observer : observers_) {
    observer.FullscreenEnabledStateChanged(controller_, model->enabled());
    if (!weak_this) {
      return;
    }
  }
}

void LegacyFullscreenMediator::FullscreenModelScrollEventStarted(
    FullscreenModel* model) {
  if (IsDisconnected()) {
    return;
  }
  DCHECK_EQ(model_, model);
  start_progress_ = model_->progress();
  StopAnimating(true /* update_model */);
  // Show the toolbars if the user begins a scroll past the bottom edge of the
  // screen and the toolbars have been fully collapsed.
  if (model_->enabled() && model_->is_scrolled_to_bottom() &&
      AreCGFloatsEqual(model_->progress(), 0.0) &&
      model_->can_collapse_toolbar()) {
    ExitFullscreen(FullscreenModeTransitionTrigger::kForcedByCode);
  }
}

void LegacyFullscreenMediator::FullscreenModelScrollEventEnded(
    FullscreenModel* model) {
  if (IsDisconnected()) {
    return;
  }
  DCHECK_EQ(model_, model);
  if (ios::provider::IsFullscreenSmoothScrollingSupported()) {
    if (model_->progress() >= 0.5) {
      fullscreen_exit_trigger_ =
          FullscreenModeTransitionTrigger::kUserInitiatedFinishedByCode;
      AnimateWithStyle(FullscreenAnimatorStyle::EXIT_FULLSCREEN);
    } else {
      AnimateWithStyle(FullscreenAnimatorStyle::ENTER_FULLSCREEN);
    }
    return;
  }
  bool scrolled_to_bottom = model_->enabled() &&
                            model_->is_scrolled_to_bottom() &&
                            AreCGFloatsEqual(model_->progress(), 0.0) &&
                            model_->can_collapse_toolbar();
  if (scrolled_to_bottom) {
    if (has_reached_bottom_once_) {
      // Subsequent times reaching the bottom: exit fullscreen.
      fullscreen_exit_trigger_ =
          FullscreenModeTransitionTrigger::kBottomReached;
      AnimateWithStyle(FullscreenAnimatorStyle::EXIT_FULLSCREEN);
    } else {
      // First time reaching the bottom.
      has_reached_bottom_once_ = true;
    }
  } else {
    has_reached_bottom_once_ = false;
    AnimateWithStyle(
        AnimatorStyleFromScrollDirection(model_->GetLastScrollDirection()));
  }
}

void LegacyFullscreenMediator::FullscreenModelWasReset(FullscreenModel* model) {
  if (IsDisconnected()) {
    return;
  }
  fullscreen_enter_trigger_ = std::nullopt;
  has_reached_bottom_once_ = false;
  // Stop any in-progress animations.  Don't update the model because this
  // callback occurs after the model's state is reset, and updating the model
  // the with active animator's current value would overwrite the reset value.
  StopAnimating(false /* update_model */);
  // Update observers for the reset progress value and set the inset range in
  // case this is a new WebState.
  base::WeakPtr<LegacyFullscreenMediator> weak_this =
      weak_factory_.GetWeakPtr();
  for (auto& observer : observers_) {
    observer.FullscreenViewportInsetRangeChanged(
        controller_, controller_->GetMinViewportInsets(),
        controller_->GetMaxViewportInsets());
    if (!weak_this) {
      return;
    }
    observer.FullscreenProgressUpdated(controller_, model_->progress());
    if (!weak_this) {
      return;
    }
  }

  [resizer_ updateForCurrentState];
}

void LegacyFullscreenMediator::AnimateWithStyle(FullscreenAnimatorStyle style) {
  if (animator_ && animator_.style == style) {
    return;
  }
  StopAnimating(true);
  DCHECK(!animator_);

  // Early return if there is no progress change.
  CGFloat start_progress = model_->progress();
  CGFloat final_progress = GetFinalFullscreenProgressForAnimation(style);
  if (AreCGFloatsEqual(start_progress, final_progress)) {
    return;
  }

  // Create the animator and set up its completion block.
  animator_ = [[FullscreenAnimator alloc] initWithStartProgress:start_progress
                                                          style:style];
  base::WeakPtr<LegacyFullscreenMediator> weak_mediator =
      weak_factory_.GetWeakPtr();
  [animator_ addAnimations:^{
    // Updates the WebView frame during the animation to have it animated.
    LegacyFullscreenMediator* mediator = weak_mediator.get();
    if (mediator) {
      [mediator->resizer_ forceToUpdateToProgress:final_progress];
    }
  }];
  [animator_ addCompletion:^(UIViewAnimatingPosition finalPosition) {
    DCHECK_EQ(finalPosition, UIViewAnimatingPositionEnd);
    LegacyFullscreenMediator* mediator = weak_mediator.get();
    if (!mediator) {
      return;
    }
    mediator->model_->AnimationEndedWithProgress(final_progress);
    mediator->animator_ = nil;

    // Histogram for entering or exiting Fullscreen mode based on
    // FullscreenModeTransitionTrigger value.
    if (model_->progress() == 0) {
      base::UmaHistogramEnumeration(
          kEnterFullscreenModeTransitionTriggerHistogram,
          FullscreenModeTransitionTrigger::kUserInitiatedFinishedByCode);
    } else if (model_->progress() == 1) {
      RecordFullscreenExitMode();
    }
    for (auto& observer : mediator->observers_) {
      observer.FullscreenDidAnimate(mediator->controller_, style);
      if (!weak_mediator) {
        return;
      }
    }
  }];

  // Notify observers that the animation will occur.
  base::WeakPtr<LegacyFullscreenMediator> weak_this =
      weak_factory_.GetWeakPtr();
  for (auto& observer : observers_) {
    observer.FullscreenWillAnimate(controller_, animator_);
    if (!weak_this) {
      return;
    }
  }

  // Only start the animator if animations have been added.
  if (animator_.hasAnimations) {
    [animator_ startAnimation];
  } else {
    animator_ = nil;
  }
}

void LegacyFullscreenMediator::StopAnimating(bool update_model) {
  if (!animator_) {
    return;
  }

  DCHECK_EQ(animator_.state, UIViewAnimatingStateActive);
  if (update_model) {
    model_->AnimationEndedWithProgress(animator_.currentProgress);
  }
  [animator_ stopAnimation:YES];
  animator_ = nil;
}

void LegacyFullscreenMediator::ResizeHorizontalInsets() {
  base::WeakPtr<LegacyFullscreenMediator> weak_this =
      weak_factory_.GetWeakPtr();
  for (auto& observer : observers_) {
    observer.ResizeHorizontalInsets(controller_);
    if (!weak_this) {
      return;
    }
  }
}

FullscreenAnimatorStyle
LegacyFullscreenMediator::AnimatorStyleFromScrollDirection(
    FullscreenModelScrollDirection direction) {
  switch (direction) {
    case FullscreenModelScrollDirection::kUp:
      fullscreen_exit_trigger_ =
          FullscreenModeTransitionTrigger::kUserInitiatedFinishedByCode;
      return FullscreenAnimatorStyle::EXIT_FULLSCREEN;
    case FullscreenModelScrollDirection::kNone:
      // Leave in fullscreen, if still in fullscreen.
      fullscreen_exit_trigger_ = FullscreenModeTransitionTrigger::kNoChange;
      return AreCGFloatsEqual(model_->progress(), 0.0)
                 ? FullscreenAnimatorStyle::ENTER_FULLSCREEN
                 : FullscreenAnimatorStyle::EXIT_FULLSCREEN;
    case FullscreenModelScrollDirection::kDown:
      return FullscreenAnimatorStyle::ENTER_FULLSCREEN;
  }
}

void LegacyFullscreenMediator::RecordFullscreenExitMode() {
  CHECK(fullscreen_exit_trigger_.has_value());
  base::UmaHistogramEnumeration(kExitFullscreenModeTransitionTriggerHistogram,
                                fullscreen_exit_trigger_.value());
}
