(ns claire-common.utils
  (:refer-clojure :exclude [delay])
  (:require
   [cljs.nodejs :as nodejs]
   [clojure.string :as string]
   [clojure.core.async
    :as async
    :refer [>! <! go chan buffer close! put! alts! timeout]])
  (:require-macros [claire-common.macros
                    :as macros
                    :refer [<? go-try]]))

;; general stuff
(defn assoc-some [m k v]
  (if (some? v) (assoc m k v) m))

(defn lower-first [s]
  (str (string/lower-case (first s)) (string/join (rest s))))

(defn upper-first [s]
  (str (string/upper-case (first s)) (string/join (rest s))))

(defn clj-case [s]
  (string/join "-" (string/split (string/lower-case s) #" ")))

(defn camel-case [s]
  (let [[f & r] (string/split  (clj-case s) "-")]
    (str f (string/join (map #(upper-first %1) r)))))

(defn map-v [f m]
  (into {} (for [[k v] m] [k (f v)])))

(defn find-first [f coll]
  (first (filter f coll)))

(defn new [class & [module a b c d e f g]]
  "Instantiates a class provided by package module whose constructor
  takes up to 6 args."
  (let [class (if (some? module) (goog.object/get module class) class)]
    (class. a b c d e f g)))

(defn err-or [& body]
  "If body throws an exception, catch it and return it" 
  (try 
    body
    (catch Exception e (do (print "ERROR CAUGHT: " e) e))))

(defn- filter-maps [key value list-of-maps]
  (filter #(= (get %1 key) value) list-of-maps))

(defn short-id []
  (letfn [(hex [] (.toString (rand-int 16) 16))]
    (let [rhex (.toString (bit-or 0x8 (bit-and 0x3 (rand-int 16))) 16)]
      (uuid
        (str (hex) (hex) (hex) (hex))))))

;; (defn- context-exists? [name contexts]
;;   (seq (filter-maps "name" name contexts)))

;; Dialogflow constants

(def ^:const yes
  ["yes" "sure" "okay" "sounds correct" "that works" "yep that's ok"
   "yes that's right" "I think so" "yes I agree" "I don't mind"
   "I agree" "yes I do" "for sure" "ok" "yes that's ok" "that one works"
   "yes you can do it" "perfect" "why not" "of course" "okay I will"
   "exactly" "sure why not" "absolutely" "it's okay" "it's fine" "go ahead"
   "confirm" "sounds good" "alright" "yeah" "yup" "yes please" "do it"
   "yes I can" "it looks perfect" "that's correct" "right" "alright why not"])

(def ^:const no
  ["don't" "nope not really" "no that's be all" "not right now" "thanks but no"
   "not" "no we are good" "nothing else" "not interested" "nah I'm good"
   "definitely not" "no never" "never" "nah" "nothing" "no maybe next time"
   "I don't want that" "no not really" "no that's ok" "no no don't"
   "na" "no way no" "I can't" "no thanks" "not at all" "not really" "I don't"
   "no I cannot" "no that's okay" "nope" "I don't think so" "no it isn't"
   "thanks but not this time" "no that's fine thank you" "no don't" 
   "don't do it" "I don't want" "not today" "I'm not" "no way" "no"
   "I disagree" "not this time" "nothing else thanks"])



;; async stuff

(defn- promised [f & args]
  "Wraps a function taking a callback into a JS promise"
  (js/Promise.
   (fn [resolve reject]
     (apply
      f
      (concat args
              [(clj->js 
                (fn [e r] (if (some? e) (reject e) (resolve r))))])))))

(defn funnel [p]
  "Convert a promise into an async channel"
  (let [c (chan)
        t (uuid "")]
    (.catch
     (.then p #(go (>! c {:action t :result (js->clj %1)})))
     #(go (>! c {:action t :error (js->clj %1)})))
    c))

(defn delay [d]
  (funnel (promised #(js/setTimeout %2 %1) d)))

(defn call-async-method [obj fname & args]
  (let [args (map clj->js args)
        f (.bind (aget obj fname) obj)
        g (fn [cbk] (apply f (concat args [cbk])))]
    (funnel (promised g))))

(defn make-promise [fun]
  "This converts a funtion returning an async channel to a
  promise-based function. It can be used as a wrapper to CLJ functions
  for seamless integration with JS callers."
  (fn [& args]
    (.then (js/Promise.
      (fn [resolve reject]
        (let [c (apply fun (js->clj args))]
          (go
            (let [r (first (<! c))]
              (if (:error r)
                (reject (clj->js (:error r)))
                (resolve (clj->js r)))))))))))

(defn js-wrapper [fun]
  "Wraps a Clojure function to marshal arguments and return value."
  (fn ([& args]
       (clj->js (apply fun (js->clj args))))))

;; stuff related to dialogflow environments and contexts

(defn make-env [env & {:keys [state context event merge?]
                       :or { merge? true }}]
  "Returns a new environment from the one passed by merging the state
  and context. A nil value for the 'state' argument is interpreted as
  'do not change the state (context)'. If merge is false, you can
  reset the state (context) by passing an empty set #{} (empty
  array [])."
  (let [
        ;;_ (println "MAKE ENV: " context " + " state " -> " env " ** " merge?)
        context (if (and (some? context)
                         (not (sequential? context)))
                  [context]
                  context)]
    (if (true? merge?)
      {:state (merge (or (:state env) {}) (or state {}))
       :context (concat (or (:context env) []) (or context []))
       :event event}
      {:state (or state (or (:state env) {}))
       :context (or context (or (:context env) []))
       :event event})))

(defn clear-env [env & {:keys [state context] :or {state [] context []}}]
  "Creates a new env from the passed one by removing the keys
  specified under the :state and :context keys. You can either pass
  scalar values for the keys or a set of keys."
  (let [_ (println "CLEANING ENV:" env)
        context (if (sequential? context)
                  context [context])
        state (if (sequential? state) state [state])
        _ (println "CLEANED STATE: " (apply dissoc (:state env) state))]
    (make-env {}
              :state (apply dissoc (:state env) state)
              :context (filter #(nil? (some #{(:name %1)} context)) 
                               (:context env))
              )))

(defn list-contains? [list val]
  (some #{val} list))

(defn context-has [env context]
  (list-contains? (:context env) context))

(defn- user-store [agent]
  (let [_ (or (and (.-user (.-conv agent))
                   (.-storage (.-user (.-conv agent))))
              (set! (.-user (.-conv agent)) (clj->js {})))]
    (.-user (.-conv agent))))

(defn user-store-set [agent value]
  "This will store value in agent's session for the current conversation"
  (goog.object/set (user-store agent) "storage" (clj->js value)))

(defn user-store-get [agent & [key]]
  "This will retrieve the value associated to key in agent's session
  for the current conversation. If no key is given, the whole map is
  returned."
  (js->clj
   (if (some? key)
     (goog.object/get
      (goog.object/get (user-store agent) "storage") key)
     (goog.object/get (user-store agent) "storage"))))

(defn make-reply [env & {:keys [state context event result say export]
                         :or {state {} context [] say "" export false}}]
  "Convenience function to construct a reply"
  {:result result
   :say say
   :event event
   :export export
   :env (make-env env :state state :context context)})

(defn parameters [agent]
;;  (apply merge (map #(js->clj (.-parameters %1)) contexts))
  (let [contexts (.-contexts agent)]
    (if (> (.-length contexts) 0)
      (js->clj (.-parameters (aget contexts 0)))
      [])))

;; Helpers for fulfillment handlers

(defn- import-contexts [agent contexts]
  (let [cs (.-contexts (.-context agent))
        _ (println "AGENT CONTEXTS importing: " cs)
        _ (println "MY CONTEXTS importing: " contexts)]
    (vals (js->clj cs))))

(defn- clear-agent-contexts [agent ctxs]
  (for [x ctxs]
    (.setContext agent (clj->js { "name" x
                                  "lifespan" "0"}))))

(defn clear-contexts [env ctxs]
  (let [c (reduce
           (fn [a v]
             (if (list-contains? ctxs (get v "name"))
               (let [_ (println "REMOVING: " (get v "name"))]
                 a)
               (let [_ (println "LETTING IN: " (get v "name"))]
                 (conj a v))))
           []
           (:context env))]
    (make-env env :context c :merge? false)))

(defn- export-contexts [agent env]
  (if-let [x (.-context agent)]
    (let [contexts (or (:context env) [])
          _ (println "EXPORTING CONTEXTS: " contexts)
          _ (println "INTO AGENT CONTEXT: " (js->clj (.-contexts x)))]
      (doseq [ctx (reverse contexts)]
        (let [ctx (if (string? ctx) {"name" ctx} ctx)
              _ (println "SETTING CTX: " ctx)]
          (.set x (clj->js (assoc ctx "lifespan" 1))))))
    (println "CONTEXT EMPTY.")))

(defn- agent-response [agent reply msgs env]
  (let [r (:result reply)
        intent (clj-case (.-intent agent))
        ctx (last (:context env))
        say (:say reply)
        _ (println "GETTING MESSAGE FOR: " say "/" ctx "/" (keyword intent))
        msg (get (get msgs (keyword intent)) say)
        _ (when (some? msg) (println "MESSAGE: " (msg (.-query agent) env)))]
    (when (some? msg)
     (msg (.-query agent) env))))

(defn- handle-result [agent reply msgs env]
  (let [_ (println "HANDLING REPLY: " reply)
        event (:event reply)
        msg (agent-response agent reply msgs env)]
    (when (some? event)
      (let [_ (println "JUMPING TO :" event)
            _ (println "MODIFIED QUERY: " (.-query agent))]
        (.setFollowupEvent agent event)))
    (if (some? msg)
      (.add agent (clj->js msg))
      (.add agent
            (or (:say reply)
                "Sorry, I could not get a response.")))))

(defn make-handler [fun msgs]
  "This can be used to wrap a function so it can be used as a
  fulfillment method. The handler takes a Dialogflow agent as its
  unique argument, conforming to Dialogflow specs, and calls a
  function that implements the desired behaviour while exposing a more
  generic interface (specifically, a pure Clojure interface). Such
  function takes a query, an environment map, and a parameters map,
  all of those extracted from the agent. The result returned by the
  function is then passed back to the agent."
  (fn [agent]
    (let [intent (.-intent agent)
          s (user-store-get agent)
          pars (parameters agent)
          _ (println "******************************")
          _ (println "ENV before EXEC: " s)
          _ (println "FUN: " fun)
          env {:state (get s "state")
               :context (import-contexts agent (get s "context"))}
          r (fun intent (.-query agent) env pars)
          ;; r (if fun
          ;;     (fun intent (.-query agent) env pars)
          ;;     (print "FAILINGINTENT: " intent))
          _ (println "RESPNSE after EXEC: " r)
          env (make-env env
                        :state (merge pars (:state (:env r)))
                        :context (:context (:env r))
                        :merge? false)]
      (println "SAVING ENV:" env)
      (when (some? env) (user-store-set agent env))
      (when (true? (:export r)) (export-contexts agent env))
      (handle-result agent r msgs env))))

;; Long query simulation

(defn active-wait [d p]
  "Waits actively (i.e., wasting CPU cycles) until p is true or d msec
  have passed"
  (let [startTime (.getTime (js/Date.))]
    (loop [currentTime startTime
           done? (p)]
      (when (and (not done?)
                 (>= (+ d startTime) currentTime))
        (if (>= (+ 250 currentTime) (.getTime (js/Date.)))
          (recur currentTime done?)
          (recur (.getTime (js/Date.)) (p)))))))

(def ^:private ^:dynamic *found* (atom true))
(def ^:private ^:dynamic *started* (atom false))

(defn long-call [& [delay]]
  (do
    (println "LONG-CALL: started " @*started* " -- found: " @*found*)
    (when (and (false? @*started*) (some? delay))
      (swap! *started* not)
      (swap! *found* not)
      (println "EXTERNAL QUERY STARTED")
      (js/setTimeout #(do (println "EXTERNAL QUERY END at "
                           (.toISOString (js/Date.)))
                          (swap! *found* not)
                          (swap! *started* not))
                     delay))
    (fn [] @*found*)))
