(ns stone.contract
  (:require
   [wisp.runtime :refer [identity odd? even? dictionary? dictionary
                         keys vals key-values merge satisfies?
                         contains-vector? map-dictionary error?
                         string? number? date? boolean? re-pattern?
                         object? nil? true? false? re-find re-matches
                         re-pattern inc dec str char int subs and or
                         print to-string = max min fn? vector?]]
   [wisp.ast :refer [symbol symbol? keyword? keyword-name name
                     namespace gensym unquote? unquote-splicing?
                     quote? syntax-quote? quote-string pr-str
                     meta with-meta]]
   [wisp.sequence :refer [lazy-seq lazy-seq? list? list cons
                          sequential? reverse map filter reduce
                          count empty? first second third rest
                          last butlast take take-while drop conj
                          assoc concat seq seq? vec sort repeat
                          every? some partition interleave nth]]
   [wisp.string :refer [split split-lines join upper-case lower-case
                        capitalize pattern-escape replace-first replace
                        blank? reverse triml trimr trim]
                        :rename {reverse string-reverse}]
   [wisp.expander :refer [macroexpand]]
   [Immutable :refer [List OrderedSet Map Seq]]))

(def contracts {:local "0x731a10897d267e19B34503aD902d0A29173Ba4B1"
                :kovan "0x83C2DE48060aDBBB0c57226eceb8E242c3779884"})
(def contract-addr (:local contracts))
(def web3 nil)
(def contract nil)
(def storage nil)
(def Stone nil)
(def abi [{:type "function"
           :name "storeFiles"
           :constant false
           :payable false
           :stateMutability "nonpayable"
           :inputs [{:name "project" :type "string"}
                    {:name "count" :type "uint256"}
                    {:name "manifestHash" :type "bytes32"}
                    {:name "manifestKey" :type "bytes32"}
                    {:name "hashes" :type "bytes32[]"}
                    {:name "lengths" :type "uint32[]"}
                    {:name "data" :type "bytes"}]
           :outputs []}
          {:type "event"
           :name "Manifest"
           :anonymous false
           :inputs [{:indexed true :name "owner" :type "address"}
                    {:indexed true :name "project" :type "string"}
                    {:indexed true :name "count" :type "uint256"}
                    {:indexed false :name "hash" :type "bytes32"}
                    {:indexed false :name "key" :type "bytes32"}]}
          {:type "event"
           :name "File"
           :anonymous false
           :inputs [{:indexed true :name "owner" :type "address"}
                    {:indexed true :name "hash" :type "bytes32"}
                    {:indexed false :name "data" :type "bytes"}]}])
(def known-hashes (Set.))

(defn config
  [options]
  (set! web3 window.web3)
  (-> (web3.eth.net.getId)
      (.then (fn [id]
               (let [type (if (== id 42) :kovan :local)]
                 (console.log "USING NETWORK: " type)
                 (set! contract-addr (get contracts type))
                 (set! Stone (:stone options))
                 (set! storage (:storage options))
                 (set! contract (web3.eth.Contract. abi contract-addr {:from (:account storage)}))
                 (console.log "Using contract" contract)
                 (fetch-hashes))))))

(defn fetch-hashes
  "Fetch hashes so we don't store duplicate data"
  []
  (-> (contract.getPastEvents "File" {:filter {:owner (:account storage)}
                                      :fromBlock "0x0"
                                      :toBlock "latest"})
      (.then (fn [events]
               (doseq [event events]
                 (known-hashes.add event.returnValues.hash))
               (console.log "Hashes" known-hashes)))))

(defn has-version
  [version]
  (known-hashes.has version))

(defn store
  [project]
  (let [hashes []
        lengths []
        bytes []
        manifest (:currentManifest project)]
    (if (has-version (:hash manifest))
      (do
        (console.log "This version of the project is already stored")
        (Promise.resolve "already stored"))
      (do
        (console.log "storing project" project)
        (store-file-params (:hash manifest) (:data manifest) hashes lengths bytes)
        (doseq [file (Stone.all-file-data project)]
          (store-file-params (:hash file) (:data file) hashes lengths bytes))
        (-> (contract.methods.storeFiles (:name project) (:count manifest) (:hash manifest) (:key manifest) hashes lengths (str "0x" (.join bytes "")))
            (.send)
            (.then (fn [receipt]
                     (console.log "RECEIPT" receipt)
                     (doseq [hash hashes] (known-hashes.add hash))))
            (.catch (fn [err] (console.log "ERROR" err))))))))

(defn store-file-params
  "hash: hex hash of file
   data: hex data of file
   hashes: array of file hashes
   lengths: array of file lengths
   bytes: array of file data (without '0x' prefix)"
  [hash data hashes lengths bytes]
  (if (not (has-version hash))
    (do
      (.push hashes hash)
      (.push lengths (/ (- (.-length data) 2) 2))
      (.push bytes (subs data 2)))))

(defn fetch-file-version
  [account hash]
  (let [event nil]
    (-> (contract.getPastEvents "File"
                                {:fromBlock 0
                                 :toBlock "latest"
                                 :filter {:hash hash :owner account}})
        (.then (fn [events]
                 (set! event (first events))
                 (if (and event (== (.toUpperCase event.return-values.owner) (.toUpperCase account)))
                   (web3.eth.getBlock event.block-number)
                   (throw (Error. "Could not retrieve file")))))
        (.then (fn [block]
                 {:date (Date. (* block.timestamp 1000))
                  :data event.return-values.data})))))
