;; 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.data.workspace.shape-layout
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.files.changes-builder :as pcb]
   [app.common.files.helpers :as cfh]
   [app.common.files.shapes-helpers :as cfsh]
   [app.common.geom.point :as gpt]
   [app.common.geom.shapes.flex-layout :as flex]
   [app.common.geom.shapes.grid-layout :as grid]
   [app.common.logic.libraries :as cll]
   [app.common.types.color :as clr]
   [app.common.types.component :as ctc]
   [app.common.types.modifiers :as ctm]
   [app.common.types.shape.layout :as ctl]
   [app.common.uuid :as uuid]
   [app.main.data.changes :as dch]
   [app.main.data.event :as ev]
   [app.main.data.helpers :as dsh]
   [app.main.data.workspace :as-alias dw]
   [app.main.data.workspace.colors :as cl]
   [app.main.data.workspace.grid-layout.editor :as dwge]
   [app.main.data.workspace.modifiers :as dwm]
   [app.main.data.workspace.reflow :as wrf]
   [app.main.data.workspace.selection :as dwse]
   [app.main.data.workspace.shapes :as dwsh]
   [app.main.data.workspace.undo :as dwu]
   [app.main.features :as features]
   [beicon.v2.core :as rx]
   [potok.v2.core :as ptk]))

(def layout-keys
  [:layout
   :layout-flex-dir
   :layout-gap-type
   :layout-gap
   :layout-align-items
   :layout-justify-content
   :layout-align-content
   :layout-wrap-type
   :layout-padding-type
   :layout-padding
   :layout-gap-type])

(def initial-flex-layout
  {:layout                 :flex
   :layout-flex-dir        :row
   :layout-gap-type        :multiple
   :layout-gap             {:row-gap 0 :column-gap 0}
   :layout-align-items     :start
   :layout-justify-content :start
   :layout-align-content   :stretch
   :layout-wrap-type       :nowrap
   :layout-padding-type    :simple
   :layout-padding         {:p1 0 :p2 0 :p3 0 :p4 0}})

(def initial-grid-layout
  {:layout :grid
   :layout-grid-dir        :row
   :layout-gap-type        :multiple
   :layout-gap             {:row-gap 0 :column-gap 0}
   :layout-align-items     :start
   :layout-justify-items   :start
   :layout-align-content   :stretch
   :layout-justify-content :stretch
   :layout-padding-type    :simple
   :layout-padding         {:p1 0 :p2 0 :p3 0 :p4 0}
   :layout-grid-cells      {}
   :layout-grid-rows       []
   :layout-grid-columns    []})

(defn get-layout-initializer
  [type from-frame? calculate-params?]
  (let [[initial-layout-data calculate-params]
        (case type
          :flex [initial-flex-layout flex/calculate-params]
          :grid [initial-grid-layout grid/calculate-params])]

    (fn [shape objects]
      (let [shape
            (-> shape
                (merge initial-layout-data)

                ;; If the original shape is not a frame we set clip content and show-viewer to false
                (cond-> (not from-frame?)
                  (assoc :show-content true :hide-in-viewer true)))

            params (when calculate-params?
                     (calculate-params objects (cfh/get-immediate-children objects (:id shape)) shape))]
        (cond-> (merge shape params)
          (= type :grid)
          (-> (ctl/assign-cells objects) ctl/reorder-grid-children))))))

;; Never call this directly but through the data-event `:layout/update`
;; Otherwise a lot of cycle dependencies could be generated
(defn- update-layout-positions
  [{:keys [page-id ids undo-group reflow-tasks]}]
  (ptk/reify ::update-layout-positions
    ptk/WatchEvent
    (watch [_ state stream]
      (let [page-id (or page-id (:current-page-id state))
            objects (dsh/lookup-page-objects state page-id)
            ids (->> ids (remove uuid/zero?) (filter #(contains? objects %)))

            update-positions-stream
            (if (d/not-empty? ids)
              (let [modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))]
                (if (features/active-feature? state "render-wasm/v1")
                  (rx/of (dwm/apply-wasm-modifiers modif-tree
                                                   :stack-undo? true
                                                   :undo-group undo-group
                                                   :ignore-touched true))
                  (rx/of (dwm/apply-modifiers {:page-id page-id
                                               :modifiers modif-tree
                                               :stack-undo? true
                                               :ignore-touched true
                                               :undo-group undo-group}))))
              (rx/empty))

            ;; The apply events above are dispatched and applied synchronously,
            ;; except while a shape-update buffer is open (token propagation),
            ;; where the commit lands on `update-shapes-buffer-commit`.
            drain-stream
            (if (and (::dwsh/update-shapes-buffer state)
                     (d/not-empty? ids))
              (->> stream
                   (rx/filter (ptk/type? ::dwsh/update-shapes-buffer-commit))
                   (rx/take 1)
                   (rx/take-until (rx/filter (ptk/type? ::dw/finalize-workspace) stream))
                   ;; No events are derived from this
                   (rx/ignore))
              (rx/empty))]

        (cond->> (rx/concat update-positions-stream drain-stream)
          (d/not-empty? reflow-tasks)
          (rx/finalize #(wrf/finish-tasks! reflow-tasks)))))))

(defn- without-root-board
  [ids]
  (into [] (remove uuid/zero?) ids))

(defn initialize-shape-layout
  []
  (ptk/reify ::initialize-shape-layout
    ptk/WatchEvent
    (watch [_ _ stream]
      (let [stopper (rx/filter (ptk/type? ::finalize-shape-layout) stream)]
        (->> stream
             (rx/filter (ptk/type? :layout/update))
             ;; Keeps each update's page and the shapes that can be laid out;
             ;; the root lays out nothing, so an update with only it is dropped.
             (rx/map (fn [event]
                       (-> (deref event)
                           (update :ids without-root-board))))
             (rx/filter #(d/not-empty? (:ids %)))
             (rx/map #(assoc % ::reflow-task (wrf/start! :layout (:ids %))))
             (rx/buffer-time 100)
             (rx/filter #(d/not-empty? %))
             (rx/mapcat
              (fn [updates]
                (->> (group-by :page-id updates)
                     (map (fn [[page-id updates]]
                            (let [ids (into #{} (mapcat :ids) updates)]
                              (update-layout-positions {:page-id page-id
                                                        :ids ids
                                                        :reflow-tasks (mapv ::reflow-task updates)})))))))
             (rx/take-until stopper)
             ;; On workspace teardown clear everything still pending.
             (rx/finalize wrf/reset-pending!))))))

(defn finalize-shape-layout
  []
  (ptk/data-event ::finalize-shape-layout))

(defn create-layout-from-id
  [id type & {:keys [from-frame? calculate-params?] :or {from-frame? false calculate-params? true}}]
  (dm/assert!
   "expected uuid for `id`"
   (uuid? id))

  (ptk/reify ::create-layout-from-id
    ptk/WatchEvent
    (watch [_ state _]
      (let [objects            (dsh/lookup-page-objects state)
            parent             (get objects id)
            undo-id            (js/Symbol)
            layout-initializer (get-layout-initializer type from-frame? calculate-params?)]

        (rx/of (dwu/start-undo-transaction undo-id)
               (dwsh/update-shapes [id] layout-initializer {:with-objects? true})
               (dwsh/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v))
               (ptk/data-event :layout/update {:ids [id]})
               (dwu/commit-undo-transaction undo-id))))))

(defn create-layout-from-selection
  [type]
  (ptk/reify ::create-layout-from-selection
    ptk/WatchEvent
    (watch [_ state _]

      (let [page-id         (:current-page-id state)
            objects         (dsh/lookup-page-objects state page-id)
            selected        (dsh/lookup-selected state)
            selected-shapes (map (d/getf objects) selected)
            single?         (= (count selected-shapes) 1)
            has-group?      (->> selected-shapes (d/seek cfh/group-shape?))
            is-group?       (and single? has-group?)
            has-mask?       (->> selected-shapes (d/seek cfh/mask-shape?))
            is-mask?        (and single? has-mask?)
            has-component?  (some true? (map ctc/instance-root? selected-shapes))
            is-component?   (and single? has-component?)
            has-variant?    (some ctc/is-variant? selected-shapes)

            has-layout?     (and single? (ctl/any-layout? (first selected-shapes)))

            new-shape-id (uuid/next)
            undo-id      (js/Symbol)]

        (if has-variant?
          (rx/empty)
          (rx/concat
           (rx/of (dwu/start-undo-transaction undo-id))
           (cond
             (and is-group? (not is-component?) (not is-mask?))
             ;; Create layout from a group:
             ;;  When creating a layout from a group we remove the group and create the layout with its children
             (let [parent-id    (:parent-id (first selected-shapes))
                   shapes-ids   (:shapes (first selected-shapes))
                   ordered-ids  (into (d/ordered-set) shapes-ids)
                   group-index  (cfh/get-index-replacement selected objects)]
               (rx/of
                (dwse/select-shapes ordered-ids)
                (dwsh/create-artboard-from-selection new-shape-id parent-id group-index (:name (first selected-shapes)))
                (cl/remove-all-fills [new-shape-id] {:color clr/black :opacity 1})
                (create-layout-from-id new-shape-id type)
                (dwsh/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto))
                (dwsh/update-shapes selected ctl/toggle-fix-if-auto)
                (dwsh/delete-shapes page-id selected)
                (ptk/data-event :layout/update {:ids [new-shape-id]})
                (dwu/commit-undo-transaction undo-id)))

             has-layout?
             (rx/of
              (create-layout-from-id (first selected) type)
              (ptk/data-event :layout/update {:ids selected})
              (dwu/commit-undo-transaction undo-id))

             ;; Create Layout from selection
             :else
             (rx/of
              (dwsh/create-artboard-from-selection new-shape-id)
              (cl/remove-all-fills [new-shape-id] {:color clr/black :opacity 1})
              (create-layout-from-id new-shape-id type)
              (dwsh/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto))
              (dwsh/update-shapes selected ctl/toggle-fix-if-auto)))

           (rx/of (ptk/data-event :layout/update {:ids [new-shape-id]})
                  (dwu/commit-undo-transaction undo-id))))))))

(defn remove-layout
  [ids]
  (ptk/reify ::remove-shape-layout
    ptk/WatchEvent
    (watch [_ _ _]
      (let [undo-id (js/Symbol)]
        (rx/of
         (dwu/start-undo-transaction undo-id)
         (dwsh/update-shapes ids #(apply dissoc % layout-keys))
         (ptk/data-event :layout/update {:ids ids})
         (dwu/commit-undo-transaction undo-id))))))

(defn create-layout
  [type]
  (ptk/reify ::create-shape-layout
    ev/Event
    (-data [_]
      {:layout (d/name type)})

    ptk/WatchEvent
    (watch [_ state _]
      (let [page-id          (:current-page-id state)
            objects          (dsh/lookup-page-objects state page-id)
            selected         (dsh/lookup-selected state)
            selected-shapes  (map (d/getf objects) selected)
            single?          (= (count selected-shapes) 1)
            is-frame?        (= :frame (:type (first selected-shapes)))
            has-layout?      (ctl/any-layout? (first selected-shapes))

            undo-id          (js/Symbol)]

        (rx/of
         (dwu/start-undo-transaction undo-id)
         (if (and single? is-frame? (not has-layout?))
           (create-layout-from-id (first selected) type :from-frame? true)
           (create-layout-from-selection type))
         (dwu/commit-undo-transaction undo-id))))))

(defn toggle-layout
  [type]
  (ptk/reify ::toggle-shape-layout
    ptk/WatchEvent
    (watch [it state _]
      (let [objects          (dsh/lookup-page-objects state)
            selected         (dsh/lookup-selected state)
            selected-shapes  (map (d/getf objects) selected)
            single?          (= (count selected-shapes) 1)
            has-layout?      (and single?
                                  (ctl/any-layout? objects (:id (first selected-shapes))))]

        (when (not= 0 (count selected))
          (let [event (if has-layout?
                        (remove-layout selected)
                        (create-layout type))]
            (rx/of (with-meta event (meta it)))))))))

(defn update-layout
  ([ids changes] (update-layout ids changes nil))
  ([ids changes options]
   (ptk/reify ::update-layout
     ptk/WatchEvent
     (watch [_ _ _]
       (let [undo-id       (js/Symbol)
             padding-attrs (-> (get changes :layout-padding)
                               keys
                               set)]
         (rx/of (dwu/start-undo-transaction undo-id)
                (dwsh/update-shapes ids (d/patch-object changes)
                                    (cond-> options
                                      (seq padding-attrs)
                                      (assoc :changed-sub-attr padding-attrs)))
                (ptk/data-event :layout/update {:ids ids})
                (dwu/commit-undo-transaction undo-id)
                (when (or (:layout-align-content changes) (:layout-justify-content changes))
                  (ev/event
                   {::ev/name "layout-change-alignment"}))
                (when (or (:layout-padding changes) (:layout-gap changes))
                  (ev/event
                   {::ev/name "layout-change-margin"}))))))))

(defn add-layout-track
  ([ids type value]
   (add-layout-track ids type value nil))
  ([ids type value index]
   (assert (#{:row :column} type))
   (ptk/reify ::add-layout-track
     ptk/WatchEvent
     (watch [_ _ _]
       (let [undo-id (js/Symbol)]
         (rx/of (dwu/start-undo-transaction undo-id)
                (dwsh/update-shapes
                 ids
                 (fn [shape]
                   (case type
                     :row    (ctl/add-grid-row shape value index)
                     :column (ctl/add-grid-column shape value index))))
                (ptk/data-event :layout/update {:ids ids})
                (dwu/commit-undo-transaction undo-id)))))))

(defn remove-layout-track
  [ids type index & {:keys [with-shapes?] :or {with-shapes? false}}]
  (assert (#{:row :column} type))

  (ptk/reify ::remove-layout-track
    ptk/WatchEvent
    (watch [_ state _]
      (let [objects (dsh/lookup-page-objects state)
            undo-id (js/Symbol)

            shapes-to-delete
            (when with-shapes?
              (->> ids
                   (mapcat
                    (fn [id]
                      (let [shape (get objects id)]
                        (if (= type :column)
                          (ctl/shapes-by-column shape index)
                          (ctl/shapes-by-row shape index)))))
                   (into #{})))]
        (rx/of (dwu/start-undo-transaction undo-id)
               (if shapes-to-delete
                 (dwsh/delete-shapes shapes-to-delete)
                 (rx/empty))
               (dwsh/update-shapes
                ids
                (fn [shape objects]
                  (case type
                    :row    (ctl/remove-grid-row shape index objects)
                    :column (ctl/remove-grid-column shape index objects)))
                {:with-objects? true})
               (ptk/data-event :layout/update {:ids ids})
               (dwu/commit-undo-transaction undo-id))))))

(defn duplicate-layout-track
  [ids type index]
  (assert (#{:row :column} type))

  (ptk/reify ::duplicate-layout-track
    ptk/WatchEvent
    (watch [it state _]
      (let [file-id      (:current-file-id state)
            page         (dsh/lookup-page state)
            objects      (:objects page)
            libraries    (dsh/lookup-libraries state)
            library-data (dsh/lookup-file state file-id)
            shape-id     (first ids)
            base-shape   (get objects shape-id)

            shapes-by-track
            (if (= type :column)
              (ctl/shapes-by-column base-shape index false)
              (ctl/shapes-by-row base-shape index false))

            ;; Change to set in order to use auxiliary functions
            selected (set shapes-by-track)

            changes
            (-> (pcb/empty-changes it)
                (cll/generate-duplicate-changes objects page selected (gpt/point 0 0) libraries library-data file-id)
                (cll/generate-duplicate-changes-update-indices objects selected))

            ;; Creates a map with shape-id => duplicated-shape-id
            ids-map
            (->> changes
                 :redo-changes
                 (filter #(= (:type %) :add-obj))
                 (filter #(selected (:old-id %)))
                 (map #(vector (:old-id %) (get-in % [:obj :id])))
                 (into {}))

            changes
            (-> changes
                (pcb/update-shapes
                 ids
                 (fn [shape objects]
                   ;; The duplication could have altered the grid so we restore the values, we'll calculate the good ones now
                   (let [shape (merge shape (select-keys base-shape [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))]
                     (case type
                       :row    (ctl/duplicate-row shape objects index ids-map)
                       :column (ctl/duplicate-column shape objects index ids-map))))
                 {:with-objects? true}))

            undo-id (js/Symbol)]
        (rx/of (dwu/start-undo-transaction undo-id)
               (dch/commit-changes changes)
               (ptk/data-event :layout/update {:ids ids})
               (dwu/commit-undo-transaction undo-id))))))

(defn reorder-layout-track
  [ids type from-index to-index move-content?]
  (assert (#{:row :column} type))

  (ptk/reify ::reorder-layout-track
    ptk/WatchEvent
    (watch [_ _ _]
      (let [undo-id (js/Symbol)]
        (rx/of (dwu/start-undo-transaction undo-id)
               (dwsh/update-shapes
                ids
                (fn [shape]
                  (case type
                    :row    (ctl/reorder-grid-row shape from-index to-index move-content?)
                    :column (ctl/reorder-grid-column shape from-index to-index move-content?))))
               (ptk/data-event :layout/update {:ids ids})
               (dwu/commit-undo-transaction undo-id))))))

(defn hover-layout-track
  [ids type index hover?]
  (assert (#{:row :column} type))

  (ptk/reify ::hover-layout-track
    ptk/UpdateEvent
    (update [_ state]
      (let [objects (dsh/lookup-page-objects state)
            shape (get objects (first ids))

            highlighted
            (when hover?
              (->> (if (= type :row)
                     (ctl/shapes-by-row shape index)
                     (ctl/shapes-by-column shape index))
                   (set)))]
        (cond-> state
          hover?
          (update-in [:workspace-grid-edition (first ids) :hover-track] (fnil conj #{}) [type index])

          (not hover?)
          (update-in [:workspace-grid-edition (first ids) :hover-track] (fnil disj #{}) [type index])

          :always
          (assoc-in [:workspace-local :highlighted] highlighted))))))

(defn change-layout-track
  [ids type index props]
  (assert (#{:row :column} type))
  (ptk/reify ::change-layout-track
    ptk/WatchEvent
    (watch [_ _ _]
      (let [undo-id (js/Symbol)
            property (case type
                       :row :layout-grid-rows
                       :column :layout-grid-columns)]
        (rx/of (dwu/start-undo-transaction undo-id)
               (dwsh/update-shapes
                ids
                (fn [shape]
                  (-> shape
                      (update-in [property index] merge props))))
               (ptk/data-event :layout/update {:ids ids})
               (dwu/commit-undo-transaction undo-id))))))

(defn fix-child-sizing
  [objects parent-changes shape]

  (let [parent (-> (cfh/get-parent objects (:id shape))
                   (d/deep-merge parent-changes))

        auto-width? (ctl/auto-width? parent)
        auto-height? (ctl/auto-height? parent)
        col? (ctl/col? parent)
        row? (ctl/row? parent)

        all-children (->> parent
                          :shapes
                          (map (d/getf objects))
                          (remove ctl/position-absolute?))]

    (cond-> shape
      ;; If the parent is hug width and the direction column
      ;; change to fixed when ALL children are fill
      (and col? auto-width? (every? ctl/fill-width? all-children))
      (assoc :layout-item-h-sizing :fix)

      ;; If the parent is hug height and the direction is column
      ;; change to fixed when ANY children is fill
      (and col? auto-height? (ctl/fill-height? shape))
      (assoc :layout-item-v-sizing :fix)

      ;; If the parent is hug width and the direction row
      ;; change to fixed when ANY children is fill
      (and row? auto-width? (ctl/fill-width? shape))
      (assoc :layout-item-h-sizing :fix)

      ;; If the parent is hug height and the direction row
      ;; change to fixed when ALL children are fill
      (and row? auto-height? (every? ctl/fill-height? all-children))
      (assoc :layout-item-v-sizing :fix))))

(defn fix-parent-sizing
  [parent objects ids-set changes]

  (let [auto-width? (ctl/auto-width? parent)
        auto-height? (ctl/auto-height? parent)
        col? (ctl/col? parent)
        row? (ctl/row? parent)

        all-children
        (->> parent :shapes
             (map (d/getf objects))
             (map (fn [shape]
                    (if (contains? ids-set (:id shape))
                      (d/deep-merge shape changes)
                      shape))))]

    (cond-> parent
      ;; Col layout and parent is hug-width if all children are fill-width
      ;; change parent to fixed
      (and col? auto-width? (every? ctl/fill-width? all-children))
      (assoc :layout-item-h-sizing :fix)

      ;; Col layout and parent is hug-height if any children is fill-height
      ;; change parent to fixed
      (and col? auto-height? (some ctl/fill-height? all-children))
      (assoc :layout-item-v-sizing :fix)

      ;; Row layout and parent is hug-width if any children is fill-width
      ;; change parent to fixed
      (and row? auto-width? (some ctl/fill-width? all-children))
      (assoc :layout-item-h-sizing :fix)

      ;; Row layout and parent is hug-height if all children are fill-height
      ;; change parent to fixed
      (and row? auto-height? (every? ctl/fill-height? all-children))
      (assoc :layout-item-v-sizing :fix))))
(defn update-layout-child
  ([ids attrs] (update-layout-child ids attrs nil))
  ([ids attrs options]
   (ptk/reify ::update-layout-child
     ptk/WatchEvent
     (watch [_ state _]
       (let [page-id      (or (get options :page-id)
                              (get state :current-page-id))
             objects      (dsh/lookup-page-objects state page-id)
             children-ids (->> ids (mapcat #(cfh/get-children-ids objects %)))
             parent-ids   (->> ids (map #(cfh/get-parent-id objects %)))
             undo-id      (js/Symbol)
             margin-attrs (-> (get attrs :layout-item-margin)
                              keys
                              set)]
         (rx/of (dwu/start-undo-transaction undo-id)
                (dwsh/update-shapes ids (d/patch-object attrs)
                                    (cond-> options
                                      (seq margin-attrs)
                                      (assoc :changed-sub-attr margin-attrs)))
                (dwsh/update-shapes children-ids (partial fix-child-sizing objects attrs) options)
                (dwsh/update-shapes
                 parent-ids
                 (fn [parent objects]
                   (-> parent
                       (fix-parent-sizing objects (set ids) attrs)
                       (cond-> (ctl/grid-layout? parent)
                         (ctl/assign-cells objects))))
                 (merge options {:with-objects? true}))
                (ptk/data-event :layout/update {:ids ids})
                (dwu/commit-undo-transaction undo-id)))))))

(defn update-grid-cells
  [layout-id ids props]
  (ptk/reify ::update-grid-cells
    ptk/WatchEvent
    (watch [_ _ _]
      (let [undo-id (js/Symbol)]
        (rx/of
         (dwu/start-undo-transaction undo-id)
         (dwsh/update-shapes
          [layout-id]
          (fn [shape]
            (->> ids
                 (reduce
                  (fn [shape cell-id]
                    (d/update-in-when
                     shape
                     [:layout-grid-cells cell-id]
                     d/patch-object props))
                  shape))))
         (ptk/data-event :layout/update {:ids [layout-id]})
         (dwu/commit-undo-transaction undo-id))))))

(defn change-cells-mode
  [layout-id ids mode]

  (ptk/reify ::change-cells-mode
    ptk/WatchEvent
    (watch [_ _ _]
      (let [undo-id (js/Symbol)]
        (rx/of
         (dwu/start-undo-transaction undo-id)
         (dwsh/update-shapes
          [layout-id]
          (fn [shape objects]
            (case mode
              :auto
              ;; change the manual cells and move to auto
              (->> ids
                   (reduce
                    (fn [shape cell-id]
                      (let [cell (get-in shape [:layout-grid-cells cell-id])]
                        (cond-> shape
                          (or (contains? #{:area :manual} (:position cell))
                              (> (:row-span cell) 1)
                              (> (:column-span cell) 1))
                          (-> (d/update-in-when [:layout-grid-cells cell-id] assoc :shapes [] :position :auto)
                              (d/update-in-when [:layout-grid-cells cell-id] dissoc :area-name)
                              (ctl/resize-cell-area (:row cell) (:column cell) (:row cell) (:column cell) 1 1)
                              (ctl/assign-cells objects)))))
                    shape))

              :manual
              (->> ids
                   (reduce
                    (fn [shape cell-id]
                      (let [cell (get-in shape [:layout-grid-cells cell-id])]
                        (cond-> shape
                          (contains? #{:area :auto} (:position cell))
                          (-> (d/assoc-in-when [:layout-grid-cells cell-id :position] :manual)
                              (d/update-in-when [:layout-grid-cells cell-id] dissoc :area-name)
                              (ctl/assign-cells objects)))))
                    shape))

              :area
              ;; Create area with the selected cells
              (let [{:keys [first-row first-column last-row last-column]}
                    (ctl/cells-coordinates (->> ids (map #(get-in shape [:layout-grid-cells %]))))

                    target-cell
                    (ctl/get-cell-by-position shape first-row first-column)

                    shape
                    (-> shape
                        (ctl/resize-cell-area
                         (:row target-cell) (:column target-cell)
                         first-row
                         first-column
                         (inc (- last-row first-row))
                         (inc (- last-column first-column)))
                        (ctl/assign-cells objects))]

                (-> shape
                    (d/update-in-when [:layout-grid-cells (:id target-cell)] assoc :position :area)))))
          {:with-objects? true})
         (dwge/clean-selection layout-id)
         (ptk/data-event :layout/update {:ids [layout-id]})
         (dwu/commit-undo-transaction undo-id))))))

(defn merge-cells
  [layout-id ids]

  (ptk/reify ::merge-cells
    ptk/WatchEvent
    (watch [_ _ _]
      (let [undo-id (js/Symbol)]
        (rx/of
         (dwu/start-undo-transaction undo-id)
         (dwsh/update-shapes
          [layout-id]
          (fn [shape objects]
            (let [cells (->> ids (map #(get-in shape [:layout-grid-cells %])))

                  {:keys [first-row first-column last-row last-column]}
                  (ctl/cells-coordinates cells)

                  target-cell
                  (ctl/get-cell-by-position shape first-row first-column)]
              (-> shape
                  (ctl/resize-cell-area
                   (:row target-cell) (:column target-cell)
                   first-row
                   first-column
                   (inc (- last-row first-row))
                   (inc (- last-column first-column)))
                  (ctl/assign-cells objects))))
          {:with-objects? true})
         (dwge/clean-selection layout-id)
         (ptk/data-event :layout/update {:ids [layout-id]})
         (dwu/commit-undo-transaction undo-id))))))

(defn update-grid-cell-position
  [layout-id cell-id props]

  (ptk/reify ::update-grid-cell-position
    ptk/WatchEvent
    (watch [_ _ _]
      (let [undo-id (js/Symbol)]
        (rx/of
         (dwu/start-undo-transaction undo-id)
         (dwsh/update-shapes
          [layout-id]
          (fn [shape objects]
            (let [prev-data (-> (dm/get-in shape [:layout-grid-cells cell-id])
                                (select-keys [:row :column :row-span :column-span]))

                  new-data (merge prev-data props)]
              (-> shape
                  (ctl/resize-cell-area (:row prev-data) (:column prev-data)
                                        (:row new-data) (:column new-data)
                                        (:row-span new-data) (:column-span new-data))
                  (ctl/assign-cells objects))))
          {:with-objects? true})
         (ptk/data-event :layout/update {:ids [layout-id]})
         (dwu/commit-undo-transaction undo-id))))))


(defn create-cell-board
  [layout-id cell-ids]
  (ptk/reify ::create-cell-board
    ptk/WatchEvent
    (watch [it state _]
      (let [page-id (:current-page-id state)
            objects (dsh/lookup-page-objects state)
            frame-id (uuid/next)

            undo-id (js/Symbol)

            shape (get objects layout-id)
            cells      (->> cell-ids (map #(get-in shape [:layout-grid-cells %])))
            selected   (into #{} (mapcat :shapes) cells)

            {:keys [first-row first-column last-row last-column]} (ctl/cells-coordinates cells)

            target-cell (ctl/get-cell-by-position shape first-row first-column)

            [_ changes]
            (-> (pcb/empty-changes it page-id)
                (pcb/with-objects objects)
                (cond-> (d/not-empty? selected)
                  (cfsh/prepare-create-artboard-from-selection
                   frame-id layout-id objects selected 0 nil true (:id target-cell)))

                (cond-> (empty? (seq selected))
                  (cfsh/prepare-create-empty-artboard
                   frame-id layout-id objects 0 nil true (:id target-cell))))

            changes
            (-> changes
                (pcb/update-shapes
                 [frame-id]
                 (fn [shape]
                   (-> shape
                       (assoc :layout-item-h-sizing :fill)
                       (assoc :layout-item-v-sizing :fill))))
                (pcb/update-shapes
                 [layout-id]
                 (fn [shape]
                   (let [new-row-span (inc (- last-row first-row))
                         new-col-span (inc (- last-column first-column))]
                     (-> shape
                         (ctl/resize-cell-area
                          (:row target-cell) (:column target-cell)
                          first-row first-column new-row-span new-col-span))))))]

        (rx/of
         (dwu/start-undo-transaction undo-id)
         (dch/commit-changes changes)
         (ptk/data-event :layout/update {:ids [layout-id]})
         (dwu/commit-undo-transaction undo-id))))))

(defn complete-rows?
  "Check if the selected cells cover complete row(s) — all columns must be included."
  [grid cells]
  (let [{:keys [first-column last-column]} (ctl/cells-coordinates cells)
        num-columns (count (:layout-grid-columns grid))]
    (and (= first-column 1)
         (= last-column num-columns))))

(defn complete-columns?
  "Check if the selected cells cover complete column(s) — all rows must be included."
  [grid cells]
  (let [{:keys [first-row last-row]} (ctl/cells-coordinates cells)
        num-rows (count (:layout-grid-rows grid))]
    (and (= first-row 1)
         (= last-row num-rows))))

(defn copy-grid-tracks
  "Store the selected track indices for later paste. Works for both
   complete rows and complete columns."
  [grid-id type]
  (assert (#{:row :column} type))
  (ptk/reify ::copy-grid-tracks
    ptk/UpdateEvent
    (update [_ state]
      (let [objects    (dsh/lookup-page-objects state)
            grid       (get objects grid-id)
            selected   (get-in state [:workspace-grid-edition grid-id :selected])
            cells      (->> selected (map #(get-in grid [:layout-grid-cells %])))
            {:keys [first-row last-row first-column last-column]} (ctl/cells-coordinates cells)
            ;; Convert 1-indexed cell positions to 0-indexed track indices
            track-indices (if (= type :row)
                            (vec (range (dec first-row) last-row))
                            (vec (range (dec first-column) last-column)))]
        (assoc-in state [:workspace-grid-edition grid-id :copied-tracks]
                  {:track-indices track-indices
                   :type type
                   :grid-id grid-id})))))

(defn paste-grid-tracks
  "Paste previously copied tracks at the end of the grid.
   Each source track is duplicated and appended after the last
   existing track. All operations are grouped in a single undo
   transaction. Follows the same pattern as `duplicate-layout-track`."
  [grid-id]
  (ptk/reify ::paste-grid-tracks
    ptk/WatchEvent
    (watch [it state _]
      (let [file-id      (:current-file-id state)
            page         (dsh/lookup-page state)
            objects      (:objects page)
            libraries    (dsh/lookup-libraries state)
            library-data (dsh/lookup-file state file-id)
            grid         (get objects grid-id)

            copied        (get-in state [:workspace-grid-edition grid-id :copied-tracks])
            track-indices (:track-indices copied)
            type          (:type copied)
            undo-id       (js/Symbol)]

        (when (and (seq track-indices) (some? type))
          (let [shapes-by-track-fn
                (if (= type :row)
                  ctl/shapes-by-row
                  ctl/shapes-by-column)

                ;; Collect shapes from all source tracks
                all-shapes
                (->> track-indices
                     (mapcat #(shapes-by-track-fn grid % false))
                     (set))

                ;; Generate duplication changes for all shapes at once
                changes
                (-> (pcb/empty-changes it)
                    (cll/generate-duplicate-changes objects page all-shapes (gpt/point 0 0) libraries library-data file-id)
                    (cll/generate-duplicate-changes-update-indices objects all-shapes))

                ;; Build ids-map: old-shape-id -> new-shape-id
                ids-map
                (->> changes
                     :redo-changes
                     (filter #(= (:type %) :add-obj))
                     (filter #(all-shapes (:old-id %)))
                     (map #(vector (:old-id %) (get-in % [:obj :id])))
                     (into {}))

                duplicate-at-fn
                (if (= type :row)
                  ctl/duplicate-row-at
                  ctl/duplicate-column-at)

                tracks-prop
                (if (= type :row)
                  :layout-grid-rows
                  :layout-grid-columns)

                ;; Sort source indices ascending — we'll append each
                ;; copy at the end in order, preserving the original
                ;; track ordering in the appended block.
                sorted-indices (vec (sort track-indices))

                changes
                (-> changes
                    (pcb/update-shapes
                     [grid-id]
                     (fn [shape objects]
                       ;; Restore grid structure (duplication may have altered it)
                       (let [shape (merge shape (select-keys grid [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))]
                         ;; Append each source track at the end.
                         ;; Process in ascending order so the copies
                         ;; appear in the same order as the originals.
                         ;; Each insertion adds one track, so both the
                         ;; target index and the source index (if it
                         ;; comes after the target) shift by 1.
                         (reduce
                          (fn [s [offset src-idx]]
                            (let [;; Source tracks don't shift because we
                                  ;; append after them (target > source).
                                  actual-src src-idx
                                  ;; Append at the end (which grows by
                                  ;; one with each iteration).
                                  target-idx (+ (count (get grid tracks-prop)) offset)]
                              (duplicate-at-fn s objects actual-src target-idx ids-map)))
                          shape
                          (map-indexed vector sorted-indices))))
                     {:with-objects? true}))]

            (rx/of (dwu/start-undo-transaction undo-id)
                   (dch/commit-changes changes)
                   (ptk/data-event :layout/update {:ids [grid-id]})
                   (dwu/commit-undo-transaction undo-id))))))))
