(require '[cljs.build.api :as api]
         '[clojure.string :as str])

;; clj build.clj help # Prints details about tasks

;;; Configuration.

(def cljsbuild-config
  {:dev
   {:ios
    {:source-paths     ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src"]
     :compiler         {:output-to     "target/ios/app.js"
                        :main          "env.ios.main"
                        :output-dir    "target/ios"
                        :npm-deps false
                        :optimizations :none}
     :warning-handlers '[status-im.utils.build/warning-handler]}
    :android
    {:source-paths     ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src"]
     :compiler         {:output-to     "target/android/app.js"
                        :main          "env.android.main"
                        :output-dir    "target/android"
                        :npm-deps false
                        :optimizations :none}
     :warning-handlers '[status-im.utils.build/warning-handler]}
    :desktop
    {:source-paths     ["components/src" "react-native/src/cljsjs" "react-native/src/desktop" "src"]
     :compiler         {:output-to     "target/desktop/app.js"
                        :main          "env.desktop.main"
                        :output-dir    "target/desktop"
                        :npm-deps false
                        :optimizations :none}
     :warning-handlers '[status-im.utils.build/warning-handler]}}

   :prod
   {:ios
    {:source-paths     ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "env/prod"]
     :compiler         {:output-to          "index.ios.js"
                        :output-dir         "target/ios-prod"
                        :static-fns         true
                        :optimize-constants true
                        :optimizations      :simple
                        :closure-defines    {"goog.DEBUG" false}
                        :parallel-build     false
                        :elide-asserts      true
                        :language-in        :ecmascript5}
     :warning-handlers '[status-im.utils.build/warning-handler]}
    :android
    {:source-paths     ["components/src" "react-native/src/cljsjs" "react-native/src/mobile" "src" "env/prod"]
     :compiler         {:output-to          "index.android.js"
                        :output-dir         "target/android-prod"
                        :static-fns         true
                        :optimize-constants true
                        :optimizations      :simple
                        :closure-defines    {"goog.DEBUG" false}
                        :parallel-build     false
                        :elide-asserts      true
                        :language-in        :ecmascript5}
     :warning-handlers '[status-im.utils.build/warning-handler]}
    :desktop
    {:source-paths     ["components/src" "react-native/src/cljsjs" "react-native/src/desktop" "src" "env/prod"]
     :compiler         {:output-to          "index.desktop.js"
                        :output-dir         "target/desktop-prod"
                        :static-fns         true
                        :optimize-constants true
                        :optimizations      :simple
                        :closure-defines    {"goog.DEBUG" false}
                        :parallel-build     false
                        :elide-asserts      true
                        :language-in        :ecmascript5}
     :warning-handlers '[status-im.utils.build/warning-handler]}}

   :test
   {:test
    {:source-paths ["src" "test/cljs"]
     :compiler     {:main          "status-im.test.runner"
                    :output-to     "target/test/test.js"
                    :output-dir    "target/test"
                    :optimizations :none
                    :preamble      ["js/hook-require.js"]
                    :target        :nodejs}}
    :protocol
    {:source-paths ["src" "test/cljs"]
     :compiler     {:main          "status-im.test.protocol.runner"
                    :output-to     "target/test/test.js"
                    :output-dir    "target/test"
                    :optimizations :none
                    :target        :nodejs}}
    :env-dev-utils
    {:source-paths ["env/dev/env/utils.cljs" "test/env/dev"]
     :compiler     {:main          "env.test.runner"
                    :output-to     "target/test/test.js"
                    :output-dir    "target/test"
                    :optimizations :none
                    :target        :nodejs}}}})

(def cli-tasks-info
  {:compile  {:desc  "Compile ClojureScript"
              :usage ["Usage: clj build.clj compile [env] [build-id] [type]"
                      ""
                      "[env] (required): Pre-defined build environment. Allowed values: \"dev\", \"prod\", \"test\""
                      "[build-id] (optional): Build ID. When omitted, this task will compile all builds from the specified [env]."
                      "[type] (optional): Build type - value could be \"once\" or \"watch\". Default: \"once\"."]}
   :watch {:desc  "Start development"
           :usage ["Usage: clj -R:dev build.clj watch [options]"
                   ""
                   "[-h|--help] to see all available options"]}
   :test     {:desc  "Run tests"
              :usage ["Usage: clj -R:test build.clj test [build-id]"
                      ""
                      "[build-id] (required): Value could be \"test\", \"protocol\" or \"env-dev-utils\". It will compile then run the tests once."]}
   :help     {:desc "Show this help"}})

;;; Helper functions.

(def reset-color "\u001b[0m")
(def red-color "\u001b[31m")
(def green-color "\u001b[32m")
(def yellow-color "\u001b[33m")

(defn- colorizer [c]
  (fn [& args]
    (str c (apply str args) reset-color)))

(defn- println-colorized [message color]
  (println ((colorizer color) message)))

(defn- elapsed [started-at]
  (let [elapsed-us (- (System/currentTimeMillis) started-at)]
    (with-precision 2
      (str (/ (double elapsed-us) 1000) " seconds"))))

(defn- try-require [ns-sym]
  (try
    (require ns-sym)
    true
    (catch Exception e
      false)))

(defmacro with-namespaces [namespaces & body]
  (if (every? try-require namespaces)
    `(do ~@body)
    `(do (println-colorized "task not available - required dependencies not found" red-color)
         (System/exit 1))))

(defn- get-cljsbuild-config [name-env & [name-build-id]]
  (try
    (let [env (keyword name-env)]
      (when-not (contains? cljsbuild-config env)
        (throw (Exception. (str "ENV " (pr-str name-env) " does not exist"))))
      (let [env-config (get cljsbuild-config env)]
        (if name-build-id
          (let [build-id (keyword name-build-id)]
            (when-not (contains? env-config build-id)
              (throw (Exception. (str "Build ID " (pr-str name-build-id) " does not exist"))))
            (get env-config build-id))
          env-config)))
    (catch Exception e
      (println-colorized (.getMessage e) red-color)
      (System/exit 1))))

(defn- get-output-files [compiler-options]
  (if-let [output-file (:output-to compiler-options)]
    [output-file]
    (into [] (map :output-to (->> compiler-options :modules vals)))))

(defn- compile-cljs-with-build-config [build-config build-fn env build-id]
  (let [{:keys [source-paths compiler]} build-config
        output-files                    (get-output-files compiler)
        started-at                      (System/currentTimeMillis)]
    (println (str "Compiling " (pr-str build-id) " for " (pr-str env) "..."))
    (flush)
    (build-fn (apply api/inputs source-paths) compiler)
    (println-colorized (str "Successfully compiled " (pr-str output-files) " in " (elapsed started-at) ".") green-color)))

(defn- compile-cljs [env & [build-id watch?]]
  (let [build-fn (if watch? api/watch api/build)]
    (if build-id
      (compile-cljs-with-build-config (get-cljsbuild-config env build-id) build-fn env build-id)
      (doseq [[build-id build-config] (get-cljsbuild-config env)]
        (compile-cljs-with-build-config (get-cljsbuild-config env build-id) build-fn env build-id)))))

(defn- show-help []
  (doseq [[task {:keys [desc usage]}] cli-tasks-info]
    (println (format (str yellow-color "%-12s" reset-color green-color "%s" reset-color)
                     (name task) desc))
    (when usage
      (println)
      (->> usage
           (map #(str "  " %))
           (str/join "\n")
           println)
      (println))))

;;; Task dispatching

(defmulti task first)

(defmethod task :default [args]
  (println (format "Unknown or missing task. Choose one of: %s\n"
                   (->> cli-tasks-info
                        keys
                        (map name)
                        (interpose ", ")
                        (apply str))))
  (show-help)
  (System/exit 1))

;;; Compiling task

(defmethod task "compile" [[_ env build-id type]]
  (case type
    (nil "once") (compile-cljs env build-id)
    "watch"      (compile-cljs env build-id true)
    (do (println "Unknown argument to compile task:" type)
        (System/exit 1))))

;;; Testing task

(defmethod task "test" [[_ build-id]]
  (with-namespaces [[doo.core :as doo]]
    (compile-cljs :test build-id)
    (doo/run-script :node (->> build-id (get-cljsbuild-config :test) :compiler))))

;;; :watch task

(defn hawk-handler-resources
  [ctx e]
  (let [path "src/status_im/utils/js_resources.cljs"
        js-resourced (slurp path)]
    (spit path (str js-resourced " ;;"))
    (spit path js-resourced))
  ctx)

(defn hawk-handler-translations
  [ctx e]
  (let [path "src/status_im/i18n.cljs"
        i18n (slurp path)]
    (spit path (str i18n " ;;"))
    (spit path i18n))
  ctx)

(defmethod task "watch" [[_ & args]]
  (with-namespaces [[hawk.core :as hawk]
                    [re-frisk-sidecar.core :as rfs]
                    [figwheel-sidecar.repl-api :as ra]
                    [clj-rn.core :as clj-rn]
                    [clj-rn.main :as main]]
    (let [options (main/parse-cli-options args main/watch-task-options)]
      (clj-rn/watch (assoc options :start-cljs-repl false))
      (rfs/-main)
      (hawk/watch! [{:paths ["resources"] :handler hawk-handler-resources}
                    {:paths ["translations"] :handler hawk-handler-translations}])
      (when (:start-cljs-repl options) (ra/cljs-repl)))))

;;; Help

(defmethod task "help" [_]
  (show-help)
  (System/exit 1))

;;; Build script entrypoint.

(task *command-line-args*)