(ns uxbox.data.workspace
  (:require [bouncer.validators :as v]
            [beicon.core :as rx]
            [uxbox.shapes :as sh]
            [uxbox.rstore :as rs]
            [uxbox.router :as r]
            [uxbox.state :as st]
            [uxbox.schema :as sc]
            [uxbox.time :as time]
            [uxbox.xforms :as xf]
            [uxbox.shapes :as shapes]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Schemas
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ^:static +shape-props-schema+
  {:x [v/integer]
   :y [v/integer]
   :width [v/integer]
   :height [v/integer]})

(def ^:static +shape-schema+
  {:x [v/integer]
   :y [v/integer]
   :width [v/integer]
   :height [v/integer]
   :type [v/required sc/shape-type]})

(def ^:static +shape-update-size-schema+
  {:width [v/integer]
   :height [v/integer]
   :lock [v/boolean]})

(def ^:static +shape-update-fill-schema+
  {:fill [sc/color]
   :opacity [v/number]})

(def ^:static +shape-update-stroke-schema+
  {:color [sc/color]
   :opacity [v/number]})

(def ^:static +shape-update-position-schema+
  {:x [v/integer]
   :y [v/integer]})

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Events (explicit)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn initialize
  "Initialize the workspace state."
  [projectid pageid]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [s {:project projectid
               :toolboxes #{:layers}
               :flags #{}
               :drawing nil
               :selected #{}
               :page pageid}]
        (assoc state :workspace s)))))

(defn toggle-tool
  "Toggle the enabled flag of the specified tool."
  [key]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [flags (get-in state [:workspace :flags])]
        (if (contains? flags key)
          (assoc-in state [:workspace :flags] (disj flags key))
          (assoc-in state [:workspace :flags] (conj flags key)))))))

(defn toggle-toolbox
  "Toggle the visibility flag of the specified toolbox."
  [toolname]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [toolboxes (get-in state [:workspace :toolboxes])]
        (assoc-in state [:workspace :toolboxes]
                  (if (contains? toolboxes toolname)
                    (disj toolboxes toolname)
                    (conj toolboxes toolname)))))))

(defn select-for-drawing
  "Mark a shape selected for drawing in the canvas."
  [shape]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (if shape
        (assoc-in state [:workspace :drawing] shape)
        (update-in state [:workspace] dissoc :drawing)))))

(defn select-shape
  "Mark a shape selected for drawing in the canvas."
  [id]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [selected (get-in state [:workspace :selected])]
        (if (contains? selected id)
          (update-in state [:workspace :selected] disj id)
          (update-in state [:workspace :selected] conj id))))))

(defn select-shapes
  "Select shapes that matches the select rect."
  [selrect]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [pageid (get-in state [:workspace :page])
            xf (comp
                (filter #(= (:page %) pageid))
                (remove :hidden)
                (remove :blocked)
                (filter #(sh/contained-in? % selrect))
                (map :id))]
        (->> (into #{} xf (vals (:shapes-by-id state)))
             (assoc-in state [:workspace :selected]))))))

(defn add-shape
  "Create and add shape to the current selected page."
  [shape props]
  (sc/validate! +shape-schema+ shape)
  (sc/validate! +shape-props-schema+ props)
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [sid (random-uuid)
            pid (get-in state [:workspace :page])
            shape (merge shape props {:id sid :page pid})]
        (as-> state $
          (update-in $ [:pages-by-id pid :shapes] conj sid)
          (assoc-in $ [:shapes-by-id sid] shape))))))


;; FIXME: delete shape that is part of group.
(defn delete-shape
  "Remove the shape using its id."
  [id]
  (letfn [(dissoc-shape [state {:keys [id] :as shape}]
            (if (= (:type shape) :builtin/group)
              (let [state (update-in state [:shapes-by-id] dissoc id)]
                (->> (map #(get-in state [:shapes-by-id %]) (:items shape))
                     (reduce dissoc-shape state)))
              (update-in state [:shapes-by-id] dissoc id)))]
    (reify
      rs/UpdateEvent
      (-apply-update [_ state]
      (let [shape (get-in state [:shapes-by-id id])
            pageid (:page shape)
            shapes (get-in state [:pages-by-id pageid :shapes])
            shapes (into [] (remove #(= % id) shapes))]
        (as-> state $
          (assoc-in $ [:pages-by-id pageid :shapes] shapes)
          (dissoc-shape $ shape)))))))

(defn move-shape
  "Mark a shape selected for drawing in the canvas."
  [sid [dx dy :as delta]]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [shape (get-in state [:shapes-by-id sid])]
        (update-in state [:shapes-by-id sid] shapes/-move {:dx dx :dy dy})))))

(defn update-shape-rotation
  [sid rotation]
  {:pre [(number? rotation)
         (>= rotation 0)
         (>= 360 rotation)]}
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid]
                 shapes/-rotate rotation))))

;; TODO: implement locked resize

(defn update-shape-size
  [sid {:keys [width height lock] :as opts}]
  (sc/validate! +shape-update-size-schema+ opts)
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [shape (get-in state [:shapes-by-id sid])
            size (select-keys shape [:width :height])
            size (merge size
                        (when width {:width width})
                        (when height {:height height}))]
        (update-in state [:shapes-by-id sid]
                   shapes/-resize size)))))

(defn update-shape-position
  [sid {:keys [x y] :as opts}]
  (sc/validate! +shape-update-position-schema+ opts)
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid]
                 merge
                 (when x {:x x})
                 (when y {:y y})))))

(defn rebuild-group-size
  [id]
  (letfn [(update-shape-pos [state {:keys [id x y] :as data}]
            (update-in state [:shapes-by-id id] assoc :x x :y y))

          (update-group-size [shape shapes]
            (let [{:keys [width height]} (sh/group-size-and-position shapes)]
              (assoc shape
                     :width width
                     :height height
                     :view-box [0 0 width height])))

          (update-group [shape shapes x y]
            (-> shape
                (update-group-size shapes)
                (sh/translate-coords x y +)))]

    (reify
      rs/UpdateEvent
      (-apply-update [_ state]
        (let [shape (get-in state [:shapes-by-id id])
              shapes (map #(get-in state [:shapes-by-id %]) (:items shape))
              x (apply min (map :x shapes))
              y (apply min (map :y shapes))
              shapes (map #(sh/translate-coords % x y) shapes)]
          (as-> state $
            (reduce update-shape-pos $ shapes)
            (update-in $ [:shapes-by-id id] #(update-group % shapes x y))))))))

;; TODO: rename fill to "color" for consistency.

(defn update-shape-fill
  [sid {:keys [fill opacity] :as opts}]
  (sc/validate! +shape-update-fill-schema+ opts)
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid]
                 merge
                 (when fill {:fill fill})
                 (when opacity {:opacity opacity})))))

(defn update-shape-stroke
  [sid {:keys [color opacity width] :as opts}]
  (sc/validate! +shape-update-stroke-schema+ opts)
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid]
                 merge
                 (when width {:stroke-width width})
                 (when color {:stroke color})
                 (when opacity {:stroke-opacity opacity})))))

(defn toggle-shape-visibility
  [sid]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [shape (get-in state [:shapes-by-id sid])
            hidden? (:hidden shape false)]
        (if hidden?
          (assoc-in state [:shapes-by-id sid] (assoc shape :hidden false))
          (assoc-in state [:shapes-by-id sid] (assoc shape :hidden true)))))))

(defn toggle-shape-blocking
  [sid]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [shape (get-in state [:shapes-by-id sid])
            blocked? (:blocked shape false)]
        (if blocked?
          (assoc-in state [:shapes-by-id sid] (assoc shape :blocked false))
          (assoc-in state [:shapes-by-id sid] (assoc shape :blocked true)))))))

(defn toggle-shape-locking
  [sid]
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [shape (get-in state [:shapes-by-id sid])
            locked? (:locked shape false)]
        (if locked?
          (assoc-in state [:shapes-by-id sid] (assoc shape :locked false))
          (assoc-in state [:shapes-by-id sid] (assoc shape :locked true)))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Events (for selected)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn deselect-all
  "Mark a shape selected for drawing in the canvas."
  []
  (reify
    rs/WatchEvent
    (-apply-watch [_ state]
      (let [selected (get-in state [:workspace :selected])
            mevent (rs/swap-state #(assoc-in state [:workspace :selected] #{}))]
        (->> (map #(get-in state [:shapes-by-id %]) selected)
             (rx/from-coll)
             (rx/filter :group)
             (rx/map :group)
             (rx/map rebuild-group-size)
             (rx/merge (rx/just mevent)))))))

(defn copy-selected
  "Copy the selected shapes."
  []
  (reify
    rs/WatchEvent
    (-apply-watch [_ state]
      (let [selected (get-in state [:workspace :selected])]
        (as-> selected $
          (map #(get-in state [:shapes-by-id %]) $)
          (map #(assoc % :id (random-uuid)) $)
          (map #(add-shape % %) $)
          (rx/from-coll $))))))

(defn group-selected
  []
  (reify rs/UpdateEvent
    (-apply-update [_ state]
      (let [shapes-by-id (get state :shapes-by-id)
            sid (random-uuid)
            pid (get-in state [:workspace :page])
            selected (get-in state [:workspace :selected])
            shapes (->> selected
                        (map #(get shapes-by-id %))
                        (map #(assoc % :group sid)))
            {:keys [width height x y]} (sh/group-size-and-position shapes)
            group {:type :builtin/group
                   :id sid
                   :name (str "Group " (rand-int 1000))
                   :x x
                   :y y
                   :width width
                   :height height
                   :items (mapv :id shapes)
                   :page (get-in state [:workspace :page])
                   :view-box [0 0 width height]}
            shapes-map (->> shapes
                            (map #(sh/translate-coords % x y))
                            (reduce #(assoc %1 (:id %2) %2) {}))
            shapes-list (->> (get-in state [:pages-by-id pid :shapes])
                             (filter (comp not selected))
                             (into [sid]))]

        (as-> state $
          (update $ :shapes-by-id merge shapes-map)
          (update $ :shapes-by-id assoc sid group)
          (update $ :workspace assoc :selected #{})
          (update-in $ [:pages-by-id pid] assoc :shapes shapes-list))))))

(defn delete-selected
  "Deselect all and remove all selected shapes."
  []
  (reify
    rs/WatchEvent
    (-apply-watch [_ state]
      (let [selected (get-in state [:workspace :selected])]
        (rx/from-coll
         (into [(deselect-all)] (map #(delete-shape %) selected)))))))

(defn move-selected
  "Move a minimal position unit the selected shapes."
  [dir]
  {:pre [(contains? #{:up :down :right :left} dir)]}
  (reify
    rs/WatchEvent
    (-apply-watch [_ state]
      (let [selected (get-in state [:workspace :selected])
            delta (case dir
                    :up [0 -1]
                    :down [0 +1]
                    :right [+1 0]
                    :left [-1 0])]
        (rx/from-coll
         (map #(move-shape % delta) selected))))))

(defn update-selected-shapes-fill
  "Update the fill related attributed on
  selected shapes."
  [opts]
  (sc/validate! +shape-update-fill-schema+ opts)
  (reify
    rs/WatchEvent
    (-apply-watch [_ state]
      (rx/from-coll
       (->> (get-in state [:workspace :selected])
            (map #(update-shape-fill % opts)))))))

