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

(ns app.plugins.reflow
  "Promise adapter for the plugin `waitForLayoutUpdate` methods. Owns the
  argument validation, the default deadline and the rejection shape; the
  workspace only reports when its pending work has drained."
  (:require
   [app.common.data.macros :as dm]
   [app.common.files.helpers :as cfh]
   [app.common.uuid :as uuid]
   [app.main.data.workspace.reflow :as wrf]
   [beicon.v2.core :as rx]))

;; Ceiling for callers that pass no timeout, so a pipeline that never drains
;; its marks rejects the promise rather than leaving it unsettled.
(def ^:private default-timeout 30000)

;; Largest value a signed 32-bit timer accepts.
(def ^:private max-timeout 2147483647)

(defn- valid-timeout?
  "Checks that a plugin timeout fits a signed 32-bit timer."
  [value]
  (or (nil? value)
      (and (number? value)
           (pos? value)
           (<= value max-timeout)
           (js/Number.isFinite value))))

(defn- reject-invalid!
  [reject value]
  (let [msg (dm/str "[PENPOT PLUGIN] Value not valid: " value
                    ". Code: " :waitForLayoutUpdate)]
    (.error js/console msg)
    (reject (js/Error. msg))))

(defn shape-wait-ids
  "Ids a per-shape wait covers: the shape subtree, its ancestors, and the file
  its components sync from."
  [objects file-id id]
  (-> (into #{} (cfh/get-children-ids-with-self objects id))
      (into (cfh/get-parent-ids objects id))
      (conj file-id)
      (disj uuid/zero)))

(defn wait-for-layout-update
  "Returns a JS Promise that resolves once every id in `ids` has drained from
  the workspace pending map. A nil `ids` waits for every pending id; an empty
  one has nothing to wait for and resolves right away.

  The promise is rejected when `timeout` (ms) is not a valid timer value, or
  when it elapses first; a nil `timeout` uses `default-timeout`."
  ([timeout]
   (wait-for-layout-update nil timeout))
  ([ids timeout]
   (js/Promise.
    (fn [resolve reject]
      (if-not (valid-timeout? timeout)
        (reject-invalid! reject timeout)
        ;; Race the settle signal against the deadline; the loser is
        ;; unsubscribed. `settled` replays on subscribe, so an already drained
        ;; map wins even against a 1ms deadline.
        (->> (rx/race (->> (rx/of :timeout)
                           (rx/delay (or timeout default-timeout)))
                      (->> (wrf/settled ids)
                           (rx/map (constantly :ok))))
             (rx/take 1)
             (rx/subs!
              (fn [value]
                (if (= value :timeout)
                  (reject (js/Error. "waitForLayoutUpdate timeout"))
                  (resolve)))
              reject)))))))
