/**
 * Plugin Name: Glossarizer
 * Author : Vinay @Pebbleroad
 * Date: 02/04/2013
 * Description: Takes glossary terms from a JSON object -> Searches for terms in your html -> Wraps a abbr tag around the matched word
 * 1. Fixed IE8 bug where whitespace get removed - Had to change `abbr` tag to a block element `div`
 */

// // Expose plugin as an AMD module if AMD loader is present:
// if (typeof define === 'function' && define.amd) {
//   // AMD. Register as an anonymous module.
//   define(['jquery'], factory);
// } else 

(function (factory) {
  "use strict";
  if (typeof exports === 'object' && typeof require === 'function') {
      // Browserify
      factory(require('jquery'));
  } else {
      // Browser globals
      factory(jQuery);
  }
}(function ($) {
  'use strict';
/**
 * Defaults
 */

var pluginName = 'glossarizer',
  defaults = {
    sourceURL: '', /* URL of the JSON file with format {"term": "", "description": ""} */
    replaceTag: 'abbr', /* Matching words will be wrapped with abbr tags by default */
    lookupTagName: 'p, ul, a, div', /* Lookup in either paragraphs or lists. Do not replace in headings */
    callback: null, /* Callback once all tags are replaced: Call or tooltip or anything you like */
    replaceOnce: false, /* Replace only once in a TextNode */
    replaceClass: 'glossarizer_replaced',
    caseSensitive: false,
    exactMatch: false
}

/**
 * Constructor
 */

function Glossarizer (el, options) {
  var base = this

  base.el = el

  /* Element */
  base.$el = $(el)

  /* Extend options */

  base.options = $.extend({}, defaults, options)

  /* Terms */

  base.terms = []

  /* Excludes array */

  base.excludes = []

  /* Replaced words array */

  base.replaced = []

  /* Regex Tags */

  base.regexOption = (base.options.caseSensitive ? '' : 'i') + (base.options.replaceOnce ? '' : 'g')

  /* Fetch glossary JSON */

  $.getJSON(this.options.sourceURL).then(function (data) {
    base.glossary = data

    if (!base.glossary.length || base.glossary.length == 0) return

    /**
     * Get all terms
     */

    for (var i = 0; i < base.glossary.length; i++) {
      var terms = base.glossary[i].term.split(',')

      for (var j = 0; j < terms.length; j++) {
        /* Trim */

        var trimmed = terms[j].replace(/^\s+|\s+$/g, ''),
          isExclusion = trimmed.indexOf('!')

        if (isExclusion == -1 || isExclusion != 0) {
          /* Glossary terms array */

          base.terms.push(trimmed)
        } else {
          /* Excluded terms array */

          base.excludes.push(trimmed.substr(1))
        }
      }
    }

    /**
     * Wrap terms
     */

    base.wrapTerms()
  })
}

/**
 * Prototypes
 */
Glossarizer.prototype = {
  getDescription: function (term) {
    var regex = new RegExp('(\,|\s*)' + this.clean(term) + '\\s*|\\,$', 'i')

    /**
     * Matches
     * 1. Starts with \s* (zero or more spaces)
     * 2. Ends with zero or more spaces
     * 3. Ends with comma
     */

    for (var i = 0; i < this.glossary.length; i++) {
      if (this.options.exactMatch) {
        if (this.glossary[i].term == this.clean(term)) {
          return this.glossary[i].description.replace(/\"/gi, '&quot;')
        }
      } else {
        if (this.glossary[i].term.match(regex)) {
          return this.glossary[i].description.replace(/\"/gi, '&quot;')
        }
      }
    }
  },
  clean: function (text) {
    var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g')

    return text.replace(reEscape, '\\$1')
  },

  /**
   * Wraps the matched terms by calling traverser
   */
  wrapTerms: function () {
    this.cleanedTerms = this.clean(this.terms.join('|'))
    this.excludedTerms = this.clean(this.excludes.join('|'))

    var nodes = this.el.querySelectorAll(this.options.lookupTagName)

    for (var i = 0; i < nodes.length; i++) {
      this.traverser(nodes[i])
    }

    /**
     * Callback
     */

    if (this.options.callback) this.options.callback.call(this.$el)
  },

  /**
   * Traverses through nodes to find the matching terms in TEXTNODES
   */

  traverser: function (node) {
    var next,
      base = this

    if (node.nodeType === 1) {
      /*
       Element Node
       */

      if (node = node.firstChild) {
        do {
          // Recursively call traverseChildNodes
          // on each child node
          next = node.nextSibling

          /**
           * Check if the node is not glossarized
           */

          if (node.className != this.options.replaceClass) {
            this.traverser(node)
          }
        } while (node = next)
      }
    } else if (node.nodeType === 3) {
      /*
       Text Node
       */

      var temp = document.createElement('div'),
        data = node.data

      var re = new RegExp('(?:^|\\b)(' + this.cleanedTerms + ')(?!\\w)', base.regexOption),
        reEx = new RegExp('(?:^|\\b)(' + this.excludedTerms + ')(?!\\w)', base.regexOption)

      if (re.test(data)) {
        var excl = reEx.exec(data)

        data = data.replace(re, function (match, item , offset, string) {
          if (base.options.replaceOnce && inArrayIn(match, base.replaced) >= 0) {
            return match
          }

          base.replaced.push(match)

          var ir = new RegExp('(?:^|\\b)' + base.clean(match) + '(?!\\w)'),
            result = ir.exec(data)

          if (result) {
            if (excl && base.excludes.length) {
              var id = offset,
                exid = excl.index,
                exl = excl.index + excl[0].length

              if (exid <= id && id <= exl) {
                return match
              } else {
                return '<' + base.options.replaceTag + ' class="' + base.options.replaceClass + '" title="' + base.getDescription(match) + '">' + match + '</' + base.options.replaceTag + '>'
              }
            } else {
              return '<' + base.options.replaceTag + ' class="' + base.options.replaceClass + '" title="' + base.getDescription(match) + '">' + match + '</' + base.options.replaceTag + '>'
            }
          }
        })

        /**
         * Only replace when a match is found
         * Resorting to jQuery html() because of IE8 whitespace issue.
         * IE 8 removes leading whitespace from Text Nodes. Hence innerhtml doesnt work.
         *
         */

        $(temp).html(data)

        while (temp.firstChild) {
          node.parentNode.insertBefore(temp.firstChild, node)
        }

        node.parentNode.removeChild(node)
      }
    }
  },

}

/**
 * Public Methods
 */

var methods = {
  destroy: function () {
    this.$el.removeData('plugin_' + pluginName)

    /* Remove abbr tag */
    this.$el.find('.' + this.options.replaceClass).each(function () {
      var $this = $(this),
        text = $this.text()

      $this.replaceWith(text)
    })
  }
}

/**
 * Extend Prototype
 */

Glossarizer.prototype = $.extend({}, Glossarizer.prototype, methods)

/**
 * Plugin
 * @param  {[type]} options
 */
$.fn[pluginName] = function (options) {
  return this.each(function () {
    var $this = $(this),
      glossarizer = $this.data('plugin_' + pluginName)

    /*
    Check if its a method
     */

    if (typeof options == 'string' && glossarizer && methods.hasOwnProperty(options)) {
      glossarizer[options].apply(glossarizer)
    } else {
      /* Destroy if exists */

      if (glossarizer) glossarizer['destroy'].apply(glossarizer)

      /* Initialize */

      $.data(this, 'plugin_' + pluginName, new Glossarizer(this, options))
    }
  })
}

/**
 * In Array
 */

function inArrayIn (elem, arr, i) {
  if (typeof elem !== 'string') {
    return $.inArray.apply(this, arguments)
  }

  if (arr) {
    var len = arr.length
    i = i ? (i < 0 ? Math.max(0, len + i) : i) : 0
    elem = elem.toLowerCase()
    for (; i < len; i++) {
      if (i in arr && arr[i].toLowerCase() == elem) {
        return i
      }
    }
  }
  return -1
}
}));
