;;; emacs-modern-init.el --- Public Emacs starter setup -*- lexical-binding: t; -*-

;;; User settings
;; Fill in this block first. The rest of this file should refer to these
;; variables instead of embedding personal paths or account details directly.
(defgroup example-emacs nil
  "Public Emacs starter settings."
  :group 'convenience)

(defcustom example-user-full-name "Example User"
  "Full name used by Emacs features such as mail, Git, and Org export."
  :type 'string
  :group 'example-emacs)

(defcustom example-user-mail-address "user@example.com"
  "Email address used by Emacs features such as mail, Git, and Org export."
  :type 'string
  :group 'example-emacs)

(defcustom example-package-archive-refresh-days 3
  "Refresh package archives when they are older than this many days."
  :type 'integer
  :group 'example-emacs)

(defcustom example-fixed-font-family "D2Coding"
  "Preferred fixed-width font family."
  :type 'string
  :group 'example-emacs)

(defcustom example-fixed-font-height 120
  "Default font height for editing buffers."
  :type 'integer
  :group 'example-emacs)

(defcustom example-fixed-font-fallback-families
  '("D2Coding" "NanumGothicCoding" "Noto Sans Mono CJK KR" "DejaVu Sans Mono" "monospace")
  "Fallback fixed-width font families, checked in order."
  :type '(repeat string)
  :group 'example-emacs)

(defcustom example-default-input-method "korean-hangul"
  "Input method toggled by `example-input-toggle-key'."
  :type 'string
  :group 'example-emacs)

(defcustom example-input-toggle-key "C-\\"
  "Key used to toggle Korean/English input inside Emacs."
  :type 'string
  :group 'example-emacs)

(defcustom example-theme 'wheatgrass
  "Theme loaded by this public starter configuration."
  :type 'symbol
  :group 'example-emacs)

(defcustom example-project-search-path
  '("~/projects" "~/work" "~/documents")
  "Directories Projectile scans for projects."
  :type '(repeat directory)
  :group 'example-emacs)

(defcustom example-codex-command
  (executable-find "codex")
  "Path to the Codex CLI executable. Set this in a private local file if needed."
  :type '(choice (const :tag "Auto-detect" nil) file)
  :group 'example-emacs)

(defcustom example-html-root-dir "./html/"
  "Directory used by the sample Org publish project."
  :type 'directory
  :group 'example-emacs)

(defcustom example-plantuml-jar-path
  (or (getenv "PLANTUML_JAR") "~/plantuml.jar")
  "Path to plantuml.jar. Prefer setting PLANTUML_JAR outside this public file."
  :type 'file
  :group 'example-emacs)

(defcustom example-local-file
  (expand-file-name "local.el" user-emacs-directory)
  "Private local override file loaded at the end when it exists."
  :type 'file
  :group 'example-emacs)

;;; Identity
(setq user-full-name example-user-full-name
      user-mail-address example-user-mail-address)

;;; Package setup
(setq package-check-signature 'allow-unsigned)
(require 'package)
(setq package-archives
      '(("gnu" . "https://elpa.gnu.org/packages/")
        ("nongnu" . "https://elpa.nongnu.org/nongnu/")
        ("melpa" . "https://melpa.org/packages/")))
(setq package-archive-priorities
      '(("gnu" . 20)
        ("nongnu" . 15)
        ("melpa" . 10)))
(package-initialize)

(defun example-package-archive-stale-p ()
  "Return non-nil when local package archive metadata is missing or stale."
  (let ((archive-file (expand-file-name "archives/gnu/archive-contents" package-user-dir)))
    (or (not package-archive-contents)
        (not (file-exists-p archive-file))
        (time-less-p
         (time-add (nth 5 (file-attributes archive-file))
                   (days-to-time example-package-archive-refresh-days))
         (current-time)))))

(when (example-package-archive-stale-p)
  (package-refresh-contents))

(unless (package-installed-p 'use-package)
  (package-install 'use-package))

(require 'use-package)
(setq use-package-always-ensure t)

;;; Core editing
(setq inhibit-startup-screen t
      initial-scratch-message nil
      ring-bell-function #'ignore
      native-comp-async-report-warnings-errors 'silent
      make-backup-files t
      backup-directory-alist `(("." . ,(expand-file-name "backups/" user-emacs-directory)))
      auto-save-file-name-transforms `((".*" ,temporary-file-directory t))
      create-lockfiles nil
      require-final-newline t
      tab-width 4
      fill-column 88)

(setq-default indent-tabs-mode nil
              tab-width 4)

(prefer-coding-system 'utf-8)
(set-language-environment "UTF-8")
(setq default-input-method example-default-input-method)
(global-set-key (kbd example-input-toggle-key) #'toggle-input-method)
(global-display-line-numbers-mode 1)
(column-number-mode 1)
(show-paren-mode 1)
(global-visual-line-mode 1)
(delete-selection-mode 1)
(electric-pair-mode 1)
(save-place-mode 1)
(savehist-mode 1)
(recentf-mode 1)
(winner-mode 1)

(defun example-available-fixed-font-family ()
  "Return the first available fixed-width font family."
  (catch 'font
    (dolist (family (delete-dups
                     (append (list example-fixed-font-family)
                             example-fixed-font-fallback-families)))
      (when (member family (font-family-list))
        (throw 'font family)))
    example-fixed-font-family))

(defun example-apply-fixed-font (&optional frame)
  "Apply the preferred fixed-width font to FRAME and Org tables."
  (when frame
    (select-frame frame))
  (when (display-graphic-p)
    (let ((family (example-available-fixed-font-family)))
      (set-face-attribute 'default nil
                          :family family
                          :height example-fixed-font-height)
      (set-face-attribute 'fixed-pitch nil
                          :family family
                          :height example-fixed-font-height)
      (set-fontset-font t 'hangul
                        (font-spec :family family)
                        nil 'prepend)
      (with-eval-after-load 'org
        (set-face-attribute 'org-table nil
                            :inherit 'fixed-pitch
                            :family family
                            :height example-fixed-font-height)))))

(example-apply-fixed-font)
(add-hook 'after-make-frame-functions #'example-apply-fixed-font)
(load-theme example-theme t)

;;; Key help
(use-package which-key
  :init
  (which-key-mode 1)
  :custom
  (which-key-idle-delay 0.4))

;;; Minibuffer completion and search
(use-package vertico
  :init
  (vertico-mode 1))

(use-package orderless
  :custom
  (completion-styles '(orderless basic))
  (completion-category-defaults nil)
  (completion-category-overrides '((file (styles partial-completion)))))

(use-package marginalia
  :init
  (marginalia-mode 1))

(use-package consult
  :bind
  (("C-s" . consult-line)
   ("C-x b" . consult-buffer)
   ("C-c r" . consult-ripgrep)
   ("C-c f" . consult-find)
   ("C-c g" . consult-git-grep)
   ("C-c i" . consult-imenu)
   ("M-y" . consult-yank-pop)))

;;; Project navigation
(use-package projectile
  :init
  (projectile-mode 1)
  :bind-keymap
  ("C-c p" . projectile-command-map)
  :custom
  (projectile-project-search-path example-project-search-path)
  (projectile-completion-system 'default))

;;; File tree
(use-package treemacs
  :defer t
  :bind
  (("M-0" . treemacs-select-window)
   ("C-x t t" . treemacs)
   ("C-x t d" . treemacs-select-directory)
   ("C-x t f" . treemacs-find-file))
  :custom
  (treemacs-width 35)
  (treemacs-position 'left)
  (treemacs-follow-after-init t)
  (treemacs-show-hidden-files t)
  :config
  (treemacs-follow-mode 1)
  (treemacs-filewatch-mode 1)
  (treemacs-fringe-indicator-mode 'always)
  (when (executable-find "git")
    (treemacs-git-mode 'deferred)))

(use-package treemacs-projectile
  :after (treemacs projectile))

(use-package treemacs-icons-dired
  :hook (dired-mode . treemacs-icons-dired-enable-once))

;;; Dired
(use-package dired
  :ensure nil
  :bind (:map dired-mode-map
              ("h" . dired-up-directory)
              ("l" . dired-find-file))
  :custom
  (dired-listing-switches "-alh --group-directories-first"))

;;; Completion in buffers
(use-package corfu
  :init
  (global-corfu-mode 1)
  :custom
  (corfu-auto t)
  (corfu-auto-delay 0.15)
  (corfu-auto-prefix 2)
  (corfu-cycle t)
  (corfu-preselect 'prompt)
  :bind (:map corfu-map
              ("TAB" . corfu-next)
              ([tab] . corfu-next)
              ("S-TAB" . corfu-previous)
              ([backtab] . corfu-previous)
              ("RET" . corfu-insert)))

(use-package cape
  :init
  (add-to-list 'completion-at-point-functions #'cape-file)
  (add-to-list 'completion-at-point-functions #'cape-dabbrev))

(use-package yasnippet
  :init
  (yas-global-mode 1))

(use-package yasnippet-snippets
  :after yasnippet)

;;; LSP, diagnostics, Python
(use-package eglot
  :ensure nil
  :hook ((python-mode python-ts-mode js-mode js-ts-mode css-mode html-mode) . eglot-ensure)
  :bind (:map eglot-mode-map
              ("C-c l a" . eglot-code-actions)
              ("C-c l r" . eglot-rename)
              ("C-c l f" . eglot-format))
  :config
  (add-to-list 'eglot-server-programs
               '((python-mode python-ts-mode)
                 . ("basedpyright-langserver" "--stdio"))))

(use-package flymake
  :ensure nil
  :bind
  (("C-c ! n" . flymake-goto-next-error)
   ("C-c ! p" . flymake-goto-prev-error)
   ("C-c ! l" . flymake-show-buffer-diagnostics)))

(setq python-indent-offset 4)

(defun example-python-format-buffer ()
  "Format the current Python buffer with ruff when available."
  (interactive)
  (cond
   ((and buffer-file-name (executable-find "ruff"))
    (shell-command (format "ruff format %s" (shell-quote-argument buffer-file-name)))
    (revert-buffer t t t))
   ((bound-and-true-p eglot--managed-mode)
    (eglot-format-buffer))
   (t
    (message "No Python formatter found. Install ruff or use an LSP formatter."))))

(add-hook 'python-mode-hook
          (lambda ()
            (local-set-key (kbd "C-c C-f") #'example-python-format-buffer)))
(add-hook 'python-ts-mode-hook
          (lambda ()
            (local-set-key (kbd "C-c C-f") #'example-python-format-buffer)))

;;; Git and GitHub
(use-package magit
  :bind
  (("C-x g" . magit-status)
   ("C-c G b" . magit-blame-addition)
   ("C-c G l" . magit-log-current)))

(use-package forge
  :after magit)

;;; Shell and terminal
(use-package vterm
  :commands vterm
  :bind ("C-c '" . vterm)
  :custom
  (vterm-max-scrollback 10000))

(use-package eat
  :commands (eat eat-project)
  :bind
  (("C-c e" . eat)
   ("C-c E" . eat-project)))

(use-package compile
  :ensure nil
  :bind
  (("C-c c" . compile)
   ("C-c C-c" . recompile))
  :custom
  (compilation-scroll-output t))

;;; Optional AI helpers
(use-package gptel
  :commands (gptel gptel-send gptel-menu)
  :bind
  (("C-c q a" . gptel)
   ("C-c q s" . gptel-send)
   ("C-c q m" . gptel-menu))
  :config
  (setq gptel-default-mode 'org-mode))

(require 'term)
(require 'project)
(require 'subr-x)

(defun example-project-root ()
  "Return the current project root, falling back to `default-directory'."
  (or (when-let ((project (project-current nil)))
        (project-root project))
      (when (and (fboundp 'projectile-project-root)
                 (projectile-project-p))
        (projectile-project-root))
      default-directory))

(defun example-codex--term (args)
  "Start Codex in an Emacs terminal with ARGS."
  (unless (and example-codex-command
               (file-executable-p example-codex-command))
    (user-error "Codex CLI not found. Set `example-codex-command'"))
  (let* ((default-directory (file-name-as-directory (example-project-root)))
         (buffer (apply #'make-term "Codex" example-codex-command nil args)))
    (switch-to-buffer buffer)
    (term-char-mode)))

(defun example-codex (&optional prompt)
  "Start an interactive Codex session at the current project root."
  (interactive
   (list (read-string "Codex prompt, empty for interactive: ")))
  (let* ((root (file-name-as-directory (example-project-root)))
         (args (append (list "--cd" root "--no-alt-screen")
                       (unless (string-empty-p prompt)
                         (list prompt)))))
    (example-codex--term args)))

(defun example-codex-exec (prompt)
  "Run Codex non-interactively with PROMPT in the current project."
  (interactive "sCodex exec prompt: ")
  (unless example-codex-command
    (user-error "Codex CLI not found. Set `example-codex-command'"))
  (let* ((root (file-name-as-directory (example-project-root)))
         (command (mapconcat #'shell-quote-argument
                             (list example-codex-command "--cd" root "exec" prompt)
                             " ")))
    (compile command)))

(defun example-codex-review ()
  "Run a non-interactive Codex code review for the current project."
  (interactive)
  (unless example-codex-command
    (user-error "Codex CLI not found. Set `example-codex-command'"))
  (let* ((root (file-name-as-directory (example-project-root)))
         (command (mapconcat #'shell-quote-argument
                             (list example-codex-command "--cd" root "review")
                             " ")))
    (compile command)))

(defun example-codex-doctor ()
  "Run Codex doctor."
  (interactive)
  (unless example-codex-command
    (user-error "Codex CLI not found. Set `example-codex-command'"))
  (compile (mapconcat #'shell-quote-argument
                      (list example-codex-command "doctor")
                      " ")))

(global-set-key (kbd "C-c q c") #'example-codex)
(global-set-key (kbd "C-c q e") #'example-codex-exec)
(global-set-key (kbd "C-c q r") #'example-codex-review)
(global-set-key (kbd "C-c q d") #'example-codex-doctor)

;;; Org workflow
(use-package org
  :ensure nil
  :bind
  (("C-c l" . org-store-link)
   ("C-c a" . org-agenda)
   ("C-c x" . org-capture))
  :custom
  (org-auto-align-tags t)
  (org-export-default-language "ko")
  (org-html-doctype "html5")
  (org-todo-keywords '((sequence "TODO" "DOING" "DONE")))
  (org-confirm-babel-evaluate nil)
  (org-src-fontify-natively t)
  (org-src-tab-acts-natively t)
  :config
  (setq org-time-stamp-formats '("%Y-%m-%d %a" . "%Y-%m-%d %a %H:%M"))

  (defun example-org-disable-line-wrap ()
    "Keep Org tables and long lines on a single screen line."
    (visual-line-mode -1)
    (setq-local truncate-lines t)
    (setq-local word-wrap nil))

  (add-hook 'org-mode-hook #'example-org-disable-line-wrap)

  (defun example-org-insert-current-timestamp ()
    "Insert an active Org timestamp with current date and time."
    (interactive)
    (insert (format-time-string "<%Y-%m-%d %a %H:%M>")))

  (define-key org-mode-map (kbd "C-c .")
              #'example-org-insert-current-timestamp)

  (org-babel-do-load-languages
   'org-babel-load-languages
   '((python . t)
     (shell . t)
     (C . t)
     (emacs-lisp . t))))

(use-package htmlize)
(require 'ox-publish)
(setq org-html-htmlize-output-type 'inline-css)

(defun example-org-html-inline-code-class (html backend info)
  "Add a stable class to Org inline code in HTML exports."
  (if (org-export-derived-backend-p backend 'html)
      (replace-regexp-in-string "\\`<code>" "<code class=\"org-code\">" html t t)
    html))

(defun example-org-html-inline-verbatim-class (html backend info)
  "Add a stable class to Org inline verbatim in HTML exports."
  (if (org-export-derived-backend-p backend 'html)
      (replace-regexp-in-string "\\`<code>" "<code class=\"org-verbatim\">" html t t)
    html))

(add-to-list 'org-export-filter-code-functions
             #'example-org-html-inline-code-class)
(add-to-list 'org-export-filter-verbatim-functions
             #'example-org-html-inline-verbatim-class)

(setq org-publish-project-alist
      `(("org"
         :base-directory "./"
         :base-extension "org"
         :publishing-directory ,example-html-root-dir
         :publishing-function org-html-publish-to-html
         :htmlized-source t
         :section-numbers nil
         :html-preamble t
         :html-validation-link nil
         :preserve-breaks t
         :with-author t
         :with-email nil
         :with-sub-superscript nil)
        ("image"
         :base-directory "./image/"
         :base-extension "jpg\\|jpeg\\|gif\\|png\\|svg"
         :publishing-directory ,(concat example-html-root-dir "image/")
         :publishing-function org-publish-attachment)
        ("src"
         :base-directory "./src/"
         :base-extension "js\\|css"
         :publishing-directory ,(concat example-html-root-dir "src/")
         :publishing-function org-publish-attachment)
        ("website" :components ("org" "image" "src"))))

;;; PlantUML
(defun example-plantuml-compile ()
  "Compile the current PlantUML file to SVG."
  (interactive)
  (unless buffer-file-name
    (user-error "Current buffer is not visiting a file"))
  (let ((compile-command (concat "java -jar "
                                 (shell-quote-argument (expand-file-name example-plantuml-jar-path))
                                 " -tsvg "
                                 (shell-quote-argument buffer-file-name))))
    (compile compile-command)))

(use-package plantuml-mode
  :mode ("\\.puml\\'" "\\.plantuml\\'")
  :custom
  (plantuml-jar-path example-plantuml-jar-path)
  (plantuml-default-exec-mode 'jar)
  :bind (:map plantuml-mode-map
              ("C-c C-e" . example-plantuml-compile)))

;;; Convenience commands
(defun insert-current-file-name-at-point (&optional full-path)
  "Insert the current file name at point.
With prefix argument FULL-PATH, insert the full path instead."
  (interactive "P")
  (let* ((buffer (if (minibufferp)
                     (window-buffer (minibuffer-selected-window))
                   (current-buffer)))
         (filename (buffer-file-name buffer)))
    (if filename
        (insert (if full-path filename (file-name-base filename)))
      (error "Buffer %s is not visiting a file" (buffer-name buffer)))))

(defun insert-current-date-time ()
  "Insert the current date and time."
  (interactive)
  (insert (format-time-string "(%Y-%m-%d %H:%M) ")))

(global-set-key (kbd "C-c 1") #'insert-current-file-name-at-point)
(global-set-key (kbd "C-c 2") #'insert-current-date-time)

;;; Local private overrides
(when (file-readable-p example-local-file)
  (load example-local-file nil t))

;;; emacs-modern-init.el ends here
