/* global indexedDB */

'use strict'

module.exports = Level

var AbstractLevelDOWN = require('abstract-leveldown').AbstractLevelDOWN
var inherits = require('inherits')
var Iterator = require('./iterator')
var serialize = require('./util/serialize')
var deserialize = require('./util/deserialize')
var support = require('./util/support')
var clear = require('./util/clear')
var createKeyRange = require('./util/key-range')

var DEFAULT_PREFIX = 'level-js-'

function Level (location, opts) {
  if (!(this instanceof Level)) return new Level(location, opts)

  AbstractLevelDOWN.call(this, {
    bufferKeys: support.bufferKeys(indexedDB),
    snapshots: true,
    permanence: true,
    clear: true
  })

  opts = opts || {}

  if (typeof location !== 'string') {
    throw new Error('constructor requires a location string argument')
  }

  this.location = location
  this.prefix = opts.prefix == null ? DEFAULT_PREFIX : opts.prefix
  this.version = parseInt(opts.version || 1, 10)
}

inherits(Level, AbstractLevelDOWN)

Level.prototype.type = 'level-js'

Level.prototype._open = function (options, callback) {
  var req = indexedDB.open(this.prefix + this.location, this.version)
  var self = this

  req.onerror = function () {
    callback(req.error || new Error('unknown error'))
  }

  req.onsuccess = function () {
    self.db = req.result
    callback()
  }

  req.onupgradeneeded = function (ev) {
    var db = ev.target.result

    if (!db.objectStoreNames.contains(self.location)) {
      db.createObjectStore(self.location)
    }
  }
}

Level.prototype.store = function (mode) {
  var transaction = this.db.transaction([this.location], mode)
  return transaction.objectStore(this.location)
}

Level.prototype.await = function (request, callback) {
  var transaction = request.transaction

  // Take advantage of the fact that a non-canceled request error aborts
  // the transaction. I.e. no need to listen for "request.onerror".
  transaction.onabort = function () {
    callback(transaction.error || new Error('aborted by user'))
  }

  transaction.oncomplete = function () {
    callback(null, request.result)
  }
}

Level.prototype._get = function (key, options, callback) {
  var store = this.store('readonly')

  try {
    var req = store.get(key)
  } catch (err) {
    return this._nextTick(callback, err)
  }

  this.await(req, function (err, value) {
    if (err) return callback(err)

    if (value === undefined) {
      // 'NotFound' error, consistent with LevelDOWN API
      return callback(new Error('NotFound'))
    }

    callback(null, deserialize(value, options.asBuffer))
  })
}

Level.prototype._del = function (key, options, callback) {
  var store = this.store('readwrite')

  try {
    var req = store.delete(key)
  } catch (err) {
    return this._nextTick(callback, err)
  }

  this.await(req, callback)
}

Level.prototype._put = function (key, value, options, callback) {
  var store = this.store('readwrite')

  try {
    // Will throw a DataError or DataCloneError if the environment
    // does not support serializing the key or value respectively.
    var req = store.put(value, key)
  } catch (err) {
    return this._nextTick(callback, err)
  }

  this.await(req, callback)
}

Level.prototype._serializeKey = function (key) {
  return serialize(key, this.supports.bufferKeys)
}

Level.prototype._serializeValue = function (value) {
  return serialize(value, true)
}

Level.prototype._iterator = function (options) {
  return new Iterator(this, this.location, options)
}

Level.prototype._batch = function (operations, options, callback) {
  if (operations.length === 0) return this._nextTick(callback)

  var store = this.store('readwrite')
  var transaction = store.transaction
  var index = 0
  var error

  transaction.onabort = function () {
    callback(error || transaction.error || new Error('aborted by user'))
  }

  transaction.oncomplete = function () {
    callback()
  }

  // Wait for a request to complete before making the next, saving CPU.
  function loop () {
    var op = operations[index++]
    var key = op.key

    try {
      var req = op.type === 'del' ? store.delete(key) : store.put(op.value, key)
    } catch (err) {
      error = err
      transaction.abort()
      return
    }

    if (index < operations.length) {
      req.onsuccess = loop
    }
  }

  loop()
}

Level.prototype._clear = function (options, callback) {
  try {
    var keyRange = createKeyRange(options)
  } catch (e) {
    // The lower key is greater than the upper key.
    // IndexedDB throws an error, but we'll just do nothing.
    return this._nextTick(callback)
  }

  if (options.limit >= 0) {
    // IDBObjectStore#delete(range) doesn't have such an option.
    // Fall back to cursor-based implementation.
    return clear(this, this.location, keyRange, options, callback)
  }

  try {
    var store = this.store('readwrite')
    var req = keyRange ? store.delete(keyRange) : store.clear()
  } catch (err) {
    return this._nextTick(callback, err)
  }

  this.await(req, callback)
}

Level.prototype._close = function (callback) {
  this.db.close()
  this._nextTick(callback)
}

// NOTE: remove in a next major release
Level.prototype.upgrade = function (callback) {
  if (this.status !== 'open') {
    return this._nextTick(callback, new Error('cannot upgrade() before open()'))
  }

  var it = this.iterator()
  var batchOptions = {}
  var self = this

  it._deserializeKey = it._deserializeValue = identity
  next()

  function next (err) {
    if (err) return finish(err)
    it.next(each)
  }

  function each (err, key, value) {
    if (err || key === undefined) {
      return finish(err)
    }

    var newKey = self._serializeKey(deserialize(key, true))
    var newValue = self._serializeValue(deserialize(value, true))

    // To bypass serialization on the old key, use _batch() instead of batch().
    // NOTE: if we disable snapshotting (#86) this could lead to a loop of
    // inserting and then iterating those same entries, because the new keys
    // possibly sort after the old keys.
    self._batch([
      { type: 'del', key: key },
      { type: 'put', key: newKey, value: newValue }
    ], batchOptions, next)
  }

  function finish (err) {
    it.end(function (err2) {
      callback(err || err2)
    })
  }

  function identity (data) {
    return data
  }
}

Level.destroy = function (location, prefix, callback) {
  if (typeof prefix === 'function') {
    callback = prefix
    prefix = DEFAULT_PREFIX
  }
  var request = indexedDB.deleteDatabase(prefix + location)
  request.onsuccess = function () {
    callback()
  }
  request.onerror = function (err) {
    callback(err)
  }
}
