
/*!
 * Connect - staticCache
 * Copyright(c) 2011 Sencha Inc.
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var http = require('http')
  , utils = require('../utils')
  , Cache = require('../cache')
  , url = require('url')
  , fs = require('fs');

/**
 * Enables a memory cache layer on top of
 * the `static()` middleware, serving popular
 * static files.
 *
 * By default a maximum of 128 objects are
 * held in cache, with a max of 256k each,
 * totalling ~32mb.
 *
 * A Least-Recently-Used (LRU) cache algo
 * is implemented through the `Cache` object,
 * simply rotating cache objects as they are
 * hit. This means that increasingly popular
 * objects maintain their positions while
 * others get shoved out of the stack and
 * garbage collected.
 *
 * Benchmarks:
 *
 *     static(): 2700 rps
 *     node-static: 5300 rps
 *     static() + staticCache(): 7500 rps
 *
 * Options:
 *
 *   - `maxObjects`  max cache objects [128]
 *   - `maxLength`  max cache object length 256kb
 *
 * @param {Type} name
 * @return {Type}
 * @api public
 */

module.exports = function staticCache(options){
  var options = options || {}
    , cache = new Cache(options.maxObjects || 128)
    , maxlen = options.maxLength || 1024 * 256;

  return function staticCache(req, res, next){
    var path = url.parse(req.url).pathname
      , ranges = req.headers.range
      , hit = cache.get(path)
      , hitCC
      , uaCC
      , header
      , age;

    // cache static
    req.on('static', function(stream){
      var headers = res._headers
        , cc = utils.parseCacheControl(headers['cache-control'] || '')
        , contentLength = headers['content-length']
        , hit;

      // ignore larger files
      if (!contentLength || contentLength > maxlen) return;

      // dont cache items we shouldn't be
      if ( cc['no-cache']
        || cc['no-store']
        || cc['private']
        || cc['must-revalidate']) return;

      // if already in cache then validate
      if (hit = cache.get(path)){
        if (headers.etag == hit[0].etag) {
          hit[0].date = new Date;
          return;
        } else {
          cache.remove(path);
        }
      }

      // validation notifiactions don't contain a steam
      if (null == stream) return;

      // add the cache object
      var arr = cache.add(path);
      arr.push(headers);

      // store the chunks
      stream.on('data', function(chunk){
        arr.push(chunk);
      });

      // flag it as complete
      stream.on('end', function(){
        arr.complete = true;
      });
    });

    // cache hit, doesnt support range requests
    if (hit && hit.complete && !ranges) {
      header = utils.merge({}, hit[0]);
      header.Age = age = (new Date - new Date(header.date)) / 1000 | 0;
      header.date = new Date().toUTCString();

      // parse cache-controls
      hitCC = utils.parseCacheControl(header['cache-control'] || '');
      uaCC = utils.parseCacheControl(req.headers['cache-control'] || '');

      // check if we must revalidate(bypass)
      if (hitCC['no-cache'] || uaCC['no-cache']) return next();

      // check freshness of entity
      if (isStale(hitCC, age) || isStale(uaCC, age)) return next();

      // conditional GET support
      if (utils.conditionalGET(req)) {
        if (!utils.modified(req, res, header)) {
          header['content-length'] = 0;
          res.writeHead(304, header);
          return res.end();
        }
      }

      // HEAD support
      if ('HEAD' == req.method) {
        header['content-length'] = 0;
        res.writeHead(200, header);
        return res.end();
      }

      // respond with cache
      res.writeHead(200, header);

      // backpressure
      function write(i) {
        var buf = hit[i];
        if (!buf) return res.end();
        if (false === res.write(buf)) {
          res.once('drain', function(){
            write(++i);
          });
        } else {
          write(++i);
        }
      }

      return write(1);
    }

    next();
  }
};

/**
 * Check if cache item is stale
 *
 * @param {Object} cc
 * @param {Number} age
 * @return {Boolean}
 * @api private
 */

function isStale(cc, age) {
  return cc['max-age'] && cc['max-age'] <= age;
}