/**
 * Module dependencies.
 */

var fs = require('fs')
  , path = require('path')
  , basename = path.basename
  , exists = fs.existsSync || path.existsSync
  , glob = require('glob')
  , join = path.join
  , debug = require('debug')('mocha:watch');

/**
 * Ignored directories.
 */

var ignore = ['node_modules', '.git'];

/**
 * Escape special characters in the given string of html.
 *
 * @param  {String} html
 * @return {String}
 * @api private
 */

exports.escape = function(html){
  return String(html)
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
};

/**
 * Array#forEach (<=IE8)
 *
 * @param {Array} array
 * @param {Function} fn
 * @param {Object} scope
 * @api private
 */

exports.forEach = function(arr, fn, scope){
  for (var i = 0, l = arr.length; i < l; i++)
    fn.call(scope, arr[i], i);
};

/**
 * Array#map (<=IE8)
 *
 * @param {Array} array
 * @param {Function} fn
 * @param {Object} scope
 * @api private
 */

exports.map = function(arr, fn, scope){
  var result = [];
  for (var i = 0, l = arr.length; i < l; i++)
    result.push(fn.call(scope, arr[i], i, arr));
  return result;
};

/**
 * Array#indexOf (<=IE8)
 *
 * @parma {Array} arr
 * @param {Object} obj to find index of
 * @param {Number} start
 * @api private
 */

exports.indexOf = function(arr, obj, start){
  for (var i = start || 0, l = arr.length; i < l; i++) {
    if (arr[i] === obj)
      return i;
  }
  return -1;
};

/**
 * Array#reduce (<=IE8)
 *
 * @param {Array} array
 * @param {Function} fn
 * @param {Object} initial value
 * @api private
 */

exports.reduce = function(arr, fn, val){
  var rval = val;

  for (var i = 0, l = arr.length; i < l; i++) {
    rval = fn(rval, arr[i], i, arr);
  }

  return rval;
};

/**
 * Array#filter (<=IE8)
 *
 * @param {Array} array
 * @param {Function} fn
 * @api private
 */

exports.filter = function(arr, fn){
  var ret = [];

  for (var i = 0, l = arr.length; i < l; i++) {
    var val = arr[i];
    if (fn(val, i, arr)) ret.push(val);
  }

  return ret;
};

/**
 * Object.keys (<=IE8)
 *
 * @param {Object} obj
 * @return {Array} keys
 * @api private
 */

exports.keys = Object.keys || function(obj) {
  var keys = []
    , has = Object.prototype.hasOwnProperty; // for `window` on <=IE8

  for (var key in obj) {
    if (has.call(obj, key)) {
      keys.push(key);
    }
  }

  return keys;
};

/**
 * Watch the given `files` for changes
 * and invoke `fn(file)` on modification.
 *
 * @param {Array} files
 * @param {Function} fn
 * @api private
 */

exports.watch = function(files, fn){
  var options = { interval: 100 };
  files.forEach(function(file){
    debug('file %s', file);
    fs.watchFile(file, options, function(curr, prev){
      if (prev.mtime < curr.mtime) fn(file);
    });
  });
};

/**
 * Array.isArray (<=IE8)
 *
 * @param {Object} obj
 * @return {Boolean}
 * @api private
 */
var isArray = Array.isArray || function (obj) {
  return '[object Array]' == {}.toString.call(obj);
};

/**
 * @description
 * Buffer.prototype.toJSON polyfill
 * @type {Function}
 */
if(typeof Buffer !== 'undefined' && Buffer.prototype) {
  Buffer.prototype.toJSON = Buffer.prototype.toJSON || function () {
    return Array.prototype.slice.call(this, 0);
  };
}

/**
 * Ignored files.
 */

function ignored(path){
  return !~ignore.indexOf(path);
}

/**
 * Lookup files in the given `dir`.
 *
 * @return {Array}
 * @api private
 */

exports.files = function(dir, ext, ret){
  ret = ret || [];
  ext = ext || ['js'];

  var re = new RegExp('\\.(' + ext.join('|') + ')$');

  fs.readdirSync(dir)
    .filter(ignored)
    .forEach(function(path){
      path = join(dir, path);
      if (fs.statSync(path).isDirectory()) {
        exports.files(path, ext, ret);
      } else if (path.match(re)) {
        ret.push(path);
      }
    });

  return ret;
};

/**
 * Compute a slug from the given `str`.
 *
 * @param {String} str
 * @return {String}
 * @api private
 */

exports.slug = function(str){
  return str
    .toLowerCase()
    .replace(/ +/g, '-')
    .replace(/[^-\w]/g, '');
};

/**
 * Strip the function definition from `str`,
 * and re-indent for pre whitespace.
 */

exports.clean = function(str) {
  str = str
    .replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, '')
    .replace(/^function *\(.*\) *{|\(.*\) *=> *{?/, '')
    .replace(/\s+\}$/, '');

  var spaces = str.match(/^\n?( *)/)[1].length
    , tabs = str.match(/^\n?(\t*)/)[1].length
    , re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm');

  str = str.replace(re, '');

  return exports.trim(str);
};

/**
 * Trim the given `str`.
 *
 * @param {String} str
 * @return {String}
 * @api private
 */

exports.trim = function(str){
  return str.replace(/^\s+|\s+$/g, '');
};

/**
 * Parse the given `qs`.
 *
 * @param {String} qs
 * @return {Object}
 * @api private
 */

exports.parseQuery = function(qs){
  return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){
    var i = pair.indexOf('=')
      , key = pair.slice(0, i)
      , val = pair.slice(++i);

    obj[key] = decodeURIComponent(val);
    return obj;
  }, {});
};

/**
 * Highlight the given string of `js`.
 *
 * @param {String} js
 * @return {String}
 * @api private
 */

function highlight(js) {
  return js
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/\/\/(.*)/gm, '<span class="comment">//$1</span>')
    .replace(/('.*?')/gm, '<span class="string">$1</span>')
    .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>')
    .replace(/(\d+)/gm, '<span class="number">$1</span>')
    .replace(/\bnew[ \t]+(\w+)/gm, '<span class="keyword">new</span> <span class="init">$1</span>')
    .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '<span class="keyword">$1</span>')
}

/**
 * Highlight the contents of tag `name`.
 *
 * @param {String} name
 * @api private
 */

exports.highlightTags = function(name) {
  var code = document.getElementById('mocha').getElementsByTagName(name);
  for (var i = 0, len = code.length; i < len; ++i) {
    code[i].innerHTML = highlight(code[i].innerHTML);
  }
};

/**
 * If a value could have properties, and has none, this function is called, which returns
 * a string representation of the empty value.
 *
 * Functions w/ no properties return `'[Function]'`
 * Arrays w/ length === 0 return `'[]'`
 * Objects w/ no properties return `'{}'`
 * All else: return result of `value.toString()`
 *
 * @param {*} value Value to inspect
 * @param {string} [type] The type of the value, if known.
 * @returns {string}
 */
var emptyRepresentation = function emptyRepresentation(value, type) {
  type = type || exports.type(value);

  switch(type) {
    case 'function':
      return '[Function]';
    case 'object':
      return '{}';
    case 'array':
      return '[]';
    default:
      return value.toString();
  }
};

/**
 * Takes some variable and asks `{}.toString()` what it thinks it is.
 * @param {*} value Anything
 * @example
 * type({}) // 'object'
 * type([]) // 'array'
 * type(1) // 'number'
 * type(false) // 'boolean'
 * type(Infinity) // 'number'
 * type(null) // 'null'
 * type(new Date()) // 'date'
 * type(/foo/) // 'regexp'
 * type('type') // 'string'
 * type(global) // 'global'
 * @api private
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
 * @returns {string}
 */
exports.type = function type(value) {
  if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
    return 'buffer';
  }
  return Object.prototype.toString.call(value)
    .replace(/^\[.+\s(.+?)\]$/, '$1')
    .toLowerCase();
};

/**
 * @summary Stringify `value`.
 * @description Different behavior depending on type of value.
 * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively.
 * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes.
 * - If `value` is an *empty* object, function, or array, return result of function
 *   {@link emptyRepresentation}.
 * - If `value` has properties, call {@link exports.canonicalize} on it, then return result of
 *   JSON.stringify().
 *
 * @see exports.type
 * @param {*} value
 * @return {string}
 * @api private
 */

exports.stringify = function(value) {
  var type = exports.type(value);

  if (!~exports.indexOf(['object', 'array', 'function'], type)) {
    if(type != 'buffer') {
      return jsonStringify(value);
    }
    var json = value.toJSON();
    // Based on the toJSON result
    return jsonStringify(json.data && json.type ? json.data : json, 2)
      .replace(/,(\n|$)/g, '$1');
  }

  for (var prop in value) {
    if (Object.prototype.hasOwnProperty.call(value, prop)) {
      return jsonStringify(exports.canonicalize(value), 2)
        .replace(/,(\n|$)/g, '$1');
    }
  }

  return emptyRepresentation(value, type);
};

/**
 * @description
 * like JSON.stringify but more sense.
 * @param {Object}  object
 * @param {Number=} spaces
 * @param {number=} depth
 * @returns {*}
 * @private
 */
function jsonStringify(object, spaces, depth) {
  if(typeof spaces == 'undefined') return _stringify(object);  // primitive types

  depth = depth || 1;
  var space = spaces * depth
    , str = isArray(object) ? '[' : '{'
    , end = isArray(object) ? ']' : '}'
    , length = object.length || exports.keys(object).length
    , repeat = function(s, n) { return new Array(n).join(s); }; // `.repeat()` polyfill

  function _stringify(val) {
    switch (exports.type(val)) {
      case 'null':
      case 'undefined':
        val = '[' + val + ']';
        break;
      case 'array':
      case 'object':
        val = jsonStringify(val, spaces, depth + 1);
        break;
      case 'boolean':
      case 'regexp':
      case 'number':
        val = val === 0 && (1/val) === -Infinity // `-0`
          ? '-0'
          : val.toString();
        break;
      case 'date':
        val = '[Date: ' + val.toISOString() + ']';
        break;
      case 'buffer':
        var json = val.toJSON();
        // Based on the toJSON result
        json = json.data && json.type ? json.data : json;
        val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']';
        break;
      default:
        val = (val == '[Function]' || val == '[Circular]')
          ? val
          : '"' + val + '"'; //string
    }
    return val;
  }

  for(var i in object) {
    if(!Object.prototype.hasOwnProperty.call(object, i)) continue;        // not my business
    --length;
    str += '\n ' + repeat(' ', space)
      + (isArray(object) ? '' : '"' + i + '": ') // key
      +  _stringify(object[i])                   // value
      + (length ? ',' : '');                     // comma
  }

  return str + (str.length != 1                    // [], {}
    ? '\n' + repeat(' ', --space) + end
    : end);
}

/**
 * Return if obj is a Buffer
 * @param {Object} arg
 * @return {Boolean}
 * @api private
 */
exports.isBuffer = function (arg) {
  return typeof Buffer !== 'undefined' && Buffer.isBuffer(arg);
};

/**
 * @summary Return a new Thing that has the keys in sorted order.  Recursive.
 * @description If the Thing...
 * - has already been seen, return string `'[Circular]'`
 * - is `undefined`, return string `'[undefined]'`
 * - is `null`, return value `null`
 * - is some other primitive, return the value
 * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method
 * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again.
 * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()`
 *
 * @param {*} value Thing to inspect.  May or may not have properties.
 * @param {Array} [stack=[]] Stack of seen values
 * @return {(Object|Array|Function|string|undefined)}
 * @see {@link exports.stringify}
 * @api private
 */

exports.canonicalize = function(value, stack) {
  var canonicalizedObj,
    type = exports.type(value),
    prop,
    withStack = function withStack(value, fn) {
      stack.push(value);
      fn();
      stack.pop();
    };

  stack = stack || [];

  if (exports.indexOf(stack, value) !== -1) {
    return '[Circular]';
  }

  switch(type) {
    case 'undefined':
    case 'buffer':
    case 'null':
      canonicalizedObj = value;
      break;
    case 'array':
      withStack(value, function () {
        canonicalizedObj = exports.map(value, function (item) {
          return exports.canonicalize(item, stack);
        });
      });
      break;
    case 'function':
      for (prop in value) {
        canonicalizedObj = {};
        break;
      }
      if (!canonicalizedObj) {
        canonicalizedObj = emptyRepresentation(value, type);
        break;
      }
    /* falls through */
    case 'object':
      canonicalizedObj = canonicalizedObj || {};
      withStack(value, function () {
        exports.forEach(exports.keys(value).sort(), function (key) {
          canonicalizedObj[key] = exports.canonicalize(value[key], stack);
        });
      });
      break;
    case 'date':
    case 'number':
    case 'regexp':
    case 'boolean':
      canonicalizedObj = value;
      break;
    default:
      canonicalizedObj = value.toString();
  }

  return canonicalizedObj;
};

/**
 * Lookup file names at the given `path`.
 */
exports.lookupFiles = function lookupFiles(path, extensions, recursive) {
  var files = [];
  var re = new RegExp('\\.(' + extensions.join('|') + ')$');

  if (!exists(path)) {
    if (exists(path + '.js')) {
      path += '.js';
    } else {
      files = glob.sync(path);
      if (!files.length) throw new Error("cannot resolve path (or pattern) '" + path + "'");
      return files;
    }
  }

  try {
    var stat = fs.statSync(path);
    if (stat.isFile()) return path;
  }
  catch (ignored) {
    return;
  }

  fs.readdirSync(path).forEach(function(file) {
    file = join(path, file);
    try {
      var stat = fs.statSync(file);
      if (stat.isDirectory()) {
        if (recursive) {
          files = files.concat(lookupFiles(file, extensions, recursive));
        }
        return;
      }
    }
    catch (ignored) {
      return;
    }
    if (!stat.isFile() || !re.test(file) || basename(file)[0] === '.') return;
    files.push(file);
  });

  return files;
};

/**
 * Generate an undefined error with a message warning the user.
 *
 * @return {Error}
 */

exports.undefinedError = function() {
  return new Error('Caught undefined error, did you throw without specifying what?');
};

/**
 * Generate an undefined error if `err` is not defined.
 *
 * @param {Error} err
 * @return {Error}
 */

exports.getError = function(err) {
  return err || exports.undefinedError();
};


/**
 * @summary
 * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`)
 * @description
 * When invoking this function you get a filter function that get the Error.stack as an input,
 * and return a prettify output.
 * (i.e: strip Mocha, node_modules, bower and componentJS from stack trace).
 * @returns {Function}
 */

exports.stackTraceFilter = function() {
  var slash = '/'
    , is = typeof document === 'undefined'
      ? { node: true }
      : { browser: true }
    , cwd = is.node
      ? process.cwd() + slash
      : location.href.replace(/\/[^\/]*$/, '/');

  function isNodeModule (line) {
    return (~line.indexOf('node_modules'));
  }

  function isMochaInternal (line) {
    return (~line.indexOf('node_modules' + slash + 'tap-mocha-reporter'))  ||
      (~line.indexOf('components' + slash + 'mochajs'))       ||
      (~line.indexOf('components' + slash + 'mocha'));
  }

  // node_modules, bower, componentJS
  function isBrowserModule(line) {
    return (~line.indexOf('node_modules')) ||
      (~line.indexOf('components'));
  }

  function isNodeInternal (line) {
    return (~line.indexOf('(timers.js:')) ||
      (~line.indexOf('(domain.js:'))      ||
      (~line.indexOf('(events.js:'))      ||
      (~line.indexOf('(node.js:'))        ||
      (~line.indexOf('(module.js:'))      ||
      (~line.indexOf('at node.js:'))      ||
      (~line.indexOf('GeneratorFunctionPrototype.next (native)')) ||
      false
  }

  return function(stack) {
    stack = stack.split('\n');

    stack = stack.reduce(function (list, line) {
      if (is.node && (isNodeModule(line) ||
        isMochaInternal(line) ||
        isNodeInternal(line)))
        return list;

      if (is.browser && (isBrowserModule(line)))
        return list;

      // Clean up cwd(absolute)
      list.push(line.replace(cwd, ''));
      return list;
    }, []);

    return stack.join('\n');
  }
};
