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

#ifndef CONTENT_BROWSER_FIND_REQUEST_MANAGER_H_
#define CONTENT_BROWSER_FIND_REQUEST_MANAGER_H_

#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include "base/cancelable_callback.h"
#include "base/containers/queue.h"
#include "base/functional/function_ref.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/stop_find_action.h"
#include "third_party/blink/public/mojom/frame/find_in_page.mojom.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"

namespace content {

class FindInPageClient;
class RenderFrameHost;
class RenderFrameHostImpl;
class WebContentsImpl;

// FindRequestManager manages all of the find-in-page requests/replies
// initiated/received through a WebContents. It coordinates searching across
// multiple (potentially out-of-process) frames, handles the aggregation of find
// results from each frame, and facilitates active match traversal. It is
// instantiated once per top-level WebContents, and is owned by that
// WebContents.
class FindRequestManager {
 public:
  explicit FindRequestManager(WebContentsImpl* web_contents);

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

  ~FindRequestManager();

  // Initiates a find operation for |search_text| with the options specified in
  // |options|. |request_id| uniquely identifies the find request.
  void Find(int request_id,
            const std::u16string& search_text,
            blink::mojom::FindOptionsPtr options,
            bool skip_delay = false);

  // Stops the active find session and clears the general highlighting of the
  // matches. |action| determines whether the last active match (if any) will be
  // activated, cleared, or remain highlighted.
  void StopFinding(StopFindAction action);

  // Handles the final update from |rfh| for the find request with id
  // |request_id|.
  void HandleFinalUpdateForFrame(RenderFrameHostImpl* rfh, int request_id);

  // The number of matches on |rfh| has changed from |old_count| to |new_count|.
  // This method updates the total number of matches and also updates
  // |active_match_ordinal_| accordingly.
  void UpdatedFrameNumberOfMatches(RenderFrameHostImpl* rfh,
                                   unsigned int old_count,
                                   unsigned int new_count);

  bool ShouldIgnoreReply(RenderFrameHostImpl* rfh, int request_id);

  void SetActiveMatchRect(const gfx::Rect& active_match_rect);

  void SetActiveMatchOrdinal(RenderFrameHostImpl* rfh,
                             int request_id,
                             int active_match_ordinal);

  // Sends the find results (as they currently are) to the WebContents.
  // |final_update| is true if we have received all of the updates from
  // every frame for this request.
  void NotifyFindReply(int request_id, bool final_update);

  // Removes a frame from the set of frames being searched. This should be
  // called whenever a frame is discovered to no longer exist.
  void RemoveFrame(RenderFrameHost* rfh);

  // Tells active frame to clear the active match highlighting.
  void ClearActiveFindMatch();

  // Runs the delayed find task if present. Returns true if there was a task
  // which got run. Returns false if there was no delayed task.
  bool CONTENT_EXPORT RunDelayedFindTaskForTesting();

#if BUILDFLAG(IS_ANDROID)
  // Selects and zooms to the find result nearest to the point (x, y), defined
  // in find-in-page coordinates.
  void ActivateNearestFindResult(float x, float y);

  // Called when a reply is received from a frame in response to the
  // GetNearestFindResult mojo call.
  void OnGetNearestFindResultReply(RenderFrameHostImpl* rfh,
                                   int request_id,
                                   float distance);

  // Requests the rects of the current find matches from the renderer process.
  void RequestFindMatchRects(int current_version);

  // Called when a reply is received from a frame in response to a request for
  // find match rects.
  void OnFindMatchRectsReply(RenderFrameHost* rfh,
                             int version,
                             const std::vector<gfx::RectF>& rects,
                             const gfx::RectF& active_rect);
#endif

  const std::unordered_set<raw_ptr<RenderFrameHost, CtnExperimental>>
  render_frame_hosts_pending_initial_reply_for_testing() const {
    return pending_initial_replies_;
  }

  gfx::Rect GetSelectionRectForTesting() { return selection_rect_; }

  using CreateFindInPageClientFunction = std::unique_ptr<FindInPageClient> (*)(
      FindRequestManager* find_request_manager,
      RenderFrameHostImpl* rfh);
  void SetCreateFindInPageClientFunctionForTesting(
      CreateFindInPageClientFunction create_func) {
    create_find_in_page_client_for_testing_ = create_func;
  }

 private:
  friend class FindRequestManagerFencedFrameTest;

  // An invalid ID. This value is invalid for any render process ID, render
  // frame ID, find request ID, or find match rects version number.
  static const int kInvalidId;

  class FrameObserver;

  // The request data for a single find request.
  struct FindRequest {
    // The find request ID that uniquely identifies this find request.
    int id = kInvalidId;

    // The text that is being searched for in this find request.
    std::u16string search_text;

    // The set of find options in effect for this find request.
    blink::mojom::FindOptionsPtr options;

    FindRequest();
    FindRequest(int id,
                const std::u16string& search_text,
                blink::mojom::FindOptionsPtr options);
    FindRequest(const FindRequest& request);
    ~FindRequest();

    FindRequest& operator=(const FindRequest& request);
  };

  // Resets all of the per-session state for a new find-in-page session.
  void Reset(const FindRequest& initial_request);

  // Called internally as find requests come up in the queue.
  void FindInternal(const FindRequest& request);

  // Called when an informative response (a response with enough information to
  // be able to route subsequent find requests) comes in for the find request
  // with ID |request_id|. Advances the |find_request_queue_| if appropriate.
  void AdvanceQueue(int request_id);

  // Sends find request |request| through mojo to the RenderFrame associated
  // with |rfh|.
  void SendFindRequest(const FindRequest& request, RenderFrameHost* rfh);

  // Returns the initial frame in search order. This will be either the first
  // frame, if searching forward, or the last frame, if searching backward.
  RenderFrameHost* GetInitialFrame(bool forward) const;

  // Traverses the frame tree to find and return the next RenderFrameHost after
  // |from_rfh| in search order. |forward| indicates whether the frame tree
  // should be traversed forward (if true) or backward (if false). If
  // |matches_only| is set, then the frame tree will be traversed until the
  // first frame is found for which matches have been found. If |wrap| is set,
  // then the traversal can wrap around past the last frame to the first one (or
  // vice-versa, if |forward| == false). If no frame can be found under these
  // conditions, nullptr is returned.
  RenderFrameHost* Traverse(RenderFrameHost* from_rfh,
                            bool forward,
                            bool matches_only,
                            bool wrap) const;

  // Adds a frame to the set of frames that are being searched. The new frame
  // will automatically be searched when added, using the same options (stored
  // in |current_request_.options|). |force| should be set to true when a
  // dynamic content change is suspected, which will treat the frame as a newly
  // added frame even if it has already been searched. This will force a
  // re-search of the frame.
  void AddFrame(RenderFrameHost* rfh, bool force);

  // Returns whether |rfh| is in the set of frames being searched in the current
  // find session.
  CONTENT_EXPORT bool CheckFrame(RenderFrameHost* rfh) const;

  // Computes and updates |active_match_ordinal_| based on |active_frame_| and
  // |relative_active_match_ordinal_|.
  void UpdateActiveMatchOrdinal();

  // Called when all pending find replies have been received for the find
  // request with ID |request_id|. The final update was received from |rfh|.
  //
  // Note that this is the final update for this particular find request, but
  // not necessarily for all issued requests. If there are still pending replies
  // expected for a previous find request, then the outgoing find reply issued
  // from this function will not be marked final.
  void FinalUpdateReceived(int request_id, RenderFrameHost* rfh);

  std::unique_ptr<FindInPageClient> CreateFindInPageClient(
      RenderFrameHostImpl* rfh);

  // Traverses all RenderFrameHosts added for find-in-page and invokes the
  // callback if the each RenderFrameHost is alive and active.
  void ForEachAddedFindInPageRenderFrameHost(
      base::FunctionRef<void(RenderFrameHostImpl*)> func_ref);

  void EmitFindRequest(int request_id,
                       const std::u16string& search_text,
                       blink::mojom::FindOptionsPtr options);

#if BUILDFLAG(IS_ANDROID)
  // Called when a nearest find result reply is no longer pending for a frame.
  void RemoveNearestFindResultPendingReply(RenderFrameHost* rfh);

  // Called when a find match rects reply is no longer pending for a frame.
  void RemoveFindMatchRectsPendingReply(RenderFrameHost* rfh);

  // State related to ActivateNearestFindResult requests.
  struct ActivateNearestFindResultState {
    // An ID to uniquely identify the current nearest find result request and
    // its replies.
    int current_request_id = kInvalidId;

    // The value of the requested point, in find-in-page coordinates.
    gfx::PointF point = gfx::PointF(0.0f, 0.0f);

    float nearest_distance = FLT_MAX;

    // The frame containing the nearest result found so far.
    raw_ptr<RenderFrameHostImpl> nearest_frame = nullptr;

    // Nearest find result replies are still pending for these frames.
    std::unordered_set<raw_ptr<RenderFrameHost, CtnExperimental>>
        pending_replies;

    ActivateNearestFindResultState();
    ActivateNearestFindResultState(float x, float y);
    ~ActivateNearestFindResultState();

    static int GetNextID() {
      static int next_id = 0;
      return next_id++;
    }
  } activate_;

  // Data for find match rects in a single frame.
  struct FrameRects {
    // The rects contained in a single frame.
    std::vector<gfx::RectF> rects;

    // The version number for these rects, as reported by their containing
    // frame. This version is incremented independently in each frame.
    int version = kInvalidId;

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

  // State related to FindMatchRects requests.
  struct FindMatchRectsState {
    // The latest find match rects version known by the requester. This will be
    // compared to |known_version_| after polling frames for updates to their
    // match rects, in order to determine if the requester already has the
    // latest version of rects or not.
    int request_version = kInvalidId;

    // The current overall find match rects version known by
    // FindRequestManager. This version should be incremented whenever
    // |frame_rects| is updated.
    int known_version = 0;

    // A map from each frame to its find match rects.
    std::unordered_map<RenderFrameHost*, FrameRects> frame_rects;

    // The active find match rect.
    gfx::RectF active_rect;

    // Find match rects replies are still pending for these frames.
    std::unordered_set<raw_ptr<RenderFrameHost, CtnExperimental>>
        pending_replies;

    FindMatchRectsState();
    ~FindMatchRectsState();
  } match_rects_;
#endif

  // The WebContents that owns this FindRequestManager. This also defines the
  // scope of all find sessions. Only frames in |contents_| and any inner
  // WebContentses within it will be searched.
  const raw_ptr<WebContentsImpl> contents_;

  // The request ID of the initial find request in the current find-in-page
  // session, which uniquely identifies this session. Request IDs are included
  // in all find-related IPCs, which allows reply IPCs containing results from
  // previous sessions (with |request_id| < |current_session_id_|) to be easily
  // identified and ignored.
  int current_session_id_ = kInvalidId;

  // The current find request.
  FindRequest current_request_;

  // The set of frames that are still expected to reply to a pending initial
  // find request. Frames are removed from |pending_initial_replies_| when their
  // reply to the initial find request is received with |final_update| set to
  // true.
  std::unordered_set<raw_ptr<RenderFrameHost, CtnExperimental>>
      pending_initial_replies_;

  // The frame (if any) that is still expected to reply to the last pending
  // "find next" request.
  raw_ptr<RenderFrameHost> pending_find_next_reply_ = nullptr;

  // Indicates whether an update to the active match ordinal is expected. Once
  // set, |pending_active_match_ordinal_| will not reset until an update to the
  // active match ordinal is received in response to the find request with ID
  // |current_request_.id| (the latest request).
  bool pending_active_match_ordinal_ = false;

  // The FindInPageClient associated with each frame. There will necessarily be
  // entries in this map for every frame that is being (or has been) searched in
  // the current find session, and no other frames.
  std::unordered_map<RenderFrameHost*, std::unique_ptr<FindInPageClient>>
      find_in_page_clients_;

  // The total number of matches found in the current find-in-page session. This
  // should always be equal to the sum of all the entries in
  // |matches_per_frame_|.
  int number_of_matches_ = 0;

  // The frame containing the active match, if one exists, or nullptr otherwise.
  raw_ptr<RenderFrameHostImpl> active_frame_ = nullptr;

  // The active match ordinal relative to the matches found in its own frame.
  int relative_active_match_ordinal_ = 0;

  // The overall active match ordinal for the current find-in-page session.
  int active_match_ordinal_ = 0;

  // The rectangle around the active match, in screen coordinates.
  gfx::Rect selection_rect_;

  // Find requests are queued here when previous requests need to be handled
  // before these ones can be properly routed.
  base::queue<FindRequest> find_request_queue_;

  // Keeps track of the find request ID of the last find reply reported via
  // NotifyFindReply().
  int last_reported_id_ = kInvalidId;

  // WebContentsObservers to observe frame changes in |contents_| and its inner
  // WebContentses.
  std::vector<std::unique_ptr<FrameObserver>> frame_observers_;

  base::CancelableOnceClosure delayed_find_task_;

  CreateFindInPageClientFunction create_find_in_page_client_for_testing_ =
      nullptr;

  base::WeakPtrFactory<FindRequestManager> weak_factory_{this};
};

}  // namespace content

#endif  // CONTENT_BROWSER_FIND_REQUEST_MANAGER_H_