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

// Do nothing callback for success
function doNothing() {}

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

    // Create database
    // TODO escape name
    this.db = window.openDatabase(
      "minimongo_" + options.namespace,
      "",
      "Minimongo:" + options.namespace,
      5 * 1024 * 1024
    )
    if (!this.db) {
      return error("Failed to create database")
    }

    const createTables = (tx: any) =>
      tx.executeSql(
        `\
CREATE TABLE IF NOT EXISTS docs (
col TEXT NOT NULL,
id TEXT NOT NULL,
state TEXT NOT NULL,
doc TEXT,
PRIMARY KEY (col, id));`,
        [],
        doNothing,
        error
      )

    // Create tables
    this.db.transaction(createTables, error, () => {
      if (success) {
        return success(this)
      }
    })
  }

  addCollection(name: any, success: any, error: any) {
    const collection = new Collection(name, this.db)
    this[name] = collection
    this.collections[name] = collection
    if (success) {
      return success()
    }
  }

  removeCollection(name: any, success: any, error: any) {
    delete this[name]
    delete this.collections[name]

    // Remove all documents of collection
    return this.db.transaction(
      (tx: any) => tx.executeSql("DELETE FROM docs WHERE col = ?", [name], success, error),
      error
    )
  }
}

// Stores data in indexeddb store
class Collection {
  constructor(name: any, db: any) {
    this.name = name
    this.db = db
  }

  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) {
    // Android 2.x requires error callback
    error = error || function () {}

    // Get all docs from collection
    return this.db.readTransaction((tx: any) => {
      return tx.executeSql(
        "SELECT * FROM docs WHERE col = ?",
        [this.name],
        function (tx: any, results: any) {
          const docs = []
          for (let i = 0, end = results.rows.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
            const row = results.rows.item(i)
            if (row.state !== "removed") {
              docs.push(JSON.parse(row.doc))
            }
          }
          if (success != null) {
            return success(processFind(docs, selector, options))
          }
        },
        error
      )
    }, error)
  }

  upsert(doc: any, success: any, error: any) {
    // Android 2.x requires error callback
    let item
    error = error || function () {}

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

    for (item of items) {
      if (!item._id) {
        item._id = createUid()
      }
    }

    return this.db.transaction(
      (tx: any) => {
        return (() => {
          const result = []
          for (item of items) {
            result.push(
              tx.executeSql(
                "INSERT OR REPLACE INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)",
                [this.name, item._id, "upserted", JSON.stringify(item)],
                doNothing,
                error
              )
            )
          }
          return result
        })()
      },
      error,
      function () {
        if (success) {
          return success(doc)
        }
      }
    )
  }

  remove(id: any, success: any, error: any) {
    // Android 2.x requires error callback
    error = error || function () {}

    // Find record
    return this.db.transaction((tx: any) => {
      return tx.executeSql(
        "SELECT * FROM docs WHERE col = ? AND id = ?",
        [this.name, id],
        (tx: any, results: any) => {
          if (results.rows.length > 0) {
            // Change to removed
            return tx.executeSql(
              'UPDATE docs SET state="removed" WHERE col = ? AND id = ?',
              [this.name, id],
              function () {
                if (success) {
                  return success(id)
                }
              },
              error
            )
          } else {
            return tx.executeSql(
              "INSERT INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)",
              [this.name, id, "removed", JSON.stringify({ _id: id })],
              function () {
                if (success) {
                  return success(id)
                }
              },
              error
            )
          }
        },
        error
      )
    }, error)
  }

  cache(docs: any, selector: any, options: any, success: any, error: any) {
    // Android 2.x requires error callback
    error = error || function () {}

    return this.db.transaction((tx: any) => {
      // Add all non-local that are not upserted or removed
      return async.eachSeries(
        docs,
        (doc: any, callback: any) => {
          return tx.executeSql(
            "SELECT * FROM docs WHERE col = ? AND id = ?",
            [this.name, doc._id],
            (tx: any, results: any) => {
              // Check if present and not upserted/deleted
              if (results.rows.length === 0 || results.rows.item(0).state === "cached") {
                const existing = results.rows.length > 0 ? JSON.parse(results.rows.item(0).doc) : null

                // If _rev present, make sure that not overwritten by lower _rev
                if (!existing || !doc._rev || !existing._rev || doc._rev >= existing._rev) {
                  // Upsert
                  return tx.executeSql(
                    "INSERT OR REPLACE INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)",
                    [this.name, doc._id, "cached", JSON.stringify(doc)],
                    () => callback(),
                    error
                  )
                } else {
                  return callback()
                }
              } else {
                return callback()
              }
            },
            callback,
            error
          )
        },
        (err: any) => {
          let sort: any
          if (err) {
            if (error) {
              error(err)
            }
            return
          }

          // Rows have been cached, now look for stale ones to remove
          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) => {
            return this.db.transaction((tx: any) => {
              return async.eachSeries(
                results,
                (result: any, callback: any) => {
                  // If not present in docs and is present locally and not upserted/deleted
                  return tx.executeSql(
                    "SELECT * FROM docs WHERE col = ? AND id = ?",
                    [this.name, result._id],
                    (tx: any, rows: any) => {
                      if (!docsMap[result._id] && rows.rows.length > 0 && rows.rows.item(0).state === "cached") {
                        // If past end on sorted limited, ignore
                        if (options.sort && options.limit && docs.length === options.limit) {
                          if (sort(result, _.last(docs)) >= 0) {
                            return callback()
                          }
                        }

                        // Item is gone from server, remove locally
                        return tx.executeSql(
                          "DELETE FROM docs WHERE col = ? AND id = ?",
                          [this.name, result._id],
                          () => callback(),
                          error
                        )
                      } else {
                        return callback()
                      }
                    },
                    callback,
                    error
                  )
                },
                function (err: any) {
                  if (err != null) {
                    if (error != null) {
                      error(err)
                    }
                    return
                  }
                  if (success != null) {
                    return success()
                  }
                }
              )
            }, error)
          }, error)
        }
      )
    }, error)
  }

  pendingUpserts(success: any, error: any) {
    // Android 2.x requires error callback
    error = error || function () {}

    return this.db.readTransaction((tx: any) => {
      return tx.executeSql(
        "SELECT * FROM docs WHERE col = ? AND state = ?",
        [this.name, "upserted"],
        function (tx: any, results: any) {
          const docs = []
          for (let i = 0, end = results.rows.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
            const row = results.rows.item(i)
            docs.push(JSON.parse(row.doc))
          }
          if (success != null) {
            return success(docs)
          }
        },
        error
      )
    }, error)
  }

  pendingRemoves(success: any, error: any) {
    // Android 2.x requires error callback
    error = error || function () {}

    return this.db.readTransaction((tx: any) => {
      return tx.executeSql(
        "SELECT * FROM docs WHERE col = ? AND state = ?",
        [this.name, "removed"],
        function (tx: any, results: any) {
          const docs = []
          for (let i = 0, end = results.rows.length, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
            const row = results.rows.item(i)
            docs.push(JSON.parse(row.doc)._id)
          }
          if (success != null) {
            return success(docs)
          }
        },
        error
      )
    }, error)
  }

  resolveUpsert(doc: any, success: any, error: any) {
    // Android 2.x requires error callback
    error = error || function () {}

    // Handle both single and multiple resolve
    let items = doc
    if (!_.isArray(items)) {
      items = [items]
    }

    // Find records
    return this.db.transaction((tx: any) => {
      return async.eachSeries(
        items,
        (item: any, cb: any) => {
          return tx.executeSql(
            "SELECT * FROM docs WHERE col = ? AND id = ?",
            [this.name, item._id],
            (tx: any, results: any) => {
              if (results.rows.length > 0) {
                // Only safely remove upsert if doc is the same
                if (
                  results.rows.item(0).state === "upserted" &&
                  _.isEqual(JSON.parse(results.rows.item(0).doc), item)
                ) {
                  tx.executeSql(
                    'UPDATE docs SET state="cached" WHERE col = ? AND id = ?',
                    [this.name, item._id],
                    doNothing,
                    error
                  )
                  return cb()
                } else {
                  return cb()
                }
              } else {
                // Upsert removed, which is fine
                return cb()
              }
            },
            error
          )
        },
        function (err: any) {
          if (err) {
            return error(err)
          }

          // Success
          if (success) {
            return success(doc)
          }
        }
      )
    }, error)
  }

  resolveRemove(id: any, success: any, error: any) {
    // Android 2.x requires error callback
    error = error || function () {}

    // Find record
    return this.db.transaction((tx: any) => {
      // Only safely remove if removed state
      return tx.executeSql(
        'DELETE FROM docs WHERE state="removed" AND col = ? AND id = ?',
        [this.name, id],
        function () {
          if (success) {
            return success(id)
          }
        },
        error
      )
    }, error)
  }

  // Add but do not overwrite or record as upsert
  seed(doc: any, success: any, error: any) {
    // Android 2.x requires error callback
    error = error || function () {}

    return this.db.transaction((tx: any) => {
      return tx.executeSql(
        "SELECT * FROM docs WHERE col = ? AND id = ?",
        [this.name, doc._id],
        (tx: any, results: any) => {
          // Only insert if not present
          if (results.rows.length === 0) {
            return tx.executeSql(
              "INSERT INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)",
              [this.name, doc._id, "cached", JSON.stringify(doc)],
              function () {
                if (success) {
                  return success(doc)
                }
              },
              error
            )
          } else {
            if (success) {
              return success(doc)
            }
          }
        },
        error
      )
    }, error)
  }

  // Add but do not overwrite upsert/removed and do not record as upsert
  cacheOne(doc: any, success: any, error: any) {
    // Android 2.x requires error callback
    error = error || function () {}

    return this.db.transaction((tx: any) => {
      return tx.executeSql(
        "SELECT * FROM docs WHERE col = ? AND id = ?",
        [this.name, doc._id],
        (tx: any, results: any) => {
          // Only insert if not present or cached
          if (results.rows.length === 0 || results.rows.item(0).state === "cached") {
            const existing = results.rows.length > 0 ? JSON.parse(results.rows.item(0).doc) : null

            // If _rev present, make sure that not overwritten by lower _rev
            if (!existing || !doc._rev || !existing._rev || doc._rev >= existing._rev) {
              return tx.executeSql(
                "INSERT OR REPLACE INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)",
                [this.name, doc._id, "cached", JSON.stringify(doc)],
                function () {
                  if (success) {
                    return success(doc)
                  }
                },
                error
              )
            } else {
              if (success) {
                return success(doc)
              }
            }
          } else {
            if (success) {
              return success(doc)
            }
          }
        },
        error
      )
    }, error)
  }
}
