module.exports = assert

/**
 * Available under MIT license
 * Josh Perez <http://josh.mit-license.org>
 * `isEqual` and its dependencies from:
 * Lo-Dash 1.0.1 <http://lodash.com/>
 * Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
 * Based on Underscore.js 1.4.4 <http://underscorejs.org/>
 * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
 */

var fn = require('fn')

/** `Object#toString` result shortcuts */
var argsClass = '[object Arguments]'
var arrayClass = '[object Array]'
var boolClass = '[object Boolean]'
var dateClass = '[object Date]'
var funcClass = '[object Function]'
var numberClass = '[object Number]'
var objectClass = '[object Object]'
var regexpClass = '[object RegExp]'
var stringClass = '[object String]'

/** Used to determine if values are of the language type Object */
var objectTypes = {
  'boolean': false,
  'function': true,
  'object': true,
  'number': false,
  'string': false,
  'undefined': false
}

var objectRef = {}
var hasOwnProperty = objectRef.hasOwnProperty

function forIn(collection, callback) {
  var index, iterable = collection, result = iterable

  if (!iterable) return result
  if (!objectTypes[typeof iterable]) return result

  for (index in iterable) {
    if (callback(iterable[index], index, collection) === false) return result
  }

  return result
}

function isEqual(a, b, callback, stackA, stackB) {
  // used to indicate that when comparing objects, `a` has at least the properties of `b`
  var whereIndicator = callback === objectRef
  if (callback && !whereIndicator) {
    var result = callback(a, b)
    if (typeof result != 'undefined') {
      return !!result
    }
  }
  // exit early for identical values
  if (a === b) {
    // treat `+0` vs. `-0` as not equal
    return a !== 0 || (1 / a == 1 / b)
  }
  var type = typeof a,
  otherType = typeof b

  // exit early for unlike primitive values
  if (a === a &&
      (!a || (type != 'function' && type != 'object')) &&
        (!b || (otherType != 'function' && otherType != 'object'))) {
    return false
  }
  // exit early for `null` and `undefined`, avoiding ES3's Function#call behavior
  // http://es5.github.com/#x15.3.4.4
  if (a == null || b == null) {
    return a === b
  }
  // compare [[Class]] names
  var className = toString.call(a),
  otherClass = toString.call(b)

  if (className == argsClass) {
    className = objectClass
  }
  if (otherClass == argsClass) {
    otherClass = objectClass
  }
  if (className != otherClass) {
    return false
  }
  switch (className) {
    case boolClass:
    case dateClass:
      // coerce dates and booleans to numbers, dates to milliseconds and booleans
      // to `1` or `0`, treating invalid dates coerced to `NaN` as not equal
      return +a == +b

    case numberClass:
      // treat `NaN` vs. `NaN` as equal
      return a != +a
        ? b != +b
        // but treat `+0` vs. `-0` as not equal
        : (a == 0 ? (1 / a == 1 / b) : a == +b)

    case regexpClass:
    case stringClass:
      // coerce regexes to strings (http://es5.github.com/#x15.10.6.4)
      // treat string primitives and their corresponding object instances as equal
      return a == b + ''
  }
  var isArr = className == arrayClass
  if (!isArr) {
    // unwrap any `lodash` wrapped values
    if (a.__wrapped__ || b.__wrapped__) {
      return isEqual(a.__wrapped__ || a, b.__wrapped__ || b, callback, stackA, stackB)
    }
    // exit for functions and DOM nodes
    if (className != objectClass) {
      return false
    }
    // in older versions of Opera, `arguments` objects have `Array` constructors
    var ctorA = a.constructor,
    ctorB = b.constructor

    // non `Object` object instances with different constructors are not equal
    if (ctorA != ctorB && !(
      isFunction(ctorA) && ctorA instanceof ctorA &&
        isFunction(ctorB) && ctorB instanceof ctorB
    )) {
      return false
    }
  }
  // assume cyclic structures are equal
  // the algorithm for detecting cyclic structures is adapted from ES 5.1
  // section 15.12.3, abstract operation `JO` (http://es5.github.com/#x15.12.3)
  stackA || (stackA = [])
  stackB || (stackB = [])

  var length = stackA.length
  while (length--) {
    if (stackA[length] == a) {
      return stackB[length] == b
    }
  }
  var size = 0
  result = true

  // add `a` and `b` to the stack of traversed objects
  stackA.push(a)
  stackB.push(b)

  // recursively compare objects and arrays (susceptible to call stack limits)
  if (isArr) {
    length = a.length
    size = b.length

    // compare lengths to determine if a deep comparison is necessary
    result = size == a.length
    if (!result && !whereIndicator) {
      return result
    }
    // deep compare the contents, ignoring non-numeric properties
    while (size--) {
      var index = length,
      value = b[size]

      if (whereIndicator) {
        while (index--) {
          if ((result = isEqual(a[index], value, callback, stackA, stackB))) {
            break
          }
        }
      } else if (!(result = isEqual(a[size], value, callback, stackA, stackB))) {
        break
      }
    }
    return result
  }
  // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys`
  // which, in this case, is more costly
  forIn(b, function(value, key, b) {
    if (hasOwnProperty.call(b, key)) {
      // count the number of properties.
      size++
      // deep compare each property value.
      return (result = hasOwnProperty.call(a, key) && isEqual(a[key], value, callback, stackA, stackB))
    }
  })

  if (result && !whereIndicator) {
    // ensure both objects have the same number of properties
    forIn(a, function(value, key, a) {
      if (hasOwnProperty.call(a, key)) {
        // `size` will be `-1` if `a` has more properties than `b`
        return (result = --size > -1)
      }
    })
  }
  return result
}

function getClass(value) {
  return objectRef.toString.call(value)
}

function eqClass(typeClass, value, message) {
  getClass(value) === typeClass || fail(value, typeClass, message)
  count++
}

function guardBlock(block, errorType, shouldThrow, message) {
  try {
    block()
    return shouldThrow && fail(block, errorType, message)
  } catch (err) {
    if (err.name == 'AssertionError') {
      throw err
    }
    if (errorType == null) {
      return shouldThrow || fail(block, errorType, message)
    } else {
      if (errorType instanceof RegExp) {
        return (errorType.test(err) && shouldThrow) ||
          fail(block, errorType, message)
      } else if (err instanceof errorType) {
        return shouldThrow || fail(block, errorType, message)
      } else if (errorType.call({}, err) === true) {
        return shouldThrow || fail(block, errorType, message)
      } else {
        throw err
      }
    }
  }
}

function fail(actual, expected, message) {
  throw new assert.AssertionError({
    message: message || 'Expected ' + expected + ' but received ' + actual,
    actual: actual,
    expected: expected,
  }, new Error(message))
}

var count = 0

function assert(value, message) {
  !!value || fail(value, true, message)
  count++
}

fn.mergeInto(assert, {
  fail: fail,
  ok: assert,
  getCount: function () { return count },
  equal: function (a, b, message) {
    a == b || fail.apply(fail, arguments)
    count++
  },
  notEqual: function (a, b, message) {
    a != b || fail.apply(fail, arguments)
    count++
  },
  deepEqual: function (a, b, message) {
    isEqual(a, b) || fail.apply(fail, arguments)
    count++
  },
  notDeepEqual: function (a, b, message) {
    !isEqual(a, b) || fail.apply(fail, arguments)
    count++
  },
  strictEqual: function (a, b, message) {
    a === b || fail.apply(fail, arguments)
    count++
  },
  notStrictEqual: function (a, b, message) {
    a !== b || fail.apply(fail, arguments)
    count++
  },
  throws: function (block, errorType, message) {
    guardBlock(block, errorType, true, message)
    count++
  },
  doesNotThrow: function (block, errorType, message) {
    guardBlock(block, errorType, false, message)
    count++
  },
  isFunction: fn.curry(eqClass, funcClass),
  isNumber: fn.curry(eqClass, numberClass),
  isString: fn.curry(eqClass, stringClass),
  isBoolean: fn.curry(eqClass, boolClass),
  isArray: fn.curry(eqClass, arrayClass),
  isObject: fn.curry(eqClass, objectClass),
  isArguments: fn.curry(eqClass, argsClass),
  isDate: fn.curry(eqClass, dateClass),
  isRegExp: fn.curry(eqClass, regexpClass),
  isUndefined: function (x, message) {
    x === undefined || fail(x, undefined, message)
    count++
  },
  isNull: function (x, message) {
    x === null || fail(x, null, message)
    count++
  },
  isNaN: function (x, message) {
    Number.isNaN(x) || fail(x, NaN, message)
    count++
  },
  isTrue: function (x, message) {
    x === true || fail(x, true, message)
    count++
  },
  isFalse: function (x, message) {
    x === false || fail(x, false, message)
    count++
  },
  isEmpty: function (x, message) {
    switch (getClass(x)) {
      case stringClass:
      case objectClass:
      case arrayClass:
      case argsClass:
        (hasOwnProperty.call(x, 'length')
          ? x.length === 0
          : Object.keys(x).length === 0) || fail(x, '{empty}', message)
      default:
        throw new TypeError('Cannot check for emptiness')
    }
    count++
  },
  AssertionError: function (options, err) {
    this.name = 'AssertionError'
    this.message = options.message
    this.actual = options.actual
    this.expected = options.expected
    this.operator = options.operator
    this.stack = err.stack
  },
  extend: function (obj) {
    return fn.mergeInto(assert, obj)
  }
})

assert.AssertionError.prototype = Object.create(Error.prototype)

fn.mergeInto(assert.AssertionError.prototype, {
  toString: function () {
    if (this.message) {
      return [this.name, ':', this.message].join(' ')
    } else {
      return this.stack
    }
  }
})
