// Copyright 2024 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/webui/webui_embedding_context.h"

#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_user_data.h"

namespace webui {
namespace {

// Tracks changes to the wrapped `browser_window_interface_`, notifying clients
// of changes as approprirate.
class EmbeddingBrowserTracker {
 public:
  explicit EmbeddingBrowserTracker(base::RepeatingClosure browser_change_cb)
      : browser_change_cb_(std::move(browser_change_cb)) {}
  EmbeddingBrowserTracker(const EmbeddingBrowserTracker&) = delete;
  EmbeddingBrowserTracker& operator=(const EmbeddingBrowserTracker&) = delete;
  ~EmbeddingBrowserTracker() = default;

  // Updates `browser_window_interface_` and registers/notifies listeners if
  // appropriate.
  void SetBrowserWindowInterface(
      BrowserWindowInterface* browser_window_interface) {
    if (browser_window_interface_ == browser_window_interface) {
      return;
    }
    browser_did_close_subscription_.reset();
    browser_window_interface_ = browser_window_interface;

    if (browser_window_interface_) {
      browser_did_close_subscription_ =
          browser_window_interface_->RegisterBrowserDidClose(
              base::BindRepeating(&EmbeddingBrowserTracker::OnBrowserDidClose,
                                  base::Unretained(this)));
    }
    browser_change_cb_.Run();
  }

  BrowserWindowInterface* browser_window_interface() {
    return browser_window_interface_;
  }

 private:
  void OnBrowserDidClose(BrowserWindowInterface* browser_window_interface) {
    CHECK_EQ(browser_window_interface_, browser_window_interface);
    SetBrowserWindowInterface(nullptr);
  }

  // The browser interface currently embedding the host contents.
  raw_ptr<BrowserWindowInterface> browser_window_interface_ = nullptr;

  // Notifies this when `browser_window_interface_` has closed.
  std::optional<base::CallbackListSubscription> browser_did_close_subscription_;

  // Notifies clients of changes to `browser_window_interface_`.
  base::RepeatingClosure browser_change_cb_;
};

// Tracks changes to the wrapped `tab_interface_`, notifying clients of changes
// as approprirate.
class EmbeddingTabTracker {
 public:
  EmbeddingTabTracker(base::RepeatingClosure tab_change_cb,
                      base::RepeatingClosure browser_change_cb)
      : tab_change_cb_(std::move(tab_change_cb)),
        browser_change_cb_(std::move(browser_change_cb)) {}
  EmbeddingTabTracker(const EmbeddingTabTracker&) = delete;
  EmbeddingTabTracker& operator=(const EmbeddingTabTracker&) = delete;
  ~EmbeddingTabTracker() = default;

  // Updates `tab_interface_` and registers/notifies listeners if appropriate.
  void SetTabInterface(tabs::TabInterface* tab_interface) {
    if (tab_interface_ == tab_interface) {
      return;
    }
    tab_will_detach_subscription_.reset();
    tab_did_insert_subscription_.reset();
    tab_interface_ = tab_interface;

    if (tab_interface) {
      tab_will_detach_subscription_ =
          tab_interface_->RegisterWillDetach(base::BindRepeating(
              &EmbeddingTabTracker::OnTabWillDetach, base::Unretained(this)));
      tab_did_insert_subscription_ =
          tab_interface_->RegisterDidInsert(base::BindRepeating(
              &EmbeddingTabTracker::OnTabDidInsert, base::Unretained(this)));
    }

    // Both browser and tab changes should be propagated.
    browser_change_cb_.Run();
    tab_change_cb_.Run();
  }

  tabs::TabInterface* tab_interface() { return tab_interface_; }

 private:
  void OnTabWillDetach(tabs::TabInterface* tab_interface,
                       tabs::TabInterface::DetachReason detach_reason) {
    CHECK_EQ(tab_interface_, tab_interface);
    if (detach_reason == tabs::TabInterface::DetachReason::kDelete) {
      SetTabInterface(nullptr);
    }
  }

  void OnTabDidInsert(tabs::TabInterface* tab_interface) {
    CHECK_EQ(tab_interface_, tab_interface);
    browser_change_cb_.Run();
  }

  // The tab interface currently embedding the host contents.
  raw_ptr<tabs::TabInterface> tab_interface_ = nullptr;

  // Notifies this when `tab_interface_` will detach, tracked to invalidate
  // the tab before destruction.
  std::optional<base::CallbackListSubscription> tab_will_detach_subscription_;

  // Notifies this when `tab_interface_` is inserted into a new browser window,
  // tracked to ensure clients are notified of browser window changes.
  std::optional<base::CallbackListSubscription> tab_did_insert_subscription_;

  // Notifies clients of changes to `tab_interface_`.
  base::RepeatingClosure tab_change_cb_;

  // Notifies clients of changes to the `tab_interface_`'s browser.
  base::RepeatingClosure browser_change_cb_;
};

// Responsible for managing embedding interface changes for the hosted
// WebContents, notifing downstream clients of embedding context changes.
class EmbedderContextData
    : public content::WebContentsUserData<EmbedderContextData> {
 public:
  EmbedderContextData(const EmbedderContextData&) = delete;
  EmbedderContextData& operator=(const EmbedderContextData&) = delete;
  ~EmbedderContextData() override = default;

  static EmbedderContextData* GetOrCreate(content::WebContents* web_contents) {
    EmbedderContextData::CreateForWebContents(web_contents);
    return EmbedderContextData::FromWebContents(web_contents);
  }

  void SetBrowserWindowInterface(
      BrowserWindowInterface* browser_window_interface) {
    // Tabs will always belong to a browser and are tracked together, both
    // browser and tab trackers should not be set independently.
    CHECK(!tab_tracker_)
        << "Browser and tab trackers should not be set independently";
    if (!browser_tracker_) {
      browser_tracker_ = std::make_unique<EmbeddingBrowserTracker>(
          base::BindRepeating(&EmbedderContextData::NotifyBrowserChanged,
                              base::Unretained(this)));
    }
    browser_tracker_->SetBrowserWindowInterface(browser_window_interface);

    // Setting browser window interface to nullptr removes the tracker
    // after calling the listeners.
    if (!browser_window_interface) {
      browser_tracker_.reset();
    }
  }

  BrowserWindowInterface* GetBrowserWindowInterface() {
    // Source the browser interface either directly from the tracked browser or
    // via the tracked tab.
    if (tab_tracker_) {
      return tab_tracker_->tab_interface()
                 ? tab_tracker_->tab_interface()->GetBrowserWindowInterface()
                 : nullptr;
    }
    if (browser_tracker_) {
      return browser_tracker_->browser_window_interface();
    }
    return nullptr;
  }

  void SetTabInterface(tabs::TabInterface* tab_interface) {
    // Tabs will always belong to a browser and are tracked together, both
    // browser and tab trackers should not be set independently.
    CHECK(!browser_tracker_)
        << "Browser and tab trackers should not be set independently";
    if (!tab_tracker_) {
      tab_tracker_ = std::make_unique<EmbeddingTabTracker>(
          base::BindRepeating(&EmbedderContextData::NotifyTabChanged,
                              base::Unretained(this)),
          base::BindRepeating(&EmbedderContextData::NotifyBrowserChanged,
                              base::Unretained(this)));
    }
    tab_tracker_->SetTabInterface(tab_interface);

    // Setting tab interface to nullptr removes the tracker after
    // calling the listeners.
    if (!tab_interface) {
      tab_tracker_.reset();
    }
  }

  tabs::TabInterface* GetTabInterface() {
    return tab_tracker_ ? tab_tracker_->tab_interface() : nullptr;
  }

  base::CallbackListSubscription RegisterBrowserWindowInterfaceChanged(
      base::RepeatingClosure browser_changed_cb) {
    return browser_change_callbacks_.Add(std::move(browser_changed_cb));
  }

  base::CallbackListSubscription RegisterTabInterfaceChanged(
      base::RepeatingClosure tab_change_cb) {
    return tab_change_callbacks_.Add(std::move(tab_change_cb));
  }

 private:
  friend class content::WebContentsUserData<EmbedderContextData>;

  explicit EmbedderContextData(content::WebContents* web_contents)
      : WebContentsUserData<EmbedderContextData>(*web_contents) {}

  // Notifies clients their embedding browser has changed.
  void NotifyBrowserChanged() { browser_change_callbacks_.Notify(); }

  // Notifies clients their embedding tab has changed.
  void NotifyTabChanged() { tab_change_callbacks_.Notify(); }

  // Defined only if actively tracking an embedding browser.
  std::unique_ptr<EmbeddingBrowserTracker> browser_tracker_;

  // Defined only if actively tracking an embedding tab.
  std::unique_ptr<EmbeddingTabTracker> tab_tracker_;

  // Client registrations for changes to the embedding browser.
  base::RepeatingCallbackList<void()> browser_change_callbacks_;

  // Client registrations for changes to the embedding tab.
  base::RepeatingCallbackList<void()> tab_change_callbacks_;

  WEB_CONTENTS_USER_DATA_KEY_DECL();
};

WEB_CONTENTS_USER_DATA_KEY_IMPL(EmbedderContextData);

}  // namespace

base::CallbackListSubscription InitEmbeddingContext(
    tabs::TabInterface* tab_interface) {
  // Set the initial tab interface and configure updates for discard changes.
  // TODO(crbug.com/371950942): Once the new discard implementation has landed
  // there is no need to re-set the interface on discard and this can be inlined
  // into TabModel.
  SetTabInterface(tab_interface->GetContents(), tab_interface);
  return tab_interface->RegisterWillDiscardContents(base::BindRepeating(
      [](tabs::TabInterface* tab, content::WebContents* old_contents,
         content::WebContents* new_contents) {
        SetTabInterface(new_contents, tab);
      }));
}

void SetBrowserWindowInterface(
    content::WebContents* host_contents,
    BrowserWindowInterface* browser_window_interface) {
  EmbedderContextData::GetOrCreate(host_contents)
      ->SetBrowserWindowInterface(browser_window_interface);
}

BrowserWindowInterface* GetBrowserWindowInterface(
    content::WebContents* host_contents) {
  return EmbedderContextData::GetOrCreate(host_contents)
      ->GetBrowserWindowInterface();
}

void SetTabInterface(content::WebContents* host_contents,
                     tabs::TabInterface* tab_interface) {
  EmbedderContextData::GetOrCreate(host_contents)
      ->SetTabInterface(tab_interface);
}

tabs::TabInterface* GetTabInterface(content::WebContents* host_contents) {
  return EmbedderContextData::GetOrCreate(host_contents)->GetTabInterface();
}

base::CallbackListSubscription RegisterBrowserWindowInterfaceChanged(
    content::WebContents* host_contents,
    base::RepeatingClosure context_changed_cb) {
  return EmbedderContextData::GetOrCreate(host_contents)
      ->RegisterBrowserWindowInterfaceChanged(std::move(context_changed_cb));
}

base::CallbackListSubscription RegisterTabInterfaceChanged(
    content::WebContents* host_contents,
    base::RepeatingClosure context_changed_cb) {
  return EmbedderContextData::GetOrCreate(host_contents)
      ->RegisterTabInterfaceChanged(std::move(context_changed_cb));
}

}  // namespace webui
