                    ::blocked
                    ::locked]))
 
+(s/def ::id uuid?)
+(s/def ::page uuid?)
+(s/def ::type #{:rect
+                :group
+                :path
+                :circle
+                :image
+                :text})
+
+(s/def ::shape
+  (s/merge (s/keys ::req-un [::id ::page ::type]) ::attributes))
+
+(s/def ::rect-like-shape
+  (s/keys :req-un [::x1 ::y1 ::x2 ::y2 ::type]))
+
+;; --- Shapes CRUD
+
+(deftype AddShape [data]
+  udp/IPageUpdate
+  ptk/UpdateEvent
+  (update [_ state]
+    (let [shape (geom/setup-proportions data)
+          page (l/focus ul/selected-page state)]
+      (impl/assoc-shape-to-page state shape page))))
+
+(defn add-shape
+  [data]
+  (AddShape. data))
+
+(defn delete-shape
+  "Remove the shape using its id."
+  [id]
+  (reify
+    udp/IPageUpdate
+    ptk/UpdateEvent
+    (update [_ state]
+      (let [shape (get-in state [:shapes id])]
+        (impl/dissoc-shape state shape)))))
+
+(defn update-shape
+  "Just updates in place the shape."
+  [{:keys [id] :as shape}]
+  (reify
+    udp/IPageUpdate
+    ptk/UpdateEvent
+    (update [_ state]
+      (update-in state [:shapes id] merge shape))))
+
+;; --- Shape Transformations
+
+(declare align-point)
+
+(def ^:private canvas-coords
+  (gpt/point c/canvas-start-x
+             c/canvas-start-y))
+
+(declare apply-temporal-displacement)
+
+(defn initial-align-shape
+  [id]
+  (reify
+    ptk/WatchEvent
+    (watch [_ state s]
+      (let [{:keys [x1 y1] :as shape} (->> (get-in state [:shapes id])
+                                           (geom/shape->rect-shape state))
+            point1 (gpt/point x1 y1)
+            point2 (gpt/add point1 canvas-coords)]
+        (->> (align-point point2)
+             (rx/map #(gpt/subtract % canvas-coords))
+             (rx/map (fn [{:keys [x y] :as pt}]
+                       (apply-temporal-displacement id (gpt/subtract pt point1)))))))))
+
+(defn update-rotation
+  [sid rotation]
+  {:pre [(number? rotation)
+         (>= rotation 0)
+         (>= 360 rotation)]}
+  (reify
+    udp/IPageUpdate
+    ptk/UpdateEvent
+    (update [_ state]
+      (update-in state [:shapes sid]
+                 geom/rotate rotation))))
+
+(defn update-size
+  "A helper event just for update the position
+  of the shape using the width and height attrs
+  instread final point of coordinates."
+  [sid opts]
+  {:pre [(uuid? sid)]}
+  (reify
+    udp/IPageUpdate
+    ptk/UpdateEvent
+    (update [_ state]
+      (update-in state [:shapes sid] geom/resize-dim opts))))
+
+;; --- Apply Temporal Displacement
+
+(deftype ApplyTemporalDisplacement [id delta]
+  udp/IPageUpdate
+  ptk/UpdateEvent
+  (update [_ state]
+    (let [shape (get-in state [:shapes id])
+          displ (:tmp-displacement shape (gpt/point 0 0))
+          delta (gpt/add displ delta)]
+      (assoc-in state [:shapes id :tmp-displacement] delta))))
+
+(defn apply-temporal-displacement
+  [id pt]
+  {:pre [(uuid? id) (gpt/point? pt)]}
+  (ApplyTemporalDisplacement. id pt))
+
+;; --- Apply Displacement
+
+;; TODO: move to shapes-impl ns.
+
+(deftype ApplyDisplacement [id]
+  udp/IPageUpdate
+  ptk/UpdateEvent
+  (update [_ state]
+    (let [{:keys [tmp-displacement type] :as shape} (get-in state [:shapes id])
+          xfmt  (gmt/translate-matrix tmp-displacement)]
+
+      (if (= type :group)
+        (letfn [(update-item [state id]
+                  (let [{:keys [type items] :as shape} (get-in state [:shapes id])]
+                    (if (= type :group)
+                      (reduce update-item state items)
+                      (update-in state [:shapes id]
+                                 (fn [shape]
+                                   (as-> (dissoc shape :tmp-displacement) $
+                                     (geom/transform state $ xfmt)))))))]
+          (-> (reduce update-item state (:items shape))
+              (update-in [:shapes id] dissoc :tmp-displacement)))
+
+        (update-in state [:shapes id] (fn [shape]
+                                        (as-> (dissoc shape :tmp-displacement) $
+                                          (geom/transform state $ xfmt))))))))
+
+(defn apply-displacement
+  [id]
+  {:pre [(uuid? id)]}
+  (ApplyDisplacement. id))
+
+;; --- Apply Temporal Resize Matrix
+
+(deftype ApplyTemporalResizeMatrix [id mx]
+  udp/IPageUpdate
+  ptk/UpdateEvent
+  (update [_ state]
+    (assoc-in state [:shapes id :tmp-resize-xform] mx)))
+
+(defn apply-temporal-resize-matrix
+  "Attach temporal resize matrix transformation to the shape."
+  [id mx]
+  (ApplyTemporalResizeMatrix. id mx))
+
+;; --- Apply Resize Matrix
+
+(declare apply-resize-matrix)
+
+(deftype ApplyResizeMatrix [id]
+  udp/IPageUpdate
+  ptk/UpdateEvent
+  (update [_ state]
+    (let [{:keys [type tmp-resize-xform]
+           :or {tmp-resize-xform (gmt/matrix)}
+           :as shape} (get-in state [:shapes id])]
+      (if (= type :group)
+        (letfn [(update-item [state id]
+                  (let [{:keys [type items] :as shape} (get-in state [:shapes id])]
+                    (if (= type :group)
+                      (reduce update-item state items)
+                      (update-in state [:shapes id]
+                                 (fn [shape]
+                                   (as-> (dissoc shape :tmp-resize-xform) $
+                                     (geom/transform state $ tmp-resize-xform)))))))]
+          (-> (reduce update-item state (:items shape))
+              (update-in [:shapes id] dissoc :tmp-resize-xform)))
+        (update-in state [:shapes id] (fn [shape]
+                                        (as-> (dissoc shape :tmp-resize-xform) $
+                                          (geom/transform state $ tmp-resize-xform))))))))
+
+(defn apply-resize-matrix
+  "Apply definitivelly the resize matrix transformation to the shape."
+  [id]
+  {:pre [(uuid? id)]}
+  (ApplyResizeMatrix. id))
+
+(defn update-position
+  "Update the start position coordenate of the shape."
+  [sid {:keys [x y] :as opts}]
+  (reify
+    ptk/UpdateEvent
+    (update [_ state]
+      (update-in state [:shapes sid] geom/absolute-move opts))))
+
+(defn update-text
+  [sid {:keys [content]}]
+  {:pre [(string? content)]}
+  (reify
+    udp/IPageUpdate
+    ptk/UpdateEvent
+    (update [_ state]
+      (assoc-in state [:shapes sid :content] content))))
+
+;; --- Update Shape Attrs
+
+(declare UpdateAttrs)
+
+(deftype UpdateAttrs [id attrs]
+  ptk/WatchEvent
+  (watch [_ state stream]
+    (println "update-attrs" id attrs)
+    (let [{:keys [type] :as shape} (get-in state [:shapes id])]
+      (if (= type :group)
+        (rx/from-coll (map #(UpdateAttrs. % attrs) (:items shape)))
+        (rx/of #(update-in % [:shapes id] merge attrs))))))
+
 (defn update-attrs
   [id attrs]
-  {:pre [(uuid? id) (us/valid? ::shape-attrs attrs)]}
-  (let [atts (us/extract attrs ::shape-attrs)]
+  {:pre [(uuid? id) (us/valid? ::attributes attrs)]}
+  (let [atts (us/extract attrs ::attributes)]
     (UpdateAttrs. id attrs)))
 
 ;; --- Shape Proportions

            [uxbox.util.rlocks :as rlocks]
            [uxbox.util.uuid :as uuid]
            [uxbox.util.workers :as uw]))

(s/def ::x1 number?)
(s/def ::y1 number?)
(s/def ::x2 number?)
(s/def ::y2 number?)
(s/def ::type keyword?)

(s/def ::rect-shape (s/keys :req-un [::x1 ::y1 ::x2 ::y2 ::type]))

;; --- Shapes CRUD

(deftype AddShape [data]
  udp/IPageUpdate
  ptk/UpdateEvent
  (update [_ state]
    (let [shape (geom/setup-proportions data)
          page (l/focus ul/selected-page state)]
      (impl/assoc-shape-to-page state shape page))))

(defn add-shape
  [data]
  (AddShape. data))

(defn delete-shape
  "Remove the shape using its id."
  [id]
  (reify
    udp/IPageUpdate
    ptk/UpdateEvent
    (update [_ state]
      (let [shape (get-in state [:shapes id])]
        (impl/dissoc-shape state shape)))))

(defn update-shape
  "Just updates in place the shape."
  [{:keys [id] :as shape}]
  (reify
    udp/IPageUpdate
    ptk/UpdateEvent
    (update [_ state]
      (update-in state [:shapes id] merge shape))))

;; --- Shape Transformations

(declare align-point)

(def ^:private canvas-coords
  (gpt/point c/canvas-start-x
             c/canvas-start-y))

(declare apply-temporal-displacement)

(defn initial-align-shape
  [id]
  (reify
    ptk/WatchEvent
    (watch [_ state s]
      (let [{:keys [x1 y1] :as shape} (->> (get-in state [:shapes id])
                                           (geom/shape->rect-shape state))
            point1 (gpt/point x1 y1)
            point2 (gpt/add point1 canvas-coords)]
        (->> (align-point point2)
             (rx/map #(gpt/subtract % canvas-coords))
             (rx/map (fn [{:keys [x y] :as pt}]
                       (apply-temporal-displacement id (gpt/subtract pt point1)))))))))

(defn update-rotation
  [sid rotation]
  {:pre [(number? rotation)
         (>= rotation 0)
         (>= 360 rotation)]}
  (reify
    udp/IPageUpdate
    ptk/UpdateEvent
    (update [_ state]
      (update-in state [:shapes sid]
                 geom/rotate rotation))))

(defn update-size
  "A helper event just for update the position
  of the shape using the width and height attrs
  instread final point of coordinates."
  [sid opts]
  {:pre [(uuid? sid)]}
  (reify
    udp/IPageUpdate
    ptk/UpdateEvent
    (update [_ state]
      (update-in state [:shapes sid] geom/resize-dim opts))))

;; --- Apply Temporal Displacement

(deftype ApplyTemporalDisplacement [id delta]
  udp/IPageUpdate
  ptk/UpdateEvent
  (update [_ state]
    (let [shape (get-in state [:shapes id])
          displ (:tmp-displacement shape (gpt/point 0 0))
          delta (gpt/add displ delta)]
      (assoc-in state [:shapes id :tmp-displacement] delta))))

(defn apply-temporal-displacement
  [id pt]
  {:pre [(uuid? id) (gpt/point? pt)]}
  (ApplyTemporalDisplacement. id pt))

;; --- Apply Displacement

;; TODO: move to shapes-impl ns.

(deftype ApplyDisplacement [id]
  udp/IPageUpdate
  ptk/UpdateEvent
  (update [_ state]
    (let [{:keys [tmp-displacement type] :as shape} (get-in state [:shapes id])
          xfmt  (gmt/translate-matrix tmp-displacement)]

      (if (= type :group)
        (letfn [(update-item [state id]
                  (let [{:keys [type items] :as shape} (get-in state [:shapes id])]
                    (if (= type :group)
                      (reduce update-item state items)
                      (update-in state [:shapes id]
                                 (fn [shape]
                                   (as-> (dissoc shape :tmp-displacement) $
                                     (geom/transform state $ xfmt)))))))]
          (-> (reduce update-item state (:items shape))
              (update-in [:shapes id] dissoc :tmp-displacement)))

        (update-in state [:shapes id] (fn [shape]
                                        (as-> (dissoc shape :tmp-displacement) $
                                          (geom/transform state $ xfmt))))))))

(defn apply-displacement
  [id]
  {:pre [(uuid? id)]}
  (ApplyDisplacement. id))

;; --- Apply Temporal Resize Matrix

(deftype ApplyTemporalResizeMatrix [id mx]
  udp/IPageUpdate
  ptk/UpdateEvent
  (update [_ state]
    (assoc-in state [:shapes id :tmp-resize-xform] mx)))

(defn apply-temporal-resize-matrix
  "Attach temporal resize matrix transformation to the shape."
  [id mx]
  (ApplyTemporalResizeMatrix. id mx))

;; --- Apply Resize Matrix

(declare apply-resize-matrix)

(deftype ApplyResizeMatrix [id]
  udp/IPageUpdate
  ptk/UpdateEvent
  (update [_ state]
    (let [{:keys [type tmp-resize-xform]
           :or {tmp-resize-xform (gmt/matrix)}
           :as shape} (get-in state [:shapes id])]
      (if (= type :group)
        (letfn [(update-item [state id]
                  (let [{:keys [type items] :as shape} (get-in state [:shapes id])]
                    (if (= type :group)
                      (reduce update-item state items)
                      (update-in state [:shapes id]
                                 (fn [shape]
                                   (as-> (dissoc shape :tmp-resize-xform) $
                                     (geom/transform state $ tmp-resize-xform)))))))]
          (-> (reduce update-item state (:items shape))
              (update-in [:shapes id] dissoc :tmp-resize-xform)))
        (update-in state [:shapes id] (fn [shape]
                                        (as-> (dissoc shape :tmp-resize-xform) $
                                          (geom/transform state $ tmp-resize-xform))))))))

(defn apply-resize-matrix
  "Apply definitivelly the resize matrix transformation to the shape."
  [id]
  {:pre [(uuid? id)]}
  (ApplyResizeMatrix. id))

(defn update-position
  "Update the start position coordenate of the shape."
  [sid {:keys [x y] :as opts}]
  (reify
    ptk/UpdateEvent
    (update [_ state]
      (update-in state [:shapes sid] geom/absolute-move opts))))

(defn update-text
  [sid {:keys [content]}]
  {:pre [(string? content)]}
  (reify
    udp/IPageUpdate
    ptk/UpdateEvent
    (update [_ state]
      (assoc-in state [:shapes sid :content] content))))

;; --- Update Shape Attrs

(declare UpdateAttrs)

(deftype UpdateAttrs [id attrs]
  ptk/WatchEvent
  (watch [_ state stream]
    (println "update-attrs" id attrs)
    (let [{:keys [type] :as shape} (get-in state [:shapes id])]
      (if (= type :group)
        (rx/from-coll (map #(UpdateAttrs. % attrs) (:items shape)))
        (rx/of #(update-in % [:shapes id] merge attrs))))))

(s/def ::fill-color us/color?)
(s/def ::fill-opacity number?)

                   ::stroke-color
                   ::stroke-opacity
                   ::rx ::ry
                   ::x1 ::x2
                   ::y1 ::y2
                   ::proportion-lock
                   ::proportion
                   ::collapsed
