/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Ubiquity.
 *
 * The Initial Developer of the Original Code is Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Jono DiCarlo <jdicarlo@mozilla.com>
 *   Blair McBride <unfocused@gmail.com>
 *   Abimanyu Raja <abimanyuraja@gmail.com>
 *   Michael Yoshitaka Erlewine <mitcho@mitcho.com>
 *   Satoshi Murakami <murky.satyr@gmail.com>
 *   Brandon Pung <brandonpung@gmail.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

// = Built-in Noun Types =
// **//FIXME//**
// \\Explain:
// * how nouns work.
// * common properties.
// ** {{{suggest}}}
// ** {{{default}}}
// ** {{{label}}} (, {{{name}}}, {{{id}}})
// ** {{{noSelection}}}
// ** {{{noExternalCalls}}}

const {classes: Cc, interfaces: Ci, utils: Cu} = Components;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://ubiquity/modules/cmdutils.js");
Cu.import("resource://ubiquity/modules/utils.js");
Cu.import("resource://ubiquity/modules/setup.js");
Cu.import("resource://gre/modules/PlacesUtils.jsm");

var {commandSource, skinService} = UbiquitySetup.createServices();

// === {{{ noun_arb_text }}} ===
// Suggests the input as is.
// * {{{text, html}}} : user input

var noun_arb_text = {
  label: "?",
  rankLast: true,
  noExternalCalls: true,
  cacheTime: -1,
  suggest: function nat_suggest(text, html, callback, selectionIndices) {
    return [CmdUtils.makeSugg(text, html, null, 0.3, selectionIndices)];
  },
  // hack to import feed-specific globals into this module
  // see feed-parts/header/initial.js
  loadGlobals: function nat_loadGlobals(source) {
    var target = (function () this)();
    for (let p of ["jQuery", "Date"]) target[p] = source[p];
    this.loadGlobals = function () {};
  }
};

// === {{{ noun_type_email_service }}} ===
// **//FIXME//**
// * {{{text}}} :
// * {{{html}}} :
// * {{{data}}} :

var noun_type_email_service = CmdUtils.NounType("email service",
                                                "googleapps gmail",
                                                "gmail");

// === {{{ noun_type_email }}} ===
// Suggests an email address (RFC2822 minus domain-lit).
// The regex is taken from:
// http://blog.livedoor.jp/dankogai/archives/51190099.html
// * {{{text, html}}} : email address

const EMAIL_ATOM = "[\\w!#$%&'*+/=?^`{}~|-]+";
var noun_type_email = {
  label: "email",
  noExternalCalls: true,
  cacheTime: -1,
  _email: RegExp("^(?:" + EMAIL_ATOM + "(?:\\." + EMAIL_ATOM +
                 ')*|(?:\\"(?:\\\\[^\\r\\n]|[^\\\\\\"])*\\"))@(' +
                 EMAIL_ATOM + "(?:\\." + EMAIL_ATOM + ")*)$"),
  _username: RegExp("^(?:" + EMAIL_ATOM + "(?:\\." + EMAIL_ATOM +
                    ')*|(?:\\"(?:\\\\[^\\r\\n]|[^\\\\\\"])*\\"))$'),
  suggest: function nt_email_suggest(text, html, cb, selectionIndices) {
    if (this._username.test(text))
      return [CmdUtils.makeSugg(text, html, null, 0.3, selectionIndices)];

    var match = text.match(this._email);
    if (!match) return [];

    var domain = match[1];
    // if the domain doesn't have a period or the TLD
    // has less than two letters, penalize
    var score = /\.(?:\d+|[a-z]{2,})$/i.test(domain) ? 1 : 0.8;

    return [CmdUtils.makeSugg(text, html, null, score, selectionIndices)];
  }
};

// === {{{ noun_type_percentage }}} ===
// Suggests a percentage value.
// * {{{text, html}}} : "?%"
// * {{{data}}} : a float number (1.0 for 100% etc.)

var noun_type_percentage = {
  label: "percentage",
  noExternalCalls: true,
  cacheTime: -1,
  default: CmdUtils.makeSugg("100%", null, 1, 0.3),
  suggest: function nt_percentage_suggest(text, html) {
    var number = parseFloat(text);
    if (isNaN(number)) return [];

    var score = text.replace(/[^-\d.Ee%]/g, "").length / text.length;
    var nopercent = text.indexOf("%") < 0;
    if (nopercent) score *= 0.9;

    var suggs = [CmdUtils.makeSugg(number + "%", null, number / 100, score)];
    // if the number's 10 or less and there's no
    // % sign, also try interpreting it as a proportion instead of a
    // percent and offer it as a suggestion as well, but with a lower
    // score.
    if (nopercent && number <= 10)
      suggs.push(CmdUtils.makeSugg(
        number * 100 + "%", null, number, score * 0.9));
    return suggs;
  },
};

// === {{{ noun_type_tab }}} ===
// Suggests currently opened tabs.
// * {{{text, html}}} : tab title or URL
// * {{{data}}} : {{{Utils}}}{{{.BrowserTab}}} instance

var noun_type_tab = {
  label: "title or URL",
  noExternalCalls: true,
  registerCacheObserver: function nt_tab_observer(flush) {
    function registerListenersForWindow(window) {
      let browser = getBrowser(window);
      if (browser == null)
        return;
      let container = browser.tabContainer;
      container.addEventListener("TabOpen", flush, false);
      container.addEventListener("TabClose", flush, false);
    }
    function unRegisterListenersForWindow(window) {
      let browser = getBrowser(window);
      if (browser == null)
        return;
      let container = browser.tabContainer;
      container.removeEventListener("TabOpen", flush, false);
      container.removeEventListener("TabClose", flush, false);
    }
    function getBrowser(window) {
      // Make sure the window is browser-like
      if (typeof window.getBrowser !== "function")
        return null;
      // Make sure it's a tabbrowser-like window
      let browser = window.getBrowser();
      if (browser == null || typeof browser.tabContainer !== "object")
        return null;
      return browser;
    }
    // Register as an observer so we can catch windows opening and closing:
    var ww = (Cc["@mozilla.org/embedcomp/window-watcher;1"]
              .getService(Ci.nsIWindowWatcher));
    ww.registerNotification({
      observe: function observe(aSubject, aTopic, aData) {
        /* Called when a window opens or closes.  Make sure that every
         * window has the appropriate listeners registered. */
        let window = aSubject.QueryInterface(Ci.nsIDOMWindow);
        if (aTopic === "domwindowopened")
          registerListenersForWindow(window);
        else if (aTopic === "domwindowclosed")
          unRegisterListenersForWindow(window);
      }
    });
    /* Also directly register the listeners for any browser window already
     * open: */
    let wm = (Cc["@mozilla.org/appshell/window-mediator;1"]
              .getService(Ci.nsIWindowMediator));
    let enumerator = wm.getEnumerator("navigator:browser");
    while (enumerator.hasMoreElements()) {
      registerListenersForWindow(enumerator.getNext());
    };
  },
  suggest: function nt_tab_suggest(text, html, cb, selectedIndices)
    [CmdUtils.makeSugg(
      tab.document.title || tab.document.location.href,
      null, tab, CmdUtils.matchScore(tab.match), selectedIndices)
     for (tab of Utils.tabs.search(text, CmdUtils.maxSuggestions))],
};

// === {{{ noun_type_search_engine }}} ===
// **//FIXME//**
// * {{{text, html}}} : name of the engine
// * {{{data}}} : engine object (see {{{nsIBrowserSearchService}}})

var noun_type_search_engine = {
  label: "search engine",
  noExternalCalls: true,
  // the default search engine should just get 0.3 or so...
  // if it's actually entered, it can get a higher score.
  default: function nt_sengine_default()
    this._sugg(this._BSS.defaultEngine, 0.3),
  suggest: function nt_sengine_suggest(text) {
    var suggs = this._BSS.getVisibleEngines({}).map(this._sugg);
    return CmdUtils.grepSuggs(text, suggs);
  },
  _BSS: (Cc["@mozilla.org/browser/search-service;1"]
         .getService(Ci.nsIBrowserSearchService)),
  _sugg: function nt_sengine__sugg(engine, score) (
    CmdUtils.makeSugg(engine.name, null, engine, score || 1)),
};

// === {{{ noun_type_tag }}} ===
// Suggests the input as comma separated tags,
// plus completions based on existing tags.
// Defaults to all tags.
// * {{{text, html}}} : comma separated tags
// * {{{data}}} : an array of tags

var noun_type_tag = {
  label: "tag1[,tag2 ...]",
  rankLast: true,
  noExternalCalls: true,
  default: function nt_tag_default()
    [CmdUtils.makeSugg(tag, null, [tag], .3)
     for (tag of PlacesUtils.tagging.allTags)],
  suggest: function nt_tag_suggest(text) {
    // can accept multiple tags, separated by commas
    var tags = text.split(/\s*,\s*/).filter(RegExp.prototype.test.bind(/\S/));
    if (!tags.length) return tags;

    var score = .3, {pow} = Math;
    var {allTags} = PlacesUtils.tagging;
    var allTagsLC = [tag.toLowerCase() for (tag of allTags)];
    for (let [i, tag] of new Iterator(tags)) {
      let index = allTagsLC.indexOf(tag.toLowerCase());
      if (~index) {
        // if preexisting, boost score
        score = pow(score, .5);
        // replace with the one with proper case
        tags[i] = allTags[index];
      }
      else
        // if multi-word, unboost score
        score /= pow(2, (tag.match(/\s+/g) || "").length);
    }
    var suggs = [CmdUtils.makeSugg(null, null, tags, score)];

    // assume last tag is still being typed - suggest completions for that
    var lastTagLC = tags[tags.length - 1].toLowerCase();
    for (let [i, atagLC] in new Iterator(allTagsLC))
      // only match from the beginning of a tag name (not the middle)
      if (lastTagLC.length < atagLC.length &&
          atagLC.indexOf(lastTagLC) === 0)
        suggs.push(CmdUtils.makeSugg(null, null,
                                     tags.slice(0, -1).concat(allTags[i]),
                                     pow(score, .5)));

    return suggs;
  }
};

// === {{{ noun_type_awesomebar }}} ===
// Suggests "Awesome Bar" query results.
// * {{{text, html}}} : title or url
// * {{{data}}} : a query result
//   (see {{{Utils}}}{{{.history.search}}})

var noun_type_awesomebar = {
  label: "query",
  rankLast: true,
  noExternalCalls: true,
  cacheTime: 0,
  suggest: function nt_awesome_suggest(text, html, callback) {
    text = text.trim();
    if (!text) return [];

    var reqObj = {readyState: 2}, {_match} = this;
    Utils.history.search(text, function nt_awesome_results(results) {
      reqObj.readyState = 4;
      if (/\s/.test(text)) { // multi-word query
        //TODO: should we calculate scores for these as well? if so, how?
        callback([CmdUtils.makeSugg(r.title || r.url, null, r)
                  for (r of results)]);
        return;
      }
      var returnArr = [], lctxt = text.toLowerCase();
      for (let r of results) {
        let u = _match(r.url, lctxt);
        let t = _match(r.title, lctxt);
        let m = u.score > t.score ? u : t;
        returnArr.push(CmdUtils.makeSugg(m.input, null, r, m.score));
      }
      callback(returnArr);
    });
    return [reqObj];
  },
  // creates a fake match object with an applicable score
  _match: function nt_awesome_match(input, lctxt) {
    var index = input.toLowerCase().indexOf(lctxt);
    var match = {index: index, input: input, 0: lctxt};
    match.score = ~index && CmdUtils.matchScore(match);
    return match;
  },
};

// === {{{ noun_type_extension }}} ===
// Suggests installed extensions.
// * {{{text, html}}} : extension name
// * {{{data}}} : contains extension {{{id}}}, {{{name}}} and {{{version}}}

var noun_type_extension = {
  label: "name",
  noExternalCalls: true,
  suggest: function nt_ext_suggest(text, html, cb) {
    if (this._list.length) return CmdUtils.grepSuggs(text, this._list);

    var fakeReq = {readyState: 2};
    ("AddonManager" in Utils
     ? Utils.AddonManager.getAllAddons(setList)
     : setList(Utils.ExtensionManager.getItemList(2, {})));
    function setList(exts) {
      var {escapeHtml} = Utils, h;
      this._list = [
        { text: ext.name, data: ext,
          html: h = escapeHtml(ext.name), summary: h }
        for (ext of exts)];
      fakeReq.readyState = 4;
      cb(CmdUtils.grepSuggs(text, this._list));
    }
    return [fakeReq];
  },
  _list: [],
};

// === {{{ noun_type_common_URI_scheme }}} ===
// Suggests common URI schemes, which are the IANA-registered ones
// plus Unofficial ones and a few Mozilla specific ones.
// See [[http://en.wikipedia.org/wiki/URI_scheme]].
// * {{{text, html}}} : URI scheme

var common_URI_schemes = "\
  aaa aaas acap cap cid crid data dav dict dns fax file ftp go gopher h323\
  http https icap im imap info ipp iris iris.beep iris.xpc iris.xpcs iris.lws\
  ldap mailto mid modem msrp msrps mtqp mupdate news nfs nntp opaquelocktoken\
  pop pres prospero rtsp service shttp sip sips snmp soap.beep soap.beeps tag\
  tel telnet tftp thismessage tip tv urn vemmi wais xmlrpc.beep xmpp\
  z39.50r z39.50s\
  about afp aim apt bolo bzr callto cel cvs daap ed2k feed fish gg git\
  gizmoproject iax2 irc ircs itms lastfm ldaps magnet mms msnim psyc rsync\
  secondlife skype ssh svn sftp smb sms soldat steam unreal ut2004 view-source\
  vzochat webcal wyciwyg xfire ymsgr\
  chrome resource\
".match(/\S+/g);

var noun_type_common_URI_scheme = CmdUtils.NounType(
  "URI scheme",
  [scheme + ":" for (scheme of common_URI_schemes)]);

// === {{{ noun_type_url }}} ===
// Suggests URLs from the user's input and/or history.
// Defaults to the current page's URL if no input is given.
// * {{{text, html}}} : URL
// * {{{data}}} : null or query result (same as {{{noun_type_awesomebar}}})

var noun_type_url = {
  label: "URL",
  noExternalCalls: true,
  cacheTime: 0,
  default: function nt_url_default() {
    var {document, location} = CmdUtils.window, {href} = location
    if (location.hostname === 'inoreader.com') {
      let a = document.querySelector('.article_current .article_title_link')
      a && ({href} = a)
    }
    var suggs = [CmdUtils.makeSugg(href, null, null, .5)]
      , {activeElement} = document
    if (activeElement && activeElement.href)
      suggs.unshift(CmdUtils.makeSugg(activeElement.href, null, null, .7));
    return suggs;
  },
  suggest: function nt_url_suggest(text, html, callback, selectionIndices) {
    text = text.trim();
    if (!text || /\s/.test(text)) return [];

    var score = 1;
    // has scheme?
    if (/^[\w.-]+:\/{0,2}(?=.)/.test(text)) {
      var {lastMatch: scheme, rightContext: postScheme} = RegExp;
      if (postScheme.indexOf(".") < 0) score *= 0.9;
    }
    // has TLD?
    else if (text.indexOf(".") > 0 && /\b[a-z]{2,}\b/i.test(text)) {
      var scheme = "http://", postScheme = text;
      if (selectionIndices)
        selectionIndices =
          [i + scheme.length for (i of selectionIndices)];
      score *= 0.9;
    }
    else return [];

    var [domain, path] = postScheme.split(/[/?#]/, 2);
    // if it's just a domain name-looking thing, lower confidence
    if (path == null) score *= 0.9;
    // LDH charcodes include "Letters, Digits, and Hyphen".
    // We'll throw in . @ : too.
    if (/^(?![A-Za-z\d-.@:]+$)/.test(domain)) score *= 0.9;

    var fakeRequest = {readyState: 2};
    Utils.history.search(text, function nt_url_search(results) {
      fakeRequest.readyState = 4;
      var suggs = [], tlc = text.toLowerCase();
      for (let r of results) {
        let urlIndex = r.url.toLowerCase().indexOf(tlc);
        if (urlIndex < 0) continue;
        let urlScore =
          CmdUtils.matchScore({index: urlIndex, 0: text, input: r.url});
        suggs.push(CmdUtils.makeSugg(
          r.url, null, r, urlScore,
          selectionIndices && [urlIndex, urlIndex + text.length]));
      }
      callback(suggs);
    });

    return [CmdUtils.makeSugg(scheme + postScheme, null, null,
                              score, selectionIndices),
            fakeRequest];
  },
};

// === {{{ noun_type_livemark }}} ===
// Suggests each livemark whose title matches the input.
// * {{{text, html}}} : title
// * {{{data.id}}} : id
// * {{{data.feed}}} : feed URL
// * {{{data.site}}} : site URL
// * {{{data.items}}} : an array of items loaded in the livemark
// {{{feeds}}} is the getter for all livemarks.

var noun_type_livemark = {
  label: "title",
  cacheTime: 0,
  suggest: function nt_livemark_suggest(text, html, cb, selected) {
    if (!text) return [];

    var {feeds} = this;
    if (!feeds.length) return [];

    var {bookmarks} = PlacesUtils;
    var suggs =  [CmdUtils.makeSugg(bookmarks.getItemTitle(id), null,
                                    {id: id, __proto__: this._proto_})
                  for (id of feeds)];
    return CmdUtils.grepSuggs(text, suggs);
  },
  get feeds() PlacesUtils.annotations
              .getItemsWithAnnotation("livemark/feedURI", {}),
  _proto_: {
    get feed() PlacesUtils.livemarks.getFeedURI(this.id).spec,
    get site() PlacesUtils.livemarks.getSiteURI(this.id).spec,
    items: function livemark_items(callback) {
      PlacesUtils.livemarks.getLivemark(
        {id: this.id},
        function (status, livemark) {
          var list = Components.isSuccessCode(status)?
            livemark.getNodesForContainer({}) : [];
          callback(list);
          list.length || livemark.reload();
        }
      );
    },
  },
};

// === {{{ noun_type_command }}} ===
// Suggests each installed command whose name matches the input.
// * {{{text, html}}} : command name
// * {{{data}}} : command object

var noun_type_command = {
  label: "name",
  noExternalCalls: true,
  cacheTime: 0,
  suggest: function nt_command_suggest(text) {
    if (!text) return [];
    var grepee = this._get();
    if (!grepee.length) return grepee;
    var suggs = CmdUtils.grepSuggs(text, grepee);
    if (!suggs.length) return suggs;
    Utils.uniq(suggs, function nt_command_id(s) s.data.id);
    for (let s of suggs) s.html = s.summary = Utils.escapeHtml(s.text);
    return suggs;
  },
  _get: function nt_command__get() {
    var cmds = (cmd for ([, cmd] in Iterator(commandSource.getAllCommands())));
    if ("disabled" in this) {
      let {disabled} = this;
      cmds = [cmd for (cmd of cmds) if (cmd.disabled === disabled)];
    }
    return [{text: name, data: cmd} for (cmd of cmds) for (name of cmd.names)];
  },
};

// === {{{ noun_type_enabled_command }}} ===
// === {{{ noun_type_disabled_command }}} ===
// Same as {{{noun_type_command}}}, but with only enabled/disabled commands.

var noun_type_enabled_command = {
  __proto__: noun_type_command,
  get disabled() false,
};

var noun_type_disabled_command = {
  __proto__: noun_type_command,
  get disabled() true,
};

// === {{{ noun_type_skin }}} ===
// Suggests each installed skin whose name matches the input.
// * {{{text, html}}} : skin name
// * {{{data}}} : skin feed object (see {{{SkinFeedPlugin}}})

var noun_type_skin = {
  label: "name",
  noExternalCalls: true,
  cacheTime: 0,
  suggest: function nt_skin_suggest(text, html, cb, selected) {
    var suggs = [CmdUtils.makeSugg(skin.metaData.name, null, skin)
                 for (skin of skinService.skins)];
    return CmdUtils.grepSuggs(text, suggs);
  },
};

// === {{{ noun_type_twitter_user }}} ===
// Suggests Twitter IDs from the user's login info.
// * {{{text, html}}} : Twitter ID
// * {{{data}}} : login data (see {{{nsILoginManager}}})

var noun_type_twitter_user = {
  label: "user",
  rankLast: true,
  noExternalCalls: true,
  cacheTime: 0,
  suggest: function nt_twuser_suggest(text, html, cb, selected) {
    // reject text from selection.
    if (!text || selected) return [];

    var foundAt = text[0] === "@";
    if (foundAt) text = text.slice(1); // strip off the @

    var suggs = CmdUtils.grepSuggs(text, this.logins());

    if (/^\w+$/.test(text) && suggs.every(function(s) s.text !== text))
      suggs.push(CmdUtils.makeSugg(text, null, {username: text}, .4));

    if (foundAt) suggs = [
      { __proto__: s,
        summary: "@" + s.summary,
        score: Math.pow(s.score, 0.8) }
      for (s of suggs)];

    return suggs;
  },
  logins: function nt_twuser_logins(reload) {
    // TODO: figure out how often to clear this list cache.
    if (this._list && !reload) return this._list;
    var list = [];
    if (Utils.loggedIn) {
      // Look for twitter usernames stored in password manager
      const {LoginManager} = Utils, usersFound = {__proto__: null};
      for (let url of ["https://twitter.com", "http://twitter.com"]) {
        for (let login of LoginManager.findLogins({}, url, "", "")) {
          let {username} = login;
          if (username in usersFound) continue;
          usersFound[username] = true;
          list.push(CmdUtils.makeSugg(username, null, login));
        }
      }
    }
    return this._list = list;
  },
  _list: null,
};

// === {{{ noun_type_number }}} ===
// Suggests a number value. Defaults to 1.
// * {{{text, html}}} : number text
// * {{{data}}} : number

var noun_type_number = {
  label: "number",
  noExternalCalls: true,
  cacheTime: -1,
  suggest: function nt_number_suggest(text) {
    var num = +text;
    return isNaN(num) ? [] : [CmdUtils.makeSugg(text, null, num)];
  },
  default: CmdUtils.makeSugg("1", null, 1, 0.5),
};

// === {{{ noun_type_bookmarklet }}} ===
// Suggests each bookmarklet whose title matches the input.
// * {{{text, html}}} : bookmarklet title
// * {{{data}}} : bookmarklet (pseudo) url
// {{{load()}}} : Reloads bookmarklets.

var noun_type_bookmarklet = {
  label: "title",
  noExternalCalls: true,
  cacheTime: 0,
  suggest: function nt_bookmarklet_suggest(text, html, cb, selected) {
    if (!text) return [];
    return CmdUtils.grepSuggs(text, this.list);
  },
  list: null,
  load: function nt_bookmarklet_load(reload) {
    var list = [];
    var {bookmarks, history} = PlacesUtils;
    var query = history.getNewQuery();
    var options = history.getNewQueryOptions();
    query.onlyBookmarked = true;
    query.searchTerms = "javascript:";
    options.queryType = options.QUERY_TYPE_BOOKMARKS;
    options.sortingMode = options.SORT_BY_TITLE_DESCENDING;
    var {root} = history.executeQuery(query, options);
    root.containerOpen = true;
    for (var i = root.childCount; i--;) {
      var node = root.getChild(i);
      if (/^javascript:/.test(node.uri) &&
          !bookmarks.getKeywordForBookmark(node.itemId))
        list.push(CmdUtils.makeSugg(node.title, null, node.uri));
    }
    root.containerOpen = false;
    this.list = list;
    return this;
  }
}.load();

// === {{{ noun_type_date }}} ===
// === {{{ noun_type_time }}} ===
// === {{{ noun_type_date_time }}} ===
// Suggests a date/time for input, using the mighty {{{Date.parse()}}}.
// Defaults to today/now.
// * {{{text, html}}} : date/time text
// * {{{data}}} : {{{Date}}} instance

function scoreDateTime(text) {
  // Give penalty for short input only slightly,
  // as Date.parse() can handle variety of lengths like:
  // "t" or "Wednesday September 18th 2009 13:29:54 GMT+0900",
  var score = Math.pow(text.length / 42, 1 / 17); // .8 ~
  return score > 1 ? 1 : score;
}

var noun_type_date = {
  label: "date",
  noExternalCalls: true,
  cacheTime: 0,
  "default": function nt_date_default() this._sugg(Date.today()),
  suggest: function nt_date_suggest(text) {
    var date = Date.parse(text);
    if (!date) return [];

    var score = scoreDateTime(text);
    if (date.isToday())
      score *= .5;
    if (date.getHours() || date.getMinutes() || date.getSeconds())
      score *= .7;

    return [this._sugg(date, score)];
  },
  _sugg: function nt_date__sugg(date, score)
    CmdUtils.makeSugg(date.toString("yyyy-MM-dd"), null, date, score),
};

var noun_type_time = {
  label: "time",
  noExternalCalls: true,
  cacheTime: 0,
  "default": function nt_time_default() this._sugg(Date.parse("now")),
  suggest: function nt_time_suggest(text, html) {
    var date = Date.parse(text);
    if (!date) return [];

    var score = scoreDateTime(text), now = Date.parse("now");
    if (Math.abs(now - date) > 9) { // not "now"
      if (!now.isSameDay(date))
        score *= .7; // not "today"
      if (!date.getHours() && !date.getMinutes() && !date.getSeconds())
        score *= .5; // "00:00:00"
    }
    return [this._sugg(date, score)];
  },
  _sugg: function nt_time__sugg(date, score)
    CmdUtils.makeSugg(date.toString("hh:mm:ss tt"), null, date, score),
};

var noun_type_date_time = {
  label: "date and time",
  noExternalCalls: true,
  cacheTime: 0,
  "default": function nt_date_time_default() this._sugg(Date.parse("now")),
  suggest: function nt_time_suggest(text) {
    var date = Date.parse(text);
    if (!date) return [];

    var score = scoreDateTime(text), now = Date.parse("now");
    if (Math.abs(now - date) > 9) { // not "now"
      if (now.isSameDay(date))
        score *= .7; // "today"
      if (!date.getHours() && !date.getMinutes() && !date.getSeconds())
        score *= .7; // "00:00:00"
    }
    return [this._sugg(date, score)];
  },
  _sugg: function nt_date_time__sugg(date, score)
    CmdUtils.makeSugg(date.toString("yyyy-MM-dd hh:mm tt"), null, date,
                      score),
};

// === {{{ noun_type_contact }}} ===
// Same as {{{noun_type_email}}}, but also suggests
// the user's contact informations that are fetched from Gmail (for now).
// * {{{text}}} : email address
// * {{{html}}} : same as {{{summary}}}
// * {{{data}}} : name of contactee

var noun_type_contact = {
  label: "name or email",
  suggest: function nt_contact_suggest(text, html, callback) {
    var suggs = noun_type_email.suggest.apply(noun_type_email, arguments);
    if (this._list) return this._grep(text).concat(suggs);
    var self = this;
    this._list = [];
    getGmailContacts(
      function nt_contact_ok(contacts) {
        var list = self._list;
        for (var {name, email} of contacts) {
          let htm = Utils.escapeHtml(name) + " &lt;" +
                    Utils.escapeHtml(email) + "&gt;";
          list.push({
            text: email, html: htm, data: name, summary: htm, score: 1});
        }
        callback(self._grep(text));
      },
      function nt_contact_ng(info) {
        Utils.setTimeout(function nt_contact_reset() { self._list = null },
                         self._retryInterval *= 2);
        Utils.reportInfo(
          info + " (retrying in " + self._retryInterval / 1e3 + " sec.)");
      });
    return suggs;
  },
  _list: null,
  _retryInterval: 5e3,
  _grep: function nt_contact__grep(text)
    Utils.uniq([].concat(CmdUtils.grepSuggs(text, this._list, "data"),
                         CmdUtils.grepSuggs(text, this._list)),
               "text"),
};

function getGmailContacts(ok, ng) {
  if (!Utils.loggedIn)
    return ng("Not logged in.");
  var logins = Utils.LoginManager
    .findLogins({}, "https://www.google.com", "", "");
  if (!logins.length)
    return ng("No Google logins.");
  var errors = 0;
  for (let login of logins) jQuery.ajax({
    type: "POST", url: "https://www.google.com/accounts/ClientLogin",
    data: {
      Email  : login.username,
      Passwd : login.password,
      accountType: "GOOGLE", service: "cp", source: "Mozilla-Ubiquity-0.6",
    },
    error: googleContactsError,
    success: function googleClientLoggedIn(data, status, xhr) {
      var [, auth] = /^Auth=(.+)/m.exec(data) || [];
      if (!auth) return this.error(xhr);
      jQuery.ajax({
        url: "https://www.google.com/m8/feeds/contacts/default/full",
        dataType: "xml",
        beforeSend: function setGoogleLoginAuth(xhr) {
          xhr.setRequestHeader("Authorization", "GoogleLogin auth=" + auth);
        },
        error: googleContactsError,
        success: function onContacts(atom) {
          var email;
          ok([
            {name  : entry.querySelector("title").textContent,
             email : email.getAttribute("address")}
            for (entry of Array.slice(atom.getElementsByTagName("entry")))
              if (email = entry.querySelector("email"))
          ]);
        },
      });
    },
  });
  function googleContactsError(xhr) {
    Utils.reportInfo(this.url + ": " + xhr.status + " " + xhr.statusText);
    ++errors == logins.length && ng("Failed retrieving Google Contacts");
  }
}

// === {{{ noun_type_geolocation }}} ===
// * {{{text, html}}} : user input / "city,( state,) country"
// * {{{data}}} : {{{null}}} or geoLocation object
//   (as returned by {{{CmdUtils}}}{{{.getGeoLocation()}}})

var noun_type_geolocation = {
  label: "geolocation",
  rankLast: true,
  default: function nt_geoloc_default(loc) {
    if (!(loc = loc || CmdUtils.geoLocation)) return null;
    var {city, state, country: text} = loc;
    if (state && state !== city) text = state + ", " + text;
    if (city) text = city + ", " + text;
    return CmdUtils.makeSugg(text, null, loc);
  },
  suggest: function nt_geoloc_suggest(text, html, callback, selectionIndices) {
    // LONGTERM TODO: try to detect whether fragment is anything like
    // a valid location or not, and don't suggest anything
    // for input that's not a location.
    var suggs = [CmdUtils.makeSugg(text, null, null, 0.3, selectionIndices)];
    // TODO: we should try to build this "here" handling into something like
    // magic words (anaphora) handling in Parser 2: make it localizable.
    if (/^\s*here\s*$/i.test(text)) {
      let me = this;
      suggs.push(CmdUtils.getGeoLocation(function nt_geoloc_async(loc) {
        callback(me.default(loc));
      }));
    }
    return suggs;
  },
};

// === {{{ noun_type_lang_google }}} ===
// Suggests languages used in various Google services.
// * {{{text, html}}} : language name
// * {{{data}}} : language code
//
// {{{getLangName(code)}}} returns the corresponding language name
// for {{{code}}}.

var noun_type_lang_google = CmdUtils.NounType("language", {
  Afrikaans: "af",
  Albanian: "sq",
  Arabic: "ar",
  Armenian: "hy",
  Azerbaijani: "az",
  Basque: "eu",
  Belarusian: "be",
  Bulgarian: "bg",
  Catalan: "ca",
  "Chinese Simplified": "zh-CN",
  "Chinese Traditional": "zh-TW",
  Croatian: "hr",
  Czech: "cs",
  Danish: "da",
  Dutch: "nl",
  English: "en",
  Estonian: "et",
  Filipino: "tl",
  Finnish: "fi",
  French: "fr",
  Galician: "gl",
  Georgian: "ka",
  German: "de",
  Greek: "el",
  Hebrew: "iw",
  Hindi: "hi",
  Hungarian: "hu",
  Icelandic: "is",
  Indonesian: "id",
  Irish: "ga",
  Italian: "it",
  Japanese: "ja",
  Korean: "ko",
  Latin: "la",
  Latvian: "lv",
  Lithuanian: "lt",
  Macedonian: "mk",
  Malay: "ms",
  Maltese: "mt",
  Norwegian: "no",
  Persian: "fa",
  Polish: "pl",
  Portuguese: "pt",
  Romanian: "ro",
  Russian: "ru",
  Serbian: "sr",
  Slovak: "sk",
  Slovenian: "sl",
  Spanish: "es",
  Swahili: "sw",
  Swedish: "sv",
  Thai: "th",
  Turkish: "tr",
  Ukrainian: "uk",
  Urdu: "ur",
  Vietnamese: "vi",
  Welsh: "cy",
  Yiddish: "yi",
});

// === {{{ noun_type_lang_wikipedia }}} ===
// Suggests languages used in Wikipedia.
// Works as same as noun_{{{type_lang_google}}}.

// from http://meta.wikimedia.org/wiki/List_of_Wikipedias
// omitting ones with 100+ articles
var noun_type_lang_wikipedia = CmdUtils.NounType("language", {
  English: "en",
  German: "de",
  French: "fr",
  Polish: "pl",
  Japanese: "ja",
  Italian: "it",
  Dutch: "nl",
  Portuguese: "pt",
  Spanish: "es",
  Russian: "ru",
  Swedish: "sv",
  Chinese: "zh",
  "Norwegian (Bokmal)": "no",
  Finnish: "fi",
  Catalan: "ca",
  Ukrainian: "uk",
  Turkish: "tr",
  Czech: "cs",
  Hungarian: "hu",
  Romanian: "ro",
  Volapuk: "vo",
  Esperanto: "eo",
  Danish: "da",
  Slovak: "sk",
  Indonesian: "id",
  Arabic: "ar",
  Korean: "ko",
  Hebrew: "he",
  Lithuanian: "lt",
  Vietnamese: "vi",
  Slovenian: "sl",
  Serbian: "sr",
  Bulgarian: "bg",
  Estonian: "et",
  Persian: "fa",
  Croatian: "hr",
  "Simple English": "simple",
  "Newar / Nepal Bhasa": "new",
  Haitian: "ht",
  "Norwegian (Nynorsk)": "nn",
  Galician: "gl",
  Thai: "th",
  Telugu: "te",
  Greek: "el",
  Malay: "ms",
  Basque: "eu",
  Cebuano: "ceb",
  Hindi: "hi",
  Macedonian: "mk",
  Georgian: "ka",
  Latin: "la",
  Bosnian: "bs",
  Luxembourgish: "lb",
  Breton: "br",
  Icelandic: "is",
  "Bishnupriya Manipuri": "bpy",
  Marathi: "mr",
  Albanian: "sq",
  Welsh: "cy",
  Azeri: "az",
  "Serbo-Croatian": "sh",
  Tagalog: "tl",
  Latvian: "lv",
  Piedmontese: "pms",
  Bengali: "bn",
  "Belarusian (Tarashkevitsa)": "be-x-old",
  Javanese: "jv",
  Tamil: "ta",
  Occitan: "oc",
  Ido: "io",
  Belarusian: "be",
  Aragonese: "an",
  "Low Saxon": "nds",
  Sundanese: "su",
  Sicilian: "scn",
  Neapolitan: "nap",
  Kurdish: "ku",
  Asturian: "ast",
  Afrikaans: "af",
  "West Frisian": "fy",
  Swahili: "sw",
  Walloon: "wa",
  Cantonese: "zh-yue",
  Samogitian: "bat-smg",
  Quechua: "qu",
  Urdu: "ur",
  Chuvash: "cv",
  Ripuarian: "ksh",
  Malayalam: "ml",
  Tajik: "tg",
  Irish: "ga",
  Venetian: "vec",
  Tarantino: "roa-tara",
  "Waray-Waray": "war",
  Uzbek: "uz",
  "Scottish Gaelic": "gd",
  Kapampangan: "pam",
  Kannada: "kn",
  Maori: "mi",
  Yiddish: "yi",
  Yoruba: "yo",
  Gujarati: "gu",
  Nahuatl: "nah",
  Lombard: "lmo",
  Corsican: "co",
  Gilaki: "glk",
  "Upper Sorbian": "hsb",
  "Min Nan": "zh-min-nan",
  Aromanian: "roa-rup",
  Alemannic: "als",
  Interlingua: "ia",
  Limburgian: "li",
  Armenian: "hy",
  Sakha: "sah",
  Kazakh: "kk",
  Tatar: "tt",
  Gan: "gan",
  Sanskrit: "sa",
  Turkmen: "tk",
  Wu: "wuu",
  "Dutch Low Saxon": "nds-nl",
  Faroese: "fo",
  "West Flemish": "vls",
  Norman: "nrm",
  Ossetian: "os",
  Voro: "fiu-vro",
  Amharic: "am",
  Romansh: "rm",
  Banyumasan: "map-bms",
  Pangasinan: "pag",
  Divehi: "dv",
  Mongolian: "mn",
  "Egyptian Arabic": "arz",
  "Northern Sami": "se",
  Zazaki: "diq",
  Nepali: "ne",
  Friulian: "fur",
  Manx: "gv",
  Scots: "sco",
  Ligurian: "lij",
  Novial: "nov",
  Bavarian: "bar",
  Bihari: "bh",
  Maltese: "mt",
  Ilokano: "ilo",
  Pali: "pi",
  "Classical Chinese": "zh-classical",
  Khmer: "km",
  "Franco-Provencal/Arpitan": "frp",
  Mazandarani: "mzn",
  Kashubian: "csb",
  Ladino: "lad",
  "Pennsylvania German": "pdc",
  Uyghur: "ug",
  Cornish: "kw",
  Sinhalese: "si",
  "Anglo-Saxon": "ang",
  Hawaiian: "haw",
  Tongan: "to",
  Sardinian: "sc",
  "Central_Bicolano": "bcl",
  Komi: "kv",
  Punjabi: "pa",
  Pashto: "ps",
  Silesian: "szl",
  Interlingue: "ie",
  Malagasy: "mg",
  Guarani: "gn",
  Lingala: "ln",
  Burmese: "my",
  "Fiji Hindi": "hif",
}, "^_");

for (let ntl of [noun_type_lang_google, noun_type_lang_wikipedia]) {
  ntl._code2name = ntl._list.reduce(function (o, s) {
    o[s.data] = s.text;
    return o;
  }, {});
  ntl.getLangName = function getLangName(langCode) this._code2name[langCode];
  ntl.noSelection = true;
}
{
  let locale =
    Utils.prefs.getValue("general.useragent.locale", "en").slice(0, 2);
  let langName = noun_type_lang_wikipedia.getLangName(locale);
  if (langName)
    noun_type_lang_wikipedia.default.push(
      CmdUtils.makeSugg(langName, null, locale));
}

function NounAsync(label, checker) {
  return {
    label: label,
    suggest: function asyncSuggest(text, html, callback, selectionIndices) {
      return [checker(text, callback, selectionIndices)];
    },
  };
}

// === {{{ noun_type_geo_country }}} ===
// === {{{ noun_type_geo_region }}} ===
// === {{{ noun_type_geo_subregion }}} ===
// === {{{ noun_type_geo_town }}} ===
// === {{{ noun_type_geo_postal }}} ===
// === {{{ noun_type_geo_address }}} ===

var noun_type_geo_country = NounGeo("country", 1, 1);
// Think American states, provinces in many countries, Japanese prefectures.
var noun_type_geo_region = NounGeo("region", 2, 2);
var noun_type_geo_subregion = NounGeo("subregion", 3, 3);
var noun_type_geo_town = NounGeo("city/town", 4, 4);
var noun_type_geo_postal = NounGeo("postal code", 5, 5);
var noun_type_geo_address = NounGeo("address", 6, 9);

// http://code.google.com/intl/ja/apis/maps/documentation/reference.html
// #GGeoAddressAccuracy
function NounGeo(label, minAccuracy, maxAccuracy) ({
  label: label,
  suggest: function geo_suggest(text, html, callback)
    [getGeo(text, callback, minAccuracy, maxAccuracy)],
});
function getGeo(query, callback, minAccuracy, maxAccuracy) jQuery.ajax({
  url: "http://maps.google.com/maps/geo",
  data: {
    q: query,
    output: "json",
    oe: "utf8",
    sensor: "false",
    key: ("ABQIAAAAzBIC_wxmje-aKLT3RzZx7BQFk1cXV-t8vQsDjF" +
          "X6X7KZv96YRxSFucHgmE5u4oZ5fuzOrPHpaB_Z2w"),
  },
  dataType: "json",
  success: function ggeo_success(data) {
    if (data.Status.code !== 200 || !(data.Placemark || "").length) return;

    var results = [];
    for (let result of data.Placemark) {
      // if there are no AddressDetails, it has no accuracy value either
      // so let's ignore it.
      if (!result.AddressDetails) continue;

      // matchScore() isn't suitable, as the API allows ambiguous matching.
      // e.g.:  nrth -> north / can -> Çan / akihabara -> 秋葉原
      let score = accuracyScore(result.AddressDetails.Accuracy,
                                minAccuracy, maxAccuracy);
      results.push(formatGooglePlacemark(result, score));
    }
    callback(results);
  }
});

function accuracyScore(score, minAccuracy, maxAccuracy) (
  score < minAccuracy
  ? Math.pow(0.8, minAccuracy - score) :
  score > maxAccuracy
  ? Math.pow(0.8, score - maxAccuracy)
  : 1);

function formatGooglePlacemark(placemark, score) {
  var data = placemark.AddressDetails;
  [data.lon, data.lat] = placemark.Point.coordinates;
  return CmdUtils.makeSugg(placemark.address, null, data, score);
}

var EXPORTED_SYMBOLS = (
  [it for (it in new Iterator(this)) if (/^noun_/.test(it[0]))]
  .map(function ([sym, noun], i) {
    noun.id = "#" + sym;
    noun.name = /^noun_(?:type_)?(.*)/.exec(sym)[1];
    return sym;
  }, this));

// ** DEPRECATED ** \\
// {{{noun_type_language}}}\\
// {{{noun_type_commands}}}\\
// {{{noun_type_emailservice}}}\\
// {{{noun_type_searchengine}}}
for (let [old, now] in new Iterator({
  language: noun_type_lang_google,
  commands: noun_type_command,
  emailservice: noun_type_email_service,
  searchengine: noun_type_search_engine,
  async_address: noun_type_geo_address,
})) {
  let sym = "noun_type_" + old;
  this[sym] = now;
  EXPORTED_SYMBOLS.push(sym);
}
