// Copyright 2019 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/overlays/model/overlay_presenter_impl.h"

#import "base/check_op.h"
#import "base/memory/ptr_util.h"
#import "base/not_fatal_until.h"
#import "ios/chrome/app/application_delegate/startup_information.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/browser/overlays/model/public/overlay_callback_manager.h"
#import "ios/chrome/browser/overlays/model/public/overlay_presentation_context.h"
#import "ios/chrome/browser/overlays/model/public/overlay_presenter_observer.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request_support.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"

#pragma mark - Factory method

// static
OverlayPresenter* OverlayPresenter::FromBrowser(Browser* browser,
                                                OverlayModality modality) {
  OverlayPresenterImpl::Container::CreateForUserData(browser, browser);
  return OverlayPresenterImpl::Container::FromUserData(browser)
      ->PresenterForModality(modality);
}

#pragma mark - OverlayPresenterImpl::Container

OverlayPresenterImpl::Container::Container(Browser* browser)
    : browser_(browser) {
  DCHECK(browser_);
}

OverlayPresenterImpl::Container::~Container() = default;

OverlayPresenterImpl* OverlayPresenterImpl::Container::PresenterForModality(
    OverlayModality modality) {
  auto& presenter = presenters_[modality];
  if (!presenter) {
    presenter = base::WrapUnique(new OverlayPresenterImpl(browser_, modality));
  }
  return presenter.get();
}

#pragma mark - OverlayPresenterImpl

OverlayPresenterImpl::OverlayPresenterImpl(Browser* browser,
                                           OverlayModality modality)
    : modality_(modality),
      web_state_list_(browser->GetWebStateList()),
      profile_state_(browser->GetSceneState().profileState) {
  if (profile_state_) {
    profile_state_observer_bridge_ =
        [[ProfileStateObserverBridge alloc] initWithObserver:this];
    [profile_state_ addObserver:profile_state_observer_bridge_];
  }
  StartObserving(browser);
  DCHECK(web_state_list_);
  SetActiveWebState(web_state_list_->GetActiveWebState());
}

OverlayPresenterImpl::~OverlayPresenterImpl() {
  [profile_state_observer_bridge_ resetObserver];

  // Notify all observers that the current OverlayPresenter will be destroyed.
  for (auto& observer : observers_) {
    observer.OverlayPresenterDestroyed(this);
  }

  // The presentation context must be reset after notifying all the observers.
  DCHECK(!presentation_context_);

  SetActiveWebState(nullptr);
  StopObserving();

  // All Webstates are detached before the Browser is destroyed so all request
  // must be cancelled at this point.
  DCHECK(!detached_presenting_request_queue_);
  removed_request_awaiting_dismissal_ = nullptr;
  web_state_list_ = nullptr;
}

#pragma mark - Public

#pragma mark OverlayPresenter

OverlayModality OverlayPresenterImpl::GetModality() const {
  return modality_;
}

void OverlayPresenterImpl::SetPresentationContext(
    OverlayPresentationContext* presentation_context) {
  // When the presentation context is reset, the presenter will begin showing
  // overlays in the new presentation context.  Cancel overlay state from the
  // previous context since this Browser's overlays will no longer be presented
  // there.
  if (presentation_context_) {
    CancelAllOverlayUI();
    presentation_context_->RemoveObserver(this);
  }

  presentation_context_ = presentation_context;

  // Reset `presenting` since it was tracking the status for the previous
  // delegate's presentation context.
  presenting_ = false;
  presented_request_ = nullptr;
  previously_presented_requests_.clear();

  if (presentation_context_) {
    presentation_context_->AddObserver(this);
    PresentOverlayForActiveRequest();
  }
}

void OverlayPresenterImpl::AddObserver(OverlayPresenterObserver* observer) {
  observers_.AddObserver(observer);
}

void OverlayPresenterImpl::RemoveObserver(OverlayPresenterObserver* observer) {
  observers_.RemoveObserver(observer);
}

bool OverlayPresenterImpl::IsShowingOverlayUI() const {
  return presenting_;
}

#pragma mark - ProfileStateObserver

void OverlayPresenterImpl::OnProfileStateDidTransitionToInitStage(
    ProfileState* profile_state,
    ProfileInitStage next_init_stage,
    ProfileInitStage from_init_stage) {
  if (next_init_stage < ProfileInitStage::kNormalUI) {
    return;
  }
  if (!presenting_) {
    PresentOverlayForActiveRequest();
    return;
  }
}

#pragma mark - Private

#pragma mark Accessors

void OverlayPresenterImpl::SetActiveWebState(web::WebState* web_state) {
  if (active_web_state_ == web_state) {
    return;
  }

  OverlayRequest* previously_active_request =
      removed_request_awaiting_dismissal_ != nullptr
          ? removed_request_awaiting_dismissal_.get()
          : GetActiveRequest();

  // The UI should be cancelled instead of hidden if the presenter does not
  // expect to show any more overlay UI for previously active WebState in the UI
  // delegate's presentation context.  This occurs:
  // - when the presenting WebState is replaced, and
  // - when the presenting WebState is detached from the WebStateList.
  const bool should_cancel_ui = detaching_presenting_web_state_;

  active_web_state_ = web_state;
  detaching_presenting_web_state_ = false;

  // Early return if there's no UI delegate, since presentation cannot occur.
  if (!presentation_context_) {
    return;
  }

  // If not already presenting, immediately show the next overlay.
  if (!presenting_) {
    PresentOverlayForActiveRequest();
    return;
  }

  // If presenting_ is true and the previously active request is not the
  // currently presented request, this is likely because the presenting overlay
  // is still in the process of being dismissed and multiple tabs have been
  // opened in the process.
  if (previously_active_request != presented_request_) {
    return;
  }

  // If the active WebState changes while an overlay is being presented, the
  // presented UI needs to be dismissed before the next overlay for the new
  // active WebState can be shown.  The new active WebState's overlays will be
  // presented when the previous overlay's dismissal callback is executed.
  DCHECK(previously_active_request);
  if (should_cancel_ui) {
    CancelOverlayUIForRequest(previously_active_request);
  } else {
    // For WebState activations, the overlay UI for the previously active
    // WebState should be hidden, as it may be shown again upon reactivating.
    presentation_context_->HideOverlayUI(previously_active_request);
  }
}

OverlayRequestQueueImpl* OverlayPresenterImpl::GetQueueForWebState(
    web::WebState* web_state) const {
  if (!web_state || !web_state->IsRealized()) {
    return nullptr;
  }

  OverlayRequestQueueImpl::Container::CreateForWebState(web_state);
  return OverlayRequestQueueImpl::Container::FromWebState(web_state)
      ->QueueForModality(modality_);
}

OverlayRequest* OverlayPresenterImpl::GetFrontRequestForWebState(
    web::WebState* web_state) const {
  OverlayRequestQueueImpl* queue = GetQueueForWebState(web_state);
  return queue ? queue->front_request() : nullptr;
}

OverlayRequestQueueImpl* OverlayPresenterImpl::GetActiveQueue() const {
  return GetQueueForWebState(active_web_state_);
}

OverlayRequest* OverlayPresenterImpl::GetActiveRequest() const {
  return GetFrontRequestForWebState(active_web_state_);
}

#pragma mark UI Presentation and Dismissal helpers

void OverlayPresenterImpl::PresentOverlayForActiveRequest() {
  // Don't show an infobar if the profile isn't in its normal state, or if the
  // application is terminating.
  if (profile_state_ &&
      (profile_state_.initStage < ProfileInitStage::kNormalUI ||
       profile_state_.startupInformation.isTerminating)) {
    return;
  }

  // Overlays cannot be shown without a presentation context or if the
  // presentation context is already showing overlay UI.
  if (!presentation_context_ || presentation_context_->IsShowingOverlayUI()) {
    return;
  }

  // No presentation is necessary if there is no active reqeust.
  OverlayRequest* request = GetActiveRequest();
  if (!request) {
    return;
  }

  // If the UI is disabled, no presentation nor preparation should occur.
  // `PrepareToShowOverlayUI()` is dismissing the keyboard, so do an early
  // return.
  if (presentation_context_->IsUIDisabled()) {
    return;
  }

  // Presentation cannot occur if the context is currently unable to show the UI
  // for `request`.  Attempt to prepare the presentation context for `request`.
  if (!presentation_context_->CanShowUIForRequest(request)) {
    presentation_context_->PrepareToShowOverlayUI(request);
    return;
  }

  // If an overlay is already presented, the Presentation Context should be
  // marked as showing an Overlay.
  DCHECK(!presenting_);
  presenting_ = true;
  presented_request_ = request;

  // Notify the observers that the overlay UI is about to be shown.
  bool initial_presentation = !previously_presented_requests_.contains(request);
  for (auto& observer : observers_) {
    if (observer.GetRequestSupport(this)->IsRequestSupported(request)) {
      observer.WillShowOverlay(this, request, initial_presentation);
    }
  }

  // Record that the request was shown, and add the completion callback to
  // remove the request from the set.
  previously_presented_requests_.insert(request);
  request->GetCallbackManager()->AddCompletionCallback(
      base::BindOnce(&OverlayPresenterImpl::OverlayWasCompleted,
                     weak_factory_.GetWeakPtr(), request));

  // Present the overlay UI via the UI delegate.
  OverlayPresentationCallback presentation_callback = base::BindOnce(
      &OverlayPresenterImpl::OverlayWasPresented, weak_factory_.GetWeakPtr(),
      presentation_context_, request);
  OverlayDismissalCallback dismissal_callback =
      base::BindOnce(&OverlayPresenterImpl::OverlayWasDismissed,
                     weak_factory_.GetWeakPtr(), presentation_context_,
                     request->GetRequestId(), GetActiveQueue()->GetWeakPtr());
  presentation_context_->ShowOverlayUI(
      request, std::move(presentation_callback), std::move(dismissal_callback));
}

void OverlayPresenterImpl::OverlayWasPresented(
    OverlayPresentationContext* presentation_context,
    OverlayRequest* request) {
  DCHECK_EQ(presentation_context_, presentation_context);
  DCHECK_EQ(presented_request_, request);
  for (auto& observer : observers_) {
    if (observer.GetRequestSupport(this)->IsRequestSupported(request)) {
      observer.DidShowOverlay(this, request);
    }
  }
}

void OverlayPresenterImpl::OverlayWasDismissed(
    OverlayPresentationContext* presentation_context,
    OverlayRequestId request_id,
    base::WeakPtr<OverlayRequestQueueImpl> queue,
    OverlayDismissalReason reason) {
  // If the UI delegate is reset while presenting an overlay, that overlay will
  // be cancelled and dismissed.  The presenter is now using the new UI
  // delegate's presentation context, so this dismissal should not trigger
  // presentation logic.
  if (presentation_context_ != presentation_context) {
    return;
  }

  // When the presenter has been replaced as the delegate of the active
  // OverlayRequestQueue, observers are notified of DidHideOverlay() and
  // `presented_request_` is reset early. Thus, there is no need to do any
  // dismissal bookkeeping since the request has been removed.
  if (detached_queue_replaced_delegate_) {
    presenting_ = false;
    detached_queue_replaced_delegate_ = false;
    if (GetActiveRequest()) {
      PresentOverlayForActiveRequest();
    }
    return;
  }

  OverlayRequest* request = presented_request_;
  if (!request && removed_request_awaiting_dismissal_ &&
      removed_request_awaiting_dismissal_->GetRequestId() == request_id) {
    request = removed_request_awaiting_dismissal_.get();
  }

  CHECK(request, base::NotFatalUntil::M156);
  DCHECK_EQ(request->GetRequestId(), request_id);

  // Pop the request for overlays dismissed by the user.  The check against
  // `removed_request_awaiting_dismissal_` prevents the queue's front request
  // from being popped if this dismissal was caused by `request`'s removal from
  // the queue.
  if (reason == OverlayDismissalReason::kUserInteraction && queue &&
      request != removed_request_awaiting_dismissal_.get()) {
    queue->PopFrontRequest();
    // Popping the request should transfer ownership of the request to the
    // OverlayPresenter until the completion of DidHideOverlay() observer
    // callbacks below.
    DCHECK_EQ(removed_request_awaiting_dismissal_.get(), request);
  }

  presenting_ = false;
  presented_request_ = nullptr;
  // The OverlayPresenter remains as the delegate for
  // `detached_presenting_request_queue_` to ensure that `presented_request_` is
  // not deleted before the dismissal of its UI is finished.  Since the UI is
  // now being dismissed, this reference is not needed anymore.
  detached_presenting_request_queue_ = nullptr;

  // Notify the observers that the overlay UI was hidden.
  for (auto& observer : observers_) {
    if (observer.GetRequestSupport(this)->IsRequestSupported(request)) {
      observer.DidHideOverlay(this, request);
    }
  }

  // Now that observers have been notified that the UI for `request` was hidden,
  // `removed_request_awaiting_dismissal_` can be reset since the request no
  // longer needs to be kept alive.
  removed_request_awaiting_dismissal_ = nullptr;

  // Only show the next overlay if the active request has changed, either
  // because the frontmost request was popped or because the active WebState has
  // changed.
  if (GetActiveRequest() != request) {
    PresentOverlayForActiveRequest();
  }
}

void OverlayPresenterImpl::OverlayWasCompleted(OverlayRequest* request,
                                               OverlayResponse* response) {
  previously_presented_requests_.erase(request);
}

#pragma mark UI Cancellation helpers

void OverlayPresenterImpl::CancelOverlayUIForRequest(OverlayRequest* request) {
  if (!presentation_context_ || !request) {
    return;
  }
  presentation_context_->CancelOverlayUI(request);
}

void OverlayPresenterImpl::CancelAllOverlayUI() {
  for (int i = 0; i < web_state_list_->count(); ++i) {
    CancelOverlayUIForRequest(
        GetFrontRequestForWebState(web_state_list_->GetWebStateAt(i)));
  }
}

#pragma mark WebState helpers

void OverlayPresenterImpl::WebStateAddedToBrowser(web::WebState* web_state) {
  OverlayRequestQueueImpl* queue = GetQueueForWebState(web_state);
  queue->AddObserver(this);
  queue->SetDelegate(this);
}

void OverlayPresenterImpl::WebStateRemovedFromBrowser(
    web::WebState* web_state) {
  OverlayRequestQueueImpl* queue = GetQueueForWebState(web_state);
  queue->RemoveObserver(this);
  // Only reset the delegate if there isn't a currently presented overlay or
  // `presented_request_`'s WebState is not the WebState being removed. This
  // will allow the presenter to extend the lifetime of `presented_request_` if
  // it is removed from the queue before its dismissal finishes.
  if (!presented_request_ ||
      presented_request_->GetQueueWebState() != web_state) {
    queue->SetDelegate(nullptr);
  }

  if (presented_request_ &&
      presented_request_->GetQueueWebState() == web_state) {
    detached_presenting_request_queue_ = GetQueueForWebState(web_state);
  } else {
    // For inactive WebState removals, the overlay UI can be cancelled
    // immediately.
    CancelOverlayUIForRequest(GetFrontRequestForWebState(web_state));
  }
}

#pragma mark -
#pragma mark TabsDependencyInstaller

void OverlayPresenterImpl::OnWebStateInserted(web::WebState* web_state) {
  WebStateAddedToBrowser(web_state);
}

void OverlayPresenterImpl::OnWebStateRemoved(web::WebState* web_state) {
  detaching_presenting_web_state_ =
      presented_request_ ? presented_request_->GetQueueWebState() == web_state
                         : false;

  WebStateRemovedFromBrowser(web_state);
}

void OverlayPresenterImpl::OnWebStateDeleted(web::WebState* web_state) {
  // Nothing to do.
}

void OverlayPresenterImpl::OnActiveWebStateChanged(web::WebState* old_active,
                                                   web::WebState* new_active) {
  SetActiveWebState(new_active);
}

#pragma mark OverlayRequestQueueImpl::Delegate

void OverlayPresenterImpl::OverlayRequestRemoved(
    OverlayRequestQueueImpl* queue,
    std::unique_ptr<OverlayRequest> request,
    bool cancelled) {
  OverlayRequest* removed_request = request.get();
  if (presented_request_ == removed_request) {
    removed_request_awaiting_dismissal_ = std::move(request);
    if (detached_presenting_request_queue_) {
      detached_presenting_request_queue_ = nullptr;
      queue->SetDelegate(nullptr);
    }
  }
  if (cancelled) {
    CancelOverlayUIForRequest(removed_request);
  }
}

void OverlayPresenterImpl::OverlayRequestQueueWillReplaceDelegate(
    OverlayRequestQueueImpl* queue) {
  if (!presented_request_ || presented_request_ != queue->front_request()) {
    return;
  }
  // If `presented_request_` is in the queue that is replacing this presenter
  // as the delegate, it is no longer possible to extend the lifetime of
  // `presented_request_`. Thus, call DidHideOverlay while it is still valid
  // and reset its reference.
  for (auto& observer : observers_) {
    if (observer.GetRequestSupport(this)->IsRequestSupported(
            presented_request_)) {
      observer.DidHideOverlay(this, presented_request_);
    }
  }
  presented_request_ = nullptr;
  detached_presenting_request_queue_ = nullptr;
  detached_queue_replaced_delegate_ = true;
}

#pragma mark OverlayRequestQueueImpl::Observer

void OverlayPresenterImpl::RequestAddedToQueue(OverlayRequestQueueImpl* queue,
                                               OverlayRequest* request,
                                               size_t index) {
  // If `request` is not active, there is no need to trigger any presentation.
  if (request != GetActiveRequest()) {
    return;
  }

  // If the added request is active and there is no presentation occurring,
  // present the overlay UI immediately.
  if (!presenting_) {
    PresentOverlayForActiveRequest();
    return;
  }

  // `request` is the new active request, but overlay UI is already
  // presented.  This occurs when:
  // 1. `request` is added after `presented_request_` is cancelled, but
  //    before its UI is finished being dismissed,
  // 2. `request` is added immediately after a WebState activation, but
  //    before the overlay UI from the previously active WebState's front
  //    request is finished being dismissed, or
  // 3. `request` is inserted to the front of the active WebState's request
  //    queue.
  //
  // For scenarios (1) and (2), the UI is already in the process of being
  // dismissed, and `request`'s UI will be presented when that dismissal
  // finishes.  For scenario (3), the UI for the presented request needs to
  // be hidden so that the UI for `request` can be presented.
  bool should_dismiss_for_inserted_request =
      presented_request_ && queue->size() > 1 &&
      queue->GetRequest(/*index=*/1) == presented_request_;
  if (should_dismiss_for_inserted_request) {
    presentation_context_->HideOverlayUI(presented_request_);
  }
}

void OverlayPresenterImpl::OverlayRequestQueueDestroyed(
    OverlayRequestQueueImpl* queue) {
  queue->RemoveObserver(this);
}

#pragma mark - OverlayPresentationContextObserver

void OverlayPresenterImpl::
    OverlayPresentationContextWillChangePresentationCapabilities(
        OverlayPresentationContext* presentation_context,
        OverlayPresentationContext::UIPresentationCapabilities capabilities) {
  DCHECK_EQ(presentation_context_, presentation_context);
  // Hide the presented overlay UI if the presentation context is transitioning
  // to a state where that UI is not supported.
  if (presented_request_ && !presentation_context->CanShowUIForRequest(
                                presented_request_, capabilities)) {
    DCHECK(presenting_);
    presentation_context_->HideOverlayUI(presented_request_);
  }
}

void OverlayPresenterImpl::
    OverlayPresentationContextDidChangePresentationCapabilities(
        OverlayPresentationContext* presentation_context) {
  DCHECK_EQ(presentation_context_, presentation_context);
  if (!presenting_) {
    PresentOverlayForActiveRequest();
  }
}

void OverlayPresenterImpl::OverlayPresentationContextDidEnableUI(
    OverlayPresentationContext* presentation_context) {
  DCHECK_EQ(presentation_context_, presentation_context);
  if (!presenting_) {
    PresentOverlayForActiveRequest();
  }
}

void OverlayPresenterImpl::OverlayPresentationContextDidMoveToWindow(
    OverlayPresentationContext* presentation_context,
    UIWindow* window) {
  DCHECK_EQ(presentation_context_, presentation_context);
  if (!presenting_ && window) {
    PresentOverlayForActiveRequest();
  }
}
