function _highlight()
{
  if (window.top.setTextFields) window.top.setTextFields();
  if (window.top.handleImages) window.top.handleImages();
  if (window.top.syncToc) window.top.syncTreeOnly();
  var words = _getWords();
  if (words.length == 0)
    return;

  var proximity = _getProximity (words);
  _processNode (document.body, proximity);
  if (proximity._postprocess)
    proximity._postprocess();
}

function _getWords ()
{
  var pretenders = _extractParameter ("words").split (",");
  var words = [];
  for (var i = 0; i < pretenders.length; ++i) {
    var word = pretenders[i];
    if (word && word != "")
      words.push (word);
  }
  return words;
}

function _getProximity (words)
{
  var proximityType = _extractParameter ("proximity").toLowerCase();
  if (proximityType == "phrase")
    return new PhraseProximity (words);
  else if (proximityType == "words")
    return new PhraseProximity (words, _extractParameter ("count"));
  else
    return new AnywhereProximity (words);
}

function _processNode (node, proximity)
{
  var children = node.childNodes;
  if (children.length == 0)
    return;

  var child     = node.firstChild;
  var nextChild = child ? child.nextSibling : null;
  while (child) {
    if (child.nodeName == "#text")
      proximity._process (child);
    else
      _processNode (child, proximity);

    child     = nextChild;
    nextChild = child ? child.nextSibling : null;
  }
}



function AnywhereProximity (words)
{
  this._words = words;

  var pattern = "";
  var len = words.length;
  for (var i = 0; i < len; i++) {
    pattern += _createWordPattern (words[i]);
    if (i < len - 1)
      pattern += "|";
  }
  this._re = new RegExp (pattern, "i");

  this._templates = _createTemplates ([
    "#FFFF66", "#A0FFFF", "#99FF99", "#FF9999",
    "#FF66FF", "#008AFF", "#FF9000", "#18A900"
  ]);

  this._process       = AnywhereProximity__process;
  this._splitTextNode = AnywhereProximity__splitTextNode;
  return this;
}

function AnywhereProximity__process (textNode)
{
  var re = this._re;
  var node = textNode;
  while (node) {
    var value = node.nodeValue;
    var occurrence = re.exec (value);
    if (!occurrence)
      return;

    node = this._splitTextNode (node, occurrence);
  }
}

function AnywhereProximity__splitTextNode (textNode, occurrence)
{
  var word = occurrence[0].toLowerCase();
  var words = this._words;
  var l = words.length;
  var templates = this._templates;
  var template = templates[0];
  for (var i = 0; i < l; ++i) {
    if (word == words[i]) {
      template = templates[i % templates.length];
      break;
    }
  }

  var wordNode   = textNode.splitText (occurrence.index);
  var remainNode = wordNode.splitText (word.length);

  var highlightNode = template.cloneNode (false);
  wordNode = textNode.parentNode.replaceChild (highlightNode, wordNode);
  highlightNode.appendChild (wordNode);

  return remainNode;
}



function PhraseProximity (words, tolerance)
{
  if (!tolerance)
    tolerance = 0;

  this._tolerance = tolerance;
  this._regExps   = _createRegExps (words);
  this._matches   = new LinkedList();
  this._results   = [];

  this._templates = _createTemplates ([
    "#FFFF66", "#A0FFFF", "#99FF99", "#FF9999",
    "#FF66FF", "#008AFF", "#FF9000", "#18A900"
  ]);

  this._process     = PhraseProximity__process;
  this._postprocess = PhraseProximity__postprocess;
  return this;
}

function PhraseProximity__process (textNode)
{
  var matches = this._matches;
  var results = this._results;
  for (var i = matches._iterator(); i._hasNext();) {
    var m = i._next();
    if (!m._matchWords (textNode, 0))
      i._remove();
    else if (m._isComplited()) {
      results.push (m);
      i._remove();
    }
  }

  var regExps   = this._regExps;
  var tolerance = this._tolerance;
  var offset = 0;
  var re = regExps[0];
  var value = textNode.nodeValue;
  var occurrence = re.exec (value);
  while (occurrence) {
    var index = occurrence.index;
    var m = new Match (regExps, tolerance);
    if (m._matchWords (textNode, offset + index)) {
      if (m._isComplited())
        results.push (m);
      else
        matches._add (m);
    }
    offset += index + occurrence[0].length;
    value = value.substr (index + occurrence[0].length);
    occurrence = re.exec (value);
  }
}

function PhraseProximity__postprocess ()
{
  if (this._results.length == 0)
    return;

  var templates = this._templates;
  var results = this._results;
  var len = results.length;
  for (var i = len - 1; i >= 0; --i)
    results[i]._highlight (templates[i % templates.length]);
}


function Match (regExps, tolerance)
{
  this._regExps   = regExps;
  this._tolerance = tolerance;

  this._cursor = 0;
  this._nodes  = new LinkedList();

  this._matchWords  = Match__matchWords;
  this._isComplited = Match__isComplited;
  this._highlight   = Match__highlight;
  return this;
}

function Match__isComplited ()
{
  return this._cursor == this._regExps.length;
}

function Match__matchWords (textNode, offset)
{
  var regExps = this._regExps;
  var len = regExps.length;
  var isAdded = false;
  var value = textNode.nodeValue.substr (offset);
  while (this._cursor < len) {
    var occurrence = regExps[this._cursor].exec (value);
    if (!occurrence) {
      this._tolerance -= _countWords (value);
      return this._tolerance >= 0;
    }

    if (!isAdded) {
      this._nodes._add (new Source (textNode, offset));
      isAdded = true;
    }
    if (this._cursor != 0)
      this._tolerance -= _countWords (value.substr (0, occurrence.index));
    if (this._tolerance < 0)
      return false;
    value = value.substr (occurrence.index + occurrence[0].length);
    ++this._cursor;
  }
  return true;
}

function Match__highlight (template)
{
  var regExps = this._regExps;
  var len = regExps.length;
  var sources = this._nodes._iterator();
  var source = sources._next();
  for (var i = 0; i < len; ++i) {
    while (source && !source._highlight (regExps[i], template))
      source = sources._next();
  }
}


function Source (textNode, pos)
{
  this._pos   = pos;
  this._start = textNode;
  this._stop  = textNode.nextSibling;

  this._highlight         = Source__highlight;
  this._findRealStartNode = Source__findRealStartNode;

  return this;
}

function Source__highlight (re, template)
{
  this._findRealStartNode();
  while (this._start && this._start != this._stop) {
    if (this._start && this._start.nodeName != "#text")
      return false;

    var value = this._start.nodeValue.substr (this._pos);
    var occurrence = re.exec (value);
    if (!occurrence) {
      this._pos = 0;
      this._start = this._start.nextSibling;
      if (this._start && this._start != this._stop &&
          this._start.nodeName != "#text")
      {
        this._start = this._start.nextSibling;
      }
    }
    else {
      var wordNode   = this._start.splitText (this._pos + occurrence.index);
      var remainNode = wordNode.splitText (occurrence[0].length);

      var highlightNode = template.cloneNode (false);
      wordNode = this._start.parentNode.replaceChild (highlightNode, wordNode);
      highlightNode.appendChild (wordNode);

      this._pos   = 0;
      this._start = remainNode;
      return true;
    }
  }
  return false;
}

function Source__findRealStartNode ()
{
  var pos  = this._pos;
  var node = this._start;
  var len  = node.nodeValue.length;
  while (pos >= len) {
    pos -= len;
    node = node.nextSibling;
    if (node) {
      if (node.nodeName != "#text")
        len = node.firstChild.nodeValue.length;
      else
        len = node.nodeValue.length;
    }
    else
      break;
  }
  this._pos   = pos;
  this._start = node;
}


function LinkedList ()
{
  this._size = 0;
  this._root = new LinkedList_Node (null);

  this._clear    = LinkedList__clear;
  this._add      = LinkedList__add;
  this._iterator = LinkedList__iterator;

  this._clear();
  return this;
}

function LinkedList__add (o)
{
  var newNode = new LinkedList_Node (o);
  var root = this._root;
  root._prev._next = newNode;
  newNode._prev = root._prev;
  newNode._next = root;
  root._prev = newNode;
  ++this._size;
}

function LinkedList__iterator ()
{
  return new LinkedList_Iterator (this._root);
}

function LinkedList__clear ()
{
  var root = this._root;
  root._next = root._prev = root;
  this._size = 0;
}

function LinkedList_Node (o)
{
  this._o = o;
  this._prev = null;
  this._next = null;
  return this;
}

function LinkedList_Iterator (root)
{
  this._current = this._root = root;

  this._hasNext = LinkedList_Iterator__hasNext;
  this._next    = LinkedList_Iterator__next;
  this._remove  = LinkedList_Iterator__remove;

  return this;
}

function LinkedList_Iterator__hasNext ()
{
  return this._current._next != this._root;
}

function LinkedList_Iterator__next ()
{
  this._current = this._current._next;
  return this._current._o;
}

function LinkedList_Iterator__remove ()
{
  this._current._prev._next = this._current._next;
  this._current._next._prev = this._current._prev;
  --this._size;
}



function _countWords (s)
{
  var re = /(\b\w+\b)|([\u3040-\u30FF,\u4E00-\u9FFF])/g;
  var occurrences = s.match (re);
  if (!occurrences)
    return 0;
  else
    return occurrences.length;
}

function _createRegExps (words)
{
  var regExps = [];
  for (var i = 0; i < words.length; ++i)
    regExps.push (new RegExp (_createWordPattern (words[i]), "i"));
  return regExps;
}

function _createWordPattern (word)
{
  var reJapanese = /[\u3040-\u30FF,\u4E00-\u9FFF]/g;
  var occurrences = word.match (reJapanese);
  if (!occurrences)
    return "\\b" + word + "\\b";
  else
    return word;
}

function _createTemplates (colors)
{
  var template = document.createElement ("b");
  var templates = [];
  for (var i = 0; i < colors.length; ++i) {
    var t = template.cloneNode (false);
    t.style.background = colors[i];
    templates.push (t);
  }
  return templates;
}

function _extractParameter (paramName)
{
  var loc = window.location.search;
  var start = loc.indexOf ('?' + paramName +'=');
  if (start == -1)
    start = loc.indexOf ('&' + paramName +'=')

  if (start == -1)
    return "";

  start = start + paramName.length + 2;
  var end = loc.indexOf ('&', start);
  if (end == -1)
    end = loc.length;

  return loc.slice (start, end);
}
