// A dependency, from Douglas Crockford's "JavaScript: The Good Parts"
if (typeof Object.create !== 'function') { Object.create = function (o) { var F = function () {}; F.prototype = o; return new F(); }; }

///////////
// Nouns //
///////////

// Examples of specific noun types
// These aren't currently used in any commands, they're just examples

var noun_type_tv_program = freebaseNounType( "tv program", [ "/tv/tv_program" ] );
var noun_type_person = freebaseNounType( "person", [ "/people/person" ] );
var noun_type_us_state = freebaseNounType( "US state", [ "/location/us_state" ] );
var noun_type_company = freebaseNounType( "company", [ "/business/company" ] );

var noun_type_freebase_type = freebaseNounType( "type", [ "/type/type" ] );


// freebaseNounType "factory"
//////////////////////////////////////////////////////////////////
// Builds Ubiquity noun types based on specified Freebase types

function freebaseNounType( name, typeFilter ) {
  var that, onPause;
  
  onPause = new Waiter( 400 );
  
  that = {
    _name: name,
    suggest: function suggest( text, html, callback ) {
      onPause.please( function() { freebaseNounType.manager.getSuggestions( text, callback, { typeFilter: typeFilter } ) } );
      return [];
    }
  }
  
  return that;
}

freebaseNounType.manager = freebaseNounTypeManager();


// noun_type_freebase_entity
/////////////////////////////////////////////////////////////
// A "generic" Ubiquity noun type for Freebase entities.
// Matches across all Freebase types, but has some built-in
// parsing to allow natural-language-based narrowing by type
// within a noun phrase. E.g., "the actor clooney", "clooney the actor",
// "an actor named clooney", "actor: clooney", "clooney, actor".
// If you want type suggestions, you can start typing a potential
// type name, add a colon, and then pause for a sec. E.g., typing
// "music:" and then pausing yields suggestions like "Musical Group:",
// "Musical Release:", "Musical Album:", etc.

var noun_type_freebase_entity = ( function() {
  var that, onPause;
  
  onPause = new Waiter( 400 );
  
  that = {
    _name: "entity",
    suggest: function suggest( text, html, callback ) {
      onPause.please( function() {
        var parsings;
        
        parse();
        getSuggestions();
        
        function parse() {
          var quoted, endsWithSeparator, anXCalledY, typePrepended, typeAppended,
              articles, words, i, startsWithArticle, endsWithArticle,
              end, suffix, type, entityText, lastArticle;
              
          parsings = {};
          
          quoted = text.match( /^(?:"|')(.+)(?:"|')$/i );
          if ( quoted ) {
            addParsing( "", quoted[ 1 ] );
            return;
          }
          
          endsWithSeparator = text.match( /^(.+?)\s*(?:\:|\-+|\,)\s*$/i );
          if ( endsWithSeparator ) {
            addParsing( endsWithSeparator[ 1 ], "" );
            return;
          }
          
          addParsing( "", text );
          
          anXCalledY = text.match( /^(?:a|an|the) (.+) (?:named|called) (.+)$/i );
          if ( anXCalledY ) {
            addParsing( anXCalledY[ 1 ], anXCalledY[ 2 ] );
          }
          
          typePrepended = text.match( /^(.+?)\s*(?:\:|\-+|\,)\s*(.+)$/i );
          if ( typePrepended ) {
            addParsing( typePrepended[ 1 ], typePrepended[ 2 ] );
          }
          
          typeAppended = text.match( /^(.+)\s*(\((.+)\)|(?:\:|\-+|\,)\s*(.+?))$/i );
          if ( typeAppended ) {
            addParsing( typeAppended[ 3 ] || typeAppended[ 4 ], typeAppended[ 1 ] );
          }
          
          articles = [];
          words = text.split( " " );
          
          for ( i = 0; i < words.length; i++ ) {
            if ( words[i].match( /\b(a|an|the)\b/i ) ) {
              articles.push( i );
              if ( i === 0 ) { startsWithArticle = true };
              if ( i === words.length - 1 ) { endsWithArticle = true };
            }
          }
          
          if ( startsWithArticle ) {
            if ( articles[ 1 ] ) {
              end = articles[ 1 ] + 1;
              suffix = " " + words.slice( end ).join( " " );
            }
            else {
              end = words.length;
              suffix = "";
            }
            for ( i = 2; i < end; i++ ) {
              type = words.slice( 1, i ).join( " " );
              entityText = words.slice( i, end ).join( " " ) + suffix;
              addParsing( type, entityText );
            }
          }
          
          if ( articles.length > 1 || !startsWithArticle && articles.length ) {
            lastArticle = articles[ articles.length - 1 ];
            type = words.slice( lastArticle + 1, words.length ).join( " " );
            entityText = words.slice( 0, lastArticle ).join( " " );
            addParsing( type, entityText );
          }
        }
        
        function addParsing( type, text ) {
          if ( parsings[ type ] === undefined ) {
            parsings[ type ] = {};
          }
          parsings[ type ][ text ] = true;
        }
        
        function getSuggestions() {
          var reqId, entityText, untypedRequests, untypedRequestsQueued, parsing, typeSearchParams;
          reqId = new Date().getTime();
          
          function makeSuccessFn( entityText ) {
            var singleton;
            
            // Handle case where we want to force a type search (user text ended with a type separator, and we have no entity text)
            if ( entityText.__count__ === 1 ) {
              for ( singleton in entityText ) {
                if ( !singleton ) {
                  return function success( freebaseResponse ) {
                    var results, i, suggText;
                    
                    results = freebaseResponse.result;
                                       
                    for ( i = 0; i < results.length && i < freebaseNounType.manager.getMaxSuggestions(); i++ ) {
                      result = results[i];
                      if ( result.id.match( /^\/user/ ) ) { continue; }
                      suggText = result.name + ": ";
                      callback( CmdUtils.makeSugg( suggText, suggText, result ) );
                    }
                  }
                }
              } 
            }
            
            // Now the "normal" case
            return function success( freebaseResponse ) {
              var results, types, typeInfo, i, result, variation, maxSuggestions;
              
              results = freebaseResponse.result;
              if ( results.length === 0 ) { return; }

              types = [];
              typeInfo = {};
              
              for ( i = 0; i < results.length; i++ ) {
                result = results[i];
                if ( result.id.match( /^\/user/ ) ) { continue; }
                typeInfo[ result.id ] = result;
                types.push( result.id );
              }
              
              function beforeSuggest( typeInfo ) {
                return function incorporateTypeInfo( entity ) {
                  var type, idx, maxTypeScore, bestType;
                  
                  // Adjust score to factor in type
                  maxTypeScore = 1.0; // Making > 0 just in case we don't find a matching type in the loop below. Shouldn't happen.
                  for ( type in typeInfo ) {
                    for ( idx in entity.type ) {
                      if ( entity.type[ idx ].id === type ) {
                        if ( typeInfo[ type ][ "relevance:score" ] > maxTypeScore ) {
                          maxTypeScore = typeInfo[ type ][ "relevance:score" ];
                          bestType = type;
                        }
                      }
                    }
                  }
                  entity[ "relevance:score" ] = ( entity[ "relevance:score" ] + ( 3 * maxTypeScore ) ) / 4;
                  
                  // Create suggestion text, with type name prepended
                  entity.suggest = typeInfo[ bestType ].name + ": " + entity.name;
                  
                  return entity;
                }
              }

              maxSuggestions = types.length * freebaseNounType.manager.getMaxSuggestions();

              for ( variation in entityText ) {
                freebaseNounType.manager.getSuggestions( variation, callback, { typeFilter: types, maxSuggestions: maxSuggestions, id: reqId, beforeSuggest: beforeSuggest( typeInfo ), deDup: true } );
              }
              
              if ( untypedRequestsQueued ) {
                for ( i = 0; i < untypedRequests.length; i++ ) {
                  untypedRequests[ i ]();
                }
                untypedRequestsQueued = false;
              }
            }
          }
          
          
          untypedRequests = [];
          untypedRequestsQueued = false;

          for ( type in parsings ) {
            if ( !type ) { 
              for ( entityText in parsings[ type ] ) {
                if ( parsings.__count__ > 1 ) {
                  untypedRequests.push(
                    ( function( entityText ) {
                      return function() { freebaseNounType.manager.getSuggestions( entityText, callback, { id: reqId, deDup: true } ); };
                    } )( entityText )
                  );
                  untypedRequestsQueued = true;
                }
                else {
                  freebaseNounType.manager.getSuggestions( entityText, callback, { id: reqId, deDup: true } );
                }
              }
              continue;
            }

            typeSearchParams = {
              prefix: type,
              typeFilter: [ "/freebase/type_profile" ],
              success: makeSuccessFn( parsings[ type ] )
            };

            Freebase.search( typeSearchParams );
          }
        }
      } );
      return [];
    }
  }
  
  return that;
} )();

// freebaseNounTypeManager
//////////////////////////////////////////////////////////////////
// Coordinates multiple, simultaneous Freebase noun requests,
// allowing them to be relevance-sorted together and returned in
// predictable order

function freebaseNounTypeManager() {
  var that, maxSuggestions, openRequests;
  
  that = {};
  maxSuggestions = 5;
  openRequests = {}; 
  
  function request( id, deDup ) {
    var that, subRequests, subRequestsClosed, afterLastSubRequest, suggestions, closed, suggested;
    
    that = {};
    subRequests = 0;
    subRequestsClosed = 0;
    afterLastSubRequest = new Waiter( 100 );
    suggestions = [];
    closed = false;
    suggested = false;
    
    function queueSuggestion( result, callback ) {
      var suggestText, suggestion;
      suggestText = result.suggest || result.name;
      suggestion = function suggestion() { callback( CmdUtils.makeSugg( suggestText, suggestText, result ) ); };
      suggestion.score = result[ "relevance:score" ];
      suggestion.guid = result.guid;
      suggestions.push( suggestion );
    }
    
    function makeSuggestions() {
      var suggGuids = {};
      suggestions = suggestions.sort( function( a, b ) {
        var scoreA = parseFloat( a.score );
        var scoreB = parseFloat( b.score );
        if ( scoreA > scoreB ) {
          return -1;
        }
        if ( scoreA < scoreB ) {
          return 1;
        }
        return 0;
      } );

      for ( var i = 0; i < suggestions.length && i < maxSuggestions; i++ ) {
        if ( deDup && suggGuids[ suggestions[i].guid ] ) { continue; }
        suggestions[ i ]();
        suggGuids[ suggestions[ i ].guid ] = true;
      } 
      
      suggested = true;
    }
    
    function close() {
      delete openRequests[ id ];
      closed = true;
      if ( !suggested && openSubRequests() === 0 ) {
        makeSuggestions();
      }
    }
    
    function openSubRequest() {
      subRequests++;
      afterLastSubRequest.please( close );
    }
    
    function closeSubRequest() {
      subRequestsClosed ++;
      if ( closed && openSubRequests() === 0 ) {
        makeSuggestions();
      }
    }
    
    function openSubRequests() {
      return subRequests - subRequestsClosed;
    }

    that.queueSuggestion = queueSuggestion;
    that.openSubRequest = openSubRequest;
    that.closeSubRequest = closeSubRequest;
    
    openRequests[ id ] = that;
    return that;
  }
  
  // options: typeFilter, maxSuggestions, id, beforeSuggest, deDup
  function getSuggestions( text, callback, options ) {
    var id;
    
    options = options || {};
    id = options.id || text;

    var req = openRequests[ id ] || request( id, options.deDup );
    req.openSubRequest();
    var params = {
      prefix: text,
      limit: options.maxSuggestions || maxSuggestions,
      success: function success( freebaseResponse ) {
        var results = freebaseResponse.result;
        for ( var idx = 0; idx < results.length; idx++ ) {
          var result = options.beforeSuggest ? options.beforeSuggest( results[idx] ) : results[ idx ];
          req.queueSuggestion( result, callback );
        }
        req.closeSubRequest();
      }
    };
    if ( options.typeFilter && options.typeFilter.length ) {
      params.typeFilter = options.typeFilter;
    }
    Freebase.search( params );
  }
  
  that.getSuggestions = getSuggestions;
  that.getMaxSuggestions = function getMaxSuggestions() { return maxSuggestions; }
  
  return that;
}


//////////////
// Commands //
//////////////

var About = {
  meta: {}, // Used to share data between preview and execute functions
  
  preview: function preview( container, entity ) {
    if ( entity.data === undefined || entity.data === null )
      return;
    
    var e = entity.data;
    
    About.meta.content = {
      name: e.name,
      freebaseId: e.id,
      wikipediaPage: null,
      status: null,
      description: "",
      imageUrl: ( e.image === undefined || e.image === null) ?
                null :
                Freebase.imageThumbUrl( {
                  id: e.image.id,
                  maxwidth: 225,
                  maxheight: 225
                } ),
      types: buildTypeList()
    };
    
    render();
    
    if ( e.article === undefined || e.article === null || !e.article ) {
      About.meta.content.description = "<em>No description available.</em>";
      render();
    }
    else {
      Freebase.blurb( {
        id: e.article.id,
        maxlength: 500,
        success: function( response ) {
          About.meta.content.description = response;
          render();
        },
        container: container
      } );
      
      Freebase.mqlRead( {
        query: {
          id: e.id,
          key: [ { namespace: "/wikipedia/en", value: null, limit: 1 } ]
        },
        success: function success( r ) {
          About.meta.content.wikipediaPage = r.response.result.key[0].value;
          render();
        }
      } );
    }
    
    // borrowed from Will Moffat's freebase command (http://hamstersoup.com/freebase/ubiquity/ubiquity.js)
    function buildTypeList() {
      if ( !e.type || !e.type.length ) { return ''; }
      var major_types = jQuery.grep( e.type, function( t ) {
            if ( t.id.indexOf('/common/') === 0 || t.id.indexOf( '/user/' ) === 0 ) { return false; } // ignore
            return true;
      } );
      var names = jQuery.map( major_types, function( t ) { return t.name || '??'; } );

      return names.join( ', ' );
    }
    
    function render() {
      container.innerHTML = CmdUtils.renderTemplate( { file: "about-preview.html" }, About.meta.content );
    }
  },
  
  execute: function execute( entity ) {
    var url = ( About.meta.content.wikipediaPage ) ?
              "http://en.wikipedia.org/wiki/" + About.meta.content.wikipediaPage :
              "http://www.freebase.com/view" + About.meta.content.freebaseId;
    Utils.openUrlInBrowser( url );
  }
}

// Preview template

var Attachments = {
  "about-preview.html" : '\
<style>\
  #wrapper { font-size: 12px; font-family:  Calibri, Arial, sans-serif; background: white; padding: 1em 0 0 0; color: Black; }\
  h2 { margin: 0 0 0 0.67em; font-size: 1.5em; }\
  h3 { color: #888; font-size: 1em; font-weight: normal; margin: 0 0 1em 1em; }\
  p { margin-left: 1em; }\
  .thumb { float: right; padding: 0 1em 1em 1em; }\
  #attr { clear: both; text-align: center; padding: 0.5em; background: #DDD; border-top: 2px solid #CCC; }\
  #attr img { vertical-align: middle; border: none; margin: 0 0.3em; }\
  #status { padding: 0.5em 1em; background: #FFE; color: #777; }\
  {if status === null}\
  #status { display: none; }\
  {/if}\
</style>\
<div id="wrapper">\
    {if imageUrl !== null}\
    <img class="thumb" src="${imageUrl}" title="${name}"/>\
    {/if}\
    <h2>${name}</h2>\
    {if types !== null}\
    <h3>${types}</h3>\
    {/if}\
    {if description !== null}\
    <p>${description}</p>\
    {/if}\
    <div id="attr">\
        <a href="http://en.wikipedia.org/wiki/${wikipediaPage}" title="Wikipedia: ${name}">\
          <img src="http://upload.wikimedia.org/wikipedia/en/5/5f/Wikipedia_button_80x15.png" />\
        </a>\
        <a href="http://www.freebase.com/view${freebaseId}" title="Freebase: ${name}">\
          <img src="http://www.freebase.com/api/trans/raw/freebase/attribution" />\
        </a>\
    </div>\
    {if status !== null}\
    <div id="status">${status}</div>\
    {/if}\
</div>\
'
}

CmdUtils.CreateCommand({
  name: "about",
  takes: { "entity": noun_type_freebase_entity },
  preview: About.preview,
  execute: About.execute,
  author: { name: "Gray Norton", email: "gray@stanfordalumni.org" },
  description: "Shows a \"baseball-card-style\" overview for any Freebase topic.",
  help: "<ul><li>An experiment in Freebase-derived nouns. Suggests topics as you type.</li><li>Overview is shown in the Ubiquity preview, along with links to Wikipedia and Freebase. Hitting 'Enter' goes to Wikipedia.</li><li>Examples: <em>about skiing</em>, <em>about washington, person</em>, <em>about musical group: boys</em>, <em>about us state: nc</em></li></ul>",
  license: "MPL"
});


// An experiment to see how commands with multiple
// dynamic nouns might work. Suffers from some known Ubiquity limitations
// (e.g., lack of support for multi-word async modifiers, need for enhanced
// suggestion scoring mechanism, etc.). Note that there's no actual
// comparison functionality (the modifier is ignored), since this was
// just a test of the multi-noun matching.

/*
CmdUtils.CreateCommand({
  name: "compare",
  takes: { "entity": noun_type_freebase_entity },
  modifiers: { to: noun_type_tv_program }, // wanted to use another freebase_entity, but didn't work -- haven't yet explored why not
  preview: About.preview,
  execute: About.execute
});
*/

//////////////////////////////
// Supporting functionality //
//////////////////////////////

// Freebase
///////////////////////////////////////////////////////////////////
// Reusable Freebase functionality, wrapping Freebase API calls

var Freebase = {
  _decodeWin1252: function _decodeWin1252( str ) {
    return str.replace( /\$([0-9A-F]{4})/g, 
                        function( whole, val ) {
                          return String.fromCharCode( parseInt( val, 16 ) );
                        } );
  },
  
  wrapQuery: function wrapQuery( query, queryLabel ) {
    if ( !queryLabel ) {
      queryLabel = "response";
    }

    var inner = { "query" : query };
    var outer = {};
    outer[ queryLabel ] = inner;
    return outer;
  },
  
  // id*, maxwidth, maxheight, mode, onfail
  imageThumbUrl: function imageThumbUrl( params ) {
    if ( params.id === undefined ) { return ""; }
    var url = "http://www.freebase.com/api/trans/image_thumb/" + params.id + "?";
    for ( var param in params ) {
      if ( param === "id" ) { continue; }
      url += param + "=" + encodeURIComponent( params[ param ] ) + "&";
    }
    return url;
  },
  
  // id*, maxlength, break_paragraphs
  blurb: new UbiquityAjaxFn( {
    name: "Freebase blurb",
    url: "http://www.freebase.com/api/trans/blurb",
    args: { maxlength: 0, break_paragraphs: 0 },
    dataType: "text",
    beforeBuild: function() {
      this.url += this.id;
      delete this.id;
    }
  } ),
  
  // (query|prefix)*, type, limit
  search: new UbiquityAjaxFn( {
    name: "Freebase search",
    url: "http://www.freebase.com/api/service/search",
    args: { query: 1, prefix: 1, typeFilter: 0, limit: 0 },
    beforeBuild: function beforeBuild() {
      if ( this.data.typeFilter !== undefined ) {
        this.data.type = this.data.typeFilter;
        delete this.data.typeFilter;
      }
    }
  } ),

  // query*
  mqlRead: new UbiquityAjaxFn( {
    name: "Freebase mqlRead",
    type: "POST",
    url: "http://www.freebase.com/api/service/mqlread",
    beforeBuild: function() {
      this.data = "queries=" + Utils.encodeJson( Freebase.wrapQuery( this.query ) );
      delete this.query;
    },
    dataFilter: function( data, type ) {
      return Freebase._decodeWin1252( data );
    }
  } )
}


// UbiquityAjaxFn
//////////////////////////////////////////////////////////////////////////////
// Builds Ajax functions for use in Ubiquity nouns and previews: sets some
// defaults, handles caching and calls CmdUtils.previewAjax when applicable

function UbiquityAjaxFn( defParams ) {
  var defaults = {
    name: "Ajax",
    args: {},
    error: function error( XMLHttpRequest, textStatus, errorThrown ) {
      CmdUtils.log( p.name + " error: " + textStatus );
    },
    success: function success( data, textStatus ) {
      var responseDisplay = ( typeof data === "Object" ) ? Utils.encodeJson( data ) : data;
      CmdUtils.log( p.name + " response: " + responseDisplay );
    },
    dataType: "json",
    cache: true
  }

  function _processArgs() {
    if ( this.data === undefined ) {
      this.data = {};
    }
    for ( var arg in this.args ) {
      if ( this[ arg ] !== undefined ) {
        this.data[ arg ] = this[ arg ];
        delete this[ arg ];
      }
    }
  }
  
  var p = Object.create( defaults );
  for ( var key in defParams ) {
    p[ key ] = defParams[ key ];
  }
  
  var cache = ( p.cache ) ? new Cache() : null;
  
  var fn = function( runParams ) { 
    var r = Object.create( p );
    var cacheSignature = {};
    for ( var key in runParams ) {
      r[ key ] = runParams[ key ];
      // Use runtime params as keys for the cache, but omit functions and DOM elements
      if ( typeof runParams[ key ] !== "function" && runParams[ key ].nodeType === undefined ) {
        cacheSignature[ key ] = runParams[ key ];
      }
    }
    
    _processArgs.call( r );
    
    if ( r.beforeBuild !== undefined ) {
      r.beforeBuild.call( r );
    }
    
    if ( cache && r.cache ) {
      var cachedResponse = cache.read( cacheSignature );
      if ( cachedResponse ) {
        return r.success( cachedResponse );
      }
      var origSuccess = r.success;
      r.success = function success( data, textStatus ) {
        cache.write( cacheSignature, data );
        origSuccess( data, textStatus );
      }
    }
    
    if ( r.container === undefined ) {
      jQuery.ajax( r );
    }
    else {
      CmdUtils.previewAjax( r.container, r );
    }
  }
  
  return fn;
}


// Cache
/////////////////////////////////////////
// Simple cache, used by UbiquityAjaxFn

function Cache() {
  var _cache = {};
  
  function _key( request ) {
    if ( typeof request === "object" ) {
      var key = Utils.encodeJson( request );
      return key;
    }
    return query;
  }
  
  this.write = function write( request, response ) {
    _cache[ _key( request ) ] = response;
  }
  
  this.read = function read( request ) {
    var response = _cache[ _key( request ) ];
    return response;
  }
}


// Waiter
/////////////////////////////////////////////////////////////
// Used by Freebase nouns to wait for a pause in typing so
// that we don't pepper Freebase with unnecessary requests,
// and to wait for suggestions to stop coming back before
// returning them from the queue

function Waiter( delay ) {
  if ( !delay ) { delay = 300; }
  var timer = null;
  
  this.please = function please( fn ) {
    if (timer) {
      Utils.clearTimeout( timer );
    }
    
    timer = Utils.setTimeout ( function() {
      timer = null;
      fn();
    }, delay );
  }
}

//////////////////////
// Currently Unused //
//////////////////////

/*

// Was doing my own Freebase noun search before discovering Freebase's search service.
// With a bit more work, might still be useful, especially for generating
// nouns that need to differentiate based on something more than just name
// and type.

    _get: function _get( callback, typeFilter, customParams ) {
      var params = {
        name: null,
        id: null,
        type: [ {
          attribution : "/user/metaweb",
          id: null,
          name: null
        } ],
        key: [ {
          // Was using "/wikipedia/en_id", but that required an extra Ajax request
          // to get the page name, so trying "/wikipedia/en"[0] on the (unverified)
          // assumption that the first Wikipedia page in each Freebase
          // record is the canonical page
          namespace: "/wikipedia/en",
          value: null,
          limit: 1
        } ],
        limit: this._maxSuggestions
      };
      if ( typeFilter !== undefined ) {
        for ( var idx = 0; idx++; idx < typeFilter.length ) {
          params[ "f" + idx + ":type" ] = typeFilter[ idx ];
        }
      }
      for ( var key in customParams ) {
        params[ key ] = customParams[ key ];
      }
      var query = [ params ];
      Freebase.mqlRead( { 
        query: query,
        success: callback
      } );
    },
    
    getTopExactMatch: function getTopExactMatch( text, callback, typeFilter ) {
      var params = { "name~=": "^" + text.replace(/ /g, "\\ ") + "$", limit: 1 };
      this._get( callback, typeFilter, params );
    },
    
    getExactMatches: function getExactMatches( text, callback, typeFilter ) {
      var params = { name: text };
      this._get( callback, typeFilter, params );
    },

    getCompletions: function getCompletions( text, callback, typeFilter ) {
      var params = { "name~=": "^" + text + "*" };
      this._get( callback, typeFilter, params );
    },

    getSimilar: function getSimilar( text, callback, typeFilter ) {
      var params = { "name~=": "*" + text + "*" };
      this._get( callback, typeFilter, params );
    },
    
    getSuggestions: function getSuggestions( text, callback, typeFilter ) {
      var that = this;
      var methods = [ this.getTopExactMatch, this.getCompletions, this.getSimilar ];
      var currentMethod = 0;
      var suggested = {}, numSuggested = 0;
      
      tryMethod();
      
      function tryMethod() {
        methods[ currentMethod ].call( that, text, processResults, typeFilter);
        
        function processResults( freebaseResponse ) {
          var results = freebaseResponse.response.result;
          for ( var idx in results ) {
            var result = results[ idx ];
            if ( suggested[ result.id ] === undefined ) {
              callback( CmdUtils.makeSugg( result.name, result.name, result ) );
              suggested[ result.id ] = true;
              if ( ++numSuggested === that._maxSuggestions) return;
            }
          }
          if ( numSuggested < that._maxSuggestions && ++currentMethod < methods.length ) tryMethod();
        }
      }
    }


// Was getting description and image from Wikipedia OpenSearch before discovering Freebase search

    function extractDescAndImage( results ) {
      const maxDim = 150;
      if (!results.Section.Item) {
        About.meta.content.description = "";
      }
      else {
        var item = ( results.Section.Item.Description ) ? results.Section.Item : results.Section.Item[0];
        About.meta.content.description = item.Description;
        if ( item.Image ) {
          var aspectRatio = item.Image.width / item.Image.height;
          var targetWidth, targetHeight;
          if ( aspectRatio > 1 ) {
            targetWidth = maxDim;
            targetHeight = Math.round( targetWidth / aspectRatio );
          }
          else {
            targetHeight = maxDim;
            targetWidth = Math.round( targetHeight * aspectRatio );
          }
          item.Image.source = item.Image.source.replace( /[0-9]+px/, targetWidth + "px" );
          About.meta.content.image = {
            src: item.Image.source,
            height: targetHeight,
            width: targetWidth
          }
        }
      }
      About.meta.content.status = null;
      render();    
    }

// Shell for some reusable Wikipedia functionality, just OpenSearch so far.

var Wikipedia = {
  _openSearchCache: new Cache(),
  
  openSearch: new UbiquityAjaxFn( {
    name: "Wikipedia OpenSearch",
    url: "http://en.wikipedia.org/w/api.php",
    args: { search: 1 },
    dataType: "xml",
    data: {
      action: "opensearch",
      format: "xml" // Asking for XML because it contains info not included in the JSON version (image, abstract, etc.)
    },
    beforeBuild: function beforeBuild() {
      var origSuccess = this.success;
      this.success = function success( data, textStatus ) {
        var json = jQuery.xml2json( data );
        origSuccess( json, textStatus );
      }
    }
  } )
}

// Dependency for Wikipedia OpenSearch implementation above

jQuery.get( "http://www.fyneworks.com/jquery/xml-to-json/jquery.xml2json.js",
            null,
            function( script ) {
              eval( script ); // using eval because jQuery.getScript doesn't seem to work in Ubiquity Command sandbox
            },
            "text" );
*/


// Some old commands

/*
CmdUtils.CreateCommand({
  name: "about-tv-program",
  synonyms: [ "about-tv-show" ],
  takes: { "entity": noun_type_tv_program },
  preview: About.preview,
  execute: About.execute,
  author: { name: "Gray Norton", email: "gray@stanfordalumni.org" },
  description: "Shows a \"baseball-card-style\" overview for any TV program. (See <strong>about</strong> for more...)",
  license: "MPL"
});

CmdUtils.CreateCommand({
  name: "about-person",
  takes: { "entity": noun_type_person },
  preview: About.preview,
  execute: About.execute,
  author: { name: "Gray Norton", email: "gray@stanfordalumni.org" },
  description: "Shows a \"baseball-card-style\" overview for any person. (See <strong>about</strong> for more...)",
  license: "MPL"
});

CmdUtils.CreateCommand({
  name: "about-us-state",
  takes: { "entity": noun_type_us_state },
  preview: About.preview,
  execute: About.execute,
  author: { name: "Gray Norton", email: "gray@stanfordalumni.org" },
  description: "Shows a \"baseball-card-style\" overview for any US state. (See <strong>about</strong> for more...)",
  license: "MPL"
});

CmdUtils.CreateCommand({
  name: "about-company",
  synonyms: [ "about-corporation" ],
  takes: { "entity": noun_type_company },
  preview: About.preview,
  execute: About.execute,
  author: { name: "Gray Norton", email: "gray@stanfordalumni.org" },
  description: "Shows a \"baseball-card-style\" overview for any company. (See <strong>about</strong> for more...)",
  license: "MPL"
});
*/