;; 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) 2016 Andrey Antukh <niwi@niwi.nz>

(ns uxbox.main.data.history
  (:require [cuerdas.core :as str]
            [beicon.core :as rx]
            [lentes.core :as l]
            [potok.core :as ptk]
            [uxbox.util.router :as r]
            [uxbox.main.repo :as rp]
            [uxbox.util.i18n :refer (tr)]
            [uxbox.util.forms :as sc]
            [uxbox.main.data.pages :as udp]
            [uxbox.main.store :as st]
            [uxbox.util.time :as dt]
            [uxbox.util.data :refer (without-keys
                                     replace-by-id
                                     index-by)]))

;; --- Watch Page Changes

(declare fetch-page-history)
(declare fetch-pinned-page-history)

(deftype WatchPageChanges [id]
  ptk/WatchEvent
  (watch [_ state stream]
    (let [stopper (->> stream
                       (rx/filter #(= % ::udp/stop-page-watcher))
                       (rx/take 1))]
      (->> stream
           (rx/take-until stopper)
           (rx/filter udp/page-persisted?)
           (rx/debounce 500)
           (rx/flat-map #(rx/of (fetch-page-history id)
                                (fetch-pinned-page-history id)))))))

(defn watch-page-changes
  [id]
  (WatchPageChanges. id))

;; --- Pinned Page History Fetched

(declare update-history-index)

(defrecord PinnedPageHistoryFetched [history]
  ptk/UpdateEvent
  (update [_ state]
    (-> state
        (assoc-in [:workspace :history :pinned-items] (mapv :version history))
        (update-history-index history true))))

;; --- Fetch Pinned Page History

(defrecord FetchPinnedPageHistory [id]
  ptk/WatchEvent
  (watch [_ state s]
    (letfn [(on-success [{history :payload}]
              (->PinnedPageHistoryFetched (into [] history)))]
      (let [params {:page id :pinned true}]
        (->> (rp/req :fetch/page-history params)
             (rx/map on-success))))))

(defn fetch-pinned-page-history
  [id]
  (->FetchPinnedPageHistory id))

;; --- Page History Fetched

(defrecord PageHistoryFetched [history append?]
  ptk/UpdateEvent
  (update [_ state]
    (letfn [(update-counters [state items]
              (-> (assoc state :min-version (apply min items))
                  (assoc :max-version (apply max items))))

            (update-lists [state items]
              (if append?
                (update state :items #(reduce conj %1 items))
                (assoc state :items items)))]

      (let [items (mapv :version history)
            hstate (-> (get-in state [:workspace :history] {})
                       (update-counters items)
                       (update-lists items))]
        (-> state
            (assoc-in [:workspace :history] hstate)
            (update-history-index history append?))))))

;; --- Fetch Page History

(defrecord FetchPageHistory [id since max]
  ptk/WatchEvent
  (watch [_ state s]
    (letfn [(on-success [{history :payload}]
              (let [history (into [] history)]
                (->PageHistoryFetched history (not (nil? since)))))]
      (let [params (merge
                    {:page id :max (or max 15)}
                    (when since {:since since}))]
        (->> (rp/req :fetch/page-history params)
             (rx/map on-success))))))

(defn fetch-page-history
  ([id]
   (fetch-page-history id nil))
  ([id params]
   (map->FetchPageHistory (assoc params :id id))))

;; --- Select Page History

(defrecord SelectPageHistory [version]
  ptk/UpdateEvent
  (update [_ state]
    (let [item (get-in state [:workspace :history :by-version version])
          page (get-in state [:pages (:page item)])
          page (assoc page
                      :history true
                      :data (:data item))]
      (-> state
          (udp/assoc-page page)
          (assoc-in [:workspace :history :selected] version)))))

(defn select-page-history
  [version]
  (SelectPageHistory. version))

;; --- Apply selected history

(defrecord ApplySelectedHistory [id]
  ptk/UpdateEvent
  (update [_ state]
    (-> state
        (update-in [:pages id] dissoc :history)
        (assoc-in [:workspace :history :selected] nil)))

  ptk/WatchEvent
  (watch [_ state s]
    (rx/of (udp/persist-page id))))

(defn apply-selected-history
  [id]
  (ApplySelectedHistory. id))

;; --- Deselect Page History

(defrecord DeselectPageHistory [id]
  ptk/UpdateEvent
  (update [_ state]
    (let [packed (get-in state [:packed-pages id])]
      (-> (udp/assoc-page state packed)
          (assoc-in [:workspace :history :deselecting] true)
          (assoc-in [:workspace :history :selected] nil))))

  ptk/WatchEvent
  (watch [_ state s]
    (->> (rx/of #(assoc-in % [:workspace :history :deselecting] false))
         (rx/delay 500))))

(defn deselect-page-history
  [id]
  (DeselectPageHistory. id))

;; --- History Item Updated

(defrecord HistoryItemUpdated [item]
  ptk/UpdateEvent
  (update [_ state]
    (-> state
        (update-in [:workspace :history :items] replace-by-id item)
        (update-in [:workspace :history :pinned-items] replace-by-id item))))

(defn history-updated?
  [item]
  (instance? HistoryItemUpdated item))

(defn history-updated
  [item]
  (HistoryItemUpdated. item))

;; --- Refresh Page History

(defrecord RefreshPageHistory [id]
  ptk/WatchEvent
  (watch [_ state s]
    (let [history (get-in state [:workspace :history])
          maxitems (count (:items history))]
      (rx/of (fetch-page-history id {:max maxitems})
             (fetch-pinned-page-history id)))))

(defn refres-page-history
  [id]
  (RefreshPageHistory. id))

;; --- Update History Item

(defrecord UpdateHistoryItem [item]
  ptk/WatchEvent
  (watch [_ state s]
    (letfn [(on-success [{item :payload}]
              (->HistoryItemUpdated item))]
      (rx/merge
       (->> (rp/req :update/page-history item)
            (rx/map on-success))
       (->> (rx/filter history-updated? s)
            (rx/take 1)
            (rx/map #(refres-page-history (:page item))))))))

(defn update-history-item
  [item]
  (UpdateHistoryItem. item))

;; --- Forward to Next Version

(defrecord ForwardToNextVersion []
  ptk/WatchEvent
  (watch [_ state s]
    (let [workspace (:workspace state)
          history (:history workspace)
          version (:selected history)]
      (cond
        (nil? version)
        (rx/empty)

        (>= (:max-version history) (inc version))
        (rx/of (select-page-history (inc version)))

        (> (inc version) (:max-version history))
        (rx/of (deselect-page-history (:page workspace)))

        :else
        (rx/empty)))))

(defn forward-to-next-version
  []
  (ForwardToNextVersion.))

;; --- Backwards to Previous Version

(defrecord BackwardsToPreviousVersion []
  ptk/WatchEvent
  (watch [_ state s]
    (let [workspace (:workspace state)
          history (:history workspace)
          version (:selected history)]
      (cond
        (nil? version)
        (let [maxv (:max-version history)]
          (rx/of (select-page-history maxv)))

        (pos? (dec version))
        (if (contains? (:by-version history) (dec version))
          (rx/of (select-page-history (dec version)))
          (let [since (:min-version history)
                page (:page workspace)
                params {:since since}]
            (rx/of (fetch-page-history page params)
                   (select-page-history (dec version)))))

        :else
        (rx/empty)))))

(defn backwards-to-previous-version
  []
  (BackwardsToPreviousVersion.))

;; --- Helpers

(defn- update-history-index
  [state history append?]
  (let [index (index-by history :version)]
    (if append?
      (update-in state [:workspace :history :by-version] merge index)
      (assoc-in state [:workspace :history :by-version] index))))

