;;; -*- lexical-binding: t; -*-

(require 'syd/base)
(require 'syd/leader)
(require 'syd/escape)
(require 'syd/undo-fu)



;; Vim emulation.
(use-package evil
  :preface
  (require 'undo-fu)
  (setq evil-want-minibuffer t
        evil-move-beyond-eol t
        evil-respect-visual-line-mode t
        evil-vsplit-window-right t
        evil-ex-search-vim-style-regexp t
        evil-want-Y-yank-to-eol t
        evil-want-C-u-scroll t
        evil-want-C-w-in-emacs-state t
        ;; - If non-nil: When using ex commands on a visual selection, pass the
        ;;   precise region selected to the command.
        ;; - If nil: Pass the region of /lines/ spanned by the visual selection.
        evil-ex-visual-char-range t
        evil-v$-excludes-newline t
        ;; Don't display the state in the mode line.
        evil-mode-line-format nil
        evil-normal-state-cursor 'box
        evil-emacs-state-cursor 'hbar
        evil-operator-state-cursor 'evil-half-cursor
        evil-insert-state-cursor 'bar
        evil-visual-state-cursor 'hollow
        ;; Search for symbols rather than words with '*'/'#'.
        evil-symbol-word-search t
        ;; Only do highlighting in selected window so that Emacs has less work
        ;; to do highlighting them all.
        evil-ex-interactive-search-highlight 'selected-window
        ;; It's is frustrating how innocuous "beginning of line" or "end of
        ;; line" errors will abort macros, so we suppress them:
        evil-kbd-macro-suppress-motion-error t
        evil-undo-system (cond ((featurep 'undo-tree) 'undo-tree)
                               ((featurep 'undo-fu) 'undo-fu)
                               (t 'undo-redo)))
  ;; These two are required for evil-collection.
  (setq evil-want-keybinding nil
        evil-want-integration t)

  :config
  ;; 'M-:' starts off in insert mode, yet the normal mode cursor lingers until a
  ;; refresh is forced.  Quick fix!  }:P
  (add-hook 'minibuffer-setup-hook #'evil-refresh-cursor)

  ;; Unbind 'C-k'.  Normally, it inserts digraphs; I have a compose key, and
  ;; it's strictly less useful than Emacs' native input methods.  It often gets
  ;; in the way of buffers with navigation, e.g. scrolling through shell/REPL
  ;; history, navigating Vertico completions, etc.
  (keymap-set evil-insert-state-map "C-k" nil)

  ;; In imitation of Vim's :mes[sages] command, define an Evil analogue to show
  ;; the echo area.
  (defun syd-evil-messages ()
    (interactive)
    (view-echo-area-messages)
     (with-current-buffer messages-buffer-name
       (evil-motion-state 1)))
  (evil-ex-define-cmd "mes[sages]" #'syd-evil-messages)

  ;; On ESC, remove highlighted search results.
  (defun syd-evil-nohl-h ()
    "If any Evil Ex search highlightings are active, remove them and return t.
Otherwise, nil."
    (let ((names '(evil-ex-substitute evil-ex-search)))
      (when (-any #'evil-ex-hl-active-p names)
          (prog1 t (evil-ex-nohighlight)))))
  (add-hook 'syd-escape-hook #'syd-evil-nohl-h)

  (general-def
    :states 'motion
    "/" #'evil-ex-search-forward
    "?" #'evil-ex-search-backward
    "n" #'evil-ex-search-next
    "N" #'evil-ex-search-previous
    "*" #'evil-ex-search-word-forward)

  ;; Replace word-wise motion with symbol-wise motion.
  (defalias 'forward-evil-word 'forward-evil-symbol)

  (evil-mode 1))


;;; Evil-collection

;; A large, community-sourced collection of preconfigured Evil-mode
;; integrations.
(use-package evil-collection
  :custom (evil-collection-setup-minibuffer t)
  :preface
  (defvar evil-collection-key-blacklist)
  (unless noninteractive
    (defvar syd-evil-collection-disabled-list
      '(anaconda-mode buff-menu calc comint company custom eldoc elisp-mode ert
                      free-keys helm help image indent kmacro kotlin-mode lispy outline
                      replace shortdoc simple slime tab-bar)
      "A list of `evil-collection' modules to ignore. See
`evil-collection-mode-list' for a list of available options.")

    ;; We do this ourselves.
    (defvar evil-collection-want-unimpaired-p nil)
    ;; We binds goto-reference on gD and goto-assignments on gA ourselves
    (defvar evil-collection-want-find-usages-bindings-p nil)
    ;; Reduces keybind conflicts between outline-mode and org-mode (which is
    ;; derived from outline-mode).
    (defvar evil-collection-outline-enable-in-minor-mode-p nil)
    ;; We handle loading evil-collection ourselves
    (defvar evil-collection--supported-modes nil)
    ;; This has to be defined here since evil-collection doesn't autoload its own.
    ;; It must be updated whenever evil-collection updates theirs.
    (defvar evil-collection-mode-list
      `(2048-game ag alchemist anaconda-mode apropos arc-mode atomic-chrome
        auto-package-update beginend bluetooth bm bookmark
        (buff-menu "buff-menu") bufler calc calendar cider citre cmake-mode
        color-rg comint company compile consult corfu crdt (csv "csv-mode")
        (custom cus-edit) cus-theme dape dashboard daemons deadgrep debbugs
        debug devdocs dictionary diff-hl diff-mode dired dired-sidebar
        disk-usage distel doc-view docker eat ebib ebuku edbi edebug ediff eglot
        elpaca ement explain-pause-mode eldoc elfeed elisp-mode elisp-refs
        elisp-slime-nav embark emms ,@(if (> emacs-major-version 28) '(emoji))
        epa ert eshell eval-sexp-fu evil-mc eww fanyi finder flycheck flymake
        forge free-keys geiser ggtags git-timemachine gited gnus go-mode gptel
        grep guix hackernews helm help helpful hg-histedit hungry-delete hyrolo
        ibuffer (image image-mode) image-dired image+ imenu imenu-list
        (indent "indent") indium info ivy js2-mode
        ,@(if (>= emacs-major-version 30) '(kmacro)) leetcode lispy lms log-edit
        log-view lsp-ui-imenu lua-mode kotlin-mode macrostep man
        (magit magit-repos magit-submodule) magit-repos magit-section
        magit-todos markdown-mode monky mpc mpdel mu4e mu4e-conversation neotree
        newsticker notmuch nov omnisharp org org-present org-roam osx-dictionary
        outline p4 (package-menu package) pass (pdf pdf-tools) popup proced
        prodigy profiler p-search python quickrun racer racket-describe realgud
        reftex replace restclient rg ripgrep rjsx-mode robe rtags ruby-mode
        scheme scroll-lock selectrum sh-script
        ,@(if (> emacs-major-version 27) '(shortdoc)) simple simple-mpc slime
        sly smerge-mode snake so-long speedbar tab-bar tablist tar-mode telega
        (term term ansi-term multi-term) tetris thread tide timer-list
        transmission trashed tuareg typescript-mode vc-annotate vc-dir vc-git
        vdiff vertico view vlf vterm vundo w3m wdired wgrep which-key
        with-editor woman xref xwidget yaml-mode youtube-dl zmusic
        (ztree ztree-diff)))

    (cl-defun syd-evil-collection-init (module &key disabled-modules)
      "Initialise evil-collection-MODULE.

A wrapper for `evil-collection-init' that respects a given list of disabled
modules."
      (let ((module* (or (car-safe module) module)))
        (unless (memq module* disabled-modules)
          (message "Loading evil-collection-%s%s"
                   module*
                   (if after-init-time "" " too early! }:("))
          (with-demoted-errors "error loading evil-collection: %s"
            (evil-collection-init (list module))))))

    ;; Allow binding to ESC.
    (syd-defadvice syd-evil-collection-disable-blacklist-a (fn)
      :around #'evil-collection-vterm-toggle-send-escape
      (let (evil-collection-key-blacklist)
        (funcall-interactively fn)))

    ;; These modes belong to packages that Emacs always loads at startup, causing
    ;; evil-collection and it's co-packages to all load immediately.  We avoid
    ;; this by loading them after evil-collection has first loaded...
    (with-eval-after-load 'evil-collection
      ;; Don't let evil-collection interfere with certain keys
      (setq evil-collection-key-blacklist
            (append (list syd-leader-key syd-localleader-key
                          syd-alt-leader-key)
                    evil-collection-key-blacklist
                    ;; Reserved for goto definition; lookup docs; eval; eval
                    ;; buffer; movement prefix; movement prefix; escaping };).
                    '("gd" "K" "gr" "gR" "[" "]" "<escape>")))

      (mapc #'syd-evil-collection-init '(comint custom))

      (with-eval-after-load 'evil
        ;; Emacs loads these two packages immediately, at startup, which needlessly
        ;; convolutes load order for evil-collection-help.
        (with-transient-after 'help-mode
          (syd-evil-collection-init 'help))
        (with-transient-after 'Buffer-menu-mode
          (syd-evil-collection-init '(buff-menu "buff-menu")))
        (with-transient-after 'calc-mode
          (syd-evil-collection-init 'calc))
        (with-transient-after 'image-mode
          (syd-evil-collection-init 'image))
        (with-transient-after 'emacs-lisp-mode
          (syd-evil-collection-init 'elisp-mode))
        (with-transient-after 'occur-mode
          (syd-evil-collection-init 'replace))
        (with-transient-after 'indent-rigidly
          (syd-evil-collection-init '(indent "indent")))
        (when (>= emacs-major-version 30)
          (with-transient-after 'kmacro-menu-mode
            (syd-evil-collection-init 'kmacro)))
        (with-transient-after 'minibuffer-setup-hook
          (when evil-collection-setup-minibuffer
            (syd-evil-collection-init 'minibuffer)
            (evil-collection-minibuffer-insert)))
        (with-transient-after 'process-menu-mode
          (syd-evil-collection-init '(process-menu simple)))
        (with-transient-after 'shortdoc-mode
          (syd-evil-collection-init 'shortdoc))
        (with-transient-after 'tabulated-list-mode
          (syd-evil-collection-init 'tabulated-list))
        (with-transient-after 'tab-bar-mode
          (syd-evil-collection-init 'tab-bar))

        ;; HACK: Do this ourselves because evil-collection break's
        ;; `eval-after-load' load order by loading their target plugin before
        ;; applying keys. This makes it hard for end-users to overwrite these
        ;; keybinds with a simple `after!' or `with-eval-after-load'.
        (dolist (mode evil-collection-mode-list)
          (dolist (req (or (cdr-safe mode) (list mode)))
            (with-eval-after-load req
              (syd-evil-collection-init
               mode
               :disabled-modules syd-evil-collection-disabled-list))))))))


;;; Surround.vim

;; Tim Pope's famous `surround.vim' for Evil.
(use-package evil-surround
  :commands (global-evil-surround-mode
             evil-surround-edit
             evil-Surround-edit
             evil-surround-region)
  :hook (on-first-input . global-evil-surround-mode))


;;; Escape with 'jk'

;; TODO: I'd like JK to escape visual state.  evil-escape only allows defining a
;; single key sequence.  Perhaps key-chord is capable of this?
(use-package evil-escape
  :hook (on-first-input . evil-escape-mode)
  :custom ((evil-escape-key-sequence "jk")
           (evil-escape-excluded-states '(normal visual multiedit emacs motion))
           (evil-escape-delay 0.15)))


;;; Commentary.vim

;; `evil-nerd-commenter' has a bunch of cool functions[1].  Here, only the Evil
;; operator is used. }:3
;; [1]: https://github.com/redguardtoo/evil-nerd-commenter?tab=readme-ov-file#commands-and-hotkeys
(use-package evil-nerd-commenter
  :commands (evilnc-comment-operator
             evilnc-inner-comment
             evilnc-outer-commenter)
  :defer t
  :bind (:map evil-normal-state-map ("#" . evilnc-comment-operator)
         :map evil-visual-state-map ("#" . evilnc-comment-operator)
         :map evil-inner-text-objects-map ("c" . evilnc-inner-comment)
         :map evil-outer-text-objects-map ("c" . evilnc-outer-comment)))

;; Enhance `evil-surround' with integration with `embrace'.
(use-package evil-embrace
  :disabled
  :after evil-surround
  :config
  (evil-embrace-enable-evil-surround-integration))


;;; Swap the contents of two given regions

;; Provides an Evil operator to swap two spans of text.
(use-package evil-exchange
  :bind (:map evil-normal-state-map ("gX" . evil-exchange)
         :map evil-visual-state-map ("gX" . evil-exchange)))


;;; Vim's 'C-a' / 'C-x' (increment/decrement number at point)

;; Evil doesn't ship with support for Vim's 'g-'/'g+'.  `evil-numbers'
;; implements this.
(use-package evil-numbers
  ;; 'g=' is a bit more comfortable than 'g+', whilst preserving the analogy.
  ;; ('=' is '+' modulo shift)
  :bind (:map evil-normal-state-map ("g=" . 'evil-numbers/inc-at-pt)
         :map evil-normal-state-map ("g-" . 'evil-numbers/dec-at-pt))
  :defer t)



;; Tree-sitter queries → Evil text objects.
(use-package evil-textobj-tree-sitter
  :defer t)


;;; Visually "flash" the region acted upon by Evil-mode operations

(use-package evil-goggles
  :hook (on-first-input . evil-goggles-mode)
  ;; The flash animation will delay actions, which can be very annoying for some
  ;; operations.  Disable `evil-goggles' for those ones.
  :custom
  ((evil-goggles-enable-delete nil)
   (evil-goggles-enable-change nil)
   (evil-goggles-duration 0.1)))


;;; Fix cursor

;; Change cursor shape and color by evil state in terminal.
(use-package evil-terminal-cursor-changer
  ;; This package is only useful in the terminal.
  :if (not (display-graphic-p))
  :defer t
  :hook (on-first-input . evil-terminal-cursor-changer-activate))


;;; Automatic alignment in region, by regexp.

(use-package evil-lion
  :hook (on-first-input . evil-lion-mode))


;;; "Entire buffer" text object

;; 'g' text object selecting the entire visible buffer.
(with-eval-after-load 'evil
  (evil-define-text-object
    evil-entire-buffer (count &optional beg end type)
    "Select entire buffer"
    (evil-range (point-min) (point-max) type))
  (define-key evil-inner-text-objects-map "g" #'evil-entire-buffer)
  (define-key evil-outer-text-objects-map "g" #'evil-entire-buffer))


;;; Enhanced 'f'/'t'

(use-package evil-snipe
  :commands (evil-snipe-local-mode evil-snipe-override-local-mode)
  :hook ((on-first-input . evil-snipe-override-mode))
  :custom ((evil-snipe-smart-case t)
           (evil-snipe-scope 'visible)
           (evil-snipe-repeat-scope 'visible)
           (evil-snipe-char-fold t)))


;;; Fix visual-state '#' / '*' to use region

;; Evil's default behaviour for '#' / '*' in visual state will remain in visual
;; mode, and jump to the next occurence of the symbol under point.  That is, the
;; movement is exactly the same as it is in normal state; if the region is over
;; the text `two words`, but the point is over `two`, Evil will search for
;; `two`.  `evil-visualstar' will instead search for `two words`.
(use-package evil-visualstar
  :defer t
  :bind (:map evil-visual-state-map
         ("*" . evil-visualstar/begin-search-forward)))


;;; Miscellaneous keybinds

;; Up/down with 'C-k'/'C-j'
(general-def
  :keymaps 'evil-ex-completion-map
  "C-k" #'previous-history-element
  "C-j" #'next-history-element)

(defun syd-insert-newline-above (count)
  "Insert a blank line below the current line."
  (interactive "p")
  (dotimes (_ count)
    (let ((point-was-at-bol-p (= (current-column) 0)))
      (save-excursion
        (evil-insert-newline-above))
      ;; Special case: with `syd-insert-newline-above' is called with point at
      ;; BOL, the point unexpectedly fails to "stick" to its original position.
      (when point-was-at-bol-p
        (next-line)))))

;;;###autoload
(defun syd-insert-newline-below (count)
  "Insert a blank line below the current line."
  (interactive "p")
  (dotimes (_ count)
    (save-excursion (evil-insert-newline-below))))

(general-def
  :states 'motion
  "[ b" #'previous-buffer
  "] b" #'next-buffer
  "] SPC" #'syd-insert-newline-below
  "[ SPC" #'syd-insert-newline-above)

(general-def
  :keymaps 'global-map
  :states 'motion
  "M-:" #'eval-expression
  "M-x" #'execute-extended-command)


;;; Save the last 'M-x eval-expression' input to an Evil register.

(defvar syd-last-eval-expression-register ?e
  "An Evil-mode register to which the last expression evaluated with an
interactive call to `eval-expression' is saved.  Note that the register is only
set by interactive usage of `eval-expression'.")

(defvar syd-last-eval-result-register ?r
  "An Evil-mode register to which the result of the last `eval-expression'
invocation is saved.  Note that the register is only set by interactive usage of
`eval-expression'.")

(with-eval-after-load 'evil
  (syd-defadvice syd--set-eval-expression-register-a (fn expr &rest args)
    :around #'eval-expression
    "When `eval-expression' is called interactively, update the registers
`syd-last-eval-expression-register' and `syd-last-eval-result-register'."
    (let ((r (apply fn expr args)))
      (when (called-interactively-p 'interactive)
        (evil-set-register syd-last-eval-expression-register
                           (->> (pp-to-string expr)
                                (string-remove-suffix "\n")))
        (evil-set-register syd-last-eval-result-register
                           (->> (pp-to-string r)
                                (string-remove-suffix "\n"))))
      r)))



;; HACK: '=' unpredictably moves the cursor when it really doesn't need to.
(defun syd-evil-dont-move-point-a (fn &rest args)
  "Used as :around advice on Evil operators to avoid moving the point."
  ;; We don't use `save-excursion', as we /only/ want to restore the point.
  (save-excursion (apply fn args)))

(with-eval-after-load 'evil
  (advice-add #'evil-indent
              :around #'syd-evil-dont-move-point-a))



(use-package evil-leap
  :straight (:type git
             :host gitlab
             :repo "msyds/evil-leap")
  :general (:states '(motion normal)
            "s" #'evil-leap-forward
            "S" #'evil-leap-backward))



(provide 'syd/evil)
