             [konserve.core :refer [go-locked]]
             [clojure.java.io :as io]
             [hasch.core :refer [uuid]]
-
             [clojure.core.async :as async
              :refer [<!! <! >! timeout chan alt! go go-loop close! put!]]
             [clojure.edn :as edn]

          fc (FileChannel/open p (into-array OpenOption []))]
      (.force fc true)
      (.close fc))))


(defn delete-store
  "Permanently deletes the folder of the store with all files."

      (catch Exception e
        nil))))

(defn list-keys
  "Lists all keys in this binary store. This operation *does not block concurrent operations* and might return an outdated key set. Keys of binary blobs are not tracked atm."
  [{:keys [folder serializer read-handlers ] :as store}]
  (let [fns (->> (io/file (str folder "/Key"))
                 .list
                 seq
                 (filter #(re-matches #"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
                                      %))
                 (map (fn [fn]
                        (go-locked
                         store fn
                         (let [f  (io/file (str folder "/Key/" fn))
                               fd (io/file (str folder "/Data/" fn))]
                           (if (and (.exists f) (.exists fd))
                             (let [fis (DataInputStream. (FileInputStream. f))]
                               (try
                                 (-deserialize serializer read-handlers fis)
                                 (catch Exception e
                                   (ex-info "Could not read key."
                                            {:type      :read-error
                                             :key       fn
                                             :exception e}))
                                 (finally
                                   (.close fis))))
                             (println "Stale key file detected: " fn))))))
                 async/merge
                 (async/into #{}))]
    fns))



;; Helper Function for FileSystemStore
(defn- read-key
  "help function for -update-in"
  [folder f fkey serializer read-handlers]
    (when (.exists f)
      (let [fis (DataInputStream. (FileInputStream. f))]
        (try
          (-deserialize serializer read-handlers fis)
          (catch Exception e
            (ex-info "Could not read key."
                     {:type      :read-error
                      :key       fkey
                      :exception e}))
          (finally
            (.close fis))))))

(defn- write-edn
  "help function for -update-in"
  [serializer write-handlers read-handlers folder key-vec fn up-fn config]
  (let [[fkey & rkey] key-vec
        f             (io/file (str folder fn))
        old           (read-key folder f fkey serializer read-handlers)
        new-file      (io/file (str folder fn ".new"))
        fos           (FileOutputStream. new-file)
        dos           (DataOutputStream. fos)
        fd            (.getFD fos)
        value           (if-not (empty? rkey)
                        (update-in old rkey up-fn)
                        (up-fn old))]
    (if (instance? Throwable old)
      old ;; return read error
      (try
        (-serialize serializer dos write-handlers value)
        (.flush dos)
        (when (:fsync config)
          (.sync fd))
        (.close dos)
        (Files/move (.toPath new-file) (.toPath f)
                    (into-array [StandardCopyOption/ATOMIC_MOVE]))
        (when (:fsync config)
          (sync-folder folder))
        [(get-in old rkey)
         (get-in value rkey)]
        (catch Exception e
          (.delete new-file)
          ;; TODO maybe need fsync new-file here?
          (throw 
           (ex-info "Could not write key."
                    {:type      :write-error
                     :key       fkey
                     :exception e})))
        (finally
          (.close dos))))))


(defn- write-edn-key
  "help function for -update-in"
  [serializer write-handlers read-handlers folder fn {:keys [key] :as key-meta} config]
  (let [f             (io/file (str folder fn))
        new-file      (io/file (str folder fn ".new"))
        fos           (FileOutputStream. new-file)
        dos           (DataOutputStream. fos)
        fd            (.getFD fos)]
    (try
      (-serialize serializer dos write-handlers key-meta)
      (.flush dos)
      (when (:fsync config)
        (.sync fd))
      (.close dos)
      (Files/move (.toPath new-file) (.toPath f)
                  (into-array [StandardCopyOption/ATOMIC_MOVE]))
      (when (:fsync config)
        (sync-folder folder))
      (catch Exception e
        (.delete new-file)
        ;; TODO maybe need fsync new-file here?
        (throw 
         (ex-info "Could not write key file."
                  {:type      :write-key-error
                   :key       key
                   :exception e})))
      (finally
        (.close dos)))))

(defn- read-memory-entry
  "Helper Function for -get-in"
  [f res-ch folder fn fkey rkey serializer read-handlers]
  (if-not (.exists f)
    (close! res-ch)
    (try
      (let [ac (AsynchronousFileChannel/open (.getPath (FileSystems/getDefault)
                                                       (str folder "/Data/" fn)
                                                       (into-array String []))
                                             (into-array StandardOpenOption
                                                         [StandardOpenOption/READ]))
            bb (ByteBuffer/allocate (.size ac))]
        (.read ac
               bb
               0
               nil
               (proxy [CompletionHandler] []
                 (completed [res att]
                   (let [bais (ByteArrayInputStream. (.array bb))]
                     (try
                       (put! res-ch
                             (-deserialize serializer read-handlers bais))
                       (catch Exception e
                         (ex-info "Could not read key."
                                  {:type      :read-error
                                   :key       fkey
                                   :exception e}))
                       (finally
                         (.close ac)))))
                 (failed [t att]
                   (put! res-ch (ex-info "Could not read key."
                                         {:type      :read-error
                                          :key       fkey
                                          :exception t}))
                   (.close ac)))))
      (catch Exception e
        (put! res-ch (ex-info "Could not read key."
                              {:type      :read-error
                               :key       fkey
                               :exception e}))))))

(defn- delete-entry
  "Delete Filestore Entry"
  [fn folder config]
  (let [file (io/file (str folder "/" fn))]
    (.delete file)
    (when (:fsync config)
      (sync-folder folder))
    nil))

(defn- binary-read [f res-ch folder fn key locked-cb]
  (if-not (.exists f)
    (close! res-ch)
    (try
      (let [ac (AsynchronousFileChannel/open (.getPath (FileSystems/getDefault)
                                                       (str folder "/Data/" fn)
                                                       (into-array String []))
                                             (into-array StandardOpenOption
                                                         [StandardOpenOption/READ]))
            bb (ByteBuffer/allocate (.size ac))]
        (.read ac
               bb
               0
               nil
               (proxy [CompletionHandler] []
                 (completed [res att]
                   (let [bais (ByteArrayInputStream. (.array bb))]
                     (try
                       (locked-cb {:input-stream bais
                                   :size         (.length f)
                                   :file         f})
                       (catch Exception e
                         (ex-info "Could not read key."
                                  {:type      :read-error
                                   :key       key
                                   :exception e}))
                       (finally
                         (close! res-ch)
                         (.close ac)))))
                 (failed [t att]
                   (put! res-ch (ex-info "Could not read key."
                                         {:type      :read-error
                                          :key       key
                                          :exception t}))
                   (close! res-ch)
                   (.close ac)))))
      (catch Exception e
        (put! res-ch (ex-info "Could not read key."
                              {:type      :read-error
                               :key       key
                               :exception e}))))))

(defn- write-binary
  "Helper Function for Binary Write"
  [folder fn key input config]
  (let [f        (io/file (str folder "/Data/" fn))
        new-file (io/file (str folder "/Data/" fn ".new"))
        fos      (FileOutputStream. new-file)
        dos      (DataOutputStream. fos)
        fd       (.getFD fos)]
    (try
      (io/copy input dos)
      (.flush dos)
      (when (:fsync config)
        (.sync fd))
      (.close fos) ;; required for windows
      (Files/move (.toPath new-file) (.toPath f)
                  (into-array [StandardCopyOption/ATOMIC_MOVE]))
      (when (:fsync config)
        (sync-folder folder))
      (catch Exception e
        (.delete new-file)
        (ex-info "Could not write key."
                 {:type      :write-error
                  :key       key
                  :exception e}))
      (finally
        (.close fos)))))

(defrecord FileSystemStore [folder serializer read-handlers write-handlers locks config]
  PEDNAsyncKeyValueStore
  (-exists? [this key]
    (let [fn  (uuid key)
          f   (io/file (str folder "/Data/" fn))
          res (chan)]
      (put! res (.exists f))
      (close! res)
      res))
  ;; non-blocking async version
  (-get-in [this key-vec]
    (let [[fkey & rkey] key-vec
          fn            (uuid fkey)
          f             (io/file (str folder "/Data/" fn))
          res-ch        (chan)]
      (read-memory-entry f res-ch folder fn fkey rkey serializer read-handlers)
      res-ch))
  (-update-in [this key-vec up-fn]
    (async/thread
      (try
        (let [file-name   (uuid (first key-vec))
              data-folder (str folder "/Data/")
              key-folder  (str folder "/Key/")]
          (write-edn-key serializer write-handlers read-handlers key-folder file-name {:key (first key-vec) :format :edn} config)
          (write-edn serializer write-handlers read-handlers data-folder key-vec file-name up-fn config))
        (catch Exception e
          e))))

  (-assoc-in [this key-vec val] (-update-in this key-vec (fn [_] val)))

  (-dissoc [this key]
    (async/thread
      (let [fn          (uuid key)
            key-folder  (str folder "/Key")
            data-folder (str folder "/Data")]
          (delete-entry fn key-folder config)
          (delete-entry fn data-folder config))))

  PBinaryAsyncKeyValueStore
  (-bget [this key locked-cb]
    (let [fn     (str (uuid key))
          f      (io/file (str folder "/Data/" fn))
          res-ch (chan)]
      (binary-read f res-ch folder fn key locked-cb)
      res-ch))

  (-bassoc [this key input]
    (let [file-name    (uuid key)
          key-folder   (str folder "/Key/")]
      (async/thread
        (do (write-edn-key serializer write-handlers read-handlers key-folder file-name {:key key :format :binary} config)
            (write-binary folder (str file-name) key input config))))))

(defmethod print-method FileSystemStore
  [store writer]
  (.write writer (str "FileStore[\"" (:folder store ) ", " (.hasheq store) "\"]")))

(defn- check-and-create-folder [path]
  (let [f         (io/file path)
        test-file (io/file (str path "/" (java.util.UUID/randomUUID)))]
        (when-not (.exists f)
          (.mkdir f))
        ;; simple test to ensure we can write to the folders
        (when-not (.createNewFile test-file)
          (throw (ex-info "Cannot write to folder." {:type   :not-writable
                                                     :folder path})))
        (.delete test-file)))

(defn filestore-schema-update
  "Lists all keys in this binary store. This operation *does not block concurrent operations* and might return an outdated key set. Keys of binary blobs are not tracked atm."
  [{:keys [folder serializer read-handlers] :as store}]
  (let [fns (->> (io/file folder)
                 .list
                 seq
                 ((fn [e] (prn "foo: " e) e))
                 (filter #(re-matches #"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
                                      %))
                 (map (fn [fn]
