;; -----------------------------
;; 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)

(defvar site-root
  (file-name-directory
   (or load-file-name buffer-file-name)))

(defun site-path (path)
  (expand-file-name path site-root))


(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 — edit button removed
    (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\">⤢</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\">tended</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=\"icon\" type=\"image/png\" sizes=\"48x48\" href=\"/assets/icons/icons8-z-key-64.png\" />

<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\" />
<link rel=\"stylesheet\" href=\"/assets/styles/cookbook.css\" />

<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />
<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />
<link href=\"https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@400;700&display=swap\" rel=\"stylesheet\" />

<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>
<script src=\"/assets/scripts/cookbook.js\" defer></script>

<script src=\"/assets/scripts/mermaid.min.js\"></script>

  <script>
    MathJax = {
      tex: {
        inlineMath: [['\\\\(', '\\\\)']],
        displayMath: [['\\\\[', '\\\\]']],
        processEscapes: true
      },
      options: {
        skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
      }
    };
  </script>
  <script src=\"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js\" async></script>

<style>
  #mv-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9999;align-items:center;justify-content:center;}
  #mv-overlay.open{display:flex;}
  #mv-modal{background:#fff;border:1px solid #ddd;border-radius:8px;width:90vw;max-width:1100px;height:85vh;display:flex;flex-direction:column;overflow:hidden;}
  #mv-toolbar{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid #eee;flex-shrink:0;}
  #mv-toolbar button{border:1px solid #ccc;border-radius:5px;padding:4px 12px;font-size:13px;cursor:pointer;background:none;}
  #mv-toolbar button:hover{background:#f5f5f5;}
  #mv-canvas{flex:1;overflow:hidden;position:relative;cursor:grab;user-select:none;}
  #mv-canvas.dragging{cursor:grabbing;}
  #mv-inner{position:absolute;top:0;left:0;transform-origin:0 0;padding:20px;}
  .mermaid-view-btn{display:inline-block;margin-top:6px;padding:3px 10px;font-size:12px;cursor:pointer;border:1px solid #ccc;border-radius:4px;background:none;color:inherit;}
</style>

<div id=\"mv-overlay\">
  <div id=\"mv-modal\">
    <div id=\"mv-toolbar\">
      <button onclick=\"mvZoom(-0.2)\">−</button>
      <span id=\"mv-lbl\" style=\"min-width:42px;text-align:center;font-size:13px;\">100%</span>
      <button onclick=\"mvZoom(0.2)\">+</button>
      <button onclick=\"mvReset()\">Reset</button>
      <button onclick=\"mvFit()\">Fit</button>
      <button onclick=\"mvClose()\" style=\"margin-left:auto;\">✕ Close</button>
    </div>
    <div id=\"mv-canvas\"><div id=\"mv-inner\"></div></div>
    <div style=\"font-size:12px;color:#888;padding:6px 14px;border-top:1px solid #eee;\">Scroll to zoom · Drag to pan · Esc to close</div>
  </div>
</div>

<script>
  (function(){
    var sc=1,tx=0,ty=0,drag=false,lx=0,ly=0;
    function C(){return document.getElementById('mv-canvas');}
    function N(){return document.getElementById('mv-inner');}
    function L(){return document.getElementById('mv-lbl');}
    function apply(){N().style.transform='translate('+tx+'px,'+ty+'px) scale('+sc+')';L().textContent=Math.round(sc*100)+'%';}
    window.mvZoom=function(d,cx,cy){var r=C().getBoundingClientRect(),p=sc;cx=cx!=null?cx:r.width/2;cy=cy!=null?cy:r.height/2;sc=Math.max(0.1,Math.min(10,sc+d));tx+=(cx-tx)*(1-sc/p);ty+=(cy-ty)*(1-sc/p);apply();};
    window.mvReset=function(){sc=1;tx=0;ty=0;apply();};
    window.mvFit=function(){var cr=C().getBoundingClientRect(),ir=N().getBoundingClientRect();var cw=ir.width/sc,ch=ir.height/sc;sc=Math.min(cr.width/cw,cr.height/ch)*0.9;tx=(cr.width-cw*sc)/2;ty=(cr.height-ch*sc)/2;apply();};
window.mvOpen=function(el){
  var svg = el.querySelector('svg');
  if (!svg) return;
  var clone = svg.cloneNode(true);
  var vb = clone.getAttribute('viewBox');
  if (vb) {
    var parts = vb.split(' ');
    clone.setAttribute('width', parts[2]);
    clone.setAttribute('height', parts[3]);
  }
  clone.style.maxWidth = 'none';
  clone.style.display = 'block';
  N().innerHTML = '';
  N().appendChild(clone);
  sc=1;tx=0;ty=0;apply();
  document.getElementById('mv-overlay').classList.add('open');
  setTimeout(mvFit, 100);
};
    window.mvClose=function(){document.getElementById('mv-overlay').classList.remove('open');};
    window.addEventListener('load',function(){
      C().addEventListener('wheel',function(e){e.preventDefault();var r=C().getBoundingClientRect();mvZoom(e.deltaY<0?.15:-.15,e.clientX-r.left,e.clientY-r.top);},{passive:false});
      C().addEventListener('mousedown',function(e){drag=true;lx=e.clientX;ly=e.clientY;C().classList.add('dragging');});
      window.addEventListener('mousemove',function(e){if(!drag)return;tx+=e.clientX-lx;ty+=e.clientY-ly;lx=e.clientX;ly=e.clientY;apply();});
      window.addEventListener('mouseup',function(){drag=false;C().classList.remove('dragging');});
      document.addEventListener('keydown',function(e){if(e.key==='Escape')mvClose();});
      document.getElementById('mv-overlay').addEventListener('click',function(e){if(e.target.id==='mv-overlay')mvClose();});
    });
  })();
</script>

<script>
  window.addEventListener('load', function() {
    mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
    document.querySelectorAll('.mermaid').forEach(el => {
      el.innerHTML = el.innerHTML
        .replace(/&gt;/g, '>')
        .replace(/&lt;/g, '<')
        .replace(/&amp;/g, '&');
    });
    mermaid.run({ querySelector: '.mermaid' }).then(function() {
      document.querySelectorAll('.mermaid').forEach(function(el) {
        if (el.querySelector('svg')) {
          var btn = document.createElement('button');
          btn.textContent = 'View diagram ↗';
          btn.className = 'mermaid-view-btn';
          btn.onclick = function(){ mvOpen(el); };
          el.parentNode.insertBefore(btn, el.nextSibling);
        }
      });
    });
  });
</script>
"
  )

(defvar z-preamble
  "
<div class=\"banner-header\">
  <div class=\"banner-left\">
  </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>

<div class=\"banner-actions\">
  <button id=\"theme-toggle\" aria-label=\"Toggle theme\" title=\"Toggle light/dark\"></button>
  <button id=\"close-all\">close all</button>
</div>
</div>
"
  )

(defvar z-postamble
  "<footer>
<div class=\"copyright-container\">
<div class=\"copyright\">
Copyright &copy; 2022-2026 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 z/org-html-mermaid-block (code _lang)
  "Export a mermaid src block as a mermaid div."
  (format "<div class=\"mermaid\">\n%s\n</div>" code))

;; Hook into ox-html's src block export
(defun z/org-html-src-block-mermaid (orig-fun src-block contents info)
  (let ((lang (org-element-property :language src-block)))
    (if (string= lang "mermaid")
        (let ((code (org-remove-indentation
                     (org-element-property :value src-block))))
          (format "<div class=\"mermaid\">\n%s\n</div>" code))
      (funcall orig-fun src-block contents info))))

(advice-add 'org-html-src-block :around #'z/org-html-src-block-mermaid)

(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."
  (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
      (expand-file-name ".org-id-locations"
                        (getenv "HOME")))
(setq org-roam-directory site-root)
(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 site-root)
         (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-html-mathjax-options
      '((path "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js")
        (scale 1.0)
        (align "center")
        (font "mathjax-modern")
        (overflow "overflow")
        (autonumber "AMS")
        (multlinewidth "85%")
        (tagside "right")
        (tagindent ".8em")))

(setq org-html-mathjax-template
      "<script>
MathJax = {
  tex: {
    inlineMath: [['\\\\(', '\\\\)']],
    displayMath: [['\\\\[', '\\\\]']]
  }
};
</script>
<script src=\"%PATH\" async></script>")

;; -------------------------------------------------------
;; MOV → MP4 conversion
;; Converts all .mov files found anywhere under assets/ to
;; .mp4 siblings IN THE SOURCE directory before publishing.
;; org-publish then copies both the .mov and the .mp4.
;; Skips conversion if an up-to-date .mp4 already exists.
;; -------------------------------------------------------

(defun z/convert-mov-to-mp4 (assets-dir)
  "Recursively convert every .mov under ASSETS-DIR to an .mp4 sibling.
Skips files where the .mp4 already exists and is newer than the .mov."
  (message "🎬 Scanning %s for .mov files…" assets-dir)
  (let ((mov-files
         (directory-files-recursively assets-dir "\\.mov$" nil)))
    (if (null mov-files)
        (message "   (no .mov files found)")
      (dolist (mov mov-files)
        (let* ((mp4 (concat (file-name-sans-extension mov) ".mp4"))
               (mov-mtime (float-time (nth 5 (file-attributes mov))))
               (mp4-mtime (if (file-exists-p mp4)
                              (float-time (nth 5 (file-attributes mp4)))
                            -1)))
          (if (> mp4-mtime mov-mtime)
              (message "   [skip]    %s  (mp4 is up to date)" (file-name-nondirectory mov))
            (message "   [convert] %s → %s"
                     (file-relative-name mov assets-dir)
                     (file-name-nondirectory mp4))
            (let ((exit-code
                   (call-process "ffmpeg" nil nil nil
                                 "-i"  mov
                                 "-c:v" "libx264"
                                 "-preset" "fast"
                                 "-crf" "23"
                                 "-c:a" "aac"
                                 "-b:a" "128k"
                                 "-movflags" "+faststart"
                                 "-y" mp4)))
              (if (= exit-code 0)
                  (message "   [done]    %s" (file-name-nondirectory mp4))
                (message "   [ERROR]   ffmpeg exited %d for %s"
                         exit-code (file-name-nondirectory mov))))))))))

(setq org-publish-project-alist
      `(
        ("org-roam"
         :base-directory "/home/zaine/master-folder/org_files/org_roam/"
         :base-extension "org"
         :publishing-directory "/home/zaine/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 "/home/zaine/master-folder/org_files/org_roam/assets/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|svg\\|mp4\\|mp3\\|mov"
         :publishing-directory "/home/zaine/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/refresh-org-roam ()
  "Force Org-roam and Org ID caches to refresh (needed for synced notes)."
  (message "starting to refresh...")
    ;; Update ID locations
  (org-id-update-id-locations)

  ;; Rebuild roam database
  (when (featurep 'org-roam)
    (org-roam-db-clear-all)
    (org-roam-db-sync))
  (message "✅ Org-roam DB refreshed"))

(defun z/build-full ()
  "Full rebuild: Org, assets, and search index."
  (interactive)
  (let* ((start-time (current-time)))
    (message "Starting full rebuild at %s"
             (format-time-string "%Y-%m-%d %H:%M:%S" start-time))

    (org-id-update-id-locations)

    ;; Convert any .mov files before publishing so org-publish
    ;; picks up the .mp4 siblings automatically.
    (z/convert-mov-to-mp4
     "/home/zaine/master-folder/org_files/org_roam/assets/")

    (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)

    (let ((elapsed (float-time (time-subtract (current-time) start-time))))
      (message "✅ Full rebuild complete in %.2f seconds" elapsed))))

(defun z/build-fast ()
  "Fast rebuild: Org + assets only (no JSON)."
  (interactive)

  ;; Convert MOVs in fast builds too — cheap (skips up-to-date files).
  (z/convert-mov-to-mp4
   "/home/zaine/master-folder/org_files/org_roam/assets/")

  (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)))
