;;; lentic.el --- One buffer as a view of another -*- lexical-binding: t -*-

;;; Header:

;; This file is not part of Emacs

;; Author: Phillip Lord <phillip.lord@russet.org.uk>
;; Maintainer: Phillip Lord <phillip.lord@russet.org.uk>
;; Version: 0.12
;; Package-Requires: ((emacs "25") (m-buffer "0.13") (dash "2.5.0"))

;; The contents of this file are subject to the GPL License, Version 3.0.

;; Copyright (C) 2014-2024  Free Software Foundation, Inc.

;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:

;; `lentic' enables /lenticular text/: simultaneous editing and viewing of the
;; same (or closely related) text in two or more buffers, potentially in
;; different modes. Lenticular text is named after lenticular printing, which
;; produce images which change depending on the angle at which they are
;; viewed.

;; Sometimes, it would be nice to edit a file in two ways at once. For
;; instance, you might have a source file in a computational language with
;; richly marked documentation. As Emacs is a modal editor, it would be nice
;; to edit this file both in a mode for the computational language and for the
;; marked up documentation.

;; One solution to this is to use a single-mode which supports both types of
;; editing. The problem with this is that it is fundamentally difficult to
;; support two types of editing at the same time; more over, you need a new
;; mode for each combination. Another solution is to use one of the
;; multiple-mode tools which are available. The problem with this is that they
;; generally need some support from the modes in question. And, again, the
;; difficulty is supporting both forms of editing in the same environment. A
;; final problem is that it is not just the editing environment that needs to
;; be adapted; the programmatic environment needs to be untroubled by the
;; documentation, and the documentation environment untroubled by the program
;; code.

;; Lenticular text provides an alternative solution. Two lentic buffers, by
;; default, the share content but are otherwise independent. Therefore,
;; you can have two buffers open, each showing the content in different modes;
;; to switch modes, you simply switch buffers. The content, location of point,
;; and view are shared.

;; Moreover, lentic buffers can also perform a bi-directional transformation
;; between the two. If this is done, then the two can have different but
;; related text. This also solves the problem of integration with a
;; tool-chain; each lentic buffer can be associated with a different file and
;; a different syntax. For example, this file is, itself, lenticular text. It
;; can be viewed either as Emacs-Lisp or in Org-Mode. In Emacs-Lisp mode, this
;; text is commented out, in org-mode it is not.

;; In fact, although the default behaviour of lentic appears to keep the same
;; text in each buffer, even it uses this bi-directional transformation
;; capability; while the text is shared, the text properties are not. This is
;; a behaviour which differs between lentic buffers and indirect buffers. The
;; lentic buffers can therefore be in different modes without fighting each
;; other to set the text properties.

;; It is possible to configure the transformation for any two buffers in a
;; extensible way. Mostly I have concentrated on mode-specific operation,
;; but, for instance, I have also used this ability on a per-project basis
;; controlling, for instance, the location of the lentic-file.

;;; Usage:

;; lentic can be installed from GNU ELPA/Marmalade then add

;;     (global-lentic-mode 1)

;; to your init file.

;; The main user entry points are accessible through the lentic edit menu, or
;; through `global-lentic-mode' which adds keybindings to create and manipulate
;; new lentic buffers. See `lentic-mode' commentary for more information.

;; By default, the lentic buffer created contains exactly the same contents as
;; the original buffer, but is otherwise separate; it can have a different major
;; modes, different syntax highlighting, invisible regions and even different
;; narrowing. Saving one buffer will save the other; killing the lentic buffer
;; does not affect the original, but killing the original also kills the lentic.

;; While this is somewhat useful, more generally a buffer will be configured to
;; produce a particular transformation. This can control many features of the
;; lentic, including the file name, major mode and an arbitrary transformation
;; between the two. Configuration is considered next.

;;; Configuration:

;; lentic buffers are configurable in a large number of ways. It is possible
;; to control the nature of the transformation, the default buffer name that a
;; lentic buffer takes, and the file location (or not) of the lentic buffer.
;; Lentic now supports any number of lentic buffers, in relatively arbitrary
;; geometries, although this requires additional support from the
;; configuration objects.

;; Configuration of a buffer happens in one of two places. First,
;; `lentic-init' is run when a lentic buffer is first created. This function
;; should return the configuration object, and is mostly designed for use as a
;; file-local or dir-local variable. This object is stored in the `lentic-config'
;; and all subsequent operation happens through this.

;; There are now a number of different configurations, which can be used for
;; general-purposes use as well as an extension points for subclass
;; configurations. The two most general configurations are:

;;  - default: this copies all text exactly, but does not transfer
;;    text-properties (which is the behaviour of indirect buffers). It is
;;    possible to configure the default file or mode on a per-object basis.
;;  - chunk: this is designed for programmatic syntaxes where chunks of code are
;;    demarcated by start and end tags, and everything else is commented by
;;    line-start comments. Comments are added or removed between the two buffers.

;; The second of these is extended in lentic-org.el to provide the
;; configuration for this file: there is a normal emacs-lisp file in one buffer
;; and an org-mode version in another. Other programmatic and documentation modes
;; are supported in other files.

;;; Status:

;; This is a beta release, but is now nearly feature complete. The core lentic
;; libraries should hopefully be fairly stable now, however, there is the
;; possibility that it will behave badly and may result in data loss. Please
;; use with care on files with backups.

;; Previous releases of this package were called "linked-buffer". I changed
;; this because I wanted a name for the general idea of text with two
;; visualisations; "linked text" doesn't work because it is sounds like
;; hyperlinked text.

;; Although it is still too early to guarantee, I hope that the current
;; configuration scheme will remain fixed, and subclass extensions should
;; require little change for the future.

;;; Code:

;; #+BEGIN_SRC emacs-lisp
(require 'eieio)
(require 'm-buffer)
(require 'm-buffer-at)
(require 'dash)

(defvar lentic-doc "lenticular.org")
(defvar lentic-doc-html-files '("lenticular.css"))
;; #+end_src

;; ** State

;; This section defines all of the variables that the basic state for lentic
;; is stored in. We deliberately have as few of these as possible, as this
;; makes re-initializing the state during development as straight-forward as
;; possible.

;; We start with `lentic-init' which provides the ability to define some default
;; configuration for a buffer. These are just functions which return
;; `lentic-configuration' objects. This is a slight step of indirection but is
;; essentially there to allow the use of file- or dir-local variables to define
;; the default behaviour for a given buffer. All the values have to be defined by
;; the user as safe, so we do not want too many different values.

;; #+begin_src emacs-lisp
(defvar lentic-init nil
  "Function that initializes lentics for this buffer.

This should be one or a list of functions that each return a
`lentic-configuration' object.")

(make-variable-buffer-local 'lentic-init)
;; #+end_src

;; The `lentic-config' variable stores all of the configuration objects for each
;; lentic-buffer of this-buffer. Each lentic-buffer should have one configuration
;; object and is this configuration object that controls the behaviour and
;; updating of that lentic. As lentics are bi-directional, the `lentic-config'
;; variable should be -- for each lentic-configuration object in this-buffer
;; pointing to that-buffer there should be one in that-buffer pointing to
;; this-buffer. This variable has to `permanent-local' otherwise a new mode (or
;; typing `normal-mode') would break everything.

;; #+begin_src emacs-lisp
(defvar lentic-config nil
  "Configuration for lentic.

This is a list of objects of the class `lentic-configuration'
lentic-configuration', which defines the way in which the text in
the different buffers is kept synchronized. This configuration is
resilient to changes of mode in the current buffer.")

(make-variable-buffer-local 'lentic-config)
(put 'lentic-config 'permanent-local t)

(defvar lentic-counter 0)
(defun lentic-config-name (buffer)
  "Given BUFFER, return a name for the configuration object."
  (format "lentic \"%s:%s\"" buffer (setq lentic-counter (+ 1 lentic-counter))))

;;;###autoload
(defvar lentic-init-functions nil
  "All functions that can be used as `lentic-init' function.")
;; #+end_src

;; ** Base Configuration

;; This section defines the base class and generic methods for all
;; lentic-configuration objects. Most of the properties of this class define the
;; behaviour of the lentic-buffer -- in other words they are configuration.
;; However, there are a few properties which store state about the last
;; before-change event that occured which are used to percolate the changes
;; correctly. This is a handy place to store these, but are not really
;; end-user properties.

;; #+begin_src emacs-lisp
(defclass lentic-configuration ()
  ((this-buffer
    :initarg :this-buffer
    :documentation
    "The this-buffer for this configuration. This should be the
    current-buffer when this configuration is present in `lentic-config'." )
   (that-buffer
    :initarg :that-buffer
    :documentation
    "The that-buffer for this configuration. The that-buffer (if
    live) should a lentic-configuration object for this-buffer in
    its `lentic-config'." )
   (creator
    :initarg :creator ;; FIXME: Not used.
    :initform nil
    :documentation
    "Non-nil if this lentic-configuration was used to create a
    lentic view. This is used to determine the behaviour when the
    buffer is killed: killing the creator kills all views, but killing
    a view does not kill the creator.")
   (delete-on-exit
    :initarg :delete-on-exit
    :initform nil
    :documentation
    "Non-nil if the file associated with this should be deleted on exit.")
   (singleton ;; FIXME: Not used?
    :initarg :singleton
    :initform nil
    :documentation
    "Non-nil if only one lentic (and therefore object) of this type
    can exist for a given buffer.")
   (sync-point
    :initarg :sync-point
    :initform t
    :documentation
    "Non-nil if changes to the location of point in this-buffer
    should be percolated into that-buffer.")
   (last-change-start
    :initarg :last-change-start ;; FIXME: Not used.
    :initform nil
    :documentation
    "The location of the start of the last before-change event.
    This should only be set by lentic.")
   (last-change-start-converted
    :initarg :last-change-start-converted ;; FIXME: Not used.
    :initform nil
    :documentation
    "The location of the start of the last before-change event,
    converted into the equivalent location in that-buffer. This
    should only be set by lentic.")
   (last-change-stop
    :initarg :last-change-stop ;; FIXME: Not used.
    :initform nil
    :documentation
    "The location of the stop of the last before-change event.
    This should only be set by lentic." )
   (last-change-stop-converted
    :initarg :last-change-stop-converted ;; FIXME: Not used.
    :initform nil
    "The location of the stop of the last before-change event,
    converted into the equivalent location in that-buffer. This
    should only be set by lentic."))
  "Configuration object for lentic which defines the behavior of
  the lentic buffer.")
;; #+end_src

;; We define a set of generic methods. I am not entirely sure what the purpose of
;; generic methods are and whether I need them or not; I think it's just a place
;; to put the documentation.

;; #+begin_src emacs-lisp
(cl-defgeneric lentic-create (conf)
  "Create the lentic for this configuration.
Given a `lentic-configuration' object, create the lentic
appropriate for that configurationuration. It is the callers
responsibility to check that buffer has not already been
created.")

(cl-defgeneric lentic-convert (conf location)
  "Convert LOCATION in this-buffer to an equivalent location in
that-buffer. LOCATION is a numeric location, rather than a
marker. By equivalent, we mean the same semantic location as
determined by the transformation between the buffers. It is
possible that a given LOCATION could map to more than one
location in the lentic buffer.")

(cl-defgeneric lentic-clone (conf)
  "Updates that-buffer to reflect the contents in this-buffer.

Updates at least the region that has been given between start and
stop in the this-buffer, into the region start-converted and
stop-converted in that-buffer.

Returns a list of the start location in that-buffer of the
change, the stop location in that-buffer of the change and the
length-before in that buffer of the region changed before the
change, if and only if the changes are exactly that suggested by
the START, STOP, _LENGTH-BEFORE, START-CONVERTED and
STOP-CONVERTED. Otherwise, this should return nil.")
;; #+end_src

;; We need an invert method because we can create the configuration object for a
;; this-buffer without actually creating that-buffer. This may happen at any
;; point in the future. So, the configuration object needs to be able to return
;; it's own inverse. This can be a configuration object of the same class which
;; is normal when the lentic transformation is symmetrical, or a different class
;; which is normal when the lentic transformation is asymmetrical.

;; #+begin_src emacs-lisp
(cl-defgeneric lentic-invert (conf)
  "Return a new configuration object for the lentic buffer.
This method is called at the time that the lentic is created. It
is the callers responsibility to ensure that this is only called
at creation time and not subsequently. The invert function should
only return the configuration object and NOT create the lentic
buffer.")

;; #+end_src

;; `lentic-coexist?' has been created to cope with the case when a buffer has two
;; or more default views. We may wish to re-initialize all the default lentic
;; views. However, this is going to be problematic if some are already there --
;; we will end up with two many. In general, configurations which have been
;; created as a result of calls to the `lentic-init' functions should return
;; false here if there is another call to the same function. Lentic buffers which
;; are being used as a persistent view should generally return true here so that
;; as many can be created as required.

;; #+begin_src emacs-lisp
(cl-defgeneric lentic-coexist? (this-conf that-conf)
  "Return non-nil if THIS-CONF and co-exist with THAT-CONF.
By co-exist this means that both configurations are valid for a
given buffer at the same time. A nil return indicates that there
should only be one of these two for a given buffer.")
;; #+end_src

;; I've implemented `lentic-this' and `lentic-that' as methods although I think I
;; have only over-ridden the implementation once in lentic-delayed which has
;; since been deleted anyway.

;; #+begin_src emacs-lisp
(cl-defmethod lentic-this ((conf lentic-configuration))
  "Returns this-buffer for this configuration object.
In most cases, this is likely to be the `current-buffer' but
this should not be relied on."
  (oref conf this-buffer))

(cl-defmethod lentic-that ((conf lentic-configuration))
  "Returns the that-buffer for this configuration object.
This may return nil if there is not that-buffer, probably because
it has not been created."
  (and (slot-boundp conf 'that-buffer)
       (oref conf that-buffer)))

(cl-defmethod lentic-ensure-that ((conf lentic-configuration))
  "Get the lentic for this configuration
or create it if it does not exist."
  (or (lentic-that conf)
      (lentic-create conf)))
;; #+end_src

;; This part of the user interface is not ideal at the moment. I need something
;; which allows me to see all the currently active lentic-buffers, but I am far
;; from convinced that the mode-line is the best place, since the mode-line gets
;; overly full for most users.

;; As a second problem, supporting mode-line display directly in the
;; configuration object seems right, and breaks the encapsulation between
;; lentic.el and lentic-mode.el. Probably this needs to be replaced by some sort
;; of status keyword return value.

;; #+begin_src emacs-lisp
(cl-defmethod lentic-mode-line-string ((conf lentic-configuration))
  "Returns a mode-line string for this configuration object."
  (when (slot-boundp conf 'that-buffer)
    (let ((that (oref conf that-buffer)))
      (if
          (and that
               (buffer-live-p that))
          "on"
        ""))))
;; #+end_src

;; ** Default Configuration

;; This is the default implementation of a lentic configuration. It provides an
;; identity transformation at that string level -- the two buffers will (should!)
;; have identical `buffer-string' at all times. Or, more strictly, identical
;; without properties, so identical ~(buffer-substring-no-properties (point-min)
;; (point-max))~, which is not nearly so snappy.

;; We add two more properties to this class -- perhaps these should be pushed upwards.

;; #+begin_src emacs-lisp
(defclass lentic-default-configuration (lentic-configuration)
  ((lentic-file
    :initform nil
    :initarg :lentic-file
    :documentation
    "The name of the file that will be associated with that lentic buffer.")
   (lentic-mode
    :initform nil
    :initarg :lentic-mode ;; FIXME: Not used.
    :documentation
    "The mode for that lentic buffer."))
  "Configuration which maintains two lentics with the same contents.")
;; #+end_src

;; We add in a string transformation function here. There has no actual
;; function within lentic per se, but it is used in lentic-dev as something we
;; can advice. This avoids bulking up the code in lentic, while still allows
;; me to affect the transformation during development of new transforms.

;; #+begin_src emacs-lisp
(defun lentic-insertion-string-transform (string)
  "Transform the STRING that is about to be inserted.
This function is not meant to do anything. It's useful to
advice."
  string)
;; #+end_src

;; The default methods should be self-explanatory!

;; #+begin_src emacs-lisp
(cl-defmethod lentic-create ((conf lentic-default-configuration))
  "Create an new lentic buffer. This creates the new buffer sets
the mode to the same as the main buffer or which ever is
specified in the configuration. The current contents of the main
buffer are copied."
  ;; make sure the world is ready for lentic buffers
  (lentic-ensure-hooks)
  ;; create lentic
  (let* ((this-buffer
          (lentic-this conf))
         (that-buffer
          (generate-new-buffer
           (format "*lentic: %s*"
                   (buffer-name
                    this-buffer))))
         (sec-file (oref conf lentic-file))
         (sec-mode
          (or
           ;; the specified normal mode
           (oref conf lentic-mode)
           ;; if we have a file try normal mode
           (if sec-file
               'normal-mode
             ;; otherwise the same mode as the main file
             major-mode))))
    (oset conf creator t)
    ;; make sure this-buffer knows about that-buffer
    (oset conf that-buffer that-buffer)
    ;; init that-buffer with mode, file and config
    ;; the mode must be init'd after adding content in case there are any
    ;; file-local variables need to be evaled
    ;; insert the contents
    (lentic-update-contents conf)
    (with-current-buffer that-buffer
      (when sec-mode
        (funcall sec-mode))
      (when sec-file
        (set-visited-file-name sec-file))
      (setq lentic-config
            (list (lentic-invert conf))))
    that-buffer))

(defun lentic--file-equal-p (f1 f2)
  (let ((a1 (file-attributes f1))
        (a2 (file-attributes f2)))
    (and a1 (equal a1 a2))))

(cl-defmethod lentic-coexist? ((this-conf lentic-default-configuration)
                            that-conf)
  "By default, we can have multiple lentic buffers with the same
configuration, unless specifically disallowed, or unless it has
the same associated file as pre-existing buffer (which is going
to break!)."
  (and
   (not (oref this-conf singleton))
   (not
    (and (oref this-conf lentic-file)
         (oref that-conf lentic-file)
         (lentic--file-equal-p
          (oref this-conf lentic-file)
          (oref that-conf lentic-file))))))

(cl-defmethod lentic-invert ((conf lentic-default-configuration))
  "By default, return a clone of the existing object, but switch
the this and that buffers around. "
  (clone
   conf
   :this-buffer (lentic-that conf)
   :that-buffer (lentic-this conf)
   :sync-point (oref conf sync-point)))

(cl-defmethod lentic-convert ((_conf lentic-default-configuration)
                           location)
  "The two buffers should be identical, so we just return the
  same location."
  location)

(cl-defmethod lentic-clone ((conf lentic-configuration)
                                &optional start stop _length-before
                                start-converted stop-converted)
  "The default clone method cuts out the before region and pastes
in the new."
  (let ((this-b (lentic-this conf))
        (that-b (lentic-that conf)))
    (with-current-buffer this-b
      ;;(lentic-log "this-b (point,start,stop)(%s,%s,%s)" (point) start stop)
      (save-window-excursion
        (save-restriction
          (widen)
          (let* ((start (or start (point-min)))
                 (stop (or stop (point-max))))
            (with-current-buffer that-b
              (save-restriction
                (widen)
                ;; get the start location that we converted before the change.
                ;; lentic-convert is not reliable now, because the two
                ;; buffers do not share state until we have percolated it
                (let ((converted-start
                       (max (point-min)
                            (or start-converted
                                (point-min))))
                      (converted-stop
                       (min (point-max)
                            (or stop-converted
                                (point-max)))))
                  (delete-region converted-start
                                 converted-stop)
                  (save-excursion
                    (goto-char converted-start)
                    ;; so this insertion is happening at the wrong place in block
                    ;; comment -- in fact, it's happening one too early
                    (insert
                     (with-current-buffer this-b
                       ;; want to see where it goes
                       ;; hence the property
                       (lentic-insertion-string-transform
                        (buffer-substring-no-properties
                         start stop))))
                    (list converted-start
                          (+ converted-start (- stop start))
                          (- converted-stop converted-start))))))))))))

;;;###autoload
(defun lentic-default-init ()
  "Default init function.
see `lentic-init' for details."
  (lentic-default-configuration
   :this-buffer (current-buffer)))

(add-to-list 'lentic-init-functions #'lentic-default-init)

;; #+end_src

;; ** Basic Operation

;; In this section, we define some utility functions and the hooks we need into
;; the core Emacs operations.

;; *** Utility

;; We start with some utility macros. These deal with the fact that a buffer can
;; have a lentic or not, and that even if it does that lentic does not need to be
;; live. This happens for instance if a lentic buffer is deleted -- the buffer
;; object will still be live (because the configuration object hangs on to it).

;; At some point, the hook system needs to clean this up by detecting the
;; buffer-kill and removing the configuration objection.

;; #+begin_src emacs-lisp
(defmacro lentic-when-lentic (&rest body)
  "Evaluate BODY when the `current-buffer' has a lentic buffer."
  (declare (debug t))
  `(when (and
          lentic-config
          (-any?
           (lambda (conf)
             (-when-let
                 (buf (lentic-that conf))
               (buffer-live-p buf)))
           lentic-config))
     ,@body))

(defmacro lentic-when-buffer (buffer &rest body)
  "When BUFFER is a live buffer eval BODY."
  (declare (debug t)
           (indent 1))
  `(when (buffer-live-p ,buffer)
     ,@body))

(defmacro lentic-when-with-current-buffer (buffer &rest body)
  "When BUFFER is a live buffer eval BODY with BUFFER current."
  (declare (debug t)
           (indent 1))
  `(lentic-when-buffer ,buffer
     (with-current-buffer ,buffer
       ,@body)))

(defmacro lentic-with-lentic-buffer (buffer &rest body)
  "With BUFFER as current, eval BODY when BUFFER has a lentic."
  (declare (debug t)
           (indent 1))
  `(lentic-when-with-current-buffer ,buffer
     (when lentic-config
       ,@body)))


(defvar lentic-condition-case-disabled
  noninteractive
  "If non-nil throw exceptions from errors.

By default this is set to the value of noninteractive, so that
Emacs crashes with backtraces in batch." )

(defmacro lentic-condition-case-unless-disabled (var bodyform &rest handlers)
  "Like `condition-case' but can be disabled like `condition-case-unless-debug'."
  (declare (debug condition-case) (indent 2))
  `(if lentic-condition-case-disabled
       ,bodyform
     (condition-case-unless-debug ,var
         ,bodyform
       ,@handlers)))

(defmacro lentic-widen (conf &rest body)
  "Widen both buffers in CONF, then evaluate BODY."
  (declare (debug t)
           (indent 1))
  `(with-current-buffer
       (lentic-that ,conf)
     (save-restriction
       (widen)
       (with-current-buffer
           (lentic-this ,conf)
         (save-restriction
           (widen)
           ,@body)))))
;; #+end_src

;; Recurse down the lentic tree to all lentic views.

;; #+begin_src emacs-lisp
(defun lentic-each (buffer fn &optional seen-buffer)
  "Starting at BUFFER, call FN on every lentic-buffer.
FN should take a single argument which is the buffer.
SEEN-BUFFER is a list of buffers to ignore."
  (lentic-with-lentic-buffer buffer
    (setq seen-buffer (cons buffer seen-buffer))
    (-map
     (lambda (conf)
       (let ((that
              (lentic-that conf)))
         (when (and (not (-contains? seen-buffer that))
                  (buffer-live-p that))
           (funcall fn that)
           (lentic-each that fn seen-buffer))))
     lentic-config)))

(defun lentic-garbage-collect-config ()
  "Remove non-live configs in current-buffer."
  (setq lentic-config
        (--filter
         (buffer-live-p
          (lentic-that it))
         lentic-config)))
;; #+end_src

;; *** Initialisation

;; #+begin_src emacs-lisp
(defun lentic-ensure-init ()
  "Ensure that the `lentic-init' has been run."
  (lentic-garbage-collect-config)
  (setq lentic-config
        ;; and attach to lentic-config
        (-concat
         lentic-config
         ;; return only those that can co-exist
         (-filter
          (lambda (this-conf)
            (-all?
             (lambda (that-conf)
               (lentic-coexist? this-conf that-conf))
             lentic-config))
          (-map
           (lambda (init)
             ;; instantiate a new conf object (but do not create the buffer)
             (funcall init))
           (if (not lentic-init)
               '(lentic-default-init)
             (-list lentic-init)))))))

(defun lentic-init-all-create ()
  "Create all lentics fo the current buffer."
  (lentic-ensure-init)
  (-map
   (lambda (conf)
     (if (and
          (slot-boundp conf 'that-buffer)
          (buffer-live-p
           (lentic-that conf)))
         (lentic-that conf)
       (lentic-create conf)))
   (-list lentic-config)))
;; #+end_src

;; *** Hook System

;; The lentic hook system is relatively involved, unfortunately, and will
;; probably become more so. In so far as possible, though, all of the complexity
;; should be here, using the methods provided in the lentic-configuration object.

;; The complexity of the hook system and the fact that it is hooked deeply into
;; the core of Emacs can make it quite hard to debug. There are a number of
;; features put in place to help deal with this. These are:

;;  - A logging system
;;  - An emergency detection system.
;;  - Two part hooks


;; Start by enabling hooks!

;; #+begin_src emacs-lisp
(defun lentic-ensure-hooks ()
  "Ensures that the hooks that this mode requires are in place."
  (add-hook 'post-command-hook
            #'lentic-post-command-hook)
  ;; FIXME: Do we really need these hook functions to affect *all* buffers?
  (add-hook 'after-change-functions
            #'lentic-after-change-function)
  (add-hook 'before-change-functions
            #'lentic-before-change-function)
  (add-hook 'after-save-hook
            #'lentic-after-save-hook)
  (add-hook 'kill-buffer-hook
            #'lentic-kill-buffer-hook)
  (add-hook 'kill-emacs-hook
            #'lentic-kill-emacs-hook))

;; #+end_src

;; The logging system which allows post-mortem analysis of what lentic has done.
;; Originally, my plan was to leave logging in place so aid analysis of bug
;; reports, but this requires so much logging that it the log buffer becomes
;; impossible to analyse.

;; #+begin_src emacs-lisp
(defvar lentic-log nil)
(defmacro lentic-log (&rest rest)
  "Log REST."
  `(when lentic-log
     (lentic-when-lentic
      (let ((msg
             (concat
              (format ,@rest)
              "\n")))
        (princ msg #'external-debugging-output)))))
;; #+end_src

;; An emergency detection system. Several of the hooks in use (post-command-hook,
;; and the before- and after-change-functions) automatically remove hook
;; functions which give errors. In development, this means that all errors are
;; silently ignored and, worse, lentic continues in an inconsistent state with
;; some hooks working and some not. Lentic catches all errors, therefore, and
;; then drops into an "lentic-emergency" state, where all lentic functionality is
;; disabled. This is still a dangerous state as changes do not percolate, but at
;; least it should be predictable. The emergency state can be changed with
;; `lentic-unemergency' and `lentic-emergency'.

;; #+begin_src emacs-lisp

(defvar lentic-emergency  nil
  "Iff non-nil halt all lentic activity.

This is not the same as disabling lentic mode. It stops all
lentic related activity in all buffers; this happens as a result
of an error condition. If lentic was to carry on in these
circumstances, serious data loss could occur. In normal use, this
variable will only be set as a result of a problem with the code;
it is not recoverable from a user perspective.

It is useful to toggle this state on during development. Once
enabled, buffers will not update automaticaly but only when
explicitly told to. This is much easier than try to debug errors
happening on the after-change-hooks. The
function `lentic-emergency' and `lentic-unemergency' functions
enable this.")

(defvar lentic-emergency-debug nil
  "Iff non-nil, lentic will store change data, even
during a `lentic-emergency'.

Normally, `lentic-emergency' disables all activity, but this makes
testing incremental changes charge. With this variable set, lentic will
attempt to store enough change data to operate manually. This does require
running some lentic code (notably `lentic-convert'). This is low
risk code, but may still be buggy, and so setting this variable can cause
repeated errors.")

(defun lentic-emergency ()
  "Stop lentic from working due to code problem."
  (interactive)
  (setq lentic-emergency t)
  (lentic-update-all-display))

(defun lentic-unemergency ()
  "Start lentic working after stop due to code problem."
  (interactive)
  (setq lentic-emergency nil)
  (lentic-update-all-display))

(defun lentic-hook-fail (err hook)
  "Give an informative message when we have to fail.
ERR is the error. HOOK is the hook type."
  (message "lentic mode has failed on \"%s\" hook: %s "
           hook (error-message-string err))
  (lentic-emergency)
  (with-output-to-temp-buffer "*lentic-fail*"
    (princ "There has been an error in lentic-mode.\n")
    (princ "The following is debugging information\n\n")
    (princ (format "Hook: %s\n" hook))
    (princ (error-message-string err)))
  (select-window (get-buffer-window "*lentic-fail*")))
;; #+end_src

;; As a byproduct of the last, lentic also has two part hooks: the real hook
;; function which just handles errors and calls the second function which does
;; the work. This make it possible to call the second function interactively,
;; without catching errors (so that they can be debugged) or causing the
;; lentic-emergency state. There are some utility functions in lentic-dev for
;; running hooks which require arguments.

;; **** General Hook

;; Start by handling saving, killing and general connecting with the Emacs
;; behaviour.

;; #+begin_src emacs-lisp
(defun lentic-after-save-hook ()
  "Error protected call to real after save hook."
  (unless lentic-emergency
    (lentic-condition-case-unless-disabled err
        (lentic-after-save-hook-1)
      (error
       (lentic-hook-fail err "after-save-hook")))))

(defun lentic-after-save-hook-1 ()
  "Respond to a save in the `current-buffer'.
This also saves every lentic which is file-associated."
  (lentic-each
   (current-buffer)
   (lambda (buffer)
     (with-current-buffer
         buffer
       (when (buffer-file-name)
         (save-buffer))))))

(defvar lentic-kill-retain nil
  "If non-nil retain files even if requested to delete on exit.")

(defun lentic-kill-buffer-hook ()
  "Error protected call to real `kill-buffer-hook'."
  (unless lentic-emergency
    (lentic-condition-case-unless-disabled err
        (lentic-kill-buffer-hook-1)
      (error
       (lentic-hook-fail err "kill-buffer-hook")))))

(defvar lentic--killing-p nil)

(defun lentic-kill-buffer-hook-1 ()
  "Respond to any buffer being killed.
If this killed buffer is lentic and is `creator', then kill all
lentic-buffers recursively. If the buffer is `delete-on-exit',
then remove any associated file."
  (lentic-when-lentic
   (when
       (and
        (--any?
         (oref it delete-on-exit)
         lentic-config)
        ;; might not exist if we not saved yet!
        (file-exists-p buffer-file-name)
        ;; if we are cloning in batch, we really do not want to kill
        ;; everything at the end
        (not noninteractive)
        ;; or we have blocked this anyway
        (not lentic-kill-retain))
     (delete-file buffer-file-name))
   ;; if we were the creator buffer, blitz the lentics (which causes their
   ;; files to delete also).
   ;; FIXME: "-p" is for *p*redicates, not boolean values.
   (defvar lentic-killing-p)
   (let ((lentic-killing-p t))
     (when
         (and
          (not lentic-killing-p)
          (--any?
           (oref it creator)
           lentic-config))
       (lentic-each
        (current-buffer)
        #'kill-buffer)))))

(defun lentic-kill-emacs-hook ()
  "Error protected call to real `kill-emacs-hook'."
  (unless lentic-emergency
    (lentic-condition-case-unless-disabled err
        (lentic-kill-emacs-hook-1)
      (error
       (lentic-hook-fail err "kill-emacs-hook")))))

(defun lentic-kill-emacs-hook-1 ()
  "Respond to `kill-emacs-hook.
This removes any files associated with lentics which are
marked as :delete-on-exit."
  (-map
   (lambda (buffer)
     (lentic-with-lentic-buffer
         buffer
       (-map
        (lambda (conf)
          (and
           (oref conf delete-on-exit)
           (file-exists-p buffer-file-name)
           (not noninteractive)
           (delete-file (buffer-file-name))))
        lentic-config)))
   (buffer-list)))
;; #+end_src

;; **** Change Hooks

;; Handling and percolating changes is the most complex part of lentic, made more
;; complex still by the decision to support multiple buffers (why did I do that
;; to myself?).

;; The `post-command-hook' just percolates location of point through all the
;; lentic buffers.

;; #+begin_src emacs-lisp
(defun lentic-post-command-hook ()
  "Update point according to config, with error handling."
  (unless lentic-emergency
    (lentic-condition-case-unless-disabled err
        (progn
          ;; we test for this later anyway, but this makes it easier to debug.
          (when lentic-config
            (lentic-post-command-hook-1 (current-buffer))))
      (error
       (lentic-hook-fail err "post-command-hook")))))

(defun lentic-post-command-hook-1 (buffer &optional seen-buffer)
  "Update point in BUFFER according to config.
SEEN-BUFFER is a list of lentics that have already been updated."
  (lentic-with-lentic-buffer
      buffer
    ;; now we have seen this buffer don't look again
    (setq seen-buffer (cons buffer seen-buffer))
    ;; for all configurations
    (-map
     (lambda (config)
       (let ((that
              (lentic-that config)))
         ;; check for the termination condition
         (unless (-contains? seen-buffer that)
           (lentic-when-buffer
               that
             ;; then update and recurse
             (lentic-update-point config))
           (lentic-post-command-hook-1 (lentic-that config) seen-buffer))))
     lentic-config)))
;; #+end_src

;; The `after-change-function' is by far the most complex of the hooks. This is because
;; we have to percolate the changes from the buffer that has changed as a result
;; of the user doing something to all the other buffers. In theory, this should be
;; straight-forward: combined with the `before-change-function', the data from
;; the `after-change-function' defines a "dirty region" which we need to update
;; by copying from the parent and then doing what ever transformation needs to
;; happen. The problem is that that the contract from the configuration objects'
;; `lentic-clone' method is that *at least* the dirty region will be replaced.
;; `lentic-clone' can actually replace much more than this, and often needs to do
;; so, to ensure a consistent transformation.

;; So, when a lentic-buffer updates it needs to update it's own dirty region but
;; also return the dirty region that it has created, so that any lentic buffers
;; that it in turn has that are still to be updated can be so. Or, if it doesn't,
;; we just assume the whole buffer is dirty which is safe but inefficient.

;; The main after-change-function also stores the its arguments if we are in
;; debug mode which allows me to run `lentic-after-change-function-1'
;; interactively with the correct arguments.

;; #+begin_src emacs-lisp
(defvar lentic-emergency-last-change nil)
(make-variable-buffer-local 'lentic-emergency-last-change)

(defun lentic-after-change-transform (_buffer _start _stop _length-before)
  "Function called after every change percolated by lentic.
This function does nothing and is meant for advising. See
lentic-dev."
)

(defun lentic-after-change-function (start stop length-before)
  "Run change update according to `lentic-config'.
Errors are handled.
START is at most the start of the change.
STOP is at least the end of the change.
LENGTH-BEFORE is the length of the area before the change."
  ;; store values in case we want to use them
  (when lentic-emergency-debug
    (setq lentic-emergency-last-change (list start stop length-before)))
  (unless lentic-emergency
    (lentic-condition-case-unless-disabled err
        (lentic-after-change-function-1
         (current-buffer) start stop length-before)
      (error
       (lentic-hook-fail err "after change")))))

(defun lentic-after-change-function-1
    (buffer start stop
            length-before &optional seen-buffer)
  "Run change update according to `lentic-config'.
BUFFER is the changed buffer.
START is at most the start of the change.
STOP is at least the end of the change.
LENGTH-BEFORE is the length of the area before the change.
SEEN-BUFFER is a list of buffers to which we have already percolated
the change."
  (lentic-with-lentic-buffer buffer
    (setq seen-buffer (cons buffer seen-buffer))
    (-map
     (lambda (config)
       (unless
           (or (-contains? seen-buffer (lentic-that config))
               (not (buffer-live-p (lentic-that config))))
         (let ((updates
                (or
                 (lentic-update-contents config
                                         start stop length-before)
                 '(nil nil nil))))
           (apply #'lentic-after-change-transform
                  (lentic-that config)
                  updates)
           (lentic-after-change-function-1
            (lentic-that config)
            (nth 0 updates)
            (nth 1 updates)
            (nth 2 updates)
            seen-buffer))))
     lentic-config)))
;; #+end_src

;; We also need to store the location of the area to be changed before the change
;; happens. Further, we need to convert this at this time to the cognate
;; positions in the lentic buffers. This is because it is only before the change
;; that this-buffer and the lentic buffers are in a consistent state; after the
;; change, this-buffer will have changes not percolated to other buffers. By
;; making this conversion now, we can ease the implementation of the
;; `lentic-convert' function because it does not have to cope with buffers with
;; inconsistent content.


;; #+begin_src emacs-lisp
(defun lentic-before-change-function (start stop)
  "Error protected call to real `before-change-function'.
START is at most the start of the change.
STOP is at least the end of the change."
  (unless (and
           lentic-emergency
           (not lentic-emergency-debug))
    (lentic-condition-case-unless-disabled err
        (lentic-before-change-function-1 (current-buffer) start stop)
      (error
       (lentic-hook-fail err "before change")))))

(defun lentic-before-change-function-1 (buffer start stop &optional seen-buffer)
  "Calculate change position in all lentic buffers.
BUFFER is the buffer being changed.
START is at most the start of the change.
STOP is at least the end of the change.
SEEN-BUFFER is a list of buffers to which the change has been percolated."
  (lentic-with-lentic-buffer buffer
    (setq seen-buffer (cons buffer seen-buffer))
    (-map
     (lambda (config)
       (unless
           (or (-contains? seen-buffer (lentic-that config))
               ;; convert uses that buffer
               (not (buffer-live-p (lentic-that config))))
         (lentic-widen
             config
           (oset config last-change-start start)
           (oset config
                 last-change-start-converted
                 (lentic-convert
                  config
                  start))
           (oset config last-change-stop stop)
           (oset config
                 last-change-stop-converted
                 (lentic-convert
                  config
                  stop))
           (lentic-before-change-function-1
            (lentic-that config)
            (oref config last-change-start-converted)
            (oref config last-change-stop-converted)
            seen-buffer))))
     lentic-config)))
;; #+end_src

;; The `lentic-update-contents' actually transfers changes from one buffer to all
;; the lentics. Unfortunately before-change-function and after-change-function
;; are not always consistent with each other. So far the main culprit I have
;; found is `subst-char-in-region', which is used under the hood of
;; `fill-paragraph'. On the b-c-f this reports the real start of the change and
;; the *maximal* end, while on the a-c-f it reports both the real start and real
;; end. Unfortunately, we did our conversion to the cognate positions in the
;; b-c-f *and* we need these values.

;; The overestimate give inconsistency between the length before on a-c-f (which
;; is the actual length) and the difference between b-c-f start and stop (which
;; is the maximal change). Unfortunately, this can also occur in some correct
;; circumstances -- replace-match for example can both insert and change
;; simultaneously.

;; So, the only solution that I have is to use a heuristic to detect /skew/ --
;; when I think the b-c-f and a-c-f are inconsistent, and if it finds it, then
;; use a full clone (i.e. the whole buffer is dirty).

;; I need to do a full survey of all the functions that call b-c-f/a-c-f (there
;; are not that many of them!) and rewrite them to all do-the-right thing. Need
;; to learn C first!

;; #+begin_src emacs-lisp
(defun lentic-update-contents (conf &optional start stop length-before)
  "Update the contents of that-buffer with the contents of this-buffer.
update mechanism depends on CONF.
START is at most the start of the change.
STOP is at least the end of the change.
LENGTH-BEFORE is the length of area before the change."
  (let ((inhibit-read-only t)
        (no-fall-back
         (and start stop length-before)))
    (when
        (and no-fall-back
             (< (+ start length-before) (oref conf last-change-stop)))
      (let ((diff
             (- (oref conf last-change-stop)
                (+ start length-before))))
        (lentic-log "Skew detected %s" this-command)
        (cl-incf length-before diff)
        (cl-incf stop diff)))
    (m-buffer-with-markers
        ((start-converted
          (when
              (and no-fall-back
                   (oref conf last-change-start-converted))
            (set-marker (make-marker)
                        (oref conf last-change-start-converted)
                        (lentic-that conf))))
         (stop-converted
          (when
              (and no-fall-back
                   (oref conf last-change-stop-converted))
            (set-marker (make-marker)
                        (oref conf last-change-stop-converted)
                        (lentic-that conf)))))
      ;; used these, so dump them
      (oset conf last-change-start nil)
      (oset conf last-change-start-converted nil)
      (oset conf last-change-stop nil)
      (oset conf last-change-stop-converted nil)
      (lentic-widen
          conf
        (if (not no-fall-back)
            (lentic-clone conf)
          (lentic-clone conf start stop length-before
                        start-converted stop-converted))))))

(defun lentic-update-point (conf)
  "Update the location of point in that-buffer to reflect this-buffer.
This also attempts to update any windows so that they show the
same top-left location. Update details depend on CONF."
  ;; only sync when we are told to!
  (when (oref conf sync-point)
    (let* ((from-point
            (lentic-convert
             conf
             (m-buffer-at-point
              (lentic-this conf))))
           (from-window-start
            (lentic-convert
             conf
             (window-start
              (get-buffer-window
               (lentic-this conf))))))
      ;; clone point in buffer important when the buffer is NOT visible in a
      ;; window at all
      ;;(lentic-log "sync(front-point)(%s)" from-point)
      (with-current-buffer
          (lentic-that conf)
        (goto-char from-point))
      ;; now clone point in all the windows that are showing the buffer
      ;; and set the start of the window which is a reasonable attempt to show
      ;; the same thing.
      (mapc
       (lambda (window)
         (with-selected-window window
           (progn
             (goto-char from-point)
             (set-window-start window from-window-start))))
       (get-buffer-window-list (lentic-that conf))))))

;; #+end_src

;; Ugly, ugly, ugly. Not happy with mode-line behaviour anyway, so this will
;; probably change into the future.

;; #+begin_src emacs-lisp
;; put this here so we don't have to require lentic-mode to ensure that the
;; mode line is updated.
(defun lentic-update-display ()
  "Update the display with information about lentic's state."
  (when (fboundp 'lentic-mode-update-mode-line)
    (lentic-mode-update-mode-line)))

(defun lentic-update-all-display ()
  (when (fboundp 'lentic-mode-update-all-display)
    (lentic-mode-update-all-display)))
;; #+end_src


;; *** Utility

;; Just a couple of convenience functions for operating on eieio objects. The
;; native `oset' only allows setting a single property-value pair which is
;; irritating syntactically, and it does not return the object which prevents
;; function chaining. Taken together, these really simplify construction of
;; objects.

;; #+begin_src emacs-lisp
(defun lentic-m-oset (obj &rest plist)
  "On OBJ set all properties in PLIST.
Returns OBJ. See also `lentic-a-oset'"
  (lentic-a-oset obj plist))

(defun lentic-a-oset (obj plist)
  "On OBJ, set all properties in PLIST.
This is a utility function which just does the same as oset, but
for lots of things at once. Returns OBJ."
  (dolist (n (-partition 2 plist))
    (eieio-oset obj (car n) (cadr n)))
  obj)
;; #+end_src

;; ** Batch Functions

;; These functions are for batch operation on lentic buffers. Mostly, these
;; are useful for writing tests, but they can be useful for generating
;; the lentic form of a file during any automated pipeline.

;; #+begin_src emacs-lisp
(defun lentic-batch-clone-and-save-with-config (filename init)
  "Open FILENAME, set INIT function, then clone and save.

This function does potentially evil things if the file or the
lentic is open already."
  (let ((retn))
    (with-current-buffer
        (find-file-noselect filename)
      (setq lentic-init init)
      (with-current-buffer
          (car
           (lentic-init-all-create))
        (setq retn lentic-config)
        (save-buffer)
        (kill-buffer))
      (kill-buffer))
    retn))

(defun lentic-batch-clone-with-config
  (filename init)
  "Open FILENAME, set INIT function, then clone.

Return the lentic contents without properties."
  (let ((retn nil))
    (with-current-buffer
        (find-file-noselect filename)
      (setq lentic-init init)
      (with-current-buffer
          (car
           (lentic-init-all-create))
        (setq retn
              (buffer-substring-no-properties
               (point-min)
               (point-max)))
        (set-buffer-modified-p nil)
        ;; don't delete -- we haven't saved but there
        ;; might already be a file with the same name,
        ;; which will get deleted
        (oset (car lentic-config) delete-on-exit nil)
        (kill-buffer))
      (set-buffer-modified-p nil)
      (kill-buffer))
    retn))

(provide 'lentic)

;;; lentic.el ends here
;; #+END_SRC
