// Globally visible bindings
var key_handler, toggle_panel, hide_prefs, new_query, refine_query,
    set_ctx_query, set_context_query, set_show_manuals, set_show_manual_titles,
    set_results_num, set_type_delay, set_highlight_color, status_line,
    saved_status = false, ctx_query_label_line, language_family;

var descriptions = new Array();

(function(){

// Configuration options (use || in case a cookie exists but is empty)
var ctx_query          = GetCookie("PLT_ContextQuery","");
var ctx_query_label    = GetCookie("PLT_ContextQueryLabel",""); // no prefs UI
var manual_settings    = parseInt(GetCookie("PLT_ManualSettings",1));
var show_manuals       = manual_settings % 10;
var show_manual_titles = ((manual_settings - show_manuals) / 10) > 0;
var results_num        = (parseInt(GetCookie("PLT_ResultsNum",false)) || 20);
var type_delay         = (parseInt(GetCookie("PLT_TypeDelay",false)) || 150);
var highlight_color    = (GetCookie("PLT_HighlightColor",false) || "#ffd");
var background_color   = "#f8f8f8";

var query, results_container, result_links;
var prev_page_link1, prev_page_link2, next_page_link1, next_page_link2;

// tabIndex fields are set:
//   1 query
//   2 index links
//   3 help/pref toggle
//   4 pref widgets
//   5 clear current context-query
//  -1 prev/next page (un-tab-able)

function MakePref(label, input) {
  return '<tr><td align="right" valign="baseline">' + label + ':&nbsp;&nbsp;</td>'
            +'<td valign="baseline">' + input + '</td></tr>';
}

function PrefInputArgs(name, desc) {
  // don't plant `desc' directly in the text -- it might have quotes
  descriptions[name] = desc;
  return 'tabIndex="4" id="'+name+'_pref"'
       +' onkeypress="hide_prefs(event);"'
       +' onchange="set_'+name+'(this); return true;"'
       +' onfocus="saved_status=status_line.innerHTML;'
                 +'status_line.innerHTML=descriptions[\''+name+'\'];"'
      +' onblur="status_line.innerHTML=(saved_status || \'&nbsp;\');"';
}

function MakeChevrons(num, middle) {
  return '<div style="text-align: center; clear:both">'
          +'<a href="#" title="Previous Page" id="prev_page_link'+num+'"'
            +' tabIndex="-1"'
            +' style="float: left; text-decoration: none; font-weight: bold;'
                   +' font-family: monospace;"'
            +' onclick="key_handler(\'PgUp\'); return false;"'
            +'>'+MakePageIcon("prev", "&lt;&lt;")+'</a>'
          +'<a href="#" title="Next Page" id="next_page_link'+num+'" '
            +'tabIndex="-1"'
            +' style="float: right; text-decoration: none; font-weight: bold;'
                   +' font-family: monospace;"'
            +' onclick="key_handler(\'PgDn\'); return false;"'
            +'>'+MakePageIcon("next", "&gt;&gt;")+'</a>'
          +middle
        +'</div>';
}

function MakeContextQueryItem(qry, desc) {
  return '<li><a href="?hq='+encodeURIComponent(qry)+'" tabIndex="4"'
              +' title="set this as your context-query"'
              +' style="text-decoration: none;"'
              +' onclick="return new_query(this,\''
                                   +encodeURIComponent(desc)
                                   +'\');">'
              + desc.replace(/{{/g, "<code><b>").replace(/}}/g, "</b></code>")
           + '</a></li>';
}

function MakeLanguageFamilySuggestions() {
    var all_families = false;

    if (plt_language_families.length == 1) {
        return "";
    }
    accum = ""

    if (all_families) {
        for (i = 0; i < plt_language_families.length; i++) {
            accum += MakeContextQueryItem("F:" + plt_language_families[i],
                                          plt_language_families[i] + " language family");
        }
    } else {
        accum += MakeContextQueryItem("F:" + language_family,
                                      language_family + " language family");
        if (language_family != plt_main_language_family) {
            accum += MakeContextQueryItem("F:" + plt_main_language_family,
                                          plt_main_language_family + " language family");
        }
    }
    return accum;
}

function MakeSVGRef(img, padding) {
    if (!padding) padding = "4pt"
    return '<svg style="padding: '+padding+'; padding-bottom: 0pt; margin-bottom: -2pt" width="12pt" height="12pt" viewBox="0 0 17 17"><use href="#'+img+'"></svg>';
}

function MakeIcon(img,label,title,action) {
  if (img) {
      label = MakeSVGRef(img);
  }
  return '<a href="#" title="'+title+'" tabIndex="3"'
          +' style="text-decoration: none; color: black;'
                 +' font-weight: bold; font-family: monospace;"'
          +' onclick="'+action+'; return false;"'
          +'>'+label+'</a>';
}

function MakePageIcon(img,label) {
    return MakeSVGRef(img);
}

function InitializeSearch() {
  var n;
  language_family = GetPageArg("fam",false);
  if (!language_family) language_family = plt_main_language_family
  n = document.getElementById("plt_search_container");
  // hack the dom widgets in
  var panelbgcolor = "background-color: #f0f0f0;";
  var panelstyle =
    'style="display: none; border: 1px solid #222; border-top: 0px;'
         +' font-family: arial, sans-serif; margin: 0em 0em 1em 0em;'
         +' padding: 0.5em; '+panelbgcolor+'"';
  n.innerHTML =
    '<div style="width: 100%; margin: 0em; padding: 0em;'
              +' background-color: '+background_color+';">'
      +'<div style="'+panelbgcolor+'">'
        +'<div style="float: right; ">'
          +MakeIcon("help", "[?]", "help",        "toggle_panel(\'help\')")
          +MakeIcon("settings", "[!]", "preferences", "toggle_panel(\'prefs\')")
        +'</div>'
        +'<input type="text" id="search_box" style="width: 90%;"'
              +' tabIndex="1" onkeydown="return key_handler(event);"'
              +' onkeydown="return key_handler(null);" />'
      +'</div>'
      +'<div id="close_panel"'
          +' style="display: none; float: right;'
                 +' margin: 0.2em 0.5em; '+panelbgcolor+'">'
        +MakeIcon(false, "&#10005;", "close", "toggle_panel(false)")
      +'</div>'
      +'<div id="help_panel" '+panelstyle+'>'
        +'<ul style="padding: 0em; margin: 0.5em 1.5em;">'
        +'<li>A search matches when all space-separated terms and modifiers match.</li>'
        +'<li>Use'
           +' &ldquo;<kbd>N:<i>str</i></kbd>&rdquo; to negate a term.'
        +'<li>Use &ldquo;<kbd>F:<i>str</i></kbd>&rdquo; to match only'
           +' entries for a language family that exactly matches'
           +' &ldquo;<kbd><i>str</i></kbd>&rdquo;.</li>'
        +'<li>Use &ldquo;<kbd>M:<i>str</i></kbd>&rdquo; to match only'
           +' identifiers from modules that (partially) match'
           +' &ldquo;<kbd><i>str</i></kbd>&rdquo;; &ldquo;<kbd>M:</kbd>&rdquo; by'
           +' itself will restrict results to bound names only.</li>'
        +'<li>Use &ldquo;<kbd>H:<i>str</i></kbd>&rdquo; to match only'
           +' modules that implement a language'
           +' &ldquo;<kbd>#lang <i>str</i></kbd>&rdquo;.</li>'
        +'<li>Use &ldquo;<kbd>R:<i>str</i></kbd>&rdquo; to match only'
           +' modules that implement a reader module'
           +' &ldquo;<kbd>#reader <i>str</i></kbd>&rdquo;.</li>'
        +'<li>&ldquo;<kbd>L:<i>str</i></kbd>&rdquo; is similar to'
           +' &ldquo;<kbd>M:<i>str</i></kbd>&rdquo;, but'
           +' &ldquo;<kbd><i>str</i></kbd>&rdquo; should match the module name'
           +' exactly; &ldquo;<kbd>L:</kbd>&rdquo; by'
           +' itself will restrict results to module names only.</li>'
        +'<li>&ldquo;<kbd>K:<i>str</i></kbd>&rdquo; restricts results to ones who'
           +' kind is &ldquo;<kbd><i>str</i></kbd>&rdquo;. Use double quotes'
           +' around &ldquo;<kbd><i>str</i></kbd>&rdquo; to include spaces.</li>'
        +'<li>&ldquo;<kbd>T:<i>str</i></kbd>&rdquo; restricts results to ones in'
           +' the &ldquo;<kbd><i>str</i></kbd>&rdquo; manual (naming the'
           +' directory where the manual is found).</li>'
        +'<li>Entries that correspond to bindings have module links that'
           +' create a query restricted to bindings in that module (using'
           +' &ldquo;<kbd>L:</kbd>&rdquo;), other entries have similar links for'
           +' restricting results to a specific manual (using'
           +' &ldquo;<kbd>T:</kbd>&rdquo;); you can control whether manual links'
           +' appear (and how) in the preferences.</li>'
        +'<li>Right-clicking these links refines the current query instead of'
           +' changing it (but some browsers don\'t support this).</li>'
        +'</ul>'
        +'Hit <kbd>PageUp</kbd>/<kbd>PageDown</kbd> or'
           +' <kbd>Ctrl</kbd>+<kbd>Enter</kbd> / <kbd>Shift</kbd>+<kbd>Ctrl</kbd>+<kbd>Enter</kbd>'
           +' to scroll through the results.'
      +'</div>'
      +'<div id="prefs_panel" '+panelstyle+'>'
        +'<table align="center" style="margin: 0em 2em;">'
        + MakePref('Show manuals',
           '<select '
                   +PrefInputArgs("show_manuals",
                                  "Controls when manual links are shown")
                   +'>'
             +'<option>never</option>'
             +'<option>except identifiers</option>'
             +'<option>always</option>'
             +'</select>&nbsp;&nbsp;'
           +'<input type="checkbox" '
                   +PrefInputArgs("show_manual_titles",
                                  "Controls how manual links are shown")
                   +'>'
           +' use titles')
        + MakePref('Results per page',
           '<input type="text" '
                   +PrefInputArgs("results_num",
                                  "How many results to show on the page")
                   +'>')
        + MakePref('Type delay',
           '<input type="text" '
                   +PrefInputArgs("type_delay",
                                  "The delay to wait (in msec) before search"
                                  +" results are updated")
                   +'>')
        + MakePref('Exact matches color',
           '<input type="text" '
                   +PrefInputArgs("highlight_color",
                                  "The color to use for highlighting exact"
                                  +" matches (a known color name or #RGB)")
                   +'>')
        + MakePref('Query prefix',
           '<input type="text" '
                   +PrefInputArgs("ctx_query",
                     "A &ldquo;context&rdquo; query that is implicitly added"
                     +" to all searches")
                   +'>&nbsp;'
           +'<a style="font-size: 82%; color: #444; text-decoration: none;"'
             +' href="#" onclick="toggle_panel(\'contexts\'); return false;">'
            +'[more]</a>')
        +'</table>'
      +'</div>'
      +'<div id="contexts_panel" '+panelstyle+'>'
        +'<table align="center" style="margin: auto 2em; padding-bottom: 2em">'
          + MakePref('Query prefix',
             '<input type="text" '
                     +PrefInputArgs("context_query",
                       "A &ldquo;context&rdquo; query that is implicitly added"
                       +" to all searches")
                     +'>' + MakeIcon("help", "[?]", "help", "toggle_panel(\'help\')"))
        +'</table>'
        +'The following links set your query prefix to a'
        +' common choice:'
        +'<ul style="padding: 0em; margin: 0.5em 1.5em;">'
        +MakeLanguageFamilySuggestions()
        +MakeContextQueryItem("M:", "binding names")
        +MakeContextQueryItem("T:reference", "Racket Reference manual")
        +MakeContextQueryItem("M:racket", "{{racket}} bindings")
        +MakeContextQueryItem("M:racket/base", "{{racket/base}} bindings")
        +MakeContextQueryItem("H:", "language names")
        +'</ul>'
        +'</div>'
        +'<div style="text-align: center; padding: 2pt"><span id="ctx_query_label" style="color: #444;">&nbsp;</span></div>'
      +MakeChevrons(1,
        '<span id="search_status"'
            +' class="ssansserif search-status"'
            +'>&nbsp;</span>')
      +'<div id="search_result_container"></div>'
      +MakeChevrons(2, '&nbsp;')
    +'</div>';
  // get the widgets we use
  query = document.getElementById("search_box");
  status_line = document.getElementById("search_status");
  ctx_query_label_line = document.getElementById("ctx_query_label");
  prev_page_link1 = document.getElementById("prev_page_link1");
  prev_page_link2 = document.getElementById("prev_page_link2");
  next_page_link1 = document.getElementById("next_page_link1");
  next_page_link2 = document.getElementById("next_page_link2");
  // result_links is the array of result link <container,link> pairs
  result_links = new Array();
  var proto_search_result = makeProtoSearchResult();
  results_container = document.getElementById('search_result_container');
  results_container.appendChild(proto_search_result);
  results_container.normalize();
  result_links.push(proto_search_result);
  AdjustResultsNum();
  // get search string
  var init_q = GetPageArg("q",false);
  if (init_q && init_q != "") query.value = init_q;
  AdjustMainLink(GetPageArg("famroot",false));
  ContextFilter();
  DoSearch();
  query.focus();
  query.select();
}

function AdjustMainLink(famroot) {
    if (!famroot) return;
    var s = document.getElementById("start-link");
    if (!s) return;
    var c = s.firstChild
    if (!c) return;
    if (c.innerHTML == "Racket Documentation") { // sanity check
        c.innerHTML = language_family + " Documentation"
    }
}

function makeProtoSearchResult() {
  var proto_search_result = document.createElement('div');
  proto_search_result.classList.add('search-result-wrapper');
  return proto_search_result;
}

function AdjustResultsNum() {
  if (result_links.length == results_num) return;
  if (results_num <= 0) results_num = 1; // expects at least one template
  while (result_links.length > results_num)
    results_container.removeChild(result_links.pop());
  while (result_links.length < results_num) {
    var n = result_links[0].cloneNode(true);
    result_links.push(n);
    results_container.appendChild(n);
  }
}

// Terms are compared using Compare(pat,str), which returns one of several
// constants, in increasing order of success:
// - C_fail: no match
// - C_words1: all of the "subwords" in pat matched, where a word is an
//   alphanumeric or a punctuation substring (for example, "foo-bar!!" has
//   these words: "foo", "-", "bar", "!!"), and a match means a prefix of a
//   subword in str matched one of pat's subwords
// - C_words2: same, but all of the subwords in str were matched (so both str
//   and pat have the same number of subwords)
// - C_words3: same, but all matches were complete (so str is a permutation of
//   pat)
// - C_match: pat matched somewhere in str
// - C_prefix: pat is a prefix of str
// - C_exact: pat is equal to str
// - C_rexact: there was a ("real") exact match, see below
// Note that the value is searched from the last one and going back, so, for
// example, if pat is a prefix of str the result is C_prefix, and no subword
// matching is attempted.  A result of C_exact can be returned by some of the
// `X:' operators: its purpose is to be able to return a result that doesn't
// affect the exactness of the search (for example L:foo searches for a source
// module that is exactly `foo', but it will return C_exact which means that it
// doesn't change whether the match is considered exact or not).
var C_fail   = 0, C_min = 0,
    C_words1 = 1,
    C_words2 = 2,
    C_words3 = 3,
    C_match  = 4,
    C_prefix = 5,
    C_exact  = 6,
    C_rexact = 7, C_max = 7;

function Compare(pat, str) {
  var i = str.indexOf(pat);
  if (i < 0) return C_fail;
  if (i > 0) return C_match;
  if (pat.length == str.length) return C_rexact;
  return C_prefix;
}
function MaxCompares(pat, strs) {
  var r = C_min, c;
  for (var i=0; i<strs.length; i++) {
    c = Compare(pat,strs[i]);
    if (c > r) { if (c >= C_max) return c; else r = c; }
  }
  return r;
}

// Utilities for dealing with "subword" searches
function SortByLength(x, y) {
  // use this to sort with descending length and otherwise lexicographically
  var xlen = x.length, ylen = y.length;
  if (xlen != ylen) return ylen - xlen;
  else if (x < y) return -1;
  else if (x > y) return +1;
  else return 0;
}
function CompileWordCompare(term) {
  var words = term.split(/\b/).sort(SortByLength);
  var word_num = words.length;
  var is_word = new Array(word_num);
  for (var i=0; i<word_num; i++) is_word[i] = (words[i].search(/\w/) >= 0);
  return function (str) {
    var str_words = str.split(/\b/).sort(SortByLength);
    var str_word_num = str_words.length;
    if (str_word_num < word_num) return C_fail; // can't work
    var r = C_words3;
    for (var i=0; i<word_num; i++) {
      var word = words[i];
      var found = -1;
      for (var j=0; j<str_word_num; j++) {
        var str_word = str_words[j];
        if (str_word != null) {
          if (word == str_word) {
            found = j;
            break;
          } else {
            var k = str_word.indexOf(word);
            if (k == 0 || (k > 0 && !is_word[i])) {
              found = j;
              r = C_words2;
              break;
            }
          }
        }
      }
      if (found >= 0) str_words[found] = null;
      else return C_fail;
    }
    // either C_words3 for a complete permutation, or C_words2 when all of the
    // subwords in str were matched
    if (word_num == str_word_num) return r;
    // otherwise return C_words1 regardless of whether all of the words in term
    // matched exactly words in str
    else return C_words1;
  };
}
// Tests:
// console.log("testing...");
// function t(x,y,e) {
//   var r = CompileWordCompare(x)(y);
//   if (e != r) {
//     console.log('CompileWordCompare("'+x+'")("'+y+'")'+
//                 " => "+r+" but expected "+e);
//   }
// }
// t("x-y","x-y",C_words3);
// t("x-y","y-x",C_words3);
// t("x-y","y--x",C_words2);
// t("x-y","y=-x",C_words2);
// t("xx-yy","x-y",C_fail);
// t("xx-yy","xxx-yyy",C_words2);
// t("x-y-x","xxx-yyy",C_fail);
// t("x-y-x","xxx-yyy-xxx",C_words2);
// t("x-y-x","yyy-xxx-xxx",C_words2);
// t("x-y-xx","xx-y-xxx",C_words2);
// console.log("done");

function NormalizeSpaces(str) {
  // use single spaces first, then trim edge spaces
  return str.replace(/\s\s*/g," ").replace(/^ /,"").replace(/ $/,"");
}

function SanitizeHTML(str) {
  // Minimal protection against bad html strings
  // HACK: we don't want to actually parse these things, but we do want a way
  // to have tt text, so use a "{{...}}" markup for that.
  return str.replace(/[&]/g, "&amp;")
            .replace(/>/g,   "&gt;")
            .replace(/</g,   "&lt;")
            .replace(/\"/g,  "&quot;")
            .replace(/{{/g,  "<code><b>")
            .replace(/}}/g,  "</b></code>");
}

function UrlToManual(url) {
  return url.replace(/#.*$/, "")       // remove fragment,
            .replace(/\?.*$/, "")      // query,
            .replace(/\/[^\/]*$/, "")  // filename,
            .replace(/^(.*\/|>)/, ""); // and directory.
}


// Index array entry constants:
const IDX_KEY = 0;
const IDX_URL = 1;
const IDX_KEY_HTML = 2;
const IDX_LIBS_SEXP = 3;
const IDX_PACKAGE = 4;
const IDX_SORT_ORDER = 5;
const IDX_LANG_FAMILY = 6;
const IDX_LIBS_HTML = 7;
const IDX_LIBS_TEXT = 8;
const IDX_KIND = 9;
const IDX_LONG_KEY = 10;

function CompareKey(term, x){
    var c = Compare(term, x[IDX_KEY]);
    if (x[IDX_LONG_KEY]) {
        var c2 = Compare(term, x[IDX_LONG_KEY]);
        if (c2 > c) return c2;
    }
    return c;
}
    
// Tests for matches and highlights:
//   "append"
//   "L:racket append"
//   "L:racket" (no exact matches except for the `racket' module)
//   "L:racke" (only module names that match `racke')
//   "edi" and "edit" (the top few should be the same)

// Additional "hidden" operators:
//   "A:{ foo bar }" -- an `and' query
//   "O:{ foo bar }" -- an `or' query
//   "Q:foo" -- stands for just "foo", useful for quoting Q:} inside the above
// Note: they're "hidden" because the syntax might change, and it's intended
// mostly for context queries.

function CompileTerm(term) {
  var op = ((term.search(/^[NFLMHRTQK]:/) == 0) && term.substring(0,1));
  if (op) term = term.substring(2);
  term = term.toLowerCase();
  switch (op) {
  case "N":
    op = CompileTerm(term);
    // return C_exact if it's not found, so it doesn't disqualify exact matches
    return function(x) { return (op(x) >= C_match) ? C_fail : C_exact; };
  case "F":
    return function(x) {
        fams = x[IDX_LANG_FAMILY].map((x) => x.toLowerCase())
        if (!fams) return C_fail;
        return (MaxCompares(term,fams) >= C_exact) ? C_exact : C_fail;
    };
  case "L":
    return function(x) {
      if (!x[IDX_LIBS_SEXP]) return C_fail;
      if (x[IDX_LIBS_SEXP] == "module" || x[IDX_LIBS_SEXP] == "language" || x[IDX_LIBS_SEXP] == "reader") // rexact allowed, show partial module matches
        return CompareKey(term, x);
      return (MaxCompares(term,x[IDX_LIBS_SEXP]) >= C_exact) ? C_exact : C_fail;
    };
  case "M":
    return function(x) {
      if (!x[IDX_LIBS_SEXP]) return C_fail;
      if (x[IDX_LIBS_SEXP] == "module" || x[IDX_LIBS_SEXP] == "language" || x[IDX_LIBS_SEXP] == "reader") return CompareKey(term, x); // rexact allowed
      return (MaxCompares(term,x[IDX_LIBS_TEXT]?x[IDX_LIBS_TEXT]:x[IDX_LIBS_SEXP]) >= C_match) ? C_exact : C_fail;
    };
  case "H":
    return function(x) {
      if (!x[IDX_LIBS_SEXP]) return C_fail;
      if (x[IDX_LIBS_SEXP] == "language") return CompareKey(term, x);
      return (MaxCompares(term,x[IDX_LIBS_TEXT]?x[IDX_LIBS_TEXT]:x[IDX_LIBS_SEXP]) >= C_exact) ? C_exact : C_fail;
    };
  case "R":
    return function(x) {
      if (!x[IDX_LIBS_SEXP]) return C_fail;
      if (x[IDX_LIBS_SEXP] == "reader") return CompareKey(term, x);
      return (MaxCompares(term,x[IDX_LIBS_TEXT]?x[IDX_LIBS_TEXT]:x[IDX_LIBS_SEXP]) >= C_exact) ? C_exact : C_fail;
    };
  case "T":
    return function(x) {
      if (Compare(term,UrlToManual(x[IDX_URL])) < C_exact) return C_fail;
      else if (x[IDX_URL].search(/\/index\.html$/) > 0) return C_rexact;
      else return C_exact;
    };
  case "K":
    return function(x) {
      if (!x[IDX_KIND]) return C_fail;
      return Compare(term,x[IDX_KIND]);
    };
  /* a case for "Q" is not needed -- same as the default case below */
  default:
    var compare_words = CompileWordCompare(term);
    return CompileOrTerms([
      function(x) {
        var r = CompareKey(term, x);
        // only bindings can be used for rexact matches
        if (r >= C_rexact) return (x[IDX_LIBS_SEXP] ? r : C_exact);
        if (r > C_words3) return r;
        else return compare_words(x[IDX_KEY]);
      },
      function(x) {
        if (x[IDX_URL].search(/\/index\.html$/) > 0) {
          return Compare(term,UrlToManual(x[IDX_URL]));
        } else {
          return C_fail;
        }
      }
    ]);
  }
}

function CompileAndTerms(preds) {
  return function(x) {
    var r = C_max, c;
    for (var i=0; i<preds.length; i++) {
      c = preds[i](x);
      if (c < r) { if (c <= C_min) return c; else r = c; }
    }
    return r;
  };
}

function CompileOrTerms(preds) {
  return function(x) {
    var r = C_min, c;
    for (var i=0; i<preds.length; i++) {
      c = preds[i](x);
      if (c > r) { if (c >= C_max) return c; else r = c; }
    }
    return r;
  };
}

function CompileTermsR(terms, nested) {
  var term, result = new Array();
  while (terms.length > 0) {
    term = terms.pop();
    switch (term) {
    case "A:{": result.push(CompileTermsR(terms, CompileAndTerms)); break;
    case "O:{": result.push(CompileTermsR(terms, CompileOrTerms));  break;
    default:
      // "}" has terminates a compound, otherwise it's an ordinary search term
      if (nested && (term == "}")) return nested(result);
      else result.push(CompileTerm(term));
    }
  }
  // all compound operators are implicitly terminated at the end
  if (nested) return nested(result);
  else return result;
}

function CompileTerms(terms, nested) {
  terms.reverse();
  return CompileTermsR(terms, nested);
}

function Id(x) {
  return x;
}

var indicators =
  (function() {
    // construct indicator lines that look like: "--->..."
    var i, j, s, a = new Array();
    for (i=0; i<11; i++) {
      s = "";
      for (j=0; j<10; j++) {
        s += (j==9 && i==10) ? "&#9632;"
           : (j<i)  ? "&mdash;"
           : (j==i) ? "&#9658;"
           : "&middot;";
      }
      a.push("&nbsp;<code>"+s+"</code>");
    }
    return a;
   }());
function MakeShowProgress() {
  var orig = status_line.innerHTML;
  return function(n) {
    status_line.innerHTML =
      orig + indicators[Math.round(10*n/search_data.length)];
  };
}

function packageAndOrderCompare(a, b) {
  var a_is_lang = (a[IDX_LANG_FAMILY].indexOf(language_family) >= 0);
  var b_is_lang = (b[IDX_LANG_FAMILY].indexOf(language_family) >= 0);
  if (a_is_lang != b_is_lang) {
      if (a_is_lang) return -1;
      if (b_is_lang) return 1;
  }

  var a_is_base = plt_base_pkgs.indexOf(a[IDX_PACKAGE]) >= 0;
  var b_is_base = plt_base_pkgs.indexOf(b[IDX_PACKAGE]) >= 0;
  if (a_is_base && b_is_base) return 0;
  if (a_is_base) return -1;
  if (b_is_base) return 1;

  var a_in_main = plt_main_dist_pkgs.indexOf(a[IDX_PACKAGE]) >= 0;
  var b_in_main = plt_main_dist_pkgs.indexOf(b[IDX_PACKAGE]) >= 0;
  if (a_in_main && b_in_main) return 0;
  if (a_in_main) return -1;
  if (b_in_main) return 1;

  // Same name: sort using `sort-order`
  if (a[IDX_KEY] == b[IDX_KEY]) {
      if (a[IDX_SORT_ORDER] < b[IDX_SORT_ORDER]) return -1;
      if (b[IDX_SORT_ORDER] < a[IDX_SORT_ORDER]) return 1;
  }

  return 0;
}

// intended to refine an existing sort, relying on a stable stort
function languageFamilyCompare(a, b) {
  if (a[0] == (C_max - C_rexact) || b[0] == (C_max - C_rexact)) {
    return a[0] - b[0];
  }
  var a_is_lang = (a[1][IDX_LANG_FAMILY].indexOf(language_family) >= 0);
  var b_is_lang = (b[1][IDX_LANG_FAMILY].indexOf(language_family) >= 0);
  if (a_is_lang != b_is_lang) {
      if (a_is_lang) return -1;
      if (b_is_lang) return 1;
  }
  return 0;
}

function splitWithQuotes(input) {
  return input.match(/(?:[^\s"]|"[^"]*")+/g).map(token =>
    token.replace(/"([^"]*)"/g, "$1")
  );
}

function Search(data, term, is_pre, K) {
  // `K' is a continuation if this run is supposed to happen in a "thread"
  // false otherwise
  var t = false;
  function Killer() { if (t) clearTimeout(t); };
  // term comes with normalized spaces (trimmed, and no double spaces)
  var preds = (term=="") ? [] : CompileTerms(splitWithQuotes(term), false);
  if (preds.length == 0) {
    var ret = is_pre ? [0,data] : [0,[]];
    if (K) { K(ret); return Killer; }
    else return ret;
  }
  var i = 0;
  var matches = new Array(C_max-C_min);
  for (i=0; i<matches.length; i++) matches[i] = new Array();
  var chunk_fuel = K ? Math.ceil(data.length/10) : data.length;
  var progress = K ? MakeShowProgress() : Id;
  i = 0;
  function DoChunk() {
    var fuel = Math.min(chunk_fuel, data.length-i);
    progress(i+fuel);
    while (fuel > 0) {
      var r, min = C_max, max = C_min;
      for (var j=0; j<preds.length; j++) {
        r = preds[j](data[i]);
        if (r < min) {
          min = r;
          if (r <= C_min) break; // get out if it's hopeless
        }
        if (r > max) max = r;
      }
      if (max >= C_rexact && min >= C_exact) min = C_rexact;
      if (min > C_min) matches[C_max - min].push(data[i]);
      fuel--; i++;
    }
    if (i<data.length) t = setTimeout(DoChunk,5);
    else {
      i = 0;
      for (i = 0; i < matches.length; i++) {
        matches[i].sort(packageAndOrderCompare);
      }

      // matches per C_x are sorted nicely, be we want to
      // elevate language-fail matches above C_x matching
      var all_matches = []
      for (i = 0; i < matches.length; i++) {
        for (j = 0; j < matches[i].length; j++) {
          all_matches.push([i, matches[i][j]]);
        }
      }
      all_matches.sort(languageFamilyCompare);

      r = [matches[0].length, all_matches.map(function (l) { return l[1]; })];
      if (K) K(r); else return r;
    }
  };
  if (!K) return DoChunk();
  else { progress(0); t = setTimeout(DoChunk,5); return Killer; }
}

function GetContextHTML() {
  // useful only when a context is set
  return (((ctx_query_label != "") && ctx_query_label)
          ? SanitizeHTML(ctx_query_label)
          : ('&ldquo;' + SanitizeHTML(ctx_query) + '&rdquo;'));
}
function GetContextClearerHTML(text) {
  return ('<a href="?hq=" tabIndex="5"'
           +' title="Clear context-query"'
           +' style="text-decoration: none; color: #444;'
                  +' font-size: 82%; font-weight: bold;"'
           +' onclick="return new_query(this,\'\');">'
          + text
          + '</a>');
}

var search_data; // pre-filtered searchable index data
function ContextFilter() {
  ctx_query = NormalizeSpaces(ctx_query);
  search_data = Search(plt_search_data, ctx_query, true, false)[1];
  button_sep = "&nbsp;&nbsp;"
  context_str = "Searching"
  if (ctx_query == "") {
    ctx_query_label_line.innerHTML =
      '<span class="ssansserif context-line">'+context_str+': all'
       +button_sep
       +'<a href="#" tabIndex="5"'
       +' title="Edit context-query"'
       +' style="text-decoration: none; color: #444;'
              +' font-size: 82%; font-weight: bold;"'
       +' onclick="toggle_panel(\'contexts\'); return false;">'
       +MakeSVGRef("settings", "0")+'</a></span>';
  } else {
    ctx_query_label_line.innerHTML =
      '<span class="ssansserif context-line">'+context_str+':&nbsp;' + GetContextHTML()
      + button_sep
      + GetContextClearerHTML(MakeSVGRef("clear", "0"))
      + button_sep
      + '<a href="#" tabIndex="5"'
          +' title="Edit context-query"'
          +' style="text-decoration: none; color: #444;'
                 +' font-size: 82%; font-weight: bold;"'
          +' onclick="toggle_panel(\'contexts\'); return false;">'
          +MakeSVGRef("settings", "0")+'</a></span>';
  }
  last_search_term = null;
  last_search_term_raw = null;
}

var last_search_term, last_search_term_raw;
var search_results = [], first_search_result, exact_results_num;
var kill_bg_search = function(){ return; };
var search_timer = false;
function DoDelayedSearch() {
  // the whole delayed search thing was done to avoid redundant searching that
  // get the UI stuck, but now the search happens on a "thread" anyway, so it
  // might not be needed
  if (search_timer) clearTimeout(search_timer);
  search_timer = setTimeout(DoSearch, type_delay);
}
function DoSearch() {
  var term = query.value;
  if (term == last_search_term_raw) return;
  last_search_term_raw = term;
  term = NormalizeSpaces(term);
  if (term == last_search_term) return;
  last_search_term = term;
  status_line.innerHTML = "Searching " + search_data.length + " entries";
  kill_bg_search();
  kill_bg_search = Search(search_data, term, false,
    // use a continuation to run this in the background
    function(res) {
      search_results = res[1];
      exact_results_num = res[0];
      first_search_result = 0;
      status_line.innerHTML = "" + search_results.length + " entries found";
      query.style.backgroundColor =
        ((search_results.length == 0) && (term != "")) ? "#ffe0e0" : "white";
      UpdateResults();
    });
}

function UncompactUrl(url) {
  return url.replace(/^>/, plt_main_url);
}

function UncompactHtml(x) {
  if (typeof x == "string") {
    return x;
  } else if (!(x instanceof Array)) {
    return alert("Internal error in PLT docs");
  } else if ((x.length == 2) && (typeof(x[0]) == "number")) {
    return '<span class="' + plt_span_classes[x[0]]
           + '">' + UncompactHtml(x[1]) + '</span>';
  } else {
    var s = "";
    for (var i=0; i<x.length; i++) s = s.concat(UncompactHtml(x[i]));
    return s;
  }
}

function StripQArg(args) {
  // Don't propagate the `q=...` argument that is used to communicate
  // to this search page
  if (!args)
      return false;
  args = args.replace(/^[?]q=[^&]*&/,"?").replace(/^[?]q=[^&]*$/,"?").replace(/&q=[^&]*/,"");
  if (args == "?")
    return false;
  else
    return args;
}

function UpdateResults() {
  // Change the URL query param to reflect the current search term
  var term = query.value;
  var new_url = GetURL();
  new_url.searchParams.set("q", term);
  window.history.replaceState({}, "", new_url);

  // Also update the "navigating as <Family>" link
  var es = document.getElementsByClassName("navfamily");
  for (var i=0; i < es.length; i++) {
    var e = es[i];
    if (e.dataset.fam != undefined) {
      var nav_as = e.children[0];
      var link = nav_as.children[0];
      var url = new URL(link.href);
      url.searchParams.delete("qfrom");
      url.searchParams.set("qfrom", new_url);
      link.href = url;
    }
  }

  if (first_search_result < 0 ||
      first_search_result >= search_results.length)
    first_search_result = 0;
  var link_args = GetPageQueryString() && StripQArg("?" + GetPageQueryString());
  var show_family = ((plt_language_families.length > 1) && !(ctx_query.includes("F:"))) || (language_family != plt_main_language_family)
  for (var i=0; i<result_links.length; i++) {
    var n = i + first_search_result;
    if (n < search_results.length) {
        var note = false, res = search_results[n], desc = res[IDX_LIBS_SEXP], lang_fams = res[IDX_LANG_FAMILY];
      if ((desc instanceof Array) && (desc.length > 0)) {
        var desc_key = res[IDX_LIBS_TEXT] ? res[IDX_LIBS_TEXT] : desc;
        var desc_display = res[IDX_LIBS_HTML];
        note = '<span class="smaller">provided from</span> ';
        for (var j=0; j<desc.length; j++)
          note +=
            (j==0 ? "" : ", ")
            + '<a href="?q=' + encodeURIComponent("L:"+desc_key[j]) + '"'
               +' class="RktMod" tabIndex="2"'
               +' title="show bindings from the '+desc_key[j]+' module'
                       +' (right-click to refine current query)"'
               +' style="text-decoration: none; color: #006;"'
               +' onclick="return new_query(this,\'\');"'
               +' oncontextmenu="return refine_query(this);">'
              + (desc_display ? UncompactHtml(desc_display[j]) : desc[j]) + '</a>';
      } else if (desc == "module") {
        note = '<span class="smaller">module</span>';
      } else if (desc == "language") {
        note = '<span class="smaller">language</span>';
      }
      if (show_manuals == 2 || (show_manuals == 1 && !desc)) {
        var manual = UrlToManual(res[IDX_URL]),
            idx = (show_manual_titles && plt_manual_ptrs[manual]);
        note = (note ? (note + " ") : "");
        note += '<span class="smaller">in</span> '
                + '<a href="?q=T:' + manual + '" tabIndex="2"'
                   +' title="show entries from the '+manual+' manual'
                           +' (right-click to refine current query)"'
                   +' style="text-decoration: none; color: #006;"'
                   +' onclick="return new_query(this,\'\');"'
                   +' oncontextmenu="return refine_query(this);">'
                + ((typeof idx == "number")
                   ? ('<i>'+UncompactHtml(search_data[idx][IDX_KEY_HTML])+'</i>')
                   : manual)
                + '</a>';
      }
      if (note)
        note = '&nbsp;&nbsp;<span class="smaller">' + note + '</span>';
      if (show_family && (lang_fams[0] != language_family))
        note = '<div class="language-family">' + lang_fams[0] + "</div>" + note;
      var href = UncompactUrl(res[IDX_URL]);
      if (link_args) {
        var hash = href.indexOf("#");
        if (hash >= 0)
          href = href.substring(0,hash) + link_args + href.substring(hash);
        else
          href = href + link_args;
      }

      result_links[i].innerHTML =
        '<div title="" class="search-result-row"><a href="' + href
        + '" class="indexlink" tabIndex="2">'
        + UncompactHtml(res[IDX_KEY_HTML]) + '</a>' + (note || "") + '</div>';
      result_links[i].classList.remove(
        'search-result-wrapper-pkg-base',
        'search-result-wrapper-pkg-main-dist'
      );
      if (language_family == plt_main_language_family) {
        if (plt_base_pkgs.indexOf(res[IDX_PACKAGE]) >= 0) {
          result_links[i].classList.add('search-result-wrapper-pkg-base');
          result_links[i].title = "from base language's official documentation";
        } else if (plt_main_dist_pkgs.indexOf(res[IDX_PACKAGE]) >= 0) {
          result_links[i].classList.add('search-result-wrapper-pkg-main-dist');
          result_links[i].title = "from distribution's official documentation";
        } else {
          result_links[i].title = '';
        }
      } else {
        result_links[i].title = '';
      }

      result_links[i].firstChild.style.backgroundColor =
        (n < exact_results_num) ? highlight_color : background_color;

      result_links[i].style.display = "block";
    } else {
      result_links[i].style.display = "none";
    }
  }
  var exact = Math.min((exact_results_num - first_search_result),
                       results_num);
  exact = (exact <= 0) ? ''
          : ' (<span style="background-color: '+highlight_color+';">'
            + ((exact == results_num) ? 'all' : exact)
            + ' exact</span>)';
  if (search_results.length == 0) {
    if (last_search_term == "") status_line.innerHTML = "&nbsp;";
    else status_line.innerHTML = 'No matches found'
           + ((ctx_query != "")
              ? (' in '+GetContextHTML()
                 +' '+GetContextClearerHTML(MakeSVGRef("clear", "0")))
              : '')
           + '<br><div style="color: black; font-size: 82%;">'
           + '(Make sure your spelling is correct'
           + (last_search_term.search(/ /)>=0 ? ', or try fewer keywords' : '')
           + ((ctx_query != "") ? ', or clear the search context' : '')
           + ')</div>';
  } else if (search_results.length <= results_num)
    status_line.innerHTML = "Showing all matches" + exact;
  else
    status_line.innerHTML =
      "Showing "
      + (first_search_result+1) + "-"
      + Math.min(first_search_result+results_num,search_results.length)
      + exact
      + " of " + search_results.length
      + ((search_results.length==search_data.length) ? "" : " matches");
  prev_page_link1.style.color = prev_page_link2.style.color =
    (first_search_result-results_num >= 0) ? "black" : "#e0e0e0";
  next_page_link1.style.color = next_page_link2.style.color =
    (first_search_result+results_num < search_results.length)
    ? "black" : "#e0e0e0";
  saved_status = false;

  document.getElementById("redo_search_global").innerHTML =
        "<a href=\"http://docs.racket-lang.org/search/index.html?" + GetPageQueryString() + "\">Click here to repeat your search globally</a>.";
}

function HandleKeyEvent(event) {
  var key = null;
  if (typeof event == "string") key = event;
  else if (event) {
    switch (event.which || event.keyCode) {
      case 13: key = (event.ctrlKey ? "C-Enter" : "Enter"); break;
      case 33: key = "PgUp";  break;
      case 34: key = "PgDn";  break;
    }
  }
  // note: uses of DoSearch() below starts a background search, which
  // means that the operation can still be done on the previously
  // displayed results.
  switch (key) {
    case "Enter": // starts a search immediately
      DoSearch();
      return false;
    case "C-Enter": // C-enter with no change scrolls down (S -> up)
      if (query.value == last_search_term_raw) {
        if (!event.shiftKey) first_search_result += results_num;
        else if (first_search_result > 0) first_search_result -= results_num;
        else first_search_result = search_results.length - results_num;
        UpdateResults();
      } else {
        DoSearch();
      }
      return false;
    case "PgUp":
      DoSearch(); // in case we didn't update it yet
      first_search_result -= results_num;
      UpdateResults();
      return false;
    case "PgDn":
      DoSearch(); // in case we didn't update it yet
      if (first_search_result + results_num < search_results.length) {
        first_search_result += results_num;
        UpdateResults();
      }
      return false;
  }
  // this function is called via onkeydown, which is happens before the change
  // is visible; so use a timer to call this after the input field is updated
  setTimeout(DoDelayedSearch, 0);
  return true;
}
key_handler = HandleKeyEvent;

// use this one to set the query field without jumping to the current url
// again, since some browsers will reload the whole page for that (it would be
// nice if there was an easy way to add it to the history too)
function NewQuery(node,label) {
  var m, href = node.href;
  if ((m = href.search(/[?]q=[^?&;]+$/)) >= 0) { // `q' cannot be empty
    query.value = decodeURIComponent(href.substring(m+3));
    query.focus();
    DoSearch();
    return false;
  } else if ((m = href.search(/[?]hq=[^?&;]*$/)) >= 0) { // `hq' can
    SetContextQuery(decodeURIComponent(href.substring(m+4)),
                    decodeURIComponent(label));
    return false;
  } else {
    return true;
  }
}
new_query = NewQuery;

// and this appends the query to the current value (it's hooked on the
// oncontextmenu handler that doesn't work everywhere, but at least in
// FF and IE)
function RefineQuery(node) {
  var m = node.href.search(/[?]q=[^?&;]+$/);
  if (m < 0) return true;
  m = decodeURIComponent(node.href.substring(m+3));
  if (query.value.indexOf(m) >= 0) return true;
  else {
    query.value = query.value + " " + m;
    query.focus();
    DoSearch();
    return false;
  }
}
refine_query = RefineQuery;

var panel_shown = false;
function TogglePanel(name) {
  if (panel_shown) {
    document.getElementById(panel_shown+"_panel").style.display = "none";
    document.getElementById("close_panel").style.display = "none";
  }
  panel_shown = ((panel_shown != name) && name);
  if (panel_shown == "prefs") {
    document.getElementById("ctx_query_pref").value = ctx_query;
    document.getElementById("show_manuals_pref").selectedIndex = show_manuals;
    document.getElementById("show_manual_titles_pref").checked
                                                          = show_manual_titles;
    document.getElementById("results_num_pref").value = results_num;
    document.getElementById("type_delay_pref").value = type_delay;
    document.getElementById("highlight_color_pref").value = highlight_color;
  } else if (panel_shown == "contexts") {
    document.getElementById("context_query_pref").value = ctx_query;
  }
  if (panel_shown) {
    document.getElementById(panel_shown+"_panel").style.display = "block";
    document.getElementById("close_panel").style.display = "block";
    // This is annoying -- would be nicer to have a way to make sure
    // that it's in view without scrolling if not necessary.
    // document.getElementById(panel_shown+"_panel").scrollIntoView();
  }
}
toggle_panel = TogglePanel;

function HidePrefs(event) {
  if ((event.which || event.keyCode) == 27) {
    query.focus();
    TogglePanel("prefs"); // this function is called only when it's shown
  }
}
hide_prefs = HidePrefs;

function SetContextQuery(inp,label) {
  // can be called with the input object, or with a string
  if (typeof inp != "string") inp = inp.value;
  if (inp != ctx_query) {
    ctx_query = inp;
    SetCookie("PLT_ContextQuery", ctx_query);
    label = label || "";
    ctx_query_label = label;
    SetCookie("PLT_ContextQueryLabel",label);
    ContextFilter();
    document.getElementById("ctx_query_pref").value = ctx_query;
    document.getElementById("context_query_pref").value   = ctx_query;
    DoSearch();
  }
}
set_ctx_query     = SetContextQuery;
set_context_query = SetContextQuery; // a different widget, same effect

function SetShowManuals(inp) {
  if (inp.selectedIndex != show_manuals) {
    show_manuals = inp.selectedIndex;
    SetCookie("PLT_ManualSettings", show_manuals+(show_manual_titles?10:0));
    UpdateResults();
  }
}
set_show_manuals = SetShowManuals;

function SetShowManualTitles(inp) {
  if (inp.checked != show_manual_titles) {
    show_manual_titles = inp.checked;
    SetCookie("PLT_ManualSettings", show_manuals+(show_manual_titles?10:0));
    UpdateResults();
  }
}
set_show_manual_titles = SetShowManualTitles;

function SetResultsNum(inp) {
  var n = (parseInt(inp.value.replace(/[^0-9]+/g,"")) || results_num);
  inp.value = n;
  if (n != results_num) {
    results_num = n;
    SetCookie("PLT_ResultsNum", results_num);
    AdjustResultsNum();
    UpdateResults();
  }
}
set_results_num = SetResultsNum;

function SetTypeDelay(inp) {
  var n = (parseInt(inp.value.replace(/[^0-9]+/g,"")) || type_delay);
  inp.value = n;
  if (n != type_delay) {
    type_delay = n;
    SetCookie("PLT_TypeDelay", type_delay);
  }
}
set_type_delay = SetTypeDelay;

function SetHighlightColor(inp) {
  var c = (inp.value.replace(/[^a-zA-Z0-9#]/g,"") || highlight_color);
  inp.value = c;
  if (c != highlight_color) {
    highlight_color = c;
    SetCookie("PLT_HighlightColor", highlight_color);
    UpdateResults();
  }
}
set_highlight_color = SetHighlightColor;

AddOnLoad(InitializeSearch);

})();


function GotoDocIndex(ver, name) {
  if (plt_base_pkgs.indexOf("racket-index") >= 0) {
    location = MergePageArgsIntoUrl(plt_main_url + name + "/index.html");
    return false;
  }
  return true;
}
