;;; -*- lexical-binding: nil; -*-
;;; howm-backend.el --- Wiki-like note-taking tool
;;; Copyright (C) 2005-2026
;;;   HIRAOKA Kazuyuki <kakkokakko@gmail.com>
;;;
;;; 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 1, 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.
;;;
;;; The GNU General Public License is available by anonymouse ftp from
;;; prep.ai.mit.edu in pub/gnu/COPYING.  Alternately, you can write to
;;; the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139,
;;; USA.
;;--------------------------------------------------------------------

(provide 'howm-backend)
(require 'howm)

;; in preparation at now.
;; many WRONG COMMENTS and TENTATIVE CODES.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; class Folder

(defun howm-folder ()
  (howm-make-folder:files (howm-search-path)))

;; * class Folder: abstraction of directory

;; (Wrong comments. Ignore me.)
;;   * grep(pattern, fixed, case_insensitive)
;;     * list of items
;;   * new_page
;;   * all_pages
;;   * all_keys
;;   * add_keys
;;   * keys_in(page)
;;     * This method is optional.

(defun howm-folder-type (folder &rest r)
  (cond ((stringp folder) ':dir)
        ((eq folder 'buf) ':buf)
        ((listp folder) (car folder))))

(howm-defvar-risky howm-folder-dispatchers (list #'howm-folder-type))

(gfunc-with howm-folder-dispatchers
  (gfunc-def howm-folder-items (folder &optional recursive-p)
    "All pages in FOLDER is returned as list of items.
When RECURSIVE-P is non-nil, pages in subfolders are also listed.")
  (gfunc-def howm-folder-grep-internal (folder pattern &optional fixed-p)
    "In FOLDER, PATTERN is searched.
Result is returned as list of items. When FIXED-P is nil, PATTERN is
regarded as regular expression.")
  ;; need to suppor below for howm-directory
  (gfunc-def howm-folder-get-page-create (folder page-name)
    "In FOLDER, get page whose name is PAGE-NAME.
If corresponding page does not exist, new page is created.
Return value is a cons pair of page and flag.
Flag is non-nil if new page is created.")
  (gfunc-def howm-folder-territory-p (folder name)
    "Non nil if FOLDER should own NAME.")
  )

;; (gfunc-def-with howm-folder-dispatchers
;;   (howm-folder-items (folder &optional recursive-p)
;;     "All pages in FOLDER is returned as list of items.
;; When RECURSIVE-P is non-nil, pages in subfolders are also listed.")
;;   (howm-folder-grep-internal (folder pattern &optional fixed-p)
;;     "In FOLDER, PATTERN is searched.
;; Result is returned as list of items. When FIXED-P is nil, PATTERN is
;; regarded as regular expression.")
;;   )

(defun howm-folder-match-under-p (dir regexp filename)
  (and (eq (howm-folder-type dir) ':dir)
       (string-match regexp (file-relative-name filename dir))))

(defun howm-make-folder-from-items (items)
  (howm-make-folder:pages (howm-cl-remove-duplicates* (mapcar #'howm-item-page
                                                              items)
                                                      :test #'howm-page=)))

;;;
;;; dir folder: single directory
;;;

(defun howm-make-folder:dir (dir)
  dir)

(defun howm-folder-items:dir (dir &optional recursive-p)
  (let ((files (if recursive-p
                    (howm-files-in-directory dir)
                 (directory-files dir t))))
    (howm-folder-items:files (howm-make-folder:files files))))

(defun howm-folder-grep-internal:dir (folder pattern &optional fixed-p)
  (howm-grep-items pattern folder fixed-p #'howm-exclude-p))

(defun howm-files-in-directory (path)
  "Return a list of files under PATH if PATH is a directory, searching
subdirectories recursively.  If PATH is a file, return a single-element
list containing it.  Files and subdirectories matching
`howm-exclude-p' are ignored, but PATH itself is always included.
This is because some users like to store their notes in ~/.howm/ but
still ignore the dot files inside it."
  (let ((full-path (expand-file-name path))
        (include-p (lambda (d) (not (howm-exclude-p d)))))
    (if (file-directory-p full-path)
        ;; comparison of two implementations [2025-09-30]
        ;; C-u 10 M-x benchmark RET (howm-list-recent) RET
        ;; <implementation A> Elapsed time: 7.524661s (1.446273s in 17 GCs)
        (seq-remove #'howm-exclude-p
                    (directory-files-recursively full-path "" nil include-p t))
        ;; ;; <implementation B> Elapsed time: 7.938591s (1.480866s in 17 GCs)
        ;; (howm-files-in-directory-sub full-path)
      (and (file-exists-p full-path) (list full-path)))))

;; (defun howm-files-in-directory-sub (dir)
;;   (let* ((fs (directory-files dir t
;;                               directory-files-no-dot-files-regexp))
;;          (filtered (cl-remove-if #'howm-exclude-p fs))
;;          (grouped (seq-group-by #'file-directory-p filtered))
;;          (files (alist-get nil grouped))
;;          (subdirs (alist-get t grouped)))
;;     (nconc files (mapcan #'howm-files-in-directory-sub subdirs))))

(defun howm-folder-get-page-create:dir (folder page-name)
  (let* ((file (expand-file-name page-name folder))
         (dir (file-name-directory file))
         (createp (not (file-exists-p file))))
    (make-directory dir t)
    (cons (howm-make-page:file file) createp)))

(defun howm-folder-territory-p:dir (folder name)
  (howm-subdirectory-p folder name))

;;;
;;; pages folder: list of 'pages'
;;;

(defun howm-make-folder:pages (pages)
  (cons ':pages pages))

(defun howm-folder-pages:pages (folder)
  (cdr folder))

(defun howm-folder-items:pages (folder &optional recursive-p)
  (let ((summary ""))
    (mapcar (lambda (p) (howm-make-item p summary))
            (howm-folder-pages:pages folder))))

;; should be removed, or renamed at least
(defun howm-folder-files:pages (folder &optional exclusion-checker)
  (remove nil (mapcar #'howm-page-name (howm-folder-pages:pages folder))))

(defun howm-folder-grep-internal:pages (folder pattern &optional fixed-p)
  (let ((h (howm-classify #'howm-page-type (howm-folder-pages:pages folder) t)))
    ;; get result for each type
    (apply #'append (mapcar (lambda (p)
                              (let ((type (car p))
                                    (searcher (cdr p)))
                                (let ((pages (reverse (cdr (assoc type h)))))
                                  (funcall searcher pages pattern fixed-p))))
                            howm-folder-grep-internal:pages-searcher))))

(howm-defvar-risky howm-folder-grep-internal:pages-searcher
  '((:file . howm-folder-grep-internal:pages-files)
    (:buf  . howm-folder-grep-internal:pages-buffers)))
(defun howm-folder-grep-internal:pages-files (pages pattern fixed-p)
  (let ((files (mapcar #'howm-page-name pages)))
    (howm-folder-grep-internal:files (howm-make-folder:files files)
                                     pattern fixed-p)))
(defun howm-folder-grep-internal:pages-buffers (pages pattern fixed-p)
  (let ((bufs pages)
        (r (howm-fake-grep-regexp pattern fixed-p))
        (c *howm-view-force-case-fold-search*))
    (let ((grep-result (cl-mapcan
                        (lambda (b)
                          (if (howm-buffer-killed-p b)
                              nil
                            (with-current-buffer b
                              (howm-fake-grep-current-buffer r b c))))
                        bufs)))
      (mapcar (lambda (g)
                (let ((buf (car g))
                      (place (cadr g))
                      (content (cl-caddr g)))
                  (howm-make-item (howm-make-page:buf buf) content place)))
              grep-result))))

(defun howm-list-buffers (&optional all)
  "Show buffer list. If ALL is non-nil, hidden buffers are also listed."
  (interactive "P")
  (let* ((bufs (if all
                   (buffer-list)
                 (cl-remove-if
                  (lambda (b)
                    (let ((name (buffer-name b)))
                      (or (null name)
                          (string-match "^ " name)
                          (member name howm-list-buffers-exclude)
                          (with-current-buffer b
                            (member major-mode
                                    '(howm-view-summary-mode
                                      howm-view-contents-mode))))))
                  (buffer-list))))
         (pages (mapcar (lambda (b) (howm-make-page:buf b)) bufs))
         (folder (howm-make-folder:pages pages)))
    (howm-view-directory folder)))
(defun howm-occur (regexp)
  "Show all lines in the current buffer containing a match for REGEXP."
  (interactive "sSearch (regexp): ")
  (let ((howm-view-use-grep (if howm-occur-force-fake-grep
                                nil
                              howm-view-use-grep)))
    (howm-view-search-folder regexp
                             (howm-make-folder:pages
                              (list (howm-make-page:buf (current-buffer)))))))
(defun howm-list-mark-ring ()
  "Show all marks in the current buffer."
  (interactive)
  (let* ((page (howm-make-page:buf (current-buffer)))
         (items (mapcar (lambda (m)
                          (let ((place (riffle-get-place m))
                                (summary (save-excursion
                                           (goto-char m)
                                           (let ((b (line-beginning-position))
                                                 (e (line-end-position)))
                                             (buffer-substring b e)))))
                            (howm-make-item page summary place)))
                        (howm-cl-remove-duplicates*
                         (cons (mark-marker) mark-ring)
                         :test #'howm-mark-same-line-p))))
    (howm-view-summary "<marks>" items)))
(defun howm-mark-same-line-p (m1 m2)
  (apply #'=
         (mapcar (lambda (m)
                   (save-excursion
                     (goto-char m)
                     (line-beginning-position)))
                 (list m1 m2))))

;;;
;;; files folder: list of file names
;;;

;;; This folder is treated specially for efficient search.

;;; Fix me: [2005-02-17]
;;; Sorry. I can't remember whether 'file' means really 'file' only.
;;; It may be 'file or directory'.

;; Try this to check it.
;; (setq howm-menu-top nil)
;; (setq howm-menu-file (expand-file-name "sample/0000-00-00-000000.howm"))
;; (setq howm-directory (howm-make-folder:files (mapcar (lambda (f) (expand-file-name f "sample/")) '("top.txt" "search.txt"))))

(defun howm-make-folder:files (files)
  (cons ':files files))

(defun howm-folder-items:files (folder &optional recursive-p)
  (let ((summary ""))
    (mapcar (lambda (f)
              (howm-make-item (howm-make-page:file f) summary))
            (howm-folder-files:files folder))))

(defun howm-folder-grep-internal:files (folder pattern &optional fixed-p)
  (howm-grep-items pattern (howm-folder-files:files folder) fixed-p))

;; should be removed, or renamed at least
(defun howm-folder-files:files (folder &optional exclusion-checker)
  (cdr folder))

;;;
;;; nest folder: list of folders
;;;

;; Try this to check it.
;; (setq howm-menu-top nil)
;; (setq howm-menu-file (expand-file-name "sample/0000-00-00-000000.howm"))
;; (setq howm-directory (howm-make-folder:nest (mapcar #'expand-file-name '("sample" "/usr/share/emacs/site-lisp/navi2ch"))))

(defun howm-make-folder:nest (list-of-folders)
  (cons ':nest list-of-folders))

(defun howm-folder-subfolders (self)
  (cdr self))

(defun howm-folder-items:nest (folder &optional recursive-p)
  (cl-mapcan (lambda (f) (howm-folder-items f recursive-p))
                  (howm-folder-subfolders folder)))

(defun howm-folder-grep-internal:nest (folder pattern &optional fixed-p)
  (cl-mapcan (lambda (f) (howm-folder-grep-internal f pattern fixed-p))
                  (howm-folder-subfolders folder)))

;;;
;;; namazu folder: namazu index directory
;;;

;; (cf.) Namazu: a Full-Text Search Engine http://www.namazu.org/index.html.en

;; test:
(defun howm-search-namazu (dir pattern)
  (interactive "Dindex directory: 
ssearch: ")
  (let ((folder (howm-make-folder:namazu (expand-file-name dir))))
    (howm-view-summary "<namazu>"
                       (howm-view-search-folder-items pattern folder))))

(defun howm-make-folder:namazu (index-dir)
  (cons ':namazu (expand-file-name index-dir)))

(defun howm-folder-items:namazu (folder &optional recursive-p)
  (let ((files (howm-folder-files:namazu folder)))
    (howm-folder-items:files (howm-make-folder:files files))))

;; should be removed, or renamed at least
(defun howm-folder-files:namazu (folder &optional exclusion-checker)
  (with-temp-buffer
    (insert-file-contents (expand-file-name "NMZ.r"
                                            (cdr folder)))
    (split-string (buffer-substring-no-properties (point-min)
                                                  (point-max))
                  "[\n\r\v]+")))

(defun howm-folder-grep-internal:namazu (folder pattern-list &optional fixed-p)
  (let* ((index-dir (cdr folder))
         (namazu-pattern (mapconcat #'identity pattern-list " or "))
         (hits (with-temp-buffer
                 (call-process "namazu" nil t nil
                               "-l" "-a" namazu-pattern index-dir)
                 (split-string (buffer-substring-no-properties (point-min)
                                                               (point-max))
                               "[\n\r\v]+")))
         (files (cl-remove-if (lambda (f) (not (file-exists-p f))) hits)))
    ;; grep again
    (let ((howm-view-use-grep nil)) ;; Japanese encoding is annoying.
      (howm-folder-grep-internal (howm-make-folder:files files)
                                 pattern-list fixed-p))))

;;;
;;; rot13dir folder: almost same as dir folder except that files are rot13ed.
;;;

(defun howm-make-folder:rot13dir (dir)
  (cons ':rot13dir dir))

(defun howm-folder-items:rot13dir (folder &optional recursive-p)
  (let ((files (if recursive-p
                   (howm-files-in-directory (cdr folder))
                 (directory-files (cdr folder) t))))
    (mapcar (lambda (f)
              (howm-make-item (howm-make-page:rot13file f)))
            files)))

(defun howm-folder-grep-internal:rot13dir (folder pattern-list &optional fixed-p)
  (let* ((dir (cdr folder))
         (ps (mapcar (lambda (p) (yarot13-rotate-string p)) pattern-list))
         (is (howm-folder-grep-internal:dir dir ps fixed-p)))
    (mapc (lambda (i)
            (let ((file (howm-page-name (howm-item-page i)))
                  (summary (howm-item-summary i)))
              (howm-item-set-page i (howm-make-page:rot13file file))
              (howm-item-set-summary i (yarot13-rotate-string summary))))
            is)
    is))

;;; For backward compatibility. Don't use it any more.

(defalias 'howm-view-directory-items  #'howm-folder-items)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Grep

;; Fix me:
;; Name of arguments are inappropriate.
;; Pattern and str may be list of strings.
;; File-list may be a string.

(defun howm-folder-grep (folder pattern &optional fixed-p)
  (when (stringp pattern)
    (setq pattern (list pattern)))
  (howm-folder-grep-internal folder pattern fixed-p))

(defvar *howm-view-force-case-fold-search* nil) ;; dirty!
(howm-defvar-risky howm-view-grep-log-file nil)
(defvar howm-view-grep-log-format "> %s | %s")

(defun howm-grep-items (str file-list &optional fixed-p exclusion-checker)
  (let* ((found (howm-grep str file-list fixed-p))
         (items (mapcar (lambda (z)
                          (let ((file (car z))
                                (place (cadr z))
                                (content (cl-caddr z)))
                            (if (and exclusion-checker
                                     (funcall exclusion-checker file))
                                nil
                              (howm-make-item file content place))))
                        found)))
    (if exclusion-checker
        (remove nil items)
      items)))

(defun howm-grep (str file-list &optional fixed-p)
  (when howm-view-grep-log-file
    (howm-write-log str howm-view-grep-log-format howm-view-grep-log-file))
  (when (stringp file-list)
    (setq file-list (list file-list)))
  (let ((grep-func (cond ((eq howm-view-use-grep t) 'howm-real-grep)
                         ((eq howm-view-use-grep 'auto) 'howm-real-grep-maybe)
                         ((null howm-view-use-grep) 'howm-fake-grep)
                         ((functionp howm-view-use-grep) howm-view-use-grep)
                         (t (error "No function %s." howm-view-use-grep)))))
    (funcall grep-func
             str file-list fixed-p *howm-view-force-case-fold-search*)))

(defun howm-real-grep-maybe (&rest args)
  (condition-case nil
      (apply #'howm-real-grep args)
    (error (apply #'howm-fake-grep args))))

(defun howm-real-grep (str file-list &optional fixed-p force-case-fold)
  "Call grep and parse its result.
\\='((file line-number line) (file line-number line) ...)
"
  (if (howm-grep-multi-p)
      (howm-real-grep-multi str file-list fixed-p force-case-fold)
    (howm-real-grep-single str file-list fixed-p force-case-fold)))

(defun howm-grep-multi-p ()
  howm-view-grep-file-stdin-option)

;; obsolete
(defun howm-real-grep-single (str file-list
                                  &optional fixed-p force-case-fold)
  "Call grep and parse its result.
\\='((file line-number line) (file line-number line) ...)
"
  (let ((trio (howm-real-grep-single-command
              str file-list fixed-p force-case-fold)))
    (with-temp-buffer
      (let* ((lines (apply #'howm-call-process* trio))
             (parsed (mapcar 'howm-grep-parse-line lines)))
        (remove nil parsed)))))

(defun howm-real-grep-single-command (str file-list
                                          &optional fixed-p force-case-fold)
  (when (listp str)
    (if (null (cdr str))
        (setq str (car str))
      (error "Multiple patterns are not supported: %s" str)))
  (let ((grep-command (or (and fixed-p howm-view-fgrep-command)
                          (howm-grep-command)))
        (opt (split-string howm-view-grep-option))
        (eopt (and howm-view-grep-expr-option
                   (list howm-view-grep-expr-option)))
        (case-fold (or force-case-fold
                       (not (let ((case-fold-search nil))
                              (string-match "[A-Z]" str)))))
        (fs (howm-expand-file-names file-list)))
    (cl-labels ((add-opt (pred x) (when (and pred x) (setq opt (cons x opt)))))
      (add-opt case-fold howm-view-grep-ignore-case-option)
      (add-opt fixed-p howm-view-grep-fixed-option)
      (add-opt (not fixed-p) howm-view-grep-extended-option))
    (list grep-command `(,@opt ,@eopt ,str) fs)))

(defun howm-real-grep-multi (str file-list &optional fixed-p force-case-fold)
  (let ((grep-command (or (and fixed-p howm-view-fgrep-command)
                          (howm-grep-command)))
        (opt (split-string howm-view-grep-option))
        (eopt (split-string howm-view-grep-file-stdin-option)))
    (let* ((str-list (cond ((stringp str) (list str))
                           ((listp str) str)
                           (t (error "Wrong type: %s" str))))
           (caps-p (cl-member-if (lambda (s) (howm-capital-p s)) str-list))
           (case-fold (or force-case-fold (not caps-p))))
      (cl-labels ((add-opt (pred x) (when (and pred x) (setq opt (cons x opt)))))
        (add-opt case-fold howm-view-grep-ignore-case-option)
        (add-opt fixed-p howm-view-grep-fixed-option)
        (add-opt (not fixed-p) howm-view-grep-extended-option))
      (with-temp-buffer
        (let* ((fs (howm-expand-file-names file-list))
               (pat (apply #'concat
                           (mapcar (lambda (s) (concat s "\n")) str-list)))
               (lines (howm-call-process* grep-command
                                          `(,@opt ,@eopt) fs
                                          nil pat))
               (parsed (mapcar 'howm-grep-parse-line lines)))
          (remove nil parsed))))))

(defun howm-grep-command ()
  (or howm-view-grep-command grep-program))

(defun howm-fake-grep (str file-list &optional fixed-p force-case-fold)
  "Search STR in files.
Return a list ((name number str) (name number str) ...), where
name is file name, number is line number, and str is line content.
FILE-LIST is list of file names.
If FIXED-P is non-nil, regexp search is performed.
If FIXED-P is nil, fixed string search is performed.
When STR has no capital letters or FORCE-CASE-FOLD is non-nil,
difference of capital letters and small letters are ignored.

Extended feature:
STR can be list of strings. They are regarded as \"or\" pattern of all elements."
  (cl-mapcan (lambda (file)
                    (howm-fake-grep-file (howm-fake-grep-regexp str fixed-p)
                                         file force-case-fold))
                  (cl-mapcan #'howm-files-in-directory file-list)))

(defun howm-fake-grep-regexp (str &optional fixed-p)
  (let ((str-list (if (stringp str) (list str) str)))
    (if fixed-p
        (regexp-opt str-list)
      (mapconcat (lambda (s) (format "\\(%s\\)" s)) str-list "\\|"))))

(defun howm-fake-grep-file (reg file force-case-fold)
  (let ((b (get-file-buffer file)))
    (if (and b howm-view-watch-modified-buffer)
        (with-current-buffer b
          (howm-fake-grep-current-buffer reg file force-case-fold))
      (with-temp-buffer
        (insert-file-contents file)
        (howm-fake-grep-current-buffer reg file force-case-fold)))))

(defun howm-fake-grep-current-buffer (reg file force-case-fold)
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-max))
      (let* ((found nil)
             (case-fold-search (or force-case-fold (not (howm-capital-p reg)))))
        (while (re-search-backward reg nil t)
          (beginning-of-line)
          (setq found
                (cons (list file
                            (riffle-get-place)
                            (buffer-substring-no-properties (point)
                                                            (line-end-position)))
                      found)))
        found))))

(defvar howm-grep-parse-regexp "^\\(\\([a-zA-Z]:/\\)?[^:]*\\):\\([0-9]*\\):\\(.*\\)$")
(defvar howm-grep-parse-file-pos 1)
(defvar howm-grep-parse-line-pos 3)
(defvar howm-grep-parse-content-pos 4)

(defun howm-grep-parse-line (line)
  (if (string-match howm-grep-parse-regexp
                    line)
      (let ((file (match-string howm-grep-parse-file-pos line))
            (line (string-to-number (match-string howm-grep-parse-line-pos line)))
            (content (match-string howm-grep-parse-content-pos line)))
        (list file line content))
    nil))

;; For backward compatibility. Don't use them any more.
(defalias 'howm-view-grep #'howm-grep)
(defalias 'howm-view-call-process #'howm-call-process)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; class Page

;; * class Page: abstraction of file

;; Fix me: confusion between 'page name' and 'file name',
;; especially for a buffer.

;; (Wrong comments. Ignore me.)
;;   * folder
;;   * name
;;   * created_time
;;   * modified_time
;;   * load
;;   * save(text)

(defun howm-page-type (page &rest r)
  (cond ((stringp page) ':file)
        ((bufferp page) ':buf)
        ((null page) ':nil)
        ((listp page) (car page))))

(howm-defvar-risky howm-page-dispatchers (list #'howm-page-type))

(gfunc-with howm-page-dispatchers
  (gfunc-def howm-page-name   (page))
  (gfunc-def howm-page-mtime  (page))
  (gfunc-def howm-page-open   (page))
  (gfunc-def howm-page-insert (page))
  (gfunc-def howm-page-viewer (page))
  (gfunc-def howm-page-set-configuration (page))
  )

(defun howm-page= (x y)
  (equal x y))

(defun howm-page-abbreviate-name (page)
  (howm-abbreviate-file-name (format "%s" (howm-page-name page))))

(defalias 'howm-save-buffer #'save-buffer)

(defun howm-insert-buffer-contents (buffer)
  (insert (with-current-buffer buffer
            (save-restriction
              (widen)
              (let ((limit (point-max)))
                (when howm-view-contents-limit
                  (setq limit (min limit howm-view-contents-limit)))
                (buffer-substring-no-properties (point-min) limit))))))

;; (defun howm-page-insert-range ()
;;   (let ((limit (point-max)))
;;     (when howm-view-contents-limit
;;       (setq limit (min limit howm-view-contents-limit)))
;;     (list (point-min) limit)))

;; (defun howm-page-save (&optional args)
;;   (interactive "p")
;;   (with-current-buffer (get-file-buffer (howm-page-name howm-buffer-page))
;;     (apply #'save-buffer args)))

;; (defun howm-save-buffer (&optional args)
;;   (interactive "p")
;;   (prog1
;;       (save-buffer args)
;;     (howm-after-save)))

;;;
;;; file page: name of file
;;;

(defun howm-make-page:file (filename)
  filename)

(defun howm-page-name:file (page)
  page)

(defun howm-page-mtime:file (page)
  (nth 5 (file-attributes (howm-page-name page))))

(defun howm-page-open:file (page)
  (find-file (howm-page-name page))
  ;; widen is desired when corresponding file is already opened and
  ;; its buffer is narrowed.
  (widen))

(defun howm-page-insert:file (page)
  (let ((b (get-file-buffer page)))
    (if (and b
             howm-view-watch-modified-buffer
             (not howm-view-use-grep))
        (howm-insert-buffer-contents b)
      (howm-insert-file-contents page))))

(defun howm-page-viewer:file (page)
  (let* ((ls (lambda (dir)
               (with-temp-buffer
                 (insert-directory dir "-l")
                 (buffer-substring-no-properties (point-min) (point-max)))))
         (dir-viewer (and (file-directory-p page)
                          (howm-make-viewer:func #'find-file ls)))
         (viewer (cdr (cl-assoc-if (lambda (reg) (string-match reg page))
                                        howm-view-external-viewer-assoc))))
    (or viewer dir-viewer
        (and howm-view-use-mailcap
             (let* ((ext (if (string-match "\\.[^\\.]+$" page)
                             (match-string 0 page)
                           ""))
                    (type (howm-funcall-if-defined
                              (mailcap-extension-to-mime ext)))
                    (type-match (lambda (r) (string-match r type))))
               (cond ((null type)
                      nil)
                     ((cl-member-if type-match howm-view-open-by-myself)
                      nil)
                     (t
                      (howm-funcall-if-defined
                          (mailcap-mime-info type)))))))))

(defun howm-page-set-configuration:file (page)
  (howm-set-configuration-for-file-name page))

;;;
;;; buffer page: buffer object
;;;

(defun howm-make-page:buf (buf)
  buf)

(defun howm-page-name:buf (page)
  (buffer-name page))

(defconst howm-dummy-mtime (encode-time 0 0 9 1 1 1970)
  "Dummy mtime which has no meaning.")

(defun howm-page-mtime:buf (page)
  howm-dummy-mtime)

(defun howm-page-open:buf (page)
  (switch-to-buffer page))

(defun howm-page-insert:buf (page)
  (when (not (howm-buffer-killed-p page))
    (howm-insert-buffer-contents page)))

(defun howm-page-viewer:buf (page)
  nil)
;;   (howm-make-viewer:func #'switch-to-buffer))

(defun howm-page-set-configuration:buf (page)
  (when (buffer-file-name page)
    (howm-set-configuration-for-file-name (buffer-file-name page))))

;;;
;;; nil page: dummy page
;;;

(defun howm-make-page:nil ()
  nil)

(defun howm-page-name:nil (page)
  "")

(defun howm-page-mtime:nil (page)
  howm-dummy-mtime)

(defun howm-page-open:nil (page)
  "Do nothing."
  nil)

(defun howm-page-insert:nil (page)
  "Do nothing."
  nil)

(defun howm-page-viewer:nil (page)
  nil)

(defun howm-page-set-configuration:nil (page)
  "Do nothing."
  nil)

;;;
;;; rot13file page: almost same as file except that it is rot13ed
;;;

(defun howm-make-page:rot13file (filename)
  (cons ':rot13file filename))

(defun howm-page-name:rot13file (page)
  (howm-page-name (cdr page)))

(defun howm-page-mtime:rot13file (page)
  (howm-page-mtime:file (cdr page)))

(defun howm-page-open:rot13file (page)
  (yarot13-find-file (howm-page-name page))
  )

(defun howm-page-insert:rot13file (page)
  (yarot13-insert-file-contents (howm-page-name page)))

(defun howm-page-viewer:rot13file (page)
  nil)

(defun howm-page-set-configuration:rot13file (page)
  (howm-set-configuration-for-file-name (howm-page-name page)))

;;; Clean me.

;; (defun howm-file-path (&optional time)
;;   (expand-file-name (howm-file-name time) howm-directory))

(defun howm-create-file (&optional keep-cursor-p)
  (let* ((pc (howm-folder-get-page-create howm-directory (howm-file-name)))
         (page (car pc))
         (createp (cdr pc)))
    (howm-page-open page)
    (when (not keep-cursor-p)
      (widen)
      (goto-char (point-max)))
    (when createp
      (run-hooks 'howm-create-file-hook))
    createp))

;; (defun howm-create-file (&optional keep-cursor-p)
;;   (let* ((file (howm-file-path))
;;          (dir (file-name-directory file))
;;          (createp (not (file-exists-p file))))
;;     (make-directory dir t)
;;     (howm-page-open file)
;;     (when createp
;;       (run-hooks 'howm-create-file-hook))
;;     (when (not keep-cursor-p)
;;       (widen)
;;       (goto-char (point-max)))
;;     createp))

;;; viewer

;; Viewer is one of the following.
;; func    ==> (func) is called after (find-file page).
;; (func)  ==> (func page) is called.
;; (func . previewer)
;;   ==> (func page) and (previewer page) are called for open and preview
;;   (previewer must return a string).
;; "str"   ==> (format "str" page) is externally executed on shell.

(defun howm-viewer-type (viewer &rest r)
  (cond ((stringp viewer)   ':str)
        ((functionp viewer) ':func0)
        ((listp viewer)     ':func)))

(howm-defvar-risky howm-viewer-dispatchers (list #'howm-viewer-type))

(gfunc-with howm-viewer-dispatchers
  (gfunc-def howm-viewer-call      (viewer page))
  (gfunc-def howm-viewer-indicator (viewer page))
)

(defun howm-make-viewer:func (f &optional previewer)
  (cons f previewer))

(when howm-view-use-mailcap
  (require 'mailcap)
  (howm-funcall-if-defined (mailcap-parse-mailcaps))
  (howm-funcall-if-defined (mailcap-parse-mimetypes)))

(defun howm-viewer-call:str (viewer page)
  (start-process "howm-view-external-viewer" nil
                 shell-file-name
                 shell-command-switch
                 (format viewer (howm-page-name page))))
(defun howm-viewer-call:func0 (viewer page)
  (howm-page-open page)
  (funcall viewer))
(defun howm-viewer-call:func (viewer page)
  (funcall (car viewer) page))

(defvar howm-viewer-indicator-format "%%%%%% %s %%%%%%")
(defun howm-viewer-indicator-gen (fmt &rest args)
  (format howm-viewer-indicator-format
          (apply #'format (cons fmt args))))
(defun howm-viewer-indicator:str (viewer page)
  (howm-viewer-indicator-gen viewer (howm-page-name page)))
(defun howm-viewer-indicator:func0 (viewer page)
  (howm-viewer-indicator-gen "%S %S" viewer page))
(defun howm-viewer-indicator:func (viewer page)
  (let ((func (car viewer))
        (previewer (cdr viewer)))
    (if previewer
        (funcall previewer page)
      (howm-viewer-indicator-gen "(%S %S)" func page))))

(define-advice action-lock-find-file (:around (orig-fun f u) external-viewer)
  (let ((viewer (howm-page-viewer f)))
    (if viewer
        (howm-viewer-call viewer (expand-file-name f))
      (funcall orig-fun f u))))

;; For backward compatibility. Don't use them any more.
(defalias 'howm-view-external-viewer      #'howm-page-viewer)
(defalias 'howm-view-call-external-viewer #'howm-viewer-call)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; class Item

;; Fix me: confusion between howm-item-page and howm-item-name

;; * class Item: abstraction of hit position in file
;;   * page
;;   * place
;;   * and conventional properties

(defun howm-make-item (page &optional summary place offset home privilege)
  (list page summary place offset home privilege))
(defun howm-item-page      (item) (nth 0 item)) ;; page can be nil.
(defun howm-item-summary   (item) (howm-item-nth 1 item ""))
(defun howm-item-place     (item) (howm-item-nth 2 item nil))
(defun howm-item-offset    (item) (howm-item-nth 3 item nil))
(defun howm-item-home      (item) (howm-item-nth 4 item nil))
(defun howm-item-privilege (item) (howm-item-nth 5 item nil))
(defun howm-item-nth (n item default)
  (or (nth n item) default))
(defun howm-item-set-page (item val)
  (setf (nth 0 item) val))
(defun howm-item-set-summary (item val)
  (setf (nth 1 item) val))
(defun howm-item-set-offset (item val)
  (setf (nth 3 item) val))
(defun howm-item-set-home (item val)
  (setf (nth 4 item) val))
(defun howm-item-set-privilege (item val)
  (setf (nth 5 item) val))

(defun howm-item-name (item)
  (format "%s" (howm-page-name (howm-item-page item))))

(defun howm-item-dup (item) (mapcar #'identity item))

;; For backward compatibility. Don't use them any more.
;; ;; item = (filename summary place offset home)
(defun howm-view-make-item (filename &rest r)
  (apply #'howm-make-item (cons (howm-make-page:file filename) r)))
(defalias 'howm-view-item-filename      #'howm-item-name)
(defalias 'howm-view-item-summary       #'howm-item-summary)
(defalias 'howm-view-item-place         #'howm-item-place)
(defalias 'howm-view-item-offset        #'howm-item-offset)
(defalias 'howm-view-item-home          #'howm-item-home)
(defalias 'howm-view-item-privilege     #'howm-item-privilege)
(defalias 'howm-view-item-set-summary   #'howm-item-set-summary)
(defalias 'howm-view-item-set-offset    #'howm-item-set-offset)
(defalias 'howm-view-item-set-home      #'howm-item-set-home)
(defalias 'howm-view-item-set-privilege #'howm-item-set-privilege)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; search path

;; historical & awkward mechanism

(howm-defvar-risky howm-search-path nil)
(defvar howm-search-other-dir nil)
(defvar *howm-independent-directories* nil) ;; for internal use

(defun howm-independent-search-path ()
  (let ((c default-directory))
    (and c
         (car (cl-member-if (lambda (dir) (howm-subdirectory-p dir c))
                                 *howm-independent-directories*)))))

(defun howm-search-path (&optional ignore-independent-search-path)
  (let ((d (howm-independent-search-path)))
    (cond ((and d (not ignore-independent-search-path)) (list d))
          (howm-search-other-dir (howm-search-path-multi))
          (t (howm-search-path-single)))))
(defun howm-search-path-single ()
  (list howm-directory))
(defun howm-search-path-multi ()
  (cons howm-directory howm-search-path))

(defun howm-search-path-folder (&optional ignore-independent-search-path)
  (howm-make-folder:nest (howm-search-path ignore-independent-search-path)))

(defun howm-toggle-search-other-dir (&optional arg)
  "Change whether `howm-search-path' is searched or not.
With arg, search `howm-search-path' iff arg is positive."
  (interactive "P")
  (setq howm-search-other-dir
        (if arg
            (> (prefix-numeric-value arg) 0)
          (not howm-search-other-dir)))
  (message "howm search-path = %s" (howm-search-path)))

(defun howm-open-directory-independently (dir)
  (interactive "DDirectory: ")
  (add-to-list '*howm-independent-directories*
               (expand-file-name dir))
  (let ((default-directory dir))
    (howm-normalize-show "" (howm-folder-items dir t))
    (howm-keyword-add-items (howm-view-item-list))))

(defvar howm-keyword-buffer-name-format " *howm-keys:%s*")
(defun howm-keyword-buffer ()
  (let* ((dir (howm-independent-search-path))
         (buffer-name (format howm-keyword-buffer-name-format
                              (if dir (expand-file-name dir) ""))))
    (if dir
        (get-buffer-create buffer-name)
      (howm-get-buffer-for-file (howm-keyword-file) buffer-name))))

;;; exclusion

;; Fix me on inefficiency.
;; 
;; [2005-02-18] I can't remember why I checked relative path in old versions.
;; [2005-04-24] Now I remember the reason.
;; Some people like ~/.howm/ rather than ~/howm/ as their howm-directory.
;; It must be included even if it matches to howm-excluded-file-regexp.
;; 
;; Bug: (howm-exclude-p "~/howm/CVS") != (howm-exclude-p "~/howm/CVS/")
(defun howm-exclude-p (filename)
  (not (cl-find-if-not
        (lambda (dir) (howm-folder-match-under-p dir
                                                 howm-excluded-file-regexp
                                                 filename))
        (howm-search-path))))

;;; howm-backend.el ends here
