;; -----------------------------
;; Package bootstrap (BATCH SAFE)
;; -----------------------------
(require 'package)

(setq package-user-dir (expand-file-name "./.packages"))
(setq package-archives
      '(("melpa" . "https://melpa.org/packages/")
        ("gnu"   . "https://elpa.gnu.org/packages/")))

(package-initialize)

(unless package-archive-contents
  (package-refresh-contents))

(unless (package-installed-p 'org-roam)
  (package-install 'org-roam))

(require 'ox-publish)
(require 'org)
(require 'ox-html)
(require 'json)
(require 'org-roam)
(require 'ob-latex)
(require 'seq)
(require 'cl-lib)

(use-package htmlize
  :ensure t)

;(setq org-export-with-broken-links t)

(setq org-id-track-globally t)
(setq org-id-locations-file
      "~/master-folder/org_files/org_roam/.org-id-locations")

(org-id-update-id-locations)

(defun z/org-html-wrap-content-for-stack (orig-fun contents info)
  "Wrap Org HTML #content in stack container, leaving postamble outside."
  (let* ((html (funcall orig-fun contents info))
         (page (file-name-base (plist-get info :output-file)))
         (input-file (plist-get info :input-file))
         
         ;; Extract planted date from filename (format: YYYYMMDDHHMMSS-title)
         (planted-date
          (if (string-match "^\\([0-9]\\{14\\}\\)" page)
              (let ((date-str (match-string 1 page)))
                (condition-case nil
                    (format-time-string "%Y-%m-%d"
                                        (encode-time
                                         (string-to-number (substring date-str 12 14)) ; second
                                         (string-to-number (substring date-str 10 12)) ; minute
                                         (string-to-number (substring date-str 8 10))  ; hour
                                         (string-to-number (substring date-str 6 8))   ; day
                                         (string-to-number (substring date-str 4 6))   ; month
                                         (string-to-number (substring date-str 0 4)))) ; year
                  (error "Unknown")))
            ;; Fallback: try to get creation date from input file
            (let ((file-path (if input-file
                                 (expand-file-name input-file (plist-get info :base-directory))
                               nil)))
              (if (and file-path (file-exists-p file-path))
                  (format-time-string "%Y-%m-%d" (nth 4 (file-attributes file-path)))
                "Unknown"))))
         
         ;; Get last modified date (last tended to)
         (last-tended
          (let ((file-path (if input-file
                               (expand-file-name input-file (plist-get info :base-directory))
                             nil)))
            (if (and file-path (file-exists-p file-path))
                (format-time-string "%Y-%m-%d" (nth 5 (file-attributes file-path)))
              "Unknown")))

         (stack-close
          "</div></article></div></div>"))

    (setq html
          (replace-regexp-in-string
           "<div id=\"content\" class=\"content\">"
           (format
            "<div id=\"stack-root\">
  <div class=\"stack-track\">
    <article class=\"stack-pane pane-root\" data-url=\"/%s.html\">
      <div id=\"content\" class=\"content\">"
            page)
           html
           nil t))

    ;; Wrap title with metadata section - use lambda to properly capture title
    ;; Handle titles with nested HTML tags by matching everything between opening and closing h1 tags
    ;; Use .* to match any characters (greedy) - will match up to the last </h1> which should be our closing tag
    (setq html
          (replace-regexp-in-string
           "<h1 class=\"title\">\\(.*\\)</h1>"
           (lambda (match)
             (let ((title-text (match-string 1 match)))
               (format
                "<div class=\"title-section\">
  <div class=\"title-controls\">
    <button class=\"pane-fullscreen\" aria-label=\"Fullscreen\">F</button>
    <button class=\"pane-edit\" aria-label=\"Edit pane\">E</button>
    <button class=\"pane-close\" aria-label=\"Close pane\">×</button>
  </div>
  <h1 class=\"title\">%s</h1>
  <div class=\"title-metadata\">
    <span class=\"metadata-item\">
      <span class=\"metadata-label\">planted:</span>
      <span class=\"metadata-value\">%s</span>
    </span>
    <span class=\"metadata-item\">
      <span class=\"metadata-label\">last tended to:</span>
      <span class=\"metadata-value\">%s</span>
    </span>
  </div>
</div>"
                title-text planted-date last-tended)))
           html
           nil t))

    (setq html
          (replace-regexp-in-string
           "<div id=\"postamble\" class=\"status\">"
           (concat stack-close "\n<div id=\"postamble\" class=\"status\">")
           html
           nil t))

    html))

(advice-add 'org-html-template :around #'z/org-html-wrap-content-for-stack)

(add-to-list 'load-path default-directory)

(defvar z-shared-head
      "
<link rel=\"stylesheet\" href=\"/assets/styles/style.css\" />
<link rel=\"stylesheet\" href=\"/assets/styles/bigger-picture.min.css\" />
<link rel=\"stylesheet\" href=\"/assets/styles/media.css\" />

<script src=\"/assets/scripts/script.js\" defer></script> 
<script src=\"/assets/scripts/search.js\" defer></script> 
<script src=\"/assets/scripts/bigger-picture.min.js\" defer></script>
<script src=\"/assets/scripts/svg-pan-zoom.min.js\" defer></script>
<script src=\"/assets/scripts/gallery-init.js\" defer></script>
"
      )

(defvar z-preamble
  "
<div class=\"banner-header\">
  <div class=\"banner-left\">
    <a href=\"/20241210233721-brain_moc.html\"> <img src=\"/assets/gr.png\" alt=\"Site Logo\" class=\"banner-logo\" /> </a>

    <div id=\"updated\">Updated: %C</div>
  </div>

  <div class=\"banner-search\">
    <input id=\"search-box\"
           type=\"search\"
           placeholder=\"Search notes…\"
           aria-label=\"Search notes\"
           autocomplete=\"off\" />
    <div id=\"search-results\"></div>
  </div>

<a href=\"https://zainezq.com\"> <img src=\"/assets/2_to_1.svg\" alt=\"Web logo\" class=\"web-logo\" /> </a>
<button id=\"close-all\">Close All</button>
</div>
"
  )

(defvar z-postamble
  "<footer>
<div class=\"copyright-container\">
<div class=\"copyright\">
Copyright &copy; 2022-2025 Zaine Qayyum. All rights reserved unless otherwise noted.</div></div>
<div class=\"generated\">
Created with %c on <a href=\"https://www.archlinux.org/\">Arch</a> <a href=\"https://www.gnu.org\">GNU</a>/<a href=\"https://www.kernel.org/\">Linux</a>
</div>
</footer>")

(defvar z/build-full-rebuild t
  "If non-nil, generate expensive artifacts like search-index.json.")

;; Babel language support
(org-babel-do-load-languages
 'org-babel-load-languages
 '((latex . t)
   (org . t)))

(setq org-confirm-babel-evaluate nil     ;; Disable confirmation for code block evaluation
      org-export-with-smart-quotes t     ;; turns " into curly quotes
      ;;org-html-self-link-headlines t     ;; headlines link to self
      org-html-validation-link nil       ;; dont validate html (in order to check it is up to web standards)
      org-html-inline-images t           ;; self-expl
      org-export-with-sub-superscripts t ;; render h_x as large h with subscript x
      )
(load-file (expand-file-name "lisp/macros.el" default-directory))

;; Configure LaTeX image generation
(setq org-latex-create-formula-image-program 'imagemagick)

(defun my/get-org-roam-backlinks (filename)
  "Return backlinks for the given org-roam file. FILENAME should be the full path to the org-roam file."
  (save-excursion
    (with-current-buffer (find-file-noselect filename)
      (goto-char (point-min))
      (if (and (featurep 'org-roam) (org-roam-node-at-point))
          (let* ((node (org-roam-node-at-point))
                 (backlinks (org-roam-backlinks-get node)))
            (mapcar (lambda (backlink)
                      (let ((source-node (org-roam-backlink-source-node backlink)))
                        (list
                         :file (org-roam-node-file source-node)
                         :title (org-roam-node-title source-node)
                         :content (with-current-buffer 
                                     (find-file-noselect (org-roam-node-file source-node))
                                   (goto-char (org-roam-backlink-point backlink))
                                   (thing-at-point 'sentence t))
                         :link (format "[[id:%s][%s]]"
                                       (org-roam-node-id source-node)
                                       (org-roam-node-title source-node)))))
                    backlinks)
            )
        nil))))


(defun my/org-export-insert-backlinks (_backend)
  "Insert Org-roam backlinks before export, with HTML attributes."
  (when (and (featurep 'org-roam)
             (org-roam-node-at-point))
    (let ((backlinks (my/get-org-roam-backlinks (buffer-file-name))))
      (when backlinks
        (goto-char (point-max))
        (insert "\n#+ATTR_HTML: :class backlinks-section :id backlinks\n Backlinks\n")
        (insert "#+ATTR_HTML: :class backlinks-list\n")
        (dolist (bl backlinks)
          (insert (format "- %s\n"
                          (plist-get bl :link))))))))


(defun my/org-export-insert-backlinks (_backend)
  "Insert Org-roam backlinks before export."
  (when (and (buffer-file-name)
             (not (string-match-p "all-files\\.org$" (buffer-file-name)))
             (featurep 'org-roam)
             (org-roam-node-at-point))
    (let ((backlinks (my/get-org-roam-backlinks (buffer-file-name))))
      (when backlinks
        (goto-char (point-max))
        (insert "\n#+ATTR_HTML: :class backlinks-section :id backlinks\n")
        (insert "* Backlinks\n")
        (insert "#+ATTR_HTML: :class backlinks-list\n")
        (dolist (bl backlinks)
          (insert (format "- %s\n" (plist-get bl :link))))))))

(setq org-id-locations-file "~/.emacs.d/.org-id-locations")
(setq org-roam-directory "~/master-folder/org_files/org_roam/")
(org-id-update-id-locations)

(setq org-html-link-home "")
(setq org-html-link-up "")

(defun z/generate-all-files-org ()
  "Generate all-files.org from all-files.json."
  (let* ((base-dir (expand-file-name "~/master-folder/org_files/org_roam/"))
         (json-file (expand-file-name "all-files.json" base-dir))
         (org-file  (expand-file-name "all-files.org" base-dir)))
    (when (file-exists-p json-file)
      (message "🧩 Generating all-files.org from JSON…")
      (let* ((json-object-type 'alist)
             (json-array-type 'list)
             (json-key-type 'symbol)
             (data (json-read-file json-file))
             (groups (alist-get 'groups data)))
        (with-temp-file org-file
          (insert "#+title: All Files\n")
          (insert "#+options: toc:nil num:nil\n\n")
          (insert "#+begin_comment\n")
          (insert "This file is auto-generated. Do not edit manually.\n")
          (insert "#+end_comment\n\n")
          (dolist (group groups)
            (let ((letter (symbol-name (car group)))
                  (entries (cdr group)))
              (insert (format "* %s\n" letter))
              (dolist (entry entries)
                (let ((id (alist-get 'id entry))
                      (title (alist-get 'title entry)))
                  (insert (format "- [[id:%s][%s]]\n" id title))))
              (insert "\n")))))
      (message "✅ all-files.org generated"))))



(setq org-publish-project-alist
      `(
        ("org-roam"
         :base-directory "~/master-folder/org_files/org_roam/"
         :base-extension "org"
         :publishing-directory "~/master-folder/org_files/org_roam/output/"
         :recursive t
         :publishing-function org-html-publish-to-html
         :with-author nil
         :with-creator nil
         :with-date t
         :with-toc nil
         :section-numbers nil

         :html-preamble ,z-preamble
         :html-postamble ,z-postamble
         :html-head ,z-shared-head)

        ("org-static"
         :base-directory "~/master-folder/org_files/org_roam/assets/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|svg"
         :publishing-directory "~/master-folder/org_files/org_roam/output/assets/"
         :recursive t
         :publishing-function org-publish-attachment)))

(defun z/org-publish-quiet (project force)
  "Publish PROJECT quietly. If FORCE is non-nil, force publishing."
  (let ((inhibit-message t)
	(message-log-max nil))
	(org-publish project force)))

(defun z/build-full ()
  "Full rebuild: Org, assets, and search index."
  (interactive)
  (setq z/build-full-rebuild t)
  (z/generate-all-files-org)
  (z/org-publish-quiet "org-roam" t)
  (z/org-publish-quiet "org-static" t)
  (message "✅ Full rebuild complete"))

(defun z/build-fast ()
  "Fast rebuild: Org + assets only (no JSON)."
  (interactive)
  (setq z/build-full-rebuild nil)
  (org-publish "org-roam" t)
  (org-publish "org-static" t)
  (message "⚡ Fast rebuild complete (no search index)"))


(cond
 ((member "--fast" command-line-args-left)
  (z/build-fast))
 (t
  (z/build-full)))
