;;; sideline.el --- Show information on the side  -*- lexical-binding: t; -*-

;; Copyright (C) 2022-2026  Shen, Jen-Chieh
;; Created date 2022-06-13 22:08:26

;; Author: Shen, Jen-Chieh <jcs090218@gmail.com>
;; URL: https://github.com/emacs-sideline/sideline
;; Package-Version: 20260716.349
;; Package-Revision: 247cd6f6a0a8
;; Package-Requires: ((emacs "28.1") (ht "2.3"))
;; Keywords: convenience

;; This file is NOT part of GNU Emacs.

;; 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 <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; This library provides the frontend UI to display information either on the
;; left/right side of the buffer window.
;;
;; 1) You would need to first set up the backends,
;;
;;   (setq sideline-backends-left '(sideline-flycheck))
;;
;; 2) Then enable the sideline in the target buffer,
;;
;;   M-x sideline-mode
;;
;; For backends choice, see https://github.com/emacs-sideline/sideline#-example-projects
;;

;;; Code:

(require 'cl-lib)
(require 'face-remap)
(require 'mule-util)
(require 'rect)
(require 'subr-x)

(require 'ht)

(defgroup sideline nil
  "Show information on the side."
  :prefix "sideline-"
  :group 'tool
  :link '(url-link :tag "Repository" "https://github.com/emacs-sideline/sideline"))

(defcustom sideline-backends-left nil
  "The list of active backends to display sideline on the left."
  :type '(list symbol)
  :group 'sideline)

(defcustom sideline-backends-right nil
  "The list of active backends to display sideline on the right."
  :type '(list symbol)
  :group 'sideline)

(defcustom sideline-order-left 'down
  "Display order on the left sidelines."
  :type '(choice (const :tag "Search up" up)
                 (const :tag "Search down" down))
  :group 'sideline)

(defcustom sideline-order-right 'up
  "Display order on the right sidelines."
  :type '(choice (const :tag "Search up" up)
                 (const :tag "Search down" down))
  :group 'sideline)

(defcustom sideline-force-display-if-exceeds nil
  "Display sideline even if the line width are wider than the window width."
  :type 'boolean
  :group 'sideline)

(defcustom sideline-truncate t
  "Truncate sideline if the line width are wider than the window width."
  :type 'boolean
  :group 'sideline)

(defface sideline-default
  '((((background light)) :foreground "DarkOrange")
    (t :foreground "yellow"))
  "Face used to highlight action text."
  :group 'sideline)

(defface sideline-backend
  '((((background light)) :foreground "#7F7F7F")
    (t :foreground "#9B9B9B"))
  "Face used to highlight action text."
  :group 'sideline)

(defcustom sideline-display-backend-name nil
  "Weather to display backend name in the candidate."
  :type 'boolean
  :group 'sideline)

(defcustom sideline-display-backend-type 'outer
  "Method type to display backend name."
  :type '(choice (const :tag "Display on left" left)
                 (const :tag "Display on right" right)
                 (const :tag "Display on inner" inner)
                 (const :tag "Display on outer" outer))
  :group 'sideline)

(defcustom sideline-display-backend-format "[%s]"
  "Format string for candidate and backend name."
  :type 'string
  :group 'sideline)

(defcustom sideline-backends-left-skip-current-line t
  "Don't display left sideline in current line."
  :type 'boolean
  :group 'sideline)

(defcustom sideline-backends-right-skip-current-line t
  "Don't display right sideline in current line."
  :type 'boolean
  :group 'sideline)

(defcustom sideline-format-left "%s   "
  "Format candidate string for left alignment."
  :type 'string
  :group 'sideline)

(defcustom sideline-format-right "   %s"
  "Format candidate string for right alignment."
  :type 'string
  :group 'sideline)

(defcustom sideline-priority 100
  "Overlays' priority."
  :type 'integer
  :group 'sideline)

(defcustom sideline-delay 0.2
  "Number of seconds to wait before showing sideline."
  :type 'number
  :group 'sideline)

(defcustom sideline-backend-delays nil
  "Number of seconds to wait before showing sideline per backend."
  :type '(list symbol)
  :group 'sideline)

(defcustom sideline-pre-render-hook nil
  "Hooks runs before rendering sidelines."
  :type 'hook
  :group 'sideline)

(defcustom sideline-post-render-hook nil
  "Hooks runs after rendering sidelines."
  :type 'hook
  :group 'sideline)

(defcustom sideline-reset-hook nil
  "Hooks runs once the sideline is reset in `post-command-hook'."
  :type 'hook
  :group 'sideline)

(defcustom sideline-inhibit-display-function #'sideline-stop-p
  "Function call to determine weather to display sideline or not."
  :type 'function
  :group 'sideline)

(defvar-local sideline--render-data nil
  "Data used to render; only used everytime before rendering.")

(defvar-local sideline--render-data-wapp (make-hash-table)
  "Record pixel position; only used everytime before rendering.")

(defvar-local sideline--overlays (make-hash-table)
  "Displayed overlays.")

(defvar-local sideline--ex-bound-or-point nil
  "Record of last bound; if this isn't the same, clean up overlays.")

(defvar-local sideline--occupied-lines-left nil
  "Occupied lines on the left.")

(defvar-local sideline--occupied-lines-right nil
  "Occupied lines on the right.")

(defvar-local sideline--text-scale-mode-amount nil
  "Record of last variable `text-scale-mode-amount'.")

(defvar-local sideline-render-this-command nil
  "If this is non-nil, re-render this command.")

(defvar-local sideline--max-remain-spaces nil
  "Hold the maximum remaining spaces when finding lines..")

(defvar-local sideline--max-remain-spaces-line nil
  "Hold the line information that has the maximum remaining spaces.")

;;
;; (@* "Render Flags" )
;;

(defvar-local sideline--ex-window nil
  "Holds previous window.")

(defvar-local sideline--ex-window-start nil
  "Holds previous window start point; detect vertical scrolling.")

(defvar-local sideline--ex-window-hscroll nil
  "Holds previous window hscroll; detect horizontal scrolling.")

(defvar-local sideline--ex-window-width nil
  "Holds previous window width; detect size change.")

(defvar-local sideline--ex-window-height nil
  "Holds previous window height; detect size change.")

(defvar-local sideline--ex-face-remapping-alist nil
  "Holds previous face remapping alist.")

;;
;; (@* "Externals" )
;;

(defvar overflow-newline-into-fringe)

(declare-function buffer-text-pixel-size "src/xdisp.c")

(declare-function shr-string-pixel-width "shr.el")  ; TODO: remove this after 29.1

(defun sideline--handle-externals ()
  "Alter external packages to work with `sideline'."
  (add-hook 'whitespace-mode-hook #'sideline-render-this)
  ;; Tree-sitter
  (add-hook 'ts-fold-on-fold-hook #'sideline-render-this)
  (add-hook 'treesit-fold-on-fold-hook #'sideline-render-this))

;;
;; (@* "Entry" )
;;

(defun sideline--enable ()
  "Enable `sideline' in current buffer."
  (setq sideline-backends-left (cl-remove-if #'null sideline-backends-left)
        sideline-backends-right (cl-remove-if #'null sideline-backends-right))
  ;; XXX: Still don't know why local variable doesn't work!
  (progn
    (sideline--delete-ovs)
    (setq-local sideline--overlays (make-hash-table)
                sideline--backend-delay-timers (make-hash-table)))
  (setq sideline--render-data-wapp (make-hash-table)
        sideline--ex-bound-or-point t  ; reset, render immediately
        sideline--text-scale-mode-amount text-scale-mode-amount)
  (add-hook 'post-command-hook #'sideline--post-command -90 t)
  (add-hook 'before-revert-hook #'sideline--before-revert nil t)
  (sideline--handle-externals)
  ;; Render immediately after reopened file!
  (sideline--post-command))

(defun sideline--disable ()
  "Disable `sideline' in current buffer."
  (remove-hook 'post-command-hook #'sideline--post-command t)
  (remove-hook 'before-revert-hook #'sideline--before-revert t)
  (sideline--reset))

;;;###autoload
(define-minor-mode sideline-mode
  "Minor mode `sideline-mode'."
  :lighter " Sideline"
  :group sideline
  (if sideline-mode (sideline--enable) (sideline--disable)))

(defun sideline--turn-on-sideline-mode ()
  "Turn on the `sideline-mode'."
  (sideline-mode 1))

;;;###autoload
(define-globalized-minor-mode global-sideline-mode
  sideline-mode sideline--turn-on-sideline-mode
  :require 'sideline)

;;
;; (@* "Util" )
;;

(defun sideline-2str (obj)
  "Convert OBJ to string."
  (format "%s" obj))

;; Copied from s.el
(defun sideline--s-replace (old new s)
  "Replace OLD with NEW in S."
  (declare (pure t) (side-effect-free t))
  (replace-regexp-in-string (regexp-quote old) new s t t))

(defmacro sideline--with-buffer-window (buffer-or-name &rest body)
  "Execute the forms in BODY with BUFFER-OR-NAME temporarily current."
  (declare (indent 1) (debug t))
  `(when-let* (((buffer-live-p ,buffer-or-name))
               (window (get-buffer-window ,buffer-or-name)))
     (with-selected-window window
       (with-current-buffer ,buffer-or-name ,@body))))

(defun sideline--window-hscroll ()
  "Like function `window-hscroll' but take more stuff into account."
  (ceiling (* (window-hscroll)
              (/ 1.0 (expt text-scale-mode-step
                           text-scale-mode-amount)))))

;; TODO: Use function `string-pixel-width' after 29.1
(defun sideline--string-pixel-width (str)
  "Return the width of STR in pixels."
  (cond ((fboundp #'buffer-text-pixel-size)
         (let ((remapping-alist face-remapping-alist))
           ;; Prevent use original buffer name for minimal side-effects
           (with-current-buffer (get-buffer-create " *sideline-string-pixel-width*")
             (setq-local display-line-numbers nil)
             (delete-region (point-min) (point-max))
             (setq-local face-remapping-alist remapping-alist)
             (insert str)
             (car (buffer-text-pixel-size nil nil t)))))
        (t
         (require 'shr)
         (shr-string-pixel-width str))))

(defun sideline--to-text-width (w)
  "Convert pixel W to row/column width."
  (let ((width (window-font-width)))
    (+ (/ w width)
       (if (zerop (% w width)) 0 1))))  ; add one if exceeed

(defun sideline--str-len (str)
  "Calculate STR in pixel width."
  (sideline--to-text-width (sideline--string-pixel-width str)))

(defun sideline--kill-timer (timer)
  "Kill TIMER."
  (when (timerp timer) (cancel-timer timer)))

(defun sideline--column-to-point (column)
  "Convert COLUMN to point."
  (save-excursion (move-to-column (max column 0)) (point)))

(defun sideline--modeline-height ()
  "Return lines modeline cost."
  (ceiling (/ (float (window-mode-line-height)) (frame-char-height))))

(defun sideline--window-end ()
  "Return the accurate window end position."
  (save-excursion
    (goto-char (window-end))
    (forward-visible-line (- 0 (sideline--modeline-height)))
    (line-beginning-position)))

(defun sideline--window-width ()
  "Correct window width for sideline."
  (window-max-chars-per-line))

(defun sideline--overlays-in (prop name &optional beg end)
  "Return overlays with PROP of NAME, from region BEG to END."
  (let* ((beg (or beg (point-min)))
         (end (or end (point-max)))
         (lst)
         (ovs (overlays-in beg end)))
    (dolist (ov ovs)
      (when (eq name (overlay-get ov prop))
        (push ov lst)))
    lst))

(defun sideline--overlays-in-line ()
  "Return sideline overlays in line."
  (sideline--overlays-in 'creator 'sideline (line-beginning-position) (line-end-position)))

(defun sideline--opposing-str-len ()
  "Return opposing overlay's content length."
  (if-let* ((ov (car (sideline--overlays-in-line)))  ; always return length of 1
            (str (overlay-get ov 'before-string)))
      (sideline--str-len str)
    0))  ; not found, then return 0

(defun sideline--align-right (str offset)
  "Align sideline STR from the right of the window.

Argument OFFSET is additional calculation from the right alignment."
  (let ((graphic-p (display-graphic-p))
        (fringes (window-fringes)))
    (list  ; use pixel instead of character unit
     (+ (sideline--string-pixel-width str)
        (* (window-font-width)
           (+ offset
              ;; If the sideline text is displayed without at least 1 pixel gap from the right fringe and
              ;; overflow-newline-into-fringe is not true, emacs will line wrap it.
              (if (and graphic-p
                       (> (nth 1 fringes) 0)
                       (not overflow-newline-into-fringe))
                  1
                0)
              (if graphic-p
                  ;; If right fringe deactivated add 1 offset
                  (if (= 0 (nth 1 fringes)) 1 0)
                1)
              (if (or
                   (bound-and-true-p whitespace-mode)
                   (bound-and-true-p global-whitespace-mode))
                  1
                0)))))))

(defun sideline--line-pixel-start ()
  "Return the pixel start of the line."
  (let* ((bol (line-beginning-position))
         (left (car (save-excursion
                      (goto-char bol)
                      (or (ht-get sideline--render-data-wapp bol)
                          (let ((wapp (window-absolute-pixel-position)))
                            (ht-set sideline--render-data-wapp bol wapp)
                            wapp)))))
         (left-edge (window-pixel-left)))
    (- left left-edge)))

(defun sideline--line-pixel-end ()
  "Return the pixel end of the line."
  (let* ((eol (line-end-position))
         (left (car (save-excursion
                      (goto-char eol)
                      (or (ht-get sideline--render-data-wapp eol)
                          (let ((wapp (window-absolute-pixel-position)))
                            (ht-set sideline--render-data-wapp eol wapp)
                            wapp)))))
         (left-edge (window-pixel-left)))
    (- left left-edge)))

(defun sideline--line-width ()
  "Return the width of the line."
  (let* ((start (sideline--line-pixel-start))  ; in pixel
         (end (sideline--line-pixel-end))      ; in pixel
         (len (- end start)))
    (sideline--to-text-width len)))            ; to text space

(defun sideline--exceed-display-p ()
  "Return non-nil if exceed display."
  (or sideline-force-display-if-exceeds
      sideline-truncate))

(defun sideline--calc-space (str-len on-left opposing-str-len bol)
  "Calculate space in current line.

Argument STR-LEN is the string size.  Another argument OPPOSING-STR-LEN is the
string size already occupied.

If argument ON-LEFT is non-nil, we calculate to the left side.  Otherwise,
calculate to the right side."
  ;; XXX: We add up the `str-len' with `opposing-str-len' because they are using
  ;; the same line!
  ;;
  ;; This is smart since we add up the string size before the calculation!
  (setq str-len (+ str-len opposing-str-len))
  ;; Start the calculation!
  (when-let* ((win-width (sideline--render-data :win-width))
              ((or (<= str-len win-width)
                   (sideline--exceed-display-p)))
              (column-start (sideline--render-data :hscroll))
              (pos-end (ignore-errors (sideline--line-width)))
              (pos-end (max pos-end column-start)))
    (cond
     (on-left
      (let* ((pos-first (save-excursion (back-to-indentation) (current-column)))
             ;; The variable `pos-first' cannot be lower than the `hscroll'.
             (pos-first (max pos-first column-start))
             (remain-spaces (- pos-first column-start)))
        (cond ((<= str-len remain-spaces)
               (cons column-start pos-first))
              ((= pos-first pos-end)
               (cons column-start win-width))
              ((and (sideline--exceed-display-p)
                    (< sideline--max-remain-spaces remain-spaces))
               (setq sideline--max-remain-spaces remain-spaces
                     sideline--max-remain-spaces-line (cons bol
                                                            (cons column-start pos-first)))
               nil))))  ; Ensure return `nil'.
     (t
      (let* ((column-end (+ column-start win-width))
             (remain-spaces (- column-end pos-end)))
        (cond ((<= str-len remain-spaces)
               (cons column-end pos-end))
              ((and (sideline--exceed-display-p)
                    (< sideline--max-remain-spaces remain-spaces))
               (setq sideline--max-remain-spaces remain-spaces
                     sideline--max-remain-spaces-line (cons bol
                                                            (cons column-end pos-end)))
               nil)))))))

(defun sideline--find-line (str-len on-left &optional direction exceeded)
  "Find a line where the string can be inserted.

Argument STR-LEN is the length of the message, use to calculate the alignment.

If argument ON-LEFT is non-nil, it will align to the left instead of right.

See variable `sideline-order' document string for optional argument DIRECTION
for details.

Optional argument EXCEEDED is set to non-nil when we have already searched
available lines in both directions (up & down)."
  (let ((occupied-lines (if on-left sideline--occupied-lines-left
                          sideline--occupied-lines-right))
        (going-up (eq direction 'up))
        (skip-first t)
        (break-it)
        (data))
    (save-excursion
      (while (not break-it)
        (if skip-first (setq skip-first nil)
          (forward-visible-line (if going-up -1 1)))
        ;; Reach window edge, means find nothing.
        (unless break-it
          (unless (if going-up
                      (<= (sideline--render-data :win-start) (point))
                    (<= (point) (sideline--render-data :win-end)))
            (setq break-it t)))
        ;; Reach buffer edge, means find nothing.
        (unless break-it
          (when (if going-up (bobp) (eobp))
            (setq break-it t)))
        (when-let*
            ((occ-bol (line-beginning-position))
             ((not (memq occ-bol occupied-lines)))
             (col (or (sideline--calc-space str-len on-left
                                            (sideline--opposing-str-len)
                                            occ-bol)
                      ;; Handle exceed display.
                      (when (and exceeded  ; after search on both sides.
                                 break-it  ; after reach edges.
                                 sideline--max-remain-spaces-line)
                        ;; Apply to BOL
                        (setq occ-bol (car sideline--max-remain-spaces-line))
                        (goto-char occ-bol)
                        ;; Return column data.
                        (cdr sideline--max-remain-spaces-line))))
             (pos-start (sideline--column-to-point (car col)))
             (pos-end   (sideline--column-to-point (cdr col)))
             ((or on-left
                  ;; Skip virtual line from `truncate-lines'.
                  (= pos-start pos-end))))
          (setq data (list pos-start pos-end occ-bol))
          (setq break-it t)
          (push occ-bol occupied-lines))))
    (if on-left
        (setq sideline--occupied-lines-left occupied-lines)
      (setq sideline--occupied-lines-right occupied-lines))
    (or data
        (and (not exceeded)
             (sideline--find-line str-len on-left (if going-up 'down 'up) t)))))

(defun sideline--create-keymap (action candidate)
  "Create keymap for sideline ACTION.

Argument CANDIDATE is the data for users."
  (let ((map (make-sparse-keymap)))
    (define-key map [down-mouse-1]
                (lambda ()
                  (interactive)
                  (funcall action candidate)))
    map))

;;
;; (@* "Overlays" )
;;

(defun sideline-backend-ovs (backend)
  "Return overlays for BACKEND."
  (sideline--overlays-in 'backend backend))

(defun sideline-delete-backend-ovs (backend)
  "Delete overlays from BACKEND."
  (dolist (ov (ht-get sideline--overlays backend))
    (let ((on-left (overlay-get ov 'left))
          (occ-pt (overlay-get ov 'occ-pt)))
      (if on-left
          (setq sideline--occupied-lines-left
                (delete occ-pt sideline--occupied-lines-left))
        (setq sideline--occupied-lines-right
              (delete occ-pt sideline--occupied-lines-right))))
    (delete-overlay ov))
  (ht-set sideline--overlays backend nil))

(defun sideline--reset-occupied-lines ()
  "Reset occupied lines."
  (let ((mark (list (line-beginning-position))))
    (setq sideline--occupied-lines-left
          (if sideline-backends-left-skip-current-line mark nil))
    (setq sideline--occupied-lines-right
          (if sideline-backends-right-skip-current-line mark nil))))

(defun sideline--delete-ovs ()
  "Clean up all overlays."
  (sideline--reset-occupied-lines)
  (ht-map (lambda (_key value)
            (mapc #'delete-overlay value))
          sideline--overlays)
  (ht-clear sideline--overlays)
  (ht-clear sideline--render-data-wapp))

(defun sideline--display-string (on-left backend-str candidate &optional type)
  "Return the display string to render the text correctly.

Argument ON-LEFT is used to calculate the output string.

Arguments BACKEND-STR and CANDIDATE are used to string concatenation, it
produces the result string.

Optional argument TYPE is used for recursive `outer' and `inner'."
  (cl-case (or type sideline-display-backend-type)
    (`left (concat backend-str " " candidate))
    (`right (concat candidate " " backend-str))
    (`inner (sideline--display-string on-left backend-str candidate (if on-left 'right 'left)))
    (`outer (sideline--display-string on-left backend-str candidate (if on-left 'left 'right)))))

(defun sideline--display-starting (on-left backend-str &optional type)
  "Return the starting text position to render the text correctly.

Argument ON-LEFT is used to calculate the starting text position..

Argument BACKEND-STR is used to calculate the starting text position.

Optional argument TYPE is used for recursive `outer' and `inner'."
  (cl-case (or type sideline-display-backend-type)
    (`left (1+ (length backend-str)))
    (`right 0)
    (`inner (sideline--display-starting on-left backend-str (if on-left 'right 'left)))
    (`outer (sideline--display-starting on-left backend-str (if on-left 'left 'right)))))

(defun sideline--create-ov (backend candidate action face name on-left order)
  "Create information (CANDIDATE) overlay.

Argument BACKEND is used to categorize overlays.

See function `sideline--render-candidates' document string for arguments ACTION,
FACE, NAME, ON-LEFT, and ORDER for details."
  (when-let*
      ((backend-str (format sideline-display-backend-format name))
       (text (if sideline-display-backend-name  ; this is the displayed text
                 (progn
                   (add-face-text-property 0 (length backend-str) 'sideline-backend nil backend-str)
                   (sideline--display-string on-left backend-str candidate))
               candidate))
       (len-text (length text))
       (len-cand (length candidate))
       (title
        (progn
          (unless (get-text-property 0 'face candidate)  ; If no face, we apply one
            (let ((start (if sideline-display-backend-name
                             (sideline--display-starting on-left backend-str)
                           0)))
              (add-face-text-property start (+ start len-cand) face nil text)))
          (when action  ; apply action listener
            (let ((keymap (sideline--create-keymap action candidate)))
              (add-text-properties 0 len-text `(keymap ,keymap mouse-face highlight) text)))
          (if on-left (format sideline-format-left text)
            (format sideline-format-right text))))
       (len-title (sideline--str-len title))
       (data (let ((sideline--max-remain-spaces 0)          ; Reset before use.
                   (sideline--max-remain-spaces-line nil))  ; Reset before use.
               (sideline--find-line len-title on-left order)))
       (pos-start (nth 0 data)) (pos-end (nth 1 data)) (occ-pt (nth 2 data))
       (hscroll (sideline--render-data :hscroll))
       (offset (- 0 hscroll))
       ;; Truncate
       (title
        (or (and sideline-truncate
                 (let* ((win-width (sideline--render-data :win-width))
                        (end-col (save-excursion
                                   (goto-char pos-end)
                                   (current-column)))
                        (available-space (if on-left
                                             (if (zerop end-col)
                                                 win-width
                                               (- end-col hscroll 1))
                                           (- win-width (- end-col hscroll))))
                        (suffix (copy-sequence (truncate-string-ellipsis))))
                   (when (< available-space len-title)
                     (set-text-properties 0 (length suffix)
                                          (text-properties-at (1- (length title)) title)
                                          suffix)
                     (truncate-string-to-width title available-space 0 nil
                                               suffix))))
            title))
       ;; Align left/right
       (str (concat
             (unless on-left
               (propertize " "
                           'display
                           `((space :align-to (- right ,(sideline--align-right title offset)))
                             (space :width 0))
                           `cursor t))
             title)))

    ;; Create overlay
    (let* ((len-str (length str))
           (left-empty-ln (= pos-start pos-end))
           (ov (make-overlay pos-start
                             (if left-empty-ln pos-start
                               ;; Cannot exceed `pos-end'.
                               (min pos-end (+ pos-start len-str)))
                             nil t t)))
      (cond (on-left
             (if left-empty-ln
                 (overlay-put ov 'before-string (concat (spaces-string hscroll) str))
               (overlay-put ov 'display str)
               (overlay-put ov 'invisible t)))
            (t (overlay-put ov 'before-string str)))
      (overlay-put ov 'window (selected-window))
      (overlay-put ov 'priority (if on-left sideline-priority
                                  ;; Add 1 to render on the same line!
                                  (1+ sideline-priority)))
      (overlay-put ov 'creator 'sideline)
      (overlay-put ov 'backend backend)
      (overlay-put ov 'on-left on-left)
      (overlay-put ov 'occ-pt occ-pt)
      (unless (gethash backend sideline--overlays)
        (setf (gethash backend sideline--overlays) nil))
      (push ov (gethash backend sideline--overlays)))))

;;
;; (@* "Async" )
;;

(defun sideline--guess-backend-name (backend)
  "Guess BACKEND's name."
  (let ((name (sideline-2str backend)))
    (setq name (sideline--s-replace "sideline-" "" name)
          name (sideline--s-replace "-sideline" "" name))
    name))

(defun sideline--render-candidates (candidates backend on-left order)
  "Render a list of backends (CANDIDATES).

Argument BACKEND is the backend symbol.

Argument ON-LEFT is a flag indicates rendering alignment; if it's non-nil then
we align to the left, otherwise to the right.

Argument ORDER determined the search order for going up or down."
  (let ((inhibit-field-text-motion t)
        (action (sideline--call-backend backend 'action))
        (face (or (sideline--call-backend backend 'face) 'sideline-default))
        (name (or (sideline--call-backend backend 'name)
                  (sideline--guess-backend-name backend))))
    (dolist (candidate candidates)
      (sideline--create-ov backend candidate action face name on-left order))))

;;
;; (@* "Core" )
;;

(defvar-local sideline--backend-delay-timers nil
  "Delay timer per backend.")

(defun sideline--call-backend (backend command)
  "Return BACKEND's result with COMMAND."
  (funcall backend command))

(defun sideline--render-backend (backend on-left order)
  "Render for single BACKEND.

All arguments BACKEND, ON-LEFT and ORDER are extracted from the function
`sideline--render-backends'."
  (when-let* ((candidates (sideline--call-backend backend 'candidates))
              (buffer (current-buffer)))  ; for async check
    (if (eq (car candidates) :async)
        (funcall (cdr candidates)
                 (lambda (cands &rest _)
                   (sideline--with-buffer-window buffer
                     (when sideline-mode
                       ;; XXX: The bug occurs when you have two or more indentical
                       ;; windows (same buffer and same cursor position) and
                       ;; frequently change between different buffers.
                       ;;
                       ;; To solve this, just record the "real" rendering
                       ;; buffer in async. So when you switch back to the same
                       ;; buffer, the sideline can adjust to find the correct
                       ;; buffer in the function `sideline--do-render-p' since
                       ;; it has been flag here.
                       (setq sideline--ex-window (selected-window))
                       (sideline--render-candidates cands backend on-left order)))))
      (sideline--render-candidates candidates backend on-left order))))

(defun sideline--render-backends (backends on-left)
  "Render a list of BACKENDS.

If argument ON-LEFT is non-nil, it will align to the left
instead of right."
  (dolist (data backends)
    (let ((is-cons (consp data)))
      (when-let* ((backend (if is-cons (car data) data))
                  (backend (if (symbolp backend) backend
                             (eval backend)))
                  (order (if is-cons (cdr data)  ; configured
                           ;; fallback to default
                           (if on-left sideline-order-left sideline-order-right))))
        ;; Handle backend delay here.
        (if-let* ((delay (alist-get backend sideline-backend-delays)))
            (progn
              (sideline--kill-timer (ht-get sideline--backend-delay-timers backend))
              (ht-set sideline--backend-delay-timers
                      backend
                      (run-with-idle-timer delay nil #'sideline--render-backend
                                           backend on-left order)))
          (sideline--render-backend backend on-left order))))))

(defun sideline-stop-p ()
  "Return non-nil if the sideline should not be display."
  (or (region-active-p)
      (bound-and-true-p company-pseudo-tooltip-overlay)
      (bound-and-true-p lsp-ui-peek--overlay)))

(defun sideline--render-data (prop)
  "Get render data by PROP."
  (plist-get sideline--render-data prop))

(defun sideline--render-data-update (&rest _)
  "Update the render data once."
  (setq sideline--render-data
        `( :win-end   ,(sideline--window-end)
           :win-start ,(window-start)
           :hscroll   ,(sideline--window-hscroll)
           :win-width ,(sideline--window-width))))

(defun sideline-render (&optional buffer)
  "Render sideline once in the BUFFER of WINDOW."
  (sideline--with-buffer-window (or buffer (current-buffer))
    (unless (funcall sideline-inhibit-display-function)
      (sideline--render-data-update)
      (run-hooks 'sideline-pre-render-hook)
      (sideline--render-backends sideline-backends-left t)
      (sideline--render-backends sideline-backends-right nil)
      (run-hooks 'sideline-post-render-hook))))

(defun sideline--do-render-p ()
  "Return non-nil if we should re-render sidelines in the post-command."
  (let ((bound-or-point  (or (bounds-of-thing-at-point 'symbol)
                             (point)))
        (window          (selected-window))
        (win-start       (window-start))
        (win-hscroll     (window-hscroll))
        (win-width       (window-width))
        (win-height      (window-height))
        (remapping-alist face-remapping-alist))
    (when  ; conditions allow to re-render sidelines
        (or (not (equal sideline--ex-bound-or-point       bound-or-point))
            (not (equal sideline--text-scale-mode-amount  text-scale-mode-amount))
            (not (equal sideline--ex-window               window))
            (not (equal sideline--ex-window-start         win-start))
            (not (equal sideline--ex-window-hscroll       win-hscroll))
            (not (equal sideline--ex-window-width         win-width))
            (not (equal sideline--ex-window-height        win-height))
            (not (equal sideline--ex-face-remapping-alist remapping-alist))
            sideline-render-this-command)
      ;; update
      (setq sideline--ex-bound-or-point       bound-or-point
            sideline--text-scale-mode-amount  text-scale-mode-amount
            sideline--ex-window               window
            sideline--ex-window-start         win-start
            sideline--ex-window-hscroll       win-hscroll
            sideline--ex-window-width         win-width
            sideline--ex-window-height        win-height
            sideline--ex-face-remapping-alist remapping-alist
            sideline-render-this-command      nil)
      t)))

(defun sideline--before-revert (&rest _)
  "Before revert."
  (sideline--reset))

(defvar-local sideline--delay-timer nil
  "Delay timer to render sideline.")

(defun sideline--post-command ()
  "Post command."
  (when (sideline--do-render-p)
    (sideline--delete-ovs)
    (sideline--kill-timer sideline--delay-timer)
    (setq sideline--delay-timer
          (run-with-idle-timer sideline-delay nil #'sideline-render
                               (current-buffer)))
    ;; XXX: Reset hook can potentially re-render sideline; update the render
    ;; data once to ensure display's correctioness.
    (sideline--render-data-update)
    (run-hooks 'sideline-reset-hook)))

;;;###autoload
(defun sideline-render-this (&rest _)
  "Use to force render the next post command."
  (setq sideline-render-this-command t))

(defun sideline--reset ()
  "Clean up for next use."
  (setq sideline--ex-bound-or-point nil)
  (sideline--delete-ovs))

(provide 'sideline)
;;; sideline.el ends here
