;;; pdf-isearch.el --- Isearch in pdf buffers. -*- lexical-binding: t -*-

;; Copyright (C) 2013, 2014  Andreas Politz

;; Author: Andreas Politz <politza@fh-trier.de>
;; Keywords: files, multimedia

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;;; Todo:
;;
;; * Add the possibility to limit the search to a range of pages.

(require 'cl-lib)
(require 'pdf-util)
(require 'pdf-info)
(require 'pdf-misc)
(require 'pdf-view)
(require 'pdf-cache)
(require 'let-alist)

;;; Code:
(defvar pdf-isearch--hl-matches-tick 0)


;; * ================================================================== *
;; * Customizations
;; * ================================================================== *

(defgroup pdf-isearch nil
  "Isearch in pdf buffers."
  :group 'pdf-tools)

(defface pdf-isearch-match
  '((((background dark)) (:inherit isearch))
    (((background light)) (:inherit isearch)))
  "Face used to determine the colors of the current match."
  :group 'pdf-isearch
  :group 'pdf-tools-faces)

(defface pdf-isearch-lazy
  '((((background dark)) (:inherit lazy-highlight))
    (((background light)) (:inherit lazy-highlight)))
  "Face used to determine the colors of non-current matches."
  :group 'pdf-isearch
  :group 'pdf-tools-faces)

(defface pdf-isearch-batch
  '((((background dark)) (:inherit match))
    (((background light)) (:inherit match)))
  "Face used to determine the colors in `pdf-isearch-batch-mode'."
  :group 'pdf-isearch
  :group 'pdf-tools-faces)

(defcustom pdf-isearch-hyphenation-character "-­"
  "Characters used as hyphens when word searching."
  :group 'pdf-isearch
  :type 'string)

(defvar pdf-isearch-search-fun-function nil
  "Search function used when searching.

Like `isearch-search-fun-function', though it should return a
function \(FN STRING &optional PAGES\), which in turn should
return a result like `pdf-info-search-regexp'.")


;; * ================================================================== *
;; * Internal Variables
;; * ================================================================== *

(defvar-local pdf-isearch-current-page nil
  "The page that is currently searched.")

(defvar-local pdf-isearch-current-match nil
  "A list ((LEFT TOP RIGHT BOT) ...) of the current match or nil.

A match may contain more than one edges-element, e.g. when regexp
searching across multiple lines.")

(defvar-local pdf-isearch-current-matches nil
  "A list of matches of the last search.")

(defvar-local pdf-isearch-current-parameter nil
  "A list of search parameter \(search-string regex-p case-fold word-search\).")


;; * ================================================================== *
;; * Modes
;; * ================================================================== *

(declare-function pdf-occur "pdf-occur.el")
(declare-function pdf-sync-backward-search "pdf-sync.el")

(defvar pdf-isearch-minor-mode-map
  (let ((kmap (make-sparse-keymap)))
    (define-key kmap [remap occur] 'pdf-occur)
    kmap)
  "Keymap used in `pdf-isearch-minor-mode'.")

(defvar pdf-isearch-active-mode-map
  (let ((kmap (make-sparse-keymap)))
    (set-keymap-parent kmap isearch-mode-map)
    (define-key kmap (kbd "C-d") 'pdf-view-dark-minor-mode)
    (define-key kmap (kbd "C-b") 'pdf-isearch-batch-mode)
    (define-key kmap (kbd "M-s o") 'pdf-isearch-occur)
    (define-key kmap (kbd "M-s s") 'pdf-isearch-sync-backward)
    kmap)
  "Keymap used in `pdf-isearch-active-mode'.

This keymap is used, when isearching in PDF buffers.  Its parent
keymap is `isearch-mode-map'.")

(put 'image-scroll-up 'isearch-scroll t)
(put 'image-scroll-down 'isearch-scroll t)

(define-minor-mode pdf-isearch-active-mode
  "This mode is enabled when isearch is active in a PDF file."
  :group 'pdf-isearch
  (cond
   (pdf-isearch-active-mode
    (set (make-local-variable 'isearch-mode-map)
         pdf-isearch-active-mode-map)
    (setq overriding-terminal-local-map
          isearch-mode-map))
   (t
    ;;(setq overriding-terminal-local-map nil) ?
    (kill-local-variable 'isearch-mode-map))))

;;;###autoload
(define-minor-mode pdf-isearch-minor-mode
  "Isearch mode for PDF buffer.

When this mode is enabled \\[isearch-forward], among other keys,
starts an incremental search in this PDF document.  Since this mode
uses external programs to highlight found matches via
image-processing, proceeding to the next match may be slow.

Therefore two isearch behaviours have been defined: Normal isearch and
batch mode.  The later one is a minor mode
\(`pdf-isearch-batch-mode'\), which when activated inhibits isearch
from stopping at and highlighting every single match, but rather
display them batch-wise.  Here a batch means a number of matches
currently visible in the selected window.

The kind of highlighting is determined by three faces
`pdf-isearch-match' \(for the current match\), `pdf-isearch-lazy'
\(for all other matches\) and `pdf-isearch-batch' \(when in batch
mode\), which see.

Colors may also be influenced by the minor-mode
`pdf-view-dark-minor-mode'.  If this is minor mode enabled, each face's
dark colors, are used (see e.g. `frame-background-mode'), instead
of the light ones.

\\{pdf-isearch-minor-mode-map}
While in `isearch-mode' the following keys are available. Note
that not every isearch command work as expected.

\\{pdf-isearch-active-mode-map}"
  :group 'pdf-isearch
  (pdf-util-assert-pdf-buffer)
  (cond
   (pdf-isearch-minor-mode
    (when (boundp 'character-fold-search)
      (setq-local character-fold-search nil))
    (set (make-local-variable 'isearch-search-fun-function)
         (lambda nil 'pdf-isearch-search-function))
    (set (make-local-variable 'isearch-push-state-function)
         'pdf-isearch-push-state-function)
    (set (make-local-variable 'isearch-wrap-function)
         'pdf-isearch-wrap-function)
    (set (make-local-variable 'isearch-lazy-highlight) nil)
    ;; Make our commands work in isearch-mode.
    (set (make-local-variable 'isearch-allow-scroll) t)
    (set (make-local-variable 'search-exit-option)
         ;; This maybe edit or t, but edit would suppress our cmds
         ;; in isearch-other-meta-char.
         (not (not search-exit-option)))
    ;; FIXME: Die Variable imagemagick-render-type entweder an anderer
    ;; Stelle global setzen oder nur irgendwo auf den
    ;; Performancegewinn hinweisen.
    (when (and (boundp 'imagemagick-render-type)
               (= 0 imagemagick-render-type))
      ;; This enormously speeds up rendering.
      (setq imagemagick-render-type 1))
    (add-hook 'isearch-mode-hook 'pdf-isearch-mode-initialize nil t)
    (add-hook 'isearch-mode-end-hook 'pdf-isearch-mode-cleanup nil t)
    (add-hook 'isearch-update-post-hook 'pdf-isearch-update nil t))
   (t
    (when (boundp 'character-fold-search)
      (kill-local-variable 'character-fold-search))
    (kill-local-variable 'search-exit-option)
    (kill-local-variable 'isearch-allow-scroll)
    (kill-local-variable 'isearch-search-fun-function)
    (kill-local-variable 'isearch-push-state-function)
    (kill-local-variable 'isearch-wrap-function)
    (kill-local-variable 'isearch-lazy-highlight)
    (remove-hook 'isearch-update-post-hook 'pdf-isearch-update t)
    (remove-hook 'isearch-mode-hook 'pdf-isearch-mode-initialize t)
    (remove-hook 'isearch-mode-end-hook 'pdf-isearch-mode-cleanup t))))

(define-minor-mode pdf-isearch-batch-mode
  "Isearch PDF documents batch-wise.

If this mode is enabled, isearching does not stop at every match,
but rather moves to the next one not currently visible.  This
behaviour is much faster than ordinary isearch, since far less
different images have to be displayed."
  :group 'pdf-isearch
  (when isearch-mode
    (pdf-isearch-redisplay)
    (pdf-isearch-message
     (if pdf-isearch-batch-mode "batch mode" "isearch mode"))))



;; * ================================================================== *
;; * Isearch interface
;; * ================================================================== *

(defvar pdf-isearch-filter-matches-function nil
  "A function for filtering isearch matches.

The function receives one argument: a list of matches, each
being a list of edges. It should return a subset of this list.
Edge coordinates are in image-space.")

(defvar pdf-isearch-narrow-to-page nil
  "Non-nil, if the search should be limited to the current page.")

(defun pdf-isearch-search-function (string &rest _)
  "Search for STRING in the current PDF buffer.

This is a Isearch interface function."
  (when (> (length string) 0)
    (let ((same-search-p (pdf-isearch-same-search-p))
          (oldpage pdf-isearch-current-page)
          (pages (or (image-mode-window-get 'displayed-pages (selected-window))
                     (list (pdf-view-current-page))))
          matches
          next-match)
      (dolist (page pages)
        (let ((page-matches (pdf-isearch-search-page string page)))
          ;; matches is a list of list of edges ((x0 y1 x1 y2) ...),
          ;; sorted top to bottom ,left to right. Coordinates are in image
          ;; space.
          (unless isearch-forward
            (setq page-matches (reverse page-matches)))
          (when pdf-isearch-filter-matches-function
            (setq page-matches (funcall pdf-isearch-filter-matches-function page-matches)))
          (push page-matches matches)))
      ;; Where to go next ?
      (setq pdf-isearch-current-page (pdf-view-current-page)
            pdf-isearch-current-matches matches
            next-match
            (pdf-isearch-next-match
             oldpage pdf-isearch-current-page
             pdf-isearch-current-match (car matches)
             same-search-p
             isearch-forward)
            pdf-isearch-current-parameter
            (list string isearch-regexp
                  isearch-case-fold-search isearch-word))
      (cl-callf nreverse matches)
      (cond
       (next-match
        (setq pdf-isearch-current-match next-match)
        (pdf-isearch-hl-matches  next-match matches nil pages)
        (pdf-isearch-focus-match next-match)
        ;; Don't get off track.
        (when (or (and (bobp) (not isearch-forward))
                  (and (eobp) isearch-forward))
          (unless pdf-view-roll-minor-mode
            (goto-char (1+ (/ (buffer-size) 2)))))
        ;; Signal success to isearch.
        ;; Moving the point is for `pdf-roll'. It ensures that
        ;; `re-search-forward' takes us back to the starting point. Otherwise
        ;; every call to `isearch-repeat' will increment/decrement the point
        ;; and that causes recentering.
        (if isearch-forward
            (progn (unless (bobp) (forward-char -1))
                   (re-search-forward "."))
          (unless (eobp) (forward-char 1))
          (re-search-backward ".")))
       ((and (not pdf-isearch-narrow-to-page)
             (not (pdf-isearch-empty-match-p pdf-isearch-current-matches)))
        (let ((next-page (pdf-isearch-find-next-matching-page
                          string pdf-isearch-current-page t)))
          (when next-page
            (pdf-view-goto-page next-page)
            (pdf-isearch-search-function string))))))))

(defun pdf-isearch-push-state-function ()
  "Push the current search state.

This is a Isearch interface function."
  (let ((hscroll (window-hscroll))
        (vscroll (window-vscroll))
        (parms pdf-isearch-current-parameter)
        (matches pdf-isearch-current-matches)
        (match pdf-isearch-current-match)
        (page pdf-isearch-current-page))
    (lambda (_state)
      (setq pdf-isearch-current-parameter parms
            pdf-isearch-current-matches matches
            pdf-isearch-current-match match
            pdf-isearch-current-page page)
      (pdf-view-goto-page pdf-isearch-current-page)
      (if pdf-isearch-current-match
          (pdf-isearch-hl-matches
           pdf-isearch-current-match
           pdf-isearch-current-matches
           nil (image-mode-window-get 'displayed-pages (selected-window)))
        (when pdf-view-roll-minor-mode
          (pdf-view-redisplay)))
      (image-set-window-hscroll hscroll)
      (image-set-window-vscroll vscroll))))

(defun pdf-isearch-wrap-function ()
  "Go to first or last page.

This is a Isearch interface function."
  (let ((page (if isearch-forward
                  1
                (pdf-cache-number-of-pages))))
    (unless (or pdf-isearch-narrow-to-page
                (= page (pdf-view-current-page)))
      (pdf-view-goto-page page)
      (let ((next-screen-context-lines 0))
        (if (= page 1)
            (image-scroll-down)
          (image-scroll-up)))))
  (setq pdf-isearch-current-match nil))

(defun pdf-isearch-mode-cleanup ()
  "Cleanup after exiting Isearch.

This is a Isearch interface function."
  (pdf-isearch-active-mode -1)
  (pdf-view-redisplay))

(defun pdf-isearch-mode-initialize ()
  "Initialize isearching.

This is a Isearch interface function."
  (pdf-isearch-active-mode 1)
  (setq pdf-isearch-current-page (pdf-view-current-page)
        pdf-isearch-current-match nil
        pdf-isearch-current-matches nil
        pdf-isearch-current-parameter nil)
  (unless pdf-view-roll-minor-mode
    (goto-char (1+ (/ (buffer-size) 2)))))

(defun pdf-isearch-same-search-p (&optional ignore-search-string-p)
  "Return non-nil, if search parameter have not changed.

Parameter inspected are `isearch-string' (unless
IGNORE-SEARCH-STRING-P is t) and `isearch-case-fold-search'.  If
there was no previous search, this function returns t."
  (or (null pdf-isearch-current-parameter)
      (let ((parameter (list isearch-string
                             isearch-regexp
                             isearch-case-fold-search
                             isearch-word)))
        (if ignore-search-string-p
            (equal (cdr pdf-isearch-current-parameter)
                   (cdr parameter))
          (equal pdf-isearch-current-parameter
                 parameter)))))

(defun pdf-isearch-next-match (last-page this-page last-match
                                         all-matches continued-p
                                         forward-p)
  "Determine the next match."
  (funcall (if pdf-isearch-batch-mode
               'pdf-isearch-next-match-batch
             'pdf-isearch-next-match-isearch)
           last-page this-page last-match
           all-matches continued-p forward-p))

(defun pdf-isearch-focus-match (current-match)
  "Make the CURRENT-MATCH visible in the window."
  (funcall (if pdf-isearch-batch-mode
               'pdf-isearch-focus-match-batch
             'pdf-isearch-focus-match-isearch)
           current-match))

(defun pdf-isearch-redisplay ()
  "Redisplay the current highlighting."
  (pdf-isearch-hl-matches
   pdf-isearch-current-match
   pdf-isearch-current-matches
   nil
   (or (image-mode-window-get 'displayed-pages (selected-window))
       (list (pdf-view-current-page)))))

(defun pdf-isearch-update ()
  "Update search and redisplay, if necessary."
  (unless (pdf-isearch-same-search-p t)
    (setq pdf-isearch-current-parameter
          (list isearch-string isearch-regexp
                isearch-case-fold-search isearch-word)
          pdf-isearch-current-matches
          (pdf-isearch-search-page isearch-string))
    (pdf-isearch-redisplay)))

(defun pdf-isearch-message (fmt &rest args)
  "Like `message', but Isearch friendly."
  (unless args (setq args (list fmt) fmt "%s"))
  (let ((msg (apply 'format fmt args)))
    (if (cl-some (lambda (buf)
                   (buffer-local-value 'isearch-mode buf))
                 (mapcar 'window-buffer (window-list)))
        (let ((isearch-message-suffix-add
               (format " [%s]" msg)))
          (isearch-message)
          (sit-for 1))
      (message "%s" msg))))

(defun pdf-isearch-empty-match-p (matches)
  (let ((all-matches (apply #'append matches)))
      (and all-matches
       (cl-every
        (lambda (match)
          (cl-every (lambda (edges)
                      (cl-every 'zerop edges))
                    match))
        all-matches))))

(defun pdf-isearch-occur ()
  "Run `occur' using the last search string or regexp."
  (interactive)
  (let ((case-fold-search isearch-case-fold-search)
        (regexp
         (cond
          ((functionp isearch-word)
           (funcall isearch-word isearch-string))
          (isearch-word (pdf-isearch-word-search-regexp
                         isearch-string nil
                         pdf-isearch-hyphenation-character))
          (isearch-regexp isearch-string))))
    (save-selected-window
      (pdf-occur (or regexp isearch-string) regexp))
    (isearch-message)))

(defun pdf-isearch-sync-backward ()
  "Visit the source of the beginning of the current match."
  (interactive)
  (pdf-util-assert-pdf-window)
  (unless pdf-isearch-current-match
    (user-error "No current or recent match"))
  (when isearch-mode
    (isearch-exit))
  (cl-destructuring-bind (left top _right _bot)
      (car pdf-isearch-current-match)
    (pdf-sync-backward-search left top)))


;; * ================================================================== *
;; * Interface to epdfinfo
;; * ================================================================== *

(defun pdf-isearch-search-page (string &optional page)
  "Search STRING on PAGE in the current window.

Returns a list of edges (LEFT TOP RIGHT BOTTOM) in PDF
coordinates, sorted top to bottom, then left to right."

  (unless page (setq page (pdf-view-current-page)))
  (mapcar (lambda (match)
            (let-alist match
              (pdf-util-scale-relative-to-pixel .edges 'round)))
          (let ((case-fold-search isearch-case-fold-search))
            (funcall (pdf-isearch-search-fun)
                     string page))))

(defun pdf-isearch-search-fun ()
  (funcall (or pdf-isearch-search-fun-function
               'pdf-isearch-search-fun-default)))

(defun pdf-isearch-search-fun-default ()
  "Return default functions to use for the search."
  (cond
   ((eq isearch-word t)
    (lambda (string &optional pages)
      ;; Use lax versions to not fail at the end of the word while
      ;; the user adds and removes characters in the search string
      ;; (or when using nonincremental word isearch)
      (let ((lax (not (or isearch-nonincremental
                          (null (car isearch-cmds))
                          (eq (length isearch-string)
                              (length (isearch--state-string
                                       (car isearch-cmds))))))))
        (pdf-info-search-regexp
         (pdf-isearch-word-search-regexp
          string lax pdf-isearch-hyphenation-character)
         pages 'invalid-regexp))))
   (isearch-regexp
    (lambda (string &optional pages)
      (pdf-info-search-regexp string pages 'invalid-regexp)))
   (t
    'pdf-info-search-string)))


(defun pdf-isearch-word-search-regexp (string &optional lax hyphenization-chars)
  "Return a PCRE which matches words, ignoring punctuation."
  (let ((hyphenization-regexp
         (and hyphenization-chars
              (format "(?:[%s]\\n)?"
                      (replace-regexp-in-string
                       "[]^\\\\-]" "\\\\\\&"
                       hyphenization-chars t)))))
    (cond
     ((equal string "") "")
     ((string-match-p "\\`\\W+\\'" string) "\\W+")
     (t (concat
         (if (string-match-p "\\`\\W" string) "\\W+"
           (unless lax "\\b"))
         (mapconcat (lambda (word)
                      (if hyphenization-regexp
                          (mapconcat
                           (lambda (ch)
                             (pdf-util-pcre-quote (string ch)))
                           (append word nil)
                           hyphenization-regexp)
                        (pdf-util-pcre-quote word)))
                    (split-string string "\\W+" t) "\\W+")
         (if (string-match-p "\\W\\'" string) "\\W+"
           (unless lax "\\b")))))))

(defun pdf-isearch-find-next-matching-page (string page &optional interactive-p)
  "Find STRING after or before page PAGE, according to FORWARD-P.

If INTERACTIVE-P is non-nil, give some progress feedback.
Returns the page number where STRING was found, or nil if there
is no such page."
  ;; Do a exponentially expanding search.
  (let* ((incr 1)
         (pages (if isearch-forward
                    (cons (1+ page)
                          (1+ page))
                  (cons (1- page)
                        (1- page))))
         (fn (pdf-isearch-search-fun))
         matched-page
         reporter)

    (while (and (null matched-page)
                (or (and isearch-forward
                         (<= (car pages)
                             (pdf-cache-number-of-pages)))
                    (and (not isearch-forward)
                         (>= (cdr pages) 1))))
      (let* ((case-fold-search isearch-case-fold-search)
             (matches (funcall fn string pages)))
        (setq matched-page
              (alist-get 'page (if isearch-forward
                                   (car matches)
                                 (car (last matches))))))
      (setq incr (* incr 2))
      (cond (isearch-forward
             (setcar pages (1+ (cdr pages)))
             (setcdr pages (min (pdf-cache-number-of-pages)
                                (+ (cdr pages) incr))))
            (t
             (setcdr pages (1- (car pages)))
             (setcar pages (max 1 (- (car pages)
                                     incr)))))
      (when interactive-p
        (when (and (not reporter)
                   (= incr 8)) ;;Don't bother right away.
          (setq reporter
                (apply
                 'make-progress-reporter "Searching"
                 (if isearch-forward
                     (list (car pages) (pdf-cache-number-of-pages) nil 0)
                   (list 1 (cdr pages) nil 0)))))
        (when reporter
          (progress-reporter-update
           reporter (if isearch-forward
                        (- (cdr pages) page)
                      (- page (car pages)))))))
    matched-page))



;; * ================================================================== *
;; * Isearch Behavior
;; * ================================================================== *

(defun pdf-isearch-next-match-isearch (last-page this-page last-match
                                                 matches same-search-p
                                                 forward)
  "Default function for choosing the next match.

Implements default isearch behaviour, i.e. it stops at every
match."
  (cond
   ((null last-match)
    ;; Goto first match from top or bottom of the window.
    (let* ((iedges (pdf-util-image-displayed-edges))
           (pos (pdf-util-with-edges (iedges)
                  (if forward
                      (list iedges-left iedges-top
                            iedges-left iedges-top)
                    (list iedges-right iedges-bot
                          iedges-right iedges-bot)))))
      (pdf-isearch-closest-match (list pos) matches forward)))
   ((not (eq last-page this-page))
    ;; First match from top-left or bottom-right of the new
    ;; page.
    (car matches))
   (same-search-p
    ;; Next match after the last one.
    (if last-match
        (cadr (member last-match matches))))
   (matches
    ;; Next match of new search closest to the last one.
    (pdf-isearch-closest-match
     last-match matches forward))))

(defun pdf-isearch-focus-match-isearch (match)
  "Make the image area in MATCH visible in the selected window."
  (pdf-util-scroll-to-edges (apply 'pdf-util-edges-union match)))

(defun pdf-isearch-next-match-batch (last-page this-page last-match
                                               matches same-search-p
                                               forward-p)
  "Select the next match, unseen in the current search direction."

  (if (or (null last-match)
          (not same-search-p)
          (not (eq last-page this-page)))
      (pdf-isearch-next-match-isearch
       last-page this-page last-match matches same-search-p forward-p)
    (pdf-util-with-edges (match iedges)
      (let ((iedges (pdf-util-image-displayed-edges)))
        (car (cl-remove-if
              ;; Filter matches visible on screen.
              (lambda (edges)
                (let ((match (apply 'pdf-util-edges-union edges)))
                  (and (<= match-right iedges-right)
                       (<= match-bot iedges-bot)
                       (>= match-left iedges-left)
                       (>= match-top iedges-top))))
              (cdr (member last-match matches))))))))

(defun pdf-isearch-focus-match-batch (match)
  "Make the image area in MATCH eagerly visible in the selected window."
  (pdf-util-scroll-to-edges (apply 'pdf-util-edges-union match) t))

(cl-deftype pdf-isearch-match ()
  `(satisfies
    (lambda (match)
      (cl-every (lambda (edges)
                  (and (consp edges)
                       (= (length edges) 4)
                       (cl-every 'numberp edges)))
                match))))

(cl-deftype list-of (type)
  `(satisfies
    (lambda (l)
      (and (listp l)
           (cl-every (lambda (x)
                       (cl-typep x ',type))
                     l)))))

(defun pdf-isearch-closest-match (match matches
                                        &optional forward-p)
  "Find the nearest element to MATCH in MATCHES.

The direction in which to look is determined by FORWARD-P.

MATCH should be a list of edges, MATCHES a list of such element;
it is assumed to be ordered with respect to FORWARD-P."


  (cl-check-type match pdf-isearch-match)
  (cl-check-type matches (list-of pdf-isearch-match))
  (let ((matched (apply 'pdf-util-edges-union match)))
    (pdf-util-with-edges (matched)
      (cl-loop for next in matches do
               (let ((edges (apply 'pdf-util-edges-union next)))
                 (pdf-util-with-edges (edges)
                   (when (if forward-p
                             (or (>= edges-top matched-bot)
                                 (and (or (>= edges-top matched-top)
                                          (>= edges-bot matched-bot))
                                      (>= edges-right matched-right)))
                           (or (<= edges-bot matched-top)
                               (and (or (<= edges-bot matched-bot)
                                        (<= edges-top matched-top))
                                    (<= edges-left matched-left))))
                     (cl-return next))))))))



;; * ================================================================== *
;; * Display
;; * ================================================================== *


(defun pdf-isearch-current-colors ()
  "Return the current color set.

The return value depends on `pdf-view-dark-minor-mode' and
`pdf-isearch-batch-mode'.  It is a list of four colors \(MATCH-FG
MATCH-BG LAZY-FG LAZY-BG\)."
  (let ((dark-p pdf-view-dark-minor-mode))
    (cond
     (pdf-isearch-batch-mode
      (let ((colors (pdf-util-face-colors 'pdf-isearch-batch dark-p)))
        (list (car colors)
              (cdr colors)
              (car colors)
              (cdr colors))))
     (t
      (let ((match (pdf-util-face-colors 'pdf-isearch-match dark-p))
            (lazy (pdf-util-face-colors 'pdf-isearch-lazy dark-p)))
        (list (car match)
              (cdr match)
              (car lazy)
              (cdr lazy)))))))

(defun pdf-isearch-hl-matches (current matches &optional occur-hack-p pages)
  "Highlighting edges CURRENT and MATCHES."
  (cl-check-type current pdf-isearch-match)
  (cl-check-type matches (list-of (list-of pdf-isearch-match)))
  (cl-destructuring-bind (fg1 bg1 fg2 bg2)
      (pdf-isearch-current-colors)
    (let* ((window (selected-window))
           (pages (or pages
                      (image-mode-window-get 'displayed-pages (selected-window))
                      (list (pdf-view-current-page))))
           (buffer (current-buffer))
           (tick (cl-incf pdf-isearch--hl-matches-tick)))
      (dolist (page pages)
        (let* ((width (car (pdf-view-image-size nil window page)))
               (pdf-info-asynchronous
                (lambda (status data)
                  (when (and (null status)
                             (eq tick pdf-isearch--hl-matches-tick)
                             (buffer-live-p buffer)
                             (window-live-p window)
                             (eq (window-buffer window)
                                 buffer))
                    (with-selected-window window
                      (when (and (derived-mode-p 'pdf-view-mode)
                                 (or isearch-mode occur-hack-p
                                     (memq last-command '(isearch-repeat-forward isearch-repeat-backward)))
                                 (or (eq page (pdf-view-current-page))
                                     (memq page (image-mode-window-get 'displayed-pages window))))
                        (pdf-view-display-image
                         (pdf-view-create-image data :width width)
                         page window)))))))
          (pdf-info-renderpage-text-regions
           page width t nil nil
           `(,fg1 ,bg1 ,@(pdf-util-scale-pixel-to-relative
                          (when (eq page (pdf-view-current-page window))
                            current)))
           `(,fg2 ,bg2 ,@(pdf-util-scale-pixel-to-relative
                          (apply 'append
                                 (remove current (pop matches)))))))))))


;; * ================================================================== *
;; * Debug
;; * ================================================================== *

;; The following isearch-search function is debuggable.
;;
(when nil
  (defun isearch-search ()
    ;; Do the search with the current search string.
    (if isearch-message-function
        (funcall isearch-message-function nil t)
      (isearch-message nil t))
    (if (and (eq isearch-case-fold-search t) search-upper-case)
        (setq isearch-case-fold-search
              (isearch-no-upper-case-p isearch-string isearch-regexp)))
    (condition-case lossage
        (let ((inhibit-point-motion-hooks
               ;; FIXME: equality comparisons on functions is asking for trouble.
               (and (eq isearch-filter-predicate 'isearch-filter-visible)
                    search-invisible))
              (inhibit-quit nil)
              (case-fold-search isearch-case-fold-search)
              (retry t))
          (setq isearch-error nil)
          (while retry
            (setq isearch-success
                  (isearch-search-string isearch-string nil t))
            ;; Clear RETRY unless the search predicate says
            ;; to skip this search hit.
            (if (or (not isearch-success)
                    (bobp) (eobp)
                    (= (match-beginning 0) (match-end 0))
                    (funcall isearch-filter-predicate
                             (match-beginning 0) (match-end 0)))
                (setq retry nil)))
          (setq isearch-just-started nil)
          (if isearch-success
              (setq isearch-other-end
                    (if isearch-forward (match-beginning 0) (match-end 0)))))

      (quit (isearch-unread ?\C-g)
            (setq isearch-success nil))

      (invalid-regexp
       (setq isearch-error (car (cdr lossage)))
       (if (string-match
            "\\`Premature \\|\\`Unmatched \\|\\`Invalid "
            isearch-error)
           (setq isearch-error "incomplete input")))

      (search-failed
       (setq isearch-success nil)
       (setq isearch-error (nth 2 lossage)))

      ;; (error
      ;;  ;; stack overflow in regexp search.
      ;;  (setq isearch-error (format "%s" lossage)))
      )

    (if isearch-success
        nil
      ;; Ding if failed this time after succeeding last time.
      (and (isearch--state-success (car isearch-cmds))
           (ding))
      (if (functionp (isearch--state-pop-fun (car isearch-cmds)))
          (funcall (isearch--state-pop-fun (car isearch-cmds))
                   (car isearch-cmds)))
      (goto-char (isearch--state-point (car isearch-cmds))))))


(provide 'pdf-isearch)

;;; pdf-isearch.el ends here

;; Local Variables:
;; byte-compile-warnings: (not obsolete)
;; End:
