(ns cljs-time.format
  "### Utilities for parsing and unparsing DateTimes as Strings.

  Parsing and printing are controlled by formatters. You can either use one
  of the built in ISO 8601 and a single RFC 822 formatters or define your own, e.g.:

    (def built-in-formatter (formatters :basic-date-time))
    (def custom-formatter (formatter \"yyyyMMdd\"))

  To see a list of available built-in formatters and an example of a date-time
  printed in their format:

    (show-formatters)

  Once you have a formatter, parsing and printing are strait-forward:

    => (parse custom-formatter \"20100311\")
    #<DateTime 2010-03-11T00:00:00.000Z>

    => (unparse custom-formatter (date-time 2010 10 3))
    \"20101003\"

  By default the parse function always returns a DateTime instance with a UTC
  time zone, and the unparse function always represents a given DateTime
  instance in UTC. A formatter can be modified to different timezones, locales,
  etc with the functions with-zone, with-locale, with-chronology, and
  with-pivot-year."
  (:require
    [cljs-time.internal.core :refer [index-of valid-date? format zero-pad]]
    [cljs-time.core :as time]
    [clojure.set :refer [difference]]
    [clojure.string :as string]
    [goog.date :as date]
    [goog.date.duration :as duration]
    [goog.string :as gstring]
    [goog.string.format]))

(def months
  ["January" "February" "March" "April" "May" "June" "July" "August"
   "September" "October" "November" "December"])

(def days
  ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"])

(defn abbreviate [n s]
  (subs s 0 n))

(def ^{:doc "**Note: not all formatters have been implemented yet.**

  The pattern syntax is mostly compatible with java.text.SimpleDateFormat -
  time zone names cannot be parsed and a few more symbols are supported. All
  ASCII letters are reserved as pattern letters, which are defined as follows:

    Symbol  Meaning                      Presentation  Examples
    ------  -------                      ------------  -------
    G       era                          text          AD
    C       century of era (>=0)         number        20
    Y       year of era (>=0)            year          1996

    x       weekyear                     year          1996
    w       week of weekyear             number        27
    e       day of week                  number        2
    E       day of week                  text          Tuesday; Tue

    y       year                         year          1996
    D       day of year                  number        189
    M       month of year                month         July; Jul; 07
    d       day of month                 number        10

    a       halfday of day               text          PM
    K       hour of halfday (0~11)       number        0
    h       clockhour of halfday (1~12)  number        12

    H       hour of day (0~23)           number        0
    k       clockhour of day (1~24)      number        24
    m       minute of hour               number        30
    s       second of minute             number        55
    S       fraction of second           number        978
    a       meridiem                     text          am; pm
    A       meridiem                     text          AM; PM

    z       time zone                    text          Pacific Standard Time; PST
    Z       time zone offset/id          zone          -0800; -08:00; America/Los_Angeles

    '       escape for text              delimiter
    ''      single quote                 literal       '

  The count of pattern letters determine the format.

  **Text:** If the number of pattern letters is 4 or more, the full form is used;
  otherwise a short or abbreviated form is used if available.

  **Number:** The minimum number of digits. Shorter numbers are zero-padded to this
  amount.

  **Year:** Numeric presentation for year and weekyear fields are handled
  specially. For example, if the count of 'y' is 2, the year will be displayed
  as the zero-based year of the century, which is two digits.

  **Month:** 3 or over, use text, otherwise use number.

  **Zone:** 'Z' outputs offset without a colon, 'ZZ' outputs the offset with a
  colon, 'ZZZ' or more outputs the zone id.

  **Zone names:** Time zone names ('z') cannot be parsed.

  Any characters in the pattern that are not in the ranges of ['a'..'z'] and
  ['A'..'Z'] will be treated as quoted text. For instance, characters like ':',
  '.', ' ', '#' and '?' will appear in the resulting time text even they are
  not embraced within single quotes."}
  date-formatters
  (let [d      #(.getDate %)
        M #(inc (.getMonth %))
        y      #(.getYear %)
        h      #(let [hr (mod (.getHours %) 12)]
                  (if (zero? hr) 12 hr))
        a      #(if (< (.getHours %) 12) "am" "pm")
        A      #(if (< (.getHours %) 12) "AM" "PM")
        H      #(.getHours %)
        m      #(.getMinutes %)
        s      #(.getSeconds %)
        S      #(.getMilliseconds %)
        Z      #(.getTimezoneOffsetString %)
        doy    #(.getDayOfYear %)
        dow    #(.getDay %)]
    {"d" d
     "dd" #(zero-pad (d %))
     "dth" #(let [d (d %)] (str d (case d 1 "st" 2 "nd" 3 "rd" 21 "st" 22 "nd" 23 "rd" 31 "st" "th")))
     "dow" #(days (dow %))
     "D" doy
     "DD" doy
     "DDD" doy
     "EEE" #(abbreviate 3 (days (dow %)))
     "EEEE" #(days (dow %))
     "M" M
     "MM" #(zero-pad (M %))
     "MMM" #(abbreviate 3 (months (dec (M %))))
     "MMMM" #(months (dec (M %)))
     "yyyy" y
     "YYYY" y
     "yy" #(mod (y %) 100)
     "YY" #(mod (y %) 100)
     "xxxx" y
     "a" a
     "A" A
     "h" h
     "H" H
     "m" m
     "s" s
     "S" S
     "hh" #(zero-pad (h %))
     "HH" #(zero-pad (H %))
     "mm" #(zero-pad (m %))
     "ss" #(zero-pad (s %))
     "SSS" #(zero-pad (S %) 3)
     "Z" Z
     "ZZ" Z
     "ww" #(zero-pad (.getWeekNumber %))
     "e" dow}))

(defn timezone-adjustment [d timezone-string]
  (let [[_ sign hh mm] (string/split timezone-string
                                     #"Z|(?:([-+])(\d{2})(?::?(\d{2}))?)$")]
    (when (and sign hh mm)
      (let [sign (cond (= sign "-") time/plus
                       (= sign "+") time/minus)
            [hh mm] (map #(js/parseInt % 10) [hh mm])
            adjusted (-> d
                         (sign (time/hours hh))
                         (sign (time/minutes mm)))]
        (.setTime d (.getTime adjusted))))
    d))

(def date-parsers
  (let [parse-int #(js/parseInt % 10)
        assoc-fn (fn [kw] #(assoc %1 kw (parse-int %2)))
        y (assoc-fn :years)
        d (assoc-fn :days)
        M #(assoc %1 :months (dec (parse-int %2)))
        h #(assoc %1 :hours (mod (parse-int %2) 12))
        a (fn [{:keys [hours] :as date} x]
            (if (#{"pm" "p"} (string/lower-case x))
              (assoc date :hours (let [hours (+ 12 hours)]
                                   (if (= hours 24) 0 hours)))
              date))
        H (assoc-fn :hours)
        m (assoc-fn :minutes)
        s (assoc-fn :seconds)
        S (assoc-fn :millis)
        MMM #(let [full (first (filter (fn [m]
                                         (re-seq (re-pattern (str "^" %2)) m))
                                       months))]
               (M %1 (str (inc (index-of months full)))))
        MMMM #(M %1 (str (inc (index-of months %2))))
        skip (fn [x & args] x)
        tz #(assoc %1 :time-zone %2)]
    {"d" ["(\\d{1,2})" d]
     "dd" ["(\\d{2})" d]
     "D" ["(\\d{1,3})" d]
     "DD" ["(\\d{2,3})" d]
     "DDD" ["(\\d{3})" d]
     "dth" ["(\\d{1,2})(?:st|nd|rd|th)" d]
     "M" ["(\\d{1,2})" M]
     "MM" ["((?:\\d{2})|(?:\\b\\d{1,2}\\b))" M]
     "y" ["(\\d{1,4})" y]
     "yy" ["(\\d{2,4})" y]
     "yyyy" ["(\\d{4})" y]
     "Y" ["(\\d{1,4})" y]
     "YY" ["(\\d{2,4})" y]
     "YYYY" ["(\\d{4})" y]
     "MMM" [(str \( (string/join \| (map (partial abbreviate 3) months)) \)) MMM]
     "MMMM" [(str \( (string/join \| months) \)) MMMM]
     "E" [(str \( (string/join \| (map (partial abbreviate 3) days)) \)) skip]
     "EEE" [(str \( (string/join \| (map (partial abbreviate 3) days)) \)) skip]
     "EEEE" [(str \( (string/join \| days) \)) skip]
     "dow" [(str \( (string/join \| days) \)) skip]
     "a" ["(am|pm|a|p|AM|PM|A|P)" a]
     "A" ["(am|pm|a|p|AM|PM|A|P)" a]
     "m" ["(\\d{1,2})" m]
     "s" ["(\\d{1,2})" s]
     "S" ["(\\d{1,2})" S]
     "h" ["(\\d{1,2})" h]
     "H" ["(\\d{1,2})" H]
     "hh" ["(\\d{2})" h]
     "HH" ["(\\d{2})" H]
     "mm" ["(\\d{2})" m]
     "ss" ["(\\d{2})" s]
     "SSS" ["(\\d{3})" S]
     "Z" ["((?:(?:\\+|-)\\d{2}:?\\d{2})|Z+)" tz]
     "ZZ" ["((?:(?:\\+|-)\\d{2}:\\d{2})|Z+)" tz]}))

(def date-setters
  {:years #(.setYear %1 %2)
   :months #(.setMonth %1 %2)
   :days #(.setDate %1 %2)
   :hours #(.setHours %1 %2)
   :minutes #(.setMinutes %1 %2)
   :seconds #(.setSeconds %1 %2)
   :millis #(.setMilliseconds %1 %2)
   :time-zone timezone-adjustment})

(defn parser-sort-order-pred [parser]
  (index-of
    ["YYYY" "YY" "Y" "yyyy" "yy" "y" "d" "dd" "D" "DD" "DDD" "dth"
     "M" "MM" "MMM" "MMMM" "dow" "h" "H" "m" "s" "S" "hh" "HH" "mm" "ss" "a" "A"
     "SSS" "Z" "ZZ"]
    parser))

(def date-format-pattern
  (re-pattern
    (str "(" (string/join ")|(" (reverse (sort-by count (keys date-formatters)))) ")")))

(defn old-string-replace [s match replacement]
  (.replace s (js/RegExp. (.-source match) "g") replacement))

(defn date-parse-pattern [formatter]
  (-> formatter
      (old-string-replace #"'([^']+)'" "$1")
      (old-string-replace date-format-pattern #(first (date-parsers %)))
      re-pattern))

(defn- parser-fn [fmts]
  (fn [s]
    (->> (interleave (nfirst (re-seq (date-parse-pattern fmts) s))
                     (map first (re-seq date-format-pattern fmts)))
         (partition 2)
         (sort-by (comp parser-sort-order-pred second)))))

(defn- formatter-fn [fmts formatters]
  (fn [date & [formatter-overrides]]
    (let [a (atom {:c 0})]
      [(old-string-replace
        fmts
        #"'([^']+)'"
        (fn [x s]
          (if (and (seq s) (= \' (first x)) (= \' (last x)))
            (let [{:keys [c]} @a
                  k (str "&&&&" c)]
              (swap! a assoc-in [:replace k] (constantly s))
              (swap! a update-in [:c] inc)
              k)
            x)))
       (-> (.-source date-format-pattern)
           (cond->>
             (:replace @a)
             (str "(" (string/join ")|(" (keys (:replace @a))) ")|"))
           (re-pattern))
       #(((merge formatters formatter-overrides (:replace @a)) %) date)])))

(defn formatter
  ([fmts]
     (formatter fmts time/utc))
  ([fmts dtz]
     (with-meta
       {:format-str fmts
        :formatters date-formatters}
       {:type ::formatter})))

(defn formatter-local [fmts]
  (with-meta
    {:format-str fmts
     :formatters (assoc date-formatters
                   "Z" (constantly "")
                   "ZZ" (constantly ""))}
    {:type ::formatter}))

(defn not-implemented [sym]
  #(throw (clj->js {:name :not-implemented
                    :message (format "%s not implemented yet" (name sym))})))

(defn with-default-year
  "Return a copy of a formatter that uses the given default year."
  [f default-year]
  (assoc f :default-year default-year))

(def ^{:doc "Map of ISO 8601 and a single RFC 822 formatters that can be used
for parsing and, in most cases, printing.

Note: due to current implementation limitations, timezone information
cannot be kept. Although the correct offset will be applied to UTC
time if supplied."}
  formatters
    {:basic-date (formatter "yyyyMMdd")
     :basic-date-time (formatter "yyyyMMdd'T'HHmmss.SSSZ")
     :basic-date-time-no-ms (formatter "yyyyMMdd'T'HHmmssZ")
     :basic-ordinal-date (formatter "yyyyDDD")
     :basic-ordinal-date-time (formatter "yyyyDDD'T'HHmmss.SSSZ")
     :basic-ordinal-date-time-no-ms (formatter "yyyyDDD'T'HHmmssZ")
     :basic-time (formatter "HHmmss.SSSZ")
     :basic-time-no-ms (formatter "HHmmssZ")
     :basic-t-time (formatter "'T'HHmmss.SSSZ")
     :basic-t-time-no-ms (formatter "'T'HHmmssZ")
     :basic-week-date (formatter "xxxx'W'wwe")
     :basic-week-date-time (formatter "xxxx'W'wwe'T'HHmmss.SSSZ")
     :basic-week-date-time-no-ms (formatter "xxxx'W'wwe'T'HHmmssZ")
     :date (formatter "yyyy-MM-dd")
     :date-element-parser (not-implemented 'dateElementParser)
     :date-hour (formatter "yyyy-MM-dd'T'HH")
     :date-hour-minute (formatter "yyyy-MM-dd'T'HH:mm")
     :date-hour-minute-second (formatter "yyyy-MM-dd'T'HH:mm:ss")
     :date-hour-minute-second-fraction (formatter "yyyy-MM-dd'T'HH:mm:ss.SSS")
     :date-hour-minute-second-ms (formatter "yyyy-MM-dd'T'HH:mm:ss.SSS")
     :date-opt-time (not-implemented 'dateOptionalTimeParser)
     :date-parser (not-implemented 'dateParser)
     :date-time (formatter "yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
     :date-time-no-ms (formatter "yyyy-MM-dd'T'HH:mm:ssZZ")
     :date-time-parser (not-implemented 'dateTimeParser)
     :hour (formatter "HH")
     :hour-minute (formatter "HH:mm")
     :hour-minute-second (formatter "HH:mm:ss")
     :hour-minute-second-fraction (formatter "HH:mm:ss.SSS")
     :hour-minute-second-ms (formatter "HH:mm:ss.SSS")
     :local-date-opt-time (not-implemented 'localDateOptionalTimeParser)
     :local-date (not-implemented 'localDateParser)
     :local-time (not-implemented 'localTimeParser)
     :ordinal-date (formatter "yyyy-DDD")
     :ordinal-date-time (formatter "yyyy-DDD'T'HH:mm:ss.SSSZZ")
     :ordinal-date-time-no-ms (formatter "yyyy-DDD'T'HH:mm:ssZZ")
     :time (formatter "HH:mm:ss.SSSZZ")
     :time-element-parser (not-implemented 'timeElementParser)
     :time-no-ms (formatter "HH:mm:ssZZ")
     :time-parser (formatter 'timeParser)
     :t-time (formatter "'T'HH:mm:ss.SSSZZ")
     :t-time-no-ms (formatter "'T'HH:mm:ssZZ")
     :week-date (formatter "xxxx-'W'ww-e")
     :week-date-time (formatter "xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ")
     :week-date-time-no-ms (formatter "xxxx-'W'ww-e'T'HH:mm:ssZZ")
     :weekyear (formatter "xxxx")
     :weekyear-week (formatter "xxxx-'W'ww")
     :weekyear-week-day (formatter "xxxx-'W'ww-e")
     :year (formatter "yyyy")
     :year-month (formatter "yyyy-MM")
     :year-month-day (formatter "yyyy-MM-dd")
     :rfc822 (formatter "EEE, dd MMM yyyy HH:mm:ss Z")
     :mysql (formatter "yyyy-MM-dd HH:mm:ss")})

(def ^{:private true} parsers
  #{:date-element-parser :date-opt-time :date-parser :date-time-parser
    :local-date-opt-time :local-date :local-time :time-element-parser
    :time-parser})

(def ^{:private true} printers
  (difference (set (keys formatters)) parsers))

(def part-splitter-regex
  #"(?:(?!(?:\+|-)\d{2}):(?!\d{2}$))|[^\w:]+|.[TW]|'[^']+'")

(defprotocol IDateMap
  (date-map [date]))

(extend-protocol IDateMap
  goog.date.Date
  (date-map [date]
    {:years 0 :months 0 :days 1})

  goog.date.DateTime
  (date-map [date]
    {:years 0 :months 0 :days 1 :hours 0 :minutes 0 :seconds 0 :millis 0})

  goog.date.UtcDateTime
  (date-map [date]
    {:years 0 :months 0 :days 1 :hours 0 :minutes 0 :seconds 0 :millis 0
     :time-zone nil}))

(defn parse* [constructor {:keys [format-str default-year] :as fmt} s]
  {:pre [(seq s)]}
  (let [min-parts (count (string/split s part-splitter-regex))]
    (let [parse-fn (parser-fn format-str)
          parse-seq (seq (map (fn [[a b]] [a (second (date-parsers b))])
                              (parse-fn s)))]
      (if (>= (count parse-seq) min-parts)
        (let [d (new constructor 0 0 0 0 0 0 0)
              empty (assoc (date-map d) :years (or default-year 0))
              setters (select-keys date-setters (keys empty))]
          (->> parse-seq
               (reduce (fn [date [part do-parse]] (do-parse date part)) empty)
               valid-date?
               (merge-with #(%1 d %2) setters))
          d)
        (throw
         (ex-info "The parser could not match the input string."
                  {:type :parser-no-match}))))))

(defn parse
  "Returns a DateTime instance in the UTC time zone obtained by parsing the
  given string according to the given formatter."
  ([fmt s]
     (parse* goog.date.UtcDateTime fmt s))
  ([s]
     (first
      (for [f (vals formatters)
            :let [d (try (parse f s) (catch :default _))]
            :when d] d))))

(defn parse-local
  "Returns a local DateTime instance obtained by parsing the
  given string according to the given formatter."
  ([fmt s]
     (parse* goog.date.DateTime fmt s))
  ([s]
     (first
      (for [f (vals formatters)
            :let [d (try (parse-local f s) (catch js/Error _ nil))]
            :when d] d))))

(defn parse-local-date
  "Returns a local Date instance obtained by parsing the
  given string according to the given formatter."
  ([fmt s]
     (parse* goog.date.Date fmt s))
  ([s]
     (first
      (for [f (vals formatters)
            :let [d (try (parse-local-date f s) (catch js/Error _ nil))]
            :when d] d))))

(defn unparse
  "Returns a string representing the given DateTime instance in UTC and in the
form determined by the given formatter."
  [{:keys [format-str formatters]} dt]
  {:pre [(not (nil? dt)) (instance? goog.date.DateTime dt)]}
  (apply old-string-replace ((formatter-fn format-str formatters) dt)))

(defn unparse-local
  "Returns a string representing the given local DateTime instance in the
  form determined by the given formatter."
  [{:keys [format-str formatters] :as fmt} dt]
  {:pre [(not (nil? dt)) (instance? goog.date.DateTime dt)]}
  (apply old-string-replace
         ((formatter-fn format-str formatters) dt (assoc date-formatters
                                                    "Z" (constantly "")
                                                    "ZZ" (constantly "")))))

(defn unparse-local-date
  "Returns a string representing the given local Date instance in the form
  determined by the given formatter."
  [{:keys [format-str formatters] :as fmt} dt]
  {:pre [(not (nil? dt)) (instance? goog.date.Date dt)]}
  (apply old-string-replace
         ((formatter-fn format-str formatters) dt (assoc date-formatters
                                                    "Z" (constantly "")
                                                    "ZZ" (constantly "")))))

(defn show-formatters
  "Shows how a given DateTime, or by default the current time, would be
formatted with each of the available printing formatters."
  ([] (show-formatters (time/now)))
  ([dt]
     (doseq [p (sort printers)]
       (let [fmt (formatters p)]
         (print (format "%-40s%s\n" p (unparse fmt dt)))))))

(defprotocol Mappable
  (instant->map [instant] "Returns a map representation of the given instant.
                          It will contain the following keys: :years, :months,
                          :days, :hours, :minutes and :seconds."))

(defn unparse-duration
  "Accepts a Period or Interval and outputs an absolute duration time
  in form of \"1 day\", \"2 hours\", \"20 minutes\", \"2 days 1 hour
  15 minutes\" etc."
  [duration]
  (-> duration time/in-millis duration/format))

(defn- to-map [years months days hours minutes seconds millis]
  {:years years
   :months months
   :days days
   :hours hours
   :minutes minutes
   :seconds seconds
   :millis millis})

(extend-protocol Mappable
  goog.date.UtcDateTime
  (instant->map [dt]
    (to-map
      (.getYear dt)
      (inc (.getMonth dt))
      (.getDate dt)
      (.getHours dt)
      (.getMinutes dt)
      (.getSeconds dt)
      (.getMilliseconds dt)))

  cljs-time.core.Period
  (instant->map [m]
    (time/->period m))

  cljs-time.core.Interval
  (instant->map [m]
    (time/->period m))

  cljs.core/PersistentArrayMap
  (instant->map [m]
    (case (:type (meta m))
      :cljs-time.core/period m
      :cljs-time.core/interval (time/->period m))))
