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

#include "content/browser/find_request_manager.h"

#include <algorithm>
#include <utility>

#include "base/containers/queue.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/notimplemented.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "content/browser/find_in_page_client.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"

namespace content {

namespace {

// The following functions allow traversal over all RenderFrameHosts, including
// those across WebContentses.
//
// An inner WebContents may be embedded in an outer WebContents via an inner
// WebContentsTreeNode of the outer WebContents's WebContentsTreeNode.
std::vector<RenderFrameHostImpl*> GetChildren(RenderFrameHostImpl* rfh) {
  std::vector<RenderFrameHostImpl*> children;
  children.reserve(rfh->child_count());
  for (size_t i = 0; i != rfh->child_count(); ++i) {
    if (auto* contents = static_cast<WebContentsImpl*>(
            WebContentsImpl::FromOuterFrameTreeNode(rfh->child_at(i)))) {
      // If the child is used for an inner WebContents then add the inner
      // WebContents.
      children.push_back(
          contents->GetPrimaryFrameTree().root()->current_frame_host());
    } else {
      children.push_back(rfh->child_at(i)->current_frame_host());
    }
  }

  return children;
}

// Returns the first child RenderFrameHostImpl under |rfh|, if |rfh| has a
// child, or nullptr otherwise.
RenderFrameHostImpl* GetFirstChild(RenderFrameHostImpl* rfh) {
  auto children = GetChildren(rfh);
  if (!children.empty())
    return children.front();
  return nullptr;
}

// Returns the last child RenderFrameHostImpl under |rfh|, if |rfh| has a
// child, or nullptr otherwise.
RenderFrameHostImpl* GetLastChild(RenderFrameHostImpl* rfh) {
  auto children = GetChildren(rfh);
  if (!children.empty())
    return children.back();
  return nullptr;
}

// Returns the deepest last child frame under |rfh| in the frame tree.
RenderFrameHostImpl* GetDeepestLastChild(RenderFrameHostImpl* rfh) {
  while (RenderFrameHostImpl* last_child = GetLastChild(rfh))
    rfh = last_child;
  return rfh;
}

// Returns the parent RenderFrameHost of |rfh|, if |rfh| has a parent, or
// nullptr otherwise.
RenderFrameHostImpl* GetAncestor(RenderFrameHostImpl* rfh) {
  if (!rfh)
    return nullptr;

  return rfh->GetParentOrOuterDocumentOrEmbedder();
}

// Returns the previous sibling RenderFrameHostImpl of |rfh|, if one exists,
// or nullptr otherwise.
RenderFrameHostImpl* GetPreviousSibling(RenderFrameHostImpl* rfh) {
  if (rfh->PreviousSibling()) {
    return rfh->PreviousSibling()->current_frame_host();
  }

  // The previous sibling may be in another WebContents.
  if (RenderFrameHostImpl* parent = GetAncestor(rfh)) {
    auto children = GetChildren(parent);
    auto it = std::ranges::find(children, rfh);
    // It is odd that this rfh may not be a child of its parent, but this is
    // actually possible during teardown, hence the need for the check for
    // "it != children.end()".
    if (it != children.end() && it != children.begin())
      return *--it;
  }

  return nullptr;
}

// Returns the next sibling RenderFrameHostImpl of |rfh|, if one exists, or
// nullptr otherwise.
RenderFrameHostImpl* GetNextSibling(RenderFrameHostImpl* rfh) {
  if (rfh->NextSibling())
    return rfh->NextSibling()->current_frame_host();

  // The next sibling may be in another WebContents.
  if (RenderFrameHostImpl* parent = GetAncestor(rfh)) {
    auto children = GetChildren(parent);
    auto it = std::ranges::find(children, rfh);
    // It is odd that this RenderFrameHost may not be a child of its parent, but
    // this is actually possible during teardown, hence the need for the check
    // for "it != children.end()".
    if (it != children.end() && ++it != children.end())
      return *it;
  }

  return nullptr;
}

// Returns the RenderFrameHostImpl directly after |rfh| in the rfh tree in
// search order, or nullptr if one does not exist. If |wrap| is set, then
// wrapping between the first and last frames is permitted. Note that this
// traversal follows the same ordering as in
// blink::FrameTree::traverseNextWithWrap().
RenderFrameHostImpl* TraverseNext(RenderFrameHostImpl* rfh, bool wrap) {
  if (RenderFrameHostImpl* first_child = GetFirstChild(rfh))
    return first_child;

  RenderFrameHostImpl* sibling = GetNextSibling(rfh);
  while (!sibling) {
    RenderFrameHostImpl* parent = GetAncestor(rfh);
    if (!parent)
      return wrap ? rfh : nullptr;
    rfh = parent;
    sibling = GetNextSibling(rfh);
  }
  return sibling;
}

// Returns the RenderFrameHostImpl directly before |rfh| in the frame tree in
// search order, or nullptr if one does not exist. If |wrap| is set, then
// wrapping between the first and last frames is permitted. Note that this
// traversal follows the same ordering as in
// blink::FrameTree::traversePreviousWithWrap().
RenderFrameHostImpl* TraversePrevious(RenderFrameHostImpl* rfh, bool wrap) {
  if (RenderFrameHostImpl* previous_sibling = GetPreviousSibling(rfh))
    return GetDeepestLastChild(previous_sibling);
  if (RenderFrameHostImpl* parent = GetAncestor(rfh))
    return parent;
  return wrap ? GetDeepestLastChild(rfh) : nullptr;
}

// The same as either TraverseNext() or TraversePrevious(), depending on
// |forward|.
RenderFrameHostImpl* TraverseFrame(RenderFrameHostImpl* rfh,
                                   bool forward,
                                   bool wrap) {
  return forward ? TraverseNext(rfh, wrap) : TraversePrevious(rfh, wrap);
}

bool IsFindInPageDisabled(RenderFrameHost* rfh) {
  return rfh && GetContentClient()->browser()->IsFindInPageDisabledForOrigin(
                    rfh->GetLastCommittedOrigin());
}

bool IsUnattachedGuestView(RenderFrameHost* rfh) {
  if (base::FeatureList::IsEnabled(features::kGuestViewMPArch)) {
    NOTIMPLEMENTED();
    return false;
  } else {
    WebContentsImpl* web_contents =
        static_cast<WebContentsImpl*>(WebContents::FromRenderFrameHost(rfh));
    if (!web_contents->IsGuest()) {
      return false;
    }

    return !web_contents->GetOuterWebContents();
  }
}

// kMinKeystrokesWithoutDelay should be high enough that script in the page
// can't provide every possible search result at the same time.
constexpr int kMinKeystrokesWithoutDelay = 4;

// The delay for very short queries, before sending find requests. This should
// be higher than the duration in between two keystrokes. This is based on
// WebCore.FindInPage.DurationBetweenKeystrokes metrics, this is higher than
// 90% of them.
constexpr int kDelayMs = 400;

}  // namespace

// Observes searched WebContentses for RenderFrameHost state updates, including
// deletion and loads.
class FindRequestManager::FrameObserver : public WebContentsObserver {
 public:
  FrameObserver(WebContents* web_contents, FindRequestManager* manager)
      : WebContentsObserver(web_contents), manager_(manager) {}

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

  ~FrameObserver() override = default;

  void RenderFrameDeleted(RenderFrameHost* rfh) override {
    manager_->RemoveFrame(rfh);
  }

  void RenderFrameHostStateChanged(
      RenderFrameHost* rfh,
      RenderFrameHost::LifecycleState old_state,
      RenderFrameHost::LifecycleState new_state) override {
    if (manager_->current_session_id_ == kInvalidId ||
        IsFindInPageDisabled(rfh)) {
      return;
    }

    if (new_state == RenderFrameHost::LifecycleState::kActive) {
      // Add the RFH to the current find-in-page session when its status
      // changes to active since this is when the document becomes part of the
      // primary page (i.e prerendered pages getting activated, or pages in
      // BackForwardCache getting restored), so that we can get the results from
      // all frames in the primary page.
      manager_->AddFrame(rfh, false /* force */);
    } else if (old_state == RenderFrameHost::LifecycleState::kActive) {
      // Remove the RFH from the current find-in-page session if it stops being
      // part of the primary page.
      manager_->RemoveFrame(rfh);
    }
  }

  void DidFinishLoad(RenderFrameHost* rfh, const GURL& validated_url) override {
    if (manager_->current_session_id_ == kInvalidId)
      return;

    manager_->RemoveFrame(rfh);
    // Make sure RenderFrameDeleted will be called to clean up
    DCHECK(rfh->IsRenderFrameLive());

    if (IsFindInPageDisabled(rfh))
      return;

    manager_->AddFrame(rfh, /*force=*/true);
  }

 private:
  // The FindRequestManager that owns this FrameObserver.
  const raw_ptr<FindRequestManager> manager_;
};

bool FindRequestManager::RunDelayedFindTaskForTesting() {
  if (!delayed_find_task_.IsCancelled()) {
    delayed_find_task_.callback().Run();
    return true;
  }
  return false;
}

FindRequestManager::FindRequest::FindRequest() = default;

FindRequestManager::FindRequest::FindRequest(
    int id,
    const std::u16string& search_text,
    blink::mojom::FindOptionsPtr options)
    : id(id), search_text(search_text), options(std::move(options)) {}

FindRequestManager::FindRequest::FindRequest(const FindRequest& request)
    : id(request.id),
      search_text(request.search_text),
      options(request.options.Clone()) {}

FindRequestManager::FindRequest::~FindRequest() = default;

FindRequestManager::FindRequest& FindRequestManager::FindRequest::operator=(
    const FindRequest& request) {
  id = request.id;
  search_text = request.search_text;
  options = request.options.Clone();
  return *this;
}

#if BUILDFLAG(IS_ANDROID)
FindRequestManager::ActivateNearestFindResultState::
ActivateNearestFindResultState() = default;
FindRequestManager::ActivateNearestFindResultState::
    ActivateNearestFindResultState(float x, float y)
    : current_request_id(GetNextID()), point(x, y) {}
FindRequestManager::ActivateNearestFindResultState::
    ~ActivateNearestFindResultState() = default;

int FindRequestManager::ActivateNearestFindResultState::GetNextID() {
  static int next_id = 0;
  return next_id++;
}

FindRequestManager::FrameRects::FrameRects() = default;
FindRequestManager::FrameRects::FrameRects(const std::vector<gfx::RectF>& rects,
                                           int version)
    : rects(rects), version(version) {}
FindRequestManager::FrameRects::~FrameRects() = default;

FindRequestManager::FindMatchRectsState::FindMatchRectsState() = default;
FindRequestManager::FindMatchRectsState::~FindMatchRectsState() = default;
#endif

// static
const int FindRequestManager::kInvalidId = -1;

FindRequestManager::FindRequestManager(WebContentsImpl* web_contents)
    : contents_(web_contents) {}

FindRequestManager::~FindRequestManager() = default;

void FindRequestManager::Find(int request_id,
                              const std::u16string& search_text,
                              blink::mojom::FindOptionsPtr options,
                              bool skip_delay) {
  // Every find request must have a unique ID, and these IDs must strictly
  // increase so that newer requests always have greater IDs than older
  // requests.
  DCHECK_GT(request_id, current_request_.id);
  DCHECK_GT(request_id, current_session_id_);

  if (skip_delay) {
    delayed_find_task_.Cancel();
    EmitFindRequest(request_id, search_text, std::move(options));
    return;
  }

  if (!options->new_session) {
    // If the user presses enter while we are waiting for a delayed find, then
    // run the find now to improve responsiveness.
    if (!delayed_find_task_.IsCancelled()) {
      delayed_find_task_.callback().Run();
    } else {
      EmitFindRequest(request_id, search_text, std::move(options));
    }
    return;
  }

  if (search_text.length() < kMinKeystrokesWithoutDelay) {
    delayed_find_task_.Reset(base::BindOnce(
        &FindRequestManager::EmitFindRequest, weak_factory_.GetWeakPtr(),
        request_id, search_text, std::move(options)));
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, delayed_find_task_.callback(), base::Milliseconds(kDelayMs));
    return;
  }

  // If we aren't going to delay, then clear any previous attempts to delay.
  delayed_find_task_.Cancel();

  EmitFindRequest(request_id, search_text, std::move(options));
}

void FindRequestManager::EmitFindRequest(int request_id,
                                         const std::u16string& search_text,
                                         blink::mojom::FindOptionsPtr options) {
  // If this is a new find session, clear any queued requests from last session.
  if (options->new_session)
    find_request_queue_ = base::queue<FindRequest>();

  find_request_queue_.emplace(request_id, search_text, std::move(options));
  if (find_request_queue_.size() == 1)
    FindInternal(find_request_queue_.front());
  if (request_id == current_session_id_)
    find_request_queue_.pop();
}

void FindRequestManager::ForEachAddedFindInPageRenderFrameHost(
    base::FunctionRef<void(RenderFrameHostImpl*)> func_ref) {
  contents_->GetPrimaryMainFrame()->ForEachRenderFrameHostImpl(
      [this, func_ref](RenderFrameHostImpl* rfh) {
        if (!CheckFrame(rfh))
          return;
        DCHECK(rfh->IsRenderFrameLive());
        DCHECK(rfh->IsActive());
        func_ref(rfh);
      });
}

void FindRequestManager::StopFinding(StopFindAction action) {
  // Cancel any delayed find-in-page requests
  delayed_find_task_.Cancel();

  ForEachAddedFindInPageRenderFrameHost([action](RenderFrameHostImpl* rfh) {
    rfh->GetFindInPage()->StopFinding(
        // TODO(dcheng): Use typemapping or use the Mojo enum directly.
        static_cast<blink::mojom::StopFindAction>(action));
  });

  current_session_id_ = kInvalidId;
#if BUILDFLAG(IS_ANDROID)
  // It is important that these pending replies are cleared whenever a find
  // session ends, so that subsequent replies for the old session are ignored.
  activate_.pending_replies.clear();
  match_rects_.pending_replies.clear();
#endif
}

bool FindRequestManager::ShouldIgnoreReply(RenderFrameHostImpl* rfh,
                                           int request_id) {
  // Ignore stale replies from abandoned find sessions or dead frames.
  return current_session_id_ == kInvalidId ||
         request_id < current_session_id_ || !CheckFrame(rfh);
}

void FindRequestManager::HandleFinalUpdateForFrame(RenderFrameHostImpl* rfh,
                                                   int request_id) {
  // This is the final update for this frame for the current find operation.
  pending_initial_replies_.erase(rfh);
  if (request_id == current_session_id_ && !pending_initial_replies_.empty()) {
    NotifyFindReply(request_id, false /* final_update */);
    return;
  }

  // This is the final update for all frames for the current find operation.
  if (request_id == current_request_.id && request_id != current_session_id_) {
    DCHECK(!current_request_.options->new_session);
    DCHECK_EQ(pending_find_next_reply_, rfh);
    pending_find_next_reply_ = nullptr;
  }

  FinalUpdateReceived(request_id, rfh);
}

void FindRequestManager::UpdatedFrameNumberOfMatches(RenderFrameHostImpl* rfh,
                                                     unsigned int old_count,
                                                     unsigned int new_count) {
  if (old_count == new_count)
    return;

  // Change the number of matches for this frame in the global count.
  number_of_matches_ -= old_count;
  number_of_matches_ += new_count;

  // All matches may have been removed since the last find reply.
  if (rfh == active_frame_ && !new_count)
    relative_active_match_ordinal_ = 0;

  // The active match ordinal may need updating since the number of matches
  // before the active match may have changed.
  UpdateActiveMatchOrdinal();
}

void FindRequestManager::SetActiveMatchRect(
    const gfx::Rect& active_match_rect) {
  selection_rect_ = active_match_rect;
}

void FindRequestManager::SetActiveMatchOrdinal(RenderFrameHostImpl* rfh,
                                               int request_id,
                                               int active_match_ordinal) {
  if (active_match_ordinal > 0) {
    // Call SetFocusedFrame on the WebContents associated with |rfh| (which
    // might not be the same as |contents_|, as a WebContents might have
    // inner WebContents). We need to focus on the frame where the active
    // match is in, which should be in the |rfh|'s associated WebContents.
    WebContentsImpl* web_contents =
        static_cast<WebContentsImpl*>(WebContents::FromRenderFrameHost(rfh));
    // Do not focus inactive RenderFrameHost.
    if (!rfh->IsActive())
      return;
    web_contents->SetFocusedFrame(rfh->frame_tree_node(),
                                  rfh->GetSiteInstance()->group());
  }
  if (rfh == active_frame_) {
    active_match_ordinal_ +=
        active_match_ordinal - relative_active_match_ordinal_;
    relative_active_match_ordinal_ = active_match_ordinal;
  } else {
    if (active_frame_) {
      // The new active match is in a different frame than the previous, so
      // the previous active frame needs to be informed (to clear its active
      // match highlighting).
      ClearActiveFindMatch();
    }
    active_frame_ = rfh;
    relative_active_match_ordinal_ = active_match_ordinal;
    UpdateActiveMatchOrdinal();
  }
  if (pending_active_match_ordinal_ && request_id == current_request_.id)
    pending_active_match_ordinal_ = false;
  AdvanceQueue(request_id);
}

void FindRequestManager::RemoveFrame(RenderFrameHost* rfh) {
  // If matches are counted for the frame that is being removed, decrement the
  // match total before erasing that entry.
  auto it = find_in_page_clients_.find(rfh);
  if (it != find_in_page_clients_.end()) {
    number_of_matches_ -= it->second->number_of_matches();
    find_in_page_clients_.erase(it);
  } else {
    // If there's no FindInPageClient for `rfh`, the state related to it must
    // have been cleared already.
    return;
  }

  // If this is a primary main frame, then clear the search queue as well, since
  // we shouldn't be dispatching any more requests. Note that if any other frame
  // is removed, we can target any queued requests to the focused frame or
  // primary main frame. However, if the primary main frame is removed we will
  // not have a valid RenderFrameHost to target for the request queue.
  if (rfh->IsInPrimaryMainFrame())
    find_request_queue_ = base::queue<FindRequest>();

  // Update the active match ordinal, since it may have changed.
  if (active_frame_ == rfh) {
    active_frame_ = nullptr;
    relative_active_match_ordinal_ = 0;
    selection_rect_ = gfx::Rect();
  }
  UpdateActiveMatchOrdinal();

#if BUILDFLAG(IS_ANDROID)
  // The removed frame may contain the nearest find result known so far. Note
  // that once all queried frames have responded, if this result was the overall
  // nearest, then no activation will occur.
  if (rfh == activate_.nearest_frame)
    activate_.nearest_frame = nullptr;

  // Match rects in the removed frame are no longer relevant.
  if (match_rects_.frame_rects.erase(rfh) != 0)
    ++match_rects_.known_version;

  // A reply should not be expected from the removed frame.
  RemoveNearestFindResultPendingReply(rfh);
  RemoveFindMatchRectsPendingReply(rfh);
#endif

  if (current_session_id_ == kInvalidId) {
    // Just remove `rfh` from things that might point to it, but don't trigger
    // any extra processing as there is no current find session ongoing.
    pending_initial_replies_.erase(rfh);
    if (pending_find_next_reply_ == rfh) {
      pending_find_next_reply_ = nullptr;
    }
    return;
  }

  // Make sure to always clear the highlighted selection. It is useful in case
  // the user goes back to the same page using the BackForwardCache.
  static_cast<RenderFrameHostImpl*>(rfh)->GetFindInPage()->StopFinding(
      blink::mojom::StopFindAction::kStopFindActionClearSelection);

  // If no pending find replies are expected for the removed frame, then just
  // report the updated results.
  if (!pending_initial_replies_.contains(rfh) &&
      pending_find_next_reply_ != rfh) {
    bool final_update =
        pending_initial_replies_.empty() && !pending_find_next_reply_;
    NotifyFindReply(current_session_id_, final_update);
    return;
  }

  if (pending_initial_replies_.erase(rfh) != 0) {
    // A reply should not be expected from the removed frame.
    if (pending_initial_replies_.empty()) {
      FinalUpdateReceived(current_session_id_, rfh);
    }
  }

  if (pending_find_next_reply_ == rfh) {
    // A reply should not be expected from the removed frame.
    pending_find_next_reply_ = nullptr;
    FinalUpdateReceived(current_request_.id, rfh);
  }
}

void FindRequestManager::ClearActiveFindMatch() {
  active_frame_->GetFindInPage()->ClearActiveFindMatch();
}

#if BUILDFLAG(IS_ANDROID)
void FindRequestManager::ActivateNearestFindResult(float x, float y) {
  if (current_session_id_ == kInvalidId)
    return;

  activate_ = ActivateNearestFindResultState(x, y);

  // Request from each frame the distance to the nearest find result (in that
  // frame) from the point (x, y), defined in find-in-page coordinates.
  ForEachAddedFindInPageRenderFrameHost([this](RenderFrameHostImpl* rfh) {
    activate_.pending_replies.insert(rfh);
    // Lifetime of FindRequestManager > RenderFrameHost > Mojo
    // connection, so it's safe to bind |this| and |rfh|.
    rfh->GetFindInPage()->GetNearestFindResult(
        activate_.point,
        base::BindOnce(&FindRequestManager::OnGetNearestFindResultReply,
                       base::Unretained(this), rfh,
                       activate_.current_request_id));
  });
}

void FindRequestManager::OnGetNearestFindResultReply(RenderFrameHostImpl* rfh,
                                                     int request_id,
                                                     float distance) {
  if (request_id != activate_.current_request_id ||
      !activate_.pending_replies.contains(rfh)) {
    return;
  }

  // Check if this frame has a nearer find result than the current nearest.
  if (distance < activate_.nearest_distance) {
    activate_.nearest_frame = rfh;
    activate_.nearest_distance = distance;
  }

  RemoveNearestFindResultPendingReply(rfh);
}

void FindRequestManager::RequestFindMatchRects(int current_version) {
  match_rects_.pending_replies.clear();
  match_rects_.request_version = current_version;
  match_rects_.active_rect = gfx::RectF();

  // Request the latest find match rects from each frame.
  ForEachAddedFindInPageRenderFrameHost([this](RenderFrameHostImpl* rfh) {
    match_rects_.pending_replies.insert(rfh);
    auto it = match_rects_.frame_rects.find(rfh);
    int version = (it != match_rects_.frame_rects.end()) ? it->second.version
                                                         : kInvalidId;
    // Lifetime of FindRequestManager > RenderFrameHost > Mojo
    // connection, so it's safe to bind |this| and |rfh|.
    rfh->GetFindInPage()->FindMatchRects(
        version, base::BindOnce(&FindRequestManager::OnFindMatchRectsReply,
                                base::Unretained(this), rfh));
  });
}

void FindRequestManager::OnFindMatchRectsReply(
    RenderFrameHost* rfh,
    int version,
    const std::vector<gfx::RectF>& rects,
    const gfx::RectF& active_rect) {
  auto it = match_rects_.frame_rects.find(rfh);
  if (it == match_rects_.frame_rects.end() || it->second.version != version) {
    // New version of rects has been received, so update the data.
    match_rects_.frame_rects[rfh] = FrameRects(rects, version);
    ++match_rects_.known_version;
  }
  if (!active_rect.IsEmpty())
    match_rects_.active_rect = active_rect;
  RemoveFindMatchRectsPendingReply(rfh);
}
#endif

void FindRequestManager::Reset(const FindRequest& initial_request) {
  current_session_id_ = initial_request.id;
  current_request_ = initial_request;
  pending_initial_replies_.clear();
  pending_find_next_reply_ = nullptr;
  pending_active_match_ordinal_ = true;
  find_in_page_clients_.clear();
  number_of_matches_ = 0;
  active_frame_ = nullptr;
  relative_active_match_ordinal_ = 0;
  active_match_ordinal_ = 0;
  selection_rect_ = gfx::Rect();
  last_reported_id_ = kInvalidId;
  frame_observers_.clear();
#if BUILDFLAG(IS_ANDROID)
  activate_ = ActivateNearestFindResultState();
  match_rects_.pending_replies.clear();
#endif
}

void FindRequestManager::FindInternal(const FindRequest& request) {
  DCHECK_GT(request.id, current_request_.id);
  DCHECK_GT(request.id, current_session_id_);

  if (!request.options->new_session) {
    // This is a find next operation.

    // This implies that there is an ongoing find session with the same search
    // text.
    DCHECK_GE(current_session_id_, 0);
    DCHECK_EQ(request.search_text, current_request_.search_text);

    // The find next request will be directed at the focused frame if there is
    // one, or the first frame with matches otherwise.
    auto* focused_web_contents = contents_->GetFocusedWebContents();
    RenderFrameHost* target_rfh = focused_web_contents
                                      ? focused_web_contents->GetFocusedFrame()
                                      : nullptr;
    if (!target_rfh || !CheckFrame(target_rfh))
      target_rfh = GetInitialFrame(request.options->forward);

    // Verify that we have a valid frame before sending the request.
    if (!target_rfh || !CheckFrame(target_rfh)) {
      // No valid frame to send the find request to. Advance the queue.
      AdvanceQueue(request.id);
      return;
    }

    SendFindRequest(request, target_rfh);
    current_request_ = request;
    pending_active_match_ordinal_ = true;
    return;
  }

  // This is an initial find operation.
  Reset(request);

  // Add and observe eligible RFHs in the WebContents. And, use
  // ForEachRenderFrameHostImpl instead of ForEachAddedFindInPageRenderFrameHost
  // because that calls CheckFrame() which will only be true if we've called
  // AddFrame() for the frame.
  contents_->GetPrimaryMainFrame()->ForEachRenderFrameHostImpl(
      [this](RenderFrameHostImpl* rfh) {
        auto* wc = WebContents::FromRenderFrameHost(rfh);
        // Make sure each WebContents is only added once.
        if (rfh->IsInPrimaryMainFrame()) {
          frame_observers_.push_back(std::make_unique<FrameObserver>(wc, this));
        }
        if (IsFindInPageDisabled(rfh))
          return;
        AddFrame(rfh, false /* force */);
      });
}

void FindRequestManager::AdvanceQueue(int request_id) {
  if (find_request_queue_.empty() ||
      request_id != find_request_queue_.front().id) {
    return;
  }

  find_request_queue_.pop();
  if (!find_request_queue_.empty())
    FindInternal(find_request_queue_.front());
}

void FindRequestManager::SendFindRequest(const FindRequest& request,
                                         RenderFrameHost* rfh) {
  DCHECK(CheckFrame(rfh));
  DCHECK(rfh->IsRenderFrameLive());
  DCHECK(rfh->IsActive());

  if (request.options->new_session)
    pending_initial_replies_.insert(rfh);
  else
    pending_find_next_reply_ = rfh;

  static_cast<RenderFrameHostImpl*>(rfh)->GetFindInPage()->Find(
      request.id, base::UTF16ToUTF8(request.search_text),
      request.options.Clone());
}

void FindRequestManager::NotifyFindReply(int request_id, bool final_update) {
  if (request_id == kInvalidId) {
    NOTREACHED();
  }

  // Ensure that replies are not reported with IDs lower than the ID of the
  // latest request we have results from.
  if (request_id < last_reported_id_)
    request_id = last_reported_id_;
  else
    last_reported_id_ = request_id;

  contents_->NotifyFindReply(request_id, number_of_matches_, selection_rect_,
                             active_match_ordinal_, final_update);
}

RenderFrameHost* FindRequestManager::GetInitialFrame(bool forward) const {
  RenderFrameHost* rfh =
      contents_->GetPrimaryFrameTree().root()->current_frame_host();
  if (!forward)
    rfh = GetDeepestLastChild(static_cast<RenderFrameHostImpl*>(rfh));

  return rfh;
}

RenderFrameHost* FindRequestManager::Traverse(RenderFrameHost* from_rfh,
                                              bool forward,
                                              bool matches_only,
                                              bool wrap) const {
  DCHECK(from_rfh);
  // If |from_rfh| is being detached, it might already be removed from
  // its parent's list of children, meaning we can't traverse it correctly.
  // We also don't traverse when |from_rfh| is in back-forward cache or is being
  // prerendered, as we don't allow any updates in these states.
  auto* from_rfh_impl = static_cast<RenderFrameHostImpl*>(from_rfh);
  if (from_rfh_impl->IsPendingDeletion() ||
      from_rfh_impl->IsInBackForwardCache() ||
      from_rfh_impl->lifecycle_state() ==
          RenderFrameHostImpl::LifecycleStateImpl::kPrerendering) {
    return nullptr;
  }

  RenderFrameHostImpl* rfh = from_rfh_impl;
  RenderFrameHostImpl* last_frame = rfh;
  while ((rfh = TraverseFrame(rfh, forward, wrap)) != nullptr) {
    if (!CheckFrame(rfh)) {
      // If we're in the same frame as before, we might got into an infinite
      // loop.
      if (last_frame == rfh)
        break;
      last_frame = rfh;
      continue;
    }
    RenderFrameHost* current_rfh = rfh;
    if (!matches_only ||
        find_in_page_clients_.find(current_rfh)->second->number_of_matches() ||
        pending_initial_replies_.contains(current_rfh)) {
      // Note that if there is still a pending reply expected for this frame,
      // then it may have unaccounted matches and will not be skipped via
      // |matches_only|.
      return rfh;
    }
    if (wrap && rfh == from_rfh)
      return nullptr;
  }

  return nullptr;
}

void FindRequestManager::AddFrame(RenderFrameHost* rfh, bool force) {
  if (!rfh || !rfh->IsRenderFrameLive() || !rfh->IsActive() ||
      IsUnattachedGuestView(rfh)) {
    return;
  }

  // A frame that is already being searched should not normally be added again.
  DCHECK(force || !CheckFrame(rfh));

  DCHECK(!IsFindInPageDisabled(rfh));

  find_in_page_clients_[rfh] =
      CreateFindInPageClient(static_cast<RenderFrameHostImpl*>(rfh));

  FindRequest request = current_request_;
  request.id = current_session_id_;
  request.options->new_session = true;
  request.options->force = force;
  SendFindRequest(request, rfh);
}

bool FindRequestManager::CheckFrame(RenderFrameHost* rfh) const {
  // TODO(crbug.com/40196212): Convert IsFindInPageDisabled to a DCHECK when we
  // replace DidFinishLoad with DidFinishNavigation in FrameObserver.
  if (!rfh || !find_in_page_clients_.contains(rfh) ||
      IsFindInPageDisabled(rfh)) {
    return false;
  }

  DCHECK(rfh->IsActive());
  return true;
}

void FindRequestManager::UpdateActiveMatchOrdinal() {
  active_match_ordinal_ = 0;

  if (!active_frame_ || !relative_active_match_ordinal_) {
    DCHECK(!active_frame_ && !relative_active_match_ordinal_);
    return;
  }

  // Traverse the frame tree backwards (in search order) and count all of the
  // matches in frames before the frame with the active match, in order to
  // determine the overall active match ordinal.
  RenderFrameHost* frame = active_frame_;
  while ((frame = Traverse(frame,
                           false /* forward */,
                           true /* matches_only */,
                           false /* wrap */)) != nullptr) {
    active_match_ordinal_ += find_in_page_clients_[frame]->number_of_matches();
  }
  active_match_ordinal_ += relative_active_match_ordinal_;
}

void FindRequestManager::FinalUpdateReceived(int request_id,
                                             RenderFrameHost* rfh) {
  if (!number_of_matches_ ||
      !current_request_.options->find_match ||
      (active_match_ordinal_ && !pending_active_match_ordinal_) ||
      pending_find_next_reply_) {
    // All the find results for |request_id| are in and ready to report. Note
    // that |final_update| will be set to false if there are still pending
    // replies expected from the initial find request.
    NotifyFindReply(request_id,
                    pending_initial_replies_.empty() /* final_update */);
    AdvanceQueue(request_id);
    return;
  }

  // There are matches, but no active match was returned, so another find next
  // request must be sent.

  RenderFrameHost* target_rfh;
  if (request_id == current_request_.id &&
      !current_request_.options->new_session) {
    // If this was a find next operation, then the active match will be in the
    // next frame with matches after this one.
    target_rfh = Traverse(rfh, current_request_.options->forward,
                          true /* matches_only */, true /* wrap */);
  } else if (contents_->GetFocusedWebContents() &&
             (target_rfh =
                  contents_->GetFocusedWebContents()->GetFocusedFrame()) !=
                 nullptr) {
    // Otherwise, if there is a focused frame, then the active match will be in
    // the next frame with matches after that one.
    target_rfh = Traverse(target_rfh, current_request_.options->forward,
                          true /* matches_only */, true /* wrap */);
  } else {
    // Otherwise, the first frame with matches will have the active match.
    target_rfh = GetInitialFrame(current_request_.options->forward);
    if (!CheckFrame(target_rfh) ||
        !find_in_page_clients_[target_rfh]->number_of_matches()) {
      target_rfh = Traverse(target_rfh, current_request_.options->forward,
                            true /* matches_only */, false /* wrap */);
    }
  }
  if (!target_rfh) {
    // Sometimes when the WebContents is deleted/navigated, we got into this
    // situation. We don't care about this WebContents anyways so it's ok to
    // just not ask for the active match and return immediately.
    // TODO(rakina): Understand what leads to this situation.
    // See: https://crbug.com/884679.
    return;
  }

  // Forward the find reply without |final_update| set because the active match
  // has not yet been found.
  NotifyFindReply(request_id, false /* final_update */);

  current_request_.options->new_session = false;
  SendFindRequest(current_request_, target_rfh);
}

std::unique_ptr<FindInPageClient> FindRequestManager::CreateFindInPageClient(
    RenderFrameHostImpl* rfh) {
  if (create_find_in_page_client_for_testing_)
    return create_find_in_page_client_for_testing_(this, rfh);
  return std::make_unique<FindInPageClient>(this, rfh);
}

#if BUILDFLAG(IS_ANDROID)
void FindRequestManager::RemoveNearestFindResultPendingReply(
    RenderFrameHost* rfh) {
  auto it = activate_.pending_replies.find(rfh);
  if (it == activate_.pending_replies.end())
    return;

  activate_.pending_replies.erase(it);
  if (activate_.pending_replies.empty() &&
      CheckFrame(activate_.nearest_frame)) {
    const auto client_it = find_in_page_clients_.find(activate_.nearest_frame);
    if (client_it != find_in_page_clients_.end())
      client_it->second->ActivateNearestFindResult(current_session_id_,
                                                   activate_.point);
  }
}

void FindRequestManager::RemoveFindMatchRectsPendingReply(
    RenderFrameHost* rfh) {
  auto it = match_rects_.pending_replies.find(rfh);
  if (it == match_rects_.pending_replies.end())
    return;

  match_rects_.pending_replies.erase(it);
  if (!match_rects_.pending_replies.empty())
    return;

  // All replies are in.
  std::vector<gfx::RectF> aggregate_rects;
  if (match_rects_.request_version != match_rects_.known_version) {
    // Request version is stale, so aggregate and report the newer find
    // match rects. The rects should be aggregated in search order.
    for (RenderFrameHost* frame = GetInitialFrame(true /* forward */); frame;
         frame = Traverse(frame, true /* forward */, true /* matches_only */,
                          false /* wrap */)) {
      auto frame_it = match_rects_.frame_rects.find(frame);
      if (frame_it == match_rects_.frame_rects.end())
        continue;

      std::vector<gfx::RectF>& frame_rects = frame_it->second.rects;
      aggregate_rects.insert(aggregate_rects.end(), frame_rects.begin(),
                             frame_rects.end());
    }
  }
  contents_->NotifyFindMatchRectsReply(
      match_rects_.known_version, aggregate_rects, match_rects_.active_rect);
}
#endif  // BUILDFLAG(IS_ANDROID)

}  // namespace content
