/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* ***** 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 Jetpack.
 *
 * The Initial Developer of the Original Code is
 * the Mozilla Foundation.
 * Portions created by the Initial Developer are Copyright (C) 2010
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Dietrich Ayala <dietrich@mozilla.com> (Original Author)
 *   Drew Willcoxon <adw@mozilla.com>
 *   Irakli Gozalishvili <gozala@mozilla.com>
 *   Alexandre Poirot <apoirot@mozilla.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 ***** */

"use strict";

const {Cc, Ci} = require("chrome");

// Widget content types
const CONTENT_TYPE_URI    = 1;
const CONTENT_TYPE_HTML   = 2;
const CONTENT_TYPE_IMAGE  = 3;

const ERR_CONTENT = "No content or contentURL property found. Widgets must "
                         + "have one or the other.",
      ERR_LABEL = "The widget must have a non-empty label property.",
      ERR_ID = "You have to specify a unique value for the id property of " +
               "your widget in order for the application to remember its " +
               "position.",
      ERR_DESTROYED = "The widget has been destroyed and can no longer be used.";

// Supported events, mapping from DOM event names to our event names
const EVENTS = {
  "click": "click",
  "mouseover": "mouseover",
  "mouseout": "mouseout",
};

if (!require("api-utils/xul-app").is("Firefox")) {
  throw new Error([
    "The widget module currently supports only Firefox.  In the future ",
    "it will support other applications. Please see ",
    "https://bugzilla.mozilla.org/show_bug.cgi?id=560716 for more information."
  ].join(""));
}

const { validateOptions } = require("api-utils/api-utils");
const panels = require("./panel");
const { EventEmitter, EventEmitterTrait } = require("api-utils/events");
const { Trait } = require("api-utils/traits");
const LightTrait = require('api-utils/light-traits').Trait;
const { Loader, Symbiont } = require("api-utils/content");
const timer = require("api-utils/timer");
const { Cortex } = require('api-utils/cortex');
const windowsAPI = require("./windows");
const unload = require("api-utils/unload");

// Data types definition
const valid = {
  number: { is: ["null", "undefined", "number"] },
  string: { is: ["null", "undefined", "string"] },
  id: {
    is: ["string"],
    ok: function (v) v.length > 0,
    msg: ERR_ID,
    readonly: true
  },
  label: {
    is: ["string"],
    ok: function (v) v.length > 0,
    msg: ERR_LABEL
  },
  panel: {
    is: ["null", "undefined", "object"],
    ok: function(v) !v || v instanceof panels.Panel
  },
  width: {
    is: ["null", "undefined", "number"],
    map: function (v) {
      if (null === v || undefined === v) v = 16;
      return v;
    },
    defaultValue: 16
  },
};

// Widgets attributes definition
let widgetAttributes = {
  label: valid.label,
  id: valid.id,
  tooltip: valid.string,
  width: valid.width,
  content: valid.string,
  panel: valid.panel
};

// Import data definitions from loader, but don't compose with it as Model
// functions allow us to recreate easily all Loader code.
let loaderAttributes = require("api-utils/content/loader").validationAttributes;
for (let i in loaderAttributes)
  widgetAttributes[i] = loaderAttributes[i];

widgetAttributes.contentURL.optional = true;

// Widgets public events list, that are automatically binded in options object
const WIDGET_EVENTS = [
  "click",
  "mouseover",
  "mouseout",
  "error",
  "message",
  "attach"
];

// `Model` utility functions that help creating these various Widgets objects
let model = {

  // Validate one attribute using api-utils.js:validateOptions function
  _validate: function _validate(name, suspect, validation) {
    let $1 = {};
    $1[name] = suspect;
    let $2 = {};
    $2[name] = validation;
    return validateOptions($1, $2)[name];
  },

  /**
   * This method has two purposes:
   * 1/ Validate and define, on a given object, a set of attribute
   * 2/ Emit a "change" event on this object when an attribute is changed
   *
   * @params {Object} object
   *    Object on which we can bind attributes on and watch for their changes.
   *    This object must have an EventEmitter interface, or, at least `_emit`
   *    method
   * @params {Object} attrs
   *    Dictionary of attributes definition following api-utils:validateOptions 
   *    scheme
   * @params {Object} values
   *    Dictionary of attributes default values
   */
  setAttributes: function setAttributes(object, attrs, values) {
    let properties = {};
    for (let name in attrs) {
      let value = values[name];
      let req = attrs[name];
      
      // Retrieve default value from typedef if the value is not defined
      if ((typeof value == "undefined" || value == null) && req.defaultValue)
        value = req.defaultValue;
      
      // Check for valid value if value is defined or mandatory
      if (!req.optional || typeof value != "undefined")
        value = model._validate(name, value, req);
      
      // In any case, define this property on `object`
      let property = null;
      if (req.readonly) {
        property = {
          value: value,
          writable: false,
          enumerable: true,
          configurable: false
        };
      }
      else {
        property = model._createWritableProperty(name, value);
      }
      
      properties[name] = property;
    }
    Object.defineProperties(object, properties);
  },

  // Generate ES5 property definition for a given attribute
  _createWritableProperty: function _createWritableProperty(name, value) {
    return {
      get: function () {
        return value;
      },
      set: function (newValue) {
        value = newValue;
        // The main goal of all this Model stuff is here:
        // We want to forward all changes to some listeners
        this._emit("change", name, value);
      },
      enumerable: true,
      configurable: false
    };
  },

  /**
   * Automagically register listeners in options dictionary
   * by detecting listener attributes with name starting with `on`
   *
   * @params {Object} object
   *    Target object that need to follow EventEmitter interface, or, at least, 
   *    having `on` method.
   * @params {Array} events
   *    List of events name to automatically bind.
   * @params {Object} listeners
   *    Dictionary of event listener functions to register.
   */
  setEvents: function setEvents(object, events, listeners) {
    for (let i = 0, l = events.length; i < l; i++) {
      let name = events[i];
      let onName = "on" + name[0].toUpperCase() + name.substr(1);
      if (!listeners[onName])
        continue;
      object.on(name, listeners[onName].bind(object));
    }
  }

};


/**
 * Main Widget class: entry point of the widget API
 *
 * Allow to control all widget across all existing windows with a single object.
 * Widget.getView allow to retrieve a WidgetView instance to control a widget
 * specific to one window.
 */
const WidgetTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
  
  _initWidget: function _initWidget(options) {
    model.setAttributes(this, widgetAttributes, options);
    
    browserManager.validate(this);
    
    // We must have at least content or contentURL defined
    if (!(this.content || this.contentURL))
      throw new Error(ERR_CONTENT);
    
    this._views = [];
    
    // Set tooltip to label value if we don't have tooltip defined
    if (!this.tooltip)
      this.tooltip = this.label;
    
    model.setEvents(this, WIDGET_EVENTS, options);
    
    this.on('change', this._onChange.bind(this));
    
    let self = this;
    this._port = EventEmitterTrait.create({
      emit: function () {
        let args = arguments;
        self._views.forEach(function(v) v.port.emit.apply(v.port, args));
      }
    });
    // expose wrapped port, that exposes only public properties. 
    this._port._public = Cortex(this._port);
    
    // Register this widget to browser manager in order to create new widget on
    // all new windows
    browserManager.addItem(this);
  },
  
  _onChange: function _onChange(name, value) {
    // Set tooltip to label value if we don't have tooltip defined
    if (name == 'tooltip' && !value) {
      // we need to change tooltip again in order to change the value of the 
      // attribute itself
      this.tooltip = this.label;
      return;
    }
    
    // Forward attributes changes to WidgetViews
    if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) {
      this._views.forEach(function(v) v[name] = value);
    }
  },

  _onEvent: function _onEvent(type, eventData) {
    this._emit(type, eventData);
  },
  
  _createView: function _createView() {
    // Create a new WidgetView instance
    let view = WidgetView(this);
    
    // Keep a reference to it
    this._views.push(view);
    
    // Emit an `attach` event with a WidgetView instance without private attrs
    this._emit("attach", view._public);
    
    return view;
  },
  
  // a WidgetView instance is destroyed
  _onViewDestroyed: function _onViewDestroyed(view) {
    let idx = this._views.indexOf(view);
    this._views.splice(idx, 1);
  },
  
  /**
   * Called on browser window closed, to destroy related WidgetViews
   * @params {ChromeWindow} window
   *         Window that has been closed
   */
  _onWindowClosed: function _onWindowClosed(window) {
    for each (let view in this._views) {
      if (view._isInChromeWindow(window)) {
        view.destroy();
        break;
      }
    }
  },
  
  /**
   * Get the WidgetView instance related to a BrowserWindow instance
   * @params {BrowserWindow} window
   *         BrowserWindow reference from "windows" module
   */
  getView: function getView(window) {
    for each (let view in this._views) {
      if (view._isInWindow(window)) {
        return view._public;
      }
    }
    return null;
  },
  
  get port() this._port._public,
  set port(v) {}, // Work around Cortex failure with getter without setter
                  // See bug 653464
  _port: null,
  
  postMessage: function postMessage(message) {
    this._views.forEach(function(v) v.postMessage(message));
  },

  destroy: function destroy() {
    if (this.panel)
      this.panel.destroy();
    
    // Dispatch destroy calls to views
    // we need to go backward as we remove items from this array in 
    // _onViewDestroyed
    for (let i = this._views.length - 1; i >= 0; i--)
      this._views[i].destroy();
    
    // Unregister widget to stop creating it over new windows
    // and allow creation of new widget with same id
    browserManager.removeItem(this);
  }

}));

// Widget constructor
const Widget = function Widget(options) {
  let w = WidgetTrait.create(Widget.prototype);
  w._initWidget(options);
  
  // Return a Cortex of widget in order to hide private attributes like _onEvent
  let _public = Cortex(w);
  unload.ensure(_public, "destroy");
  return _public;
}
exports.Widget = Widget;



/**
 * WidgetView is an instance of a widget for a specific window.
 *
 * This is an external API that can be retrieved by calling Widget.getView or
 * by watching `attach` event on Widget.
 */
const WidgetViewTrait = LightTrait.compose(EventEmitterTrait, LightTrait({
  
  // Reference to the matching WidgetChrome
  // set right after constructor call
  _chrome: null,
  
  // Public interface of the WidgetView, passed in `attach` event or in
  // Widget.getView
  _public: null,
  
  _initWidgetView: function WidgetView__initWidgetView(baseWidget) {
    this._baseWidget = baseWidget;
    
    model.setAttributes(this, widgetAttributes, baseWidget);
    
    this.on('change', this._onChange.bind(this));
    
    let self = this;
    this._port = EventEmitterTrait.create({
      emit: function () {
        if (!self._chrome)
          throw new Error(ERR_DESTROYED);
        self._chrome.update(self._baseWidget, "emit", arguments);
      }
    });
    // expose wrapped port, that exposes only public properties. 
    this._port._public = Cortex(this._port);
    
    this._public = Cortex(this);
  },
  
  _onChange: function WidgetView__onChange(name, value) {
    if (name == 'tooltip' && !value) {
      this.tooltip = this.label;
      return;
    }
    
    // Forward attributes changes to WidgetChrome instance
    if (['width', 'tooltip', 'content', 'contentURL'].indexOf(name) != -1) {
      this._chrome.update(this._baseWidget, name, value);
    }
  },

  _onEvent: function WidgetView__onEvent(type, eventData, domNode) {
    // Dispatch event in view
    this._emit(type, eventData);
    
    // And forward it to the main Widget object
    if ("click" == type || type.indexOf("mouse") == 0)
      this._baseWidget._onEvent(type, this._public);
    else
      this._baseWidget._onEvent(type, eventData);

    // Special case for click events: if the widget doesn't have a click
    // handler, but it does have a panel, display the panel.
    if ("click" == type && !this._listeners("click").length && this.panel)
      this.panel.show(domNode);
  },
  
  _isInWindow: function WidgetView__isInWindow(window) {
    return windowsAPI.BrowserWindow({
      window: this._chrome.window
    }) == window;
  },
  
  _isInChromeWindow: function WidgetView__isInChromeWindow(window) {
    return this._chrome.window == window;
  },
  
  _onPortEvent: function WidgetView__onPortEvent(args) {
    let port = this._port;
    port._emit.apply(port, args);
    let basePort = this._baseWidget._port;
    basePort._emit.apply(basePort, args);
  },
  
  get port() this._port._public,
  set port(v) {}, // Work around Cortex failure with getter without setter
                  // See bug 653464
  _port: null,
  
  postMessage: function WidgetView_postMessage(message) {
    if (!this._chrome)
      throw new Error(ERR_DESTROYED);
    this._chrome.update(this._baseWidget, "postMessage", message);
  },

  destroy: function WidgetView_destroy() {
    this._chrome.destroy();
    delete this._chrome;
    this._baseWidget._onViewDestroyed(this);
    this._emit("detach");
  }

}));

const WidgetView = function WidgetView(baseWidget) {
  let w = WidgetViewTrait.create(WidgetView.prototype);
  w._initWidgetView(baseWidget);
  return w;
}



/** 
 * Keeps track of all browser windows.
 * Exposes methods for adding/removing widgets
 * across all open windows (and future ones).
 * Create a new instance of BrowserWindow per window.
 */
let browserManager = {
  items: [],
  windows: [],

  // Registers the manager to listen for window openings and closings.  Note
  // that calling this method can cause onTrack to be called immediately if
  // there are open windows.
  init: function () {
    let windowTracker = new (require("api-utils/window-utils").WindowTracker)(this);
    unload.ensure(windowTracker);
  },

  // Registers a window with the manager.  This is a WindowTracker callback.
  onTrack: function browserManager_onTrack(window) {
    if (this._isBrowserWindow(window)) {
      let win = new BrowserWindow(window);
      win.addItems(this.items);
      this.windows.push(win);
    }
  },

  // Unregisters a window from the manager.  It's told to undo all 
  // modifications.  This is a WindowTracker callback.  Note that when
  // WindowTracker is unloaded, it calls onUntrack for every currently opened
  // window.  The browserManager therefore doesn't need to specially handle
  // unload itself, since unloading the browserManager means untracking all
  // currently opened windows.
  onUntrack: function browserManager_onUntrack(window) {
    if (this._isBrowserWindow(window)) {
      this.items.forEach(function(i) i._onWindowClosed(window));
      for (let i = 0; i < this.windows.length; i++) {
        if (this.windows[i].window == window) {
          this.windows.splice(i, 1)[0];
          return;
        }
      }
      
    }
  },
  
  // Used to validate widget by browserManager before adding it,
  // in order to check input very early in widget constructor
  validate : function (item) {
    let idx = this.items.indexOf(item);
    if (idx > -1)
      throw new Error("The widget " + item + " has already been added.");
    if (item.id) {
      let sameId = this.items.filter(function(i) i.id == item.id);
      if (sameId.length > 0)
        throw new Error("This widget ID is already used: " + item.id);
    } else {
      item.id = this.items.length;
    }
  },

  // Registers an item with the manager. It's added to all currently registered 
  // windows, and when new windows are registered it will be added to them, too.
  addItem: function browserManager_addItem(item) {
    this.items.push(item);
    this.windows.forEach(function (w) w.addItems([item]));
  },

  // Unregisters an item from the manager.  It's removed from all windows that 
  // are currently registered.
  removeItem: function browserManager_removeItem(item) {
    let idx = this.items.indexOf(item);
    if (idx > -1)
      this.items.splice(idx, 1);
  },

  _isBrowserWindow: function browserManager__isBrowserWindow(win) {
    let winType = win.document.documentElement.getAttribute("windowtype");
    return winType === "navigator:browser";
  }
};



/** 
 * Keeps track of a single browser window.
 *
 * This is where the core of how a widget's content is added to a window lives.
 */
function BrowserWindow(window) {
  this.window = window;
  this.doc = window.document;
}

BrowserWindow.prototype = {

  // Adds an array of items to the window.
  addItems: function BW_addItems(items) {
    items.forEach(this._addItemToWindow, this);
  },
  
  _addItemToWindow: function BW__addItemToWindow(baseWidget) {
    // Create a WidgetView instance
    let widget = baseWidget._createView();
    
    // Create a WidgetChrome instance
    let item = new WidgetChrome({
      widget: widget,
      doc: this.doc,
      window: this.window
    });
    
    widget._chrome = item;
      
    this._insertNodeInToolbar(item.node);
    
    // We need to insert Widget DOM Node before finishing widget view creation
    // (because fill creates an iframe and tries to access its docShell)
    item.fill();
  },
  
  _insertNodeInToolbar: function BW__insertNodeInToolbar(node) {
    // Add to the customization palette
    let toolbox = this.doc.getElementById("navigator-toolbox");
    let palette = toolbox.palette;
    palette.appendChild(node);

    // Search for widget toolbar by reading toolbar's currentset attribute
    let container = null;
    let toolbars = this.doc.getElementsByTagName("toolbar");
    let id = node.getAttribute("id");
    for (let i = 0, l = toolbars.length; i < l; i++) {
      let toolbar = toolbars[i];
      if (toolbar.getAttribute("currentset").indexOf(id) == -1)
        continue;
      container = toolbar;
    }
    
    // if widget isn't in any toolbar, add it to the addon-bar
    // TODO: we may want some "first-launch" module to do this only on very 
    // first execution
    if (!container) {
      container = this.doc.getElementById("addon-bar");
      // TODO: find a way to make the following code work when we use "cfx run":
      // http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#8586
      // until then, force display of addon bar directly from sdk code
      // https://bugzilla.mozilla.org/show_bug.cgi?id=627484
      if (container.collapsed)
        this.window.toggleAddonBar();
    }
    
    // Now retrieve a reference to the next toolbar item
    // by reading currentset attribute on the toolbar
    let nextNode = null;
    let currentSet = container.getAttribute("currentset");
    let ids = (currentSet == "__empty") ? [] : currentSet.split(",");
    let idx = ids.indexOf(id);
    if (idx != -1) {
      for (let i = idx; i < ids.length; i++) {
        nextNode = this.doc.getElementById(ids[i]);
        if (nextNode)
          break;
      }
    }
    
    // Finally insert our widget in the right toolbar and in the right position
    container.insertItem(id, nextNode, null, false);
    
    // Update DOM in order to save position if we remove/readd the widget
    container.setAttribute("currentset", container.currentSet);
    // Save DOM attribute in order to save position on new window opened
    this.window.document.persist(container.id, "currentset");
  }
}


/**
 * Final Widget class that handles chrome DOM Node:
 *  - create initial DOM nodes
 *  - receive instruction from WidgetView through update method and update DOM
 *  - watch for DOM events and forward them to WidgetView
 */
function WidgetChrome(options) {
  this.window = options.window;
  this._doc = options.doc;
  this._widget = options.widget;
  this._symbiont = null; // set later
  this.node = null; // set later
  
  this._createNode();
}

// Update a property of a widget.
WidgetChrome.prototype.update = function WC_update(updatedItem, property, value) {
  switch(property) {
    case "contentURL":
    case "content":
      this.setContent();
      break;
    case "width":
      this.node.style.minWidth = value + "px";
      this.node.querySelector("iframe").style.width = value + "px";
      break;
    case "tooltip":
      this.node.setAttribute("tooltiptext", value);
      break;
    case "postMessage":
      this._symbiont.postMessage(value);
      break;
    case "emit":
      let port = this._symbiont.port;
      port.emit.apply(port, value);
      break;
  }
}

// Add a widget to this window.
WidgetChrome.prototype._createNode = function WC__createNode() {
  // XUL element container for widget
  let node = this._doc.createElement("toolbaritem");
  let guid = require("api-utils/xpcom").makeUuid().toString();
  
  // Temporary work around require("self") failing on unit-test execution ...
  let jetpackID = "testID";
  try {
    jetpackID = require("self").id;
  } catch(e) {}
  
  // Compute an unique and stable widget id with jetpack id and widget.id
  let id = "widget:" + jetpackID + "-" + this._widget.id;
  node.setAttribute("id", id);
  node.setAttribute("label", this._widget.label);
  node.setAttribute("tooltiptext", this._widget.tooltip);
  node.setAttribute("align", "center");

  // TODO move into a stylesheet, configurable by consumers.
  // Either widget.style, exposing the style object, or a URL
  // (eg, can load local stylesheet file).
  node.setAttribute("style", [
      "overflow: hidden; margin: 1px 2px 1px 2px; padding: 0px;",
      "min-height: 16px;",
  ].join(""));

  node.style.minWidth = this._widget.width + "px";
  
  this.node = node;
}

// Initial population of a widget's content.
WidgetChrome.prototype.fill = function WC_fill() {
  // Create element
  var iframe = this._doc.createElement("iframe");
  iframe.setAttribute("type", "content");
  iframe.setAttribute("transparent", "transparent");
  iframe.style.overflow = "hidden";
  iframe.style.height = "16px";
  iframe.style.maxHeight = "16px";
  iframe.style.width = this._widget.width + "px";
  iframe.setAttribute("flex", "1");
  iframe.style.border = "none";
  iframe.style.padding = "0px";
  
  // Do this early, because things like contentWindow are null
  // until the node is attached to a document.
  this.node.appendChild(iframe);

  // add event handlers
  this.addEventHandlers();

  // set content
  this.setContent();
}

// Get widget content type.
WidgetChrome.prototype.getContentType = function WC_getContentType() {
  if (this._widget.content)
    return CONTENT_TYPE_HTML;
  return (this._widget.contentURL && /\.(jpg|gif|png|ico)$/.test(this._widget.contentURL))
    ? CONTENT_TYPE_IMAGE : CONTENT_TYPE_URI;
}

// Set widget content.
WidgetChrome.prototype.setContent = function WC_setContent() {
  let type = this.getContentType();
  let contentURL = null;

  switch (type) {
    case CONTENT_TYPE_HTML:
      contentURL = "data:text/html," + encodeURIComponent(this._widget.content);
      break;
    case CONTENT_TYPE_URI:
      contentURL = this._widget.contentURL;
      break;
    case CONTENT_TYPE_IMAGE:
      let imageURL = this._widget.contentURL;
      contentURL = "data:text/html,<html><body><img src='" +
                   encodeURI(imageURL) + "'></body></html>";
      break;
    default:
      throw new Error("The widget's type cannot be determined.");
  }

  let iframe = this.node.firstElementChild;

  let self = this;
  // Cleanup previously created symbiont (in case we are update content)
  if (this._symbiont)
    this._symbiont.destroy();
  
  this._symbiont = Trait.compose(Symbiont.resolve({
    _onContentScriptEvent: "_onContentScriptEvent-not-used"
  }), {
    _onContentScriptEvent: function () {
      // Redirect events to WidgetView
      self._widget._onPortEvent(arguments);
    }
  })({
    frame: iframe,
    contentURL: contentURL,
    contentScriptFile: this._widget.contentScriptFile,
    contentScript: this._widget.contentScript,
    contentScriptWhen: this._widget.contentScriptWhen,
    allow: this._widget.allow,
    onMessage: function(message) {
      timer.setTimeout(function() {
        self._widget._onEvent("message", message);
      }, 0);
    }
  });
}

// Detect if document consists of a single image.
WidgetChrome._isImageDoc = function WC__isImageDoc(doc) {
  return doc.body.childNodes.length == 1 &&
         doc.body.firstElementChild &&
         doc.body.firstElementChild.tagName == "IMG";
}

// Set up all supported events for a widget.
WidgetChrome.prototype.addEventHandlers = function WC_addEventHandlers() {
  let contentType = this.getContentType();

  let self = this;
  let listener = function(e) {
    // Ignore event firings that target the iframe.
    if (e.target == self.node.firstElementChild)
      return;

    // The widget only supports left-click for now,
    // so ignore right-clicks.
    if (e.type == "click" && e.button == 2)
      return;

    // Proxy event to the widget
    timer.setTimeout(function() {
      self._widget._onEvent(EVENTS[e.type], null, self.node);
    }, 0);
  };

  this.eventListeners = {};
  let iframe = this.node.firstElementChild;
  for (let [type, method] in Iterator(EVENTS)) {
    iframe.addEventListener(type, listener, true, true);

    // Store listeners for later removal
    this.eventListeners[type] = listener;
  }
  
  // On document load, make modifications required for nice default
  // presentation.
  let self = this;
  function loadListener(e) {
    // Ignore event firings that target the iframe
    if (e.target == iframe)
      return;
    // Ignore about:blank loads
    if (e.type == "load" && e.target.location == "about:blank")
      return;
    
    // We may have had an unload event before that cleaned up the symbiont
    if (!self._symbiont)
      self.setContent();
    
    let doc = e.target;
    if (contentType == CONTENT_TYPE_IMAGE || WidgetChrome._isImageDoc(doc)) {
      // Force image content to size.
      // Add-on authors must size their images correctly.
      doc.body.firstElementChild.style.width = self._widget.width + "px";
      doc.body.firstElementChild.style.height = "16px";
    }

    // Allow all content to fill the box by default.
    doc.body.style.margin = "0";
  }
  iframe.addEventListener("load", loadListener, true);
  this.eventListeners["load"] = loadListener;
  
  // Register a listener to unload symbiont if the toolbaritem is moved
  // on user toolbars customization
  function unloadListener(e) {
    if (e.target.location == "about:blank")
      return;
    self._symbiont.destroy();
    self._symbiont = null;
    // This may fail but not always, it depends on how the node is 
    // moved or removed
    try {
      self.setContent();
    } catch(e) {}
    
  }
  
  iframe.addEventListener("unload", unloadListener, true);
  this.eventListeners["unload"] = unloadListener;
}

// Remove and unregister the widget from everything
WidgetChrome.prototype.destroy = function WC_destroy(removedItems) {
  // remove event listeners
  for (let [type, listener] in Iterator(this.eventListeners))
    this.node.firstElementChild.removeEventListener(type, listener, true);
  // remove dom node
  this.node.parentNode.removeChild(this.node);
  // cleanup symbiont
  this._symbiont.destroy();
  // cleanup itself
  this.eventListeners = null;
  this._widget = null;
  this._symbiont = null;
}

// Init the browserManager only after setting prototypes and such above, because
// it will cause browserManager.onTrack to be called immediately if there are
// open windows.
browserManager.init();
