;;; jao-org-notes.el --- A simple system for org note taking  -*- lexical-binding: t; -*-

;; Copyright (C) 2020, 2021  jao

;; Author: jao <mail@jao.io>
;; Keywords: tools

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; An org note per file, with consultable title and tags and a
;; backlinks approximation.

;;; Code:
(require 'org)
(require 'consult)

(defvar jao-org-notes-dir (expand-file-name "notes" org-directory))

(defun jao-org-notes--rg (str)
  `("rg" "--null" "--line-buffered" "--color=never" "--max-columns=250"
    "--no-heading" "--line-number" "--smart-case" "." "-e"
    ,(format "^(#.(title|filetags): .*)%s" str)))

(defun jao-org-notes--clean-match (m)
  (cons (format "%s %s"
                (replace-regexp-in-string "^\\./" "" (car m))
                (replace-regexp-in-string "[0-9]+:#\\+\\(file\\)?\\(title\\|tags\\):"
                                          " (\\2)" (cadr m)))
        (expand-file-name (car m) default-directory)))

(defun jao-org-notes--matches (lines)
  (mapcar (lambda (l) (jao-org-notes--clean-match (split-string l "\0" t))) lines))

(defvar jao-org-notes--grep-history nil)

(defun jao-org--grep (prompt &optional cat no-req)
  (let ((default-directory (expand-file-name (or cat "") jao-org-notes-dir)))
    (consult--read
     (consult--async-command #'jao-org-notes--rg
       (consult--async-transform jao-org-notes--matches))
     :prompt prompt
     :lookup (lambda (_ cands cand) (or (cdr (assoc cand cands)) cand))
     :initial (consult--async-split-initial "")
     :add-history (concat (consult--async-split-initial (thing-at-point 'symbol)))
     :require-match (not no-req)
     :category 'jao-org-notes-lookup
     :history '(:input jao-org-notes--grep-history))))

(defun jao-org-notes-cats ()
  (seq-difference (directory-files jao-org-notes-dir) '("." ".." "attic")))

(defun jao-org-notes--cat ()
  (let* ((cat (completing-read "Top level category: " (jao-org-notes-cats))))
    (cond ((file-exists-p (expand-file-name cat jao-org-notes-dir)) cat)
          ((yes-or-no-p "New category, create?") cat)
          (t (jao-roam--cat)))))

(defun jao-org-notes--insert-title ()
  (let* ((cat (jao-org-notes--cat))
         (title (file-name-base (jao-org--grep "Title: " cat t)))
         (title (replace-regexp-in-string "^#" "" title)))
    (when (not (string-empty-p title))
      (let* ((base (replace-regexp-in-string " +" "-" (downcase title)))
             (fname (expand-file-name (concat cat "/" base ".org")
                                      jao-org-notes-dir))
             (exists? (file-exists-p fname)))
        (find-file fname)
        (when (not exists?)
          (insert "#+title: " title "\n")
          t)))))

(defvar jao-org-notes--tags nil)
(defvar jao-org-notes-tags-cache-file "~/.emacs.d/cache/tags.eld")

(defun jao-org-notes--save-tags ()
  (with-current-buffer (find-file-noselect jao-org-notes-tags-cache-file)
    (delete-region (point-min) (point-max))
    (print jao-org-notes--tags (current-buffer))
    (let ((message-log-max nil)
          (inhibit-message t))
      (save-buffer))))

(defun jao-org-notes--read-tags-cache ()
  (let ((b (find-file-noselect jao-org-notes-tags-cache-file)))
    (with-current-buffer b (goto-char (point-min)))
    (setq jao-org-notes--tags (read b))))

(defun jao-org-notes--read-tags ()
  (unless jao-org-notes--tags (jao-org-notes--read-tags-cache))
  (let* ((tags (completing-read-multiple "Tags: " jao-org-notes--tags))
         (new-tags (seq-difference tags jao-org-notes--tags)))
    (when new-tags
      (setq jao-org--notes-tags
            (sort (append new-tags jao-org-notes--tags) #'string<))
      (jao-org-notes--save-tags))
    tags))

(defun jao-org-notes--insert-tags ()
  (insert "#+filetags: " (mapconcat #'identity (jao-org-notes--read-tags) " ") "\n"))

(defun jao-org-notes--insert-date ()
  (insert "#+date: ")
  (org-insert-time-stamp (current-time))
  (insert "\n"))

(defun jao-org-notes--template (k)
  `(,k "Note" plain (file jao-org-notes-open-or-create)
       "\n- %a\n  %i"
       :jump-to-captured t))

;;;###autoload
(defun jao-org-notes-open ()
  "Search for a note file, matching tags and titles with completion."
  (interactive)
  (when-let (f (jao-org--grep "Search notes: "))
    (find-file f)))

;;;###autoload
(defun jao-org-notes-open-or-create ()
  "Open or create a new note file, matching tags and titles with completion."
  (interactive)
  (when (jao-org-notes--insert-title)
    (jao-org-notes--insert-date)
    (jao-org-notes--insert-tags))
  (save-buffer)
  (buffer-file-name))

;;;###autoload
(defun jao-org-notes-grep (&optional initial)
  "Perform a grep search on all org notes body, via consult-ripgrep."
  (interactive)
  (consult-ripgrep jao-org-notes-dir initial))

;;;###autoload
(defun jao-org-notes-backlinks ()
  "Show a list of note files linking to the current one."
  (interactive)
  (jao-org-notes-search (concat "\\[\\[file:\\(.*/\\)?" (buffer-name))))

;;;###autoload
(defun jao-org-notes-insert-tags ()
  "Insert a list of tags at point, with completing read."
  (interactive)
  (insert (mapconcat 'identity (jao-org-notes--read-tags) " ")))

;;;###autoload
(defun jao-org-notes-insert-link ()
  "Select a note file (with completion) and insert a link to it."
  (interactive)
  (when-let (f (jao-org--grep "Notes file: "))
    (let ((rel-path (file-relative-name f default-directory))
          (title (with-current-buffer (find-file-noselect f)
                   (save-excursion
                     (goto-char (point-min))
                     (when (re-search-forward "^#\\+title: \\(.+\\)" nil t)
                       (match-string 1))))))
      (insert (format "[[file:%s][%s]]" rel-path title)))))

;;;###autoload
(defun jao-org-notes-setup (mnemonic)
  "Set up the notes system, providing a mnemonic character for its org template."
  (setq org-capture-templates
        (add-to-list 'org-capture-templates (jao-org-notes--template mnemonic)))
  (when (fboundp 'org-capture-upgrade-templates)
    (org-capture-upgrade-templates org-capture-templates)))

(provide 'jao-org-notes)
;;; jao-org-notes.el ends here