/*
 * Behave.js
 *
 * Copyright 2013, Jacob Kelley - http://jakiestfu.com/
 * Released under the MIT Licence
 * http://opensource.org/licenses/MIT
 *
 * Github:  http://github.com/jakiestfu/Behave.js/
 * Version: 1.5
 */

(function (undefined) {

  'use strict';

  var BehaveHooks = BehaveHooks || (function () {
      var hooks = {};

      return {
        add: function (hookName, fn) {
          if (typeof hookName == "object") {
            var i;
            for (i = 0; i < hookName.length; i++) {
              var theHook = hookName[i];
              if (!hooks[theHook]) {
                hooks[theHook] = [];
              }
              hooks[theHook].push(fn);
            }
          } else {
            if (!hooks[hookName]) {
              hooks[hookName] = [];
            }
            hooks[hookName].push(fn);
          }
        },
        get: function (hookName) {
          if (hooks[hookName]) {
            return hooks[hookName];
          }
        }
      };

    })(),
    Behave = Behave || function (userOpts) {

      if (typeof String.prototype.repeat !== 'function') {
        String.prototype.repeat = function (times) {
          if (times < 1) {
            return '';
          }
          if (times % 2) {
            return this.repeat(times - 1) + this;
          }
          var half = this.repeat(times / 2);
          return half + half;
        };
      }

      if (typeof Array.prototype.filter !== 'function') {
        Array.prototype.filter = function (func /*, thisp */ ) {
          if (this === null) {
            throw new TypeError();
          }

          var t = Object(this),
            len = t.length >>> 0;
          if (typeof func != "function") {
            throw new TypeError();
          }
          var res = [],
            thisp = arguments[1];
          for (var i = 0; i < len; i++) {
            if (i in t) {
              var val = t[i];
              if (func.call(thisp, val, i, t)) {
                res.push(val);
              }
            }
          }
          return res;
        };
      }

      var defaults = {
          textarea: null,
          replaceTab: true,
          softTabs: true,
          tabSize: 4,
          autoOpen: true,
          overwrite: true,
          autoStrip: true,
          autoIndent: true,
          fence: false
        },
        tab,
        newLine,
        charSettings = {

          keyMap: [
            {
              open: "\"",
              close: "\"",
              canBreak: false
            },
            {
              open: "'",
              close: "'",
              canBreak: false
            },
            {
              open: "(",
              close: ")",
              canBreak: false
            },
            {
              open: "[",
              close: "]",
              canBreak: true
            },
            {
              open: "{",
              close: "}",
              canBreak: true
            }
            ]

        },
        utils = {

          _callHook: function (hookName, passData) {
            var hooks = BehaveHooks.get(hookName);
            passData = typeof passData == "boolean" && passData === false ? false : true;

            if (hooks) {
              if (passData) {
                var theEditor = defaults.textarea,
                  textVal = theEditor.value,
                  caretPos = utils.cursor.get(),
                  i;

                for (i = 0; i < hooks.length; i++) {
                  hooks[i].call(undefined, {
                    editor: {
                      element: theEditor,
                      text: textVal,
                      levelsDeep: utils.levelsDeep()
                    },
                    caret: {
                      pos: caretPos
                    },
                    lines: {
                      current: utils.cursor.getLine(textVal, caretPos),
                      total: utils.editor.getLines(textVal)
                    }
                  });
                }
              } else {
                for (i = 0; i < hooks.length; i++) {
                  hooks[i].call(undefined);
                }
              }
            }
          },

          defineNewLine: function () {
            var ta = document.createElement('textarea');
            ta.value = "\n";

            if (ta.value.length == 2) {
              newLine = "\r\n";
            } else {
              newLine = "\n";
            }
          },
          defineTabSize: function (tabSize) {
            if (typeof defaults.textarea.style.OTabSize != "undefined") {
              defaults.textarea.style.OTabSize = tabSize;
              return;
            }
            if (typeof defaults.textarea.style.MozTabSize != "undefined") {
              defaults.textarea.style.MozTabSize = tabSize;
              return;
            }
            if (typeof defaults.textarea.style.tabSize != "undefined") {
              defaults.textarea.style.tabSize = tabSize;
              return;
            }
          },
          cursor: {
            getLine: function (textVal, pos) {
              return ((textVal.substring(0, pos)).split("\n")).length;
            },
            get: function () {

              if (typeof document.createElement('textarea').selectionStart === "number") {
                return defaults.textarea.selectionStart;
              } else if (document.selection) {
                var caretPos = 0,
                  range = defaults.textarea.createTextRange(),
                  rangeDupe = document.selection.createRange().duplicate(),
                  rangeDupeBookmark = rangeDupe.getBookmark();
                range.moveToBookmark(rangeDupeBookmark);

                while (range.moveStart('character', -1) !== 0) {
                  caretPos++;
                }
                return caretPos;
              }
            },
            set: function (start, end) {
              if (!end) {
                end = start;
              }
              if (defaults.textarea.setSelectionRange) {
                defaults.textarea.focus();
                defaults.textarea.setSelectionRange(start, end);
              } else if (defaults.textarea.createTextRange) {
                var range = defaults.textarea.createTextRange();
                range.collapse(true);
                range.moveEnd('character', end);
                range.moveStart('character', start);
                range.select();
              }
            },
            selection: function () {
              var textAreaElement = defaults.textarea,
                start = 0,
                end = 0,
                normalizedValue,
                range,
                textInputRange,
                len,
                endRange;

              if (typeof textAreaElement.selectionStart == "number" && typeof textAreaElement.selectionEnd == "number") {
                start = textAreaElement.selectionStart;
                end = textAreaElement.selectionEnd;
              } else {
                range = document.selection.createRange();

                if (range && range.parentElement() == textAreaElement) {

                  normalizedValue = utils.editor.get();
                  len = normalizedValue.length;

                  textInputRange = textAreaElement.createTextRange();
                  textInputRange.moveToBookmark(range.getBookmark());

                  endRange = textAreaElement.createTextRange();
                  endRange.collapse(false);

                  if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
                    start = end = len;
                  } else {
                    start = -textInputRange.moveStart("character", -len);
                    start += normalizedValue.slice(0, start).split(newLine).length - 1;

                    if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
                      end = len;
                    } else {
                      end = -textInputRange.moveEnd("character", -len);
                      end += normalizedValue.slice(0, end).split(newLine).length - 1;
                    }
                  }
                }
              }

              return start == end ? false : {
                start: start,
                end: end
              };
            }
          },
          editor: {
            getLines: function (textVal) {
              return (textVal).split("\n").length;
            },
            get: function () {
              return defaults.textarea.value.replace(/\r/g, '');
            },
            set: function (data) {
              defaults.textarea.value = data;
            }
          },
          fenceRange: function () {
            if (typeof defaults.fence == "string") {

              var data = utils.editor.get(),
                pos = utils.cursor.get(),
                hacked = 0,
                matchedFence = data.indexOf(defaults.fence),
                matchCase = 0;

              while (matchedFence >= 0) {
                matchCase++;
                if (pos < (matchedFence + hacked)) {
                  break;
                }

                hacked += matchedFence + defaults.fence.length;
                data = data.substring(matchedFence + defaults.fence.length);
                matchedFence = data.indexOf(defaults.fence);

              }

              if ((hacked) < pos && ((matchedFence + hacked) > pos) && matchCase % 2 === 0) {
                return true;
              }
              return false;
            } else {
              return true;
            }
          },
          isEven: function (_this, i) {
            return i % 2;
          },
          levelsDeep: function () {
            var pos = utils.cursor.get(),
              val = utils.editor.get();

            var left = val.substring(0, pos),
              levels = 0,
              i, j;

            for (i = 0; i < left.length; i++) {
              for (j = 0; j < charSettings.keyMap.length; j++) {
                if (charSettings.keyMap[j].canBreak) {
                  if (charSettings.keyMap[j].open == left.charAt(i)) {
                    levels++;
                  }

                  if (charSettings.keyMap[j].close == left.charAt(i)) {
                    levels--;
                  }
                }
              }
            }

            var toDecrement = 0,
              quoteMap = ["'", "\""];
            for (i = 0; i < charSettings.keyMap.length; i++) {
              if (charSettings.keyMap[i].canBreak) {
                for (j in quoteMap) {
                  toDecrement += left.split(quoteMap[j]).filter(utils.isEven).join('').split(charSettings.keyMap[i].open).length - 1;
                }
              }
            }

            var finalLevels = levels - toDecrement;

            return finalLevels >= 0 ? finalLevels : 0;
          },
          deepExtend: function (destination, source) {
            for (var property in source) {
              if (source[property] && source[property].constructor &&
                source[property].constructor === Object) {
                destination[property] = destination[property] || {};
                utils.deepExtend(destination[property], source[property]);
              } else {
                destination[property] = source[property];
              }
            }
            return destination;
          },
          addEvent: function addEvent(element, eventName, func) {
            if (element.addEventListener) {
              element.addEventListener(eventName, func, false);
            } else if (element.attachEvent) {
              element.attachEvent("on" + eventName, func);
            }
          },
          removeEvent: function addEvent(element, eventName, func) {
            if (element.addEventListener) {
              element.removeEventListener(eventName, func, false);
            } else if (element.attachEvent) {
              element.detachEvent("on" + eventName, func);
            }
          },

          preventDefaultEvent: function (e) {
            if (e.preventDefault) {
              e.preventDefault();
            } else {
              e.returnValue = false;
            }
          }
        },
        intercept = {
          tabKey: function (e) {

            if (!utils.fenceRange()) {
              return;
            }

            if (e.keyCode == 9) {
              utils.preventDefaultEvent(e);

              var toReturn = true;
              utils._callHook('tab:before');

              var selection = utils.cursor.selection(),
                pos = utils.cursor.get(),
                val = utils.editor.get();

              if (selection) {

                var tempStart = selection.start;
                while (tempStart--) {
                  if (val.charAt(tempStart) == "\n") {
                    selection.start = tempStart + 1;
                    break;
                  }
                }

                var toIndent = val.substring(selection.start, selection.end),
                  lines = toIndent.split("\n"),
                  i;

                if (e.shiftKey) {
                  for (i = 0; i < lines.length; i++) {
                    if (lines[i].substring(0, tab.length) == tab) {
                      lines[i] = lines[i].substring(tab.length);
                    }
                  }
                  toIndent = lines.join("\n");

                  utils.editor.set(val.substring(0, selection.start) + toIndent + val.substring(selection.end));
                  utils.cursor.set(selection.start, selection.start + toIndent.length);

                } else {
                  for (i in lines) {
                    lines[i] = tab + lines[i];
                  }
                  toIndent = lines.join("\n");

                  utils.editor.set(val.substring(0, selection.start) + toIndent + val.substring(selection.end));
                  utils.cursor.set(selection.start, selection.start + toIndent.length);
                }
              } else {
                var left = val.substring(0, pos),
                  right = val.substring(pos),
                  edited = left + tab + right;

                if (e.shiftKey) {
                  if (val.substring(pos - tab.length, pos) == tab) {
                    edited = val.substring(0, pos - tab.length) + right;
                    utils.editor.set(edited);
                    utils.cursor.set(pos - tab.length);
                  }
                } else {
                  utils.editor.set(edited);
                  utils.cursor.set(pos + tab.length);
                  toReturn = false;
                }
              }
              utils._callHook('tab:after');
            }
            return toReturn;
          },
          enterKey: function (e) {

            if (!utils.fenceRange()) {
              return;
            }

            if (e.keyCode == 13) {

              utils.preventDefaultEvent(e);
              utils._callHook('enter:before');

              var pos = utils.cursor.get(),
                val = utils.editor.get(),
                left = val.substring(0, pos),
                right = val.substring(pos),
                leftChar = left.charAt(left.length - 1),
                rightChar = right.charAt(0),
                numTabs = utils.levelsDeep(),
                ourIndent = "",
                closingBreak = "",
                finalCursorPos,
                i;
              if (!numTabs) {
                finalCursorPos = 1;
              } else {
                while (numTabs--) {
                  ourIndent += tab;
                }
                ourIndent = ourIndent;
                finalCursorPos = ourIndent.length + 1;

                for (i = 0; i < charSettings.keyMap.length; i++) {
                  if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar) {
                    closingBreak = newLine;
                  }
                }

              }

              var edited = left + newLine + ourIndent + closingBreak + (ourIndent.substring(0, ourIndent.length - tab.length)) + right;
              utils.editor.set(edited);
              utils.cursor.set(pos + finalCursorPos);
              utils._callHook('enter:after');
            }
          },
          deleteKey: function (e) {

            if (!utils.fenceRange()) {
              return;
            }

            if (e.keyCode == 8) {
              utils.preventDefaultEvent(e);

              utils._callHook('delete:before');

              var pos = utils.cursor.get(),
                val = utils.editor.get(),
                left = val.substring(0, pos),
                right = val.substring(pos),
                leftChar = left.charAt(left.length - 1),
                rightChar = right.charAt(0),
                i;

              if (utils.cursor.selection() === false) {
                for (i = 0; i < charSettings.keyMap.length; i++) {
                  if (charSettings.keyMap[i].open == leftChar && charSettings.keyMap[i].close == rightChar) {
                    var edited = val.substring(0, pos - 1) + val.substring(pos + 1);
                    utils.editor.set(edited);
                    utils.cursor.set(pos - 1);
                    return;
                  }
                }
                var edited = val.substring(0, pos - 1) + val.substring(pos);
                utils.editor.set(edited);
                utils.cursor.set(pos - 1);
              } else {
                var sel = utils.cursor.selection(),
                  edited = val.substring(0, sel.start) + val.substring(sel.end);
                utils.editor.set(edited);
                utils.cursor.set(pos);
              }

              utils._callHook('delete:after');

            }
          }
        },
        charFuncs = {
          openedChar: function (_char, e) {
            utils.preventDefaultEvent(e);
            utils._callHook('openChar:before');
            var pos = utils.cursor.get(),
              val = utils.editor.get(),
              left = val.substring(0, pos),
              right = val.substring(pos),
              edited = left + _char.open + _char.close + right;

            defaults.textarea.value = edited;
            utils.cursor.set(pos + 1);
            utils._callHook('openChar:after');
          },
          closedChar: function (_char, e) {
            var pos = utils.cursor.get(),
              val = utils.editor.get(),
              toOverwrite = val.substring(pos, pos + 1);
            if (toOverwrite == _char.close) {
              utils.preventDefaultEvent(e);
              utils._callHook('closeChar:before');
              utils.cursor.set(utils.cursor.get() + 1);
              utils._callHook('closeChar:after');
              return true;
            }
            return false;
          }
        },
        action = {
          filter: function (e) {

            if (!utils.fenceRange()) {
              return;
            }

            var theCode = e.which || e.keyCode;

            if (theCode == 39 || theCode == 40 && e.which === 0) {
              return;
            }

            var _char = String.fromCharCode(theCode),
              i;

            for (i = 0; i < charSettings.keyMap.length; i++) {

              if (charSettings.keyMap[i].close == _char) {
                var didClose = defaults.overwrite && charFuncs.closedChar(charSettings.keyMap[i], e);

                if (!didClose && charSettings.keyMap[i].open == _char && defaults.autoOpen) {
                  charFuncs.openedChar(charSettings.keyMap[i], e);
                }
              } else if (charSettings.keyMap[i].open == _char && defaults.autoOpen) {
                charFuncs.openedChar(charSettings.keyMap[i], e);
              }
            }
          },
          listen: function () {

            if (defaults.replaceTab) {
              utils.addEvent(defaults.textarea, 'keydown', intercept.tabKey);
            }
            if (defaults.autoIndent) {
              utils.addEvent(defaults.textarea, 'keydown', intercept.enterKey);
            }
            if (defaults.autoStrip) {
              utils.addEvent(defaults.textarea, 'keydown', intercept.deleteKey);
            }

            utils.addEvent(defaults.textarea, 'keypress', action.filter);

            utils.addEvent(defaults.textarea, 'keydown', function () {
              utils._callHook('keydown');
            });
            utils.addEvent(defaults.textarea, 'keyup', function () {
              utils._callHook('keyup');
            });
          }
        },
        init = function (opts) {

          if (opts.textarea) {
            utils._callHook('init:before', false);
            utils.deepExtend(defaults, opts);
            utils.defineNewLine();

            if (defaults.softTabs) {
              tab = " ".repeat(defaults.tabSize);
            } else {
              tab = "\t";

              utils.defineTabSize(defaults.tabSize);
            }

            action.listen();
            utils._callHook('init:after', false);
          }

        };

      this.destroy = function () {
        utils.removeEvent(defaults.textarea, 'keydown', intercept.tabKey);
        utils.removeEvent(defaults.textarea, 'keydown', intercept.enterKey);
        utils.removeEvent(defaults.textarea, 'keydown', intercept.deleteKey);
        utils.removeEvent(defaults.textarea, 'keypress', action.filter);
      };

      init(userOpts);

    };

  if (typeof module !== 'undefined' && module.exports) {
    module.exports = Behave;
  }

  if (typeof ender === 'undefined') {
    this.Behave = Behave;
    this.BehaveHooks = BehaveHooks;
  }

  if (typeof define === "function" && define.amd) {
    define("behave", [], function () {
      return Behave;
    });
  }

}).call(this);
