 (s/def ::hidden boolean?)
 (s/def ::blocked boolean?)
 (s/def ::locked boolean?)
-
-(s/def ::shape-attrs
+(s/def ::x1 number?)
+(s/def ::y1 number?)
+(s/def ::x2 number?)
+(s/def ::y2 number?)
+
+(s/def ::attributes
   (s/keys :opt-un [::fill-color
                    ::fill-opacity
                    ::line-height

                   ::stroke-color
                   ::stroke-opacity
                   ::rx ::ry
                   ::proportion-lock
                   ::proportion
                   ::collapsed

                   ::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? ::attributes attrs)]}
  (let [atts (us/extract attrs ::attributes)]
    (UpdateAttrs. id attrs)))

;; --- Shape Proportions
