;; 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>

(ns uxbox.main.data.workspace
  (:require [cljs.spec :as s]
            [beicon.core :as rx]
            [potok.core :as ptk]
            [lentes.core :as l]
            [uxbox.main.store :as st]
            [uxbox.main.constants :as c]
            [uxbox.main.lenses :as ul]
            [uxbox.main.workers :as uwrk]
            [uxbox.main.data.projects :as dp]
            [uxbox.main.data.pages :as udp]
            [uxbox.main.data.shapes :as uds]
            [uxbox.main.data.icons :as udi]
            [uxbox.main.data.shapes-impl :as shimpl]
            [uxbox.main.data.lightbox :as udl]
            [uxbox.main.data.history :as udh]
            [uxbox.main.data.workspace.scroll :as wscroll]
            [uxbox.util.uuid :as uuid]
            [uxbox.util.spec :as us]
            [uxbox.util.forms :as sc]
            [uxbox.util.geom.point :as gpt]
            [uxbox.util.time :as dt]
            [uxbox.util.math :as mth]
            [uxbox.util.data :refer (index-of)]))

;; --- Expose inner functions

(def start-viewport-positioning wscroll/start-viewport-positioning)
(def stop-viewport-positioning wscroll/stop-viewport-positioning)

;; --- Initialize Workspace

(declare initialize-alignment)

(defrecord InitializeWorkspace [project page]
  ptk/UpdateEvent
  (update [_ state]
    (if (:workspace state)
      (update state :workspace merge
              {:project project
               :page page
               :selected #{}
               :drawing nil
               :tooltip nil})
      (assoc state :workspace
             {:project project
              :zoom 1
              :page page
              :flags #{:sitemap :drawtools :layers :element-options}
              :selected #{}
              :drawing nil
              :tooltip nil})))

  ptk/WatchEvent
  (watch [_ state s]
    (let [page-id page
          page (get-in state [:pages page-id])]

      ;; Activate loaded if page is not fetched.
      (when-not page (reset! st/loader true))

      (rx/merge
       (if page
         (rx/of (initialize-alignment page-id))
         (rx/merge
          (rx/of (udp/fetch-pages project))
          (->> (rx/filter udp/pages-fetched? s)
               (rx/take 1)
               (rx/do #(reset! st/loader false))
               (rx/map #(initialize-alignment page-id)))))

       ;; Initial history loading
       (rx/of
        (udh/fetch-page-history page-id)
        (udh/fetch-pinned-page-history page-id)))))

  ptk/EffectEvent
  (effect [_ state stream]
    ;; Optimistic prefetch of projects if them are not already fetched
    (when-not (seq (:projects state))
      (st/emit! (dp/fetch-projects)))))

(defn initialize
  "Initialize the workspace state."
  [project page]
  (InitializeWorkspace. project page))

;; --- Select for Drawing

(deftype SelectForDrawing [shape]
  ptk/UpdateEvent
  (update [_ state]
    (let [current (l/focus ul/selected-drawing state)]
      (if (or (nil? shape)
              (= shape current))
        (update state :workspace dissoc :drawing)
        (assoc-in state [:workspace :drawing] shape)))))

(defn select-for-drawing
  [shape]
  (SelectForDrawing. shape))

;; --- Workspace Flags

(deftype ActivateFlag [flag]
  ptk/UpdateEvent
  (update [_ state]
    (update-in state [:workspace :flags] conj flag)))

(defn activate-flag
  [flag]
  {:pre [(keyword? flag)]}
  (ActivateFlag. flag))

(deftype DeactivateFlag [flag]
  ptk/UpdateEvent
  (update [_ state]
    (update-in state [:workspace :flags] disj flag)))

(defn deactivate-flag
  [flag]
  {:pre [(keyword? flag)]}
  (DeactivateFlag. flag))

(deftype ToggleFlag [flag]
  ptk/WatchEvent
  (watch [_ state stream]
    (let [flags (get-in state [:workspace :flags])]
      (if (contains? flags flag)
        (rx/of (deactivate-flag flag))
        (rx/of (activate-flag flag))))))

(defn toggle-flag
  [flag]
  (ToggleFlag. flag))

;; --- Icons Toolbox

(deftype SelectIconsToolboxCollection [id]
  ptk/UpdateEvent
  (update [_ state]
    (assoc-in state [:workspace :icons-toolbox] id))

  ptk/WatchEvent
  (watch [_ state stream]
    (rx/of (udi/fetch-icons id))))

(defn select-icons-toolbox-collection
  [id]
  {:pre [(or (nil? id) (uuid? id))]}
  (SelectIconsToolboxCollection. id))

(deftype InitializeIconsToolbox []
  ptk/UpdateEvent
  (update [_ state]
    state)

  ptk/WatchEvent
  (watch [_ state stream]
    (letfn [(get-first-with-icons [colls]
              (->> (sort-by :name colls)
                   (filter #(> (:num-icons %) 0))
                   (first)
                   (:id)))
            (on-fetched [event]
              (let [coll (get-first-with-icons @event)]
                (println "first" coll)
                (select-icons-toolbox-collection coll)))]
      (rx/merge
       (rx/of (udi/fetch-collections)
              (udi/fetch-icons nil))

       ;; Only perform the autoselection if it is not
       ;; previously already selected by the user.
       (when-not (contains? (:workspace state) :icons-toolbox)
         (->> stream
              (rx/filter udi/collections-fetched?)
              (rx/take 1)
              (rx/map on-fetched)))))))

(defn initialize-icons-toolbox
  []
  (InitializeIconsToolbox.))

;; --- Clipboard Management

(defrecord CopyToClipboard []
  ptk/UpdateEvent
  (update [_ state]
    (let [selected (get-in state [:workspace :selected])
          item {:id (uuid/random)
                :created-at (dt/now)
                :items selected}
          clipboard (-> (:clipboard state)
                        (conj item))]
      (assoc state :clipboard
             (if (> (count clipboard) 5)
               (pop clipboard)
               clipboard)))))

(defn copy-to-clipboard
  "Copy selected shapes to clipboard."
  []
  (CopyToClipboard.))

(defrecord PasteFromClipboard [id]
  ptk/UpdateEvent
  (update [_ state]
    (let [page (get-in state [:workspace :page])
          selected (if (nil? id)
                     (first (:clipboard state))
                     (->> (:clipboard state)
                          (filter #(= id (:id %)))
                          (first)))]
      (shimpl/duplicate-shapes state (:items selected) page))))

(defn paste-from-clipboard
  "Copy selected shapes to clipboard."
  ([] (PasteFromClipboard. nil))
  ([id] (PasteFromClipboard. id)))

;; --- Zoom Management

(deftype IncreaseZoom []
  ptk/UpdateEvent
  (update [_ state]
    (let [increase #(nth c/zoom-levels
                         (+ (index-of c/zoom-levels %) 1)
                         (last c/zoom-levels))]
      (update-in state [:workspace :zoom] (fnil increase 1)))))

(defn increase-zoom
  []
  (IncreaseZoom.))

(deftype DecreaseZoom []
  ptk/UpdateEvent
  (update [_ state]
    (let [decrease #(nth c/zoom-levels
                         (- (index-of c/zoom-levels %) 1)
                         (first c/zoom-levels))]
      (update-in state [:workspace :zoom] (fnil decrease 1)))))

(defn decrease-zoom
  []
  (DecreaseZoom.))

(deftype ResetZoom []
  ptk/UpdateEvent
  (update [_ state]
    (assoc-in state [:workspace :zoom] 1)))

(defn reset-zoom
  []
  (ResetZoom.))

;; --- Tooltips

(defrecord SetTooltip [text]
  ptk/UpdateEvent
  (update [_ state]
    (assoc-in state [:workspace :tooltip] text)))

(defn set-tooltip
  [text]
  (SetTooltip. text))

;; --- Grid Alignment

(declare initialize-alignment?)

(defrecord InitializeAlignment [id]
  ptk/WatchEvent
  (watch [_ state stream]
    (let [page (get-in state [:pages id])
          opts (:metadata page)
          params {:width c/viewport-width
                  :height c/viewport-height
                  :x-axis (:grid-x-axis opts c/grid-x-axis)
                  :y-axis (:grid-y-axis opts c/grid-y-axis)}
          stoper (->> (rx/filter initialize-alignment? stream)
                      (rx/take 1))]
      (->> (rx/just nil)
           (rx/delay 1000)
           (rx/take-until stoper)
           (rx/flat-map (fn [_]
                          (rx/merge (->> (uwrk/initialize-alignment params)
                                         (rx/map #(activate-flag :grid-indexed)))
                                    (when (:grid-alignment opts)
                                      (rx/of (activate-flag :grid-alignment))))))))))

(defn initialize-alignment?
  [v]
  (instance? InitializeAlignment v))

(defn initialize-alignment
  [id]
  (InitializeAlignment. id))

;; --- Update Metadata

;; Is a workspace aware wrapper over uxbox.data.pages/UpdateMetadata event.

(defrecord UpdateMetadata [id metadata]
  ptk/WatchEvent
  (watch [_ state s]
    (rx/of (udp/update-metadata id metadata)
           (initialize-alignment id))))

(defn update-metadata
  [id metadata]
  {:pre [(uuid? id) (us/valid? ::udp/metadata metadata)]}
  (UpdateMetadata. id metadata))
