;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL

(ns app.main.ui.workspace.tokens.management.forms.controls.fonts-combobox
  (:require-macros [app.main.style :as stl])
  (:require
   [app.common.data :as d]
   [app.common.files.tokens :as cfo]
   [app.common.types.token :as cto]
   [app.common.types.tokens-lib :as ctob]
   [app.config :as cf]
   [app.main.data.style-dictionary :as sd]
   [app.main.data.tokenscript :as ts]
   [app.main.data.workspace.tokens.errors :as wte]
   [app.main.fonts :as fonts]
   [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
   [app.main.ui.ds.controls.input :refer [input*]]
   [app.main.ui.ds.foundations.assets.icon :as i]
   [app.main.ui.forms :as fc]
   [app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
   [app.main.ui.workspace.tokens.management.forms.controls.utils :as csu]
   [app.util.dom :as dom]
   [app.util.forms :as fm]
   [app.util.i18n :refer [tr]]
   [beicon.v2.core :as rx]
   [cuerdas.core :as str]
   [rumext.v2 :as mf]))

;; --- Font Picker Inputs -------------------------------------------------------
;;
;; We provide two versions of the font picker: the normal (primitive) input and
;; the composite input. Both allow selecting a font-family either by typing free
;; text or by choosing from the list of fonts loaded in the application. Comma-
;; separated values are treated as literal font-family lists.
;;
;; 1) Normal Font Picker (primitive)
;;    - Used when the token’s value *is itself* a font-family (string) or a
;;      reference to another token.
;;    - The input writes directly to `:value`.
;;    - Validation ensures the string is a valid font-family or a valid token
;;      reference.
;;
;; 2) Composite Font Picker
;;    - Used inside typography tokens, where `:value` is a map (e.g. contains
;;      :font-family, :font-weight, :letter-spacing, etc.).
;;    - The input writes to the specific value-subfield `[:value :font-family]`.
;;    - Only this field is validated and updated—other typography fields remain
;;      untouched.
;;
;; Both modes share the same UI behaviour, but differ in how they store and
;; validate data within the form state.

(defn- resolve-value
  [tokens prev-token _token-name value]
  (let [tmp-value (cto/split-font-family value)
        tmp-name  "__PENPOT__FONT_FAMILY__PLACEHOLDER__"

        ;; Create a temporary font-family token to validate the value
        token
        {:name tmp-name
         :type :font-family
         :value (if (= (:type prev-token) :typography)
                  (assoc (:value prev-token) :font-family tmp-value)
                  tmp-value)}

        tokens
        (update tokens (:name token) #(ctob/make-token (merge % prev-token token)))]
    ;; TODO: Review this when tokenscript is fully integrated.
    (if (cfo/token-circular-reference? tokens (:name token))
      (rx/of {:error (wte/error-with-value :error.token/circular-reference nil)})
      (->> (if (contains? cf/flags :tokenscript)
             (rx/of (ts/resolve-tokens tokens))
             (sd/resolve-tokens-interactive tokens))
           (rx/mapcat
            (fn [resolved-tokens]
              (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
                    resolved-value (if (contains? cf/flags :tokenscript)
                                     (ts/tokenscript-symbols->penpot-unit resolved-value)
                                     resolved-value)]
                (if resolved-value
                  (rx/of {:value resolved-value})
                  (rx/of {:error (if errors
                                   (first errors)
                                   (wte/error-with-value :error/unknown value))})))))))))

(mf/defc fonts-combobox*
  [{:keys [token tokens name] :rest props}]
  (let [form       (mf/use-ctx fc/context)
        input-name name
        token-name (get-in @form [:data :name] nil)

        touched?
        (and (contains? (:data @form) input-name)
             (get-in @form [:touched input-name]))

        error
        (get-in @form [:errors input-name])

        value
        (get-in @form [:data input-name] "")

        font (fonts/find-font-family value)

        resolve-stream
        (mf/with-memo [token]
          (if-let [value (:value token)]
            (rx/behavior-subject value)
            (rx/subject)))

        hint*
        (mf/use-state {})

        hint
        (deref hint*)

        font-selector-open* (mf/use-state false)
        font-selector-open? (deref font-selector-open*)

        on-click-dropdown-button
        (mf/use-fn
         (mf/deps font-selector-open?)
         (fn [e]
           (dom/prevent-default e)
           (reset! font-selector-open* (not font-selector-open?))))

        font-selector-button
        (mf/html
         [:> icon-button*
          {:on-click on-click-dropdown-button
           :aria-label (tr "workspace.tokens.token-font-family-select")
           :icon i/arrow-down
           :variant "action"
           :type "button"}])

        on-close-font-selector
        (mf/use-fn
         (fn []
           (reset! font-selector-open* false)))

        on-select-font
        (mf/use-fn
         (mf/deps font)
         (fn [{:keys [family] :as font}]
           (when (not= value family)
             (fm/on-input-change form input-name family true)
             (rx/push! resolve-stream family))))

        on-change
        (mf/use-fn
         (mf/deps resolve-stream input-name)
         (fn [event]
           (let [value (-> event dom/get-target dom/get-input-value)]
             (fm/on-input-change form input-name value false)
             (rx/push! resolve-stream value))))

        props
        (mf/spread-props props   {:on-change on-change
                                  :value (or value "")
                                  :hint-message (:message hint)
                                  :slot-end font-selector-button
                                  :variant "comfortable"
                                  :hint-type (:type hint)})

        props
        (if (and error touched?)
          (mf/spread-props props {:hint-type "error"
                                  :hint-message (:message error)})
          props)]

    (mf/with-effect [resolve-stream tokens token input-name token-name]
      (let [subs (->> resolve-stream
                      (rx/debounce 300)
                      (rx/mapcat (partial resolve-value tokens token token-name))
                      (rx/map (fn [result]
                                (d/update-when result :error wte/resolve-error-assoc-message)))
                      (rx/subs! (fn [{:keys [error value]}]
                                  (let [touched? (get-in @form [:touched input-name])]
                                    (when touched?
                                      (if error
                                        (let [error' (:message error)]
                                          (if (csu/group-name-conflict-error? error' token-name)
                                            (swap! form assoc-in [:extra-errors ""] error')
                                            (do
                                              (swap! form assoc-in [:extra-errors input-name] error')
                                              (reset! hint* {:message error' :type "error"}))))
                                        (let [message (tr "workspace.tokens.resolved-value" value)]
                                          (swap! form update :extra-errors dissoc input-name)
                                          (reset! hint* {:message message :type "hint"}))))))))]
        (fn []
          (rx/dispose! subs))))

    [:*
     [:> input* props]
     (when font-selector-open?
       [:div {:class (stl/css :font-select-wrapper)}
        [:> font-selector* {:current-font font
                            :on-select on-select-font
                            :on-close on-close-font-selector}]])]))

(defn- on-composite-combobox-token-change
  ([form field value]
   (on-composite-combobox-token-change form field value false))
  ([form field value trim?]
   (letfn [(clean-errors [errors]
             (-> errors
                 (dissoc field)
                 (not-empty)))]
     (swap! form (fn [state]
                   (-> state
                       (assoc-in [:data :value field] (if trim? (str/trim value) value))
                       (update :errors clean-errors)
                       (update :extra-errors clean-errors)
                       (update :extra-errors dissoc "")))))))

(mf/defc composite-fonts-combobox*
  [{:keys [token tokens name] :rest props}]
  (let [form       (mf/use-ctx fc/context)
        input-name name
        token-name (get-in @form [:data :name] nil)
        error
        (get-in @form [:errors :value input-name])

        value
        (get-in @form [:data :value input-name] "")

        font (fonts/find-font-family value)

        resolve-stream
        (mf/with-memo [token]
          (if-let [value (get-in token [:value input-name])]
            (rx/behavior-subject value)
            (rx/subject)))


        hint*
        (mf/use-state {})

        hint
        (deref hint*)

        font-selector-open* (mf/use-state false)
        font-selector-open? (deref font-selector-open*)

        on-click-dropdown-button
        (mf/use-fn
         (mf/deps font-selector-open?)
         (fn [e]
           (dom/prevent-default e)
           (reset! font-selector-open* (not font-selector-open?))))

        font-selector-button
        (mf/html
         [:> icon-button*
          {:on-click on-click-dropdown-button
           :aria-label (tr "workspace.tokens.token-font-family-select")
           :icon i/arrow-down
           :variant "action"
           :type "button"}])

        on-close-font-selector
        (mf/use-fn
         (fn []
           (reset! font-selector-open* false)))

        on-select-font
        (mf/use-fn
         (mf/deps font)
         (fn [{:keys [family] :as font}]
           (when (not= value family)
             (on-composite-combobox-token-change form input-name family true)
             (rx/push! resolve-stream family))))

        on-change
        (mf/use-fn
         (mf/deps resolve-stream input-name)
         (fn [event]
           (let [value (-> event dom/get-target dom/get-input-value)]
             (on-composite-combobox-token-change form input-name value false)
             (rx/push! resolve-stream value))))

        props
        (mf/spread-props props   {:on-change on-change
                                  :value (or value "")
                                  :hint-message (:message hint)
                                  :slot-end font-selector-button
                                  :variant "comfortable"
                                  :hint-type (:type hint)})

        props
        (if error
          (mf/spread-props props {:hint-type "error"
                                  :hint-message (:message error)})
          props)]

    (mf/with-effect [resolve-stream tokens token input-name token-name]
      (let [subs (->> resolve-stream
                      (rx/debounce 300)
                      (rx/mapcat (partial resolve-value tokens token token-name))
                      (rx/map (fn [result]
                                (d/update-when result :error wte/resolve-error-message)))
                      (rx/subs!
                       (fn [{:keys [error value]}]
                         (cond
                           (and error (str/empty? (:error/value error)))
                           (do
                             (swap! form update-in [:errors :value] dissoc input-name)
                             (swap! form update-in [:data :value] dissoc input-name)
                             (swap! form update :extra-errors dissoc :value)
                             (reset! hint* {}))


                           (some? error)
                           (let [error' (:message error)]
                             (if (csu/group-name-conflict-error? error' token-name)
                               (swap! form assoc-in [:extra-errors ""] {:message error'})
                               (do
                                 (swap! form assoc-in  [:extra-errors :value input-name] {:message error'})
                                 (reset! hint* {:message error' :type "error"}))))

                           :else
                           (let [message (tr "workspace.tokens.resolved-value" value)
                                 input-value (get-in @form [:data :value input-name] "")]
                             (swap! form update :errors dissoc :value)
                             (swap! form update :extra-errors dissoc :value)
                             (if (or (empty? value) (= input-value value))
                               (reset! hint* {})
                               (reset! hint* {:message message :type "hint"})))))))]

        (fn []
          (rx/dispose! subs))))

    [:*
     [:> input* props]
     (when font-selector-open?
       [:div {:class (stl/css :font-select-wrapper-composite)}
        [:> font-selector* {:current-font font
                            :on-select on-select-font
                            :on-close on-close-font-selector}]])]))