;;; ---------------------------------------------------------------------------
;;; NoteBook.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; STANDARD NOTES LIBRARY
;;;
;;; PURPOSE
;;;   Keeps your office's standard notes in one plain text file and drops any
;;;   selection of them onto a drawing, correctly numbered.
;;;
;;;   Every practice has a master list of notes - the hundred-odd paragraphs that
;;;   appear on drawing after drawing. Retyping them invites typos; copying them
;;;   from the last job propagates whatever was wrong on the last job. This reads
;;;   them from one file that one person maintains.
;;;
;;; THE NOTES FILE
;;;   Plain text. A line beginning with a dollar sign and a number starts a note;
;;;   everything after it belongs to that note until the next note begins. A line
;;;   beginning with an asterisk is a section heading and is not itself a note.
;;;
;;;       * CONCRETE
;;;       $1
;;;       All concrete shall attain a minimum compressive strength
;;;       of 25 MPa at 28 days unless noted otherwise.
;;;
;;;       $2
;;;       Reinforcement shall comply with AS/NZS 4671.
;;;
;;;   Notes do not have to be in order and the numbers do not have to be
;;;   contiguous - gaps are normal as a library grows and notes are retired.
;;;
;;; HOW IT WORKS
;;;   1. The file is read ONCE per command into a list of notes held in memory.
;;;      The routine this replaces reopened and re-scanned the file for every
;;;      single note inserted, which on a fifty-note file meant fifty passes.
;;;
;;;   2. You ask for notes by number, in any mix of singles and ranges:
;;;
;;;        1,3,5-9      notes 1, 3, and 5 through 9
;;;        1-8          notes 1 through 8
;;;        12           just note 12
;;;
;;;      Ranges are expanded and the whole list de-duplicated, so 1-5,3 gives
;;;      five notes rather than six.
;;;
;;;   3. LIBRARY NUMBERS ARE NOT DRAWING NUMBERS. Note $47 in the library might
;;;      be the third note on this drawing, so it gets numbered 3. You give the
;;;      first drawing number and the notes are numbered up from there in the
;;;      order you asked for them.
;;;
;;;   4. The number sits in its own column to the left of the note text, so
;;;      wrapped lines line up under each other instead of under the number.
;;;
;;;   5. Any note number you ask for that is not in the library is reported at
;;;      the end rather than silently skipped - a missing note on a drawing is a
;;;      lot more expensive than a message on the command line.
;;;
;;; SIZE
;;;   Text height is DIMSCALE times 0.15 unless the current text style has a
;;;   fixed height, in which case the style wins. Line spacing and the number
;;;   column are both multiples of that height, so the block scales as one.
;;;
;;;   NOTEBOOK  - browse, search and insert standard notes
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; SESSION MEMORY
;;; ---------------------------------------------------------------------------

(if (null *NoteBook:Prefs*)
    (setq *NoteBook:Prefs*
        (list (cons "FILE"   nil)    ; full path to the notes file
              (cons "INDENT" 6.0)    ; number column width, in text heights
              (cons "LEAD"   1.6667) ; line spacing, in text heights
              (cons "NEXT"   1)      ; next drawing note number
        )
    )
)

(defun NoteBook:Get ( key ) (cdr (assoc key *NoteBook:Prefs*)))

(defun NoteBook:Put ( key val )
    (setq *NoteBook:Prefs*
        (cons (cons key val)
              (vl-remove-if '(lambda (p) (= (car p) key)) *NoteBook:Prefs*)))
    val
)

;;; ---------------------------------------------------------------------------
;;; READING THE LIBRARY
;;;
;;; Returns a list of ( number "line" "line" ... ), in file order.
;;;
;;; Parsing once into memory is what makes everything else here cheap: search,
;;; listing and insertion all walk the same list rather than re-reading the file.
;;; ---------------------------------------------------------------------------

(defun NoteBook:Parse ( file / fp line first out num body )

    (if (setq fp (open file "r"))
        (progn
            (while (setq line (read-line fp))
                ;; Tabs at the start of a line upset the TEXT command's leading
                ;; whitespace handling, so they are turned into spaces on the way in.
                (while (vl-string-search "\t" line)
                    (setq line (vl-string-subst "    " "\t" line)))

                (setq first (substr line 1 1))

                (cond
                    ;; A new note begins. Bank whatever was being collected.
                    ((= first "$")
                     (if num (setq out (cons (cons num (reverse body)) out)))
                     (setq num  (atoi (substr line 2 8))
                           body nil))

                    ;; A section heading closes the current note without starting
                    ;; a new one.
                    ((= first "*")
                     (if num (setq out (cons (cons num (reverse body)) out)))
                     (setq num nil body nil))

                    ;; Anything else is a line of the note currently open.
                    (num (setq body (cons line body)))
                )
            )
            ;; The last note in the file has no following marker to close it.
            (if num (setq out (cons (cons num (reverse body)) out)))
            (close fp)

            ;; Trailing blank lines are formatting in the file, not part of the note.
            (mapcar
                '(lambda (n / b)
                     (setq b (reverse (cdr n)))
                     (while (and b (= "" (vl-string-trim " " (car b))))
                         (setq b (cdr b)))
                     (cons (car n) (reverse b)))
                (reverse out))
        )
    )
)

;;; ---------------------------------------------------------------------------
;;; PARSING A REQUEST LIKE  1,3,5-9
;;;
;;; Split on commas first, then expand any dashes. Returns a list of integers in
;;; the order asked for, with duplicates removed but the order of first mention
;;; preserved - asking for 5-9,7 should not move 7 to the end.
;;; ---------------------------------------------------------------------------

(defun NoteBook:Split ( s sep / pos out )
    (while (setq pos (vl-string-search sep s))
        (setq out (cons (substr s 1 pos) out)
              s   (substr s (+ pos 1 (strlen sep)))))
    (reverse (cons s out))
)

(defun NoteBook:Expand ( s / out dash lo hi tmp seen )
    (foreach part (NoteBook:Split s ",")
        (setq part (vl-string-trim " " part))
        (if (/= part "")
            (if (setq dash (vl-string-search "-" part))
                (progn
                    (setq lo (atoi (substr part 1 dash))
                          hi (atoi (substr part (+ dash 2))))
                    ;; A reversed range is a typo, not a request for nothing,
                    ;; so 9-5 is read as 5-9 rather than yielding nothing.
                    (if (> lo hi) (setq tmp lo lo hi hi tmp))
                    (while (<= lo hi)
                        (setq out (cons lo out) lo (1+ lo))))
                (setq out (cons (atoi part) out)))))
    (setq out (reverse out))

    ;; De-duplicate while keeping the order of first mention, so 5-9,7 gives
    ;; five notes with 7 still in its original position.
    (foreach n out (if (not (member n seen)) (setq seen (cons n seen))))
    (reverse seen)
)

;;; ---------------------------------------------------------------------------
;;; LOCATING THE LIBRARY
;;; ---------------------------------------------------------------------------

(defun NoteBook:FindFile ( / f )
    (cond
        ;; Already known and still there.
        ((and (NoteBook:Get "FILE") (findfile (NoteBook:Get "FILE")))
         (NoteBook:Get "FILE"))

        ;; The conventional name, anywhere on the support path.
        ((setq f (findfile "StandardNotes.txt")) (NoteBook:Put "FILE" f))

        ;; The name the older libraries used.
        ((setq f (findfile "stdnotes.txt"))      (NoteBook:Put "FILE" f))

        ;; Ask, once.
        (t
            (princ "\nNo standard notes file found on the support path.")
            (if (setq f (getfiled "Select the standard notes file" "" "txt" 4))
                (NoteBook:Put "FILE" f)))
    )
)

;;; ---------------------------------------------------------------------------
;;; TEXT HEIGHT
;;;
;;; A fixed-height text style overrides anything supplied and the TEXT command
;;; does not prompt for a height in that case, so the two situations need
;;; different command sequences. Resolved once and handed around.
;;; ---------------------------------------------------------------------------

(defun NoteBook:StyleHeight ( / h )
    (setq h (cdr (assoc 40 (tblsearch "style" (getvar "TEXTSTYLE")))))
    (if h h 0.0)
)

(defun NoteBook:Text ( pt height styleH s )
    (if (zerop styleH)
        (command "_.TEXT" pt height 0 s)
        (command "_.TEXT" pt 0 s))
    (princ)
)

;;; ---------------------------------------------------------------------------
;;; MAIN COMMAND
;;; ---------------------------------------------------------------------------

(defun c:NOTEBOOK ( / *error* vars vals file notes opt height styleH
                      lead indent want start pt y missing placed hit s
                      lines num )

    (setq vars '("CMDECHO" "OSMODE" "BLIPMODE" "TEXTSTYLE")
          vals (mapcar 'getvar vars))

    (defun NoteBook:Restore ( )
        (mapcar 'setvar vars vals)
        (while (= 8 (logand 8 (getvar 'undoctl))) (command "_.UNDO" "_End"))
        (vl-catch-all-apply '(lambda ( ) (*pop-error-mode*)) '())
        (princ)
    )

    (defun *error* ( msg )
        (NoteBook:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** NOTEBOOK error: " msg " **")))
        (princ)
    )

    (setvar "CMDECHO" 0)
    ;; AutoCAD 2015 and later refuse (command) inside an *error* handler
    ;; unless the routine says up front that it will use one. Restore does,
    ;; to close this undo group. The declaring call is absent on older
    ;; releases, so it is wrapped rather than tested for.
    (vl-catch-all-apply '(lambda ( ) (*push-error-using-command*)) '())
    (command "_.UNDO" "_Begin")

    (cond
        ((null (setq file (NoteBook:FindFile)))
         (princ "\nNo notes file selected - nothing to do."))

        ((null (setq notes (NoteBook:Parse file)))
         (princ (strcat "\n** " file " could not be read, or contains no notes."
                        "\n   A note starts with a $ and its number on its own line. **")))

        (t
            (princ (strcat "\nNotes library: " file
                           "  (" (itoa (length notes)) " notes)"))

            (setq styleH (NoteBook:StyleHeight)
                  height (if (zerop styleH) (* (getvar "DIMSCALE") 0.15) styleH)
                  lead   (* height (NoteBook:Get "LEAD"))
                  indent (* height (NoteBook:Get "INDENT")))
            (if (<= height 0.0) (setq height 0.15 lead 0.25 indent 0.9))

            (setq opt "x")
            (while (and opt (/= opt "Quit"))
                (initget "Insert List Search File Quit")
                (setq opt (getkword "\n[Insert/List/Search/File/Quit] <Insert>: "))
                (if (null opt) (setq opt "Insert"))

                (cond
                    ;; ---------------------------------------------------------
                    ((= opt "List")
                     (textscr)
                     (princ (strcat "\n\n  " (itoa (length notes)) " notes in "
                                    file "\n  " ))
                     (foreach n notes
                         (princ (strcat "\n  $" (itoa (car n)) "  "
                                        (if (cdr n) (cadr n) "(empty)"))))
                     (princ "\n"))

                    ;; ---------------------------------------------------------
                    ((= opt "Search")
                     (setq s (strcase (getstring t "\nText to search for: ")))
                     (if (= s "")
                         (princ "\nNothing to search for.")
                         (progn
                             (setq hit nil)
                             (foreach n notes
                                 (if (vl-some
                                         '(lambda (ln) (wcmatch (strcase ln) (strcat "*" s "*")))
                                         (cdr n))
                                     (setq hit (cons n hit))))
                             (setq hit (reverse hit))
                             (if (null hit)
                                 (princ "\nNo notes contain that.")
                                 (progn
                                     (princ (strcat "\n" (itoa (length hit)) " note(s) matched:"))
                                     (foreach n hit
                                         (princ (strcat "\n  $" (itoa (car n)) "  "
                                                        (if (cdr n) (cadr n) "")))))))))

                    ;; ---------------------------------------------------------
                    ((= opt "File")
                     (if (setq s (getfiled "Select the standard notes file" file "txt" 4))
                         (progn
                             (NoteBook:Put "FILE" s)
                             (setq file s notes (NoteBook:Parse s))
                             (princ (strcat "\nNow using " s " ("
                                            (itoa (length notes)) " notes)")))))

                    ;; ---------------------------------------------------------
                    ((= opt "Insert")
                     (setq s (getstring "\nNote numbers, e.g. 1,3,5-9: "))
                     (if (= (vl-string-trim " " s) "")
                         (princ "\nNo notes requested.")
                         (progn
                             (setq want (NoteBook:Expand s))

                             (initget 6)
                             (setq start (getint
                                             (strcat "\nFirst drawing note number <"
                                                     (itoa (NoteBook:Get "NEXT")) ">: ")))
                             (if (null start) (setq start (NoteBook:Get "NEXT")))

                             (setvar "BLIPMODE" 0)
                             (setq pt (getpoint "\nTop left of the note block: "))

                             (if (null pt)
                                 (princ "\nCancelled.")
                                 (progn
                                     (setvar "OSMODE" 0)
                                     (setq y       0.0
                                           num     start
                                           missing nil
                                           placed  0)

                                     (foreach n want
                                         (if (setq hit (assoc n notes))
                                             (progn
                                                 (setq lines (cdr hit))
                                                 (if (null lines) (setq lines (list "")))

                                                 ;; The number, in its own column
                                                 ;; to the left of the text.
                                                 (NoteBook:Text
                                                     (polar pt (* pi 1.5) y)
                                                     height styleH
                                                     (strcat (itoa num) "."))

                                                 ;; The note itself, indented so
                                                 ;; wrapped lines align under one
                                                 ;; another rather than under the
                                                 ;; number.
                                                 (foreach ln lines
                                                     (NoteBook:Text
                                                         (polar (polar pt 0.0 indent) (* pi 1.5) y)
                                                         height styleH ln)
                                                     (setq y (+ y lead)))

                                                 ;; A blank line between notes.
                                                 (setq y      (+ y lead)
                                                       num    (1+ num)
                                                       placed (1+ placed)))
                                             (setq missing (cons n missing))))

                                     (NoteBook:Put "NEXT" num)

                                     (princ (strcat "\n" (itoa placed)
                                                    " note(s) placed, numbered "
                                                    (itoa start) " to " (itoa (1- num)) "."))
                                     (if missing
                                         (progn
                                             (princ "\n** NOT IN THE LIBRARY, nothing placed for: **")
                                             (foreach m (reverse missing)
                                                 (princ (strcat " $" (itoa m))))))
                                     (setvar "OSMODE" (nth (vl-position "OSMODE" vars) vals))
                                 )
                             )
                         )
                     )
                    )
                )
            )
        )
    )

    (NoteBook:Restore)
    (princ)
)

(princ)
