;(function () { // closure for web browsers

if (module) {
  module.exports = LRUCache
} else {
  // just set the global for non-node platforms.
  this.LRUCache = LRUCache
}

function hOP (obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key)
}

function naiveLength () { return 1 }

function LRUCache (maxLength, lengthCalculator, maxAge) {
  if (!(this instanceof LRUCache)) {
    return new LRUCache(maxLength, lengthCalculator)
  }

  if (typeof lengthCalculator !== "function") {
    lengthCalculator = naiveLength
  }
  if (!maxLength || !(typeof maxLength === "number") || maxLength <= 0 ) {
    maxLength = Infinity
  }


  var cache = {} // hash of items by key
    , lruList = {} // list of items in order of use recency
    , lru = 0 // least recently used
    , mru = 0 // most recently used
    , length = 0 // number of items in the list
    , itemCount = 0


  // resize the cache when the maxLength changes.
  Object.defineProperty(this, "maxLength",
    { set : function (mL) {
        if (!mL || !(typeof mL === "number") || mL <= 0 ) mL = Infinity
        maxLength = mL
        // if it gets above double maxLength, trim right away.
        // otherwise, do it whenever it's convenient.
        if (length > maxLength) trim()
      }
    , get : function () { return maxLength }
    , enumerable : true
    })

  // resize the cache when the lengthCalculator changes.
  Object.defineProperty(this, "lengthCalculator",
    { set : function (lC) {
        if (typeof lC !== "function") {
          lengthCalculator = naiveLength
          length = itemCount
          Object.keys(cache).forEach(function (key) {
            cache[key].length = 1
          })
        } else {
          lengthCalculator = lC
          length = 0
          Object.keys(cache).forEach(function (key) {
            cache[key].length = lengthCalculator(cache[key].value)
            length += cache[key].length
          })
        }

        if (length > maxLength) trim()
      }
    , get : function () { return lengthCalculator }
    , enumerable : true
    })

  Object.defineProperty(this, "length",
    { get : function () { return length }
    , enumerable : true
    })


  Object.defineProperty(this, "itemCount",
    { get : function () { return itemCount }
    , enumerable : true
    })

  this.reset = function () {
    cache = {}
    lruList = {}
    lru = 0
    mru = 0
    length = 0
    itemCount = 0
  }

  // Provided for debugging/dev purposes only. No promises whatsoever that
  // this API stays stable.
  this.dump = function () {
    return cache
  }

  this.set = function (key, value) {
    if (hOP(cache, key)) {
      this.get(key)
      cache[key].value = value
      return true
    }

    var hit = {key:key, value:value, lu:mru++, length:lengthCalculator(value)}
    if (maxAge) hit.now = Date.now()

    // oversized objects fall out of cache automatically.
    if (hit.length > maxLength) return false

    length += hit.length
    lruList[hit.lu] = cache[key] = hit
    itemCount ++

    if (length > maxLength) trim()
    return true
  }

  this.get = function (key) {
    if (!hOP(cache, key)) return
    var hit = cache[key]
    if (maxAge && Date.now() - hit.now > maxAge) {
      this.del(key)
      return
    }
    delete lruList[hit.lu]
    if (hit.lu === lru) lruWalk()
    hit.lu = mru ++
    lruList[hit.lu] = hit
    return hit.value
  }

  this.del = function (key) {
    if (!hOP(cache, key)) return
    var hit = cache[key]
    delete cache[key]
    delete lruList[hit.lu]
    if (hit.lu === lru) lruWalk()
    length -= hit.length
    itemCount --
  }

  function lruWalk () {
    // lru has been deleted, hop up to the next hit.
    lru = Object.keys(lruList)[0]
  }

  function trim () {
    if (length <= maxLength) return
    var prune = Object.keys(lruList)
    for (var i = 0; i < prune.length && length > maxLength; i ++) {
      length -= lruList[prune[i]].length
      delete cache[ lruList[prune[i]].key ]
      delete lruList[prune[i]]
    }

    lruWalk()
  }
}

})()
