text: var ecstatic = require('../sexstatic'),
      fs = require('fs'),
      path = require('path'),
      he = require('he'),
      etag = require('./etag'),
      url = require('url'),
      status = require('./status-handlers');
  
  module.exports = function (opts, stat, modify) {
    // opts are parsed by opts.js, defaults already applied
    var cache = opts.cache,
        root = path.resolve(opts.root),
        baseDir = opts.baseDir,
        humanReadable = opts.humanReadable,
        si = opts.si;
  
    opts.modifyFunctions = modify;
  
    return function middleware (req, res, next) {
  
      // Figure out the path for the file from the given url
      var parsed = url.parse(req.url),
          pathname = decodeURIComponent(parsed.pathname),
          dir = path.normalize(
            path.join(root,
              path.relative(
                path.join('/', baseDir),
                pathname
              )
            )
          );
  
      fs.stat(dir, function (err, stat) {
        if (err) {
          return status[500](res, next, { error: err });
        }
  
        // files are the listing of dir
        fs.readdir(dir, function (err, files) {
          if (err) {
            return status[500](res, next, { error: err });
          }
          res.setHeader('content-type', 'text/html');
          res.setHeader('etag', etag(stat));
          res.setHeader('last-modified', (new Date(stat.mtime)).toUTCString());
          res.setHeader('cache-control', cache);
  
          sortByIsDirectory(files, function (lolwuts, dirs, files) {
            // It's possible to get stat errors for all sorts of reasons here.
            // Unfortunately, our two choices are to either bail completely,
            // or just truck along as though everything's cool. In this case,
            // I decided to just tack them on as "??!?" items along with dirs
            // and files.
            //
            // Whatever.
  
            // if it makes sense to, add a .. link
            if (path.resolve(dir, '..').slice(0, root.length) == root) {
              return fs.stat(path.join(dir, '..'), function (err, s) {
                if (err) {
                  return status[500](res, next, { error: err });
                }
                dirs.unshift([ '..', s ]);
                render(dirs, files, lolwuts);
              });
            }
            render(dirs, files, lolwuts);
          });
  
          function sortByIsDirectory(paths, cb) {
            // take the listing file names in `dir`
            // returns directory and file array, each entry is
            // of the array a [name, stat] tuple
            var pending = paths.length,
                errs = [],
                dirs = [],
                files = [];
  
            if (!pending) {
              return cb(errs, dirs, files);
            }
  
            paths.forEach(function (file) {
              fs.stat(path.join(dir, file), function (err, s) {
                if (err) {
                  errs.push([file, err]);
                }
                else if (s.isDirectory()) {
                  dirs.push([file, s]);
                }
                else {
                  files.push([file, s]);
                }
  
                if (--pending === 0) {
                  cb(errs, dirs, files);
                }
              });
            });
          }
  
          function render(dirs, files, lolwuts) {
            // each entry in the array is a [name, stat] tuple
  
            // TODO: use stylessheets?
            var html = [
              '<!doctype html>',
              '<html>',
              '  <head>',
              '    <meta charset="utf-8">',
              '    <title>Index of ' + pathname +'</title>',
              '  </head>',
              '  <body>',
              '<h1>Index of ' + pathname + '</h1>'
            ].join('\n') + '\n';
  
            html += '<table>';
  
            var failed = false;
            var writeRow = function (file, i) {
              // render a row given a [name, stat] tuple
              var isDir = file[1].isDirectory && file[1].isDirectory();
              var href =
                parsed.pathname.replace(/\/$/, '') +
                '/' + encodeURIComponent(file[0]);
  
              // append trailing slash and query for dir entry
              if (isDir) {
                href += '/' + ((parsed.search)? parsed.search:'');
              }
  
              var displayName = he.encode(file[0]) + ((isDir)? '/':'');
  
              // TODO: use stylessheets?
              html += '<tr>' +
                '<td><code>(' + permsToString(file[1]) + ')</code></td>' +
                '<td style="text-align: right; padding-left: 1em"><code>' + sizeToString(file[1], humanReadable, si) + '</code></td>' +
                '<td style="padding-left: 1em"><a href="' + href + '">' + displayName + '</a></td>' +
                '</tr>\n';
            };
  
            dirs.sort(function (a, b) { return b[0] - a[0]; }).forEach(writeRow);
            files.sort(function (a, b) { return b.toString().localeCompare(a.toString()); }).forEach(writeRow);
            lolwuts.sort(function (a, b) { return b[0] - a[0] }).forEach(writeRow);
  
            html += '</table>\n';
            html += '<br><address>Node.js ' +
              process.version +
              '/ <a href="https://github.com/gstack/node-sexstatic">sexstatic</a> ' +
              'server running @ ' +
              he.encode(req.headers.host || '') + '</address>\n' +
              '</body></html>'
            ;
  
            if (!failed) {
              res.writeHead(200, { "Content-Type": "text/html" });
  
              for (var i=0;i<opts.modifyFunctions.length;i++)
              {
                html = opts.modifyFunctions[i](html);
              }
  
              res.end(html);
            }
          }
        });
      });
    };
  };
  
  function permsToString(stat) {
  
    if (!stat.isDirectory || !stat.mode) {
      return '???!!!???';
    }
  
    var dir = stat.isDirectory() ? 'd' : '-',
        mode = stat.mode.toString(8);
  
    return dir + mode.slice(-3).split('').map(function (n) {
      return [
        '---',
        '--x',
        '-w-',
        '-wx',
        'r--',
        'r-x',
        'rw-',
        'rwx'
      ][parseInt(n, 10)];
    }).join('');
  }
  
  // given a file's stat, return the size of it in string
  // humanReadable: (boolean) whether to result is human readable
  // si: (boolean) whether to use si (1k = 1000), otherwise 1k = 1024
  // adopted from http://stackoverflow.com/a/14919494/665507
  function sizeToString(stat, humanReadable, si) {
      if (stat.isDirectory && stat.isDirectory()) {
        return '';
      }
  
      var sizeString = '';
      var bytes = stat.size;
      var threshold = si ? 1000 : 1024;
  
      if (!humanReadable || bytes < threshold) {
        return bytes + 'B';
      }
  
      var units = [ 'k','M','G','T','P','E','Z','Y' ];
      var u = -1;
      do {
          bytes /= threshold;
          ++u;
      } while (bytes >= threshold);
  
      var b = bytes.toFixed(1);
      if (isNaN(b)) b = '??';
  
      return b + units[u];
  }
  