;; 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.colorpicker
  (:require-macros [app.main.style :as stl])
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.geom.matrix :as gmt]
   [app.common.geom.point :as gpt]
   [app.common.types.color :as cc]
   [app.common.types.fills :as types.fills]
   [app.common.types.tokens-lib :as ctob]
   [app.config :as cfg]
   [app.main.data.event :as ev]
   [app.main.data.modal :as modal]
   [app.main.data.shortcuts :as dsc]
   [app.main.data.workspace.colors :as dc]
   [app.main.data.workspace.libraries :as dwl]
   [app.main.data.workspace.media :as dwm]
   [app.main.data.workspace.undo :as dwu]
   [app.main.features :as features]
   [app.main.refs :as refs]
   [app.main.store :as st]
   [app.main.ui.components.file-uploader :refer [file-uploader]]
   [app.main.ui.components.radio-buttons :refer [radio-buttons radio-button]]
   [app.main.ui.components.select :refer [select]]
   [app.main.ui.ds.buttons.button :refer [button*]]
   [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
   [app.main.ui.ds.foundations.assets.icon :as i]
   [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
   [app.main.ui.hooks :as hooks]
   [app.main.ui.icons :as deprecated-icon]
   [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs*]]
   [app.main.ui.workspace.colorpicker.color-tokens :refer [token-section*]]
   [app.main.ui.workspace.colorpicker.gradients :refer [gradients*]]
   [app.main.ui.workspace.colorpicker.harmony :refer [harmony-selector*]]
   [app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector*]]
   [app.main.ui.workspace.colorpicker.libraries :refer [libraries*]]
   [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
   [app.main.ui.workspace.colorpicker.shortcuts :as sc]
   [app.util.dom :as dom]
   [app.util.i18n :as i18n :refer [tr]]
   [app.util.timers :as ts]
   [cuerdas.core :as str]
   [okulary.core :as l]
   [rumext.v2 :as mf]
   [rumext.v2.util :as mfu]))

;; --- Refs

(def picking-color?
  (l/derived :picking-color? refs/workspace-global))

(def picked-color
  (l/derived :picked-color refs/workspace-global))

(def picked-color-select
  (l/derived :picked-color-select refs/workspace-global))

(def viewport
  (l/derived :vport refs/workspace-local))

(defn opacity->string
  [opacity]
  (if (not= opacity :multiple)
    (dm/str (-> opacity
                (d/coalesce 1)
                (* 100)))
    :multiple))

;; --- Color Picker Modal

(defn use-color-picker-css-variables! [node-ref current-color]
  (mf/with-effect [current-color]
    (let [node (mf/ref-val node-ref)
          {:keys [r g b h v]} current-color
          rgb [r g b]
          hue-rgb (cc/hsv->rgb [h 1.0 255])
          hsl-from (cc/hsv->hsl [h 0.0 v])
          hsl-to (cc/hsv->hsl [h 1.0 v])

          ;; HSL-mode gradients. For S: fix current lightness, sweep
          ;; saturation 0 → 1. For L: fix current saturation, sweep
          ;; lightness 0 → 0.5 (pure hue) → 1. All computed at the
          ;; current hue.
          [_ cur-hsl-s cur-hsl-l] (cc/rgb->hsl rgb)
          hsl-sat-from  [h 0.0 cur-hsl-l]
          hsl-sat-to    [h 1.0 cur-hsl-l]
          lightness-mid [h cur-hsl-s 0.5]

          format-hsl (fn [[h s l]]
                       (str/fmt "hsl(%s, %s, %s)"
                                h
                                (str (* s 100) "%")
                                (str (* l 100) "%")))]
      (dom/set-css-property! node "--color" (str/join ", " rgb))
      (dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb))
      (dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from))
      (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))
      (dom/set-css-property! node "--hsl-saturation-grad-from" (format-hsl hsl-sat-from))
      (dom/set-css-property! node "--hsl-saturation-grad-to" (format-hsl hsl-sat-to))
      (dom/set-css-property! node "--lightness-grad-mid" (format-hsl lightness-mid)))))

(mf/defc colorpicker*
  [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change tab applied-token]}]
  (let [state                  (mf/deref refs/colorpicker)
        node-ref               (mf/use-ref)

        should-update?         (mf/use-var true)
        token-color            (contains? cfg/flags :token-color)
        color-style*           (mf/use-state (d/nilv tab :direct-color))
        color-style            (deref color-style*)
        toggle-token-color
        (mf/use-fn
         (mf/deps color-style)
         (fn []
           (let [new-style (if (= :direct-color color-style) :token-color :direct-color)]
             (reset! color-style* new-style))))

        ;; TODO: I think we need to put all this picking state under
        ;; the same object for avoid creating adhoc refs for each
        ;; value
        picking-color?         (mf/deref picking-color?)
        picked-color           (mf/deref picked-color)
        picked-color-select    (mf/deref picked-color-select)

        current-color          (:current-color state)

        active-fill-tab        (if (:image data)
                                 :image
                                 (if-let [gradient (:gradient data)]
                                   (case (:type gradient)
                                     :linear :linear-gradient
                                     :radial :radial-gradient)
                                   :color))

        active-color-tab*      (hooks/use-persisted-state ::color-tab "ramp")
        active-color-tab       (deref active-color-tab*)

        ;; Inline HSB/HSL toggle inside the HSBA tab — shared between
        ;; the slider selector (for labels) and the numeric inputs.
        hsb-mode*              (hooks/use-persisted-state ::hsb-mode :hsb)
        hsb-mode               (deref hsb-mode*)

        drag?*                 (mf/use-state false)
        drag?                  (deref drag?*)

        type                   (if (= active-color-tab "hsva") :hsb :rgb)

        fill-image-ref         (mf/use-ref nil)

        color-type             (get state :type :color)
        selected-mode          (case color-type
                                 (:linear-gradient :radial-gradient)
                                 :gradient

                                 color-type)

        disabled-color-accept? (and
                                (= selected-mode :image)
                                (not (:image current-color)))

        on-fill-image-success
        (mf/use-fn
         (fn [image]
           ;; FIXME: revisit
           (st/emit! (dc/update-colorpicker-color
                      {:image (-> (select-keys image [:id :width :height :mtype :name])
                                  (assoc :keep-aspect-ratio true))}
                      (not drag?)))))

        on-fill-image-click
        (mf/use-fn #(dom/click (mf/ref-val fill-image-ref)))

        on-fill-image-selected
        (mf/use-fn
         (fn [file]
           (st/emit! (dwm/upload-fill-image file on-fill-image-success))))

        handle-change-keep-aspect-ratio
        (mf/use-fn
         (mf/deps current-color)
         (fn []
           (let [keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio not)
                 image              (-> (:image current-color)
                                        (assoc :keep-aspect-ratio keep-aspect-ratio?))]

             (st/emit!
              (dc/update-colorpicker-color {:image image} true)
              (ev/event {::ev/name "toggle-image-aspect-ratio"
                         ::ev/origin "workspace:colorpicker"
                         :checked keep-aspect-ratio?})))))

        on-change-tab
        (mf/use-fn #(reset! active-color-tab* %))

        handle-change-mode
        (mf/use-fn
         (fn [value]
           (case value
             :color (st/emit! (dc/activate-colorpicker-color))
             :gradient (st/emit! (dc/activate-colorpicker-gradient :linear-gradient))
             :image (st/emit! (dc/activate-colorpicker-image))
             nil)))

        handle-change-color
        (mf/use-fn
         (mf/deps current-color drag?)
         (fn [color]
           (let [color (merge current-color color)
                 color (dc/materialize-color-components color)]
             (st/emit! (dc/update-colorpicker-color color (not drag?))))))

        handle-click-picker
        (mf/use-fn
         (mf/deps picking-color?)
         (fn []
           (if picking-color?
             (do (modal/disallow-click-outside!)
                 (st/emit! (dc/stop-picker)))
             (do (modal/allow-click-outside!)
                 (st/emit! (dc/start-picker))))))

        on-select-library-color
        (mf/use-fn
         (mf/deps data handle-change-color)
         (fn [_ color]
           (if (and (some? (:color color)) (some? (:gradient data)))
             (handle-change-color {:hex (:color color) :alpha (:opacity color)})
             (let [color (d/without-qualified color)]
               (st/emit! (dc/add-recent-color color)
                         (dc/apply-color-from-colorpicker color))
               (on-change color)))))

        on-add-library-color
        (mf/use-fn
         (mf/deps state)
         (fn [_]
           (st/emit! (dwl/add-color (dc/get-color-from-colorpicker-state state)))))

        on-start-drag
        (mf/use-fn
         (mf/deps drag?* node-ref)
         (fn []
           (reset! should-update? false)
           (reset! drag?* true)
           (st/emit! (dwu/start-undo-transaction (mf/ref-val node-ref)))))

        on-finish-drag
        (mf/use-fn
         (mf/deps drag?* node-ref)
         (fn []
           (reset! should-update? true)
           (reset! drag?* false)
           (st/emit! (dwu/commit-undo-transaction (mf/ref-val node-ref)))))

        on-color-accept
        (mf/use-fn
         (mf/deps state)
         (fn []
           (on-accept (dc/get-color-from-colorpicker-state state))
           (modal/hide!)))

        options
        (mf/with-memo [selected-mode disable-gradient disable-image]
          (d/concat-vec
           [{:value :color :label (tr "media.solid")}]
           (when (not disable-gradient)
             [{:value :gradient :label (tr "media.gradient")}])
           (when (not disable-image)
             [{:value :image :label (tr "media.image")}])))

        handle-change-gradient-selected-stop
        (mf/use-fn
         (fn [index]
           (st/emit! (dc/select-colorpicker-gradient-stop index))))

        handle-change-gradient-type
        (mf/use-fn
         (fn [type]
           (st/emit! (dc/activate-colorpicker-gradient type))))

        handle-gradient-change-stop
        (mf/use-fn
         (mf/deps state)
         (fn [prev-stop new-stop]
           (let [stops (->> (:stops state)
                            (mapv #(if (= % prev-stop) new-stop %)))]
             (st/emit! (dc/update-colorpicker-stops stops)))))

        handle-gradient-add-stop-auto
        (mf/use-fn
         (fn []
           (st/emit! (dc/update-colorpicker-add-auto))))

        handle-gradient-add-stop-preview
        (mf/use-fn
         (fn [offset]
           (st/emit! (dc/update-colorpicker-add-stop offset))))

        handle-gradient-remove-stop
        (mf/use-fn
         (fn [index]
           (st/emit! (dc/remove-gradient-stop index))))

        handle-stop-edit-start
        (mf/use-fn
         #(reset! should-update? false))

        handle-stop-edit-finish
        (mf/use-fn
         (fn []
           (reset! should-update? true)

           ;; Update asynchronously so we can update with the new stops
           (ts/schedule #(st/emit! (dc/sort-colorpicker-stops)))))

        handle-rotate-stops
        (mf/use-fn
         (mf/deps state)
         (fn []
           (let [gradient (:gradient state)
                 mtx (gmt/rotate-matrix 90 (gpt/point 0.5 0.5))

                 start-p (-> (gpt/point (:start-x gradient) (:start-y gradient))
                             (gpt/transform mtx))

                 end-p (-> (gpt/point (:end-x gradient) (:end-y gradient))
                           (gpt/transform mtx))]
             (st/emit! (dc/update-colorpicker-gradient {:start-x (:x start-p)
                                                        :start-y (:y start-p)
                                                        :end-x (:x end-p)
                                                        :end-y (:y end-p)})))))

        handle-reverse-stops
        (mf/use-fn
         (mf/deps (:stops state))
         (fn []
           (let [stops (->> (:stops state)
                            (map (fn [it] (update it :offset #(+ 1 (* -1 %)))))
                            (sort-by :offset)
                            (into []))]
             (st/emit! (dc/update-colorpicker-stops stops)))))

        handle-reorder-stops
        (mf/use-fn
         (mf/deps (:stops state))
         (fn [from-index to-index]
           (let [stops (:stops state)
                 new-stops
                 (-> stops
                     (d/insert-at-index to-index [(get stops from-index)]))
                 stops
                 (mapv #(assoc %2 :offset (:offset %1)) stops new-stops)]
             (st/emit! (dc/update-colorpicker-stops stops)))))

        render-wasm?
        (features/use-feature "render-wasm/v1")

        tabs
        (mf/with-memo []
          [{:aria-label (tr "workspace.libraries.colors.rgba")
            :icon i/rgba
            :id "ramp"}
           {:aria-label "Harmony"
            :icon i/rgba-complementary
            :id "harmony"}
           {:aria-label "HSBA"
            :icon i/hsva
            :id "hsva"}])

        show-tokens? (contains? #{:fill :stroke-color :color-selection} color-origin)]

    ;; Initialize colorpicker state
    (mf/with-effect []
      (st/emit! (dc/initialize-colorpicker on-change active-fill-tab))
      ;; Always deactivate picking mode on unmount so that :picking-color? never
      ;; stays true if the modal closes for any reason other than the normal
      ;; pointer-up path (e.g. ESC, navigation, programmatic hide).
      (fn []
        (st/emit! (dc/stop-picker)
                  (dc/finalize-colorpicker))))

    ;; Update colorpicker with external color changes
    (mf/with-effect [data]
      (when @should-update?
        (st/emit! (dc/update-colorpicker data))))

    ;; Updates the CSS color variable when there is a change in the color
    (use-color-picker-css-variables! node-ref current-color)

    ;; Updates color when pixel picker is used
    (mf/with-effect [picking-color? picked-color picked-color-select]
      (when (and picking-color? picked-color picked-color-select)
        (let [[r g b alpha] picked-color
              hex (cc/rgb->hex [r g b])
              [h s v] (cc/hex->hsv hex)]
          (handle-change-color {:hex hex
                                :r r :g g :b b
                                :h h :s s :v v
                                :alpha (/ alpha 255)}))))

    [:*
     [:div {:class (stl/css :colorpicker)
            :ref node-ref
            :style {:touch-action "none"}}
      [:div {:class (stl/css :top-actions)}

       [:div {:class (stl/css :top-actions-right)}

        (when (and (= color-style :direct-color)
                   (or (not disable-gradient) (not disable-image)))
          [:div {:class (stl/css :select)}
           [:& select
            {:default-value selected-mode
             :options options
             :on-change handle-change-mode}]])

        (when (and (= origin :sidebar) show-tokens? token-color)
          [:& radio-buttons {:selected color-style
                             :on-change toggle-token-color
                             :name "color-style"}
           [:& radio-button {:icon i/swatches
                             :value :direct-color
                             :title (tr "labels.color")
                             :id "opt-color"}]
           [:& radio-button {:icon i/tokens
                             :value :token-color
                             :title (tr "workspace.colorpicker.color-tokens")
                             :id "opt-token-color"}]])]

       (when (and (not= selected-mode :image)
                  (= color-style :direct-color))
         [:> icon-button* {:icon i/picker
                           :variant "ghost"
                           :aria-label (tr "workspace.colorpicker.color-picker")
                           :aria-pressed picking-color?
                           :class (stl/css :picker-btn)
                           :on-click handle-click-picker}])

       (when (= color-style :token-color)
         [:div {:class (stl/css :token-color-title)}
          (tr "workspace.colorpicker.color-tokens")])]

      (if (= color-style :direct-color)
        [:*
         (when (= selected-mode :gradient)
           [:> gradients*
            {:type (:type state)
             :stops (if render-wasm? (vec (take types.fills/MAX-GRADIENT-STOPS (:stops state))) (:stops state))
             :editing-stop (:editing-stop state)
             :on-stop-edit-start handle-stop-edit-start
             :on-stop-edit-finish handle-stop-edit-finish
             :on-select-stop handle-change-gradient-selected-stop
             :on-change-type handle-change-gradient-type
             :on-change-stop handle-gradient-change-stop
             :on-add-stop-auto handle-gradient-add-stop-auto
             :on-add-stop-preview handle-gradient-add-stop-preview
             :on-remove-stop handle-gradient-remove-stop
             :on-rotate-stops handle-rotate-stops
             :on-reverse-stops handle-reverse-stops
             :on-reorder-stops handle-reorder-stops}])

         (if (= selected-mode :image)
           (let [uri (cfg/resolve-file-media (:image current-color))
                 keep-aspect-ratio? (-> current-color :image :keep-aspect-ratio)]
             [:div {:class (stl/css :select-image)}
              [:div {:class (stl/css :content)}
               (when (:image current-color)
                 [:img {:src uri
                        :class (stl/css :content-image)}])]

              (when (some? (:image current-color))
                [:div {:class (stl/css :checkbox-option)}
                 [:label {:for "keep-aspect-ratio"
                          :class (stl/css-case  :global/checked keep-aspect-ratio?)}
                  [:span {:class (stl/css-case :global/checked keep-aspect-ratio?)}
                   (when keep-aspect-ratio?
                     deprecated-icon/status-tick)]
                  (tr "media.keep-aspect-ratio")
                  [:input {:type "checkbox"
                           :id "keep-aspect-ratio"
                           :checked keep-aspect-ratio?
                           :on-change handle-change-keep-aspect-ratio}]]])

              [:> button* {:class (stl/css :choose-image)
                           :variant "secondary"
                           :on-click on-fill-image-click}
               (tr "media.choose-image")
               [:& file-uploader
                {:input-id "fill-image-upload"
                 :accept dwm/accept-image-types
                 :multi false
                 :ref fill-image-ref
                 :on-selected on-fill-image-selected}]]])

           [:*
            [:div {:class (stl/css :colorpicker-tabs)}
             [:> tab-switcher* {:tabs tabs
                                :selected active-color-tab
                                :on-change on-change-tab}
              (if picking-color?
                [:div {:class (stl/css :picker-detail-wrapper)}
                 [:div {:class (stl/css :center-circle)}]
                 [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]]


                (case active-color-tab
                  "ramp"
                  [:> ramp-selector*
                   {:color current-color
                    :disable-opacity disable-opacity
                    :on-change handle-change-color
                    :on-start-drag on-start-drag
                    :on-finish-drag on-finish-drag}]

                  "harmony"
                  [:> harmony-selector*
                   {:color current-color
                    :disable-opacity disable-opacity
                    :on-change handle-change-color
                    :on-start-drag on-start-drag
                    :on-finish-drag on-finish-drag}]

                  "hsva"
                  [:> hsva-selector*
                   {:color current-color
                    :disable-opacity disable-opacity
                    :mode hsb-mode
                    :on-change handle-change-color
                    :on-start-drag on-start-drag
                    :on-finish-drag on-finish-drag}]))]]

            [:> color-inputs*
             {:type type
              :disable-opacity disable-opacity
              :mode hsb-mode
              :on-mode-change #(reset! hsb-mode* %)
              :color current-color
              :on-change handle-change-color}]

            [:> libraries*
             {:state state
              :current-color current-color
              :disable-gradient disable-gradient
              :disable-opacity disable-opacity
              :disable-image disable-image
              :on-select-color on-select-library-color
              :on-add-library-color on-add-library-color}]])]

        [:> token-section* {:combined-tokens combined-tokens
                            :on-token-change on-token-change
                            :applied-token applied-token
                            :color-origin color-origin}])]
     (when (fn? on-accept)
       [:div {:class (stl/css :actions)}
        [:> button* {:class (stl/css :accept-color)
                     :variant "primary"
                     :on-click on-color-accept
                     :disabled disabled-color-accept?}
         (tr "workspace.libraries.colors.save-color")]])]))

(defn calculate-position
  "Calculates the style properties for the given coordinates and position"
  [{vh :height} position x y gradient?]
  (let [;; picker size in pixels
        h (if gradient? 820 510)
        w 284
        ;; Checks for overflow outside the viewport height
        max-y   (- vh h)
        rulers? (mf/deref refs/rulers?)
        left-offset (if rulers? 40 18)
        right-offset (+ w 40)
        top-offset (dm/str (- y 70) "px")
        bottom-offset "1rem"
        max-height-top (str "calc(100vh - " top-offset)
        max-height-bottom (str "calc(100vh -" bottom-offset)]
    (cond
      (or (nil? x) (nil? y))
      #js {:left "auto" :right "16rem" :top "4rem" :maxHeight "calc(100vh - 4rem)"}

      (= position :left)
      (if (> y max-y)
        #js {:left (dm/str (- x right-offset) "px")
             :bottom bottom-offset
             :maxHeight max-height-bottom}
        #js {:left (dm/str (- x right-offset) "px")
             :top top-offset
             :maxHeight max-height-top})

      (= position :right)
      (if (> y max-y)
        #js {:left (dm/str (+ x 80) "px")
             :bottom bottom-offset
             :maxHeight max-height-bottom}
        #js {:left (dm/str (+ x 80) "px")
             :top top-offset
             :maxHeight max-height-top})

      :else
      (if (> y max-y)
        #js {:left (dm/str (+ x left-offset) "px")
             :bottom bottom-offset
             :maxHeight max-height-bottom}
        #js {:left (dm/str (+ x left-offset) "px")
             :top top-offset
             :maxHeight max-height-top}))))

(defn group-sets
  "Groups sets by their parent path (everything before the last '/') if present.
   The set name is always the last part of the path.

   Input:
   [{:set \"brand/subgroup/one\" :tokens [{:name \"background\"}]}
    {:set \"brand/subgroup/two\" :tokens [{:name \"foreground\"}]}
    {:set \"primitives\" :tokens [{:name \"blue-100\"}]}]

   Output:
   [{:group \"brand/subgroup\"
     :sets  [\"one\" \"two\"]
     :tokens [\"background\" \"foreground\"]}
    {:group nil
     :sets  [\"primitives\"]
     :tokens [\"blue-100\"]}]"

  [sets]
  (->> sets
       (group-by (fn [{:keys [set]}]
                   (when (str/includes? set "/")
                     (str/join "/" (butlast (str/split set #"/"))))))
       (map (fn [[group grouped-sets]]
              (if group
                {:group group
                 :sets  (map (fn [{:keys [id set]}]
                               {:id id
                                :name (last (str/split set #"/"))})
                             grouped-sets)
                 :tokens (->> grouped-sets
                              (mapcat :tokens)
                              (map :name)
                              distinct)}
                (map (fn [{:keys [id set tokens]}]
                       {:group nil
                        :sets [{:id id :name set}]
                        :tokens (map :name tokens)})
                     grouped-sets))))
       flatten))

(defn- combine-groups-with-resolved
  "Replaces token names in grouped sets with their full resolved token objects.

     Input:
     - groups: [{:group \"brand\"
                 :sets [\"light\" \"dark\"]
                 :tokens [\"background\" \"foreground\"]} ...]
     - resolved-tokens: [{:name \"background\" :type \"color\" :value \"{red-100}\" ...} ...]

     Output:
     [{:group \"brand\"
       :sets [\"light\" \"dark\"]
       :tokens [{:name \"background\" :type \"color\" :value \"{red-100}\" ...}
                {:name \"foreground\" :type \"color\" :value \"{green-100}\" ...}]}]"

  [groups resolved-tokens]
  (let [token-map (into {} (map (juxt :name identity) resolved-tokens))]
    (map (fn [{:keys [group sets tokens]}]
           {:group group
            :sets  sets
            :tokens (->> tokens
                         (map #(get token-map %))
                         (remove #(or (nil? %)
                                      (:errors %)
                                      (nil? (:resolved-value %))))
                         vec)})
         groups)))

(defn- filter-non-empty-sets
  "Removes sets that have no tokens.

   Input:
   [{:set \"brand/light\" :tokens []}
    {:set \"brand/dark\"  :tokens [{:name \"background\"}]}]

   Output:
   [{:set \"brand/dark\" :tokens [{:name \"background\"}]}]"
  [sets]
  (filter (fn [{:keys [tokens]}]
            (some #(= (:type %) :color) tokens))
          sets))

(defn- add-tokens-to-sets
  "Extracts set name and its tokens from raw set objects.

     Input:
     A vector of set objects (raw domain type), each compatible with:
     {:id ... :name \"brand/light\" :tokens {...}}

     Output:
     A vector of simplified maps:
     [{:set \"brand/light\" :tokens [{:name \"background\" ...} ...]}]"
  [sets]
  (map (fn [s]
         {:set    (ctob/get-name s)
          :id     (ctob/get-id s)
          :tokens (vals (ctob/get-tokens- s))})  ;; TODO: this function should be moved to common.logic and refactored
       sets))

(defn- filter-active-sets
  "Filters sets to only include those whose :set value is in active-set-names.

     Input:
     - sets: [{:set \"brand/light\" :tokens [...]},
              {:set \"brand/dark\" :tokens [...]},
              {:set \"primitivos\" :tokens [...]},
              ...]
     - active-set-names: #{\"brand/light\" \"primitivos\"}

     Output:
     [{:set \"brand/light\" :tokens [...]}
      {:set \"primitivos\" :tokens [...]}]"
  [sets active-set-names]
  (filter #(contains? active-set-names (:set %)) sets))

(mf/defc colorpicker-modal
  {::mf/register modal/components
   ::mf/register-as :colorpicker
   ::mf/props :obj}
  [{:keys [x y data position
           disable-gradient
           disable-opacity
           disable-image
           active-tokens
           on-change
           origin
           color-origin
           on-token-change
           on-close
           tab
           applied-token
           on-accept]}]
  (let [vport       (mf/deref viewport)
        dirty?      (mf/use-var false)
        last-change (mf/use-var nil)
        position    (d/nilv position :left)
        style       (calculate-position vport position x y (some? (:gradient data)))
        active-tokens (if (object? active-tokens)
                        (mfu/bean active-tokens)
                        active-tokens)

        on-change'
        (mf/use-fn
         (mf/deps on-change)
         (fn [new-data]
           (reset! dirty? (not= data new-data))
           (when (not= new-data @last-change)
             (reset! last-change new-data)
             (if (fn? on-change)
               (on-change new-data)
               (st/emit! (dc/update-colorpicker new-data))))))

        tokens-lib
        (mf/deref refs/tokens-lib)

        active-sets-names
        (mf/with-memo [tokens-lib]
          (some-> tokens-lib
                  (ctob/get-active-themes-set-names)))

        active-tokens (if (delay? active-tokens)
                        @active-tokens
                        active-tokens)

        color-tokens (:color active-tokens)

        grouped-tokens-by-set
        (mf/with-memo [tokens-lib active-sets-names color-tokens]
          (some-> tokens-lib
                  (ctob/get-sets)
                  ;; Show highest-precedence (last-defined) sets first in the
                  ;; picker; the Tokens panel itself keeps definition order.
                  ;; https://github.com/penpot/penpot/issues/10552
                  (reverse)
                  (add-tokens-to-sets)
                  (filter-active-sets active-sets-names)
                  (filter-non-empty-sets)
                  (group-sets)
                  (combine-groups-with-resolved  color-tokens)))]

    (mf/with-effect []
      (st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts :workspace)))
      (fn []
        (st/emit! (dsc/pop-shortcuts ::colorpicker))
        (when (and @dirty? @last-change on-close)
          (on-close @last-change))))

    [:div {:class (stl/css :colorpicker-tooltip)
           :data-testid "colorpicker"
           :style style}

     [:> colorpicker* {:data data
                       :combined-tokens grouped-tokens-by-set
                       :disable-gradient disable-gradient
                       :disable-opacity disable-opacity
                       :disable-image disable-image
                       :on-token-change on-token-change
                       :applied-token applied-token
                       :on-change on-change'
                       :origin origin
                       :tab tab
                       :color-origin color-origin
                       :on-accept on-accept}]]))
