// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
let LocalStorageDb
import _ from "lodash"
import { createUid, processFind } from "./utils"
import { compileSort } from "./selector"

export default LocalStorageDb = class LocalStorageDb {
  constructor(options: any, success: any) {
    this.collections = {}

    if (options && options.namespace && window.localStorage) {
      this.namespace = options.namespace
    }

    if (success) {
      success(this)
    }
  }

  addCollection(name: any, success: any, error: any) {
    // Set namespace for collection
    let namespace
    if (this.namespace) {
      namespace = this.namespace + "." + name
    }

    const collection = new Collection(name, namespace)
    this[name] = collection
    this.collections[name] = collection
    if (success != null) {
      return success()
    }
  }

  removeCollection(name: any, success: any, error: any) {
    if (this.namespace && window.localStorage) {
      const keys = []
      for (let i = 0, end = window.localStorage.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
        keys.push(window.localStorage.key(i))
      }

      for (let key of keys) {
        if (key.substring(0, this.namespace.length + 1) === this.namespace + ".") {
          window.localStorage.removeItem(key)
        }
      }
    }

    delete this[name]
    delete this.collections[name]
    if (success != null) {
      return success()
    }
  }
}

// Stores data in memory, optionally backed by local storage
class Collection {
  constructor(name: any, namespace: any) {
    this.name = name
    this.namespace = namespace

    this.items = {}
    this.upserts = {} // Pending upserts by _id. Still in items
    this.removes = {} // Pending removes by _id. No longer in items

    // Read from local storage
    if (window.localStorage && namespace != null) {
      this.loadStorage()
    }
  }

  loadStorage() {
    // Read items from localStorage
    let key
    this.itemNamespace = this.namespace + "_"

    for (let i = 0, end = window.localStorage.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
      key = window.localStorage.key(i)
      if (key.substring(0, this.itemNamespace.length) === this.itemNamespace) {
        const item = JSON.parse(window.localStorage[key])
        this.items[item._id] = item
      }
    }

    // Read upserts
    const upsertKeys = window.localStorage[this.namespace + "upserts"]
      ? JSON.parse(window.localStorage[this.namespace + "upserts"])
      : []
    for (key of upsertKeys) {
      this.upserts[key] = this.items[key]
    }

    // Read removes
    const removeItems = window.localStorage[this.namespace + "removes"]
      ? JSON.parse(window.localStorage[this.namespace + "removes"])
      : []
    return (this.removes = _.fromPairs(_.zip(_.map(removeItems, "_id"), removeItems)))
  }

  find(selector: any, options: any) {
    return {
      fetch: (success: any, error: any) => {
        return this._findFetch(selector, options, success, error)
      }
    }
  }

  findOne(selector: any, options: any, success: any, error: any) {
    if (_.isFunction(options)) {
      ;[options, success, error] = [{}, options, success]
    }

    return this.find(selector, options).fetch(function (results: any) {
      if (success != null) {
        return success(results.length > 0 ? results[0] : null)
      }
    }, error)
  }

  _findFetch(selector: any, options: any, success: any, error: any) {
    if (success != null) {
      return success(processFind(this.items, selector, options))
    }
  }

  upsert(doc: any, success: any, error: any) {
    // Handle both single and multiple upsert
    let items = doc
    if (!_.isArray(items)) {
      items = [items]
    }

    // Handle case of array
    for (let item of items) {
      if (!item._id) {
        item._id = createUid()
      }

      // Replace/add
      this._putItem(item)
      this._putUpsert(item)
    }

    if (success) {
      return success(doc)
    }
  }

  remove(id: any, success: any, error: any) {
    if (_.has(this.items, id)) {
      this._putRemove(this.items[id])
      this._deleteItem(id)
      this._deleteUpsert(id)
    } else {
      this._putRemove({ _id: id })
    }

    if (success != null) {
      return success()
    }
  }

  _putItem(doc: any) {
    this.items[doc._id] = doc
    if (this.namespace) {
      return (window.localStorage[this.itemNamespace + doc._id] = JSON.stringify(doc))
    }
  }

  _deleteItem(id: any) {
    delete this.items[id]
    if (this.namespace) {
      return window.localStorage.removeItem(this.itemNamespace + id)
    }
  }

  _putUpsert(doc: any) {
    this.upserts[doc._id] = doc
    if (this.namespace) {
      return (window.localStorage[this.namespace + "upserts"] = JSON.stringify(_.keys(this.upserts)))
    }
  }

  _deleteUpsert(id: any) {
    delete this.upserts[id]
    if (this.namespace) {
      return (window.localStorage[this.namespace + "upserts"] = JSON.stringify(_.keys(this.upserts)))
    }
  }

  _putRemove(doc: any) {
    this.removes[doc._id] = doc
    if (this.namespace) {
      return (window.localStorage[this.namespace + "removes"] = JSON.stringify(_.values(this.removes)))
    }
  }

  _deleteRemove(id: any) {
    delete this.removes[id]
    if (this.namespace) {
      return (window.localStorage[this.namespace + "removes"] = JSON.stringify(_.values(this.removes)))
    }
  }

  cache(docs: any, selector: any, options: any, success: any, error: any) {
    // Add all non-local that are not upserted or removed
    let sort: any
    for (let doc of docs) {
      this.cacheOne(doc)
    }

    const docsMap = _.fromPairs(_.zip(_.map(docs, "_id"), docs))

    if (options.sort) {
      sort = compileSort(options.sort)
    }

    // Perform query, removing rows missing in docs from local db
    return this.find(selector, options).fetch((results: any) => {
      for (let result of results) {
        if (!docsMap[result._id] && !_.has(this.upserts, result._id)) {
          // If past end on sorted limited, ignore
          if (options.sort && options.limit && docs.length === options.limit) {
            if (sort(result, _.last(docs)) >= 0) {
              continue
            }
          }
          this._deleteItem(result._id)
        }
      }

      if (success != null) {
        return success()
      }
    }, error)
  }

  pendingUpserts(success: any) {
    return success(_.values(this.upserts))
  }

  pendingRemoves(success: any) {
    return success(_.map(this.removes, "_id"))
  }

  resolveUpsert(doc: any, success: any) {
    // Handle both single and multiple upsert
    let items = doc
    if (!_.isArray(items)) {
      items = [items]
    }

    for (let item of items) {
      if (this.upserts[item._id]) {
        // Only safely remove upsert if item is unchanged
        if (_.isEqual(item, this.upserts[item._id])) {
          this._deleteUpsert(item._id)
        }
      }
    }
    if (success != null) {
      return success()
    }
  }

  resolveRemove(id: any, success: any) {
    this._deleteRemove(id)
    if (success != null) {
      return success()
    }
  }

  // Add but do not overwrite or record as upsert
  seed(doc: any, success: any) {
    if (!_.has(this.items, doc._id) && !_.has(this.removes, doc._id)) {
      this._putItem(doc)
    }
    if (success != null) {
      return success()
    }
  }

  // Add but do not overwrite upserts or removes
  cacheOne(doc: any, success: any) {
    if (!_.has(this.upserts, doc._id) && !_.has(this.removes, doc._id)) {
      const existing = this.items[doc._id]

      // If _rev present, make sure that not overwritten by lower _rev
      if (!existing || !doc._rev || !existing._rev || doc._rev >= existing._rev) {
        this._putItem(doc)
      }
    }
    if (success != null) {
      return success()
    }
  }
}
