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

#include "chrome/browser/ui/ash/shelf/browser_status_monitor.h"

#include <memory>

#include "ash/public/cpp/shelf_types.h"
#include "base/debug/dump_without_crashing.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ash/browser_delegate/browser_controller.h"
#include "chrome/browser/ash/browser_delegate/browser_delegate.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chrome/browser/ui/ash/shelf/shelf_spinner_controller.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"

namespace {

// Checks if a given browser is running a windowed app. It will return true for
// web apps, hosted apps, and packaged V1 apps.
bool IsAppBrowser(const ash::BrowserDelegate* browser) {
  const ash::BrowserType browser_type = browser->GetType();
  if (browser_type != ash::BrowserType::kApp &&
      browser_type != ash::BrowserType::kAppPopup) {
    return false;
  }

  return browser->GetAppId().has_value();
}

BrowserWindowInterface* GetBrowserWithTabStripModel(
    TabStripModel* tab_strip_model) {
  BrowserWindowInterface* found_browser = nullptr;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&](BrowserWindowInterface* browser) {
        if (browser->GetTabStripModel() == tab_strip_model) {
          found_browser = browser;
        }
        return !found_browser;
      });
  return found_browser;
}

}  // namespace

// This class monitors the WebContent of the all tab and notifies a navigation
// to the BrowserStatusMonitor.
class BrowserStatusMonitor::LocalWebContentsObserver
    : public content::WebContentsObserver {
 public:
  LocalWebContentsObserver(content::WebContents* contents,
                           BrowserStatusMonitor* monitor)
      : content::WebContentsObserver(contents), monitor_(monitor) {}

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

  ~LocalWebContentsObserver() override = default;

  // content::WebContentsObserver
  void PrimaryPageChanged(content::Page& page) override {
    monitor_->OnTabNavigationFinished(web_contents());
  }

 private:
  raw_ptr<BrowserStatusMonitor> monitor_;
};

BrowserStatusMonitor::BrowserStatusMonitor(
    ChromeShelfController* shelf_controller)
    : shelf_controller_(shelf_controller),
      browser_tab_strip_tracker_(this, nullptr) {
  DCHECK(shelf_controller_);

  app_service_instance_helper_ =
      shelf_controller->app_service_app_window_controller()
          ->app_service_instance_helper();
  DCHECK(app_service_instance_helper_);
}

BrowserStatusMonitor::~BrowserStatusMonitor() {
  DCHECK(initialized_);

  ash::BrowserController::GetInstance()->RemoveObserver(this);

  // Simulate OnBrowserClosed() for all Browsers.
  ash::BrowserController::GetInstance()->ForEachBrowser(
      ash::BrowserController::BrowserOrder::kAscendingActivationTime,
      [&](ash::BrowserDelegate& browser_delegate) {
        OnBrowserClosed(&browser_delegate);
        return ash::BrowserController::kContinueIteration;
      });
}

void BrowserStatusMonitor::Initialize() {
  DCHECK(!initialized_);
  initialized_ = true;

  // Simulate OnBrowserCreated() for all existing Browsers.
  ash::BrowserController::GetInstance()->ForEachBrowser(
      ash::BrowserController::BrowserOrder::kAscendingActivationTime,
      [&](ash::BrowserDelegate& browser_delegate) {
        OnBrowserCreated(&browser_delegate);
        return ash::BrowserController::kContinueIteration;
      });

  // ash::BrowserController::AddObserver() comes before
  // BrowserTabStripTracker::Init() to ensure that OnBrowserCreated() is always
  // invoked before OnTabStripModelChanged() is invoked to describe the initial
  // state of the Browser.
  ash::BrowserController::GetInstance()->AddObserver(this);
  browser_tab_strip_tracker_.Init();
}

void BrowserStatusMonitor::ActiveUserChanged(const std::string& user_email) {
  // When the active profile changes, all windowed and tabbed apps owned by the
  // newly selected profile are added to the shelf, and the ones owned by other
  // profiles are removed.
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&](BrowserWindowInterface* browser) {
        const bool owned =
            multi_user_util::IsProfileFromActiveUser(browser->GetProfile());
        TabStripModel* const tab_strip_model = browser->GetTabStripModel();

        const BrowserWindowInterface::Type browser_type = browser->GetType();
        if (browser_type == BrowserWindowInterface::TYPE_APP ||
            browser_type == BrowserWindowInterface::TYPE_APP_POPUP) {
          // Add windowed apps owned by the current profile, and remove the one
          // owned by other profiles.
          const bool app_in_shelf = IsAppBrowserInShelf(browser);
          content::WebContents* const active_web_contents =
              tab_strip_model->GetActiveWebContents();

          if (owned && !app_in_shelf) {
            // Adding an app to the shelf consists of two actions: add the
            // browser (shelf item) and add the content (shelf item status).
            ash::BrowserDelegate* browser_delegate =
                ash::BrowserController::GetInstance()->GetDelegate(browser);
            AddAppBrowserToShelf(browser_delegate);
            if (active_web_contents) {
              shelf_controller_->UpdateAppState(active_web_contents,
                                                false /*remove*/);
            }
          } else if (!owned && app_in_shelf) {
            // Removing an app from the shelf requires to remove the content and
            // the shelf item (reverse order of addition).
            if (active_web_contents) {
              shelf_controller_->UpdateAppState(active_web_contents,
                                                true /*remove*/);
            }
            ash::BrowserDelegate* browser_delegate =
                ash::BrowserController::GetInstance()->GetDelegate(browser);
            RemoveAppBrowserFromShelf(browser_delegate);
          }

        } else if (browser_type == BrowserWindowInterface::TYPE_NORMAL) {
          // Add tabbed apps owned by the current profile, and remove the ones
          // owned by other profiles.
          for (int i = 0; i < tab_strip_model->count(); ++i) {
            shelf_controller_->UpdateAppState(
                tab_strip_model->GetWebContentsAt(i), !owned /*remove*/);
          }
        }
        return true;
      });

  // Update the browser state since some of the additions/removals above might
  // have had an impact on the browser item state.
  UpdateBrowserItemState();
}

void BrowserStatusMonitor::UpdateAppItemState(content::WebContents* contents,
                                              bool remove) {
  DCHECK(contents);
  DCHECK(initialized_);
  // It is possible to come here from Browser::SwapTabContent where the contents
  // cannot be associated with a browser. A removal however should be properly
  // processed.
  ash::BrowserDelegate* browser =
      ash::BrowserController::GetInstance()->GetBrowserForTab(contents);
  if (remove || (browser && multi_user_util::IsProfileFromActiveUser(
                                browser->GetBrowser().GetProfile()))) {
    shelf_controller_->UpdateAppState(contents, remove);
  }
}

void BrowserStatusMonitor::UpdateBrowserItemState() {
  DCHECK(initialized_);
  shelf_controller_->UpdateBrowserItemState();
}

void BrowserStatusMonitor::OnBrowserCreated(
    ash::BrowserDelegate* browser_delegate) {
  DCHECK(initialized_);

  BrowserWindowInterface* browser = &browser_delegate->GetBrowser();

#if DCHECK_IS_ON()
  auto insert_result = known_browsers_.insert(browser);
  DCHECK(insert_result.second);
#endif

  if (IsAppBrowser(browser_delegate) &&
      multi_user_util::IsProfileFromActiveUser(browser->GetProfile())) {
    AddAppBrowserToShelf(browser_delegate);
  }
}

void BrowserStatusMonitor::OnBrowserClosed(
    ash::BrowserDelegate* browser_delegate) {
  DCHECK(initialized_);

  BrowserWindowInterface* browser = &browser_delegate->GetBrowser();

#if DCHECK_IS_ON()
  size_t num_removed = known_browsers_.erase(browser);
  DCHECK_EQ(num_removed, 1U);
#endif

  if (IsAppBrowser(browser_delegate) &&
      multi_user_util::IsProfileFromActiveUser(browser->GetProfile())) {
    RemoveAppBrowserFromShelf(browser_delegate);
  }

  UpdateBrowserItemState();

  if (app_service_instance_helper_) {
    app_service_instance_helper_->OnBrowserRemoved();
  }
}

void BrowserStatusMonitor::OnTabStripModelChanged(
    TabStripModel* tab_strip_model,
    const TabStripModelChange& change,
    const TabStripSelectionChange& selection) {
  // OnBrowserAdded() must be invoked before OnTabStripModelChanged(). See
  // comment in constructor.
  BrowserWindowInterface* const browser =
      GetBrowserWithTabStripModel(tab_strip_model);
#if DCHECK_IS_ON()
  DCHECK(known_browsers_.contains(browser));
#endif

  if (change.type() == TabStripModelChange::kInserted) {
    for (const auto& contents : change.GetInsert()->contents) {
      if (webcontents_to_observer_map_.contains(contents.contents)) {
#if DCHECK_IS_ON()
        {
          // The tab must be in the set of tabs in transit.
          size_t num_removed = tabs_in_transit_.erase(contents.contents.get());
          DCHECK_EQ(num_removed, 1u);
        }
#endif
        OnTabMoved(tab_strip_model, contents.contents);
      } else {
#if DCHECK_IS_ON()
        DCHECK(!tabs_in_transit_.contains(contents.contents));
#endif
        OnTabInserted(tab_strip_model, contents.contents);
      }
    }
    UpdateBrowserItemState();
  } else if (change.type() == TabStripModelChange::kRemoved) {
    auto* remove = change.GetRemove();
    for (const auto& contents : remove->contents) {
      if (TabRemoveReasonUtils::WillDeleteTab(contents.remove_reason)) {
#if DCHECK_IS_ON()
        DCHECK(!tabs_in_transit_.contains(contents.contents));
#endif
        OnTabClosing(contents.contents);
      } else {
        // The tab will be reinserted immediately into another browser, so
        // this event is ignored.
        if (browser->GetType() == BrowserWindowInterface::TYPE_DEVTOOLS) {
          // TODO(crbug.com/40773744): when a dev tools window is docked, and
          // its WebContents is removed, it will not be reinserted into
          // another tab strip, so it should be treated as closed.
          OnTabClosing(contents.contents);
        } else {
#if DCHECK_IS_ON()
          // The tab must not be already in the set of tabs in transit.
          DCHECK(tabs_in_transit_.insert(contents.contents).second);
#endif
        }
      }
    }
  } else if (change.type() == TabStripModelChange::kReplaced) {
    auto* replace = change.GetReplace();
    OnTabReplaced(tab_strip_model, replace->old_contents,
                  replace->new_contents);
  }

  if (tab_strip_model->empty()) {
    return;
  }

  if (selection.active_tab_changed()) {
    OnActiveTabChanged(selection.old_contents, selection.new_contents);
  }
}

void BrowserStatusMonitor::AddAppBrowserToShelf(
    ash::BrowserDelegate* browser_delegate) {
  DCHECK(IsAppBrowser(browser_delegate));
  DCHECK(initialized_);

  const std::string app_id = *browser_delegate->GetAppId();
  DCHECK(!app_id.empty());
  if (!IsAppBrowserInShelfWithAppId(app_id)) {
    if (auto* chrome_controller = ChromeShelfController::instance()) {
      chrome_controller->GetShelfSpinnerController()->CloseSpinner(app_id);
    }
    shelf_controller_->SetAppStatus(app_id, ash::STATUS_RUNNING);
  }
  BrowserWindowInterface* browser = &browser_delegate->GetBrowser();
  browser_to_app_id_map_[browser] = app_id;
}

void BrowserStatusMonitor::RemoveAppBrowserFromShelf(
    ash::BrowserDelegate* browser_delegate) {
  DCHECK(IsAppBrowser(browser_delegate));
  DCHECK(initialized_);

  BrowserWindowInterface* browser = &browser_delegate->GetBrowser();
  auto iter = browser_to_app_id_map_.find(browser);
  if (iter != browser_to_app_id_map_.end()) {
    const std::string app_id = iter->second;
    browser_to_app_id_map_.erase(iter);
    if (!IsAppBrowserInShelfWithAppId(app_id)) {
      shelf_controller_->SetAppStatus(app_id, ash::STATUS_CLOSED);
    }
  }
}

bool BrowserStatusMonitor::IsAppBrowserInShelf(
    BrowserWindowInterface* browser) {
  return browser_to_app_id_map_.find(browser) != browser_to_app_id_map_.end();
}

bool BrowserStatusMonitor::IsAppBrowserInShelfWithAppId(
    const std::string& app_id) {
  for (const auto& iter : browser_to_app_id_map_) {
    if (iter.second == app_id) {
      return true;
    }
  }
  return false;
}

void BrowserStatusMonitor::OnActiveTabChanged(
    content::WebContents* old_contents,
    content::WebContents* new_contents) {
  // Use |new_contents|. |old_contents| could be nullptr.
  DCHECK(new_contents);
  ash::BrowserDelegate* browser =
      ash::BrowserController::GetInstance()->GetBrowserForTab(new_contents);

  // Update immediately on a tab change.
  if (old_contents &&
      (TabStripModel::kNoTab !=
       browser->GetBrowser().GetTabStripModel()->GetIndexOfWebContents(
           old_contents))) {
    UpdateAppItemState(old_contents, false /*remove*/);
  }

  if (new_contents) {
    UpdateAppItemState(new_contents, false /*remove*/);
    UpdateBrowserItemState();
    SetShelfIDForBrowserWindowContents(browser, new_contents);
  }

  if (app_service_instance_helper_) {
    app_service_instance_helper_->OnActiveTabChanged(old_contents,
                                                     new_contents);
  }
}

void BrowserStatusMonitor::OnTabReplaced(TabStripModel* tab_strip_model,
                                         content::WebContents* old_contents,
                                         content::WebContents* new_contents) {
  DCHECK(old_contents);
  DCHECK(new_contents);
  ash::BrowserDelegate* browser =
      ash::BrowserController::GetInstance()->GetBrowserForTab(new_contents);

  UpdateAppItemState(old_contents, true /*remove*/);
  RemoveWebContentsObserver(old_contents);

  UpdateAppItemState(new_contents, false /*remove*/);
  UpdateBrowserItemState();

  if (browser && IsAppBrowserInShelf(&browser->GetBrowser()) &&
      multi_user_util::IsProfileFromActiveUser(
          browser->GetBrowser().GetProfile())) {
    shelf_controller_->SetAppStatus(browser->GetAppId().value_or(std::string()),
                                    ash::STATUS_RUNNING);
  }

  if (tab_strip_model->GetActiveWebContents() == new_contents) {
    SetShelfIDForBrowserWindowContents(browser, new_contents);
  }

  AddWebContentsObserver(new_contents);

  if (app_service_instance_helper_) {
    app_service_instance_helper_->OnTabReplaced(old_contents, new_contents);
  }
}

void BrowserStatusMonitor::OnTabInserted(TabStripModel* tab_strip_model,
                                         content::WebContents* contents) {
  UpdateAppItemState(contents, false /*remove*/);
  // If the visible navigation entry is the initial entry, wait until a
  // navigation status changes before setting the browser window Shelf ID
  // (done by the web contents observer added by AddWebContentsObserver()).
  if (tab_strip_model->GetActiveWebContents() == contents &&
      !contents->GetController().GetVisibleEntry()->IsInitialEntry()) {
    ash::BrowserDelegate* browser =
        ash::BrowserController::GetInstance()->GetBrowserForTab(contents);
    SetShelfIDForBrowserWindowContents(browser, contents);
  }

  AddWebContentsObserver(contents);

  if (app_service_instance_helper_) {
    app_service_instance_helper_->OnTabInserted(contents);
  }
}

void BrowserStatusMonitor::OnTabClosing(content::WebContents* contents) {
  UpdateAppItemState(contents, true /*remove*/);
  RemoveWebContentsObserver(contents);

  if (app_service_instance_helper_) {
    app_service_instance_helper_->OnTabClosing(contents);
  }
}

void BrowserStatusMonitor::OnTabMoved(TabStripModel* tab_strip_model,
                                      content::WebContents* contents) {
  // TODO(crbug.com/40763808): split this into inserted and moved cases.
  OnTabInserted(tab_strip_model, contents);
}

void BrowserStatusMonitor::OnTabNavigationFinished(
    content::WebContents* contents) {
  UpdateAppItemState(contents, false /*remove*/);
  UpdateBrowserItemState();

  // Navigating may change the ShelfID associated with the WebContents.
  ash::BrowserDelegate* browser =
      ash::BrowserController::GetInstance()->GetBrowserForTab(contents);
  if (browser && browser->GetActiveWebContents() == contents) {
    SetShelfIDForBrowserWindowContents(browser, contents);
  }
}

void BrowserStatusMonitor::AddWebContentsObserver(
    content::WebContents* contents) {
  if (webcontents_to_observer_map_.find(contents) ==
      webcontents_to_observer_map_.end()) {
    webcontents_to_observer_map_[contents] =
        std::make_unique<LocalWebContentsObserver>(contents, this);
  }
}

void BrowserStatusMonitor::RemoveWebContentsObserver(
    content::WebContents* contents) {
  DCHECK(webcontents_to_observer_map_.find(contents) !=
         webcontents_to_observer_map_.end());
  webcontents_to_observer_map_.erase(contents);
}

void BrowserStatusMonitor::SetShelfIDForBrowserWindowContents(
    ash::BrowserDelegate* browser,
    content::WebContents* web_contents) {
  shelf_controller_->SetShelfIDForBrowserWindowContents(browser, web_contents);

  if (app_service_instance_helper_) {
    app_service_instance_helper_->OnSetShelfIDForBrowserWindowContents(
        web_contents);
  }
}
