(function($) {

  $.fn.glossaryHighlighter = function(method) {

    var defaults = {
      path: 'glossary.json',
      wrapTag: 'em',
      wrapClass: 'glossary-item',
      ghostTag: 'span',
      ghostClass: 'glossary-ghost',
      dialogPrefix: 'glossary-num-',
      dialogs: {},
      initialState: 'off',
      onGlossaryChange: null
    },
    glossaryData = {
      glossaryTerms: [],
      fullVariantsList: '',
      hasGhostElements: false
    };

    function toggle($this, action) {
      
      var current = ($this.find(defaults.wrapTag+'.'+defaults.wrapClass).length > 0) ? 'on' : 'off';
      
      if (action === current) return;
      
      if (action === '') {
        action = (current === 'on') ? 'off' : 'on';
      } else {
        action = (action === "on") ? "on" : "off";
      }
      
      if (action === "on") {
        
        // First time wrapping terms so use findAndReplaceInDOM
        if (!glossaryData.hasGhostElements) {
          // Make regex and use to replace in DOM
          var regex = RegExp("\\b\(" + glossaryData.fullVariantsList + "\)\\b", "gim");
          findAndReplaceInDOM($this[0], regex);
          // After we wrap all the glossary terms we need to add the identifier class
          $this.find(defaults.wrapTag + '.' + defaults.wrapClass).each(function() {
            var $glossaryTerm = $(this);
            $glossaryTerm.addClass($('div[rel*="' + $glossaryTerm.text().toLowerCase() + '|"]').attr('id'));
          });
        } else {
          // Grab any elements we tagged with the ghost tag.class and change them to the glossary tag.class
          $this.find(defaults.ghostTag + '.' + defaults.ghostClass).each(function() {
            swapElements($(this), false);
          });
        }
        
      } else {
        
        // Find each glossary item and replace it with the ghost element
        $this.find(defaults.wrapTag + "." + defaults.wrapClass).each(function() {
          swapElements($(this), true);
        });
        // Close any open dialogs
        if (typeof defaults.dialogs.closeAll === 'function') {
          defaults.dialogs.closeAll();
        }
        // We now have ghost elements so no need to do any DOM recursing
        glossaryData.hasGhostElements = true;

        $this.data('glossaryData', glossaryData);
      
      }
      
      if (typeof defaults.onGlossaryChange === 'function') defaults.onGlossaryChange(action);
    }

    function swapElements($this, toGhost) {
      // Used for changing the tag and the generic class on a glossary item
      var newClass = (toGhost) ? defaults.ghostClass : defaults.wrapClass;
      var oldClass = (toGhost) ? defaults.wrapClass : defaults.ghostClass;
      var newTag = (toGhost) ? defaults.ghostTag : defaults.wrapTag;
      var classes = $this.attr('class').replace(oldClass, newClass);

      $this.replaceWith('<' + newTag + ' class="' + classes + '">' + $this.html() + '</' + newTag + '>');
    }

    function findAndReplaceInDOM(node, regex) {

      var start, end, match, parent, leftNode, rightNode, replacementNode, text, d = document,
        _stringBuilder = "",
        leftNodeText = "",
        rightNodeText = "",
        replacementText = "",
        previousRight = "",
        lastIndex = 0,
        hasMatch = false,
        nextSib = null;

      // Loop through all childNodes of "node"
      if (node = node && node.firstChild) {
        do {

          nextSib = node.nextSibling;

          if (node.nodeType === 1) {

            // Regular element, recurse:
            findAndReplaceInDOM(node, regex);

          } else if (node.nodeType === 3) {

            // Text node, introspect
            parent = node.parentNode;
            text = node.data;

            regex.lastIndex = 0;

            // Loop thru each match
            while (match = regex.exec(text)) {
              hasMatch = true;

              end = regex.lastIndex;
              start = end - match[0].length;
              // The goal here is to wrap all match worded in some identifying text which can then easily be swapped for html later
              if (previousRight === "") {
                // First match will use the initial values
                replacementText = "____|" + match[0] + "|____";
                leftNodeText = text.substring(0, start);
                rightNodeText = text.substring(end);
              } else {
                // Subsequent matches will use the values set by the previous loop
                replacementText = "____|" + previousRight.substring(start - lastIndex, end - lastIndex) + "|____";
                leftNodeText = previousRight.substring(0, start - lastIndex);
                rightNodeText = previousRight.substring(end - lastIndex);
              }

              // Append the leftNodeText and replacementText to a string that will rebuild the text node as it loops
              _stringBuilder += leftNodeText + replacementText;

              // Set the lastIndex and the previous right text node for the next loop
              lastIndex = regex.lastIndex;
              previousRight = rightNodeText;

            }

            // Loop is done, if we have a match then convert the identifying text to html
            if (hasMatch) {
              _stringBuilder += rightNodeText;
              var _t = $(node);
              var _p = _t.parent();
              _p.html(_p.html().replace(_t[0].data, _stringBuilder.replace(/____\|/g, '<' + defaults.wrapTag + ' class="' + defaults.wrapClass + '">').replace(/\|____/g, '</' + defaults.wrapTag + '>')));
            }

          }
        }
        while (node = nextSib);
      }
      
    }
    
    function extendDefaults($el) {
      $.extend(true, defaults, ($el.data('glossaryHighlighter') || {}));
      $.extend(true, glossaryData, ($el.data('glossaryData') || {}));
    }

    var methods = {

      init: function(options) {

        if (options) {
          $.extend(true, defaults, options);
        }

        var $this = $(this);
        // Load the json
        $.getJSON(defaults.path, function(data) {
          var i = data.length,
              fullVariantsList = '';
          while (i--) {
            var variants = data[i].variants.replace(/\s*,\s*/g, "|").toLowerCase();
            if (!variants || typeof variants === "undefined") {
              variants = data[i].name;
            }
            fullVariantsList += variants + "|";

            // If we want dialog popups when items are clicked
            if (typeof defaults.dialogs.init === 'function') {
              defaults.dialogs.init({
                id: defaults.dialogPrefix + i,
                definition: data[i].definition,
                variants: variants + "|",
                name: data[i].name
              });
            }
          }
          glossaryData.fullVariantsList = fullVariantsList.substring(0, fullVariantsList.length - 1);
          
          if (typeof defaults.dialogs.click === 'function') {
            // Bind click for glossary item
            $this.delegate(defaults.wrapTag + '.' + defaults.wrapClass, 'click', function(e) {
              e.preventDefault();
              e.stopImmediatePropagation();
              defaults.dialogs.click($(this), defaults.dialogPrefix);
            });
          }

          // Set terms so we can proceed with the initialState
          glossaryData.glossaryTerms = data;

          // Apply the initial state
          return $this.each(function() {
            $this.data('glossaryHighlighter', defaults);
            $this.data('glossaryData', glossaryData);
            toggle($this, defaults.initialState);
          });

        });
      },

      on: function() {
        return this.each(function() {
          var $this = $(this);
          extendDefaults($this);
          toggle($this, 'on');
        });
      },

      off: function() {
        return this.each(function() {
          var $this = $(this);
          extendDefaults($this);
          toggle($this, 'off');
        });
      },

      toggle: function() {
        return this.each(function() {
          var $this = $(this);
          extendDefaults($this);
          toggle($this, '');
        });
      }

    };

    if (methods[method]) {
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    } else if (typeof method === 'object' || !method) {
      return methods.init.apply(this, arguments);
    } else {
      $.error('Method ' + method + ' does not exist on jQuery.glossaryHighlighter');
    }
  };

})(jQuery);

