/**
 * Module dependencies.
 */

var debug = require('debug')('koa-send');
var resolvePath = require('resolve-path');
var assert = require('assert');
var path = require('path');
var normalize = path.normalize;
var basename = path.basename;
var extname = path.extname;
var resolve = path.resolve;
var parse = path.parse;
var sep = path.sep;
var fs = require('mz/fs');
var co = require('co');

/**
 * Expose `send()`.
 */

module.exports = send;

/**
 * Send file at `path` with the
 * given `options` to the koa `ctx`.
 *
 * @param {Context} ctx
 * @param {String} path
 * @param {Object} [opts]
 * @return {Function}
 * @api public
 */

function send(ctx, path, opts) {
  return co(function *(){

    assert(ctx, 'koa context required');
    assert(path, 'pathname required');
    opts = opts || {};

    // options
    debug('send "%s" %j', path, opts);
    var root = opts.root ? normalize(resolve(opts.root)) : '';
    var trailingSlash = '/' == path[path.length - 1];
    path = path.substr(parse(path).root.length);
    var index = opts.index;
    var maxage = opts.maxage || opts.maxAge || 0;
    var hidden = opts.hidden || false;
    var format = opts.format === false ? false : true;
    var extensions = Array.isArray(opts.extensions) ? opts.extensions : false;
    var gzip = opts.gzip === false ? false : true;
    var setHeaders = opts.setHeaders;

    if (setHeaders && typeof setHeaders !== 'function') {
      throw new TypeError('option setHeaders must be function')
    }

    var encoding = ctx.acceptsEncodings('gzip', 'deflate', 'identity');

    // normalize path
    path = decode(path);

    if (-1 == path) return ctx.throw('failed to decode', 400);

    // index file support
    if (index && trailingSlash) path += index;

    path = resolvePath(root, path);

    // hidden file support, ignore
    if (!hidden && isHidden(root, path)) return;

    // serve gzipped file when possible
    if (encoding === 'gzip' && gzip && (yield fs.exists(path + '.gz'))) {
      path = path + '.gz';
      ctx.set('Content-Encoding', 'gzip');
      ctx.res.removeHeader('Content-Length');
    }

    if (extensions && !/\..*$/.exec(path)) {
      var list = [].concat(extensions);
      for (var i = 0; i < list.length; i++) {
        var ext = list[i];
        if (typeof ext !== 'string') {
          throw new TypeError('option extensions must be array of strings or false');
        }
        if (!/^\./.exec(ext)) ext = '.' + ext;
        if (yield fs.exists(path + ext)) {
          path = path + ext;
          break;
        }
      }
    }

    // stat
    try {
      var stats = yield fs.stat(path);

      // Format the path to serve static file servers
      // and not require a trailing slash for directories,
      // so that you can do both `/directory` and `/directory/`
      if (stats.isDirectory()) {
        if (format && index) {
          path += '/' + index;
          stats = yield fs.stat(path);
        } else {
          return;
        }
      }
    } catch (err) {
      var notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
      if (~notfound.indexOf(err.code)) return;
      err.status = 500;
      throw err;
    }

    if (setHeaders) setHeaders(ctx.res, path, stats);

    // stream
    ctx.set('Content-Length', stats.size);
    if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString());
    if (!ctx.response.get('Cache-Control')) ctx.set('Cache-Control', 'max-age=' + (maxage / 1000 | 0));
    ctx.type = type(path);
    ctx.body = fs.createReadStream(path);

    return path;
  });
}

/**
 * Check if it's hidden.
 */

function isHidden(root, path) {
  path = path.substr(root.length).split(sep);
  for(var i = 0; i < path.length; i++) {
    if(path[i][0] === '.') return true;
  }
  return false;
}

/**
 * File type.
 */

function type(file) {
  return extname(basename(file, '.gz'));
}

/**
 * Decode `path`.
 */

function decode(path) {
  try {
    return decodeURIComponent(path);
  } catch (err) {
    return -1;
  }
}