(ns stone.main
  (: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.engine.browser :refer [evaluate]]
   [wisp.expander :refer [macroexpand]]
   [jsyaml]
   [stone.init :refer [zip find ->vec Fork]]
   [stone.eval :refer [bind-eval]]
   [stone.contract :as Contract]
   [stone.crypto :as Crypto]
   [Immutable :refer [List OrderedSet Map Seq]]
   [Handlebars :refer [compile]]
   ;;[Bluebird :refer [Promise]]
   [EthereumjsUtil]
   [TreeView]
   [_]
   [jQuery :as $]))

;;(def parity window.parity.api)
(def parity nil)
(def infura? false)
(def char-a (.charCodeAt "a" 0))
(def char-A (.charCodeAt "A" 0))
(def char-0 (.charCodeAt "0" 0))
(def account nil)
;;(def public-key nil)
(def buf-constructor (.-constructor (EthereumjsUtil.toBuffer "hello")))
(def project-uis {})
(def storage (or (and localStorage.stone (JSON.parse localStorage.stone))
                 {:projects {}
                  :identities {}
                  :contacts {}}))
(def project-guis {})
(def selected-file nil)
(def redisplay-file-contents (fn []))
(def id-counter -1)
(def whisper-topic nil)
(def disable-persistence false)
(def file-key nil)
(def encrypted-file-key nil)
(def dialog-ok-action (fn []))
(def dialog-cancel-action (fn []))
(def dialog nil)
(def listening nil)
(def private-sym-key nil) ;; key computed from account used to encrypt manifests
(def identities {})
(def current-ecdh nil)
(def requests {})
(def hex-pattern (RegExp. "^0x[0-9a-fA-F]"))

;;; PROJECTS

(defn make-manifest
  []
  {:hash nil
   :count -1
   :key nil
   :files {}   ; hash -> {:name "" :key ""}
   :history [] ; {:hash "" :key "" :count N}
   })

(defn make-project
  [name]
  (let [project {:name name
                 :count 0
                 :manifests []
                 :currentManifest (make-manifest)
                 :latestStoredManifest nil}]
    (set! (get (:projects storage) name) project)
    (save-storage)
    (add-project project)))

(defn add-project
  [project]
    (let [proj (-> ($ "#project-template")
                  (.clone)
                  (.removeAttr "id")
                  (.appendTo "#projects"))
          push-files-button (-> proj (.find "[name='push-files-button']"))
          clear-files-button (-> proj (.find "[name='clear-files-button']"))
          name (-> proj (.find "[name='project-name']"))
          dnd (-> proj (.find "[name='file-drop']"))
          file-title (-> proj (.find "[name='file-title']"))
          project-ui {:redisplayFileContents (fn [])
                      :pushFilesButton push-files-button}
          accept-request-field (-> proj (.find "[name='accept-request-field']"))]
      (set! (get project-uis (:name project)) project-ui)
      (-> push-files-button
          (.on "click" (fn [] (push-files project proj project-ui)))
          (.tooltip {:disabled true
                     :items push-files-button
                     :content "<b>Pushing files</b>"}))
      (-> clear-files-button (.on "click" (fn [] (clear-files project proj project-ui))))
      (-> name (.html (:name project)))
      (bind-dnd project dnd proj project-ui)
      (populate-file-table project proj project-ui)
      (if (Contract.has-version (:hash (:currentManifest project)))
        (disable-push-files project-ui))
      (-> accept-request-field
          (.on "paste" (fn [e] (accept-request e project proj accept-request-field)))
          (.tooltip {:disabled true
                     :items accept-request-field
                     :content "<b>Request Accepted</b>"}))))

(defn accept-request
  [e project proj accept-request-field]
  (try
    (let [e e.originalEvent
          request (decode (.getData (or e.clipboardData window.clipboardData) "Text"))
          contact (get storage.contacts request.signer)
          requested-ident (get identities request.to)
          current-req nil]
      (e.stopPropagation)
      (e.preventDefault)
      (e.target.blur)
      (-> proj
          (.find "[name='current-request-signer']")
          (.text contact.name))
      (-> (create-signed-ecdh requested-ident.private-key)
          (.then (fn [ecdh]
                   (set! current-req ecdh)
                   (Object.assign ecdh {:to request.signer
                                        :signer request.to
                                        :keys {}})
                   (set! (get requests project.name) ecdh)
                   (compute-secret-key ecdh request.public)))
          (.then (fn []
                   (-> proj
                       (.find ".send-file-button")
                       (.removeAttr "disabled"))
                   (-> accept-request-field
                       (.tooltip "option" "content" (str "<b>Accepted request from " contact.name "</b>")))
                   (flash-tooltip accept-request-field)
                   (Promise.all (map #(encrypt-key current-req %) (_.values project.current-manifest.files)))))
          (.then (fn [] (console.log "COMPUTED KEY" current-req))))
      (console.log "PASTED" request))))

(defn encrypt-key
  [req file-entry]
  (-> (sym-encrypt req.shared-key file-entry.key)
      (.then (fn [encrypted-key] (set! (get req.keys file-entry.hash) encrypted-key)))))

(defn new-project
  [new-project-name]
  (if (.val new-project-name)
    (do
      (make-project (.val new-project-name))
      (.val new-project-name ""))))

(defn bind-ui
  []
  (let [new-project-name ($ "#new-project-name")]
    (-> ($ "#new-project-button")
        (.on "click" (fn [] (new-project new-project-name))))
    (-> ($ "#new-project-name")
        (.on "keypress" (fn [e] (if (== (.-key e) "Enter") (new-project new-project-name)))))
    (-> ($ ".selector")
        (.on "click" click-tab))
    (-> ($ "#file-key")
        (.on "keypress" (fn [e] (if (== (.-key e) "Enter") (changed-file-key-field))))
        (.on "blur" changed-file-key-field)
        (.on "paste" (fn [] (setTimeout changed-file-key-field 100))))
    (-> ($ "#add-contact-button")
        (.on "click" add-contact))
    (set! dialog ($ "#dialog"))
    (-> dialog (.dialog {:autoOpen false
                         :buttons [{:text "OK"
                                    :click dialog-ok}
                                   {:text "Cancel"
                                    :class "cancel-button"
                                    :click dialog-cancel}]}))
    (bind-eval eval-func)
    (populate-settings)))

;;; HEX

(defn json->hex
  [obj]
  (web3.utils.asciiToHex (JSON.stringify obj)))

(defn hex->json
  [hex]
  (JSON.parse (web3.utils.hexToAscii hex)))

(defn encode
  [obj]
  (Base32768.encode (Uint8Array. (LZMA.compress (JSON.stringify obj) 9))))

(defn decode
  [str]
  (JSON.parse (if (str.match hex-pattern)
                (web3.utils.hexToAscii str)
                (LZMA.decompress (Int8Array. (Base32768.decode str))))))

(set! window.encode encode)
(set! window.decode decode)

(defn buf-to-hex
  [buf]
  (str "0x" (.join (Array.prototype.map.call (Uint8Array. buf) (fn [n] (.slice  (+ "0" (.toString n 16)) -2))) "")))

(defn prettify-hex
  [hex]
  (.toUpperCase (let [result ""]
                  (loop [hex (subs hex 2)]
                    (if hex
                      (do
                        (set! result (str result (subs hex 0 64) "<br>"))
                        (recur (subs hex 64)))
                      result)))))

(defn upcase-hex
  [hex]
  (-> (.toUpperCase hex) (.replace (RegExp. "0X" "g") "0x")))

;;; CONTACTS

(defn add-contact
  []
  (let [contact-info-field ($ "#new-contact-info")]
    (try
      (let [info (decode (.val contact-info-field))]
        (set! (-> storage (:contacts) (get (:publicKey info))) info)
        (.val contact-info-field "")
        (save-storage)
        (populate-contacts))
      (catch err
          (if (.val contact-info-field)
            (pop-dialog "Could not process contact info, please paste it again." true)
            (indicate-input-error contact-info-field))))))

(defn populate-contacts
  []
  (let [sorted-identities (_.sortBy (_.values (:identities storage)) #(:name %))
        cts (:contacts storage)
        ids identities
        table ($ "#contacts-table")]
    (-> table
        (.children)
        (.remove))
    (if (not (_.isEmpty (:contacts storage)))
      (do
        (-> table
            (.append ($ (str "<tr><th></th>" (.join (map #(str "<th>" (:name %) "</th>") sorted-identities) "")  "</tr>"))))
        (doseq [contact (_.sortBy (_.values (:contacts storage)) #(:name %))]
          (let [row ($ (str "<tr data-contact='" contact.public-key "'><td>" contact.name "&nbsp;</td>"
                            (.join (map (fn [ident]
                                          (str "<td data-identity='" ident.public-key "'>"
                                               "<button data-type='request'>Create Request Token</button>")) sorted-identities) "")
                            "<td><button name='delete-contact-button'><i class='fa fa-window-close'></i></button></td></tr>"))
                copy-buttons (-> row (.find "[data-type='request']"))]
            (-> copy-buttons
                    (.tooltip {:disabled true
                               :items copy-buttons
                               :content "<b>Copied request token to clipboard</b>"}))
            (doseq [cpy-btn (->vec copy-buttons)]
              (let [cpy-btn ($ cpy-btn)
                    ident-hex (-> cpy-btn (.closest "td") (.attr "data-identity"))]
                (flashy (Clipboard. (get cpy-btn 0)
                                    {:text (fn [trigger]
                                             (let [ecdh (-> identities (get ident-hex) (:ecdh))]
                                               (generate-key ident-hex)
                                               (set! ecdh.to contact.public-key)
                                               (set! current-ecdh ecdh)
                                               (encode (_.pick ecdh ["to" "signer" "signature" "public"]))))}))))
            (-> row
                (.find "[name='delete-contact-button']")
                (.on "click" (fn [e] (delete-contact contact))))
            (-> table
                (.append row))))))))

(defn flashy
  [cb]
  (.on cb
       "success"
       (fn [e] (flash-tooltip ($ e.trigger)))))

(defn flash-tooltip
  [element]
  (-> element (.tooltip "open"))
  (setTimeout (fn [] (-> element (.tooltip "close"))), 2000))

(defn generate-key
  [public-hex]
  ;(debugger!)
  (let [ident (-> identities (get public-hex))
        private (:privateKey ident)]
    (-> (create-signed-ecdh private)
        (.then (fn [ecdh]
                 (set! (:signer ecdh) public-hex)
                 (set! (:ecdh ident) ecdh))))))

(defn delete-contact
  [contact]
  (delete (-> storage (:contacts) (get (:publicKey contact))))
  (populate-contacts)
  (save-storage))

;; BUTTONS

(defn toggle-check-button
  [e]
  (let [button ($ (:target e))]
    (console.log "TOGGLE CHECK BUTTON" e button)
    (.attr button "data-checked" (String (not (== "true" (.toLowerCase (.attr button "data-checked"))))))))

(defn check-button
  [attrs content]
  (str "<button data-checked='false' " attrs ">" content "</button>"))

;;; DIALOGS

(defn pop-dialog
  [message hide-cancel]
  (if hide-cancel
    (do
      (console.log "HIDING CANCEL BUTTON")
      (-> dialog (.closest "[role=dialog]") (.addClass "hide-cancel")))
    (-> dialog (.closest "[role=dialog]") (.removeClass "hide-cancel")))
  (-> ($ "#dialog-text") (.html message))
  (-> dialog
      (.dialog "open")
      (.dialog "option" "width" (+ 64 (-> ($ "#dialog-text") (.prop "scrollWidth")))))
  (Promise. (fn [resolve reject]
              (set! dialog-ok-action resolve)
              (set! dialog-cancel-action reject))))

(defn dialog-ok
  []
  (dialog-ok-action)
  (-> dialog (.dialog "close")))

(defn dialog-cancel
  []
  (dialog-cancel-action)
  (-> dialog (.dialog "close")))

(defn populate-settings
  []
  (let [acct-selector-html (-> ($ "#account-selector-template") (.html))]
    (console.log "account" account)
    (-> (web3.eth.getAccounts)
        (.then (fn [accts]
                 (-> ($ "#account-selectors")
                     (.html ""))
                 (doseq [acct (sort accts)]
                   (-> ($ acct-selector-html)
                       (.on "click" (fn [] (click-account acct)))
                       (.attr "name" acct)
                       (.appendTo ($ "#account-selectors"))
                       (.find "[name='acct-display']")
                       (.html (.toUpperCase acct)))))))
    (-> ($ "#new-identity-button")
        (.on "click" new-identity))
    (populate-identities)
    (populate-contacts)))

(defn indicate-input-error
  [input]
  (Promise. (fn [resolve reject] (blink-input-error input 8 resolve))))

(defn blink-input-error
  [input count continuation]
  (if (even? count)
    (-> input (.addClass "error"))
    (-> input (.removeClass "error")))
  (if (<= count 0)
    (continuation)
    (setTimeout (fn [] (blink-input-error input (dec count) continuation)) 150)))

;;; IDENTITIES

(defn new-identity
  []
  (let [name-field ($ "#new-identity-name")
        name (-> name-field (.val))]
    (if (not name)
      (indicate-input-error name-field)
      (if (verify-new-name name name-field "")
        (-> (create-identity)
            (.then (fn [key]
                     (let [ident (Object.assign (_.pick key ["publicKey" "privateKey"]) {:name name})
                           pub (:publicKey ident)]
                       (set! (get (:identities storage) pub) ident)
                       (add-identity (:publicKey ident) (:key key))
                       (console.log "IDENT" ident "KEY" (:key key))
                       (save-storage)
                       (.val name-field "")
                       (populate-identities)))))))))

(defn add-identity
  [public-hex key]
  (set! (get identities public-hex) key)
  (generate-key public-hex))

(defn populate-identities
  []
  (-> ($ "#identities")
      (.children)
      (.remove))
  (doseq [ident (_.sortBy (_.values (:identities storage)) #(:name %))]
    (add-identity-entry ident)))

(defn create-identity
  []
  (create-identity-dsa))

(defn create-identity-dsa
  []
  (-> (crypto.subtle.generateKey {:name "ECDSA"
                                  :namedCurve "P-256"}
                                 true
                                 ["sign", "verify"])
      (.then (fn [k]
               (Promise.all [k
                             (crypto.subtle.exportKey "jwk" k.publicKey)
                             (crypto.subtle.exportKey "jwk" k.privateKey)])))
      (.then (fn [keys]
               {:key (get keys 0)
                :publicKey (encode (get keys 1))
                :privateKey (encode (get keys 2))}))))

(defn create-identity-shh
  []
  (let [key-pair nil
        pub-exp nil]
    (-> (web3.shh.newKeyPair)
        (.then (fn [id]
                 (set! key-pair id)
                 (web3.shh.getPublicKey key-pair)))
        (.then (fn [exp]
                 (set! pub-exp exp)
                 (web3.shh.getPrivateKey key-pair)))
        (.then (fn [exp] {:id key-pair :publicKey pub-exp :privateKey exp})))))

(defn create-identity-rsa
  []
  (let [key-pair nil
        pub-exp nil]
    (-> (crypto.subtle.generateKey {:name "RSASSA-PKCS1-v1_5"
                                    :modulusLength 2048
                                    :publicExponent (Uint8Array. [1 0 1])
                                    :hash {:name "SHA-256"}}
                                   true
                                   ["sign", "verify"])
        (.then (fn [k]
                 (set! key-pair k)
                 (crypto.subtle.exportKey "jwk" (:publicKey key-pair))))
        (.then (fn [exp]
                 (set! pub-exp exp)
                 (crypto.subtle.exportKey "jwk" (:privateKey key-pair))))
        (.then (fn [exp] [key-pair {:publicKey pub-exp :privateKey exp}])))))

(defn add-identity-entry
  [ident]
  (let [id-template (-> ($ "#id-templates [name='id-settings-entry']") (.clone))
        copy-button (-> id-template (.find "[name='id-copy-button']"))
        id-name (-> id-template (.find "[name='id-name']"))
        ecdh-key-pair nil]
    (-> id-template
        (.appendTo ($ "#identities")))
    (-> id-name
        (.val (:name ident))
        (.on "blur" (fn [] (rename-identity ident id-name)))
        (.on "keypress" (fn [e] (if (== (.-key e) "Enter") (rename-identity ident id-name)))))
    (-> id-template
        (.find "[name='id-key']")
        (.val (:key ident)))
    (-> id-template
        (.find "[name='id-delete-button']")
        (.on "click" (fn [] (delete-identity ident))))
    (-> copy-button
        (.tooltip {:disabled true
                   :items copy-button
                   :content "<b>Copied ID</b>"}))
    (flashy (Clipboard. (get copy-button 0)
                        {:text (fn [trigger] (encode (_.pick ident ["name" "publicKey"])))}))))

(defn rename-identity
  [ident name-field]
  (let [new-name (.val name-field)
        old-name (:name ident)]
    (if (not new-name)
      (-> (indicate-input-error name-field)
          (.then (fn [] (.val name-field old-name))))
      (if (and (not (== (:name ident) new-name))
               (verify-new-name new-name name-field old-name))
        (do
          (set! (:name ident) new-name)
          (save-storage)
          (populate-identities))))))

(defn verify-new-name
  [new-name name-field old-name]
  (if (_.find (:identities storage) (fn [id] (== (:name id) new-name)))
          (do
            (pop-dialog (str "There is already an identity named " new-name) true)
            (.val name-field old-name)
            false)
          true))

(defn delete-identity
  [ident]
  (-> (pop-dialog (str "Are you sure you want to delete identity '" (:name ident) "'?"))
      (.then (fn []
               ;;(console.log "IDENTITIES" (:identities storage))
               ;;(console.log "DELETE IDENTITY" (:name ident) (get (:identities storage) (:publicKey ident)))
               (delete (get (:identities storage) (:publicKey ident)))
               (save-storage)
               (populate-identities)))
      (.catch (fn []))))

(defn fetch-identities
  []
  (-> (Promise.all (map (fn [ident]
                          (-> (Promise.all [(crypto.subtle.importKey
                                              "jwk"
                                              (decode (:publicKey ident))
                                              {:name "ECDSA"
                                               :namedCurve "P-256"}
                                              false
                                              ["verify"])
                                            (crypto.subtle.importKey
                                              "jwk"
                                              (decode (:privateKey ident))
                                              {:name "ECDSA"
                                               :namedCurve "P-256"}
                                              false
                                              ["sign"])])
                              (.then (fn [k]
                                       (add-identity (:publicKey ident)
                                                     {:publicKey (get k 0)
                                                      :privateKey (get k 1)})))))
                        (_.values (:identities storage))))
      (.then (fn [] (console.log "IDENTITIES" identities)))))

;;; CRYPTO

(defn create-signed-ecdh
  [dsa]
  (let [keys nil
        public nil]
    (-> (crypto.subtle.generate-key {:name "ECDH"
                                     :namedCurve "P-256"}
                                    true
                                    ["deriveKey" "deriveBits"])
        (.then (fn [k]
                 (Promise.all [k
                               (crypto.subtle.exportKey "jwk" k.publicKey)])))
        (.then (fn [key-results]
                 (set! keys key-results)
                 (set! public (encode (get keys 1)))
                 (crypto.subtle.sign {:name "ECDSA"
                                      :hash "SHA-256"}
                                     dsa
                                     (EthereumjsUtil.toBuffer public))))
        (.then (fn [signature]
                 (let [result (get keys 0)]
                   (Object.assign result
                                  {:public public
                                   :signature (buf-to-hex signature)})))))))

(defn compute-secret-key
  [ecdh public-key]
  (console.log "importing" (decode public-key))
  (-> (crypto.subtle.import-key "jwk"
                                (decode public-key)
                                {:name "ECDH"
                                 :namedCurve "P-256"}
                                false
                                [])
      (.then (fn [key]
               (crypto.subtle.deriveKey {:name "ECDH"
                                         :namedCurve "P-256"
                                         :public key}
                                        ecdh.private-key
                                        {:name "AES-CBC"
                                         :length 256}
                                        false
                                        ["encrypt" "decrypt"])))
      (.then (fn [key]
               (set! ecdh.shared-key key)))))

(defn sym-encrypt
  [pass data]
  (let [iv (Uint8Array. 16)]
    (crypto.getRandomValues iv)
    (-> (if (instance? CryptoKey pass)
          (Promise.resolve pass)
          (password->key pass))
        (.then (fn [key] (crypto.subtle.encrypt {:name "AES-CBC" :iv iv} key (EthereumjsUtil.toBuffer data))))
        (.then (fn [result]
                 (let [result (Uint8Array. result)
                       final (Uint8Array. (+ 16 result.length))]
                   (.set final iv)
                   (.set final result 16)
                   (buf-to-hex final)))))))

(defn sym-decrypt
  [pass encrypted-data]
  (let [enc (EthereumjsUtil.toBuffer encrypted-data)]
    (-> (if (instance? CryptoKey pass)
          (Promise.resolve pass)
          (password->key pass))
        (.then (fn [key]
                 (let [iv (.slice enc 0 16)
                       data (.slice enc 16)]
                   (crypto.subtle.decrypt {:name "AES-CBC" :iv iv} key data))))
        (.then (fn [decrypted] (buf-to-hex (Uint8Array. decrypted)))))))

(defn password->key
  [pass]
  (-> (crypto.subtle.importKey "raw" (EthereumjsUtil.toBuffer pass) "PBKDF2" false ["deriveKey" "deriveBits"])
      (.then (fn [k]
               (crypto.subtle.deriveKey
                {:name "PBKDF2"
                 :salt (EthereumjsUtil.toBuffer "STONE")
                 :iterations 1000
                 :hash "SHA-256"}
                k
                {:name "AES-CBC"
                 :length 256}
                true
                ["encrypt" "decrypt"])))))

(defn encrypt
  [doc]
  (sym-encrypt private-sym-key doc))

(defn decrypt
  [encrypted-doc]
  (sym-decrypt private-sym-key encrypted-doc))

;; public key allows encryption
(defn fetch-public-key
  []
  (if account
    (let [key (:publicKey storage)]
      (if key
        (Promise.resolve key)
        (let [msg (web3.utils.asciiToHex "stone")
              hash (EthereumjsUtil.hashPersonalMessage (EthereumjsUtil.toBuffer msg))]
          (-> (web3.eth.sign msg account)
              (.then (fn [s]
                       (let [sig (EthereumjsUtil.fromRpcSig s)
                             pub-key-buf (EthereumjsUtil.ecrecover hash (:v sig) (:r sig) (:s sig))
                             pub-key (buf-to-hex pub-key-buf)]
                         (set! (:privateSymPass storage) s)
                         (set! (:publicKey storage) pub-key)
                         (save-storage)
                         pub-key)))))))))

;;; TABS

(defn click-tab
  [e]
  (let [tabName (-> (:target e) (.getAttribute "data-panel"))]
    (select-tab tabName)))

(defn select-tab
  [tab-name]
  (-> ($ ".current.tab")
      (.add ".selector.current")
      (.removeClass "current"))
  (-> ($ (str "#" tab-name))
      (.add (-> ($ (str "[data-panel='" tab-name "']")) (.closest ".selector")))
      (.addClass "current")))

;;; FILES

(defn disable-push-files
  [project-ui]
  (-> (:pushFilesButton project-ui)
      (.attr "disabled" true)
      (.attr "title" "Already pushed")
      (.tooltip)))

(defn enable-push-files
  [project-ui]
  (-> (:pushFilesButton project-ui)
      (.attr "disabled" false)
      (.attr "title" "")))

(defn populate-file-table
  [project proj project-ui]
  (console.log "PROJECT" project proj)
  (let [files (manifest-file-list project.current-manifest)
        file-table (-> proj (.find "[name='files']"))]
    (-> proj
        (.find "[name=push-files-button]")
        (.attr "disabled" (Contract.has-version project.current-manifest.hash)))
    (-> file-table (.children) (.remove))
    (-> ($ "#project-template [name='file-list-header']") (.clone) (.appendTo file-table))
    (if (.-length files)
      (doseq [file files]
        (let [code-button-id (str "code-button-" (set! id-counter (inc id-counter)))
              row ($ (str "<tr><td class='stored-check'><i name='stored'></i></td><th>" file.name "</th><td><span class='hex-64'>" (prettify-hex file.hash) "</span></td><td><button id='" code-button-id "' class='send-file-button'>Copy File Key</button></td><td><button name='key-button'>Show key</button><span name='key-text' class='hex-64'></span></td></tr>"))
              key-button (-> row (.find "[name='key-button']"))
              code-button (-> row (.find (str "#" code-button-id)))
              key-text (-> row (.find "[name='key-text']"))]
          (-> file-table (.append row))
          (.on key-button "click" (fn [] (reveal-key key-text file)))
          (-> code-button
              (.tooltip {:disabled true
                         :items code-button
                         :content "<b>Copied File Code</b>"}))
          (-> row (.find "[name=stored]")
              (.attr "class" (if (Contract.has-version file.hash)
                               "fa fa-check-square-o"
                               "fa fa-square-o")))
          (flashy (Clipboard. (str "#" code-button-id)
                              {:text (fn [trigger] (copy-code project file))}))
          (if (not (get requests  project.name))
            (-> proj
                (.find ".send-file-button")
                (.attr "disabled" true)))
          (.on row "click" (fn [e] (click-file e row file project proj project-ui))))))))

(defn reveal-key
  [span file-entry]
  (if (.html span)
    (.html span "")
    (.html span (prettify-hex (:key file-entry)))))

(defn copy-code
  [project file-entry]
  (let [req (get requests project.name)]
    (encode {:public req.public
             :signer req.signer
             :account account
             :signature req.signature
             :to req.to
             :hash (:hash file-entry)
             :name file-entry.name
             :encryptedKey (get req.keys file-entry.hash)})))

(defn changed-file-key-field
  []
  (if (-> ($ "#file-key") (.val))
    (let [file-key (decode (-> ($ "#file-key") (.val)))
          owner file-key.signer
          account file-key.account
          hash file-key.hash
          encrypted-key  file-key.encrypted-key
          key nil]
      (try
        (-> ($ "#file-key") (.val ""))
        (console.log "FILE KEY" file-key)
        (-> ($ "#browsing-acct") (.html (upcase-hex account)))
        (-> ($ "#browsing-hash") (.html (upcase-hex hash)))
        (-> ($ "#browsing-name") (.html file-key.name))
        (-> (Fork. (compute-secret-key current-ecdh file-key.public))
            (.then (fn [] (sym-decrypt current-ecdh.shared-key encrypted-key)))
            (.catch (fn [err]
                      (display-error-with-key "<span class='error'>COULDN'T DECRYPT KEY</span>" file-key err)))
            (.then (fn [decrypted-key]
                     (set! key decrypted-key)
                     (-> ($ "#browsing-key") (.html (upcase-hex key)))
                     (Contract.fetch-file-version account hash)))
            (.catch (fn [err]
                      (display-error-with-key "<span class='error'>COULDN'T RETRIEVE VERSION</span>" file-key err)))
            (.then (fn [info]
                     (-> ($ "#browsing-date")
                         (.text info.date))
                     (sym-decrypt key info.data)))
            (.catch (fn [err]
                      (display-error-with-key "<span class='error'>COULDN'T DECRYPT FILE</span>" file-key err)))
            (.then (fn [decrypted]
                     (display-file-contents (.replace (web3.utils.hexToAscii decrypted) (RegExp. "<") "&lt;")))))
        (catch err
            (if (not (or (== "" (-> ($ "#file-password") (.val)))
                         (== "" (-> ($ "#file-key") (.val)))))
              (display-error-with-key "<span class='error'>COULDN'T DECRYPT CODE WITH GIVEN PASSWORD</span>" file-key err)))))))

(defn display-error-with-key
  [msg key err]
  (console.log "KEY" key)
  (display-file-contents msg)
  (throw err))

(defn display-file-contents
  [contents]
  (-> ($ "#file-contents")
      (.html contents)))

(defn click-file
  [e row file project proj ui]
  (if (not (instance? HTMLButtonElement (:target e)))
    (do
      (set! (:redisplayFileContents ui) (fn [enc] (click-file e row file project proj enc)))
      (-> proj (.find ".selected-file") (.removeClass "selected-file"))
      (let [file-contents-encrypted (-> proj  (.find "[name='file-contents-encrypted']"))
            file-contents-hex (-> proj  (.find "[name='file-contents-hex']"))
            file-contents-ascii (-> proj  (.find "[name='file-contents-ascii']"))]
        (if (== file selected-file)
          (do
            (set! selected-file nil)
            (.html file-contents-encrypted "")
            (.html file-contents-hex "")
            (.html file-contents-ascii ""))
          (let [data (find-file-data (:hash file) project)]
            (set! selected-file file)
            (-> row (.addClass "selected-file"))
            (.html file-contents-encrypted (prettify-hex data))
            (-> (sym-decrypt (:key file) data)
                (.then (fn [decrypted]
                         (.html file-contents-hex (prettify-hex (subs decrypted 2)))
                         (.html file-contents-ascii (web3.utils.hexToAscii decrypted)))))))))))

(defn all-file-data
  [project]
  (let [pending project.current-manifest.pending
        data pending.data
        hashes pending.hashes
        result []]
    (loop [i 0]
      (if (< i data.length)
        (do
          (.push result {:data (get data i) :hash (get hashes i)})
          (recur (+ 1 i)))))
    result))

(defn find-file-data
  [hash project]
  (let [pending project.current-manifest.pending
        data (:data pending)
        hashes (:hashes pending)]
    (loop [i 0]
      (if (< i (.-length data))
        (if (== (get hashes i) hash)
          (get data i)
          (recur (+ 1 i)))
        nil))))

(defn bind-dnd
  [project node proj ui]
  (-> node
      (.on "dragover" (fn [e] (handle-drag-over e project proj)))
      (.on "drop" (fn [e] (handle-drop e project proj ui)))))

(defn handle-drop
  [e project node ui]
  (.preventDefault e)
  (let [manifest (:currentManifest project)
        files {}
        promises []
        file-table (-> node (.find "[name='files']"))
        data []
        hashes []
        entries []
        man-entry nil]
    (doseq [item (->vec e.originalEvent.dataTransfer.items)]
      (let [entry (.webkitGetAsEntry item)]
        (.push promises
               (-> (all-files entry)
                   (.then (fn [items]
                            (Promise.all
                             (map
                              (fn [item]
                                (let [reader (FileReader.)
                                      file (:file item)
                                      path (:path item)]
                                  (-> (Promise. (fn [resolve reject]
                                                  (.addEventListener reader "load" resolve)
                                                  (.addEventListener reader "error" reject)
                                                  (.addEventListener reader "abort" reject)
                                                  (.readAsArrayBuffer reader file)))
                                      (.then (fn [] (file-entry (str path (.-name file)) (.-result reader))))
                                      (.then (fn [f-entry]
                                               (.push entries f-entry)
                                               (set! (get files (:name f-entry)) (_.pick f-entry ["hash", "key"])))))))
                              items))))))))
    (-> (Promise.all promises)
        (.then (fn []
                 (set! (:files manifest) files)
                 (set! (:pending manifest) {:data data :hashes hashes})
                 (file-entry ".manifest.yaml" (jsyaml.dump (_.pick manifest ["files", "history"]) {:sortKeys true}))))
        (.then (fn [computed-man-entry]
                 (let [file-list (manifest-file-list manifest)]
                   (set! man-entry computed-man-entry)
                   (console.log "MAN ENTRY" man-entry)
                   (set! (:manifest manifest) man-entry)
                   (set! (:hash manifest) (:hash man-entry))
                   (set! (:data manifest) (:data man-entry))
                   (set! (:key manifest) (:key man-entry))
                   (set! (:currentManifest storage) manifest)
                   (doseq [file entries]
                     (.push data (:data file))
                     (.push hashes (:hash file)))
                   (save-storage)
                   (populate-file-table project node ui)
                   (encrypt (:key manifest)))))
        (.then (fn [key]
                 (set! (:encryptedKey manifest) key))))))

(defn push-files
  [project proj project-ui]
  (let [old-count (:count (:currentManifest project))]
    (set! (:count (:currentManifest project)) (inc old-count))
    (-> (Contract.store project)
        (.then (fn [] (populate-file-table project proj project-ui)))
        (.catch (fn [] (set! (:count (:currentManifest project)) old-count))))))

(defn clear-files
  [project proj project-ui]
  (let [manifest (:currentManifest project)
        files (manifest-file-list manifest)]
    (set! (:files manifest) [])
    (populate-file-table project proj project-ui)))

(defn manifest-file-list
  [manifest]
  (let [files []
        man-files (:files manifest)]
    (doseq [file-name (_.keys man-files)]
      (.push files (_.merge {:name file-name} (get man-files file-name))))
    (_.sortBy files #(:name %))))

(defn file-entry
  [name data]
  (let [buf (buf-constructor data)
        buf-hex (EthereumjsUtil.bufferToHex buf)
        file-hash (web3.utils.soliditySha3 buf-hex)
        key (web3.utils.soliditySha3 file-hash buf-hex)]
    (-> (sym-encrypt key buf)
        (.then (fn [encrypted] {:name name
                                :hash file-hash
                                :key key
                                :data encrypted})))))

(defn handle-drag-over
  [e]
  (.preventDefault e)
  (set! e.originalEvent.dataTransfer.dropEffect "copy"))

(defn all-files
  [item]
  (let [items []]
    (-> (sub-all-files item true "" items)
        (.then (fn [] items)))))

(defn sub-all-files
  [item first path items]
  (if item.isFile
    (Promise. (fn [resolve reject]
                (item.file (fn [file err]
                             (if err
                               (reject err)
                               (do
                                 (.push items {:file file :path path})
                                 (resolve true)))))))
    (if item.isDirectory
      (Promise. (fn [resolve reject]
                  (-> (item.createReader)
                      (.readEntries
                       (fn [entries err]
                         (if err
                           (reject err)
                           (resolve (Promise.all (map #(sub-all-files % false (if (not first) (str path (.-name item) "/") "") items)
                                                      (->vec entries))))))))))
      (Promise.reject (Error. "Unrecognized file type")))))

;;; ACCOUNTS

(defn click-account
  [acct]
  (console.log "click account" acct))

(defn get-account
  []
  (-> (web3.eth.getAccounts)
      (.then first)))

(defn save-storage
  []
  (doseq [project-pair (_.toPairs project-uis)]
    (let [name (get project-pair 0)
          ui (get project-pair 1)]
      (console.log "project" name ui)))
  (set! localStorage.stone (JSON.stringify storage)))

;;; CONFIG

(defn eval-func
  [str]
  (eval str))

(defn config-non-infura
  []
  (console.log "not using infura")
  (-> ($ document.body) (.removeClass "infura"))
  (select-tab "my-files"))

(defn config-web3
  []
  (if window.parity
    (do
      (config-non-infura)
      (-> (window.parity.api.parity.wsUrl)
          (.then (fn [url]
                   (console.log "SETTING web3 to new Web3(" (str "ws://" url) ")")
                   (set! window.web3 (Web3. (str "ws://" url)))))))
    (do
      (set! infura? (not (or (and window.Web3 window.Web3.givenProvider)
                             (and window.web3 window.web3.currentProvider))))
      (if (not infura?) (config-non-infura)
        (console.log "using infura"))
      (Promise.resolve
       (set! window.web3
             (Web3. (or (and infura? (Web3.providers.HttpProvider. "https://kovan.infura.io/88wLRnZytgQhFHdO2Nco"))
                        (and window.Web3 window.Web3.givenProvider Web3.givenProvider)
                        (and window.web3 window.web3.currentProvider web3.currentProvider))))))))

(defn config-contract
  []
  (Contract.config {:storage storage :stone exports}))

(-> (config-web3)
    (.then (fn []
             (if parity (set! web3.shh.parity true))
             (get-account)))
    (.then (fn [acc]
             (set! account acc)
             (set! (:account storage) acc)
             ;; (COM.config {:storage storage
             ;;              :saveStorage save-storage})
             (-> ($ (str "#account-selectors [name='" acc "']"))
                 (.addClass "selected"))
             (config-contract)))
    (.then (fn [] (fetch-public-key)))
    (.then (fn [key] (set! public-key key)
             (fetch-identities)))
    (.then (fn []
             (set! private-sym-key (:privateSymPass storage))
             (bind-ui)
             (doseq [proj (_.keys (:projects storage))]
               (add-project (get (:projects storage) proj))))))

(set! window.storage storage)
(set! window.save-storage save-storage)
(set! window.evaluate evaluate)
(set! window.identities identities)
(set! window.buf-to-hex buf-to-hex)
