;;; ~/.emacs -*- lexical-binding: t; -*-
;; Author Case Duckworth <acdw@acdw.net>
;; Bankruptcy: 12

;;; Initialization -- see also ~/.emacs.d/early-init.el

(setopt custom-file (locate-user-emacs-file "custom.el"))
(load custom-file :no-error)

(defvar private-file (locate-user-emacs-file "private.el")
  "Private customizations")
(load private-file :no-error)           ; might as well do this now

;; (load-theme 'modus-operandi :no-confirm)

;;; Custom functions

(define-advice startup-echo-area-message (:override ())
  (if (get-buffer "*Warnings*")
      ";_;"
    "^_^"))

(defun create-missing-directories ()
  "Automatically create missing directories."
  (let ((target-dir (file-name-directory buffer-file-name)))
    (unless (file-exists-p target-dir)
      (make-directory target-dir :parents))))

(defun delete-trailing-whitespace-except-current-line ()
  (save-excursion
    (delete-trailing-whitespace (point-min)
                                (line-beginning-position))
    (delete-trailing-whitespace (line-end-position)
                                (point-max))))

(defun run-after-frame-init (func)
  "Run FUNC after the first frame is initialized.
If already so, run FUNC immediately."
  (cond
   ((daemonp)
    (add-hook 'server-after-make-frame-hook func)
    (advice-add func :after (lambda ()
                              (remove-hook 'server-after-make-frame-hook
                                           func)
                              (advice-remove func
                                             'after-frame-init-removing-advice))


                '((name . after-frame-init-removing-advice))))
   ((not after-init-time)
    (add-hook 'after-init-hook func))
   (:else (funcall func))))

(defun first-found-font (&rest cands)
  "Return the first font of CANDS that is installed, or nil."
  (cl-loop with ffl = (font-family-list)
           for font in cands
           if (member font ffl)
           return font))

(defun setup-faces ()
  "Setup Emacs faces."
  (set-face-attribute 'variable-pitch nil
                      :family (first-found-font "Public Sans")
                      :height 1.0)
  (set-face-attribute 'fixed-pitch nil
                      :family (first-found-font "Recursive Mono Linear Static"))
  (set-face-attribute 'fixed-pitch-serif nil
                      :family (first-found-font "Go Mono"
                                                "DejaVu Sans Mono"))

  ;; Emojis
  (cl-loop with ffl = (font-family-list)
           for font in '("Noto Emoji" "Noto Color Emoji"
                         "Segoe UI Emoji" "Apple Color Emoji"
                         "FreeSans" "FreeMono" "FreeSerif"
                         "Unifont" "Symbola")
           if (member font ffl)
           do (set-fontset-font t 'symbol font))

  ;; International fonts
  (cl-loop with ffl = (font-family-list)
           for (charset . font)
           in '((latin . "Noto Sans")
                (han . "Noto Sans CJK SC Regular")
                (kana . "Noto Sans CJK JP Regular")
                (hangul . "Noto Sans CJK KR Regular")
                (cjk-misc . "Noto Sans CJK KR Regular")
                (khmer . "Noto Sans Khmer")
                (lao . "Noto Sans Lao")
                (burmese . "Noto Sans Myanmar")
                (thai . "Noto Sans Thai")
                (ethiopic . "Noto Sans Ethiopic")
                (hebrew . "Noto Sans Hebrew")
                (arabic . "Noto Sans Arabic")
                (gujarati . "Noto Sans Gujarati")
                (devanagari . "Noto Sans Devanagari")
                (kannada . "Noto Sans Kannada")
                (malayalam . "Noto Sans Malayalam")
                (oriya . "Noto Sans Oriya")
                (sinhala . "Noto Sans Sinhala")
                (tamil . "Noto Sans Tamil")
                (telugu . "Noto Sans Telugu")
                (tibetan . "Noto Sans Tibetan"))
           if (member font ffl)
           do (set-fontset-font t charset font)))

(defmacro inhibit-messages (&rest body)
  "Inhibit all messages in BODY."
  (declare (indent defun))
  `(cl-letf (((symbol-function 'message) #'ignore))
     ,@body))

(defun kill-buffer-dwim (&optional buffer-or-name)
  "Kill BUFFER-OR-NAME or the current buffer."
  (interactive "P")
  (cond
   ((bufferp buffer-or-name)
    (kill-buffer buffer-or-name))
   ((null buffer-or-name)
    (kill-current-buffer))
   (:else
    (kill-buffer (read-buffer "Kill: " nil :require-match)))))

(defun other-window-dwim (&optional arg)
  "Switch to another window/buffer.
Calls `other-window', which see, unless
- the current window is alone on its frame
- `other-window-dwim' is called with \\[universal-argument]
In these cases, switch to the last-used buffer."
  (interactive "P")
  (if (or arg (one-window-p))
      (switch-to-buffer (other-buffer) nil t)
    (other-window 1)))

(defun delete-window-dwim ()
  "Delete the current window or bury its buffer.
If the current window is alone in its frame, bury the buffer
instead."
  (interactive)
  (unless (ignore-errors (delete-window) t)
    (bury-buffer)))

(defun cycle-spacing* (&optional n)
  "Negate N argument on `cycle-spacing'."
  (interactive "*p")
  (cycle-spacing (- n)))

(defun find-user-init-file (&optional arg)
  "Edit `user-init-file' in current window.
With ARG, edit in other window."
  (interactive "P")
  (funcall (if arg #'find-file-other-window #'find-file)
           user-init-file))

(defun find-user-private-file (&optional arg)
  "Edit `private-file'.
With ARG, edit in other window."
  (interactive "P")
  (funcall (if arg #'find-file-other-window #'find-file)
           private-file))

(defun package-ensure (pkg)
  "Install PKG if it's not already installed."
  (unless (package-installed-p pkg)
    (package-install pkg)))

(defun minibuffer-delete-directory ()
  (interactive)
  (let ((here (point))
        (meta (completion-metadata
               "" minibuffer-completion-table
               minibuffer-completion-predicate)))
    (if (eq (completion-metadata-get meta 'category) 'file)
        (when (search-backward "/" (minibuffer-prompt-end) t)
          (delete-region (point) here))
      (backward-kill-word 1))))

;;; Basic settings

(recancel-colors)
(tooltip-mode -1)

;; Dialogs
(setopt use-dialog-box nil)
(setopt use-file-dialog nil)
(setopt read-answer-short t)
(setopt use-short-answers t)
(setopt echo-keystrokes 0.01)

;; Cursor
(blink-cursor-mode -1)

;; Fonts
(setopt x-underline-at-descent-line t)
(run-after-frame-init #'setup-faces)

;;; Look and feel

;; Whitespace
(setopt whitespace-style '(face trailing tabs tab-mark))
(setopt whitespace-global-modes '(not rcirc-mode))
(global-whitespace-mode)
(add-hook 'before-save-hook #'delete-trailing-whitespace-except-current-line)
(set-face-attribute 'whitespace-tab nil :background nil :foreground "#888")
(setf (alist-get 'tab-mark whitespace-display-mappings)
      '(9 [?· 9] [?» 9] [?\\ 9]))

;;; Completions

(setopt tab-always-indent 'complete)
(setopt completion-styles '(basic partial-completion substring flex))

(setopt completion-ignore-case t)
(setopt read-buffer-completion-ignore-case t)
(setopt read-file-name-completion-ignore-case t)
(setopt completion-flex-nospace t)

(setopt completion-show-help nil)
(setopt completions-detailed t)
(setopt completions-group t)
(setopt completion-auto-help 'visible)
(setopt completion-auto-select 'second-tab)
(setopt completions-header-format nil)
(setopt completions-format 'one-column)
(setopt completions-max-height 10)

(keymap-set minibuffer-local-map "C-p" #'minibuffer-previous-completion)
(keymap-set minibuffer-local-map "C-n" #'minibuffer-next-completion)
(keymap-set minibuffer-local-map "M-DEL" #'minibuffer-delete-directory)

(setopt enable-recursive-minibuffers t)
(setopt minibuffer-default-prompt-format " [%s]")
(minibuffer-depth-indicate-mode)
(minibuffer-electric-default-mode)

(setopt file-name-shadow-properties '(invisible t intangible t))
(file-name-shadow-mode)

(setopt history-length t)
(setopt history-delete-duplicates t)
(setopt savehist-save-minibuffer-history t)
(setopt savehist-autosave-interval 5)
(savehist-mode)

(define-minor-mode truncate-lines-local-mode
  "Toggle `truncate-lines' in the current buffer."
  :lighter ""
  (setq-local truncate-lines truncate-lines-local-mode))

(add-hook 'completion-list-mode-hook #'truncate-lines-local-mode)
(add-hook 'minibuffer-setup-hook #'truncate-lines-local-mode)

;; Consult/Marginalia

(package-ensure 'consult)
(require 'consult)
(keymap-global-set "C-x b" #'consult-buffer)
(keymap-global-set "C-x 4 b" #'consult-buffer-other-window)
(keymap-global-set "C-x 5 b" #'consult-buffer-other-frame)
(keymap-global-set "C-x r b" #'consult-bookmark)
(keymap-global-set "M-y" #'consult-yank-pop)
(keymap-global-set "M-g g" #'consult-goto-line)
(keymap-global-set "M-g M-g" #'consult-goto-line)
(keymap-global-set "M-g o" #'consult-outline)
(keymap-global-set "M-g m" #'consult-mark)
(keymap-global-set "M-g i" #'consult-imenu)
(keymap-global-set "M-s d" #'consult-find)
(keymap-global-set "M-s D" #'consult-locate)
(keymap-global-set "M-s l" #'consult-line)
(keymap-global-set "M-s k" #'consult-keep-lines)
(keymap-global-set "M-s u" #'consult-focus-lines)
(keymap-global-set "M-s e" #'consult-isearch-history)
(keymap-set isearch-mode-map "M-e" #'consult-isearch-history)
(keymap-set isearch-mode-map "M-s e" #'consult-isearch-history)
(keymap-set isearch-mode-map "M-s l" #'consult-line)
(setopt xref-show-xrefs-function #'consult-xref)
(setopt xref-show-definitions-function
        #'xref-show-definitions-completing-read)
(setopt consult-preview-key "M-.")

(package-ensure 'marginalia)
(marginalia-mode)

;;; Frames / Windows

(winner-mode)

;;; Files

(setopt auto-revert-verbose nil)
(setopt global-auto-revert-non-file-buffers t)
(global-auto-revert-mode)

(setopt create-lockfiles nil)
(setopt mode-require-final-newline t)
(setopt view-read-only t)
(setopt save-silently t)
(setopt delete-by-moving-to-trash t)
(setopt auto-save-default nil)
(setopt auto-save-no-message t)
(setopt auto-save-interval 2)
(setopt auto-save-timeout 2)
(setopt auto-save-visited-interval 5)
(setopt remote-file-name-inhibit-auto-save t)
(setopt remote-file-name-inhibit-auto-save-visited t)
(add-to-list 'auto-save-file-name-transforms
             `(".*" ,(locate-user-emacs-file "auto-save/") t))
(auto-save-visited-mode)

(add-function :after after-focus-change-function
              (defun focus-out-save ()
                (save-some-buffers t)))

(setopt backup-by-copying t)
(setopt version-control t)
(setopt kept-new-versions 3)
(setopt kept-old-versions 3)
(setopt delete-old-versions t)
(add-to-list 'backup-directory-alist '("^/dev/shm/" . nil))
(add-to-list 'backup-directory-alist '("^/tmp/" . nil))
(when-let ((xrd (getenv "XDG_RUNTIME_DIR")))
  (add-to-list 'backup-directory-alist (cons xrd nil)))
(add-to-list 'backup-directory-alist
             (cons "." (locate-user-emacs-file "backup/"))
             :append)

(setopt recentf-max-menu-items 100)
(setopt recentf-max-saved-items nil)
(setopt recentf-case-fold-search t)
(with-eval-after-load 'recentf
  (add-to-list 'recentf-exclude "-autoloads.el\\'"))
(add-hook 'buffer-list-update-hook #'recentf-track-opened-file)
(add-hook 'after-save-hook #'recentf-save-list)
(recentf-mode)

(setopt save-place-forget-unreadable-files (eq system-type 'gnu/linux))
(save-place-mode)

(add-hook 'find-file-not-found-functions #'create-missing-directories)

;;; Buffers

;; Unique names
(setopt uniquify-buffer-name-style 'forward)

;; Persistent undo
(package-ensure 'undo-fu-session)
(setopt undo-fu-session-incompatible-files '("/COMMIT_EDITMSG\\'"
                                             "/git-rebase-todo\\'"))
(undo-fu-session-global-mode)

;; Encodings
(set-language-environment "UTF-8")
(setopt buffer-file-coding-system 'utf-8-unix)
(setopt coding-system-for-read 'utf-8-unix)
(setopt coding-system-for-write 'utf-8-unix)
(setopt default-process-coding-system '(utf-8-unix . utf-8-unix))
(setopt locale-coding-system 'utf-8-unix)
(set-charset-priority 'unicode)
(prefer-coding-system 'utf-8-unix)
(set-default-coding-systems 'utf-8-unix)
(set-terminal-coding-system 'utf-8-unix)
(set-keyboard-coding-system 'utf-8-unix)
(pcase system-type
  ((or 'ms-dos 'windows-nt)
   (set-clipboard-coding-system 'utf-16-le)
   (set-selection-coding-system 'utf-16-le))
  (_
   (set-selection-coding-system 'utf-8)
   (set-clipboard-coding-system 'utf-8)))

;;; Search

(setopt isearch-lazy-count t)
(setopt isearch-regexp-lax-whitespace t)
(setopt isearch-wrap-pause 'no)
(setopt search-whitespace-regexp ".*?") ; swiper-style
(setopt search-ring-max 256)
(setopt regexp-search-ring-max 256)

(define-advice isearch-cancel (:before () add-to-history)
  "Add search string to history when canceling isearch."
  (unless (string-equal "" isearch-string)
    (isearch-update-ring isearch-string isearch-regexp)))

(package-ensure 'isearch-mb)
(with-eval-after-load 'isearch-mb
  (with-eval-after-load 'consult
    (add-to-list 'isearch-mb--with-buffer #'consult-isearch-history)
    (keymap-set isearch-mb-minibuffer-map "M-r" #'consult-isearch-history)
    (add-to-list 'isearch-mb--after-exit #'consult-line)
    (keymap-set isearch-mb-minibuffer-map "M-s l" #'consult-line)))
(isearch-mb-mode)

;; Default to regexen
(setopt search-default-mode t)          ; Isearch
(keymap-global-set "M-%" #'query-replace-regexp)
(keymap-global-set "C-M-%" #'query-replace)

;;; Keybinds

(keymap-global-set "C-x C-c" #'save-buffers-kill-terminal)
(keymap-global-set "C-x C-k" #'kill-buffer-dwim)
(keymap-global-set "M-o" #'other-window-dwim)
(keymap-global-set "C-x o" #'other-window-dwim)
(keymap-global-set "C-x 0" #'delete-window-dwim)
(keymap-global-set "M-SPC" #'cycle-spacing*)
(keymap-global-set "C-x C-b" #'ibuffer)
(keymap-global-set "M-/" #'hippie-expand)
(keymap-global-set "M-u" #'universal-argument)
(keymap-set universal-argument-map "M-u" #'universal-argument-more)
(keymap-global-set "C-c i" #'find-user-init-file)
(keymap-global-set "C-c p" #'find-user-private-file)
(keymap-global-set "C-c s" #'eshell)

(keymap-global-set "C-c d"
                   (defun insert-current-iso8601 ()
                     (interactive)
                     (insert (format-time-string "%FT%TZ" (current-time) t))))

(keymap-global-set "C-M-\\"
                   (defun indent-buffer ()
                     (interactive)
                     (save-mark-and-excursion
                       (indent-region (point-min) (point-max))
                       (if (apply #'derived-mode-p space-indent-modes)
                           (untabify (point-min) (point-max))
                         (tabify (point-min) (point-max))))))

(keymap-global-set "C-c t"
                   (define-keymap
                     :prefix 'toggle-map
                     "e" #'toggle-debug-on-error
                     "q" #'toggle-debug-on-quit
                     "c" #'column-number-mode
                     "l" #'line-number-mode
                     "L" #'display-line-numbers-mode
                     "t" #'truncate-lines-local-mode))

;; Un-keybinds
(keymap-global-unset "C-<wheel-down>" t)
(keymap-global-unset "C-<wheel-up>" t)
;; I only ever fat-finger this key and never want to change encoding
(keymap-global-unset "C-\\" t)
(keymap-global-unset "C-z" t)

;; Key settings
(setopt set-mark-command-repeat-pop t)

;;; Writing

(add-hook 'text-mode-hook #'visual-line-mode)
(add-hook 'text-mode-hook #'auto-fill-mode)

;;; Hungry delete
;; I was using the hungry-delete package, but it turns out I can get *most* of
;; the features with just these functions.

(defun %hungry-delete (skip-fn del-key)
  (let ((here (point)))
    (funcall skip-fn " \t")
    (if (or (= (point) here)
            (apply 'derived-mode-p
                   '(eshell-mode          ; add other modes to skip here.
                     nim-mode
                     pyton-mode)))
        (call-interactively (keymap-lookup (list (current-local-map)
                                                 (current-global-map))
                                           del-key))
      (delete-region (point) here))))

(defun hungry-delete-forward ()
  "Delete forward, hungrily."
  (interactive)
  (%hungry-delete #'skip-chars-forward "C-d"))

(defun hungry-delete-backward ()
  "Delete backward, hungrily."
  (interactive)
  (%hungry-delete #'skip-chars-backward "DEL"))

(define-minor-mode hungry-delete-mode
  "Hungrily delete stuff."
  :global t
  :lighter " h"
  :keymap (define-keymap
            "DEL" #'hungry-delete-backward
            "C-d" #'hungry-delete-forward))

(hungry-delete-mode)

;;; Programming

(add-hook 'prog-mode-hook #'electric-pair-local-mode)
(setopt tab-width 8)
(setopt sh-basic-offset tab-width)
(setopt perl-indent-level tab-width)
(setopt c-basic-offset tab-width)

;; Elisp
(defun pulse@eval (start end &rest _)
  (pulse-momentary-highlight-region start end))

(keymap-set emacs-lisp-mode-map "C-c C-c" #'eval-defun)
(keymap-set emacs-lisp-mode-map "C-c C-b"
            (defun eval-buffer@pulse () (interactive)
                   (eval-buffer)
                   (pulse@eval (point-min) (point-max))))
(advice-add 'eval-region :after #'pulse@eval)

(defvar space-indent-modes '(emacs-lisp-mode
                             lisp-interaction-mode
                             lisp-mode
                             scheme-mode
                             python-mode
                             haskell-mode
                             text-mode
                             web-mode
                             css-mode)
  "Modes to indent with spaces, not tabs.")

(defun indent-tabs-mode-maybe ()
  (setq indent-tabs-mode
        (if (apply #'derived-mode-p space-indent-modes) nil t)))
(add-hook 'prog-mode-hook #'indent-tabs-mode-maybe)

;; Makefile
(setopt makefile-backslash-align nil)
(setopt makefile-cleanup-continuations t)

(add-hook 'makefile-mode-hook
          (defun makefile-stop-complaining ()
            (remove-hook 'write-file-functions
                         'makefile-warn-suspicious-lines t)
            (remove-hook 'write-file-functions
                         'makefile-warn-continuations t)))

;; Scheme -- CHICKEN
(setopt scheme-program-name (or (executable-find "csi")))
(add-to-list 'auto-mode-alist '("\\.egg\\'" . scheme-mode))

;; Scheme Indentation
(defun scheme-module-indent (state indent-point normal-indent) 0)
(put 'module 'scheme-indent-function 'scheme-module-indent)
(put 'and-let* 'scheme-indent-function 1)
(put 'parameterize 'scheme-indent-function 1)
(put 'handle-exceptions 'scheme-indent-function 1)
(put 'when 'scheme-indent-function 1)
(put 'unless 'scheme-indent-function 1)
(put 'match 'scheme-indent-function 1)

;; Geiser
(package-ensure 'geiser)
(package-ensure 'geiser-chicken)
(setopt geiser-mode-auto-p nil)
(setopt geiser-repl-history-filename "~/.emacs.d/geiser-history")
(setopt geiser-chicken-init-file "~/.csirc")
(add-hook 'scheme-mode-hook #'geiser-mode)
(add-hook 'geiser-repl-mode-hook #'electric-pair-local-mode)
(advice-add 'geiser-eval-region :after #'pulse@eval)

;; VC
(add-hook 'vc-dir-mode-hook #'hl-line-mode)
(defun vc-jump ()
  (interactive)
  (vc-dir default-directory))
(keymap-global-set "C-x v j" #'vc-jump)

;;; Compilation

(setopt compilation-always-kill t)
(setopt compilation-ask-about-save nil)

;;; Languages

(package-ensure 'gemtext-mode)

;;; Miscellaneous settings

(add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p)
(add-hook 'prog-mode-hook #'auto-fill-mode)
(add-hook 'prog-mode-hook #'electric-pair-local-mode)
(global-display-fill-column-indicator-mode)
(delete-selection-mode)
(global-so-long-mode)
(global-goto-address-mode)
(context-menu-mode)
(setopt scroll-conservatively 101)
(setopt display-fill-column-indicator-character ?·)
(setopt disabled-command-function nil)
(setopt electric-pair-skip-whitespace 'chomp)
(setopt fill-column 80)
(setopt recenter-positions '(top middle bottom))
(setopt eval-expression-print-level nil)
(setopt eval-expression-print-length nil)
(setopt show-paren-delay 0.01)
(setopt show-paren-style 'parenthesis)
(setopt show-paren-when-point-in-periphery t)
(setopt show-paren-when-point-inside-paren t)
(show-paren-mode)

(with-eval-after-load 'ibuffer
  (add-hook 'ibuffer-mode-hook #'hl-line-mode))

(with-eval-after-load 'proced
  (add-hook 'proced-mode-hook #'hl-line-mode))

;;; RCIRC

(setopt rcirc-kill-channel-buffers t)
(setopt rcirc-display-server-buffer nil)

(defun run-rcirc ()
  (interactive)
  (shell-command "chat up")
  (call-interactively #'rcirc))

(add-hook 'rcirc-mode-hook
          (defun @rcirc ()
            (rcirc-track-minor-mode)
            (rcirc-omit-mode)
            (visual-line-mode)
            (setq default-directory (expand-file-name "~"))))

;;; Eshell

(setopt eshell-banner-message
        (format "%s\n\n" (mapconcat #'identity
                                    (process-lines "fortune" "-s")
                                    "\n")))
(setopt eshell-prompt-function
        (defun @eshell-prompt ()
          (let ((rootp (zerop (user-uid))))
            (propertize
             (concat "( "
                    (abbreviate-file-name (eshell/pwd))
                    (if rootp ":root" "")
                    " ) ")
             'face 'bold))))
(setopt eshell-prompt-regexp "^(.*) ")

;;; Browsing

;; Dired (files)
(setopt dired-dwim-target t)
(setopt dired-listing-switches "-AlF")
(setopt dired-ls-F-marks-symlinks t)
(setopt dired-recursive-copies 'always)
(setopt dired-recursive-deletes 'always)
(setopt dired-auto-revert-buffer t)
(setopt dired-hide-details-hide-symlink-targets nil)
(with-eval-after-load 'dired
  (require 'dired-x)
  (add-hook 'dired-mode-hook #'dired-hide-details-mode)
  (add-hook 'dired-mode-hook #'hl-line-mode)
  (add-hook 'dired-mode-hook #'truncate-lines-local-mode)
  (keymap-set dired-mode-map "C-j" #'dired-up-directory))

;; Elpher (gemini/gopher)
(package-ensure 'elpher)

;; Browse-url (http)
(setopt browse-url-new-window-flag t)
(setopt browse-url-firefox-arguments '("--new-tab"))
(setopt browse-url-firefox-new-window-is-tab t)