;;; rg-header.el --- Header line for rg-mode -*- lexical-binding: t; -*-

;; Copyright (C) 2018 David Landell <david.landell@sunnyhill.email>
;;
;; Author: David Landell <david.landell@sunnyhill.email>
;; URL: https://github.com/dajva/rg.el

;; 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, write to the Free Software
;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
;; 02110-1301, USA.

;;; Commentary:

;; Header line format for rg-mode result buffer.

;;; Code:

(require 'mouse)

(declare-function rg-cur-search-pattern "rg-result.el")


;; Customization
(defcustom rg-header-max-search-string-length nil
  "The max line length of header line search string item."
  :type '(choice (const :tag "Don't truncate" nil)
                 (number :tag "The max width"))
  :group 'rg)


;; Faces
(defface rg-toggle-on-face
  '((t :inherit rg-file-tag-face))
  "face for toggle \"on\" text in header."
  :group 'rg-face)

(defface rg-toggle-off-face
  '((t :inherit rg-error-face))
  "face for toggle \"off\" text in header."
  :group 'rg-face)

(defface rg-literal-face
  '((t :inherit rg-filename-face))
  "face for literal label in header."
  :group 'rg-face)

(defface rg-regexp-face
  '((t :inherit compilation-line-number))
  "face for regexp label in header."
  :group 'rg-face)


;; Defuns
(defun rg-header-render-label (labelform)
  "Return a fontified header label.
LABELFORM is either a string to render or a form where the `car' is a
conditional and the two following items are then and else specs.
Specs are lists where the the `car' is the labels string and the
`cadr' is font to use for that string."
  (list '(:propertize "[" font-lock-face (header-line bold))
        (cond
         ((stringp labelform)
          `(:propertize ,labelform font-lock-face (header-line bold)))
         ((listp labelform)
          (let* ((condition (nth 0 labelform))
                 (then (nth 1 labelform))
                 (else (nth 2 labelform)))
            `(:eval (if ,condition
                        (propertize ,(nth 0 then) 'font-lock-face '(,(nth 1 then) header-line bold))
                      (propertize ,(nth 0 else) 'font-lock-face '(,(nth 1 else) header-line bold))))))
         (t (error "Not a string or list")))
        '(:propertize "]" font-lock-face (header-line bold))
        '(": ")))

(defun rg-header-render-toggle (on)
  "Return a fontified toggle symbol.
If ON is non nil, render \"on\" string, otherwise render \"off\"
string."
  `(:eval (let* ((on ,on)
                 (value (if on "on " "off"))
                 (face (if on 'rg-toggle-on-face 'rg-toggle-off-face)))
            (propertize value 'font-lock-face `(bold ,face)))))

(defun rg-header-mouse-action (command help &rest items)
  "Add a keymap with mouse click action for COMMAND.
When hoovering HELP is shown as a tooltip.  ITEMS is the header line
items that the map will be applied to."
  (let ((map (make-sparse-keymap)))
    (define-key map
      (if (or (> emacs-major-version 30)
              (and (= emacs-major-version 30) (>= emacs-minor-version 2)))
          [header-line mouse-1]
        [header-line mouse-2])
      (lambda (click)
        (interactive "e")
        (mouse-select-window click)
        (call-interactively command)))
    `(:propertize ,items mouse-face header-line-highlight
                  help-echo ,help
                  keymap ,map)))

(defun rg-header-truncate-search-pattern (search)
  "Truncate SEARCH if it exceeds `rg-header-max-search-string-length'."
  (if (rg-header-truncates-p search)
      (truncate-string-to-width search
                                rg-header-max-search-string-length
                                0
                                nil
                                t)
    search))

(defun rg-header-truncates-p (search)
  "Verify that SEARCH would be truncated."
  (and (numberp rg-header-max-search-string-length)
       (< rg-header-max-search-string-length (length search))))

(defun rg-header-search-help ()
  "Get the search help for the current buffer."
  (let ((pattern (rg-cur-search-pattern)))

    (if (rg-header-truncates-p pattern)
        (concat "Change search string: " pattern)
      "Change search string")))

;; Use full-command here to avoid dependency on rg-search
;; struct. Should be properly fixed.
(defun rg-create-header-line (search full-command)
  "Create the header line for SEARCH.
If FULL-COMMAND specifies if the full command line search was done."
  (let ((itemspace "  "))
    (setq header-line-format
          (if full-command
              (list (rg-header-render-label "command line") "no refinement")
            (list
             (rg-header-mouse-action
              'rg-rerun-toggle-rexexp-literal "Toggle literal/regexp"
              (rg-header-render-label `((rg-search-literal ,search)
                                        ("literal" rg-literal-face)
                                        ("regexp" rg-regexp-face))))
             (rg-header-mouse-action
              'rg-rerun-change-query (rg-header-search-help)
              `(:eval (rg-header-truncate-search-pattern (rg-search-pattern ,search))))
             itemspace
             (rg-header-render-label "files")
             (rg-header-mouse-action
              'rg-rerun-change-files "Change file types"
              `(:eval (rg-search-files ,search)))
             itemspace
             (rg-header-render-label "case")
             (rg-header-mouse-action
              'rg-rerun-toggle-case "Toggle case"
              (rg-header-render-toggle
               `(not (member "-i" (rg-search-flags ,search)))))
             itemspace
             (rg-header-render-label "ign")
             (rg-header-mouse-action
              'rg-rerun-toggle-ignore "Toggle ignore"
              (rg-header-render-toggle
               `(not (member "--no-ignore" (rg-search-flags ,search)))))
             itemspace
             (rg-header-render-label "hits")
               '(:eval (format "%d" rg-hit-count)))))))

(provide 'rg-header)

;;; rg-header.el ends here
