;   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 ^{:doc "Clojure String utilities

It is poor form to (:use clojure.string). Instead, use require
with :as to specify a prefix, e.g.

(ns your.namespace.here
  (:require [clojure.string :as str]))

Design notes for clojure.string:

1. Strings are Erlang binaries (as opposed to sequences). As such, the
   string being manipulated is the first argument to a function;
   passing nil will result in an error unless documented otherwise.
   If you want sequence-y behavior instead, use a sequence.

2. Functions are generally not lazy, and call straight to host
   methods where those are available and efficient.

3. Functions take advantage of String implementation details to
   write high-performing loop/recurs instead of using higher-order
   functions. (This is not idiomatic in general-purpose application
   code.)"
      :author "Stuart Sierra, Stuart Halloway, David Liebke"}
  clojure.string
  (:refer-clojure :exclude (replace reverse)))

(set! *warn-on-infer* true)

(defn ^clojerl.String reverse
  "Returns s with its characters reversed."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (-> (.str s)
      unicode/characters_to_list
      lists/reverse
      erlang/list_to_binary))

(defn ^clojerl.String re-quote-replacement
  "Given a replacement string that you wish to be a literal
   replacement for a pattern match in replace or replace-first, do the
   necessary escaping of special characters in the replacement."
  {:added "1.5"}
  [^clojerl.IStringable replacement]
  (erlang.util.Regex/quote (when replacement (str replacement))))

(defn- replace-by
  [^clojerl.IStringable s re f]
  (with-open [buffer (new erlang.io.StringWriter)]
    (let [^clojerl.String s (.str s)
          append erlang.io.StringWriter/write.2
          length (.length s)
          matches (re-run re s :global #erl [:capture :all :index])]
      (loop [index 0
             [[[i len] :as m] & ms] matches]
        (if m
          (let [m (map (fn [[i len]] (subs s i (+ i len))) m)
                m (if (= (count m) 1) (first m) m)]
            (append buffer (subs s index i))
            (append buffer (f m))
            (recur (+ i len) ms))
          (do
            (append buffer (subs s index length))
            (str buffer)))))))

(defn ^clojerl.String replace
  "Replaces all instance of match with replacement in s.

   match/replacement can be:

   string / string
   char / char
   pattern / (string or function of match).

   See also replace-first.

   The replacement is literal (i.e. none of its characters are treated
   specially) for all cases above except pattern / string.

   For pattern / string, $1, $2, etc. in the replacement string are
   substituted with the string that matched the corresponding
   parenthesized group in the pattern.  If you wish your replacement
   string r to be used literally, use (re-quote-replacement r) as the
   replacement argument.

   Example:
   (clojure.string/replace \"Almost Pig Latin\" #\"\\b(\\w)(\\w+)\\b\" \"$2$1ay\")
   -> \"lmostAay igPay atinLay\""
  {:added "1.2"}
  [^clojerl.IStringable s match replacement]
  (let [^clojerl.String s (.str s)
        replace erlang.util.Regex/replace.4
        opts #erl(:global)]
    (cond
      (string? match) (.replace ^clojerl.String s match replacement)
      (regex? match) (if (or (string? replacement)
                             (instance? erlang.io.StringWriter replacement))
                       (replace match s (str replacement) opts)
                       (replace-by s match replacement))
      :else (throw (clojerl.BadArgumentError. (str "Invalid match arg: " match))))))

(defn- replace-first-by
  [^clojerl.String s re f]
  (with-open [buffer (new erlang.io.StringWriter)]
    (let [append erlang.io.StringWriter/write.2
          length (.length s)
          [[i len] :as matches] (re-run re s #erl [:capture :all :index])
          matches (map (fn [[i len]] (subs s i (+ i len))) matches)
          matches (if (= (count matches) 1) (first matches) matches)]
      (if i
        (do
          (append buffer (subs s 0 i))
          (append buffer (f matches))
          (append buffer (subs s (+ i len) length))
          (str buffer))
        s))))

(defn- replace-first-str
  [^clojerl.String s match replace]
  (let [i (.index_of s match)]
    (if (= -1 i)
      s
      (str (subs s 0 i) replace (subs s (+ i (count match)))))))

(defn ^clojerl.String replace-first
  "Replaces the first instance of match with replacement in s.

   match/replacement can be:

   char / char
   string / string
   pattern / (string or function of match).

   See also replace.

   The replacement is literal (i.e. none of its characters are treated
   specially) for all cases above except pattern / string.

   For pattern / string, $1, $2, etc. in the replacement string are
   substituted with the string that matched the corresponding
   parenthesized group in the pattern.  If you wish your replacement
   string r to be used literally, use (re-quote-replacement r) as the
   replacement argument.

   Example:
   (clojure.string/replace-first \"swap first two words\"
                                 #\"(\\w+)(\\s+)(\\w+)\" \"$3$2$1\")
   -> \"first swap two words\""
  {:added "1.2"}
  [^clojerl.IStringable s match replacement]
  (let [^clojerl.String s (.str s)
        replace erlang.util.Regex/replace.4
        opts #erl ()]
    (cond
      (or (string? match)
          (instance? erlang.io.StringWriter match))
      (replace-first-str s (str match) (str replacement))
      (regex? match)
      (if (or (string? replacement)
              (instance? erlang.io.StringWriter replacement))
        (replace match s (str replacement) opts)
        (replace-first-by s match replacement))
      :else (throw (clojerl.BadArgumentError. (str "Invalid match arg: " match))))))

(defn ^clojerl.String join
  "Returns a string of all elements in coll, as returned by (seq coll),
   separated by an optional separator."
  {:added "1.2"}
  ([coll]
   (apply str coll))
  ([separator coll]
   (.join ^clojerl.String (clj_rt/to_list coll) separator)))

(defn ^clojerl.String upper-case
  "Converts string to all upper-case."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (.to_upper ^clojerl.String (.str s)))

(defn ^clojerl.String lower-case
  "Converts string to all lower-case."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (.to_lower ^clojerl.String (.str s)))

(defn ^clojerl.String capitalize
  "Converts first character of the string to upper-case, all other
  characters to lower-case."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (let [^clojerl.String s (.str s)]
    (if (< (.length s) 2)
      (.to_upper ^clojerl.String s)
      (.append ^clojerl.String
               (.to_upper ^clojerl.String (.substring s 0 1))
               (.to_lower ^clojerl.String (.substring s 1))))))

(defn split
  "Splits string on a regular expression.  Optional argument limit is
  the maximum number of splits. Not lazy. Returns vector of the splits."
  {:added "1.2"}
  ([^clojerl.IStringable s ^erlang.util.Regex re]
   (vec (.split re (.str s) #erl (:trim))))
  ([^clojerl.IStringable s ^erlang.util.Regex re limit]
   (vec (.split re (.str s) #erl(:trim :global #erl [:match_limit limit])))))

(defn split-lines
  "Splits s on \\n or \\r\\n."
  {:added "1.2"}
  [s]
  (split s #"\r?\n"))

(defn ^clojerl.String trim
  "Removes whitespace from both ends of string."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (let [^clojerl.String s (.str s)
        len (.length s)]
    (loop [rindex len]
      (if (zero? rindex)
        ""
        (if (.is_whitespace ^clojerl.String (.char_at s (dec rindex)))
          (recur (dec rindex))
          ;; there is at least one non-whitespace char in the string,
          ;; so no need to check for lindex reaching len.
          (loop [lindex 0]
            (if (.is_whitespace ^clojerl.String (.char_at s lindex))
              (recur (inc lindex))
              (subs s lindex rindex))))))))

(defn ^clojerl.String triml
  "Removes whitespace from the left side of string."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (let [^clojerl.String s (.str s)
        len (.length s)]
    (loop [index 0]
      (if (= len index)
        ""
        (if (.is_whitespace ^clojerl.String (.char_at s index))
          (recur (inc index))
          (subs s index len))))))

(defn ^clojerl.String trimr
  "Removes whitespace from the right side of string."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (let [^clojerl.String s (.str s)]
    (loop [index (.length s)]
      (if (zero? index)
        ""
        (if (.is_whitespace ^clojerl.String (.char_at s (dec index)))
          (recur (dec index))
          (subs s 0 index))))))

(defn ^clojerl.String trim-newline
  "Removes all trailing newline \\n or return \\r characters from
  string.  Similar to Perl's chomp."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (let [^clojerl.String s (.str s)]
    (loop [index (.length s)]
      (if (zero? index)
        ""
        (let [ch (.char_at s (dec index))]
          (if (or (= ch \newline) (= ch \return))
            (recur (dec index))
            (subs (.str s) 0 index)))))))

(defn blank?
  "True if s is nil, empty, or contains only whitespace."
  {:added "1.2"}
  [^clojerl.IStringable s]
  (if s
    (let [^clojerl.String s (.str s)
          len (.length s)]
      (loop [index (int 0)]
        (if (= len index)
          true
          (if (= :whitespace (clj_utils/char_type (binary/at s index)))
            (recur (inc index))
            false))))
    true))

(defn starts-with?
  "True if s starts with substr."
  {:added "1.8"}
  [^clojerl.IStringable s substr]
  (.starts_with ^clojerl.String (.str s) substr))

(defn ^clojerl.String 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."
  {:added "1.2"}
  [^clojerl.IStringable s cmap]
  (let [^clojerl.String s (.str s)
        length (.length s)
        append erlang.io.StringWriter/write.2]
    (with-open [buffer (new erlang.io.StringWriter)]
      (loop [index 0
             buffer buffer]
        (if (= length index)
          (str buffer)
          (let [ch (.char_at s index)]
            (if-let [replacement (cmap ch)]
              (append buffer replacement)
              (append buffer ch))
            (recur (inc index) buffer)))))))

(defn index-of
  "Return index of value (string or char) in s, optionally searching
  forward from from-index or nil if not found."
  {:added "1.8"}
  ([^clojerl.IStringable s value]
   (let [result (.index_of ^clojerl.String (.str s) value)]
     (if (= result -1)
       nil
       result)))
  ([^clojerl.IStringable s value from-index]
   (let [result (.index_of ^clojerl.String (.str s) value from-index)]
     (if (= result -1)
       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."
  {:added "1.8"}
  ([^clojerl.IStringable s value]
   (let [result (.last_index_of ^clojerl.String (.str s) value)]
     (if (= result -1)
       nil
       result)))
  ([^clojerl.IStringable s value from-index]
   (let [result (.last_index_of ^clojerl.String (.str s) value from-index)]
     (if (= result -1)
       nil
       result))))

(defn ends-with?
  "True if s ends with substr."
  {:added "1.8"}
  [^clojerl.IStringable s ^clojerl.String substr]
  (.ends_with ^clojerl.String (.str s) substr))

(defn includes?
  "True if s includes substr."
  {:added "1.8"}
  [^clojerl.IStringable s ^clojerl.String substr]
  (.contains ^clojerl.String (.str s) substr))
