(ns devtools.formatters
  (:require [goog.labs.userAgent.browser :as ua]
            [devtools.prefs :as prefs]
            [devtools.util :refer [get-formatters-safe set-formatters-safe!]]
            [devtools.formatters.core :refer [header-api-call has-body-api-call body-api-call]]))

(def ^:dynamic *installed* false)
(def ^:dynamic *sanitizer-enabled* true)
(def ^:dynamic *monitor-enabled* false)

(def obsolete-formatter-key "devtoolsFormatter")

(defn ^:dynamic available? []
  (and (ua/isChrome) (ua/isVersionOrHigher 47)))                                                                              ; Chrome 47+

(deftype CLJSDevtoolsFormatter [])

; devtools.debug namespace may not be present => no debugging
(defn- find-fn-in-debug-ns [fn-name]
  (try
    (aget js/window "devtools" "debug" fn-name)
    (catch :default _
      nil)))

(defn- monitor-api-call-if-avail [name api-call args]
  (if-let [monitor-api-call (find-fn-in-debug-ns "monitor_api_call")]
    (monitor-api-call name api-call args)
    (apply api-call args)))

(defn- log-exception-if-avail [& args]
  (if-let [log-exception (find-fn-in-debug-ns "log_exception")]
    (apply log-exception args)))

; monitors api calls in a separate debug console if debug namespace is available
(defn- monitor-api-calls [name api-call]
  (fn [& args]
    (if-not *monitor-enabled*
      (apply api-call args)
      (monitor-api-call-if-avail name api-call args))))

; wraps our api calls in a try-catch block to prevent leaking of exceptions in case something went wrong
(defn- sanitize [name api-call]
  (fn [& args]
    (if-not *sanitizer-enabled*
      (apply api-call args)                                                                                                   ; raw API call
      (try
        (apply api-call args)                                                                                                 ; wrapped API call
        (catch :default e
          (log-exception-if-avail (str name ": " e))
          nil)))))

(defn- build-cljs-formatter []
  (let [wrap (fn [name api-call]
               (let [monitor (partial monitor-api-calls name)
                     sanitizer (partial sanitize name)]
                 ((comp monitor sanitizer) api-call)
                 api-call))
        formatter (CLJSDevtoolsFormatter.)
        define! (fn [name fn]
                  (aset formatter name (wrap name fn)))]
    (define! "header" header-api-call)
    (define! "hasBody" has-body-api-call)
    (define! "body" body-api-call)
    formatter))

(defn- is-ours? [o]
  (instance? CLJSDevtoolsFormatter o))

(defn- present? []
  (let [formatters (get-formatters-safe)]
    (boolean (some is-ours? formatters))))

(defn- install-our-formatter! [formatter]
  (let [formatters (.slice (get-formatters-safe))]                                                                            ; slice effectively duplicates the array
    (.push formatters formatter)                                                                                              ; acting on duplicated array
    (set-formatters-safe! formatters)
    (if (prefs/pref :legacy-formatter)
      (aset js/window obsolete-formatter-key formatter))))

(defn- uninstall-our-formatters! []
  (let [new-formatters (remove is-ours? (vec (get-formatters-safe)))
        new-formatters-js (if (empty? new-formatters) nil (into-array new-formatters))]
    (set-formatters-safe! new-formatters-js)))

; -- installation -----------------------------------------------------------------------------------------------------------

(defn installed? []
  *installed*)

(defn install! []
  (when-not *installed*
    (set! *installed* true)
    (install-our-formatter! (build-cljs-formatter))
    true))

(defn uninstall! []
  (when *installed*
    (set! *installed* false)
    (uninstall-our-formatters!)))