'use strict'

var assert = require('assert')
var inherits = require('util').inherits
var events = require('events')
var async = require('async')
var _ = require('lodash')
var constants = require('./constants')
var utils = require('./utils')
var Message = require('./message')
var Contact = require('./contact')
var Item = require('./item')
var RoutingTable = require('./routing-table')

/**
 * Represents a routing table of known {@link Contact}s; used by {@link Node}.
 * @constructor
 * @param {Object} options
 * @param {Logger} options.logger - Logger instance to use
 * @param {RPC} options.transport - Transport adapter (RPC) to use
 * @param {Router~validator} options.validator - Key-Value validation function
 * @emits Router#add
 * @emits Router#drop
 * @emits Router#shift
 */
function Router (options) {
  if (!(this instanceof Router)) {
    return new Router(options)
  }
  this._log = options.logger
  this._rpc = options.transport
  this._self = this._rpc._contact
  this._validator = options.validator
  this._routingTable = new RoutingTable(options.storage, this._rpc)
}
/**
 * Called when a value is returned from a lookup to validate it
 * @callback Router~validator
 * @param {String} key - The key at which the value is stored
 * @param {String} value - The returned value from the lookup
 * @param {Function} callback - Called with boolean indicating validity
 */

/**
 * Add event is triggered when a new {@link Contact} is added to the router
 * @event Router#add
 */

/**
 * Drop event is triggered when a {@link Contact} is dropped from the router
 * @event Router#drop
 */

/**
 * Shift event is triggered when a {@link Contact} changes position in a
 * {@link Bucket}
 * @event Router#shift
 */

inherits(Router, events.EventEmitter)

/**
 * Execute this router's find operation with the shortlist
 * @param {String} type - One of "NODE" or "VALUE"
 * @param {String} key - Key to use for lookup
 * @param {Router~lookupCallback} callback - Called when the lookup is complete
 */
Router.prototype.lookup = function (type, key, callback) {
  assert(['NODE', 'VALUE'].indexOf(type) !== -1, 'Invalid search type')
  var self = this
  this._createLookupState(type, key, function (err, state) {
    if (!state.closestNode) {
      return callback(new Error('Not connected to any peers'))
    }
    state.closestNodeDistance = utils.getDistance(
      state.hashedKey,
      state.closestNode.nodeID
    )
    self._log.debug('performing network walk', {
      type: type,
      key: key
    })
    self._iterativeFind(state, state.shortlist, callback)
  })
}
/**
 * This callback is called upon completion of {@link Router#lookup}
 * @callback Router~lookupCallback
 * @param {Error|null} err - The error object, if any
 * @param {String} type - One of "NODE" or "VALUE"
 * @param {Array|String} result - The {@link Contact}s returned or the value
 */

/**
 * Returns the number of contacts in the routing table - used as the getter for
 * the Router#length property
 * @private
 */
Router.prototype.getSize = function (callback) {
  this._routingTable.getSize(callback)
}

/**
 * Empties the routing table, clearing all known contacts
 */
Router.prototype.empty = function (callback) {
  this._routingTable.empty(callback)
}

Router.prototype.isConnected = function (contact, callback) {
  this._routingTable.inTable(contact, function (err) {
    if (err) {
      return callback()
    } else {
      return callback(new Error('Contact already in routing table'))
    }
  })
}

/**
 * Removes the given contact from the routing table
 * @param {Contact} contact - The contact to drop from the router
 * @param {Function} callback - Contact was in expected bucket
 */
Router.prototype.removeContact = function (contact, callback) {
  var self = this
  var index = utils.getBucketIndex(this._self.nodeID, contact.nodeID)
  this._log.debug('removing contact', {
    contact: contact
  })
  assert(index < constants.B, 'Bucket index may not exceed B')
  async.waterfall([
    self._routingTable.getBucket.bind(self._routingTable, index),
    function (bucket, removeCb) {
      bucket.removeContact(contact, function (err, contact) {
        return removeCb(err, bucket, contact)
      })
    },
    function (bucket, contact, saveCb) {
      bucket.save(saveCb)
    },
    function (dropCb) {
      self.emit('drop', contact)
      return dropCb()
    }
  ], callback)
}

/**
 * Returns the {@link Contact} in the routing table by the given node ID
 * @param {String} nodeID - The nodeID of the {@link Contact} to return
 */
Router.prototype.getContactByNodeID = function (nodeID, callback) {
  var self = this
  var lookup = function (nodeID, lookupCb) {
    self.lookup('NODE', nodeID, function (err, type, shortlist) {
      if (err) {
        return lookupCb(err)
      } else {
        var result = _.find(shortlist, function (contact) {
          return nodeID === contact.nodeID
        })
        if (result) {
          return lookupCb(null, result)
        } else {
          return lookupCb(new Error('Contact not found'))
        }
      }
    })
  }
  this._routingTable.getContact(nodeID, function (err, result) {
    if (err) {
      return lookup(nodeID, callback)
    } else {
      return callback(null, result)
    }
  })
}

/**
 * Creates a state machine for a lookup operation
 * @private
 * @param {String} type - One of 'NODE' or 'VALUE'
 * @param {String} key - 160 bit key
 * @returns {Object} Lookup state machine
 */
Router.prototype._createLookupState = function (type, key, callback) {
  var state = {
    type: type,
    key: key,
    hashedKey: utils.createID(key),
    limit: constants.ALPHA,
    previousClosestNode: null,
    contacted: {},
    foundValue: false,
    value: null,
    contactsWithoutValue: []
  }
  this.getNearestContacts(
    key,
    state.limit,
    this._self.nodeID,
    function (err, shortlist) {
      state.shortlist = shortlist
      state.closestNode = state.shortlist[0]
      return callback(err, state)
    }
  )
}

/**
 * Execute the find operation for this router type
 * @private
 * @param {Object} state - State machine returned from _createLookupState()
 * @param {Array} contacts - List of contacts to query
 * @param {Function} callback
 */
Router.prototype._iterativeFind = function (state, contacts, callback) {
  var self = this
  var failures = 0

  function queryContact (contact, next) {
    self._queryContact(state, contact, function (err) {
      if (err) {
        failures++
      }
      return next()
    })
  }

  this._log.debug('starting contact iteration', {
    key: state.key
  })
  async.each(contacts, queryContact, function () {
    self._log.debug('finished iteration, handling results')

    if (failures === contacts.length) {
      return callback(new Error('Lookup operation failed to return results'))
    }

    self._handleQueryResults(state, callback)
  })
}

/**
 * Send this router's RPC message to the contact
 * @private
 * @param {Object} state - State machine returned from _createLookupState()
 * @param {Contact} contact - Contact to query
 * @param {Function} callback
 */
Router.prototype._queryContact = function (state, contactInfo, callback) {
  var self = this
  var contact = this._rpc._createContact(contactInfo)
  var message = new Message({
    method: 'FIND_' + state.type,
    params: {
      key: state.key,
      contact: this._self
    }
  })

  this._log.debug('querying', {
    nodeID: contact.nodeID,
    key: state.key
  })
  this._rpc.send(contact, message, function (err, response) {
    if (err) {
      self._log.warn(
        'query failed, removing contact for shortlist', {
          error: err.message
        }
      )
      self._removeFromShortList(state, contact.nodeID)
      self.removeContact(contact)
      return callback(err)
    }

    self._handleFindResult(state, response, contact, callback)
  })
}

/**
 * Handle the results of the contact query
 * @private
 * @param {Object} state - State machine returned from _createLookupState()
 * @param {Message} message - Received response to FIND_* RPC
 * @param {Contact} contact - Sender of the message
 * @param {Function} callback
 */
Router.prototype._handleFindResult = function (state, msg, contact, callback) {
  var distance = utils.getDistance(state.hashedKey, contact.nodeID)

  state.contacted[contact.nodeID] = this.updateContact(contact)

  if (utils.compareKeys(distance, state.closestNodeDistance) === -1) {
    state.previousClosestNode = state.closestNode
    state.closestNode = contact
    state.closestNodeDistance = distance
  }

  if (state.type === 'NODE') {
    this._addToShortList(state, msg.result.nodes)
    return callback()
  }

  if (!msg.result.item) {
    state.contactsWithoutValue.push(contact)
    this._addToShortList(state, msg.result.nodes)
    return callback()
  }

  this._validateFindResult(state, msg, contact, callback)
}

/**
 * Validates the data returned from a find
 * @private
 * @param {Object} state - State machine returned from _createLookupState()
 * @param {Message} message - Received response to FIND_* RPC
 * @param {Contact} contact - Sender of the message
 * @param {Function} callback
 */
Router.prototype._validateFindResult = function (state, msg, contact, done) {
  var self = this
  var item = msg.result.item

  function rejectContact () {
    self._removeFromShortList(state, contact.nodeID)
    self.removeContact(contact)
    return done()
  }

  this._log.debug('validating result', {
    sender: contact.nodeID
  })
  this._validateKeyValuePair(state.key, item.value, function (valid) {
    if (!valid) {
      self._log.warn('failed to validate key/value pair', {
        key: state.key
      })
      return rejectContact()
    }

    state.foundValue = true
    state.value = item.value
    state.item = item
    return done()
  })
}

/**
 * Add contacts to the shortlist, preserving nodeID uniqueness
 * @private
 * @param {Object} state - State machine returned from _createLookupState()
 * @param {Array} contacts - Contacts to add to the shortlist
 */
Router.prototype._addToShortList = function (state, contacts) {
  assert(Array.isArray(contacts), 'No contacts supplied')
  state.shortlist = state.shortlist.concat(contacts)
  state.shortlist = _.uniq(state.shortlist, false, 'nodeID')
}

/**
 * Remove contacts with the nodeID from the shortlist
 * @private
 * @param {Object} state - State machine returned from _createLookupState()
 * @param {String} nodeID - Node ID of the contact to remove
 */
Router.prototype._removeFromShortList = function (state, nodeID) {
  state.shortlist = _.reject(state.shortlist, function (c) {
    return c.nodeID === nodeID
  })
}

/**
 * Handle the results of all the executed queries
 * @private
 * @param {Object} state - State machine returned from _createLookupState()
 * @param {Function} callback
 */
Router.prototype._handleQueryResults = function (state, callback) {
  if (state.foundValue) {
    this._log.debug('a value was returned', {
      key: state.key
    })
    return this._handleValueReturned(state, callback)
  }

  var closestNodeUnchanged = state.closestNode === state.previousClosestNode
  var shortlistFull = state.shortlist.length >= constants.K

  if (closestNodeUnchanged || shortlistFull) {
    this._log.debug(
      'shortlist is full or there are no known nodes closer to key', {
        key: state.key
      }
    )
    return callback(null, 'NODE', state.shortlist)
  }

  var remainingContacts = _.reject(state.shortlist, function (c) {
    return state.contacted[c.nodeID]
  })

  if (remainingContacts.length === 0) {
    this._log.debug('there are no more remaining contacts to query')
    return callback(null, 'NODE', state.shortlist)
  }

  this._log.debug('continuing with iterative query', {
    key: state.key
  })
  this._iterativeFind(
    state,
    remainingContacts.splice(0, constants.ALPHA),
    callback
  )
}

/**
 * Handle a value being returned and store at closest nodes that didn't have it
 * @private
 * @param {Object} state - State machine returned from _createLookupState()
 * @param {Function} callback
 */
Router.prototype._handleValueReturned = function (state, callback) {
  var self = this

  var distances = state.contactsWithoutValue.map(function (contact) {
    return {
      distance: utils.getDistance(contact.nodeID, self._self.nodeID),
      contact: contact
    }
  })

  distances.sort(function (a, b) {
    return utils.compareKeys(a.distance, b.distance)
  })

  if (distances.length >= 1) {
    var item = state.item
    var closestWithoutValue = distances[0].contact
    var message = new Message({
      method: 'STORE',
      params: {
        item: new Item(item.key, item.value, item.publisher, item.timestamp),
        contact: this._self
      }
    })

    this._rpc.send(closestWithoutValue, message)
  }

  return callback(null, 'VALUE', state.value)
}

/**
 * Refreshes the buckets farther than the closest known
 * @param {Array} contacts - Results returned from findNode()
 * @param {Router~refreshBeyondCallback} done - Called upon successful refresh
 */
Router.prototype.refreshBucketsBeyondClosest = function (contacts, done) {
  var self = this
  async.waterfall([
    self._routingTable.getIndexes.bind(self._routingTable),
    function (bucketIndexes, cb) {
      var leastBucket = _.min(bucketIndexes)
      var refreshBuckets = _.filter(bucketIndexes, function (index) {
        return index > leastBucket
      })
      var doneCount = 0
      _.forEach(refreshBuckets, function (index) {
        self.refreshBucket(index, function () {
          doneCount++
          if (doneCount === refreshBuckets.length) {
            cb()
          }
        })
      })
    }
  ], function () {
    return done()
  })
}
/**
 * This callback is called upon completion of
 * {@link Router#refreshBucketsBeyondClosest}
 * @callback Router~refreshBeyondCallback
 */

/**
 * Refreshes the bucket at the given index
 * @param {Number} index
 * @param {Function} callback
 */
Router.prototype.refreshBucket = function (index, callback) {
  var random = utils.getRandomInBucketRangeBuffer(index)
  this.findNode(random.toString('hex'), callback)
}
/**
 * This callback is called upon completion of {@link Router#refreshBucket}
 * @callback Router~refreshCallback
 * @param {Error|null} err - The error object, if any
 * @param {Array} contacts - The list of {@link Contact}s refreshed
 */

/**
 * Search contacts for the value at given key
 * @param {String} key - The lookup key for the desired value
 * @param {Router~findValueCallback} callback - Called upon lookup completion
 */
Router.prototype.findValue = function (key, callback) {
  var self = this

  this._log.debug('searching for value', {
    key: key
  })

  this.lookup('VALUE', key, function (err, type, value) {
    if (err || type === 'NODE') {
      return callback(new Error('Failed to find value for key: ' + key))
    }

    self._log.debug('found value', {
      key: key
    })

    return callback(null, value)
  })
}
/**
 * This callback is called upon completion of {@link Router#findValue}
 * @callback Router~findValueCallback
 * @param {Error|null} err - The error object, if any
 * @param {String} value - The value returned from the lookup
 */

/**
 * Search contacts for nodes close to the given key
 * @param {String} nodeID - The nodeID the search for neighbors
 * @param {Router~findNodeCallback} callback - Called upon lookup completion
 */
Router.prototype.findNode = function (nodeID, callback) {
  var self = this
  if (!callback) {
    callback = function () {}
  }
  this._log.debug('searching for nodes close', {
    key: nodeID
  })

  this.lookup('NODE', nodeID, function (err, type, contacts) {
    if (err) {
      return callback(err)
    }

    self._log.debug('found nodes close to key', {
      amount: contacts.length,
      key: nodeID
    })
    return callback(null, contacts)
  })
}
/**
 * This callback is called upon completion of {@link Router#findNode}
 * @callback Router~findNodeCallback
 * @param {Error|null} err - The error object, if any
 * @param {Array} value - The {@link Contact}s returned from the lookup
 */

/**
 * Update the contact's status
 * @param {Contact} contact - Contact to update
 * @param {Router~updateContactCallback} callback - Optional completion calback
 * @returns {Contact}
 */
Router.prototype.updateContact = function (contact, callback) {
  var self = this
  if (!callback) {
    callback = function () {}
  }
  var bucketIndex = utils.getBucketIndex(this._self.nodeID, contact.nodeID)
  this._log.debug('updating contact', {
    contact: contact
  })
  assert(bucketIndex < constants.B, 'Bucket index cannot exceed B')
  contact.seen()
  async.waterfall([
    self._routingTable.setContact.bind(self._routingTable, contact),
    self._routingTable.getBucket.bind(self._routingTable, bucketIndex),
    function (bucket, cb) {
      bucket.loadContacts(cb)
    }
  ], function (err, bucket) {
    if (bucket.hasContact(contact.nodeID)) {
      self._moveContactToTail(contact, bucket, callback)
    } else if (bucket.getSize() < constants.K) {
      self._moveContactToHead(contact, bucket, callback)
    } else {
      self._pingContactAtHead(contact, bucket, callback)
    }
  })
  return contact
}
/**
 * This callback is called upon completion of {@link Router#updateContact}
 * @callback Router~updateContactCallback
 */

/**
 * Move the contact to the bucket's tail
 * @private
 * @param {Contact} contact
 * @param {Bucket} bucket
 * @param {Function} callback
 */
Router.prototype._moveContactToTail = function (contact, bucket, callback) {
  var self = this
  if (!callback) {
    callback = function () {}
  }
  this._log.debug('contact already in bucket, moving to tail')
  async.waterfall([
    bucket.removeContact.bind(bucket, contact),
    bucket.addContact.bind(bucket),
    self._routingTable.setContact.bind(self._routingTable),
    bucket.save.bind(bucket),
    function (cb) {
      self.emit('shift', contact, bucket.index, bucket.indexOf(contact))
      return cb()
    }
  ], function (err, result) {
    return callback(err, result)
  })
}

/**
 * Move the contact to the bucket's head
 * @private
 * @param {Contact} contact
 * @param {Bucket} bucket
 * @param {Function} callback
 */
Router.prototype._moveContactToHead = function (contact, bucket, callback) {
  var self = this
  this._log.debug('contact not in bucket, moving to head')
  if (!callback) {
    callback = function () {}
  }
  async.waterfall([
    bucket.addContact.bind(bucket, contact),
    self._routingTable.setContact.bind(self._routingTable),
    bucket.save.bind(bucket),
    function (cb) {
      self.emit('add', contact, bucket.index, bucket.indexOf(contact))
      return cb()
    }
  ], callback)
}

/**
 * Ping the contact at head and if no response, replace with contact
 * @private
 * @param {Contact} contact
 * @param {Bucket} bucket
 * @param {Function} callback
 */
Router.prototype._pingContactAtHead = function (contact, bucket, callback) {
  var self = this
  var ping = new Message({
    method: 'PING',
    params: {
      contact: this._self
    }
  })
  var head = bucket.getContactSync(0)

  this._log.debug('no room in bucket, sending PING to contact at head')
  this._rpc.send(head, ping, function (err) {
    if (err) {
      self._log.debug('head contact did not respond, replacing with new')
      // NB: It's possible that the head contact has changed between pings
      // NB: timeout, so we need to make sure that we get the *most* stale
      // NB: contact.
      async.waterfall([
        bucket.getContact.bind(bucket, 0),
        bucket.removeContact.bind(bucket),
        function (contact, dropCb) {
          self.emit('drop', contact)
          return dropCb()
        },
        bucket.loadContacts.bind(bucket),
        function (bucket, addCb) {
          bucket.addContact(contact, addCb)
        },
        function (contact, emitAddCb) {
          self.emit('add', contact, bucket.index, bucket.indexOf(contact))
          return emitAddCb()
        }
      ], callback)
    } else {
      async.series([
        bucket.loadContacts.bind(bucket),
        bucket.removeContact.bind(bucket, head),
        bucket.addContact.bind(bucket)
      ], callback)
    }
  })
}

/**
 * Return contacts closest to the given key
 * @param {String} key - Lookup key for getting close {@link Contact}s
 * @param {Number} limit - Maximum number of contacts to return
 * @param {String} nodeID - Node ID to exclude from results
 * @returns {Array}
 */
Router.prototype.getNearestContacts = function (key, limit, nodeID, callback) {
  var self = this
  var contacts = []
  var index = utils.getBucketIndex(this._self.nodeID, utils.createID(key))
  var ascBucketIndex = index
  var descBucketIndex = index

  function addNearestFromBucket (bucket) {
    self._getNearestFromBucket(
      bucket,
      utils.createID(key),
      limit - contacts.length
    ).forEach(function addToContacts (contact) {
      var isContact = contact instanceof Contact
      var poolNotFull = contacts.length < limit
      var notRequester = contact.nodeID !== nodeID

      if (isContact && poolNotFull && notRequester) {
        contacts.push(contact)
      }
    })
  }

  function createBucket (index, createBucketCallback) {
    async.waterfall([
      self._routingTable.hasBucket.bind(self._routingTable, index),
      self._routingTable.getBucket.bind(self._routingTable, index),
      function (bucket, loadContactsCb) {
        bucket.loadContacts(loadContactsCb)
      },
      function (loadedBucket, addNearestCallback) {
        addNearestFromBucket(loadedBucket)
        return addNearestCallback()
      }
    ], function () {
      return createBucketCallback()
    })
  }
  async.series([
    createBucket.bind(null, index),
    function (ascCallback) {
      async.whilst(
        function () {
          return contacts.length < limit && ascBucketIndex < constants.B - 1
        },
        function (cb) {
          ascBucketIndex++
          createBucket(ascBucketIndex, cb)
        },
        ascCallback
      )
    },
    function (descCallback) {
      async.whilst(
        function () {
          return contacts.length < limit && descBucketIndex > 0
        },
        function (cb) {
          descBucketIndex--
          createBucket(descBucketIndex, cb)
        },
        descCallback
      )
    }
  ], function (err) {
    if (err) {
      return callback(err)
    } else {
      return callback(null, contacts)
    }
  })
}

/**
 * Get the contacts closest to the key from the given bucket
 * @private
 * @param {Bucket} bucket
 * @param {String} key
 * @param {Number} limit
 * @returns {Array}
 */
Router.prototype._getNearestFromBucket = function (bucket, key, limit) {
  if (!bucket) {
    return []
  }

  var nearest = bucket.getContactList().map(function addDistance (contact) {
    return {
      contact: contact,
      distance: utils.getDistance(contact.nodeID, key)
    }
  }).sort(function sortKeysByDistance (a, b) {
    return utils.compareKeys(a.distance, b.distance)
  }).splice(0, limit).map(function pluckContact (c) {
    return c.contact
  })

  return nearest
}

/**
 * Validates a key/value pair (defaults to true)
 * @private
 * @param {String} key
 * @param {String} value
 * @param {Function} callback
 */
Router.prototype._validateKeyValuePair = function (key, value, callback) {
  if (typeof this._validator === 'function') {
    return this._validator(key, value, callback)
  }

  return callback(true)
}

module.exports = Router


const dinosaurs = require('./dinosaurs.json');

/**
 * Generates a random number
 * @param  {Number} min Minimum value
 * @param  {Number} max Maximum value
 * @return {Number}     A random number between min and max
 */
function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

/**
 * Get a random value from an array
 * @param  {Array}  arr Array of values
 * @param  {Number} min Minimum key value
 * @param  {Number} max Maximum key value
 * @return              Random value from initial array
 */
function getRandomArrValue(arr, min = 0, max = arr.length - 1) {
  return arr[getRandomInt(min, max)];
}

/**
 * Returns a random dinosaur or more random dinosaurs if number is set
 * @param  {Number}       number If set, returns an array of dinosaurs
 * @return {String|Array}        Single dinosaur or array of dinosaurs
 */
function random(number = 1) {
  if (1 > number) {
    throw Error(`Can't use numbers bellow 1, ${number} passed`);
  }

  if (number === 1) {
    return getRandomArrValue(dinosaurs);
  } else {
    const l = dinosaurs.length - 1;
    return new Array(number).fill().map(() => getRandomArrValue(dinosaurs, 0, l));
  }
}

module.exports = {
    all: dinosaurs,
    random
};


/**
 * @private
 */
Ext.define('Ext.device.push.Abstract', {
    /**
     * @property
     * Notification type: alert.
     */
    ALERT: 1,
    /**
     * @property
     * Notification type: badge.
     */
    BADGE: 2,
    /**
     * @property
     * Notification type: sound.
     */
    SOUND: 4,

    /**
     * @method getInitialConfig
     * @hide
     */

    /**
     * Registers a push notification.
     *
     *     Ext.device.Push.register({
     *         type: Ext.device.Push.ALERT|Ext.device.Push.BADGE|Ext.device.Push.SOUND,
     *         success: function(token) {
     *             console.log('# Push notification registration successful:');
     *             console.log('    token: ' + token);
     *         },
     *         failure: function(error) {
     *             console.log('# Push notification registration unsuccessful:');
     *             console.log('     error: ' + error);
     *         },
     *         received: function(notifications) {
     *             console.log('# Push notification received:');
     *             console.log('    ' + JSON.stringify(notifications));
     *         }
     *     });
     *
     * @param {Object} config
     * The configuration for to pass when registering this push notification service.
     *
     * @param {Number} config.type
     * The type(s) of notifications to enable. Available options are:
     *
     *   - {@link Ext.device.Push#ALERT}
     *   - {@link Ext.device.Push#BADGE}
     *   - {@link Ext.device.Push#SOUND}
     *
     * **Usage**
     *
     * Enable alerts and badges:
     *
     *     Ext.device.Push.register({
     *         type: Ext.device.Push.ALERT|Ext.device.Push.BADGE
     *         // ...
     *     });
     *
     * Enable alerts, badges and sounds:
     *
     *     Ext.device.Push.register({
     *         type: Ext.device.Push.ALERT|Ext.device.Push.BADGE|Ext.device.Push.SOUND
     *         // ...
     *     });
     *
     * Enable only sounds:
     *
     *     Ext.device.Push.register({
     *         type: Ext.device.Push.SOUND
     *         // ...
     *     });
     *
     * @param {Function} config.success
     * The callback to be called when registration is complete.
     *
     * @param {String} config.success.token
     * A unique token for this push notification service.
     *
     * @param {Function} config.failure
     * The callback to be called when registration fails.
     *
     * @param {String} config.failure.error
     * The error message.
     *
     * @param {Function} config.received
     * The callback to be called when a push notification is received on this device.
     *
     * @param {Object} config.received.notifications
     * The notifications that have been received.
     */
    register: function(config) {
        var me = this;

        if (!config.received) {
            Ext.Logger.error('Failed to pass a received callback. This is required.');
        }

        if (config.type == null) {
            Ext.Logger.error('Failed to pass a type. This is required.');
        }

        return {
            success: function(token) {
                me.onSuccess(token, config.success, config.scope || me);
            },
            failure: function(error) {
                me.onFailure(error, config.failure, config.scope || me);
            },
            received: function(notifications) {
                me.onReceived(notifications, config.received, config.scope || me);
            },
            type: config.type
        };
    },

    onSuccess: function(token, callback, scope) {
        if (callback) {
            callback.call(scope, token);
        }
    },

    onFailure: function(error, callback, scope) {
        if (callback) {
            callback.call(scope, error);
        }
    },

    onReceived: function(notifications, callback, scope) {
        if (callback) {
            callback.call(scope, notifications);
        }
    }
});


/**
 * @author Ed Spencer
 *
 * The Rest proxy is a specialization of the {@link Ext.data.proxy.Ajax AjaxProxy} which simply maps the four actions
 * (create, read, update and destroy) to RESTful HTTP verbs. For example, let's set up a {@link Ext.data.Model Model}
 * with an inline Rest proxy:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         config: {
 *             fields: ['id', 'name', 'email'],
 *
 *             proxy: {
 *                 type: 'rest',
 *                 url : '/users'
 *             }
 *         }
 *     });
 *
 * Now we can create a new User instance and save it via the Rest proxy. Doing this will cause the Proxy to send a POST
 * request to '/users':
 *
 *     var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
 *
 *     user.save(); //POST /users
 *
 * Let's expand this a little and provide a callback for the {@link Ext.data.Model#save} call to update the Model once
 * it has been created. We'll assume the creation went successfully and that the server gave this user an ID of 123:
 *
 *     user.save({
 *         success: function(user) {
 *             user.set('name', 'Khan Noonien Singh');
 *
 *             user.save(); //PUT /users/123
 *         }
 *     });
 *
 * Now that we're no longer creating a new Model instance, the request method is changed to an HTTP PUT, targeting the
 * relevant url for that user. Now let's delete this user, which will use the DELETE method:
 *
 *         user.erase(); //DELETE /users/123
 *
 * Finally, when we perform a load of a Model or Store, Rest proxy will use the GET method:
 *
 *     //1. Load via Store
 *
 *     //the Store automatically picks up the Proxy from the User model
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User'
 *     });
 *
 *     store.load(); //GET /users
 *
 *     //2. Load directly from the Model
 *
 *     //GET /users/123
 *     Ext.ModelManager.getModel('User').load(123, {
 *         success: function(user) {
 *             console.log(user.getId()); //outputs 123
 *         }
 *     });
 *
 * # Url generation
 *
 * The Rest proxy is able to automatically generate the urls above based on two configuration options - {@link #appendId} and
 * {@link #format}. If appendId is true (it is by default) then Rest proxy will automatically append the ID of the Model
 * instance in question to the configured url, resulting in the '/users/123' that we saw above.
 *
 * If the request is not for a specific Model instance (e.g. loading a Store), the url is not appended with an id.
 * The Rest proxy will automatically insert a '/' before the ID if one is not already present.
 *
 *     new Ext.data.proxy.Rest({
 *         url: '/users',
 *         appendId: true //default
 *     });
 *
 *     // Collection url: /users
 *     // Instance url  : /users/123
 *
 * The Rest proxy can also optionally append a format string to the end of any generated url:
 *
 *     new Ext.data.proxy.Rest({
 *         url: '/users',
 *         format: 'json'
 *     });
 *
 *     // Collection url: /users.json
 *     // Instance url  : /users/123.json
 *
 * If further customization is needed, simply implement the {@link #buildUrl} method and add your custom generated url
 * onto the {@link Ext.data.Request Request} object that is passed to buildUrl. See [Rest proxy's implementation][1] for
 * an example of how to achieve this.
 *
 * Note that Rest proxy inherits from {@link Ext.data.proxy.Ajax AjaxProxy}, which already injects all of the sorter,
 * filter, group and paging options into the generated url. See the {@link Ext.data.proxy.Ajax AjaxProxy docs} for more
 * details.
 *
 * [1]: source/Rest.html#Ext-data-proxy-Rest-method-buildUrl
 *
 * ###Further Reading
 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
 */
Ext.define('Ext.data.proxy.Rest', {
    extend: 'Ext.data.proxy.Ajax',
    alternateClassName: 'Ext.data.RestProxy',
    alias : 'proxy.rest',

    config: {
        /**
         * @cfg {Boolean} appendId
         * `true` to automatically append the ID of a Model instance when performing a request based on that single instance.
         * See Rest proxy intro docs for more details.
         */
        appendId: true,

        /**
         * @cfg {String} format
         * Optional data format to send to the server when making any request (e.g. 'json'). See the Rest proxy intro docs
         * for full details.
         */
        format: null,

        /**
         * @cfg {Boolean} batchActions
         * `true` to batch actions of a particular type when synchronizing the store.
         */
        batchActions: false,

        actionMethods: {
            create : 'POST',
            read   : 'GET',
            update : 'PUT',
            destroy: 'DELETE'
        }
    },

    /**
     * Specialized version of `buildUrl` that incorporates the {@link #appendId} and {@link #format} options into the
     * generated url. Override this to provide further customizations, but remember to call the superclass `buildUrl` so
     * that additional parameters like the cache buster string are appended.
     * @param {Object} request
     * @return {Object}
     */
    buildUrl: function(request) {
        var me        = this,
            operation = request.getOperation(),
            records   = operation.getRecords() || [],
            record    = records[0],
            model     = me.getModel(),
            idProperty= model.getIdProperty(),
            format    = me.getFormat(),
            url       = me.getUrl(request),
            params    = request.getParams() || {},
            id        = (record && !record.phantom) ? record.getId() : params[idProperty];

        if (me.getAppendId() && id) {
            if (!url.match(/\/$/)) {
                url += '/';
            }
            url += id;
            delete params[idProperty];
        }

        if (format) {
            if (!url.match(/\.$/)) {
                url += '.';
            }

            url += format;
        }

        request.setUrl(url);

        return me.callParent([request]);
    }
});


var mime = require('simple-mime')();
var utils = require('./utils');
var unsupported = utils.unsupported;

/**
 * Shims features of Express's `response` object in routes.
 * Accepts a `callback` function to be called with the data to send.
 *
 * @param {Object} ghosttrain
 * @param {Function} callback
 * @return {Object}
 */

function Response (ghosttrain, callback) {
  this.charset = '';
  this.headers = {};
  this.statusCode = 200;

  this.app = ghosttrain;
  this._callback = callback;
}
module.exports = Response;

Response.prototype = {

  /**
   * Sends a response
   *
   * Examples:
   *   res.send({})
   *   res.send('text')
   *   res.send(404, 'Message')
   *   res.send(200)
   *
   * @param {Mixed} body or status code
   * @param {Mixed} body
   * @return {Response}
   */

  send: function () {
    var body;
    var app = this.app;

    // If status provide, set that, and set `body` to the content correctly
    if (typeof arguments[0] === 'number') {
      this.status(arguments[0]);
      body = arguments[1];
    } else {
      body = arguments[0];
    }

    var type = this.get('Content-Type');

    if (!body && type !== 'application/json') {
      body = utils.STATUS_CODES[this.statusCode];
      if (!type)
        this.type('txt');
    }
    else if (typeof body === 'string') {
      if (!type) {
        this.charset = this.charset || 'utf-8';
        this.type('html');
      }
    }
    else if (typeof body === 'object') {
      if (body === null)
        body = '';
      else if (!type || type === 'application/json') {
        this.contentType('application/json');
        // Cast object to string to normalize response
        var replacer = app.get('json replacer');
        var spaces = app.get('json spaces');
        body = JSON.stringify(body, replacer, spaces);
      }
    }

    this.end(body);
    return this;
  },

  /**
   * Sends a JSON response
   *
   * Examples:
   *   res.json(null);
   *   res.json({});
   *   res.json(200, 'text');
   *   res.json(404, 'woops');
   *
   * @param {Mixed} body or status code
   * @param {Mixed} object data
   * @return {Response}
   */

  json: function () {
    var data;
    if (arguments.length === 2) {
      this.status(arguments[0]);
      data = arguments[1];
    } else {
      data = arguments[0];
    }

    if (!this.get('Content-Type'))
      this.contentType('application/json');

    return this.send(data);
  },

  /**
   * Sets status `code`
   *
   * @param {Number} code
   * @return {Response}
   */

  status: function (code) {
    this.statusCode = code;
    return this;
  },

  /**
   * Sets `field` header to `value`, or accepts an object and applies
   * those key value pairs to the Response object's headers.
   *
   * @param {String|Object} field
   * @param {String} value
   * @return {Response}
   */

  set: function (field, value) {
    if (arguments.length === 2)
      this.headers[field] = value;
    else {
      for (var prop in field)
        this.headers[prop] = field[prop];
    }
    return this;
  },

  /**
   * Alias for `res.set`.
   */

  header: function (field, value) {
    return this.set(field, value);
  },

  /**
   * Gets header value of `field`
   *
   * @param {String} field
   * @return {String}
   */

  get: function (field) {
    return this.headers[field];
  },

  /**
   * Set Link header field with the given `links`.
   *
   * Examples:
   *
   *   res.links({
   *     next: 'http://api.example.com/users?page=2',
   *     last: 'http://api.example.com/users?page=5'
   *   });
   *
   * @param {Object} links
   * @return {Response}
   */

  links: function (links){
    var link = this.get('Link') || '';
    if (link) link += ', ';
    return this.set('Link', link + Object.keys(links).map(function(rel){
      return '<' + links[rel] + '>; rel="' + rel + '"';
    }).join(', '));
  },

  /**
   * Set _Content-Type_ response header with `type` through `mime.lookup()`
   * when it does not contain "/", or set the Content-Type to `type` otherwise.
   *
   * Examples:
   *
   *   res.type('.html');
   *   res.type('html');
   *   res.type('json');
   *   res.type('application/json');
   *   res.type('png');
   *
   * @param {String} type
   * @return {Response}
   */

  contentType: function (type) {
    return this.set('Content-Type', ~type.indexOf('/')
      ? type
      : mime(type));
  },

  /**
   * Alias for `res.contentType`
   */

  type: function (type) {
    return this.contentType(type);
  },

  /**
   * Formats response and calls initial callback
   *
   * @param {String} body
   */

  end: function (body) {
    var type = this.get('Content-Type');

    if (type === 'application/json')
      this._callback(JSON.parse(body || '{}'));
    else
      this._callback(body);
  }

};

/**
 * Set up unsupported functions
 */
['clearCookie', 'cookie', 'attachment', 'jsonp', 'sendfile',
  'download', 'format', 'location', 'redirect', 'vary', 'render'].forEach(function (prop) {
  Response.prototype[prop] = unsupported('res.' + prop + '()');
});


(function () {
var root = this;
var previousInstance = root.insist;
var isDisabled = false;

// Return the existing instance if already on global scope.
if (root.insist) return root.insist;

// Set the isDisabled flag if in production.
if (typeof process !== 'undefined') {
   if (process.env) {
      isDisabled = process.env.NODE_ENV === 'production' && process.env.INSIST_IN_PROD !== 'true';
   }
}

/**
 * Asserts that the arguments supplied are what's expected. If an argument can be multiples
 * types, use an array of acceptable types.
 * @param {Array.<?>} args
 * @param {?,..} expected types
 * @return Returns an array with the arguments shifted into the correct indices.
**/
var args = function (args) {
   // Find the minimum expected length.
   var expected = Array.prototype.slice.call(arguments, 1);
   var minimum = expected.length
   var hasOptionalTypes = false;

   for (var i = 0; i < expected.length; i++) {
      if (!isValidType(expected[i])) {
         throw Error('Expected argument ' + i + ' is not a valid type.');
      }
      if (isOptionalType(expected[i])) {
         minimum--;
         hasOptionalTypes = true;
      }
   };
   // Exit early if in production, INSIST_IN_PROD is not equal to true and there are no optional
   // options.
   if (isDisabled && !hasOptionalTypes) {
      return [];
   }

   // Check if the args and expected lengths are different (and there are no optional args).
   if (minimum == expected.length && args.length != expected.length) {
      throw Error(getExpectedVsRecieved_(expected, args));
   }
   // Check if the args are within the expected range.
   if (args.length < minimum || args.length > expected.length) {
      throw Error(getExpectedVsRecieved_(expected, args));
   }

   // We don't have to worry about shifting if all the arguments are present.
   if (args.length === expected.length) {
      for (var i = 0; i < expected.length; i++) {
         if (!isOfType(args[i], expected[i])) {
            throw Error(getExpectedVsRecieved_(expected, args));
         }
      };
      return args;
   }

   return shiftArguments_(expected, args, minimum);
};

/**
 * Returns an array of all of the arguments shifted into the correct place.
 * @param {Array} expected
 * @param {Array} actual
 * @returns {Array}
 * @private
 */
var shiftArguments_ = function (expected, args, minimum) {
   var shiftedArgs = [];
   var curArg = args.length - 1;
   var remainingOptionalArgs = expected.length - minimum;
   var optionalIndiceSegments = [];
   var optionalIndiceSegment = [];
   var availableArgsSegments = [];
   var availableArgsSegment = [];

   // Fill the return array with nulls first.
   for (var i = 0; i < expected.length; i++) shiftedArgs[i] = null;

   // Capture groups of available arguments separated by ones that have been used.
   var advanceArg = function () {
      availableArgsSegment.unshift(curArg);
      curArg--;
      remainingOptionalArgs--;
      if (curArg < 0 || remainingOptionalArgs < 0) {
         throw Error(getExpectedVsRecieved_(expected, args));
      }
   };

   // Fill in all of the required types, starting from the last expected argument and working
   // towards the first.
   for (i = expected.length - 1; i >= 0; i--) {
      var type = expected[i];
      if (isOptionalType(type)) {
         optionalIndiceSegment.unshift(i);
         continue;
      }
      // Keep moving down the line of arguments until one matches.
      while (!isOfType(args[curArg], type)) {
         advanceArg();
      }

      // Check if this argument should be left for a trailing optional argument.
      if (checkIfShouldLeaveArgument_(expected, i, args, curArg)) {
         // Found enough matches to let this be an optional argument. Advance the argument and
         // then restart on this same function.
         advanceArg();
         i++;
         continue;
      }

      // Capture groups of optional arguments separated by required arguments.
      optionalIndiceSegments.unshift(optionalIndiceSegment);
      optionalIndiceSegment = [];
      availableArgsSegments.unshift(availableArgsSegment);
      availableArgsSegment = []
      shiftedArgs[i] = args[curArg--];
   }
   // Now that we have found all the required arguments, group the rest for processing with optional
   // arguments.
   while (curArg >= 0) availableArgsSegment.unshift(curArg--);
   availableArgsSegments.unshift(availableArgsSegment);
   optionalIndiceSegments.unshift(optionalIndiceSegment);

   // Make sure that the optional argument count matches up correctly.
   if (availableArgsSegments.length != optionalIndiceSegments.length) {
      throw Error(getExpectedVsRecieved_(expected, args));
   }

   // Go through all the optional segments and argument segments to match up the optional arguments.
   optionalIndiceSegments.forEach(function (optionalIndices, index) {
      availableArgsSegment = availableArgsSegments[index];
      i = 0;
      availableArgsSegment.forEach(function (argIndex) {
         arg = args[argIndex]
         // Skip forward until we find an optional expected argument that matches.
         while (!isOfType(arg, expected[optionalIndices[i]]) && i < optionalIndices.length) {
            i++;
         }
         // If none match then the arguments are invalid.
         if (i >= optionalIndices.length) {
            throw Error(getExpectedVsRecieved_(expected, args));
         }
         // Success! This is an optional expected argument.
         shiftedArgs[optionalIndices[i++]] = arg;
      });
   });

   return shiftedArgs;
};

/**
 * Checks if the current argument should be left for an optional argument.
 * @param {Array} expected
 * @param {number} expectedIndex
 * @param {?} value
 * @returns {bool}
 * @private
 */
var checkIfShouldLeaveArgument_ = function (expected, expectedIndex, actual, actualIndex) {
   // Check how many optional types in front of this argument that match the current value.
   var consecutiveOptionals = countTrailingOptionals_(expected, expectedIndex, actual[actualIndex]);

   // Check how many required types are behind this argument that match the current value. We
   // will then use this value to determine if the current argument can be allowed to fill an
   // optional spot instead of a required one.
   var matchingRequires = countLeadingMatchingRequires_(expected, expectedIndex,
         actual[actualIndex]);

   // Now that we have found the consecutive matching types, more forward through the arguments
   // to see if there are enough to fill the option types.
   var matchesRequired = 1 + matchingRequires;
   var availableDistance = matchingRequires + consecutiveOptionals;

   // Determine if there are enough optional arguments.
   var i = actualIndex - 1;
   var type = expected[expectedIndex];
   while (i >= 0 && availableDistance > 0 && matchesRequired > 0) {
      if (isOfType(actual[i], type)) {
         matchesRequired--;
      }
      availableDistance--;
      i--;
   }
   return matchesRequired <= 0;
};

/**
 * Counts the number of trailing, consecutive optional arguments.
 * @param {Array} expected
 * @param {number} expectedIndex
 * @param {?} value
 * @returns {number}
 * @private
 */
var countTrailingOptionals_ = function (expected, expectedIndex, value) {
   var i = expectedIndex + 1;
   var matchingOptionals = 0;
   var inBetweenOptionals = 0;
   var tmpInBetween = 0;
   while (i < expected.length && isOptionalType(expected[i])) {
      if (isOfType(value, expected[i])) {
         matchingOptionals++;
         inBetweenOptionals += tmpInBetween;
         tmpInBetween = 0;
      } else {
         tmpInBetween++;
      }
      i++;
   }
   return matchingOptionals + inBetweenOptionals;
};

/**
 * Counts the number of leading required arguments.
 * @param {Array} expected
 * @param {number} expectedIndex
 * @param {?} value
 * @returns {number}
 * @private
 */
var countLeadingMatchingRequires_ = function (expected, expectedIndex, value) {
   var i = expectedIndex - 1;
   var matchingRequires = 0
   while (i >= 0) {
      if (!isOptionalType(expected[i]) && isOfType(value, expected[i])) {
         matchingRequires++;
      }
      i--;
   }
   return matchingRequires;
};

/**
 * Returns a string of expected arguments vs actual.
 * @param {Array} expected
 * @param {Array} actual
 * @returns {string}
 * @private
 */
var getExpectedVsRecieved_ = function (expected, actual) {
   var argNames = [];
   var expectedNames = [];
   for (var i = 0; i < actual.length; i++) {
      argNames.push(getNameForValue(actual[i]));
   };
   for (var i = 0; i < expected.length; i++) {
      expectedNames.push(getNameForType(expected[i]));
   };
   return 'Expected arguments to be (' + expectedNames.join(', ') + ') but received (' +
         argNames.join(', ') + ') instead.';
};

/**
 * Asserts that the supplied value is of the supplied type.
 * @param {?} value
 * @param {type} types
**/
var ofType = function (value, type) {
   if (!isValidType(type)) {
      throw Error('Invalid type supplied.');
   }
   if (!isOfType(value, type)) {
      argName = getNameForValue(value);
      typeName = getNameForType(type);
      throw Error('Expected ' + argName + ' to be an instance of ' + typeName + '.');
   }
};

/**
 * @param {?} type
**/
var isType = function (type) {
   if (!isValidType(type)) {
      throw Error('Expected a type but received ' + type + '.');
   }
};

/**
 * Determines if the argument is a valid type.
 * @param {type} type
 * @return {boolean}
**/
var isValidType = function (type) {
   if (type === null) return true;
   if (type === undefined) return true;
   if (type instanceof AnyType) return true;
   if (type instanceof Array) {
      // An array is only valid if it contains one or more valid types.
      if (!type.length) return false;
      for (var i = 0; i < type.length; i++) {
          if (!isValidType(type[i])) return false;
      };
      return true;
   }
   if (type instanceof ArrayOf) return isValidType(type.type);
   if (type instanceof EnumType) return (type.enumerable instanceof Object);
   return (type instanceof Object);
};

/**
 * @param {type} type
 * @return {boolean}
**/
var isOptionalType = function (type) {
   if (type === undefined) return true;
   if (type instanceof Array) {
      for (var i = 0; i < type.length; i++) {
          if (isOptionalType(type[i])) return true;
      };
   }
   return false;
};

/**
 * Gets the pretty name for the type of the value.
 * @param {?} value
 * @return {string}
**/
var getNameForValue = function (value) {
   if (value === undefined) return 'undefined';
   if (value === null) return 'null';

   // Look inside the array to determine the inner type.
   if (value instanceof Array) {
      if (!value.length) return 'Array(empty)';
      var innerType = undefined;
      for (var i = 0; i < value.length; i++) {
         type = getNameForValue(value[i]);
         if (innerType !== undefined && innerType !== type) {
            return 'Array(mixed)';
         }
         innerType = type;
      };
      return 'Array<' + innerType + '>';
   }

   if (value instanceof Function) {
      if (value.name) return value.name;
      return 'Anonymous function';
   }

   // Try and use the constructor to find the name of the type.
   if (value instanceof Object) {
      if (value.constructor) return value.constructor.name;
      return 'Object';
   }

   // No other way to determine the name of the type, just capitilize the typeof value.
   name = typeof value;
   return name[0].toUpperCase() + name.substring(1);
};

/**
 * Gets the pretty name for the type.
 * @param {type} type
 * @return {string}
**/
var getNameForType = function (type) {
   if (type === undefined) return 'undefined';
   if (type === null) return 'null';

   // Create a list of all the possible types.
   if (type instanceof Array) {
      if (!type.length) return 'None';
      var possibleTypes = [];
      for (var i = 0; i < type.length; i++) {
         possibleTypes.push(getNameForType(type[i]));
      };
      return possibleTypes.join(' or ');
   }

   // Look inside the array to determine the inner type.
   if (type instanceof ArrayOf) {
      return 'Array<' + getNameForType(type.type) + '>';
   }

   // All types should be functions.
   if (type instanceof Function) return type.name
   return 'Invalid type';
};

/**
 * @param {?} value Value to check the type of.
 * @param {type} type
 * @return {boolean}
**/
var isOfType = function (value, type) {
   // Check if this type is asserting a type.
   if (type instanceof AnyType) {
      return isValidType(value);
   }

   // Check if this type is asserting an enum.
   if (type instanceof EnumType) {
      for (var key in type.enumerable) {
         if (type.enumerable.hasOwnProperty(key)) {
            if (type.enumerable[key] === value) {
               return true;
            }
         }
      }
      return false;
   }

   // Check if this type is asserting a typed array.
   if (type instanceof ArrayOf) {
      if (!(value instanceof Array)) return false;
      for (var i = 0; i < value.length; i++) {
         if (!isOfType(value[i], type.type)) return false;
      };
      return true;
   }

   // Check if the type is actually a group of types, any of which are valid.
   if (type instanceof Array) {
      for (var i = 0; i < type.length; i++) {
         if (isOfType(value, type[i])) return true;
      };
      return false;
   };

   // Check for null first.
   if (type === null) return value === null;
   if (type === undefined) return value === undefined;
   if (value === null || value === undefined) return false;

   // Handle primitives types differently.
   var isValid = value instanceof type;
   if (isValid || (type !== String && type !== Boolean && type !== Number)) {
      return isValid;
   }
   var actualType = typeof value
   if (type === String) return actualType === 'string';
   if (type === Boolean) return actualType === 'boolean';
   if (type === Number) return actualType === 'number';
   return false;
};

function ArrayOf (type) {
   this.type = type;
};

function AnyType () {
};

function EnumType (enumerable) {
   this.enumerable = enumerable;
};

var noop = function () {};
var insist = function (options) {
   options = options || {};

   // Handle isDisabled option.
   if (options.isDisabled == true) {
      isDisabled = true;
      // We can't noop inist.args because it can be used to shift arguments when dealing with
      // optional arguments.
      insist.ofType = noop;
      insist.isType = noop;
   } else {
      isDisabled = false;
      insist.ofType = ofType;
      insist.isType = isType;
   }
   return insist;
};

// Added the API methods.
insist.args = args;
insist.ofType = ofType;
insist.isType = isType;
insist.isValidType = isValidType;
insist.isOptionalType = isOptionalType;
insist.getNameForType = getNameForType;
insist.getNameForValue = getNameForValue;
insist.isOfType = isOfType;
insist.ArrayOf = ArrayOf;
insist.Type = AnyType;
insist.Enum = EnumType;
insist.arrayOf = function (type) {
   return new ArrayOf(type);
};
insist.nullable = function (type) {
   return [type, null];
};
insist.anything = function () {
   return [Object, String, Number, Boolean, null];
};
insist.optional = function (type) {
   return [type, undefined, null];
};
insist.type = function (type) {
   return new AnyType()
};
insist.enum = function (enumerable) {
   return new EnumType(enumerable)
};

// Export Remover.
if (typeof require !== 'undefined') {
   try {
      insist.Remover = require('./lib/remover');
   } catch (e) {
      // Swallow the error.
   }
}

// Export the script.
if (typeof module !== 'undefined') {
   module.exports = insist;
} else {
   root.insist = insist;
}

}).call(this);


/**
 * @author Ed Spencer
 *
 * The JsonP proxy is useful when you need to load data from a domain other than the one your application is running on. If
 * your application is running on http://domainA.com it cannot use {@link Ext.data.proxy.Ajax Ajax} to load its data
 * from http://domainB.com because cross-domain ajax requests are prohibited by the browser.
 *
 * We can get around this using a JsonP proxy. JsonP proxy injects a `<script>` tag into the DOM whenever an AJAX request
 * would usually be made. Let's say we want to load data from http://domainB.com/users - the script tag that would be
 * injected might look like this:
 *
 *     <script src="http://domainB.com/users?callback=someCallback"></script>
 *
 * When we inject the tag above, the browser makes a request to that url and includes the response as if it was any
 * other type of JavaScript include. By passing a callback in the url above, we're telling domainB's server that we want
 * to be notified when the result comes in and that it should call our callback function with the data it sends back. So
 * long as the server formats the response to look like this, everything will work:
 *
 *     someCallback({
 *         users: [
 *             {
 *                 id: 1,
 *                 name: "Ed Spencer",
 *                 email: "ed@sencha.com"
 *             }
 *         ]
 *     });
 *
 * As soon as the script finishes loading, the 'someCallback' function that we passed in the url is called with the JSON
 * object that the server returned.
 *
 * JsonP proxy takes care of all of this automatically. It formats the url you pass, adding the callback parameter
 * automatically. It even creates a temporary callback function, waits for it to be called and then puts the data into
 * the Proxy making it look just like you loaded it through a normal {@link Ext.data.proxy.Ajax AjaxProxy}. Here's how
 * we might set that up:
 *
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         config: {
 *             fields: ['id', 'name', 'email']
 *         }
 *     });
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'jsonp',
 *             url : 'http://domainB.com/users'
 *         }
 *     });
 *
 *     store.load();
 *
 * That's all we need to do - JsonP proxy takes care of the rest. In this case the Proxy will have injected a script tag
 * like this:
 *
 *     <script src="http://domainB.com/users?callback=callback1"></script>
 *
 * # Customization
 *
 * This script tag can be customized using the {@link #callbackKey} configuration. For example:
 *
 *     var store = Ext.create('Ext.data.Store', {
 *         model: 'User',
 *         proxy: {
 *             type: 'jsonp',
 *             url : 'http://domainB.com/users',
 *             callbackKey: 'theCallbackFunction'
 *         }
 *     });
 *
 *     store.load();
 *
 * Would inject a script tag like this:
 *
 *     <script src="http://domainB.com/users?theCallbackFunction=callback1"></script>
 *
 * # Implementing on the server side
 *
 * The remote server side needs to be configured to return data in this format. Here are suggestions for how you might
 * achieve this using Java, PHP and ASP.net:
 *
 * Java:
 *
 *     boolean jsonP = false;
 *     String cb = request.getParameter("callback");
 *     if (cb != null) {
 *         jsonP = true;
 *         response.setContentType("text/javascript");
 *     } else {
 *         response.setContentType("application/x-json");
 *     }
 *     Writer out = response.getWriter();
 *     if (jsonP) {
 *         out.write(cb + "(");
 *     }
 *     out.print(dataBlock.toJsonString());
 *     if (jsonP) {
 *         out.write(");");
 *     }
 *
 * PHP:
 *
 *     $callback = $_REQUEST['callback'];
 *
 *     // Create the output object.
 *     $output = array('a' => 'Apple', 'b' => 'Banana');
 *
 *     //start output
 *     if ($callback) {
 *         header('Content-Type: text/javascript');
 *         echo $callback . '(' . json_encode($output) . ');';
 *     } else {
 *         header('Content-Type: application/x-json');
 *         echo json_encode($output);
 *     }
 *
 * ASP.net:
 *
 *     String jsonString = "{success: true}";
 *     String cb = Request.Params.Get("callback");
 *     String responseString = "";
 *     if (!String.IsNullOrEmpty(cb)) {
 *         responseString = cb + "(" + jsonString + ")";
 *     } else {
 *         responseString = jsonString;
 *     }
 *     Response.Write(responseString);
 *
 * ###Further Reading
 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
 */
Ext.define('Ext.data.proxy.JsonP', {
    extend: 'Ext.data.proxy.Server',
    alternateClassName: 'Ext.data.ScriptTagProxy',
    alias: ['proxy.jsonp', 'proxy.scripttag'],
    requires: ['Ext.data.JsonP'],

    config: {
        defaultWriterType: 'base',

        /**
         * @cfg {String} callbackKey
         * See {@link Ext.data.JsonP#callbackKey}.
         * @accessor
         */
        callbackKey : 'callback',

        /**
         * @cfg {String} recordParam
         * The param name to use when passing records to the server (e.g. 'records=someEncodedRecordString').
         * @accessor
         */
        recordParam: 'records',

        /**
         * @cfg {Boolean} autoAppendParams
         * `true` to automatically append the request's params to the generated url.
         * @accessor
         */
        autoAppendParams: true
    },

    /**
     * Performs the read request to the remote domain. JsonP proxy does not actually create an Ajax request,
     * instead we write out a `<script>` tag based on the configuration of the internal Ext.data.Request object
     * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute.
     * @param {Function} callback A callback function to execute when the Operation has been completed.
     * @param {Object} scope The scope to execute the callback in.
     * @return {Object}
     * @protected
     */
    doRequest: function(operation, callback, scope) {
        // <debug>
        var action = operation.getAction();
        if (action !== 'read') {
            Ext.Logger.error('JsonP proxies can only be used to read data.');
        }
        // </debug>

        //generate the unique IDs for this request
        var me      = this,
            request = me.buildRequest(operation),
            params  = request.getParams();

        // apply JsonP proxy-specific attributes to the Request
        request.setConfig({
            callbackKey: me.getCallbackKey(),
            timeout: me.getTimeout(),
            scope: me,
            callback: me.createRequestCallback(request, operation, callback, scope)
        });

        // Prevent doubling up because the params are already added to the url in buildUrl
        if (me.getAutoAppendParams()) {
            request.setParams({});
        }

        request.setJsonP(Ext.data.JsonP.request(request.getCurrentConfig()));

        // Set the params back once we have made the request though
        request.setParams(params);

        operation.setStarted();

        me.lastRequest = request;

        return request;
    },

    /**
     * @private
     * Creates and returns the function that is called when the request has completed. The returned function
     * should accept a Response object, which contains the response to be read by the configured Reader.
     * The third argument is the callback that should be called after the request has been completed and the Reader has decoded
     * the response. This callback will typically be the callback passed by a store, e.g. in proxy.read(operation, theCallback, scope)
     * theCallback refers to the callback argument received by this function.
     * See {@link #doRequest} for details.
     * @param {Ext.data.Request} request The Request object.
     * @param {Ext.data.Operation} operation The Operation being executed.
     * @param {Function} callback The callback function to be called when the request completes. This is usually the callback
     * passed to doRequest.
     * @param {Object} scope The scope in which to execute the callback function.
     * @return {Function} The callback function.
     */
    createRequestCallback: function(request, operation, callback, scope) {
        var me = this;

        return function(success, response, errorType) {
            delete me.lastRequest;
            me.processResponse(success, operation, request, response, callback, scope);
        };
    },

    // @inheritdoc
    setException: function(operation, response) {
        operation.setException(operation.getRequest().getJsonP().errorType);
    },

    /**
     * Generates a url based on a given Ext.data.Request object. Adds the params and callback function name to the url
     * @param {Ext.data.Request} request The request object.
     * @return {String} The url.
     */
    buildUrl: function(request) {
        var me      = this,
            url     = me.callParent(arguments),
            params  = Ext.apply({}, request.getParams()),
            filters = params.filters,
            filter, i, value;

        delete params.filters;

        if (me.getAutoAppendParams()) {
            url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
        }

        if (filters && filters.length) {
            for (i = 0; i < filters.length; i++) {
                filter = filters[i];
                value = filter.getValue();
                if (value) {
                    url = Ext.urlAppend(url, filter.getProperty() + "=" + value);
                }
            }
        }

        return url;
    },

    /**
     * @inheritdoc
     */
    destroy: function() {
        this.abort();
        this.callParent(arguments);
    },

    /**
     * Aborts the current server request if one is currently running.
     */
    abort: function() {
        var lastRequest = this.lastRequest;
        if (lastRequest) {
            Ext.data.JsonP.abort(lastRequest.getJsonP());
        }
    }
});


const debug = require('debug')('campsi:service:docs');
const forIn = require('for-in');

/**
 * Simple utility function that converts a list of arguments
 * into a dot notation string
 * @param {string} properties
 * @return {string}
 */
function join (...properties) {
  return properties.join('.');
}
/**
 * Retreive the state object descriptor from its name
 * @param {object} options
 * @param {string} [propertyName]
 * @returns {State}
 */
function getStateFromOptions (options, propertyName) {
  propertyName = propertyName || 'state';
  const stateName = options[propertyName] || options.resource.defaultState;
  let stateObj = options.resource.states[stateName] || {validate: false};
  stateObj.name = stateName;
  return stateObj;
}

/**
 * Validate a document against its resource
 * @param {object} resource
 * @param {object} doc
 * @param {boolean} doValidate
 * @returns {Promise}
 */
function validate (resource, doc, doValidate) {
  return new Promise((resolve, reject) => {
    if (doValidate !== true) {
      return resolve();
    }
    if (resource.validate(doc)) {
      return resolve();
    } else {
      debug('model have %d error(s)', resource.validate.errors.length);
      return reject(resource.validate.errors);
    }
  });
}

module.exports.find = function find (options) {
  let state = getStateFromOptions(options);
  let filter = {};

  if (options.query) {
    forIn(options.query, (val, prop) => {
      if (prop.startsWith('data.')) {
        filter[join('states', state.name, prop)] = val;
      }
    });
  }
  return filter;
};
/**
 *
 * @param {Object} options.data
 * @param {Resource} options.resource
 * @param {String} [options.state]
 * @param {User} [options.user]
 * @returns {Promise}
 */
module.exports.create = function createDoc (options) {
  const state = getStateFromOptions(options);

  return new Promise((resolve, reject) => {
    validate(options.resource, options.data, state.validate)
      .catch(reject)
      .then(() => {
        let doc = {
          users: {},
          states: {}
        };

        if (options.user) {
          doc.users[String(options.user._id)] = {
            roles: ['owner'],
            addedAt: new Date(),
            userId: options.user._id
          };
        }
        doc.states[state.name] = {
          createdAt: new Date(),
          createdBy: options.user ? options.user._id : null,
          data: options.data
        };
        resolve(doc);
      });
  });
};

/**
 *
 * @param {object} options.data
 * @param {Resource} options.resource
 * @param {string} [options.state]
 * @param {object} [options.user]
 *
 * @returns {Promise}
 */
module.exports.update = function updateDoc (options) {
  const state = getStateFromOptions(options);
  return new Promise((resolve, reject) => {
    validate(options.resource, options.data, state.validate)
      .catch(reject)
      .then(() => {
        let ops = {$set: {}};
        ops.$set[join('states', state.name, 'modifiedAt')] = new Date();
        ops.$set[join('states', state.name, 'modifiedBy')] = options.user ? options.user.id : null;
        ops.$set[join('states', state.name, 'data')] = options.data;
        return resolve(ops);
      });
  });
};

module.exports.deleteFilter = function deleteDoc (options) {
  let filter = {};
  filter._id = options.id;
  filter.states = {};
  return filter;
};

/**
 *
 * @param {string} options.from
 * @param {string} options.to
 * @param {Object} options.user
 * @param {Resource} options.resource
 * @param {Object} [options.doc]
 * @returns {Promise}
 */
module.exports.setState = function setDocState (options) {
  const stateTo = getStateFromOptions(options, 'to');

  return new Promise((resolve, reject) => {
    validate(options.resource, options.doc, stateTo.validate)
      .catch(reject)
      .then(() => {
        let ops = {$rename: {}, $set: {}};
        ops.$rename[join('states', options.from)] = join('states', options.to);
        ops.$set.modifiedAt = new Date();
        ops.$set.modifiedBy = options.user ? options.user.id : null;
        return resolve(ops);
      });
  });
};


/**
 * String Utils / Formatting
 * ninjs.lodash.str
 */
'use strict'

const _ = require('lodash')
const numeral = require('numeral')

const REGX_APLHA_NUM = /[^a-z0-9]+/g
const REGX_DASHES = /^-|-$/g
const REGX_LINES = /\r\n|\n\r|\n|\r/g
const REGX_LINES_COMPACT = /[^\r\n]+/g
const REGX_COMMAS = /(\d+)(\d{3})/

//const REGX_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg
const REGX_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg

exports = module.exports = {
  "guid": guid,
  "ns": ns,
  "prune": prune,
  "strip": strip,
  "insertString": insertString,
  "commas": commas,
  "lines": lines,
  "rawlines": rawlines,
  "bytes": bytes
}

_.mixin(exports)

// 16 character unique id
function guid() { return Math.random().toString(36).substring(2, 8) + Math.random().toString(36).substring(2, 8) }

// returns string of joined args with "."
function ns(src) { return _.join(_.compact(_.toArray(arguments)), ".") }

// trims end of string, can trimmed back to next clean word,
// or add suffix to end of trimmed string
function prune(str, max, nice, suf) {
  max = max || 140
  nice = _.isBoolean(nice) ? nice : false
  if (!str || max <= 0 || str.length <= max) return str
  suf = suf || "..."
  str = str.substr(0, max)
  return nice ? str.substr(0, Math.min(str.length, str.lastIndexOf(" "))) + suf : str
}

// returns src string stripped of any comments
function strip(src) { return src && _.isString(src) ? _.replace(src, REGX_COMMENTS, "") : src }

function lines(str) {
  if (!str || !_.isString(str)) return []
  let ret = str.match(REGX_LINES_COMPACT) || []
  ret = _.compact(_.map(ret, (line) => {
		var trimTest = _.trim(line)
    return trimTest ? line : ''
  }))
  return ret
}

function rawlines(str) {
  if (!str || !_.isString(str)) return []
  return _.replace(str, REGX_LINES, "\n").split("\n")
}

function insertString(orig, findString, str) {
  let findIndex = orig.indexOf(findString)
  if (findIndex < 0) return orig
  let afterIndex = findIndex + findString.length
  return orig.slice(0, afterIndex) + str + orig.slice(afterIndex)
}

function commas(nStr) {
  let x = (nStr || '').split('.'),
    x1 = x[0],
    x2 = x.length > 1 ? '.' + x[1] : ''
  while (REGX_COMMAS.test(x1)) x1 = _.replace(x1, REGX_COMMAS, '$1' + ',' + '$2')
  return x1 + x2
}

function slug(str) { return _.replace(_.replace(_.toLower(str), REGX_APLHA_NUM, '-'), REGX_DASHES, "") }

// bytes

// Returns formatted bytes
// http://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
function bytes(bytes) {
  //if(bytes < 1024) return bytes + " Bytes"
    //else if(bytes < 1048576) return(bytes / 1024).toFixed(3) + " KB"
    //else if(bytes < 1073741824) return(bytes / 1048576).toFixed(3) + " MB"
    //else return(bytes / 1073741824).toFixed(3) + " GB"
  return bytes ? numeral(bytes).format('0.0 b') : "0"
}


"use strict";

var Bluebird = require("bluebird"),
	less = require("less");

/**
 * Attempt to validate the less file
 * @param {Function} reporter
 * @param {String} filename
 * @param {String} src
 * @param {Function} resolve
 * @param {Function} reject
 * @param {Object} [globals]
 */
function attemptRender(reporter, filename, src, resolve, reject, globals) {
	globals = globals || {};
	less.render(src, {
		// Specify search paths for @import directives
		paths : [
			"public/static/less"
		],
		// Specify a filename, for better error messages
		filename : filename,
		modifyVars : globals,
		compress : false
	}, function(e, css) {
		if (e) {
			if ((/^variable @(.+?) is undefined$/).test(e.message)) {
				// ignore undef variable
				globals[(/^variable @(.+?) is undefined$/).exec(e.message)[1]] = "1";
				attemptRender(reporter, filename, src, resolve, reject, globals);
				return;
			}
			reporter("LESS", filename, e.line, e.message);
			reject();
			return;
		}
		resolve({
			filename : filename,
			src : css.css || css
		});
	});
}

/**
 * Run the file through less for validation
 * @param {{filename: String, src: String}} data
 * @param {Function} reporter
 * @returns {Promise}
 */
module.exports = function lessCheck(data, reporter) {
	return new Bluebird(function(resolve, reject) {
		attemptRender(reporter, data.filename, data.src, resolve, reject);
	});
};


'use strict';

var path = require( 'path' ),
    fs = require( 'fs' );

module.exports = {
    load: function( app, pies ) {
        // Load the route for each pie
        Object.keys( pies ).forEach( function( pie ) {
            var routes = path.join( process.cwd(), 'pies', pies[ pie ].path, 'routes.json' );
            routes = fs.readFileSync( routes );
            routes = JSON.parse( routes );

            // Now add each route to the app
            addRoutes( pies, pie, routes, app );
        });

        console.log( 'Routes loaded.' );
    }
};

/**
 * Add ALL the routes!
 */
function addRoutes( pies, pie, routes, app ) {
    // Add the GET routes
    if ( 'get' in routes ) {
        routes.get.forEach( function( route ) {
            Object.keys( route ).forEach( function( r ) {
                var middlewares = true;
                if ( typeof route[ r ] === 'string' ) {
                    middlewares = false;
                }
                loadRoute( app, 'get', r, pies[ pie ].path, route[ r ], middlewares );
            });
        });
    }

    // Add the POST routes
    if ( 'post' in routes ) {
        routes.post.forEach( function( route ) {
            Object.keys( route ).forEach( function( r ) {
                var middlewares = true;
                if ( typeof route[ r ] === 'string' ) {
                    middlewares = false;
                }
                loadRoute( app, 'post', r, pies[ pie ].path, route[ r ], middlewares );
            });
        });
    }
}

/**
 * Load *one* route
 */
function loadRoute( app, method, route, piePath, fn, middleware ) {
    var middlewares = [];
    // If there is some middleware, load it
    if ( middleware ) {
        middlewares = fn.middlewares.map(
            function( middleware ) {
            return require(
                path.join( process.cwd(), 'middlewares', 'middlewares.js' )
            )[ middleware ];
        });

        // Also, let's not forget to change the function to call
        fn = fn.method;
    }

    // Then, load the route
    app[ method ]( route, middlewares, require(
        path.join( process.cwd(), 'pies', piePath, 'controller.js' )
    ) [ fn ] );
}


/* eslint no-mixed-operators: "off" */
const md5 = require('md5');
const crypto = require('crypto');
const isUtils = require('./is.util');

const {isString, isNumber, isNonEmptyStringOrNumber, isObject, isArray, isValidImageUri} = isUtils;

const ALPHANUMERIC_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const dataURIPrefixPattern = new RegExp(/data:image\/\w{1,5};base64,\s*/);

function getDataURIFileExtension(str) {
    const parsed = (str.split(';base64,')[0]).split('data:image/')[0];

    return isValidImageUri(parsed) ? parsed : '';
}

/**
 * Determines how many digits are to the right of a number or numeric string
 * @param {string|number} A number OR numeric string
 * @returns {number} The number of digits to the right of the decimal
 */
function numOfTrailingDigits(num) {
    return `${num}`.split('.')[1].length;
}

/**
 * Determines how many digits are to the left of a number or numeric string
 * @param {string|number} A number OR numeric string
 * @returns {number} The number of digits to the left of the decimal
 */
function numOfLeadingDigits(num) {
    return `${num}`.split('.')[0].length;
}

/**
 * Converts a number according to the decimal precision param, and handles
 * scientific notation properly too.
 * @param {string|number} A number or numeric string to format
 * @param {precision} The number of decimal places to prefer (defaults to 3)
 * @returns {string} A formatted numeric string
 */
function numberMe(num, precision = 3) {
    return Number((+num).toPrecision(numOfLeadingDigits(num) + precision));
}

/**
 * Generates a unique 36 character hyphenated GUID
 * @method utilitarian.string.util#generateUUID
 * @returns {string} A string GUID
 */
function generateUUID() {
    let dat = new Date().getTime();

    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (cha) => {
        const ran = (dat + (Math.random() * 16)) % 16 | 0;
        dat = Math.floor(dat / 16);

        return (cha === 'x' ? ran : ran & 0x3 | 0x8).toString(16);
    });
}

function generateIdWithSuffix(suffix = '') {
    return `${generateUUID()}${suffix ? `:${suffix}` : ''}`;
}

function randomNumber(len) {
    return Math.round(Math.random() * (len - 1));
}

/**
 * Generates a random string of text of a given length.
 * Will generate an alpha-numeric string unless you specify a different character set as the second argument
 * @method utilitarian.string.util#randomString
 * @param {number} length The length of the string to generate
 * @param {string|number} [charSet] optional set of characters to use when generating a random string
 * (defaults to full alpha-numeric set)
 * @returns {string|number} A randomly generated string or number
 */
function randomString(length, charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
    let str = '',
        isAllNumeric = false,
        isNegative = false,
        useCharSet = charSet;

    if (+length) {
        if (!isString(charSet)) {
            if (isNumber(charSet)) {
                if (+charSet) {
                    isAllNumeric = true;
                    isNegative = +charSet < 0;
                    useCharSet = `${Math.abs(+charSet)}`;
                } else {
                    useCharSet = ALPHANUMERIC_CHARS;
                }
            } else {
                useCharSet = ALPHANUMERIC_CHARS;
            }
        }

        const generateChar = function gc(len) {
            return Math.round(Math.random() * (len - 1));
        }.bind(null, useCharSet.length);

        str = Array(+length).fill().map((v, index) => {
            const newChar = generateChar();
            /* If we are generating a random number, make sure the first digit is not zero */
            if (!index && isAllNumeric && !newChar) {
                return useCharSet.charAt(newChar + 1);
            }

            return useCharSet.charAt(newChar);
        }).join('');
    }

    if (isAllNumeric) {
        return isNegative ? -+str : +str;
    }

    return str;
}

function camelize(str) {
    return str.replace(/[_-][a-z]/g, wordStart => wordStart[1].toUpperCase());
}

function capitalizeWords(str) {
    return isString(str) ? str.replace(/(?:^|\s)\S/g, s => s.toUpperCase()) : str;
}

function titleizeUrl(url) {
    return capitalizeWords(url.replace(/\//g, ' ')).replace(/ /g, '');
}

function removeSymbols(str) {
    return str.replace(/[.,:;"/\\`~=+[\]{}<>'!@#&*^()%$_]/g, '');
}

function hyphenate(str) {
    let formatted = str.replace(/ /g, '-');
    while (/--/.test(formatted)) {
        formatted = formatted.replace(/--/g, '-');
    }

    return formatted;
}

function generateBase64String() {
    return crypto.randomBytes(64).toString('base64');
}

function createPbkdf2Hash(salt, secret, digest = 'sha256') {
    if (!secret) {
        throw new Error('Missing Secret value for creating Hash');
    }

    if (!crypto.getHashes().includes(digest)) {
        throw new Error(`${digest} is not a valid digest option to create a hash`);
    }

    return crypto.pbkdf2Sync(secret, salt, 1000, 64, digest).toString('base64');
}

function createShaHash(str, digest = 'sha256') {
    if (!crypto.getHashes().includes(digest)) {
        throw new Error(`${digest} is not a valid digest option to verifying this hash`);
    }

    return crypto.createHash(digest).update(str, 'base64').digest('base64');
}

/**
 * Converts a string value to `Uint8array`
 * @method utilitarian.string.util#toUint
 * @param {string} str A string value to convert to `Uint8array`
 * @returns {Uint8Array} A `Uint8Array` representation of the original string
 */
function toUint(str) {
    const string = window.btoa(unescape(encodeURIComponent(str))),
        chars = string.split(''),
        len = chars.length,
        uintArray = [];

    Array(len).fill().forEach((val, i) => uintArray.push(chars[i].charCodeAt(0)));

    return new Uint8Array(uintArray);
}

/**
 * Converts a string value to an `ArrayBuffer`
 * @method utilitarian.string.util#toArrayBuffer
 * @param {string} str A string value to convert to `ArrayBuffer`
 * @returns {Uint8Array} An `ArrayBuffer` representation of the original string
 */
function toArrayBuffer(str) {
    /* eslint no-bitwise: "off" */
    const len = isString(str) ? str.length : 0,
        buf = new ArrayBuffer(len),
        view = new Uint8Array(buf);

    Array(len).fill().forEach((val, i) => (view[i] = str.charCodeAt(i) & 0xFF));

    return view;
}

/**
 * Converts a base64 encoded string to a `data:image/png` prefixed string,
 * ready to embed into an img HTML element
 * @method utilitarian.string.util#toBase64ImageSrc
 * @param {string} str A base64 encoded image string
 * @returns {string} An formatted base64 image string
 */
function toBase64ImageSrc(str) {
    return (str ? `data:image/png;base64, ${str}` : '');
}

function formatDecimal(num = 0, precision = 2) {
    return +(Math.round(num * 100) / 100).toFixed(precision);
}

function fullTrim(str = '') {
    return str.replace(/\t|\n|\s+/g, ' ').trim();
}

function hashMe(value) {
    let str = (isString(value) || isNumber(value)) ? `${value}` : '';

    if (isArray(value)) {
        str = [...value.filter(isNonEmptyStringOrNumber)].sort().join('');
    } else if (isObject(value)) {
        str = Object.values(value).filter(isNonEmptyStringOrNumber).sort().join('');
    }

    return md5(str);
}

const StringUtils = {
    camelize,
    capitalizeWords,
    fullTrim,
    hyphenate,
    numOfTrailingDigits,
    numOfLeadingDigits,
    numberMe,
    removeSymbols,
    toArrayBuffer,
    toBase64ImageSrc,
    toUint
};

function setStringPrototypes() {
    Object.keys(StringUtils)
        .filter(prop => !Object.keys(String.prototype).includes(prop))
        .forEach((prop) => {
            /* eslint func-names: "off" */
            Object.assign(String.prototype, {
                [prop]: function(...args) {
                    return args.length ?
                        StringUtils[prop].apply(this, [this, ...args]) :
                        StringUtils[prop](this);
                }
            });
        });
}

Object.assign(exports, StringUtils, {
    hashMe,
    dataURIPrefixPattern,
    getDataURIFileExtension,
    formatDecimal,
    generateUUID,
    generateIdWithSuffix,
    randomNumber,
    randomString,
    titleizeUrl,
    generateBase64String,
    createPbkdf2Hash,
    createShaHash,
    setStringPrototypes
});