;;;; -*- lisp -*-

(defpackage :footnotes
  (:use :common-lisp :cl-ppcre)
  (:export #:reorder-footnotes-by-order-of-appearence
           #:reorder-footnotes-by-order-listing-order)
  (:documentation "A simple tool for re-ordering footnotes in a text file."))

(in-package :footnotes)

(defun replace-with-reordered-notes (line notes-mapping)
  "Given a line of text, which may or may not contain footnote markers, replace any markers with the
numbers specified in NOTES-MAPPING.

LINE is simply a string. NOTES-MAPPING is an alist mapping the original marker, a string like
\"[X]\", to its replacement \"[Y]\"."
  (regex-replace-all
   "\\[\\d+\\]"
   line
   (lambda (target-string start end match-start match-end reg-starts reg-ends)
     (declare (ignore start end reg-starts reg-ends))
     (let ((replacement (second (find (subseq target-string match-start match-end) notes-mapping :test #'string= :key #'first))))
       (unless replacement
         ;; didn't find a replacement for this footnote, this should not happen. allow the user to
         ;; continue using "[X]" instead.
         (setf replacement "[X]")
         (cerror "Use \"[X]\" instead."
                 "Unable to find replacement for ~S in ~S." (subseq target-string match-start match-end) notes-mapping))
       replacement))))

(defun collect-notes-in-line (line notes-mapping)
  "Adds to NOTES-MAPPING any markers found in the string LINE. 

The notes in LINE are mapped to new notes according to their order of appearence in LINE."
  (do-matches-as-strings (footnote-marker "\\[(\\d+)\\]" line)
    (pushnew (list footnote-marker (format nil "[~D]" (1+ (length notes-mapping))))
             notes-mapping
             :key #'first :test #'string=))
  notes-mapping)

(defun reorder-footnotes-by-order-of-appearence (filename)
  "Rewrites the footnotes in FILENAME based on their order of appearence in the original text."
  (with-open-file (input filename :direction :input :if-does-not-exist :error)
    (let ((notes-mapping '()))
      ;; loop #1 - rewrite text collecting footnotes as they occur
      (loop
        for line = (read-line input nil input nil)
        until (eq line input)
        do (setf notes-mapping (collect-notes-in-line line notes-mapping))
        do (write-line (replace-with-reordered-notes line notes-mapping)
                       *standard-output*)
        when (string= "@footnote:" line) do (return))

      ;; loop #2 - rewrite the trailing footnotes section
      (loop
        for line = (read-line input nil input nil)
        until (eq line input)

        do (write-line (replace-with-reordered-notes line notes-mapping)
                 *standard-output*)))))

(defun reorder-footnotes-by-order-listing-order (filename)
  "Rewrites the footnotes in FILENAME based on their order of appearence in the @foonote: section.

In order to avoid keeping the whole file in memory we perform two passes over FILENAME. The first
pass collects the notes in the @footnote: section and computes their replacements, the second pass
rewrites the text, outputing it to *standard-output*."
  (let ((notes-mapping '()))
    (with-open-file (input filename :direction :input)
      ;; loop #1 read up to @footnote: section
      (loop
        for line = (read-line input nil input nil)
        until (eq line input)
        when (string= "@footnote:" line)
          do (return))

      ;; loop #2 - collect notes here in the order they occur.
      (loop
        for line = (read-line input nil input nil)
        until (eq line input)
        do (setf notes-mapping (collect-notes-in-line line notes-mapping))))
    ;; loop #3 - go over the whole file again and emit it with the reordered notes.
    (with-open-file (input filename :direction :input)
      (loop
        for line = (read-line input nil input nil)
        until (eq line input)
        do (write-line (replace-with-reordered-notes line notes-mapping)
                       *standard-output*)))))

;; Copyright (c) 2008 Edward Marco Baringer
;; All rights reserved.
;;
;; Redistribution and use in source and binary forms, with or without
;; modification, are permitted provided that the following conditions are
;; met:
;;
;;  - Redistributions of source code must retain the above copyright
;;    notice, this list of conditions and the following disclaimer.
;;
;;  - Redistributions in binary form must reproduce the above copyright
;;    notice, this list of conditions and the following disclaimer in the
;;    documentation and/or other materials provided with the distribution.
;;
;;  - Neither the name of Edward Marco Baringer, Luca Capello, nor
;;    BESE, nor the names of its contributors may be used to endorse
;;    or promote products derived from this software without specific
;;    prior written permission.
;;
;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
;; A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
;; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
;; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
;; OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.