(ns buddho.macros.dhamma
  (:require [clojure.set :as set]))

(declare defdhamma)

(defmacro defconstructor
  "Create a dhamma constructor.
  For `dh-name`, constructor function will be bound to `dh-name!`.

  Intended for use with `defdhamma`; design may seem insane outside of that context.

  The `:meta` and `:data` kwargs take a binding-to-field symbol-to-keyword map,
  where the domain specifies the (keyword) arguments given to the constructor,
  and the codomain specifies where in the resulting dhamma object they will end up,
  with metadata additionally namespaced under the dhamma name.

  The `:defaults` kwarg gives defaults for what will be the kwargs _of the constructor_.

  The `:dh-defaults` kwarg gives dhamma construction default arguments,
  which may be overridden by `:dh-args` in the constructor.

  Example usage:
  (defconstructor example
    :meta {foo :foo-dest}
    :data {bar :bar-dest}
    :defaults {foo 1337}
    :dh-defaults {:transient true})

  >  (example!
       :bar true
       :dh-args {:is-aspect-of? (constantly false)})

  Effectively runs:
  => (register-dhamma!
       :example
       :data {:foo-dest 1337}
       :meta {:example {:bar-dest true}}
       :is-aspect-of (constantly false)
       :transient true)
  "
  [dh-name & {:keys [meta data defaults dh-defaults]
              :or {meta {} data {} dh-defaults {}}}]
  `(do
     (defn ~(symbol (str dh-name "!"))
     [& {:keys [~@(concat (keys meta) (keys data)) ~'dh-args]
         :or ~defaults}]
     (#'buddho.core.dhamma.state/register-dhamma!
      ~(keyword dh-name)
      (merge
       ~(merge dh-defaults
               {:data (set/map-invert data)}
               {:meta {(keyword dh-name) (set/map-invert meta)}})
       ~'dh-args)))

     (defdhamma
       ~(symbol (str dh-name ".construct")))

       ))

(defn extraction-bindings [datasym bindings extractor defaults]
  `(~datasym
    (~extractor
     [~'dhamma ~'parent]
     ~defaults)

    ~@(mapcat (fn [[k v]] `(~k (~v ~datasym))) bindings)))

(defmacro defdhamma
  "Define a dhamma given by symbol `name`, defining for it:
  - a constructor using `defconstructor`;
  - documentation from `docstring` using `describe-dhamma!`;
  - an `impl!` execution body.

  Takes a map with the following keyword spec:
  -  `:data` and `:meta` -- maps of symbols to keywords, defining the data and metadata fields of the dhamma.
      The symbol will define the kwarg to pass to the generated constructor,
      while the keyword defines under what name the data is stored in actual dhamma objects.
      Additionally and importantly, the symbol will be bound to the corresponding value in the `impl!` body.
  - `:defaults` -- symbol->value map specifying default values for the above.
  - `:dh-defaults` -- default dhamma creation args; see `defconstructor`.
  - `:data-as` and `:meta-as` -- symbols to which all available (meta)data will be bound within the `impl!` body.
     Mind that this includes (meta)data from the parent!

  After this spec, the remainder of the input will constitute the `impl!` body, in which certain symbols will be bound as specified above.

  Example usage:

  (defdhamma
     make-sprite
     \"Make a cool sprite!\"
     {:data {url :sprite-url}
      :meta {thing :some-field}
      :defaults {thing \"value\"}
      :data-as all-data}

     (make-the-sprite-with url)
     (println all-data)
     (become-epic thing)
     (with-return parent :identifier 1234))
  "
  [name docstring
   {:keys [data meta
           defaults
           dh-defaults
           meta-as data-as
           make-constructor]
    :or {defaults {}
         make-constructor true}}
   & body]
  (let [kw (keyword name)
        default-data
        (->> data
             (map (fn [[k v]] [v (defaults k)]))
             (into {}))
        default-meta
        (->> meta
             (map (fn [[k v]] [v (defaults k)]))
             (into {}))]
    `(do
       (when ~make-constructor
         (defconstructor ~name
           :data ~data
           :meta ~meta
           :dh-defaults ~dh-defaults
           :defaults ~defaults))

       (#'buddho.core.describe/describe-dhamma!
        ~kw
        ~docstring)

       (defmethod buddho.core.dhamma.occur/impl! ~kw ~(symbol (str "impl-" name))
         [~'dhamma & [~'parent]]
         (let [~@(when (or data data-as)
                   (extraction-bindings
                    (or data-as `data#)
                    data
                    '#'buddho.core.dhamma.data/extract-data
                    default-data))

               ~@(when (or meta meta-as)
                   (extraction-bindings
                    (or meta-as `meta#)
                    meta
                    '#'buddho.core.dhamma.data/extract-meta
                    default-meta))]
           ~@body)))))
