;   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 clojure.string
  (:refer-clojure :exclude [replace reverse])
  (:require [goog.string :as gstring])
  (:import [goog.string StringBuffer]))

(defn- seq-reverse
  [coll]
  (reduce conj () coll))

(def ^:private re-surrogate-pair
  (js/RegExp. "([\\uD800-\\uDBFF])([\\uDC00-\\uDFFF])" "g"))

(defn reverse
  "Returns s with its characters reversed."
  [s]
  (-> (.replace s re-surrogate-pair "$2$1")
      (.. (split "") (reverse) (join ""))))

(defn- replace-all
  [s re replacement]
  (let [r (js/RegExp. (.-source re)
                      (cond-> "g"
                        (.-ignoreCase re) (str "i")
                        (.-multiline re) (str "m")
                        (.-unicode re) (str "u")))]
    (.replace s r replacement)))

(defn- replace-with
  [f]
  (fn [& args]
    (let [matches (drop-last 2 args)]
      (if (= (count matches) 1)
        (f (first matches))
        (f (vec matches))))))

(defn replace
  "Replaces all instance of match with replacement in s.
   match/replacement can be:

   string / string
   pattern / (string or function of match)."
  [s match replacement]
  (cond
    (string? match)
    (.replace s (js/RegExp. (gstring/regExpEscape match) "g") replacement)

    (instance? js/RegExp match)
    (if (string? replacement)
      (replace-all s match replacement)
      (replace-all s match (replace-with replacement)))

    :else (throw (str "Invalid match arg: " match))))

(defn replace-first
  "Replaces the first instance of match with replacement in s.
   match/replacement can be:

   string / string
   pattern / (string or function of match)."
  [s match replacement]
  (.replace s match replacement))

(defn join
  "Returns a string of all elements in coll, as returned by (seq coll),
  separated by an optional separator."
  ([coll]
   (loop [sb (StringBuffer.) coll (seq coll)]
     (if-not (nil? coll)
       (recur (. sb (append (str (first coll)))) (next coll))
       (.toString sb))))
  ([separator coll]
   (loop [sb (StringBuffer.) coll (seq coll)]
     (if-not (nil? coll)
       (do
         (. sb (append (str (first coll))))
         (let [coll (next coll)]
           (when-not (nil? coll)
             (. sb (append separator)))
           (recur sb coll)))
       (.toString sb)))))

(defn upper-case
  "Converts string to all upper-case."
  [s]
  (.toUpperCase s))

(defn lower-case
  "Converts string to all lower-case."
  [s]
  (.toLowerCase s))

(defn capitalize
  "Converts first character of the string to upper-case, all other
  characters to lower-case."
  [s]
  (if (< (count s) 2)
    (upper-case s)
    (str (upper-case (subs s 0 1))
         (lower-case (subs s 1)))))

;; The JavaScript split function takes a limit argument but the return
;; value is not the same as the Java split function.
;;
;; Java: (.split "a-b-c" #"-" 2) => ["a" "b-c"]
;; JavaScript: (.split "a-b-c" #"-" 2) => ["a" "b"]
;;
;; For consistency, the three arg version has been implemented to
;; mimic Java's behavior.

(defn- pop-last-while-empty
  [v]
  (loop [v v]
    (if (identical? "" (peek v))
      (recur (pop v))
      v)))

(defn- discard-trailing-if-needed
  [limit v]
  (if (and (== 0 limit) (< 1 (count v)))
    (pop-last-while-empty v)
    v))

(defn- split-with-empty-regex
  [s limit]
  (if (or (<= limit 0) (>= limit (+ 2 (count s))))
    (conj (vec (cons "" (map str (seq s)))) "")
    (condp == limit
      1 (vector s)
      2 (vector "" s)
      (let [c (- limit 2)]
        (conj (vec (cons "" (subvec (vec (map str (seq s))) 0 c))) (subs s c))))))

(defn split
  "Splits string on a regular expression. Optional argument limit is
  the maximum number of splits. Not lazy. Returns vector of the splits."
  ([s re]
     (split s re 0))
    ([s re limit]
     (discard-trailing-if-needed limit
       (if (identical? "/(?:)/" (str re))
         (split-with-empty-regex s limit)
         (if (< limit 1)
           (vec (.split (str s) re))
           (loop [s s
                  limit limit
                  parts []]
             (if (== 1 limit)
               (conj parts s)
               (let [m (re-find re s)]
                 (if-not (nil? m)
                   (let [index (.indexOf s m)]
                     (recur (.substring s (+ index (count m)))
                       (dec limit)
                       (conj parts (.substring s 0 index))))
                   (conj parts s))))))))))

(defn split-lines
  "Splits s on \n or \r\n."
  [s]
  (split s #"\n|\r\n"))

(defn trim
  "Removes whitespace from both ends of string."
  [s]
  (gstring/trim s))

(defn triml
  "Removes whitespace from the left side of string."
  [s]
  (gstring/trimLeft s))

(defn trimr
  "Removes whitespace from the right side of string."
  [s]
  (gstring/trimRight s))

(defn trim-newline
  "Removes all trailing newline \\n or return \\r characters from
  string.  Similar to Perl's chomp."
  [s]
  (loop [index (.-length s)]
    (if (zero? index)
      ""
      (let [ch (get s (dec index))]
        (if (or (identical? \newline ch)
                (identical? \return ch))
          (recur (dec index))
          (.substring s 0 index))))))

(defn ^boolean blank?
  "True is s is nil, empty, or contains only whitespace."
  [s]
  (gstring/isEmptySafe s))

(defn escape
  "Return a new string, using cmap to escape each character ch
   from s as follows:

   If (cmap ch) is nil, append ch to the new string.
   If (cmap ch) is non-nil, append (str (cmap ch)) instead."
  [s cmap]
  (let [buffer (StringBuffer.)
        length (.-length s)]
    (loop [index 0]
      (if (== length index)
        (. buffer (toString))
        (let [ch (.charAt s index)
              replacement (get cmap ch)]
          (if-not (nil? replacement)
            (.append buffer (str replacement))
            (.append buffer ch))
          (recur (inc index)))))))

(defn index-of
  "Return index of value (string or char) in s, optionally searching
  forward from from-index or nil if not found."
  ([s value]
   (let [result (.indexOf s value)]
     (if (neg? result)
       nil
       result)))
  ([s value from-index]
   (let [result (.indexOf s value from-index)]
     (if (neg? result)
       nil
       result))))

(defn last-index-of
  "Return last index of value (string or char) in s, optionally
  searching backward from from-index or nil if not found."
  ([s value]
   (let [result (.lastIndexOf s value)]
     (if (neg? result)
       nil
       result)))
  ([s value from-index]
   (let [result (.lastIndexOf s value from-index)]
     (if (neg? result)
       nil
       result))))

(defn ^boolean starts-with?
  "True if s starts with substr."
  [s substr]
  (gstring/startsWith s substr))

(defn ^boolean ends-with?
  "True if s ends with substr."
  [s substr]
  (gstring/endsWith s substr))

(defn ^boolean includes?
  "True if s includes substr."
  [s substr]
  (gstring/contains s substr))
