/*!
 * memorystore
 * Copyright(c) 2020 Rocco Musolino <@roccomuso>
 * MIT Licensed
 */

var debug = require('debug')('memorystore')
var LRU = require('lru-cache')
var util = require('util')

/**
 * One day in milliseconds.
 */

var oneDay = 86400000

function getTTL (options, sess, sid) {
  if (typeof options.ttl === 'number') return options.ttl
  if (typeof options.ttl === 'function') return options.ttl(options, sess, sid)
  if (options.ttl) throw new TypeError('`options.ttl` must be a number or function.')

  var maxAge = (sess && sess.cookie) ? sess.cookie.maxAge : null
  return (typeof maxAge === 'number'
    ? Math.floor(maxAge)
    : oneDay)
}

function prune (store) {
  debug('Pruning expired entries')
  store.forEach(function (value, key) {
    store.get(key)
  })
}

var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }

/**
 * Return the `MemoryStore` extending `express`'s session Store.
 *
 * @param {object} express session
 * @return {Function}
 * @api public
 */

module.exports = function (session) {
  /**
   * Express's session Store.
   */

  var Store = session.Store

  /**
   * Initialize MemoryStore with the given `options`.
   *
   * @param {Object} options
   * @api public
   */

  function MemoryStore (options) {
    if (!(this instanceof MemoryStore)) {
      throw new TypeError('Cannot call MemoryStore constructor as a function')
    }

    options = options || {}
    Store.call(this, options)

    this.options = {}
    this.options.checkPeriod = options.checkPeriod
    this.options.max = options.max || Infinity
    this.options.ttl = options.ttl
    this.options.dispose = options.dispose
    this.options.stale = options.stale
    this.options.noDisposeOnSet = options.noDisposeOnSet

    this.serializer = options.serializer || JSON
    this.store = LRU(this.options)
    debug('Init MemoryStore')

    this.startInterval()
  }

  /**
   * Inherit from `Store`.
   */

  util.inherits(MemoryStore, Store)

  /**
   * Attempt to fetch session by the given `sid`.
   *
   * @param {String} sid
   * @param {Function} fn
   * @api public
   */

  MemoryStore.prototype.get = function (sid, fn) {
    var store = this.store

    debug('GET "%s"', sid)

    var data = store.get(sid)
    if (!data) return fn()

    debug('GOT %s', data)
    var err = null
    var result
    try {
      result = this.serializer.parse(data)
    } catch (er) {
      err = er
    }

    fn && defer(fn, err, result)
  }

  /**
   * Commit the given `sess` object associated with the given `sid`.
   *
   * @param {String} sid
   * @param {Session} sess
   * @param {Function} fn
   * @api public
   */

  MemoryStore.prototype.set = function (sid, sess, fn) {
    var store = this.store

    var ttl = getTTL(this.options, sess, sid)
    try {
      var jsess = this.serializer.stringify(sess)
    } catch (err) {
      fn && defer(fn, err)
    }

    store.set(sid, jsess, ttl)
    debug('SET "%s" %s ttl:%s', sid, jsess, ttl)
    fn && defer(fn, null)
  }

  /**
   * Destroy the session associated with the given `sid`.
   *
   * @param {String} sid
   * @api public
   */

  MemoryStore.prototype.destroy = function (sid, fn) {
    var store = this.store

    if (Array.isArray(sid)) {
      sid.forEach(function (s) {
        debug('DEL "%s"', s)
        store.del(s)
      })
    } else {
      debug('DEL "%s"', sid)
      store.del(sid)
    }
    fn && defer(fn, null)
  }

  /**
   * Refresh the time-to-live for the session with the given `sid`.
   *
   * @param {String} sid
   * @param {Session} sess
   * @param {Function} fn
   * @api public
   */

  MemoryStore.prototype.touch = function (sid, sess, fn) {
    var store = this.store

    var ttl = getTTL(this.options, sess, sid)

    debug('EXPIRE "%s" ttl:%s', sid, ttl)
    var err = null
    if (store.get(sid) !== undefined) {
      try {
        var s = this.serializer.parse(store.get(sid))
        s.cookie = sess.cookie
        store.set(sid, this.serializer.stringify(s), ttl)
      } catch (e) {
        err = e
      }
    }
    fn && defer(fn, err)
  }

  /**
   * Fetch all sessions' ids
   *
   * @param {Function} fn
   * @api public
   */

  MemoryStore.prototype.ids = function (fn) {
    var store = this.store

    var Ids = store.keys()
    debug('Getting IDs: %s', Ids)
    fn && defer(fn, null, Ids)
  }

  /**
   * Fetch all sessions
   *
   * @param {Function} fn
   * @api public
   */

  MemoryStore.prototype.all = function (fn) {
    var store = this.store
    var self = this

    debug('Fetching all sessions')
    var err = null
    var result = {}
    try {
      store.forEach(function (val, key) {
        result[key] = self.serializer.parse(val)
      })
    } catch (e) {
      err = e
    }
    fn && defer(fn, err, result)
  }

  /**
   * Delete all sessions from the store
   *
   * @param {Function} fn
   * @api public
   */

  MemoryStore.prototype.clear = function (fn) {
    var store = this.store
    debug('delete all sessions from the store')
    store.reset()
    fn && defer(fn, null)
  }

  /**
   * Get the count of all sessions in the store
   *
   * @param {Function} fn
   * @api public
   */

  MemoryStore.prototype.length = function (fn) {
    var store = this.store
    debug('getting length', store.itemCount)
    fn && defer(fn, null, store.itemCount)
  }

  /**
   * Start the check interval
   * @api public
   */

  MemoryStore.prototype.startInterval = function () {
    var self = this
    var ms = this.options.checkPeriod
    if (ms && typeof ms === 'number') {
      clearInterval(this._checkInterval)
      debug('starting periodic check for expired sessions')
      this._checkInterval = setInterval(function () {
        prune(self.store) // iterates over the entire cache proactively pruning old entries
      }, Math.floor(ms)).unref()
    }
  }

  /**
   * Stop the check interval
   * @api public
   */

  MemoryStore.prototype.stopInterval = function () {
    debug('stopping periodic check for expired sessions')
    clearInterval(this._checkInterval)
  }

  /**
   * Remove only expired entries from the store
   * @api public
   */

  MemoryStore.prototype.prune = function () {
    prune(this.store)
  }

  return MemoryStore
}
