(ns emptyhead.repl.core
  (:require [clojure.string :as str]
            [emptyhead.thought.extend :as extend]
            [emptyhead.thought.eval :as teval]
            [emptyhead.idea.property :as prop]
            [emptyhead.util.magic :as magic]
            [emptyhead.idea.protocol :as prtc]
            [emptyhead.idea.crud :as idea]
            [emptyhead.idea.memtag :as memtag]
            [emptyhead.thought.define :as def]
            [emptyhead.thought.crud :as thought]
            [emptyhead.lib.core]
            [emptyhead.lib.context :as ctx]))

(defonce context (ctx/new-context))
(defn clear-context [] (set! context (ctx/new-context)))

(defn undot-operator [dotted-sym]
  (mapv keyword (str/split (name dotted-sym) #"\.")))

(defn eval! [thought & [parent]]
  (set! context (teval/execute! thought parent)))

(defn parse-command [cmd]
  (cond
    (= cmd '.BEGIN)         (thought/register-thought! [:emptyhead :repl :begin-block])
    (= cmd '.END)           (thought/register-thought! [:emptyhead :repl :end-block])

    (symbol? cmd)           (thought/register-thought!
                             [:emptyhead :compose :append]
                             :data (thought/register-thought! (undot-operator cmd)))

    :else                   (thought/register-thought!
                             [:emptyhead :compose :append]
                             :data (thought/register-thought! [:emptyhead :core :return] :data cmd))))

(def/define! [:emptyhead :repl :begin-block]
  (fn [thought parent]
    (let [ctx-creator (thought/register-thought! [:emptyhead :core :context :construct])
          ;; Create a new context with `parent` as parent.
          [parent ctx] (->> parent (teval/execute! ctx-creator) thought/pop-stack)
          ctx (prop/just-property ctx)
          ;; Make `parent` put the new context on its stack during its execution flow
          ;; TODO refactor
          parent (teval/execute! (thought/register-thought!
                                  [:emptyhead :compose :append]
                                  :data (thought/register-thought! [:emptyhead :core :return]
                                                                   :data (memtag/uid-of ctx)))
                                 parent)]

      ;; NOTE `ctx` becomes the new `parent`!
      [ctx
       ctx])))

(def/define! [:emptyhead :repl :end-block]
  ;; Exiting context, so its parent becomes the new parent.
  (fn [thought parent]
    (let [parent-ctx (-> parent prtc/value :data :parent prop/just-property)]
      [parent-ctx nil])))

(defn read [& input]
  (map parse-command input))

(defn pprint-tos [thought]
  (let [printer #(println (str "EMPTYHEAD> " %))
        tos (-> thought prtc/value :return last)]
    (-> tos printer)
    tos))

(defn run [& input]
  (clear-context)
  (run! #(eval! % context) (apply read input))
  (pprint-tos context)
  (prtc/reference (teval/execute! context context))
  )

;; XXX an annoying class of bug is eg  forgetting the return value here, which yields just
;; >No protocol method Idea.value defined for type null:
;; In this case originationg from [[emptyhead.thought.eval/execute!]]
;; Widely catching these errors earlier would be wise since we don't have
;; a good stack trace for this stuff
(def/define! [:emptyhead :debug :track]
  (fn [thought parent]
    (let [ctx (:_parent (prtc/value parent))]
      (print "RUNNING " (:operator (prtc/value parent)) " ON ")
      (print "CONTEXT " ctx " WITH \n")
      (print "STACK: ")
      (print (:return ctx))
      (print "\n\n")
      [parent nil]
      )))

(def sample (thought/register-thought! [:foo]))

;; XXX This has to have no :PRE-EXECUTE stage otherwise it will loop on itself ww
(defonce tracker (thought/register-thought! [:emptyhead :debug :track] :ext-stages [[:EXECUTE]]))

(defn enable-tracking []
  (extend/register-extension! tracker [:PRE-EXECUTE]))

;; XXX accidentally registering one extension multiple times is a known footgun right now
;; and probably needs some feature to prevent this
;; (but mind that it shouldn't be *illegal*)
;; Another issue is just general... garbage collection, that's a big thing
(defn disable-tracking []
  (map #(extend/remove-extension! % [:PRE-EXECUTE]) (extend/get-extensions [:PRE-EXECUTE])))

(defn name-idea
  [idea context name]
  (prop/register-property! idea [:emptyhead :name name (prtc/reference context)]))

(defn resolve-name
  [context name]
  (prop/just-property [:emptyhead :name name (prtc/reference context)]))

(defn deref-name
  [self name]
  (or (and (= (prtc/val-fn :operator self) [:emptyhead :core :context])
           (resolve-name self name))
      (and (prtc/val-fn :_parent self)
           (deref-name (prtc/val-fn :_parent self) name))))

(def/define! [:GRAB]
  (fn [thought parent]
    (let [arg-num (inc (or (get-in parent [:data :last-arg]) -1))]
      [(assoc-in parent [:data :last-arg] arg-num)
       (-> parent prtc/value :_parent prtc/value :return (#(nth % (- (count %) arg-num 1))))])))

(def/define! [:RETURN]
  (fn [thought parent]
    (let [[parent val] (thought/pop-stack parent)]
      [(update-in parent [:data :proc-ret] (fnil conj []) val) nil])))

;; FIXME this wil bind two things to the same name if you name severa things the same
;; FIXME this needs to wrap data in an [:emptyhead :core :return] and then evaluate that on deref
(def/define! [:emptyhead :core :assign-name]
  (fn [_ parent]
    (let [[parent name] (thought/pop-stack parent)
          [_ op] (thought/pop-stack parent)
          exe (prop/just-property op)]
      (extend/register-extension! exe (into [:_name] name))
      [parent nil])))

;; FIXME this is a weird use of the extension system -- figure out how names should actually work!
;; (in light of the way extensions usually work, see e.g. [emptyhead.thought.extend/get-extensions]) comment
;; FIXME this needs to wrap data in an [:emptyhead :core :return] and then evaluate that on deref
(def/define! [:emptyhead :core :deref-name]
  (fn [_ parent]
    (let [[parent name] (thought/pop-stack parent)
          deref (memtag/uid-of
                 (prop/just-property (magic/extension-prop (into [:_name] name))))]
      [parent deref])))

(def tst
  '(.BEGIN
    "<> -- COMMENCE PROCEDURE -- <>"
    emptyhead.io.print

    GRAB emptyhead.io.print
    GRAB emptyhead.io.print

    "<> -- PROCEDURE COMPLETE -- <>"

    42 RETURN
    69 RETURN
    .END
    emptyhead.io.print

    [:meme]
    emptyhead.core.assign-name

    "hello"
    "goodbye"
    [:meme]
    emptyhead.core.deref-name
    emptyhead.core.execute
    emptyhead.core.pop ;; pop deref'd name

    "\nReturn values of previous:\n"
    emptyhead.io.print
    emptyhead.core.pop

    [:meme]
    emptyhead.core.deref-name
    emptyhead.core.execute
    ))
