(ns app.main.data.tokenscript
  (:require
   ["@penpot/tokenscript" :refer [BaseSymbolType
                                  ColorSymbol
                                  ListSymbol NumberSymbol
                                  NumberWithUnitSymbol
                                  ProcessorError
                                  processTokens
                                  TokenSymbol
                                  makeConfig]]
   [app.common.logging :as l]
   [app.common.time :as ct]
   [app.main.data.workspace.tokens.errors :as wte]))

(l/set-level! :debug)

;; Config ----------------------------------------------------------------------

(def config (makeConfig))

;; Class predicates ------------------------------------------------------------
;; Predicates to get information about the tokenscript interpreter symbol type
;; Or to determine the error

(defn tokenscript-symbol? [v]
  (instance? BaseSymbolType v))

(defn structured-token? [v]
  (instance? TokenSymbol v))

(defn structured-record-token? [^js v]
  (and (structured-token? v) (instance? js/Map (.-value v))))

(defn structured-array-token? [^js v]
  (and (structured-token? v) (instance? js/Array (.-value v))))

(defn number-with-unit-symbol? [v]
  (instance? NumberWithUnitSymbol v))

(defn number-symbol? [v]
  (instance? NumberSymbol v))

(defn list-symbol? [v]
  (instance? ListSymbol v))

(defn color-symbol? [v]
  (instance? ColorSymbol v))

(defn processor-error? [err]
  (instance? ProcessorError err))

;; Conversion Tools ------------------------------------------------------------
;; Helpers to convert tokenscript symbols to penpot accepted formats

(defn color-symbol->hex-string [^js v]
  (when (color-symbol? v)
    (.toString (.to v "hex"))))

(defn color-alpha [^js v]
  (if (.isHex v)
    1
    (or (.getAttribute v "alpha") 1)))

(defn color-symbol->penpot-color [^js v]
  {:color (color-symbol->hex-string v)
   :opacity (color-alpha v)})

(defn rem-number-with-unit? [v]
  (and (number-with-unit-symbol? v)
       (= (.-unit v) "rem")))

(defn percent-number-with-unit? [v]
  (and (number-with-unit-symbol? v)
       (= (.-unit v) "%")))

(defn rem->px [^js v]
  (* (.-value v) 16))

(declare tokenscript-symbols->penpot-unit)

(defn structured-token->penpot-map
  "Converts structured token (record or array) to penpot map format.
  Structured tokens are non-primitive token types like `typography` or `box-shadow`."
  [^js token-symbol]
  (if (instance? js/Array (.-value token-symbol))
    (mapv tokenscript-symbols->penpot-unit (.-value token-symbol))
    (let [entries (es6-iterator-seq (.entries (.-value token-symbol)))]
      (into {} (map (fn [[k v :as V]]
                      [(keyword k) (tokenscript-symbols->penpot-unit v)])
                    entries)))))

(defn tokenscript-symbols->penpot-unit [^js v]
  (cond
    (nil? v) nil
    (structured-token? v) (structured-token->penpot-map v)
    (list-symbol? v) (structured-token->penpot-map v)
    (color-symbol? v) (.-value (.to v "hex"))
    (rem-number-with-unit? v) (rem->px v)
    (percent-number-with-unit? v) (/ (.-value v) 100)
    :else (.-value v)))

;; Processors ------------------------------------------------------------------
;; The processor resolves tokens
;; resolved/error tokens get put back into a clojure structure directly during build time
;; For updating tokens we use the `TokenResolver` crud methods from the processing result
;; The `TokenResolver` has detailed information for each tokens dependency graph

(defn create-token-builder
  "Collects resolved tokens during build time into a clojure structure.
   Returns Tokenscript Symbols in `:resolved-value` key."
  [tokens]
  (let [output (volatile! tokens)

        ;; When a token is resolved (No parsing / reference errors) we assing `:resolved-value` for the original token
        on-resolve
        (fn [^js/String token-name ^js/Symbol resolved-symbol]
          (vswap! output assoc-in [token-name :resolved-value] resolved-symbol))

        ;; When a token contains any errors we assing `:errors` for the original token
        on-error
        (fn [^js/String token-name ^js/Error _error ^js/String _original-value]
          (let [value (get tokens token-name)
                default-error [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]]
            (vswap! output assoc-in [token-name :errors] default-error)))

        ;; Extract the atom value
        get-result
        (fn [] @output)]
    #js {:onResolve on-resolve
         :onError on-error
         :getResult get-result}))

(defn clj->token->tokenscript-token
  "Convert penpot token into a format that tokenscript can handle."
  [{:keys [type value]}]
  #js {"$type" (name type)
       "$value" (clj->js value)})

(defn clj-tokens->tokenscript-tokens
  "Convert penpot map of tokens into tokenscript map structure.
  tokenscript accepts a map of [token-name {\"$type\": string, \"$value\": any}]"
  [tokens]
  (let [token-map (js/Map.)]
    (doseq [[k token] tokens]
      (.set token-map k (clj->token->tokenscript-token token)))
    token-map))

(defn process-tokens
  "Builds tokens using `tokenscript`."
  [tokens]
  (let [input (clj-tokens->tokenscript-tokens tokens)
        result (processTokens input #js {:config config
                                         :builder (create-token-builder tokens)})]
    result))

(defn update-token
  [tokens token]
  (let [result (process-tokens tokens)
        resolver (.-resolver result)]
    (.updateToken resolver #js {:tokenPath (:name token)
                                :tokenData (clj->token->tokenscript-token token)})))

;; Main ------------------------------------------------------------------------

(defn resolve-tokens [tokens]
  (let [tpoint (ct/tpoint-ms)
        result (process-tokens tokens)
        elapsed (tpoint)]
    (l/dbg :hint "tokenscript/resolve-tokens" :elapsed elapsed)
    (.-output result)))
