;; 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) KALEIDOS INC Sucursal en España SL

(ns app.plugins.page
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.files.helpers :as cfh]
   [app.common.geom.point :as gpt]
   [app.common.schema :as sm]
   [app.common.types.color :as cc]
   [app.common.uuid :as uuid]
   [app.main.data.comments :as dc]
   [app.main.data.common :as dcm]
   [app.main.data.plugins :as dp]
   [app.main.data.workspace :as dw]
   [app.main.data.workspace.guides :as dwgu]
   [app.main.data.workspace.interactions :as dwi]
   [app.main.data.workspace.pages :as dwpg]
   [app.main.repo :as rp]
   [app.main.router :as-alias rt]
   [app.main.store :as st]
   [app.plugins.comments :as pc]
   [app.plugins.format :as format]
   [app.plugins.parser :as parser]
   [app.plugins.register :as r]
   [app.plugins.ruler-guides :as rg]
   [app.plugins.shape :as shape]
   [app.plugins.system-events :as se]
   [app.plugins.utils :as u]
   [app.util.object :as obj]
   [beicon.v2.core :as rx]
   [cuerdas.core :as str]
   [potok.v2.core :as ptk]))

(declare page-proxy)

(defn flow-proxy? [p]
  (obj/type-of? p "FlowProxy"))

(defn flow-proxy
  [plugin-id file-id page-id id]
  (obj/reify {:name "FlowProxy"}
    :$plugin {:enumerable false :get (fn [] plugin-id)}
    :$file {:enumerable false :get (fn [] file-id)}
    :$page {:enumerable false :get (fn [] page-id)}
    :$id {:enumerable false :get (fn [] id)}

    :page
    {:enumerable false
     :get
     (fn []
       (page-proxy plugin-id file-id page-id))}

    :name
    {:this true
     :get #(-> % u/proxy->flow :name)
     :set
     (fn [_ value]
       (cond
         (or (not (string? value)) (empty? value))
         (u/not-valid plugin-id :name value)

         :else
         (st/emit! (dwi/update-flow page-id id #(assoc % :name value)))))}

    :startingBoard
    {:this true
     :get
     (fn [self]
       (when-let [frame (-> self u/proxy->flow :starting-frame)]
         (shape/shape-proxy plugin-id file-id page-id frame)))
     :set
     (fn [_ value]
       (cond
         (not (shape/shape-proxy? value))
         (u/not-valid plugin-id :startingBoard value)

         :else
         (st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))}

    :remove
    (fn []
      (st/emit! (dwi/remove-flow page-id id)))))

(defn page-proxy? [proxy]
  (obj/type-of? proxy "PageProxy"))

(defn page-proxy
  [plugin-id file-id id]
  (obj/reify {:name "PageProxy"}
    :$plugin {:enumerable false :get (fn [] plugin-id)}
    :$file {:enumerable false :get (fn [] file-id)}
    :$id {:enumerable false :get (fn [] id)}

    :id
    {:get #(dm/str id)}

    :name
    {:this true
     :get #(-> % u/proxy->page :name)
     :set
     (fn [_ value]
       (cond
         (not (string? value))
         (u/not-valid plugin-id :name value)

         (not (r/check-permission plugin-id "content:write"))
         (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")

         :else
         (st/emit! (dw/rename-page id value))))}

    :getRoot
    (fn []
      (shape/shape-proxy plugin-id file-id id uuid/zero))

    :root
    {:this true
     :enumerable false
     :get #(.getRoot ^js %)}

    :remove
    (fn []
      (let [pages (-> (u/locate-file file-id) :data :pages)]
        (cond
          (not (r/check-permission plugin-id "content:write"))
          (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")

          (nil? (u/locate-page file-id id))
          (u/not-valid plugin-id :remove "Page not found")

          (<= (count pages) 1)
          (u/not-valid plugin-id :remove "Cannot remove the last page of the file")

          :else
          (st/emit! (dw/delete-page id)))))

    :background
    {:this true
     :get #(or (-> % u/proxy->page :background) cc/canvas)
     :set
     (fn [_ value]
       (cond
         (or (not (string? value)) (not (cc/valid-hex-color? value)))
         (u/not-valid plugin-id :background value)

         (not (r/check-permission plugin-id "content:write"))
         (u/not-valid plugin-id :background "Plugin doesn't have 'content:write' permission")

         :else
         (st/emit! (dw/change-canvas-color id {:color value}))))}

    :flows
    {:this true
     :get
     (fn [self]
       (let [flows (d/nilv (-> (u/proxy->page self) :flows) [])]
         (->> (vals flows)
              (format/format-array #(flow-proxy plugin-id file-id id (:id %))))))}

    :rulerGuides
    {:this true
     :get
     (fn [self]
       (let [guides (-> (u/proxy->page self) :guides)]
         (->> guides
              (vals)
              (filter #(nil? (:frame-id %)))
              (format/format-array #(rg/ruler-guide-proxy plugin-id file-id id (:id %))))))}


    :getShapeById
    (fn [shape-id]
      (cond
        (not (string? shape-id))
        (u/not-valid plugin-id :getShapeById shape-id)

        :else
        (let [shape-id (uuid/parse shape-id)
              shape (u/locate-shape file-id id shape-id)]
          (when (some? shape)
            (shape/shape-proxy plugin-id file-id id shape-id)))))

    :findShapes
    (fn [criteria]
      ;; Returns a lazy (iterable) of all available shapes
      (let [criteria (parser/parse-criteria criteria)
            match-criteria?
            (if (some? criteria)
              (fn [[_ shape]]
                (and
                 (or (not (:name criteria))
                     (= (str/lower (:name criteria)) (str/lower (:name shape))))

                 (or (not (:name-like criteria))
                     (str/includes? (str/lower (:name shape)) (str/lower (:name-like criteria))))

                 (or (not (:type criteria))
                     (= (:type criteria) (:type shape)))))
              identity)]
        (when (and (some? file-id) (some? id))
          (let [page (u/locate-page file-id id)
                xf (comp
                    (filter match-criteria?)
                    (map #(shape/shape-proxy plugin-id file-id id (first %))))]
            (apply array (sequence xf (:objects page)))))))

    ;; Plugin data
    :getPluginData
    (fn [key]
      (cond
        (not (string? key))
        (u/not-valid plugin-id :page-plugin-data-key key)

        :else
        (let [page (u/locate-page file-id id)]
          (dm/get-in page [:plugin-data (keyword "plugin" (str plugin-id)) key]))))

    :setPluginData
    (fn [key value]
      (cond
        (not (string? key))
        (u/not-valid plugin-id :setPluginData-key key)

        (and (some? value) (not (string? value)))
        (u/not-valid plugin-id :setPluginData-value value)

        (not (r/check-permission plugin-id "content:write"))
        (u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission")

        :else
        (st/emit! (dp/set-plugin-data file-id :page id (keyword "plugin" (str plugin-id)) key value))))

    :getPluginDataKeys
    (fn []
      (let [page (u/locate-page file-id id)]
        (apply array (keys (dm/get-in page [:plugin-data (keyword "plugin" (str plugin-id))])))))

    :getSharedPluginData
    (fn [namespace key]
      (cond
        (not (string? namespace))
        (u/not-valid plugin-id :page-plugin-data-namespace namespace)

        (not (string? key))
        (u/not-valid plugin-id :page-plugin-data-key key)

        :else
        (let [page (u/locate-page file-id id)]
          (dm/get-in page [:plugin-data (keyword "shared" namespace) key]))))

    :setSharedPluginData
    (fn [namespace key value]
      (cond
        (not (string? namespace))
        (u/not-valid plugin-id :setSharedPluginData-namespace namespace)

        (not (string? key))
        (u/not-valid plugin-id :setSharedPluginData-key key)

        (and (some? value) (not (string? value)))
        (u/not-valid plugin-id :setSharedPluginData-value value)

        (not (r/check-permission plugin-id "content:write"))
        (u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission")

        :else
        (st/emit! (dp/set-plugin-data file-id :page id (keyword "shared" namespace) key value))))

    :getSharedPluginDataKeys
    (fn [namespace]
      (cond
        (not (string? namespace))
        (u/not-valid plugin-id :page-plugin-data-namespace namespace)

        :else
        (let [page (u/locate-page file-id id)]
          (apply array (keys (dm/get-in page [:plugin-data (keyword "shared" namespace)]))))))

    :openPage
    (fn [new-window]
      (cond
        (not (r/check-permission plugin-id "content:read"))
        (u/not-valid plugin-id :openPage "Plugin doesn't have 'content:read' permission")

        (true? new-window)
        (do (st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window true))
            (js/Promise.resolve nil))

        ;; Navigating to the already-active page emits no initialization
        ;; event, so resolve right away instead of waiting forever.
        (u/page-active? id)
        (js/Promise.resolve nil)

        :else
        (js/Promise.
         (fn [resolve _]
           (->> st/stream
                (rx/filter (ptk/type? ::dwpg/initialized))
                (rx/filter #(= (deref %) id))
                (rx/take 1)
                (rx/subs! #(resolve nil)))
           (st/emit! (dcm/go-to-workspace :page-id id))))))

    :createFlow
    (fn [name frame]
      (cond
        (or (not (string? name)) (empty? name))
        (u/not-valid plugin-id :createFlow-name name)

        (not (shape/shape-proxy? frame))
        (u/not-valid plugin-id :createFlow-frame frame)

        :else
        (let [flow-id (uuid/next)]
          (st/emit!
           (dwi/add-flow flow-id id name (obj/get frame "$id"))
           (se/event plugin-id "add-flow"))
          (flow-proxy plugin-id file-id id flow-id))))

    :removeFlow
    (fn [flow]
      (cond
        (not (flow-proxy? flow))
        (u/not-valid plugin-id :removeFlow-flow flow)

        :else
        (st/emit!
         (dwi/remove-flow id (obj/get flow "$id"))
         (se/event plugin-id "remove-flow"))))

    :addRulerGuide
    (fn [orientation value board]
      (let [shape (u/proxy->shape board)]
        (cond
          (not (sm/valid-safe-number? value))
          (u/not-valid plugin-id :addRulerGuide "Value not a safe number")

          (not (contains? #{"vertical" "horizontal"} orientation))
          (u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")

          (and (some? shape)
               (or (not (shape/shape-proxy? board))
                   (not (cfh/frame-shape? shape))))
          (u/not-valid plugin-id :addRulerGuide "The shape is not a board")

          (not (r/check-permission plugin-id "content:write"))
          (u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission")

          (not (u/page-active? id))
          (u/not-valid plugin-id :addRulerGuide "Cannot modify a page that is not currently active")

          :else
          (let [ruler-id (uuid/next)]
            (st/emit!
             (-> (dwgu/update-guides
                  (d/without-nils
                   {:id       ruler-id
                    :axis     (parser/orientation->axis orientation)
                    :position value
                    :frame-id (when board (obj/get board "$id"))}))
                 (se/add-event plugin-id)))
            (rg/ruler-guide-proxy plugin-id file-id id ruler-id)))))

    :removeRulerGuide
    (fn [value]
      (cond
        (not (rg/ruler-guide-proxy? value))
        (u/not-valid plugin-id :removeRulerGuide "Guide not provided")

        (not (r/check-permission plugin-id "content:write"))
        (u/not-valid plugin-id :removeRulerGuide "Plugin doesn't have 'comment:write' permission")

        (not (u/page-active? id))
        (u/not-valid plugin-id :removeRulerGuide "Cannot modify a page that is not currently active")

        :else
        (let [guide (u/proxy->ruler-guide value)]
          (st/emit! (-> (dwgu/remove-guide guide)
                        (se/add-event plugin-id))))))

    :addCommentThread
    (fn [content position board]
      (let [shape (when board (u/proxy->shape board))
            position (parser/parse-point position)]
        (cond
          (or (not (string? content)) (empty? content))
          (u/not-valid plugin-id :addCommentThread "Content not valid")

          (or (not (sm/valid-safe-number? (:x position)))
              (not (sm/valid-safe-number? (:y position))))
          (u/not-valid plugin-id :addCommentThread "Position not valid")

          (and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape))))
          (u/not-valid plugin-id :addCommentThread "Board not valid")

          (not (r/check-permission plugin-id "comment:write"))
          (u/not-valid plugin-id :addCommentThread "Plugin doesn't have 'comment:write' permission")

          :else
          (let [position
                (cond-> position
                  (some? board)
                  (->  (update :x - (:x board))
                       (update :y - (:y board))))]
            (js/Promise.
             (fn [resolve]
               (st/emit!
                (-> (dc/create-thread-on-workspace
                     {:file-id file-id
                      :page-id id
                      :position (gpt/point position)
                      :content content}
                     (fn [data]
                       (resolve (pc/comment-thread-proxy plugin-id file-id id data)))
                     false)
                    (se/add-event plugin-id)))))))))

    :removeCommentThread
    (fn [thread]
      (cond
        (not (pc/comment-thread-proxy? thread))
        (u/not-valid plugin-id :removeCommentThread "Comment thread not valid")

        (not (r/check-permission plugin-id "comment:write"))
        (u/not-valid plugin-id :removeCommentThread "Plugin doesn't have 'content:write' permission")

        :else
        (js/Promise.
         (fn [resolve]
           (let [thread-id (obj/get thread "$id")]
             (st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve))))))))

    :findCommentThreads
    (fn [criteria]
      (let [only-yours    (boolean (obj/get criteria "onlyYours" false))
            show-resolved (boolean (obj/get criteria "showResolved" true))
            user-id       (:profile-id @st/state)]
        (js/Promise.
         (fn [resolve reject]
           (cond
             (not (r/check-permission plugin-id "comment:read"))
             (do
               (u/not-valid plugin-id :findCommentThreads "Plugin doesn't have 'comment:read' permission")
               (reject "Plugin doesn't have 'comment:read' permission"))

             :else
             (->> (rp/cmd! :get-comment-threads {:file-id file-id})
                  (rx/subs!
                   (fn [threads]
                     (let [threads
                           (cond->> threads
                             (not show-resolved)
                             (filter (comp not :is-resolved))

                             only-yours
                             (filter #(contains? (:participants %) user-id)))]
                       (resolve
                        (format/format-array
                         #(pc/comment-thread-proxy plugin-id file-id id %) threads))))
                   reject)))))))))
