/*!
 * v0.1.5
 * Copyright (c) 2014 First Opinion
 * formatter.js is open sourced under the MIT license.
 *
 * thanks to digitalBush/jquery.maskedinput for some of the trickier
 * keycode handling
 */ 

//
// Uses CommonJS, AMD or browser globals to create a jQuery plugin.
//
// Similar to jqueryPlugin.js but also tries to
// work in a CommonJS environment.
// It is unlikely jQuery will run in a CommonJS
// environment. See jqueryPlugin.js if you do
// not want to add the extra CommonJS detection.
//
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['jQuery'], factory);
  } else if (typeof exports === 'object') {
    factory(require('jQuery'));
  } else {
    // Browser globals
    factory(root.jQuery);
  }
}(this, function (jQuery) {


/*
 * pattern.js
 *
 * Utilities to parse str pattern and return info
 *
 */
var pattern = function () {
    // Define module
    var pattern = {};
    // Match information
    var DELIM_SIZE = 4;
    // Our regex used to parse
    var regexp = new RegExp('{{([^}]+)}}', 'g');
    //
    // Helper method to parse pattern str
    //
    var getMatches = function (pattern) {
      // Populate array of matches
      var matches = [], match;
      while (match = regexp.exec(pattern)) {
        matches.push(match);
      }
      return matches;
    };
    //
    // Create an object holding all formatted characters
    // with corresponding positions
    //
    pattern.parse = function (pattern) {
      // Our obj to populate
      var info = {
          inpts: {},
          chars: {}
        };
      // Pattern information
      var matches = getMatches(pattern), pLength = pattern.length;
      // Counters
      var mCount = 0, iCount = 0, i = 0;
      // Add inpts, move to end of match, and process
      var processMatch = function (val) {
        var valLength = val.length;
        for (var j = 0; j < valLength; j++) {
          info.inpts[iCount] = val.charAt(j);
          iCount++;
        }
        mCount++;
        i += val.length + DELIM_SIZE - 1;
      };
      // Process match or add chars
      for (i; i < pLength; i++) {
        if (mCount < matches.length && i === matches[mCount].index) {
          processMatch(matches[mCount][1]);
        } else {
          info.chars[i - mCount * DELIM_SIZE] = pattern.charAt(i);
        }
      }
      // Set mLength and return
      info.mLength = i - mCount * DELIM_SIZE;
      return info;
    };
    // Expose
    return pattern;
  }();
/*
 * utils.js
 *
 * Independent helper methods (cross browser, etc..)
 *
 */
var utils = function () {
    // Define module
    var utils = {};
    // Useragent info for keycode handling
    var uAgent = typeof navigator !== 'undefined' ? navigator.userAgent : null;
    //
    // Shallow copy properties from n objects to destObj
    //
    utils.extend = function (destObj) {
      for (var i = 1; i < arguments.length; i++) {
        for (var key in arguments[i]) {
          destObj[key] = arguments[i][key];
        }
      }
      return destObj;
    };
    //
    // Add a given character to a string at a defined pos
    //
    utils.addChars = function (str, chars, pos) {
      return str.substr(0, pos) + chars + str.substr(pos, str.length);
    };
    //
    // Remove a span of characters
    //
    utils.removeChars = function (str, start, end) {
      return str.substr(0, start) + str.substr(end, str.length);
    };
    //
    // Return true/false is num false between bounds
    //
    utils.isBetween = function (num, bounds) {
      bounds.sort(function (a, b) {
        return a - b;
      });
      return num > bounds[0] && num < bounds[1];
    };
    //
    // Helper method for cross browser event listeners
    //
    utils.addListener = function (el, evt, handler) {
      return typeof el.addEventListener !== 'undefined' ? el.addEventListener(evt, handler, false) : el.attachEvent('on' + evt, handler);
    };
    //
    // Helper method for cross browser implementation of preventDefault
    //
    utils.preventDefault = function (evt) {
      return evt.preventDefault ? evt.preventDefault() : evt.returnValue = false;
    };
    //
    // Helper method for cross browser implementation for grabbing
    // clipboard data
    //
    utils.getClip = function (evt) {
      if (evt.clipboardData) {
        return evt.clipboardData.getData('Text');
      }
      if (window.clipboardData) {
        return window.clipboardData.getData('Text');
      }
    };
    //
    // Loop over object and checking for matching properties
    //
    utils.getMatchingKey = function (which, keyCode, keys) {
      // Loop over and return if matched.
      for (var k in keys) {
        var key = keys[k];
        if (which === key.which && keyCode === key.keyCode) {
          return k;
        }
      }
    };
    //
    // Returns true/false if k is a del keyDown
    //
    utils.isDelKeyDown = function (which, keyCode) {
      var keys = {
          'backspace': {
            'which': 8,
            'keyCode': 8
          },
          'delete': {
            'which': 46,
            'keyCode': 46
          }
        };
      return utils.getMatchingKey(which, keyCode, keys);
    };
    //
    // Returns true/false if k is a del keyPress
    //
    utils.isDelKeyPress = function (which, keyCode) {
      var keys = {
          'backspace': {
            'which': 8,
            'keyCode': 8,
            'shiftKey': false
          },
          'delete': {
            'which': 0,
            'keyCode': 46
          }
        };
      return utils.getMatchingKey(which, keyCode, keys);
    };
    // //
    // // Determine if keydown relates to specialKey
    // //
    // utils.isSpecialKeyDown = function (which, keyCode) {
    //   var keys = {
    //     'tab': { 'which': 9, 'keyCode': 9 },
    //     'enter': { 'which': 13, 'keyCode': 13 },
    //     'end': { 'which': 35, 'keyCode': 35 },
    //     'home': { 'which': 36, 'keyCode': 36 },
    //     'leftarrow': { 'which': 37, 'keyCode': 37 },
    //     'uparrow': { 'which': 38, 'keyCode': 38 },
    //     'rightarrow': { 'which': 39, 'keyCode': 39 },
    //     'downarrow': { 'which': 40, 'keyCode': 40 },
    //     'F5': { 'which': 116, 'keyCode': 116 }
    //   };
    //   return utils.getMatchingKey(which, keyCode, keys);
    // };
    //
    // Determine if keypress relates to specialKey
    //
    utils.isSpecialKeyPress = function (which, keyCode) {
      var keys = {
          'tab': {
            'which': 0,
            'keyCode': 9
          },
          'enter': {
            'which': 13,
            'keyCode': 13
          },
          'end': {
            'which': 0,
            'keyCode': 35
          },
          'home': {
            'which': 0,
            'keyCode': 36
          },
          'leftarrow': {
            'which': 0,
            'keyCode': 37
          },
          'uparrow': {
            'which': 0,
            'keyCode': 38
          },
          'rightarrow': {
            'which': 0,
            'keyCode': 39
          },
          'downarrow': {
            'which': 0,
            'keyCode': 40
          },
          'F5': {
            'which': 116,
            'keyCode': 116
          }
        };
      return utils.getMatchingKey(which, keyCode, keys);
    };
    //
    // Returns true/false if modifier key is held down
    //
    utils.isModifier = function (evt) {
      return evt.ctrlKey || evt.altKey || evt.metaKey;
    };
    //
    // Iterates over each property of object or array.
    //
    utils.forEach = function (collection, callback, thisArg) {
      if (collection.hasOwnProperty('length')) {
        for (var index = 0, len = collection.length; index < len; index++) {
          if (callback.call(thisArg, collection[index], index, collection) === false) {
            break;
          }
        }
      } else {
        for (var key in collection) {
          if (collection.hasOwnProperty(key)) {
            if (callback.call(thisArg, collection[key], key, collection) === false) {
              break;
            }
          }
        }
      }
    };
    // Expose
    return utils;
  }();
/*
* pattern-matcher.js
*
* Parses a pattern specification and determines appropriate pattern for an
* input string
*
*/
var patternMatcher = function (pattern, utils) {
    //
    // Parse a matcher string into a RegExp. Accepts valid regular
    // expressions and the catchall '*'.
    // @private
    //
    var parseMatcher = function (matcher) {
      if (matcher === '*') {
        return /.*/;
      }
      return new RegExp(matcher);
    };
    //
    // Parse a pattern spec and return a function that returns a pattern
    // based on user input. The first matching pattern will be chosen.
    // Pattern spec format:
    // Array [
    //  Object: { Matcher(RegExp String) : Pattern(Pattern String) },
    //  ...
    // ]
    function patternMatcher(patternSpec) {
      var matchers = [], patterns = [];
      // Iterate over each pattern in order.
      utils.forEach(patternSpec, function (patternMatcher) {
        // Process single property object to obtain pattern and matcher.
        utils.forEach(patternMatcher, function (patternStr, matcherStr) {
          var parsedPattern = pattern.parse(patternStr), regExpMatcher = parseMatcher(matcherStr);
          matchers.push(regExpMatcher);
          patterns.push(parsedPattern);
          // Stop after one iteration.
          return false;
        });
      });
      var getPattern = function (input) {
        var matchedIndex;
        utils.forEach(matchers, function (matcher, index) {
          if (matcher.test(input)) {
            matchedIndex = index;
            return false;
          }
        });
        return matchedIndex === undefined ? null : patterns[matchedIndex];
      };
      return {
        getPattern: getPattern,
        patterns: patterns,
        matchers: matchers
      };
    }
    // Expose
    return patternMatcher;
  }(pattern, utils);
/*
 * inpt-sel.js
 *
 * Cross browser implementation to get and set input selections
 *
 */
var inptSel = function () {
    // Define module
    var inptSel = {};
    //
    // Get begin and end positions of selected input. Return 0's
    // if there is no selectiion data
    //
    inptSel.get = function (el) {
      // If normal browser return with result
      if (typeof el.selectionStart === 'number') {
        return {
          begin: el.selectionStart,
          end: el.selectionEnd
        };
      }
      // Uh-Oh. We must be IE. Fun with TextRange!!
      var range = document.selection.createRange();
      // Determine if there is a selection
      if (range && range.parentElement() === el) {
        var inputRange = el.createTextRange(), endRange = el.createTextRange(), length = el.value.length;
        // Create a working TextRange for the input selection
        inputRange.moveToBookmark(range.getBookmark());
        // Move endRange begin pos to end pos (hence endRange)
        endRange.collapse(false);
        // If we are at the very end of the input, begin and end
        // must both be the length of the el.value
        if (inputRange.compareEndPoints('StartToEnd', endRange) > -1) {
          return {
            begin: length,
            end: length
          };
        }
        // Note: moveStart usually returns the units moved, which 
        // one may think is -length, however, it will stop when it
        // gets to the begin of the range, thus giving us the
        // negative value of the pos.
        return {
          begin: -inputRange.moveStart('character', -length),
          end: -inputRange.moveEnd('character', -length)
        };
      }
      //Return 0's on no selection data
      return {
        begin: 0,
        end: 0
      };
    };
    //
    // Set the caret position at a specified location
    //
    inptSel.set = function (el, pos) {
      // Normalize pos
      if (typeof pos !== 'object') {
        pos = {
          begin: pos,
          end: pos
        };
      }
      // If normal browser
      if (el.setSelectionRange) {
        el.focus();
        el.setSelectionRange(pos.begin, pos.end);
      } else if (el.createTextRange) {
        var range = el.createTextRange();
        range.collapse(true);
        range.moveEnd('character', pos.end);
        range.moveStart('character', pos.begin);
        range.select();
      }
    };
    // Expose
    return inptSel;
  }();
/*
 * formatter.js
 *
 * Class used to format input based on passed pattern
 *
 */
var formatter = function (patternMatcher, inptSel, utils) {
    // Defaults
    var defaults = {
        persistent: false,
        repeat: false,
        placeholder: ' '
      };
    // Regexs for input validation
    var inptRegs = {
        '9': /[0-9]/,
        'a': /[A-Za-z]/,
        '*': /[A-Za-z0-9]/
      };
    //
    // Class Constructor - Called with new Formatter(el, opts)
    // Responsible for setting up required instance variables, and
    // attaching the event listener to the element.
    //
    function Formatter(el, opts) {
      // Cache this
      var self = this;
      // Make sure we have an element. Make accesible to instance
      self.el = el;
      if (!self.el) {
        throw new TypeError('Must provide an existing element');
      }
      // Merge opts with defaults
      self.opts = utils.extend({}, defaults, opts);
      // 1 pattern is special case
      if (typeof self.opts.pattern !== 'undefined') {
        self.opts.patterns = self._specFromSinglePattern(self.opts.pattern);
        delete self.opts.pattern;
      }
      // Make sure we have valid opts
      if (typeof self.opts.patterns === 'undefined') {
        throw new TypeError('Must provide a pattern or array of patterns');
      }
      self.patternMatcher = patternMatcher(self.opts.patterns);
      // Upate pattern with initial value
      self._updatePattern();
      // Init values
      self.hldrs = {};
      self.focus = 0;
      // Add Listeners
      utils.addListener(self.el, 'keydown', function (evt) {
        self._keyDown(evt);
      });
      utils.addListener(self.el, 'keypress', function (evt) {
        self._keyPress(evt);
      });
      utils.addListener(self.el, 'paste', function (evt) {
        self._paste(evt);
      });
      // Persistence
      if (self.opts.persistent) {
        // Format on start
        self._processKey('', false);
        self.el.blur();
        // Add Listeners
        utils.addListener(self.el, 'focus', function (evt) {
          self._focus(evt);
        });
        utils.addListener(self.el, 'click', function (evt) {
          self._focus(evt);
        });
        utils.addListener(self.el, 'touchstart', function (evt) {
          self._focus(evt);
        });
      }
    }
    //
    // @public
    // Add new char
    //
    Formatter.addInptType = function (chr, reg) {
      inptRegs[chr] = reg;
    };
    //
    // @public
    // Apply the given pattern to the current input without moving caret.
    //
    Formatter.prototype.resetPattern = function (str) {
      // Update opts to hold new pattern
      this.opts.patterns = str ? this._specFromSinglePattern(str) : this.opts.patterns;
      // Get current state
      this.sel = inptSel.get(this.el);
      this.val = this.el.value;
      // Init values
      this.delta = 0;
      // Remove all formatted chars from val
      this._removeChars();
      this.patternMatcher = patternMatcher(this.opts.patterns);
      // Update pattern
      var newPattern = this.patternMatcher.getPattern(this.val);
      this.mLength = newPattern.mLength;
      this.chars = newPattern.chars;
      this.inpts = newPattern.inpts;
      // Format on start
      this._processKey('', false, true);
    };
    //
    // @private
    // Determine correct format pattern based on input val
    //
    Formatter.prototype._updatePattern = function () {
      // Determine appropriate pattern
      var newPattern = this.patternMatcher.getPattern(this.val);
      // Only update the pattern if there is an appropriate pattern for the value.
      // Otherwise, leave the current pattern (and likely delete the latest character.)
      if (newPattern) {
        // Get info about the given pattern
        this.mLength = newPattern.mLength;
        this.chars = newPattern.chars;
        this.inpts = newPattern.inpts;
      }
    };
    //
    // @private
    // Handler called on all keyDown strokes. All keys trigger
    // this handler. Only process delete keys.
    //
    Formatter.prototype._keyDown = function (evt) {
      // The first thing we need is the character code
      var k = evt.which || evt.keyCode;
      // If delete key
      if (k && utils.isDelKeyDown(evt.which, evt.keyCode)) {
        // Process the keyCode and prevent default
        this._processKey(null, k);
        return utils.preventDefault(evt);
      }
    };
    //
    // @private
    // Handler called on all keyPress strokes. Only processes
    // character keys (as long as no modifier key is in use).
    //
    Formatter.prototype._keyPress = function (evt) {
      // The first thing we need is the character code
      var k, isSpecial;
      // Mozilla will trigger on special keys and assign the the value 0
      // We want to use that 0 rather than the keyCode it assigns.
      k = evt.which || evt.keyCode;
      isSpecial = utils.isSpecialKeyPress(evt.which, evt.keyCode);
      // Process the keyCode and prevent default
      if (!utils.isDelKeyPress(evt.which, evt.keyCode) && !isSpecial && !utils.isModifier(evt)) {
        this._processKey(String.fromCharCode(k), false);
        return utils.preventDefault(evt);
      }
    };
    //
    // @private
    // Handler called on paste event.
    //
    Formatter.prototype._paste = function (evt) {
      // Process the clipboard paste and prevent default
      this._processKey(utils.getClip(evt), false);
      return utils.preventDefault(evt);
    };
    //
    // @private
    // Handle called on focus event.
    //
    Formatter.prototype._focus = function () {
      // Wrapped in timeout so that we can grab input selection
      var self = this;
      setTimeout(function () {
        // Grab selection
        var selection = inptSel.get(self.el);
        // Char check
        var isAfterStart = selection.end > self.focus, isFirstChar = selection.end === 0;
        // If clicked in front of start, refocus to start
        if (isAfterStart || isFirstChar) {
          inptSel.set(self.el, self.focus);
        }
      }, 0);
    };
    //
    // @private
    // Using the provided key information, alter el value.
    //
    Formatter.prototype._processKey = function (chars, delKey, ignoreCaret) {
      // Get current state
      this.sel = inptSel.get(this.el);
      this.val = this.el.value;
      // Init values
      this.delta = 0;
      // If chars were highlighted, we need to remove them
      if (this.sel.begin !== this.sel.end) {
        this.delta = -1 * Math.abs(this.sel.begin - this.sel.end);
        this.val = utils.removeChars(this.val, this.sel.begin, this.sel.end);
      } else if (delKey && delKey === 46) {
        this._delete();
      } else if (delKey && this.sel.begin - 1 >= 0) {
        // Always have a delta of at least -1 for the character being deleted.
        this.val = utils.removeChars(this.val, this.sel.end - 1, this.sel.end);
        this.delta -= 1;
      } else if (delKey) {
        return true;
      }
      // If the key is not a del key, it should convert to a str
      if (!delKey) {
        // Add char at position and increment delta
        this.val = utils.addChars(this.val, chars, this.sel.begin);
        this.delta += chars.length;
      }
      // Format el.value (also handles updating caret position)
      this._formatValue(ignoreCaret);
    };
    //
    // @private
    // Deletes the character in front of it
    //
    Formatter.prototype._delete = function () {
      // Adjust focus to make sure its not on a formatted char
      while (this.chars[this.sel.begin]) {
        this._nextPos();
      }
      // As long as we are not at the end
      if (this.sel.begin < this.val.length) {
        // We will simulate a delete by moving the caret to the next char
        // and then deleting
        this._nextPos();
        this.val = utils.removeChars(this.val, this.sel.end - 1, this.sel.end);
        this.delta = -1;
      }
    };
    //
    // @private
    // Quick helper method to move the caret to the next pos
    //
    Formatter.prototype._nextPos = function () {
      this.sel.end++;
      this.sel.begin++;
    };
    //
    // @private
    // Alter element value to display characters matching the provided
    // instance pattern. Also responsible for updating
    //
    Formatter.prototype._formatValue = function (ignoreCaret) {
      // Set caret pos
      this.newPos = this.sel.end + this.delta;
      // Remove all formatted chars from val
      this._removeChars();
      // Switch to first matching pattern based on val
      this._updatePattern();
      // Validate inputs
      this._validateInpts();
      // Add formatted characters
      this._addChars();
      // Set value and adhere to maxLength
      this.el.value = this.val.substr(0, this.mLength);
      // Set new caret position
      if (typeof ignoreCaret === 'undefined' || ignoreCaret === false) {
        inptSel.set(this.el, this.newPos);
      }
    };
    //
    // @private
    // Remove all formatted before and after a specified pos
    //
    Formatter.prototype._removeChars = function () {
      // Delta shouldn't include placeholders
      if (this.sel.end > this.focus) {
        this.delta += this.sel.end - this.focus;
      }
      // Account for shifts during removal
      var shift = 0;
      // Loop through all possible char positions
      for (var i = 0; i <= this.mLength; i++) {
        // Get transformed position
        var curChar = this.chars[i], curHldr = this.hldrs[i], pos = i + shift, val;
        // If after selection we need to account for delta
        pos = i >= this.sel.begin ? pos + this.delta : pos;
        val = this.val.charAt(pos);
        // Remove char and account for shift
        if (curChar && curChar === val || curHldr && curHldr === val) {
          this.val = utils.removeChars(this.val, pos, pos + 1);
          shift--;
        }
      }
      // All hldrs should be removed now
      this.hldrs = {};
      // Set focus to last character
      this.focus = this.val.length;
    };
    //
    // @private
    // Make sure all inpts are valid, else remove and update delta
    //
    Formatter.prototype._validateInpts = function () {
      // Loop over each char and validate
      for (var i = 0; i < this.val.length; i++) {
        // Get char inpt type
        var inptType = this.inpts[i];
        // Checks
        var isBadType = !inptRegs[inptType], isInvalid = !isBadType && !inptRegs[inptType].test(this.val.charAt(i)), inBounds = this.inpts[i];
        // Remove if incorrect and inbounds
        if ((isBadType || isInvalid) && inBounds) {
          this.val = utils.removeChars(this.val, i, i + 1);
          this.focusStart--;
          this.newPos--;
          this.delta--;
          i--;
        }
      }
    };
    //
    // @private
    // Loop over val and add formatted chars as necessary
    //
    Formatter.prototype._addChars = function () {
      if (this.opts.persistent) {
        // Loop over all possible characters
        for (var i = 0; i <= this.mLength; i++) {
          if (!this.val.charAt(i)) {
            // Add placeholder at pos
            this.val = utils.addChars(this.val, this.opts.placeholder, i);
            this.hldrs[i] = this.opts.placeholder;
          }
          this._addChar(i);
        }
        // Adjust focus to make sure its not on a formatted char
        while (this.chars[this.focus]) {
          this.focus++;
        }
      } else {
        // Avoid caching val.length, as they may change in _addChar.
        for (var j = 0; j <= this.val.length; j++) {
          // When moving backwards there are some race conditions where we
          // dont want to add the character
          if (this.delta <= 0 && j === this.focus) {
            return true;
          }
          // Place character in current position of the formatted string.
          this._addChar(j);
        }
      }
    };
    //
    // @private
    // Add formattted char at position
    //
    Formatter.prototype._addChar = function (i) {
      // If char exists at position
      var chr = this.chars[i];
      if (!chr) {
        return true;
      }
      // If chars are added in between the old pos and new pos
      // we need to increment pos and delta
      if (utils.isBetween(i, [
          this.sel.begin - 1,
          this.newPos + 1
        ])) {
        this.newPos++;
        this.delta++;
      }
      // If character added before focus, incr
      if (i <= this.focus) {
        this.focus++;
      }
      // Updateholder
      if (this.hldrs[i]) {
        delete this.hldrs[i];
        this.hldrs[i + 1] = this.opts.placeholder;
      }
      // Update value
      this.val = utils.addChars(this.val, chr, i);
    };
    //
    // @private
    // Create a patternSpec for passing into patternMatcher that
    // has exactly one catch all pattern.
    //
    Formatter.prototype._specFromSinglePattern = function (patternStr) {
      return [{ '*': patternStr }];
    };
    // Expose
    return Formatter;
  }(patternMatcher, inptSel, utils);



// A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations
var pluginName = 'formatter';

$.fn[pluginName] = function (options) {

  // Initiate plugin if options passed
  if (typeof options == 'object') {
    this.each(function () {
      if (!$.data(this, 'plugin_' + pluginName)) {
        $.data(this, 'plugin_' + pluginName,
        new formatter(this, options));
      }
    });
  }

  // Add resetPattern method to plugin
  this.resetPattern = function (str) {
    this.each(function () {
      var formatted = $.data(this, 'plugin_' + pluginName);
      // resetPattern for instance
      if (formatted) { formatted.resetPattern(str); }
    });
    // Chainable please
    return this;
  };

  // Chainable please
  return this;
};

$.fn[pluginName].addInptType = function (chr, regexp) {
  formatter.addInptType(chr, regexp);
};


}));