// 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 COMPONENTS_PDF_RENDERER_PDF_ACCESSIBILITY_TREE_H_
#define COMPONENTS_PDF_RENDERER_PDF_ACCESSIBILITY_TREE_H_

#include <map>
#include <memory>
#include <optional>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "content/public/renderer/plugin_ax_tree_action_target_adapter.h"
#include "content/public/renderer/render_frame_observer.h"
#include "pdf/accessibility_structs.h"
#include "pdf/page_character_index.h"
#include "pdf/pdf_accessibility_data_handler.h"
#include "services/screen_ai/buildflags/buildflags.h"
#include "third_party/blink/public/web/web_ax_object.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_id_forward.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_source.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/vector2d_f.h"

namespace blink {
class WebPluginContainer;
}  // namespace blink

namespace chrome_pdf {

class PdfAccessibilityActionHandler;

}  // namespace chrome_pdf

namespace content {
class RenderAccessibility;
class RenderFrame;
}  // namespace content

namespace gfx {
class Transform;
}  // namespace gfx

namespace pdf {

class PdfAccessibilityTree : public ui::AXTreeSource<const ui::AXNode*,
                                                     ui::AXTreeData*,
                                                     ui::AXNodeData>,
                             public content::PluginAXTreeActionTargetAdapter,
                             public content::RenderFrameObserver,
                             public chrome_pdf::PdfAccessibilityDataHandler {
 public:
  PdfAccessibilityTree(
      content::RenderFrame* render_frame,
      chrome_pdf::PdfAccessibilityActionHandler* action_handler,
      blink::WebPluginContainer* plugin_container);
  ~PdfAccessibilityTree() override;

  static bool IsDataFromPluginValid(
      const std::vector<chrome_pdf::AccessibilityTextRunInfo>& text_runs,
      const std::vector<chrome_pdf::AccessibilityCharInfo>& chars,
      const chrome_pdf::AccessibilityPageObjects& page_objects);

  // Stores the page index and annotation index in the page.
  struct AnnotationInfo {
    AnnotationInfo(uint32_t page_index, uint32_t annotation_index);
    AnnotationInfo(const AnnotationInfo& other);
    ~AnnotationInfo();

    uint32_t page_index;
    uint32_t annotation_index;
  };

  // chrome_pdf::PdfAccessibilityDataHandler:
  void SetAccessibilityViewportInfo(
      chrome_pdf::AccessibilityViewportInfo viewport_info) override;
  void SetAccessibilityDocInfo(
      std::unique_ptr<chrome_pdf::AccessibilityDocInfo> doc_info) override;
  void SetAccessibilityPageInfo(
      chrome_pdf::AccessibilityPageInfo page_info,
      std::vector<chrome_pdf::AccessibilityTextRunInfo> text_runs,
      std::vector<chrome_pdf::AccessibilityCharInfo> chars,
      chrome_pdf::AccessibilityPageObjects page_objects) override;
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
  void OnHasSearchifyText() override;
#endif

  void HandleAction(const chrome_pdf::AccessibilityActionData& action_data);
  std::optional<AnnotationInfo> GetPdfAnnotationInfoFromAXNode(
      int32_t ax_node_id) const;

  // Given the AXNode and the character offset within the AXNode, finds the
  // respective page index and character index within the page. Returns
  // false if the `node` is not a valid static text or inline text box
  // AXNode. Used to find the character offsets of selection.
  bool FindCharacterOffset(
      const ui::AXNode& node,
      uint32_t char_offset_in_node,
      chrome_pdf::PageCharacterIndex& page_char_index) const;

  // ui::AXTreeSource:
  bool GetTreeData(ui::AXTreeData* tree_data) const override;
  ui::AXNode* GetRoot() const override;
  ui::AXNode* GetFromId(int32_t id) const override;
  int32_t GetId(const ui::AXNode* node) const override;
  void CacheChildrenIfNeeded(const ui::AXNode*) override {}
  size_t GetChildCount(const ui::AXNode*) const override;
  const ui::AXNode* ChildAt(const ui::AXNode* node, size_t) const override;
  void ClearChildCache(const ui::AXNode*) override {}
  ui::AXNode* GetParent(const ui::AXNode* node) const override;
  bool IsIgnored(const ui::AXNode* node) const override;
  bool IsEqual(const ui::AXNode* node1, const ui::AXNode* node2) const override;
  const ui::AXNode* GetNull() const override;
  void SerializeNode(const ui::AXNode* node,
                     ui::AXNodeData* out_data) const override;

  // content::PluginAXTreeActionTargetAdapter:
  std::unique_ptr<ui::AXActionTarget> CreateActionTarget(
      ui::AXNodeID id) override;

  // content::RenderFrameObserver:
  void AccessibilityModeChanged(const ui::AXMode& mode) override;
  void OnDestruct() override {}
  void WasHidden() override;
  void WasShown() override;

  bool ShowContextMenu();

  ui::AXTree& tree_for_testing() { return tree_; }
  ui::AXTreeData& tree_data_for_testing() { return tree_data_; }

  // Sets the ID of a child tree which this node will be hosting. In this way,
  // multiple trees could be stitched together. Clears any existing descendants
  // of the hosting node in order to maintain the consistency of the tree
  // structure, and because they would be hidden by the child tree anyway.
  bool SetChildTree(const ui::AXNodeID& target_node_id,
                    const ui::AXTreeID& child_tree_id);

  void ForcePluginAXObjectForTesting(const blink::WebAXObject& obj);
  void FindNodeOffsetForTesting(bool end_of_selection,
                                uint32_t page_index,
                                uint32_t page_char_index,
                                int32_t* out_node_id,
                                int32_t* out_node_char_index) const;

 private:
  // Update the AXTreeData when the selected range changed.
  void UpdateAXTreeDataFromSelection();

  void DoSetAccessibilityViewportInfo(
      const chrome_pdf::AccessibilityViewportInfo& viewport_info);
  void DoSetAccessibilityDocInfo(
      std::unique_ptr<chrome_pdf::AccessibilityDocInfo> doc_info);
  void DoSetAccessibilityPageInfo(
      const chrome_pdf::AccessibilityPageInfo& page_info,
      const std::vector<chrome_pdf::AccessibilityTextRunInfo>& text_runs,
      const std::vector<chrome_pdf::AccessibilityCharInfo>& chars,
      const chrome_pdf::AccessibilityPageObjects& page_objects);

  // Given a 0-based page index and 0-based character index within a page,
  // find the node ID of the associated static text AXNode, and the character
  // index within that text node. Used to find the start and end of the
  // selected text range.
  void FindNodeOffset(bool end_of_selection,
                      uint32_t page_index,
                      uint32_t page_char_index,
                      int32_t* out_node_id,
                      int32_t* out_node_char_index) const;

  bool RecursiveFindNodeOffset(bool end_of_selection,
                               ui::AXNode* node,
                               uint32_t page_char_index,
                               int32_t* out_node_id,
                               int32_t* out_node_char_index) const;

  // Called after the data for some pages in the PDF have been received and
  // sends the data on the added pages to the host tree.
  void UnserializeNodes();

  // If needed sets the status message when all pages are loaded.
  void SetFinalStatusMessage();

  void AddPageContent(
      const chrome_pdf::AccessibilityPageInfo& page_info,
      uint32_t page_index,
      const std::vector<chrome_pdf::AccessibilityTextRunInfo>& text_runs,
      const std::vector<chrome_pdf::AccessibilityCharInfo>& chars,
      const chrome_pdf::AccessibilityPageObjects& page_objects);

  // Clears the local cache of node data used to create the tree so that
  // replacement node data can be introduced.
  void ClearAccessibilityNodes();

  std::optional<blink::WebAXObject> GetPluginContainerAXObject();

  std::unique_ptr<gfx::Transform> MakeTransformFromViewInfo() const;

  // Set the status node's message.
  void SetStatusMessage(int message_id);

  void ResetStatusNodeAttributes();

  // Handles an accessibility change only if there is a valid
  // `RenderAccessibility` for the frame. `LoadAccessibility()` will be
  // triggered in `PdfViewWebPlugin` when `always_load_or_reload_accessibility`
  // is true, even if the accessibility state is `AccessibilityState::kLoaded`.
  void MaybeHandleAccessibilityChange(bool always_load_or_reload_accessibility);

  // Marks the plugin container dirty to ensure serialization of the PDF
  // contents.
  void MarkPluginContainerDirty();

  // Let our dependent objects know about our lifetime; `set_this`, if true,
  // sets `this` in our dependents; nullptr otherwise.
  // Returns true on successful update.
  bool UpdateDependentObjects(bool set_this);

  // Returns a weak pointer for an instance of this class.
  base::WeakPtr<PdfAccessibilityTree> GetWeakPtr() {
    return weak_ptr_factory_.GetWeakPtr();
  }

  ui::AXTreeData tree_data_;
  ui::AXTree tree_;

  // Unowned. Must outlive `this`.
  const raw_ptr<chrome_pdf::PdfAccessibilityActionHandler> action_handler_;
  const raw_ptr<blink::WebPluginContainer> plugin_container_;

  // `zoom_` signifies the zoom level set in for the browser content.
  // `scale_` signifies the scale level set by user. Scale is applied
  // by the OS while zoom is applied by the application. Higher scale
  // values are usually set to increase the size of everything on screen.
  // Preferred by people with blurry/low vision. `zoom_` and `scale_`
  // both help us increase/descrease the size of content on screen.
  // From PDF plugin we receive all the data in logical pixels. Which is
  // without the zoom and scale factor applied. We apply the `zoom_` and
  // `scale_` to generate the final bounding boxes of elements in accessibility
  // tree. `orientation_` represents page rotations as multiples of 90 degrees,
  // based on `chrome_pdf::PageOrientation`.
  double zoom_ = 1.0;
  double scale_ = 1.0;
  int32_t orientation_ = 0;
  gfx::Vector2dF scroll_;
  gfx::Vector2dF offset_;
  chrome_pdf::Selection selection_;
  uint32_t page_count_ = 0;
  bool is_tagged_ = false;
  std::unique_ptr<chrome_pdf::AccessibilityStructureElement>
      doc_structure_tree_root_;
  std::unique_ptr<ui::AXNodeData> doc_node_;
  // The banner node will have an appropriate ARIA landmark for easy navigation
  // for screen reader users. It will contain the status node below.
  std::unique_ptr<ui::AXNodeData> banner_node_;
  // The status node contains a notification message for the user.
  std::unique_ptr<ui::AXNodeData> status_node_;
  std::unique_ptr<ui::AXNodeData> status_node_text_;
  std::vector<std::unique_ptr<ui::AXNodeData>> nodes_;

  // Map from the id of each static text AXNode and inline text box
  // AXNode to the page index and index of the first character within its
  // page. Used to find the node associated with the start or end of
  // a selection and vice-versa. The map is ordered in reverse order of the
  // AXNodes creation, which is the same as the reverse order of the tree,
  // and the same order as the list of characters for the PDF page.
  std::map<int32_t, chrome_pdf::PageCharacterIndex> node_id_to_page_char_index_;

  // Map between AXNode id to annotation object. Used to find the annotation
  // object to which an action can be passed.
  std::map<int32_t, AnnotationInfo> node_id_to_annotation_info_;
  bool invalid_plugin_message_received_ = false;

  // Index of the next expected PDF accessibility page info, used to ignore
  // outdated calls of SetAccessibilityPageInfo().
  uint32_t next_page_index_ = 0;

  // Indicates that the PDF had accessible text (at least on some pages) without
  // applying searchify.
  bool had_accessible_text_ = false;
  bool did_have_an_image_ = false;

  // Initialize `currently_in_foreground_` to be true as an associated render
  // frame would be most likely in foreground when being created. If it goes to
  // background, this value will be flipped to false in `WasHidden()`.
  bool currently_in_foreground_ = true;

  // Forces a WebAXObject for the plugin container to be returned, even if the
  // plugin container is nullptr. Enables lower level tests to function.
  blink::WebAXObject force_plugin_ax_object_for_testing_;

#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
  // Flag indicating if any text was converted from images by OCR.
  bool was_text_converted_from_image_ = false;

  // Flag indicating that searchify (OCR) ran on some pages.
  bool did_searchify_run_ = false;
#endif  // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)

  base::WeakPtrFactory<PdfAccessibilityTree> weak_ptr_factory_{this};
};

}  // namespace pdf

#endif  // COMPONENTS_PDF_RENDERER_PDF_ACCESSIBILITY_TREE_H_
