;; Copyright 2015
;; Kaz Kylheku <kaz@kylheku.com>
;; Vancouver, Canada
;; All rights reserved.
;;
;; Redistribution of this software in source and binary forms, with or without
;; modification, is permitted provided that the following two conditions are met.
;;
;; Use of this software in any manner constitutes agreement with the disclaimer
;; which follows the two conditions.
;;
;; 1. Redistributions of source code must retain the above copyright
;;    notice, this list of conditions and the following disclaimer.
;; 2. 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.
;;
;; THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
;; WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
;; MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  IN NO EVENT SHALL THE
;; COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DAMAGES, HOWEVER CAUSED,
;; AND UNDER ANY THEORY OF LIABILITY, ARISING IN ANY WAY OUT OF THE USE OF THIS
;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

(progn
  (macro-time
    (defvar *place-clobber-expander* (hash))
    (defvar *place-update-expander* (hash))
    (defvar *place-delete-expander* (hash))
    (defvar sys:*lisp1* nil)

    (defun sys:eval-err (. params)
      (throwf 'eval-error . params))

    (defmacro sys:l1-setq (sym new-val :env e)
      (caseq (lexical-lisp1-binding e sym)
        (:var ^(sys:setq ,sym ,new-val))
        (:symacro (sys:eval-err "sys:l1-setq: invalid use on symbol macro"))
        (t (if (boundp sym)
             ^(sys:setq ,sym ,new-val)
             ^(sys:lisp1-setq ,sym ,new-val)))))

    (defmacro sys:l1-val (u-expr :env e)
      (let ((e-expr (macroexpand u-expr e)))
        (if (and (symbolp e-expr) (not (constantp e-expr)))
          (caseq (lexical-lisp1-binding e e-expr)
            (:fun ^(fun ,u-expr))
            (:var u-expr)
            (nil (if (boundp e-expr)
                   u-expr
                   ^(sys:lisp1-value ,u-expr)))
            (t (sys:eval-err "sys:l1-val: invalid case")))
          u-expr)))

    (defun sys:sym-update-expander (getter-name setter-name
                                    place-expr op-body)
      (if sys:*lisp1*
        ^(macrolet ((,getter-name () ^(sys:l1-val ,',place-expr))
                    (,setter-name (val-expr) ^(sys:l1-setq ,',place-expr
                                                           ,val-expr)))
           ,op-body)
        ^(macrolet ((,getter-name () ',place-expr)
                    (,setter-name (val-expr) ^(sys:setq ,',place-expr
                                                        ,val-expr)))
           ,op-body)))

    (defun sys:sym-clobber-expander (simple-setter-name
                                     place-expr op-body)
      ^(macrolet ((,simple-setter-name (val-expr)
                     ^(,(if sys:*lisp1* 'sys:l1-setq 'sys:setq)
                        ,',place-expr ,val-expr)))
         ,op-body))

    (defun sys:sym-delete-expander (deleter-name
                                    place-expr . op-body)
      ^(macrolet ((,deleter-name (:env env)
                    (when (lexical-var-p env ',place-expr)
                      (sys:eval-err "~s is a lexical variable, thus not deletable"
                                    ',place-expr))
                    ^(prog1
                       (symbol-value ',',place-expr)
                       (makunbound ',',place-expr))))
         ,*op-body))

    (defun get-update-expander (place)
      (cond
        ((symbolp place) (fun sys:sym-update-expander))
        ((consp place) (or [*place-update-expander* (car place)]
                           (sys:eval-err "~s is not an assignable place" place)))
        (t (sys:eval-err "form ~s is not syntax denoting an assignable place" place))))

    (defun get-clobber-expander (place)
      (cond
        ((symbolp place) (fun sys:sym-clobber-expander))
        ((consp place) (or [*place-clobber-expander* (car place)]
                           (iflet ((fun [*place-update-expander* (car place)]))
                              (op apply fun (gensym) @1 @2 @rest))
                           (sys:eval-err "~s is not an assignable place" place)))
        (t (sys:eval-err "form ~s is not syntax denoting an assignable place" place))))

    (defun get-delete-expander (place)
      (cond
        ((symbolp place) (fun sys:sym-delete-expander))
        ((consp place) (or [*place-delete-expander* (car place)]
                           (sys:eval-err "~s is not a deletable place" place)))
        (t (sys:eval-err "form ~s is not syntax denoting a deletable place" place)))))

  (defmacro rlet (bindings :env e . body)
    (let ((exp-bindings (mapcar (aret ^(,@1 ,(macroexpand @2 e))) bindings)))
      (let ((renames [keep-if constantp exp-bindings second])
            (regular [remove-if constantp exp-bindings second]))
        (cond ((and renames regular)
                ^(symacrolet ,renames
                   (let ,regular ,*body)))
              (renames ^(symacrolet ,renames ,*body))
              (regular ^(let ,regular ,*body))
              (t ^(progn ,*body))))))

  (defmacro with-gensyms (syms . body)
    ^(let ,(zip syms (repeat '((gensym)))) ,*body))

  (macro-time
    (defun call-update-expander (getter setter unex-place env body)
      (let* ((place (sys:expand unex-place env))
             (expander (get-update-expander place)))
        [expander getter setter place body]))

    (defun call-clobber-expander (ssetter unex-place env body)
      (let* ((place (sys:expand unex-place env))
             (expander (get-clobber-expander place)))
        [expander ssetter place body]))

    (defun call-delete-expander (deleter unex-place env body)
      (let* ((place (sys:expand unex-place env))
             (expander (get-delete-expander place)))
        [expander deleter place body])))

  (defmacro with-update-expander ((getter setter) unex-place env body)
    ^(with-gensyms (,getter ,setter)
       (call-update-expander ,getter ,setter ,unex-place ,env ,body)))

  (defmacro with-clobber-expander ((ssetter) unex-place env body)
    ^(with-gensyms (,ssetter)
       (call-clobber-expander ,ssetter ,unex-place ,env ,body)))

  (defmacro with-delete-expander ((deleter) unex-place env body)
    ^(with-gensyms (,deleter)
       (call-delete-expander ,deleter ,unex-place ,env ,body)))

  (defmacro set (:env env . place-value-pairs)
    (let ((assign-forms (mapcar (tb ((place : (value nil value-present-p)))
                                  (unless value-present-p
                                    (sys:eval-err "set: arguments must be pairs"))
                                  (with-clobber-expander (ssetter) place env
                                     ^(,ssetter ,value)))
                                (tuples 2 place-value-pairs))))
      (if (cdr assign-forms)
        ^(progn ,*assign-forms)
        (car assign-forms))))

  (defmacro pset (:env env . place-value-pairs)
    (let ((len (length place-value-pairs)))
      (cond
        ((oddp len) (sys:eval-err "pset: arguments must be pairs"))
        ((<= len 2) ^(set ,*place-value-pairs))
        (t (let* ((pvtgs (mapcar (tb ((a b))
                                   (list a b (gensym) (gensym) (gensym)))
                                 (tuples 2 place-value-pairs)))
                  (ls (reduce-left (tb ((lets stores) (place value temp getter setter))
                                       (list ^((,temp ,value) ,*lets)
                                             ^((,setter ,temp) ,*stores)))
                                   pvtgs '(nil nil)))
                  (lets (first ls))
                  (stores (second ls))
                  (body-form ^(let (,*lets) ,*stores)))
             (reduce-left (tb (accum-form (place value temp getter setter))
                            (call-update-expander getter setter
                                                  place env accum-form))
                          pvtgs body-form))))))

  (defmacro zap (place :env env)
    (with-update-expander (getter setter) place env
      ^(prog1 (,getter) (,setter nil))))

  (defmacro flip (place :env env)
    (with-update-expander (getter setter) place env
      ^(,setter (not (,getter)))))

  (defmacro inc (place : (delta 1) :env env)
    (with-update-expander (getter setter) place env
      (caseql delta
        (0 ^(,setter (,getter)))
        (1 ^(,setter (succ (,getter))))
        (2 ^(,setter (ssucc (,getter))))
        (3 ^(,setter (sssucc (,getter))))
        (t ^(,setter (+ (,getter) ,delta))))))

  (defmacro dec (place : (delta 1) :env env)
    (with-update-expander (getter setter) place env
      (caseql delta
        (0 ^(,setter (,getter)))
        (1 ^(,setter (pred (,getter))))
        (2 ^(,setter (ppred (,getter))))
        (3 ^(,setter (pppred (,getter))))
        (t ^(,setter (- (,getter) ,delta))))))

  (defmacro swap (place-0 place-1 :env env)
    (with-gensyms (tmp)
      (with-update-expander (getter-0 setter-0) place-0 env
        (with-update-expander (getter-1 setter-1) place-1 env
          ^(let ((,tmp (,getter-0)))
             (,setter-0 (,getter-1))
             (,setter-1 ,tmp))))))

  (defmacro push (new-item place :env env)
    (with-gensyms (new-sym)
      ^(let ((,new-sym ,new-item))
         ,(with-update-expander (getter setter) place env
            ^(,setter (cons ,new-sym (,getter)))))))

  (defmacro pop (place :env env)
    (with-gensyms (tmp)
      (with-update-expander (getter setter) place env
        ^(let ((,tmp (,getter)))
           (prog1 (car ,tmp) (,setter (cdr ,tmp)))))))

  (defmacro pushnew (new-item place :env env :
                              (testfun :)
                              (keyfun :))
    (with-update-expander (getter setter) place env
      (with-gensyms (new-item-sym old-list-sym)
        ^(let ((,new-item-sym ,new-item))
           ,(with-update-expander (getter setter) place env
              ^(let ((,old-list-sym (,getter)))
                 (if (member ,new-item-sym ,old-list-sym ,testfun ,keyfun)
                   ,old-list-sym
                   (,setter (cons ,new-item-sym ,old-list-sym)))))))))

  (defmacro shift (:env env . places)
    (tree-case places
      (() (sys:eval-err "shift: need at least two arguments"))
      ((place) (sys:eval-err "shift: need at least two arguments"))
      ((place newvalue)
       (with-update-expander (getter setter) place env
          ^(prog1 (,getter) (,setter ,newvalue))))
      ((place . others)
        (with-update-expander (getter setter) place env
          ^(prog1 (,getter) (,setter (shift ,*others)))))))

  (defmacro rotate (:env env . places)
    (tree-case places
      (() ())
      ((fplace) fplace)
      ((fplace . rplaces)
       (with-gensyms (tmp)
         (with-update-expander (getter-f setter-f) fplace env
           ^(let ((,tmp (,getter-f)))
              (,setter-f (shift ,*rplaces ,tmp))
              ,tmp))))))

  (defmacro del (place :env env)
    (with-delete-expander (deleter) place env
      ^(,deleter)))

  (defmacro defplace (place-destructuring-args body-sym
                      (getter-sym setter-sym update-body) :
                      ((ssetter-sym clobber-body))
                      ((deleter-sym delete-body)))
    (symacrolet ((name (car place-destructuring-args))
                 (args (cdr place-destructuring-args)))
      (unless (and name
                   (symbolp name)
                   (not (keywordp name))
                   (not (eq t name)))
        (sys:eval-err "~s: ~s cannot be used as a place name"
                  'defplace name))
      (with-gensyms (place)
        ^(macro-time
           (sethash *place-update-expander* ',name
             (lambda (,getter-sym ,setter-sym ,place ,body-sym)
               (tree-bind ,args (cdr ,place)
                  ,update-body)))
           ,*(if ssetter-sym
              ^((sethash *place-clobber-expander* ',name
                  (lambda (,ssetter-sym ,place ,body-sym)
                    (tree-bind ,args (cdr ,place)
                       ,clobber-body)))))
           ,*(if deleter-sym
               ^((sethash *place-delete-expander* ',name
                   (lambda (,deleter-sym ,place ,body-sym)
                     (tree-bind ,args (cdr ,place)
                        ,delete-body)))))))))

  (defplace (car cell) body
    (getter setter
      (with-gensyms (cell-sym)
        ^(rlet ((,cell-sym ,cell))
           (macrolet ((,getter () ^(car ,',cell-sym))
                      (,setter (val) ^(sys:rplaca ,',cell-sym ,val)))
             ,body))))
    (ssetter
      ^(macrolet ((,ssetter (val) ^(sys:rplaca ,',cell ,val)))
        ,body))
    (deleter
      ^(macrolet ((,deleter () ^(pop ,',cell)))
         ,body)))

  (defplace (cdr cell) body
    (getter setter
      (with-gensyms (cell-sym)
        ^(rlet ((,cell-sym ,cell))
           (macrolet ((,getter () ^(cdr ,',cell-sym))
                      (,setter (val) ^(sys:rplacd ,',cell-sym ,val)))
             ,body))))
    (ssetter
      ^(macrolet ((,ssetter (val) ^(sys:rplacd ,',cell ,val)))
         ,body))
    (deleter
      ^(macrolet ((,deleter () ^(zap (cdr ,',cell))))
         ,body)))

   (defplace (vecref vector index :whole args) body
     (getter setter
       (with-gensyms (vec-sym ind-sym)
         ^(rlet ((,vec-sym ,vector)
                 (,ind-sym ,index))
            (macrolet ((,getter () ^(vecref ,',vec-sym ,',ind-sym))
                       (,setter (val) ^(refset ,',vec-sym ,',ind-sym ,val)))
               ,body))))
     (ssetter
       ^(macrolet ((,ssetter (val) ^(refset ,*',args ,val)))
          ,body))
     (deleter
       (with-gensyms (vec-sym ind-sym)
         ^(rlet ((,vec-sym ,vector)
                 (,ind-sym ,index))
            (macrolet ((,deleter ()
                          ^(prog1 (vecref ,',vec-sym ,',ind-sym)
                                  (replace-vec ,',vec-sym nil
                                               ,',ind-sym (succ ,',ind-sym)))))
              ,body)))))

   (defplace (chr-str string index :whole args) body
     (getter setter
       (with-gensyms (str-sym ind-sym)
         ^(rlet ((,str-sym ,string)
                 (,ind-sym ,index))
            (macrolet ((,getter () ^(chr-str ,',str-sym ,',ind-sym))
                       (,setter (val) ^(chr-str-set ,',str-sym ,',ind-sym ,val)))
               ,body))))
     (ssetter
       ^(macrolet ((,ssetter (val) ^(chr-str-set ,*',args ,val)))
          ,body))
     (deleter
       (with-gensyms (str-sym ind-sym)
         ^(rlet ((,str-sym ,string)
                 (,ind-sym ,index))
            (macrolet ((,deleter ()
                          ^(prog1 (chr-str ,',str-sym ,',ind-sym)
                                  (replace-str ,',str-sym nil
                                               ,',ind-sym (succ ,',ind-sym)))))
              ,body)))))

   (defplace (ref seq index :whole args) body
     (getter setter
       (with-gensyms (seq-sym ind-sym)
         ^(rlet ((,seq-sym ,seq)
                 (,ind-sym ,index))
            (macrolet ((,getter () ^(ref ,',seq-sym ,',ind-sym))
                       (,setter (val) ^(refset ,',seq-sym ,',ind-sym ,val)))
               ,body))))
     (ssetter
       ^(macrolet ((,ssetter (val) ^(refset ,*',args ,val)))
          ,body))
     (deleter
       (with-gensyms (seq-sym ind-sym)
         ^(rlet ((,seq-sym ,seq)
                 (,ind-sym ,index))
            (macrolet ((,deleter ()
                          ^(prog1 (ref ,',seq-sym ,',ind-sym)
                                  (replace ,',seq-sym nil
                                           ,',ind-sym (succ ,',ind-sym)))))
              ,body)))))

   (defplace (gethash hash key : (default nil have-default-p)) body
     (getter setter
       (with-gensyms (entry-sym)
         ^(let ((,entry-sym (inhash ,hash ,key ,default)))
            (macrolet ((,getter () ^(cdr ,',entry-sym))
                       (,setter (val) ^(sys:rplacd ,',entry-sym ,val)))
               ,body))))
     :
     (deleter
       ^(macrolet ((,deleter ()
                     (if ,have-default-p
                       (with-gensyms (entry-sym
                                      dfl-sym)
                         ^(rlet ((,entry-sym (inhash ,',hash ,',key))
                                 (,dfl-sym ,',default))
                            (if ,entry-sym
                              (remhash ,',hash ,',key)
                              ,dfl-sym)))
                       ^(remhash ,',hash ,',key))))
          ,body)))

   (defplace (dwim obj-place index : (default nil have-default-p) :env env) body
     (getter setter
       (with-gensyms (ogetter-sym osetter-sym obj-sym
                      oldval-sym newval-sym
                      index-sym index-sym
                      oldval-sym dflval-sym)
         (let ((sys:*lisp1* (or (symbolp obj-place) sys:*lisp1*)))
           (with-update-expander (ogetter-sym osetter-sym) obj-place nil
             ^(rlet ((,obj-sym (,ogetter-sym))
                     (,index-sym (sys:l1-val ,index))
                     ,*(if have-default-p
                         ^((,dflval-sym (sys:l1-val ,default)))))
                (let ((,oldval-sym [,obj-sym
                                   ,index-sym
                                   ,*(if have-default-p ^(,dflval-sym))]))
                  (macrolet ((,getter () ',oldval-sym)
                             (,setter (val)
                                ^(rlet ((,',newval-sym ,val))
                                   (,',osetter-sym
                                     (sys:dwim-set ,',obj-sym
                                                   ,',index-sym ,',newval-sym))
                                   ,',newval-sym)))
                     ,body)))))))
     (ssetter
       (with-gensyms (osetter-sym ogetter-sym
                      obj-sym newval-sym index-sym)
         (let ((sys:*lisp1* (or (symbolp obj-place) sys:*lisp1*)))
           (with-update-expander (ogetter-sym osetter-sym) obj-place nil
             ^(macrolet ((,ssetter (val)
                          ^(rlet ((,',obj-sym (,',ogetter-sym))
                                  (,',index-sym (sys:l1-val ,',index))
                                  (,',newval-sym ,val))
                             (,',osetter-sym
                               (sys:dwim-set ,',obj-sym
                                             ,*(if ,have-default-p
                                                 ^((prog1
                                                     ,',index-sym
                                                     (sys:l1-val ,',default)))
                                                 ^(,',index-sym))
                                             ,',newval-sym))
                             ,',newval-sym)))
                  ,body)))))

     (deleter
       (with-gensyms (osetter-sym ogetter-sym
                      obj-sym index-sym oldval-sym
                      dflval-sym)
         (let ((sys:*lisp1* (or (symbolp obj-place) sys:*lisp1*)))
           (with-update-expander (ogetter-sym osetter-sym) obj-place nil
             ^(macrolet ((,deleter ()  ;; todo: place must not have optional val
                          ^(rlet ((,',obj-sym (,',ogetter-sym)))
                             (let* ((,',index-sym (sys:l1-val ,',index))
                                    (,',oldval-sym [,',obj-sym
                                                    ,',index-sym
                                                    ,*(if ,have-default-p
                                                        ^(,',default))]))
                               (progn
                                 (,',osetter-sym
                                   (sys:dwim-del ,',obj-sym ,',index-sym))
                                 ,',oldval-sym)))))
                  ,body))))))

   (defplace (force promise) body
     (getter setter
       (with-gensyms (promise-sym)
         ^(rlet ((,promise-sym ,promise))
             (macrolet ((,getter ()
                           ^(force ,',promise-sym))
                        (,setter (val)
                           ^(set (car (cdr ,',promise-sym)) ,val)))
               ,body))))
     (ssetter
       (with-gensyms (promise-sym)
         ^(rlet ((,promise-sym ,promise))
            (macrolet ((,ssetter (val)
                         ^(prog1
                            (set (car (cdr ,',promise-sym)) ,val)
                            (set (car ,',promise-sym) 'sys:promise-forced))))
              ,body)))))

   (defplace (errno) body
     (getter setter
       ^(macrolet ((,getter () '(errno))
                   (,setter (val-expr)
                      (with-gensyms (val-sym)
                        ^(rlet ((,val-sym ,val-expr))
                            (progn (errno ,val-sym) ,val-sym)))))
          ,body)))

   (defplace (fun sym) body
     (getter setter
       ^(macrolet ((,getter () ^(fun ,',sym))
                   (,setter (val) ^(sys:setqf ,',sym ,val)))
          ,*body))
     :
     (deleter
       ^(macrolet ((,deleter (:env env)
                      (when (lexical-fun-p env ',sym)
                        (sys:eval-err "~s is a lexical function, \
                                      \ thus not deletable"
                                    ',sym))
                       ^(fmakunbound ',',sym)))
          ,*body)))

   (defun sys:get-fb (sym)
     (or (gethash sys:top-fb sym)
         (sys:eval-err "unbound function ~s" sym)))

   (defplace (symbol-function sym-expr) body
     (getter setter
       (with-gensyms (binding-sym)
         ^(let ((,binding-sym (sys:get-fb ,sym-expr)))
             (macrolet ((,getter () ^(cdr ,',binding-sym))
                        (,setter (val) ^(sys:rplacd ,',binding-sym ,val)))
               ,*body))))
     :
     (deleter
       ^(macrolet ((,deleter () ^(fmakunbound ,',sym-expr)))
          ,*body)))

   (defun sys:get-vb (sym)
     (or (gethash sys:top-vb sym)
         (sys:eval-err "unbound variable ~s" sym)))

   (defplace (symbol-value sym-expr) body
     (getter setter
       (with-gensyms (binding-sym)
         ^(let ((,binding-sym (sys:get-vb ,sym-expr)))
             (macrolet ((,getter () ^(cdr ,',binding-sym))
                        (,setter (val) ^(sys:rplacd ,',binding-sym ,val)))
               ,*body))))
     :
     (deleter
       ^(macrolet ((,deleter () ^(makunbound ,',sym-expr)))
          ,*body)))

   (macro-time
     (each ((from '(car cdr))
            (to '(first rest)))
       (each ((table (list *place-update-expander*
                           *place-clobber-expander*
                           *place-delete-expander*)))
         (set [table to] [table from]))))

   (defmacro define-modify-macro (name lambda-list function)
     (let ((cleaned-lambda-list (mapcar [iffi consp car]
                                        (remql : lambda-list))))
       (with-gensyms (place-sym args-sym)
         ^(defmacro ,name (:env env ,place-sym ,*lambda-list)
            (with-update-expander (getter setter) ,place-sym env
              ^(,setter (,',function (,getter) ,,*cleaned-lambda-list))))))))