var sass = require('../sass')
  , fs = require('fs')
  , url = require('url')
  , basename = require('path').basename
  , dirname = require('path').dirname
  , mkdirp = require('mkdirp')
  , join = require('path').join;

var imports = {};

/**
 * Return Connect middleware with the given `options`.
 *
 * Options:
 *
 *    `force`     Always re-compile
 *    `debug`     Output debugging information
 *    `src`       Source directory used to find .styl files
 *    `dest`      Destination directory used to output .css files
 *                when undefined defaults to `src`.
 *
 * Examples:
 *
 * Pass the middleware to Connect, grabbing .scss files from this directory
 * and saving .css files to _./public_.
 *
 * Following that we have a `staticProvider` layer setup to serve the .css
 * files generated by Sass.
 *
 *      var server = connect.createServer(
 *          sass.middleware({
 *              src: __dirname
 *            , dest: __dirname + '/public'
 *          })
 *        , connect.static(__dirname + '/public')
 *      );
 *
 * @param {Object} options
 * @return {Function}
 * @api public
 */

module.exports = function(options){
  options = options || {};

  // Accept src/dest dir
  if ('string' == typeof options) {
    options = { src: options };
  }

  // Force compilation
  var force = options.force;

  // Debug option
  var debug = options.debug;

  // Source dir required
  var src = options.src;
  if (!src) throw new Error('sass.middleware() requires "src" directory');

  // Default dest dir to source
  var dest = options.dest
    ? options.dest
    : src;

  // Default compile callback
  options.compile = options.compile || function(){
    return sass
  };

  // Middleware
  return function sass(req, res, next){
    if ('GET' != req.method && 'HEAD' != req.method) return next();
    var path = url.parse(req.url).pathname;
    if (/\.css$/.test(path)) {
      var cssPath = join(dest, path)
        , sassPath = join(src, path.replace('.css', '.scss'))
        , sassDir = dirname(sassPath)

      if (debug) {
        log('source', sassPath);
        log('dest', cssPath);
      }

      // Ignore ENOENT to fall through as 404
      function error(err) {
        next('ENOENT' == err.code
          ? null
          : err);
      }

      // Force
      if (force) return compile();

      // Compile to cssPath
      function compile() {
        if (debug) log('read', cssPath);
        fs.readFile(sassPath, 'utf8', function(err, str){
          if (err) return error(err);
          var style = options.compile();
          var paths = [];
          delete imports[sassPath];
          style.render(str, function(err, css){
            if (err) return next(err);
            if (debug) log('render', sassPath);
            imports[sassPath] = paths;
            mkdirp(dirname(cssPath), 0700, function(err){
              if (err) return error(err);
              fs.writeFile(cssPath, css, 'utf8', next);
            });
          }, {
            include_paths: [ sassDir ].concat(options.include_paths || []),
            output_style: options.output_style
          });
        });
      }

      // Re-compile on server restart, disregarding
      // mtimes since we need to map imports
      if (!imports[sassPath]) return compile();

      // Compare mtimes
      fs.stat(sassPath, function(err, sassStats){
        if (err) return error(err);
        fs.stat(cssPath, function(err, cssStats){
          // CSS has not been compiled, compile it!
          if (err) {
            if ('ENOENT' == err.code) {
              if (debug) log('not found', cssPath);
              compile();
            } else {
              next(err);
            }
          } else {
            // Source has changed, compile it
            if (sassStats.mtime > cssStats.mtime) {
              if (debug) log('modified', cssPath);
              compile();
            // Already compiled, check imports
            } else {
              checkImports(sassPath, function(changed){
                if (debug && changed && changed.length) {
                  changed.forEach(function(path) {
                    log('modified import %s', path);
                  });
                }
                changed && changed.length ? compile() : next();
              });
            }
          }
        });
      });
    } else {
      next();
    }
  }
};

/**
 * Check `path`'s imports to see if they have been altered.
 *
 * @param {String} path
 * @param {Function} fn
 * @api private
 */

function checkImports(path, fn) {
  var nodes = imports[path];
  if (!nodes) return fn();
  if (!nodes.length) return fn();

  var pending = nodes.length
    , changed = [];

  nodes.forEach(function(imported){
    fs.stat(imported.path, function(err, stat){
      // error or newer mtime
      if (err || !imported.mtime || stat.mtime > imported.mtime) {
        changed.push(imported.path);
      }
      --pending || fn(changed);
    });
  });
}

/**
 * Log a message.
 *
 * @api private
 */

function log(key, val) {
  console.error('  \033[90m%s :\033[0m \033[36m%s\033[0m', key, val);
}
