;; 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) 2015-2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>

(ns uxbox.main.data.shapes
  (:require [beicon.core :as rx]
            [uxbox.util.uuid :as uuid]
            [uxbox.main.constants :as c]
            [uxbox.util.rstore :as rs]
            [uxbox.util.router :as r]
            [uxbox.util.schema :as sc]
            [uxbox.main.geom :as geom]
            [uxbox.util.geom.point :as gpt]
            [uxbox.util.workers :as uw]
            [uxbox.main.state :as st]
            [uxbox.main.state.shapes :as stsh]
            [uxbox.main.data.core :refer (worker)]
            [uxbox.main.data.pages :as udp]
            [uxbox.util.rlocks :as rlocks]
            [uxbox.util.geom.point :as gpt]
            [uxbox.util.data :refer (index-of)]))

;; --- Shapes CRUD

(defn add-shape
  "Create and add shape to the current selected page."
  [shape]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [page (get-in state [:workspace :page])
            shape (geom/setup-proportions shape)]
        (stsh/assoc-shape-to-page state shape page)))))

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

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

;; --- Shape Transformations

(defn move-shape
  "Move shape using relative position (delta)."
  [sid delta]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [shape (get-in state [:shapes-by-id sid])]
        (update-in state [:shapes-by-id sid] geom/move delta)))))

(declare align-point)

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

(defn initial-align-shape
  [id]
  (reify
    rs/WatchEvent
    (-apply-watch [_ state s]
      (let [shape (get-in state [:shapes-by-id id])
            shape (geom/outer-rect state shape)
            point (gpt/point (:x shape) (:y shape))
            point (gpt/add point canvas-coords)]
        (->> (align-point point)
             (rx/map #(gpt/subtract % point))
             (rx/map #(move-shape id %)))))))

(defn update-line-attrs
  [sid {:keys [x1 y1 x2 y2] :as opts}]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [shape (get-in state [:shapes-by-id sid])
            props (select-keys opts [:x1 :y1 :x2 :y2])
            props' (select-keys shape [:x1 :y1 :x2 :y2])]
        (update-in state [:shapes-by-id sid] geom/setup
                   (merge props' props))))))

(defn update-rotation
  [sid rotation]
  {:pre [(number? rotation)
         (>= rotation 0)
         (>= 360 rotation)]}
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id 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
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid] geom/resize-dim opts))))

(defn update-vertex-position
  [id {:keys [vid delta]}]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id id] geom/move-vertex vid delta))))

(defn initial-vertext-align
  [id vid]
  (reify
    rs/WatchEvent
    (-apply-watch [_ state s]
      (let [shape (get-in state [:shapes-by-id id])
            point (geom/get-vertex-point shape vid)
            point (gpt/add point canvas-coords)]
        (->> (align-point point)
             (rx/map #(gpt/subtract % point))
             (rx/map #(update-vertex-position id {:vid vid :delta %})))))))

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

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

(defn update-fill-attrs
  [sid {:keys [color opacity] :as opts}]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid]
                 merge
                 (when color {:fill color})
                 (when opacity {:fill-opacity opacity})))))

(defn update-font-attrs
  [sid {:keys [family style weight size align
               letter-spacing line-height] :as opts}]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid :font]
                 merge
                 (when line-height {:line-height line-height})
                 (when letter-spacing {:letter-spacing letter-spacing})
                 (when align {:align align})
                 (when family {:family family})
                 (when style {:style style})
                 (when weight {:weight weight})
                 (when size {:size size})))))

(defn update-stroke-attrs
  [sid {:keys [color opacity type width] :as opts}]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid]
                 merge
                 (when type {:stroke-type type})
                 (when width {:stroke-width width})
                 (when color {:stroke color})
                 (when opacity {:stroke-opacity opacity})))))

(defn update-radius-attrs
  [sid {:keys [rx ry] :as opts}]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid]
                 merge
                 (when rx {:rx rx})
                 (when ry {:ry ry})))))


;; --- Shape Proportions

(defn lock-proportions
  "Mark proportions of the shape locked and save the current
  proportion as additional precalculated property."
  [sid]
  {:pre [(uuid? sid)]}
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [[width height] (-> (get-in state [:shapes-by-id sid])
                               (geom/size)
                               (keep [:width :height]))
            proportion (/ width height)]
        (update-in state [:shapes-by-id sid] assoc
                   :proportion proportion
                   :proportion-lock true)))))

(defn unlock-proportions
  [sid]
  {:pre [(uuid? sid)]}
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id sid] assoc
                 :proportion-lock false))))

(defn setup-proportions
  [sid]
  {:pre [(uuid? sid)]}
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [[width height] (-> (get-in state [:shapes-by-id sid])
                               (geom/size)
                               (keep [:width :height]))
            proportion (/ width height)]
        (update-in state [:shapes-by-id sid] assoc
                   :proportion proportion)))))

;; --- Group Collapsing

(defn collapse-shape
  [id]
  {:pre [(uuid? id)]}
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id id] assoc :collapsed true))))

(defn uncollapse-shape
  [id]
  {:pre [(uuid? id)]}
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (update-in state [:shapes-by-id id] assoc :collapsed false))))

;; --- Shape Visibility

(defn hide-shape
  [sid]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (assoc-in state [:shapes-by-id sid :hidden] true))

    rs/WatchEvent
    (-apply-watch [_ state s]
      (let [shape (get-in state [:shapes-by-id sid])]
        (if-not (= (:type shape) :group)
          (rx/empty)
          (rx/from-coll
           (map hide-shape (:items shape))))))))

(defn show-shape
  [sid]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (assoc-in state [:shapes-by-id sid :hidden] false))

    rs/WatchEvent
    (-apply-watch [_ state s]
      (let [shape (get-in state [:shapes-by-id sid])]
        (if-not (= (:type shape) :group)
          (rx/empty)
          (rx/from-coll
           (map show-shape (:items shape))))))))

(defn block-shape
  [sid]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (assoc-in state [:shapes-by-id sid :blocked] true))

    rs/WatchEvent
    (-apply-watch [_ state s]
      (let [shape (get-in state [:shapes-by-id sid])]
        (if-not (= (:type shape) :group)
          (rx/empty)
          (rx/from-coll
           (map block-shape (:items shape))))))))

(defn unblock-shape
  [sid]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (assoc-in state [:shapes-by-id sid :blocked] false))

    rs/WatchEvent
    (-apply-watch [_ state s]
      (let [shape (get-in state [:shapes-by-id sid])]
        (if-not (= (:type shape) :group)
          (rx/empty)
          (rx/from-coll
           (map unblock-shape (:items shape))))))))

(defn lock-shape
  [sid]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (assoc-in state [:shapes-by-id sid :locked] true))

    rs/WatchEvent
    (-apply-watch [_ state s]
      (let [shape (get-in state [:shapes-by-id sid])]
        (if-not (= (:type shape) :group)
          (rx/empty)
          (rx/from-coll
           (map lock-shape (:items shape))))))))

(defn unlock-shape
  [sid]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (assoc-in state [:shapes-by-id sid :locked] false))

    rs/WatchEvent
    (-apply-watch [_ state s]
      (let [shape (get-in state [:shapes-by-id sid])]
        (if-not (= (:type shape) :group)
          (rx/empty)
          (rx/from-coll
           (map unlock-shape (:items shape))))))))

(defn drop-shape
  "Event used in drag and drop for transfer shape
  from one position to an other."
  [sid tid loc]
  {:pre [(not (nil? tid))
         (not (nil? sid))]}
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (stsh/drop-shape state sid tid loc))))

(defn select-first-shape
  "Mark a shape selected for drawing in the canvas."
  []
  (reify
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [page (get-in state [:workspace :page])
            id (first (get-in state [:pages-by-id page :shapes]))]
        (assoc-in state [:workspace :selected] #{id})))))

(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])
            state (if (contains? selected id)
                    (update-in state [:workspace :selected] disj id)
                    (update-in state [:workspace :selected] conj id))]
        (update-in state [:workspace :flags] conj :element-options)))))

;; --- Select Shapes

(defrecord SelectShapes [selrect]
  rs/UpdateEvent
  (-apply-update [_ state]
    (let [page (get-in state [:workspace :page])
          shapes (stsh/match-by-selrect state page selrect)]
      (assoc-in state [:workspace :selected] shapes))))

(defn select-shapes
  "Select shapes that matches the select rect."
  [selrect]
  (SelectShapes. selrect))

;; --- Update Interaction

(defrecord UpdateInteraction [shape interaction]
  udp/IPageUpdate
  rs/UpdateEvent
  (-apply-update [_ state]
    (let [id (or (:id interaction)
                 (uuid/random))
          data (assoc interaction :id id)]
      (assoc-in state [:shapes-by-id shape :interactions id] data))))

(defn update-interaction
  [shape interaction]
  (UpdateInteraction. shape interaction))

;; --- Delete Interaction

(defrecord DeleteInteracton [shape id]
  udp/IPageUpdate
  rs/UpdateEvent
  (-apply-update [_ state]
    (update-in state [:shapes-by-id shape :interactions] dissoc id)))

(defn delete-interaction
  [shape id]
  {:pre [(uuid? id) (uuid? shape)]}
  (DeleteInteracton. shape id))

;; --- Path Modifications

(defrecord UpdatePath [id index delta]
  rs/UpdateEvent
  (-apply-update [_ state]
    (update-in state [:shapes-by-id id :points index] gpt/add delta)))

(defn update-path
  "Update a concrete point in the path shape."
  [id index delta]
  {:pre [(uuid? id) (number? index) (gpt/point? delta)]}
  (UpdatePath. id index delta))

(defrecord InitialPathPointAlign [id index]
  rs/WatchEvent
  (-apply-watch [_ state s]
    (let [shape (get-in state [:shapes-by-id id])
          point (get-in shape [:points index])
          point (gpt/add point canvas-coords)]
      (->> (align-point point)
           (rx/map #(gpt/subtract % point))
           (rx/map #(update-path id index %))))))

(defn initial-path-point-align
  "Event responsible of align a specified point of the
  shape by `index` with the grid."
  [id index]
  {:pre [(uuid? id)
         (number? index)
         (not (neg? index))]}
  (InitialPathPointAlign. id index))

;; --- Start shape "edition mode"

(defrecord StartEditionMode [id]
  rs/UpdateEvent
  (-apply-update [_ state]
    (assoc-in state [:workspace :edition] id))

  rs/EffectEvent
  (-apply-effect [_ state]
    (rlocks/acquire! :shape/edition)))

(defn start-edition-mode
  [id]
  {:pre [(uuid? id)]}
  (StartEditionMode. id))

;; --- Events (implicit) (for selected)

(defrecord DeselectAll []
  rs/UpdateEvent
  (-apply-update [_ state]
    (-> state
        (assoc-in [:workspace :selected] #{})
        (assoc-in [:workspace :edition] nil)
        (assoc-in [:workspace :drawing] nil)))

  rs/EffectEvent
  (-apply-effect [_ state]
    (rlocks/release! :shape/edition)))

(defn deselect-all
  "Clear all possible state of drawing, edition
  or any similar action taken by the user."
  []
  (DeselectAll.))

(defn group-selected
  []
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [pid (get-in state [:workspace :page])
            selected (get-in state [:workspace :selected])]
        (stsh/group-shapes state selected pid)))))

(defn degroup-selected
  []
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [pid (get-in state [:workspace :page])
            selected (get-in state [:workspace :selected])]
        (stsh/degroup-shapes state selected pid)))))

;; TODO: maybe split in two separate events
(defn duplicate-selected
  []
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [selected (get-in state [:workspace :selected])]
        (stsh/duplicate-shapes state selected)))))

(defn delete-selected
  "Deselect all and remove all selected shapes."
  []
  (reify
    rs/WatchEvent
    (-apply-watch [_ state s]
      (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] (move-selected dir 1))
  ([dir n]
   {:pre [(contains? #{:up :down :right :left} dir)]}
   (reify
     rs/WatchEvent
     (-apply-watch [_ state s]
       (let [selected (get-in state [:workspace :selected])
             delta (case dir
                    :up (gpt/point 0 (- n))
                    :down (gpt/point 0 n)
                    :right (gpt/point n 0)
                    :left (gpt/point (- n) 0))]
         (rx/from-coll
          (map #(move-shape % delta) selected)))))))

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


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

(defn move-selected-layer
  [loc]
  (reify
    udp/IPageUpdate
    rs/UpdateEvent
    (-apply-update [_ state]
      (let [selected (get-in state [:workspace :selected])]
        (stsh/move-layer state selected loc)))))

;; --- Point Alignment (with Grid)

(defn align-point
  [point]
  (let [message {:cmd :grid/align :point point}]
    (->> (uw/ask! worker message)
         (rx/map :point))))
