text: var ERRORS = require('./errors')
  var NATIVE = require('./native')
  
  // short-hand
  var tfJSON = ERRORS.tfJSON
  var TfTypeError = ERRORS.TfTypeError
  var TfPropertyTypeError = ERRORS.TfPropertyTypeError
  var tfSubError = ERRORS.tfSubError
  var getValueTypeName = ERRORS.getValueTypeName
  
  var TYPES = {
    arrayOf: function arrayOf (type) {
      type = compile(type)
  
      function _arrayOf (array, strict) {
        if (!NATIVE.Array(array)) return false
  
        return array.every(function (value, i) {
          try {
            return typeforce(type, value, strict)
          } catch (e) {
            throw tfSubError(e, i)
          }
        })
      }
      _arrayOf.toJSON = function () { return '[' + tfJSON(type) + ']' }
  
      return _arrayOf
    },
  
    maybe: function maybe (type) {
      type = compile(type)
  
      function _maybe (value, strict) {
        return NATIVE.Nil(value) || type(value, strict, maybe)
      }
      _maybe.toJSON = function () { return '?' + tfJSON(type) }
  
      return _maybe
    },
  
    map: function map (propertyType, propertyKeyType) {
      propertyType = compile(propertyType)
      if (propertyKeyType) propertyKeyType = compile(propertyKeyType)
  
      function _map (value, strict) {
        if (!NATIVE.Object(value, strict)) return false
        if (NATIVE.Nil(value, strict)) return false
  
        for (var propertyName in value) {
          try {
            if (propertyKeyType) {
              typeforce(propertyKeyType, propertyName, strict)
            }
          } catch (e) {
            throw tfSubError(e, propertyName, 'key')
          }
  
          try {
            var propertyValue = value[propertyName]
            typeforce(propertyType, propertyValue, strict)
          } catch (e) {
            throw tfSubError(e, propertyName)
          }
        }
  
        return true
      }
  
      if (propertyKeyType) {
        _map.toJSON = function () {
          return '{' + tfJSON(propertyKeyType) + ': ' + tfJSON(propertyType) + '}'
        }
      } else {
        _map.toJSON = function () { return '{' + tfJSON(propertyType) + '}' }
      }
  
      return _map
    },
  
    object: function object (uncompiled) {
      var type = {}
  
      for (var typePropertyName in uncompiled) {
        type[typePropertyName] = compile(uncompiled[typePropertyName])
      }
  
      function _object (value, strict) {
        if (!NATIVE.Object(value)) return false
        if (NATIVE.Nil(value)) return false
  
        var propertyName
  
        try {
          for (propertyName in type) {
            var propertyType = type[propertyName]
            var propertyValue = value[propertyName]
  
            typeforce(propertyType, propertyValue, strict)
          }
        } catch (e) {
          throw tfSubError(e, propertyName)
        }
  
        if (strict) {
          for (propertyName in value) {
            if (type[propertyName]) continue
  
            throw new TfPropertyTypeError(undefined, propertyName)
          }
        }
  
        return true
      }
      _object.toJSON = function () { return tfJSON(type) }
  
      return _object
    },
  
    oneOf: function oneOf () {
      var types = [].slice.call(arguments).map(compile)
  
      function _oneOf (value, strict) {
        return types.some(function (type) {
          try {
            return typeforce(type, value, strict)
          } catch (e) {
            return false
          }
        })
      }
      _oneOf.toJSON = function () { return types.map(tfJSON).join('|') }
  
      return _oneOf
    },
  
    quacksLike: function quacksLike (type) {
      function _quacksLike (value) {
        return type === getValueTypeName(value)
      }
      _quacksLike.toJSON = function () { return type }
  
      return _quacksLike
    },
  
    tuple: function tuple () {
      var types = [].slice.call(arguments).map(compile)
  
      function _tuple (values, strict) {
        return types.every(function (type, i) {
          try {
            return typeforce(type, values[i], strict)
          } catch (e) {
            throw tfSubError(e, i)
          }
        }) && (!strict || values.length === arguments.length)
      }
      _tuple.toJSON = function () { return '(' + types.map(tfJSON).join(', ') + ')' }
  
      return _tuple
    },
  
    value: function value (expected) {
      function _value (actual) {
        return actual === expected
      }
      _value.toJSON = function () { return expected }
  
      return _value
    }
  }
  
  function compile (type) {
    if (NATIVE.String(type)) {
      if (type[0] === '?') return TYPES.maybe(compile(type.slice(1)))
  
      return NATIVE[type] || TYPES.quacksLike(type)
    } else if (type && NATIVE.Object(type)) {
      if (NATIVE.Array(type)) return TYPES.arrayOf(compile(type[0]))
  
      return TYPES.object(type)
    } else if (NATIVE.Function(type)) {
      return type
    }
  
    return TYPES.value(type)
  }
  
  function typeforce (type, value, strict, surrogate) {
    if (NATIVE.Function(type)) {
      if (type(value, strict)) return true
  
      throw new TfTypeError(surrogate || type, value)
    }
  
    // JIT
    return typeforce(compile(type), value, strict)
  }
  
  // assign types to typeforce function
  for (var typeName in NATIVE) {
    typeforce[typeName] = NATIVE[typeName]
  }
  
  for (typeName in TYPES) {
    typeforce[typeName] = TYPES[typeName]
  }
  
  var EXTRA = require('./extra')
  for (typeName in EXTRA) {
    typeforce[typeName] = EXTRA[typeName]
  }
  
  // async wrapper
  function __async (type, value, strict, callback) {
    // default to falsy strict if using shorthand overload
    if (typeof strict === 'function') return __async(type, value, false, strict)
  
    try {
      typeforce(type, value, strict)
    } catch (e) {
      return callback(e)
    }
  
    callback()
  }
  
  typeforce.async = __async
  typeforce.compile = compile
  typeforce.TfTypeError = TfTypeError
  typeforce.TfPropertyTypeError = TfPropertyTypeError
  
  module.exports = typeforce
  