(ns app.main.ui.workspace.tokens.token
  (:require
   [app.common.data :as d]
   [clojure.set :as set]
   [cuerdas.core :as str]))

(defn resolve-token-value [{:keys [value resolved-value] :as _token}]
  (or
   resolved-value
   (d/parse-double value)))

(defn attributes-map
  "Creats an attributes map using collection of `attributes` for `id`."
  [attributes id]
  (->> (map (fn [attr] {attr id}) attributes)
       (into {})))

(defn remove-attributes-for-token-id
  "Removes applied tokens with `token-id` for the given `attributes` set from `applied-tokens`."
  [attributes token-id applied-tokens]
  (let [attr? (set attributes)]
    (->> (remove (fn [[k v]]
                   (and (attr? k)
                        (= v token-id)))
                 applied-tokens)
        (into {}))))

(defn token-attribute-applied?
  "Test if `token` is applied to a `shape` on single `token-attribute`."
  [token shape token-attribute]
  (when-let [id (get-in shape [:applied-tokens token-attribute])]
    (= (:id token) id)))

(defn token-applied?
  "Test if `token` is applied to a `shape` with at least one of the one of the given `token-attributes`."
  [token shape token-attributes]
  (some #(token-attribute-applied? token shape %) token-attributes))

(defn token-applied-attributes
  "Return a set of which `token-attributes` are applied with `token`."
  [token shape token-attributes]
  (-> (filter #(token-attribute-applied? token shape %) token-attributes)
      (set)))

(defn shapes-token-applied?
  "Test if `token` is applied to to any of `shapes` with at least one of the one of the given `token-attributes`."
  [token shapes token-attributes]
  (some #(token-applied? token % token-attributes) shapes))

(defn shapes-token-applied-all?
  "Test if `token` is applied to to any of `shapes` with at least one of the one of the given `token-attributes`."
  [token shapes token-attributes]
  (some #(token-applied? token % token-attributes) shapes))

(defn shapes-ids-by-applied-attributes [token shapes token-attributes]
  (reduce (fn [acc shape]
            (let [applied-ids-by-attribute (->> (map #(when (token-attribute-applied? token shape %)
                                                        [% #{(:id shape)}])
                                                     token-attributes)
                                                (filter some?)
                                                (into {}))]
              (merge-with into acc applied-ids-by-attribute)))
          {} shapes))

(defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
  (every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))

(defn group-shapes-by-all-applied
  [token shapes token-attributes]
  (reduce
   (fn [acc cur-shape]
     (let [applied-attrs (token-applied-attributes token cur-shape token-attributes)]
       (cond
         (empty? applied-attrs) (update acc :none (fnil conj []) cur-shape)
         (= applied-attrs token-attributes) (update acc :all (fnil conj []) cur-shape)
         :else (reduce (fn [acc' cur']
                         (update-in acc' [:some cur'] (fnil conj []) cur-shape))
                       acc applied-attrs))))
   {} shapes))

(defn group-shapes-by-all-applied-all? [grouped-shapes]
  (and (seq (:all grouped-shapes))
       (empty? (:other grouped-shapes))
       (empty? (:some grouped-shapes))))

(defn token-name->path
  "Splits token-name into a path vector split by `.` characters.

  Will concatenate multiple `.` characters into one."
  [token-name]
  (str/split token-name #"\.+"))

(defn token-name->path-selector
  "Splits token-name into map with `:path` and `:selector` using `token-name->path`.

  `:selector` is the last item of the names path
  `:path` is everything leading up the the `:selector`."
  [token-name]
  (let [path-segments (token-name->path token-name)
        last-idx (dec (count path-segments))
        [path [selector]] (split-at last-idx path-segments)]
    {:path (seq path)
     :selector selector}))

(defn token-names-map
  "Convert tokens into a map with their `:name` as the key.

  E.g.: {\"sm\" {:token-type :border-radius :id #uuid \"000\" ...}}"
  [tokens]
  (->> (map (fn [{:keys [name] :as token}] [name token]) tokens)
       (into {})))

(defn token-names-tree
  "Convert tokens into a nested tree with their `:name` as the path."
  [tokens]
  (reduce
   (fn [acc [_ {:keys [name] :as token}]]
     (when (string? name)
       (let [path (token-name->path name)]
         (assoc-in acc path token))))
   {} tokens))

(defn token-name-path-exists?
  "Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.

  It's not allowed to create a token inside a token. E.g.:
  Creating a token with

    {:name \"foo.bar\"}

  in the tokens tree:

    {\"foo\" {:name \"other\"}}"
  [token-name token-names-tree]
  (let [{:keys [path selector]} (token-name->path-selector token-name)
        path-target (reduce
                     (fn [acc cur]
                       (let [target (get acc cur)]
                         (cond
                           ;; Path segment doesn't exist yet
                           (nil? target) (reduced false)
                           ;; A token exists at this path
                           (:name target) (reduced true)
                           ;; Continue traversing the true
                           :else target)))
                     token-names-tree path)]
    (cond
      (boolean? path-target) path-target
      (get path-target :name) true
      :else (-> (get path-target selector)
                (seq)
                (boolean)))))
