(ns no.en.core
  (:refer-clojure :exclude [replace read-string])
  (:require [clojure.string :refer [blank? join replace split upper-case]]
            #?(:clj [clojure.edn :refer [read-string]])
            #?(:cljs [cljs.reader :refer [read-string]])
            #?(:cljs [goog.crypt.base64 :as base64]))
  #?(:clj (:import [java.net URLEncoder URLDecoder]
                   [org.apache.commons.codec.binary Base64])))

(def port-number
  {:amqp 5672
   :http 80
   :https 443
   :mysql 3306
   :postgresql 5432
   :rabbitmq 5672
   :zookeeper 2181})

(def url-regex #"([^:]+)://(([^:]+):([^@/]+)@)?(([^:/]+)(:([0-9]+))?((/[^?#]*)(\?([^#]*))?)?)(\#(.*))?")

(defn split-by-regex
  "Split the string `s` by the regex `pattern`."
  [s pattern]
  (if (sequential? s)
    s (if-not (blank? s)
        (split s pattern))))

(defn split-by-comma
  "Split the string `s` by comma."
  [s] (split-by-regex s #"\s*,\s*"))

(defn utf8-string
  "Returns `bytes` as an UTF-8 encoded string."
  [bytes]
  #?(:clj (String. bytes "UTF-8")
     :cljs (throw (ex-info "utf8-string not implemented yet" bytes))))

(defn base64-encode
  "Returns `s` as a Base64 encoded string."
  [bytes]
  (when bytes
    #?(:clj (String. (Base64/encodeBase64 bytes))
       :cljs (base64/encodeString bytes false))))

(defn base64-decode
  "Returns `s` as a Base64 decoded string."
  [s]
  (when s
    #?(:clj (Base64/decodeBase64 (.getBytes s))
       :cljs (base64/decodeString s false))))

(defn compact-map
  "Removes all map entries where the value of the entry is empty."
  [m]
  (reduce
   (fn [m k]
     (let [v (get m k)]
       (if (or (nil? v)
               (and (or (map? v)
                        (sequential? v))
                    (empty? v)))
         (dissoc m k) m)))
   m (keys m)))

(defn url-encode
  "Returns `s` as an URL encoded string."
  [s & [encoding]]
  (when s
    #?(:clj (-> (URLEncoder/encode (str s) (or encoding "UTF-8"))
                (replace "%7E" "~")
                (replace "*" "%2A")
                (replace "+" "%20"))
       :cljs (-> (js/encodeURIComponent (str s))
                 (replace "*" "%2A")))))

(defn url-decode
  "Returns `s` as an URL decoded string."
  [s & [encoding]]
  (when s
    #?(:clj (URLDecoder/decode s (or encoding "UTF-8"))
       :cljs (js/decodeURIComponent s))))

(defn pow [n x]
  #?(:clj (Math/pow n x)
     :cljs (.pow js/Math n x)))

(def byte-scale
  {"B" (pow 1024 0)
   "K" (pow 1024 1)
   "M" (pow 1024 2)
   "G" (pow 1024 3)
   "T" (pow 1024 4)
   "P" (pow 1024 5)
   "E" (pow 1024 6)
   "Z" (pow 1024 7)
   "Y" (pow 1024 8)})

(defn- apply-unit [number unit]
  (if (string? unit)
    (case (upper-case unit)
      (case unit
        "M" (* number 1000000)
        "B" (* number 1000000000)))
    number))

(defn- parse-number [s parse-fn]
  (if-let [matches (re-matches #"\s*([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)(M|B)?.*" (str s))]
    #?(:clj
       (try (let [number (parse-fn (nth matches 1))
                  unit (nth matches 3)]
              (apply-unit number unit))
            (catch NumberFormatException _ nil))
       :cljs
       (let [number (parse-fn (nth matches 1))
             unit (nth matches 3)]
         (if-not (js/isNaN number)
           (apply-unit number unit))))))

(defn parse-bytes [s]
  (if-let [matches (re-matches #"\s*([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)(B|K|M|G|T|P|E|Z|Y)?.*" (str s))]
    (let [number (read-string (nth matches 1))
          unit (nth matches 3)]
      (long (* (long (read-string (str (nth matches 1))))
               (get byte-scale (upper-case (or unit "")) 1))))))

(defn parse-integer
  "Parse `s` as a integer number."
  [s]
  (parse-number s #(#?(:clj Integer/parseInt :cljs js/parseInt) %1)))

(defn parse-long
  "Parse `s` as a long number."
  [s]
  (parse-number s #(#?(:clj Long/parseLong :cljs js/parseInt) %1)))

(defn parse-double
  "Parse `s` as a double number."
  [s]
  (parse-number s #(#?(:clj Double/parseDouble :cljs js/parseFloat) %1)))

(defn parse-float
  "Parse `s` as a float number."
  [s]
  (parse-number s #(#?(:clj Float/parseFloat :cljs js/parseFloat) %1)))

(defn format-query-params
  "Format the map `m` into a query parameter string."
  [m]
  (let [params (->> (sort-by first (seq m))
                    (remove #(blank? (str (second %1))))
                    (map #(vector (url-encode (name (first %1)))
                                  (url-encode (second %1))))
                    (map #(join "=" %1))
                    (join "&"))]
    (if-not (blank? params)
      params)))

(defn format-url
  "Format the Ring map as an url."
  [m]
  (if (not (empty? m))
    (let [query-params (:query-params m)]
      (str (if (:scheme m)
             (str (name (:scheme m)) "://"))
           (let [{:keys [username password]} m]
             (when username
               (str username (when password (str ":" password)) "@")))
           (:server-name m)
           (if-let [port (:server-port m)]
             (if-not (= port (port-number (:scheme m)))
               (str ":" port)))
           (if (and (nil? (:uri m))
                    (not (empty? query-params)))
             "/" (:uri m))
           (if-not (empty? query-params)
             (str "?" (format-query-params query-params)))
           (if-not (blank? (:fragment m))
             (str "#" (:fragment m)))))))

(defn public-url
  "Return the formatted `url` without password as a string."
  [url]
  (format-url (dissoc url :password)))

(defn parse-percent
  "Parse `s` as a percentage."
  [s]
  (parse-double (replace s "%" "")))

(defn pattern-quote
  "Quote the special characters in `s` that are used in regular expressions."
  [s]
  (replace (name s) #"([\[\]\^\$\|\(\)\\\+\*\?\{\}\=\!.])" "\\\\$1"))

(defn separator
  "Returns the first string that separates the components in `s`."
  [s]
  (if-let [matches (re-matches #"(?i)([a-z0-9_-]+)([^a-z0-9_-]+).*" s)]
    (nth matches 2)))

(defn parse-query-params
  "Parse the query parameter string `s` and return a map."
  [s]
  (if s
    (->> (split (str s) #"&")
         (map #(split %1 #"="))
         (filter #(= 2 (count %1)))
         (mapcat #(vector (keyword (url-decode (first %1))) (url-decode (second %1))))
         (apply hash-map))))

(defn parse-url
  "Parse the url `s` and return a Ring compatible map."
  [s]
  (if-let [matches (re-matches url-regex (str s))]
    (let [scheme (keyword (nth matches 1))]
      (compact-map
       {:scheme scheme
        :username (nth matches 3)
        :password (nth matches 4)
        :server-name (nth matches 6)
        :server-port (or (parse-integer (nth matches 8)) (port-number scheme))
        :uri (nth matches 10)
        :query-params (parse-query-params  (nth matches 12))
        :query-string (nth matches 12)
        :fragment (nth matches 14)}))))

(defmacro prog1 [& body]
  "Evaluate `body`, returning the result of the first form."
  `(let [result# ~(first body)]
     ~@(rest body)
     result#))

(defn with-retries*
  "Executes thunk. If an exception is thrown, will retry. At most n retries
  are done. If still some exception is thrown it is bubbled upwards in
  the call chain."
  [n thunk]
  (loop [n n]
    (if-let [result
             (try
               [(thunk)]
               (catch #?(:clj Exception :cljs js/Error) e
                 (when (zero? n)
                   (throw e))))]
      (result 0)
      (recur (dec n)))))

(defmacro with-retries
  "Executes body. If an exception is thrown, will retry. At most n retries
  are done. If still some exception is thrown it is bubbled upwards in
  the call chain."
  [n & body]
  `(no.en.core/with-retries* ~n (fn [] ~@body)))

(defn- editable? [coll]
  #?(:clj  (instance? clojure.lang.IEditableCollection coll)
     :cljs (satisfies? cljs.core.IEditableCollection coll)))

(defn- reduce-map [f coll]
  (if (editable? coll)
    (persistent! (reduce-kv (f assoc!) (transient (empty coll)) coll))
    (reduce-kv (f assoc) (empty coll) coll)))

(defn map-keys
  "Maps a function over the keys of an associative collection."
  [f coll]
  (reduce-map (fn [xf] (fn [m k v] (xf m (f k) v))) coll))

(defn map-vals
  "Maps a function over the values of an associative collection."
  [f coll]
  (reduce-map (fn [xf] (fn [m k v] (xf m k (f v)))) coll))

(defn deep-merge
  "Like merge, but merges maps recursively."
  [& maps]
  (if (every? map? maps)
    (apply merge-with deep-merge maps)
    (last maps)))

(defn deep-merge-with
  "Like merge-with, but merges maps recursively, applying the given fn
  only when there's a non-map at a particular level."
  [f & maps]
  (apply
   (fn m [& maps]
     (if (every? map? maps)
       (apply merge-with m maps)
       (apply f maps)))
   maps))
