;; Copyright (c) Rich Hickey. All rights reserved.
;; The use and distribution terms for this software are covered by the
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this distribution.
;; By using this software in any fashion, you are agreeing to be bound by
;; the terms of this license.
;; You must not remove this notice, or any other, from this software.

(ns cljs.stacktrace
  (:require #?@(:clj  [[cljs.util :as util]
                       [clojure.java.io :as io]]
                :cljs [[goog.string :as gstring]])
            [clojure.string :as string])
  #?(:clj (:import [java.util.regex Pattern]
                   [java.io File])))

(defmulti parse-stacktrace
  "Parse a JavaScript stacktrace string into a canonical data form. The
  arguments:

  repl-env - the repl environment, an optional map with :host and :port keys
             if the stacktrace includes url, not file references
  st       - the original stacktrace string to parse
  err      - an error map. :ua-product key defines the type of stacktrace parser
             to use, for example :chrome
  opts     - additional options. :output-dir maybe given in this argument if
             :host and :port do not apply, for example, a file path

  The canonical stacktrace representation can easily be mapped to a
  ClojureScript one see mapped-stacktrace and mapped-stacktrace-str"
  (fn [repl-env st err opts] (:ua-product err)))

(defn parse-int [s]
  #?(:clj  (Long/parseLong s)
     :cljs (js/parseInt s 10)))

(defn starts-with?
  #?(:cljs {:tag boolean})
  [^String s0 s1]
  #?(:clj  (.startsWith s0 s1)
     :cljs (gstring/startsWith s0 s1)))

(defn ends-with?
  #?(:cljs {:tag boolean})
  [^String s0 s1]
  #?(:clj  (.endsWith s0 s1)
     :cljs (gstring/endsWith s0 s1)))

(defn string->regex [s]
  #?(:clj  (Pattern/compile s)
     :cljs (js/RegExp. s)))

(defn output-directory [opts]
  #?(:clj  (util/output-directory opts)
     :cljs (or (:output-dir opts) "out")))

(defmethod parse-stacktrace :default
  [repl-env st err opts] st)

(defn parse-file-line-column [flc]
  (if-not (re-find #":" flc)
    [flc nil nil]
    (let [xs (string/split flc #":")
          [pre [line column]]
          (reduce
            (fn [[pre post] [x i]]
              (if (<= i 2)
                [pre (conj post x)]
                [(conj pre x) post]))
            [[] []] (map vector xs (range (count xs) 0 -1)))
          file (string/join ":" pre)]
      [(cond-> file
         (starts-with? file "(") (string/replace "(" ""))
       (parse-int
         (cond-> line
           (ends-with? line ")") (string/replace ")" "")))
       (parse-int
         (cond-> column
           (ends-with? column ")") (string/replace ")" "")))])))

(defn parse-file
  "Given a browser file url convert it into a relative path that can be used
   to locate the original source."
  [{:keys [host host-port port] :as repl-env} file {:keys [asset-path] :as opts}]
  (let [urlpat (if host
                 (string->regex
                   (str "http://" host ":" (or host-port port) "/"))
                 "")
        match  (if host
                 (re-find urlpat file)
                 (contains? opts :output-dir))]
    (if match
      (-> file
        (string/replace urlpat "")
        (string/replace
          (string->regex
            ;; if :asset-path specified drop leading slash
            (str "^" (or (and asset-path (string/replace asset-path #"^/" ""))
                         (output-directory opts)) "/"))
          ""))
      (if-let [asset-root (:asset-root opts)]
        (string/replace file asset-root "")
        (throw
          (ex-info (str "Could not relativize URL " file)
            {:type :parse-stacktrace
             :reason :relativize-url}))))))

;; -----------------------------------------------------------------------------
;; Chrome Stacktrace

(defn chrome-st-el->frame
  [repl-env st-el opts]
  (let [xs (-> st-el
             (string/replace #"\s+at\s+" "")
             (string/split #"\s+"))
        [function flc] (if (== 1 (count xs))
                         [nil (first xs)]
                         [(first xs) (last xs)])
        [file line column] (parse-file-line-column flc)]
    (if (and file function line column)
      {:file (parse-file repl-env file opts)
       :function (string/replace function #"Object\." "")
       :line line
       :column column}
      (when-not (string/blank? function)
        {:file nil
         :function (string/replace function #"Object\." "")
         :line nil
         :column nil}))))

(comment
  (chrome-st-el->frame {:host "localhost" :port 9000}
    "\tat cljs$core$ffirst (http://localhost:9000/out/cljs/core.js:5356:34)" {})
  )

(defmethod parse-stacktrace :chrome
  [repl-env st err opts]
  (->> st
    string/split-lines
    (drop-while #(starts-with? % "Error"))
    (take-while #(not (starts-with? % "    at eval")))
    (map #(chrome-st-el->frame repl-env % opts))
    (remove nil?)
    vec))

(comment
  (parse-stacktrace {:host "localhost" :port 9000}
    "Error: 1 is not ISeqable
    at Object.cljs$core$seq [as seq] (http://localhost:9000/out/cljs/core.js:4258:8)
    at Object.cljs$core$first [as first] (http://localhost:9000/out/cljs/core.js:4288:19)
    at cljs$core$ffirst (http://localhost:9000/out/cljs/core.js:5356:34)
    at http://localhost:9000/out/cljs/core.js:16971:89
    at cljs.core.map.cljs$core$map__2 (http://localhost:9000/out/cljs/core.js:16972:3)
    at http://localhost:9000/out/cljs/core.js:10981:129
    at cljs.core.LazySeq.sval (http://localhost:9000/out/cljs/core.js:10982:3)
    at cljs.core.LazySeq.cljs$core$ISeqable$_seq$arity$1 (http://localhost:9000/out/cljs/core.js:11073:10)
    at Object.cljs$core$seq [as seq] (http://localhost:9000/out/cljs/core.js:4239:13)
    at Object.cljs$core$pr_sequential_writer [as pr_sequential_writer] (http://localhost:9000/out/cljs/core.js:28706:14)"
    {:ua-product :chrome}
    nil)

  (parse-stacktrace {:host "localhost" :port 9000}
    "Error: 1 is not ISeqable
    at Object.cljs$core$seq [as seq] (http://localhost:9000/js/cljs/core.js:4258:8)
    at Object.cljs$core$first [as first] (http://localhost:9000/js/cljs/core.js:4288:19)
    at cljs$core$ffirst (http://localhost:9000/js/cljs/core.js:5356:34)
    at http://localhost:9000/js/cljs/core.js:16971:89
    at cljs.core.map.cljs$core$map__2 (http://localhost:9000/js/cljs/core.js:16972:3)
    at http://localhost:9000/js/cljs/core.js:10981:129
    at cljs.core.LazySeq.sval (http://localhost:9000/js/cljs/core.js:10982:3)
    at cljs.core.LazySeq.cljs$core$ISeqable$_seq$arity$1 (http://localhost:9000/js/cljs/core.js:11073:10)
    at Object.cljs$core$seq [as seq] (http://localhost:9000/js/cljs/core.js:4239:13)
    at Object.cljs$core$pr_sequential_writer [as pr_sequential_writer] (http://localhost:9000/js/cljs/core.js:28706:14)"
    {:ua-product :chrome}
    {:asset-path "/js"})

  (parse-stacktrace {:host "localhost" :port 9000}
    "Error: 1 is not ISeqable
    at Object.cljs$core$seq [as seq] (http://localhost:9000/out/cljs/core.js:4259:8)
    at Object.cljs$core$first [as first] (http://localhost:9000/out/cljs/core.js:4289:19)
    at cljs$core$ffirst (http://localhost:9000/out/cljs/core.js:5357:18)
    at eval (eval at <anonymous> (http://localhost:9000/out/clojure/browser/repl.js:23:272), <anonymous>:1:106)
    at eval (eval at <anonymous> (http://localhost:9000/out/clojure/browser/repl.js:23:272), <anonymous>:9:3)
    at eval (eval at <anonymous> (http://localhost:9000/out/clojure/browser/repl.js:23:272), <anonymous>:14:4)
    at http://localhost:9000/out/clojure/browser/repl.js:23:267
    at clojure$browser$repl$evaluate_javascript (http://localhost:9000/out/clojure/browser/repl.js:26:4)
    at Object.callback (http://localhost:9000/out/clojure/browser/repl.js:121:169)
    at goog.messaging.AbstractChannel.deliver (http://localhost:9000/out/goog/messaging/abstractchannel.js:142:13)"
    {:ua-product :chrome}
    nil)

  ;; Node.js example
  (parse-stacktrace {}
    "Error: 1 is not ISeqable
    at Object.cljs$core$seq [as seq] (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:3999:8)
    at Object.cljs$core$first [as first] (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:4018:19)
    at cljs$core$ffirst (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:5161:34)
    at /home/my/cool/project/.cljs_bootstrap/cljs/core.js:16006:88
    at cljs.core.map.cljs$core$IFn$_invoke$arity$2 (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:16007:3)
    at cljs.core.LazySeq.sval (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:10244:109)
    at cljs.core.LazySeq.cljs$core$ISeqable$_seq$arity$1 (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:10335:10)
    at Object.cljs$core$seq [as seq] (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:3980:13)
    at Object.cljs$core$pr_sequential_writer [as pr_sequential_writer] (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:28084:14)
    at cljs.core.LazySeq.cljs$core$IPrintWithWriter$_pr_writer$arity$3 (/home/my/cool/project/.cljs_bootstrap/cljs/core.js:28812:18)"
    {:ua-product :chrome}
    {:output-dir "/home/my/cool/project/.cljs_bootstrap"})
  )

;; -----------------------------------------------------------------------------
;; Safari Stacktrace

(defn safari-st-el->frame
  [repl-env st-el opts]
  (let [[function flc] (if (re-find #"@" st-el)
                         (string/split st-el #"@")
                         [nil st-el])
        [file line column] (parse-file-line-column flc)]
    (if (and file function line column)
      {:file (parse-file repl-env file opts)
       :function (string/trim function)
       :line line
       :column column}
      (when-not (string/blank? function)
        {:file nil
         :function (string/trim function)
         :line nil
         :column nil}))))

(comment
  (safari-st-el->frame {:host "localhost" :port 9000}
    "cljs$core$seq@http://localhost:9000/out/cljs/core.js:4259:17" {})

  (safari-st-el->frame {:host "localhost" :port 9000}
    "cljs$core$seq@http://localhost:9000/js/cljs/core.js:4259:17" {:asset-path "js"})
  )

(defmethod parse-stacktrace :safari
  [repl-env st err opts]
  (->> st
    string/split-lines
    (drop-while #(starts-with? % "Error"))
    (take-while #(not (starts-with? % "eval code")))
    (remove string/blank?)
    (map #(safari-st-el->frame repl-env % opts))
    (remove nil?)
    vec))

(comment
  (parse-stacktrace {}
    "cljs$core$seq@out/cljs/core.js:3999:17
    cljs$core$first@out/cljs/core.js:4018:22
    cljs$core$ffirst@out/cljs/core.js:5161:39
    global code"
    {:ua-product :safari}
    {:output-dir "out"})

  (parse-stacktrace {:host "localhost" :port 9000}
    "cljs$core$seq@http://localhost:9000/out/cljs/core.js:4259:17
cljs$core$first@http://localhost:9000/out/cljs/core.js:4289:22
cljs$core$ffirst@http://localhost:9000/out/cljs/core.js:5357:39
http://localhost:9000/out/cljs/core.js:16972:92
http://localhost:9000/out/cljs/core.js:16973:3
http://localhost:9000/out/cljs/core.js:10982:133
sval@http://localhost:9000/out/cljs/core.js:10983:3
cljs$core$ISeqable$_seq$arity$1@http://localhost:9000/out/cljs/core.js:11074:14
cljs$core$seq@http://localhost:9000/out/cljs/core.js:4240:44
cljs$core$pr_sequential_writer@http://localhost:9000/out/cljs/core.js:28707:17
cljs$core$IPrintWithWriter$_pr_writer$arity$3@http://localhost:9000/out/cljs/core.js:29386:38
cljs$core$pr_writer_impl@http://localhost:9000/out/cljs/core.js:28912:57
cljs$core$pr_writer@http://localhost:9000/out/cljs/core.js:29011:32
cljs$core$pr_seq_writer@http://localhost:9000/out/cljs/core.js:29015:20
cljs$core$pr_sb_with_opts@http://localhost:9000/out/cljs/core.js:29078:24
cljs$core$pr_str_with_opts@http://localhost:9000/out/cljs/core.js:29092:48
cljs$core$pr_str__delegate@http://localhost:9000/out/cljs/core.js:29130:34
cljs$core$pr_str@http://localhost:9000/out/cljs/core.js:29139:39
eval code
eval@[native code]
http://localhost:9000/out/clojure/browser/repl.js:23:271
clojure$browser$repl$evaluate_javascript@http://localhost:9000/out/clojure/browser/repl.js:26:4
http://localhost:9000/out/clojure/browser/repl.js:121:173
deliver@http://localhost:9000/out/goog/messaging/abstractchannel.js:142:21
xpcDeliver@http://localhost:9000/out/goog/net/xpc/crosspagechannel.js:733:19
messageReceived_@http://localhost:9000/out/goog/net/xpc/nativemessagingtransport.js:321:23
fireListener@http://localhost:9000/out/goog/events/events.js:741:25
handleBrowserEvent_@http://localhost:9000/out/goog/events/events.js:862:34
http://localhost:9000/out/goog/events/events.js:276:42"
    {:ua-product :safari}
    nil)
  )

;; -----------------------------------------------------------------------------
;; Firefox Stacktrace

(defn firefox-clean-function [f]
  (as-> f f
    (cond
      (string/blank? f) nil
      (not= (.indexOf f "</") -1)
      (let [idx (.indexOf f "</")]
        (.substring f (+ idx 2)))
      :else f)
    (-> f
      (string/replace #"<" "")
      (string/replace #?(:clj #"\/" :cljs (js/RegExp. "\\/")) ""))))

(defn firefox-st-el->frame
  [repl-env st-el opts]
  (let [[function flc] (if (re-find #"@" st-el)
                         (string/split st-el #"@")
                         [nil st-el])
        [file line column] (parse-file-line-column flc)]
    (if (and file function line column)
      {:file (parse-file repl-env file opts)
       :function (firefox-clean-function function)
       :line line
       :column column}
      (when-not (string/blank? function)
        {:file nil
         :function (firefox-clean-function function)
         :line nil
         :column nil}))))

(comment
  (firefox-st-el->frame {:host "localhost" :port 9000}
    "cljs$core$seq@http://localhost:9000/out/cljs/core.js:4258:8" {})

  (firefox-st-el->frame {:host "localhost" :port 9000}
    "cljs.core.map</cljs$core$map__2/</<@http://localhost:9000/out/cljs/core.js:16971:87" {})

  (firefox-st-el->frame {:host "localhost" :port 9000}
    "cljs.core.map</cljs$core$map__2/</<@http://localhost:9000/out/cljs/core.js:16971:87" {})

  (firefox-st-el->frame {:host "localhost" :port 9000}
    "cljs.core.pr_str</cljs$core$pr_str@http://localhost:9000/out/cljs/core.js:29138:8" {})

  (firefox-st-el->frame {:host "localhost" :port 9000}
    "cljs.core.pr_str</cljs$core$pr_str__delegate@http://localhost:9000/out/cljs/core.js:29129:8" {})
  )

(defmethod parse-stacktrace :firefox
  [repl-env st err opts]
  (->> st
    string/split-lines
    (drop-while #(starts-with? % "Error"))
    (take-while #(= (.indexOf % "> eval") -1))
    (remove string/blank?)
    (map #(firefox-st-el->frame repl-env % opts))
    (remove nil?)
    vec))

(comment
  (parse-stacktrace {:host "localhost" :port 9000}
    "cljs$core$seq@http://localhost:9000/out/cljs/core.js:4258:8
cljs$core$first@http://localhost:9000/out/cljs/core.js:4288:9
cljs$core$ffirst@http://localhost:9000/out/cljs/core.js:5356:24
cljs.core.map</cljs$core$map__2/</<@http://localhost:9000/out/cljs/core.js:16971:87
cljs.core.map</cljs$core$map__2/<@http://localhost:9000/out/cljs/core.js:16970:1
cljs.core.LazySeq.prototype.sval/self__.s<@http://localhost:9000/out/cljs/core.js:10981:119
cljs.core.LazySeq.prototype.sval@http://localhost:9000/out/cljs/core.js:10981:13
cljs.core.LazySeq.prototype.cljs$core$ISeqable$_seq$arity$1@http://localhost:9000/out/cljs/core.js:11073:1
cljs$core$seq@http://localhost:9000/out/cljs/core.js:4239:8
cljs$core$pr_sequential_writer@http://localhost:9000/out/cljs/core.js:28706:4
cljs.core.LazySeq.prototype.cljs$core$IPrintWithWriter$_pr_writer$arity$3@http://localhost:9000/out/cljs/core.js:29385:8
cljs$core$pr_writer_impl@http://localhost:9000/out/cljs/core.js:28911:8
cljs$core$pr_writer@http://localhost:9000/out/cljs/core.js:29010:8
cljs$core$pr_seq_writer@http://localhost:9000/out/cljs/core.js:29014:1
cljs$core$pr_sb_with_opts@http://localhost:9000/out/cljs/core.js:29077:1
cljs$core$pr_str_with_opts@http://localhost:9000/out/cljs/core.js:29091:23
cljs.core.pr_str</cljs$core$pr_str__delegate@http://localhost:9000/out/cljs/core.js:29129:8
cljs.core.pr_str</cljs$core$pr_str@http://localhost:9000/out/cljs/core.js:29138:8
@http://localhost:9000/out/clojure/browser/repl.js line 23 > eval:1:25
@http://localhost:9000/out/clojure/browser/repl.js line 23 > eval:1:2
clojure$browser$repl$evaluate_javascript/result<@http://localhost:9000/out/clojure/browser/repl.js:23:267
clojure$browser$repl$evaluate_javascript@http://localhost:9000/out/clojure/browser/repl.js:23:15
clojure$browser$repl$connect/</<@http://localhost:9000/out/clojure/browser/repl.js:121:128
goog.messaging.AbstractChannel.prototype.deliver@http://localhost:9000/out/goog/messaging/abstractchannel.js:142:5
goog.net.xpc.CrossPageChannel.prototype.xpcDeliver@http://localhost:9000/out/goog/net/xpc/crosspagechannel.js:733:7
goog.net.xpc.NativeMessagingTransport.messageReceived_@http://localhost:9000/out/goog/net/xpc/nativemessagingtransport.js:321:1
goog.events.fireListener@http://localhost:9000/out/goog/events/events.js:741:10
goog.events.handleBrowserEvent_@http://localhost:9000/out/goog/events/events.js:862:1
goog.events.getProxy/f<@http://localhost:9000/out/goog/events/events.js:276:16"
    {:ua-product :firefox}
    nil)
  )

;; -----------------------------------------------------------------------------
;; Rhino Stacktrace

(defmethod parse-stacktrace :rhino
  [repl-env st err {:keys [output-dir] :as opts}]
  (letfn [(process-frame [frame-str]
            (when-not (or (string/blank? frame-str)
                          (== -1 (.indexOf frame-str "\tat")))
              (let [[file-side line-fn-side] (string/split frame-str #":")
                   file                      (string/replace file-side #"\s+at\s+" "")
                   [line function]           (string/split line-fn-side #"\s+")]
               {:file     (string/replace file
                            (str output-dir
                              #?(:clj File/separator :cljs "/"))
                            "")
                :function (when function
                            (-> function
                              (string/replace "(" "")
                              (string/replace ")" "")))
                :line     (when (and line (not (string/blank? line)))
                            (parse-int line))
                :column   0})))]
    (->> (string/split st #"\n")
      (map process-frame)
      (remove nil?)
      vec)))

(comment
  (parse-stacktrace {}
    "\tat .cljs_rhino_repl/goog/../cljs/core.js:4215 (seq)
     \tat .cljs_rhino_repl/goog/../cljs/core.js:4245 (first)
     \tat .cljs_rhino_repl/goog/../cljs/core.js:5295 (ffirst)
     \tat <cljs repl>:1
     \tat <cljs repl>:1"
    {:ua-product :rhino}
    {:output-dir ".cljs_rhino_repl"})

  (parse-stacktrace {}
    "org.mozilla.javascript.JavaScriptException: Error: 1 is not ISeqable (.cljs_rhino_repl/goog/../cljs/core.js#3998)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:3998 (cljs$core$seq)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:4017 (cljs$core$first)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:5160 (cljs$core$ffirst)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:16005
   \tat .cljs_rhino_repl/goog/../cljs/core.js:16004
   \tat .cljs_rhino_repl/goog/../cljs/core.js:10243
   \tat .cljs_rhino_repl/goog/../cljs/core.js:10334
   \tat .cljs_rhino_repl/goog/../cljs/core.js:3979 (cljs$core$seq)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28083 (cljs$core$pr_sequential_writer)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28811
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28267 (cljs$core$pr_writer_impl)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28349 (cljs$core$pr_writer)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28353 (cljs$core$pr_seq_writer)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28416 (cljs$core$pr_sb_with_opts)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28430 (cljs$core$pr_str_with_opts)
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28524
   \tat .cljs_rhino_repl/goog/../cljs/core.js:28520 (cljs$core$pr_str)
   at <cljs repl>:1
   "
    {:ua-product :rhino}
    {:output-dir ".cljs_rhino_repl"})
  )

;; -----------------------------------------------------------------------------
;; Nashorn Stacktrace

(defmethod parse-stacktrace :nashorn
  [repl-env st err {:keys [output-dir] :as opts}]
  (letfn [(process-frame [frame-str]
            (when-not (or (string/blank? frame-str)
                          (== -1 (.indexOf frame-str "\tat")))
              (let [frame-str               (string/replace frame-str #"\s+at\s+" "")
                    [function file-and-line] (string/split frame-str #"\s+")
                    [file-part line-part]    (string/split file-and-line #":")]
                {:file     (string/replace (.substring file-part 1)
                             (str output-dir
                               #?(:clj File/separator :cljs "/"))
                             "")
                 :function function
                 :line     (when (and line-part (not (string/blank? line-part)))
                             (parse-int
                               (.substring line-part 0
                                 (dec (count line-part)))))
                 :column   0})))]
    (->> (string/split st #"\n")
      (map process-frame)
      (remove nil?)
      vec)))

;; -----------------------------------------------------------------------------
;; Graal.JS Stacktrace

(defmethod parse-stacktrace :graaljs
  [repl-env st err {:keys [output-dir] :as opts}]
  (letfn [(process-frame [frame-str]
            (when-not (string/blank? frame-str)
              (let [[function file-and-line] (string/split frame-str #"\(")
                    [file-part line-part]    (string/split file-and-line #":")]
                {:file     (string/replace file-part
                             (str output-dir
                               #?(:clj File/separator :cljs "/"))
                             "")
                 :function function
                 :line     (when (and line-part (not (string/blank? line-part)))
                             (parse-int
                               (.substring line-part 0
                                 (dec (count line-part)))))
                 :column   0})))]
    (->> (string/split st #"\n")
      (map process-frame)
      (remove nil?)
      vec)))

(comment
  (parse-stacktrace {}
    "Error: 1 is not ISeqable
    \tat cljs$core$seq (.cljs_nashorn_repl/goog/../cljs/core.js:3998)
    \tat cljs$core$first (.cljs_nashorn_repl/goog/../cljs/core.js:4017)
    \tat cljs$core$ffirst (.cljs_nashorn_repl/goog/../cljs/core.js:5160)
    \tat <anonymous> (.cljs_nashorn_repl/goog/../cljs/core.js:16005)
    \tat <anonymous> (.cljs_nashorn_repl/goog/../cljs/core.js:16004)
    \tat sval (.cljs_nashorn_repl/goog/../cljs/core.js:10243)
    \tat cljs$core$ISeqable$_seq$arity$1-6 (.cljs_nashorn_repl/goog/../cljs/core.js:10334)
    \tat cljs$core$seq (.cljs_nashorn_repl/goog/../cljs/core.js:3979)
    \tat cljs$core$pr_sequential_writer (.cljs_nashorn_repl/goog/../cljs/core.js:28083)
    \tat cljs$core$IPrintWithWriter$_pr_writer$arity$3-5 (.cljs_nashorn_repl/goog/../cljs/core.js:28811)
    \tat cljs$core$pr_writer_impl (.cljs_nashorn_repl/goog/../cljs/core.js:28267)
    \tat cljs$core$pr_writer (.cljs_nashorn_repl/goog/../cljs/core.js:28349)
    \tat cljs$core$pr_seq_writer (.cljs_nashorn_repl/goog/../cljs/core.js:28353)
    \tat cljs$core$pr_sb_with_opts (.cljs_nashorn_repl/goog/../cljs/core.js:28416)
    \tat cljs$core$pr_str_with_opts (.cljs_nashorn_repl/goog/../cljs/core.js:28430)
    \tat cljs$core$IFn$_invoke$arity$variadic-71 (.cljs_nashorn_repl/goog/../cljs/core.js:28524)
    \tat cljs$core$pr_str (.cljs_nashorn_repl/goog/../cljs/core.js:28520)
    \tat <anonymous> (<eval>:1)
    \tat <program> (<eval>:1)\n"
    {:ua-product :nashorn}
    {:output-dir ".cljs_nashorn_repl"})
  )

;; -----------------------------------------------------------------------------
;; Node.js Stacktrace

(defmethod parse-stacktrace :nodejs
  [repl-env st err {:keys [output-dir] :as opts}]
  (letfn [(parse-source-loc-info [x]
            (when (and x (not (string/blank? x)))
              (parse-int x)))
          (process-frame [frame-str]
            (when-not (or (string/blank? frame-str)
                          (nil? (re-find #"^\s+at" frame-str)))
              (let [frame-str (string/replace frame-str #"\s+at\s+" "")]
                (when-not (string/starts-with? frame-str "repl:")
                  (let [parts (string/split frame-str #"\s+")
                        [function file&line] (if (== 2 (count parts))
                                                   [(first parts)
                                                    (subs (second parts) 1
                                                      (dec (count (second parts))))]
                                                   [nil (first parts)])
                        [file-part line-part col-part] (string/split file&line #":")]
                    {:file     (if function
                                 (cond-> file-part
                                   output-dir
                                   (string/replace
                                     (str output-dir
                                       #?(:clj File/separator :cljs "/"))
                                     ""))
                                 file-part)
                     :function function
                     :line     (parse-source-loc-info line-part)
                     :column   (parse-source-loc-info col-part)})))))]
    (->> (string/split st #"\n")
      (map process-frame)
      (remove nil?)
      vec)))

(comment
  (parse-stacktrace {}
    "Error: 1 is not ISeqable
    at cljs$core$seq (.cljs_node_repl/cljs/core.cljs:1118:20)
    at repl:1:65
    at repl:9:4
    at repl:17:3
    at repl:22:4
    at Object.exports.runInThisContext (vm.js:54:17)
    at Domain.<anonymous> ([stdin]:41:34)
    at Domain.run (domain.js:228:14)
    at Socket.<anonymous> ([stdin]:40:25)
    at emitOne (events.js:77:13)"

    {:ua-product :nodejs}
    {:output-dir ".cljs_node_repl"})
  )

;; -----------------------------------------------------------------------------
;; Stacktrace Mapping

(defn remove-ext [file]
  (-> file
    (string/replace #"\.js$" "")
    (string/replace #"\.cljs$" "")
    (string/replace #"\.cljc$" "")
    (string/replace #"\.clj$" "")))

(defn mapped-line-column-call
  "Given a cljs.source-map source map data structure map a generated line
   and column back to the original line, column, and function called."
  [sms file line column]
  (let [source-map (get sms (symbol (string/replace (remove-ext file) "/" ".")))]
    ;; source maps are 0 indexed for columns
    ;; multiple segments may exist at column
    ;; the last segment seems most accurate
    (letfn [(get-best-column [columns column]
             (last (or (get columns
                         (last (filter #(<= % (dec column))
                                 (sort (keys columns)))))
                     (second (first columns)))))
           (adjust [mapped]
             (vec (map #(%1 %2) [inc inc identity] mapped)))]
     (let [default [line column nil]]
       ;; source maps are 0 indexed for lines
       (if-let [columns (get source-map (dec line))]
         (adjust (map (get-best-column columns column) [:line :col :name]))
         default)))))

(defn mapped-frame
  "Given opts and a canonicalized JavaScript stacktrace frame, return the
  ClojureScript frame."
  [{:keys [function file line column]} sms opts]
  (let [no-source-file?      (if-not file true (starts-with? file "<"))
        [line' column' call] (if no-source-file?
                               [line column nil]
                               (mapped-line-column-call sms file line column))
        file'                (when-not no-source-file?
                               (if (ends-with? file ".js")
                                 (str (subs file 0 (- (count file) 3)) ".cljs")
                                 file))]
    {:function function
     :call     call
     :file     (if no-source-file?
                 (str "NO_SOURCE_FILE" (when file (str " " file)))
                 file')
     :line     line'
     :column   column'}))

(defn mapped-stacktrace
  "Given a vector representing the canonicalized JavaScript stacktrace
   return the ClojureScript stacktrace. The canonical stacktrace must be
   in the form:

    [{:file <string>
      :function <string>
      :line <integer>
      :column <integer>}*]

   :file must be a URL path (without protocol) relative to :output-dir or a
   identifier delimited by angle brackets. The returned mapped stacktrace will
   also contain :url entries to the original sources if it can be determined
   from the classpath."
  ([stacktrace sms]
   (mapped-stacktrace stacktrace sms nil))
  ([stacktrace sms opts]
   (letfn [(call->function [x]
             (if (:call x)
               (hash-map :function (:call x))
               {}))
           (call-merge [function call]
             (merge-with
               (fn [munged-fn-name unmunged-call-name]
                 (if (= munged-fn-name
                        (string/replace (munge unmunged-call-name) "." "$"))
                   unmunged-call-name
                   munged-fn-name))
               function call))]
     (let [mapped-frames (map (memoize #(mapped-frame % sms opts)) stacktrace)]
       ;; take each non-nil :call and optionally merge it into :function one-level
       ;; up to avoid replacing with local symbols, we only replace munged name if
       ;; we can munge call symbol back to it
       (vec (map call-merge
              (map #(dissoc % :call) mapped-frames)
              (concat (rest (map call->function mapped-frames)) [{}])))))))

(defn mapped-stacktrace-str
  "Given a vector representing the canonicalized JavaScript stacktrace and a map
  of library names to decoded source maps, print the ClojureScript stacktrace .
  See mapped-stacktrace."
  ([stacktrace sms]
   (mapped-stacktrace-str stacktrace sms nil))
  ([stacktrace sms opts]
   (with-out-str
     (doseq [{:keys [function file line column]}
             (mapped-stacktrace stacktrace sms opts)]
       (println "\t"
         (str (when function (str function " "))
              "(" file (when line (str ":" line))
                       (when column (str ":" column)) ")"))))))

(comment
  (require '[cljs.closure :as cljsc]
           '[clojure.data.json :as json]
           '[cljs.source-map :as sm]
           '[clojure.pprint :as pp])

  (cljsc/build "samples/hello/src"
    {:optimizations :none
     :output-dir "samples/hello/out"
     :output-to "samples/hello/out/hello.js"
     :source-map true})

  (def sms
    {'hello.core
     (sm/decode
       (json/read-str
         (slurp "samples/hello/out/hello/core.js.map")
         :key-fn keyword))})

  (pp/pprint sms)

  ;; maps to :line 5 :column 24
  (mapped-stacktrace
    [{:file "hello/core.js"
      :function "first"
      :line 6
      :column 0}]
    sms {:output-dir "samples/hello/out"})

  (mapped-stacktrace-str
    [{:file "hello/core.js"
      :function "first"
      :line 6
      :column 0}]
    sms {:output-dir "samples/hello/out"})
  )
