text: var inherit = require('../inherit');
  var sceneObject = require('../sceneObject');
  var $ = require('jquery');
  
  /**
   * @typedef {object} geo.gui.widget.position
   * @property {string|number} [top] The position to the top of the container.
   *   A string css position or a number in pixels.
   * @property {string|number} [right] The position to the right of the
   *   container.  A string css position or a number in pixels.
   * @property {string|number} [bottom] The position to the bottom of the
   *   container.  A string css position or a number in pixels.
   * @property {string|number} [left] The position to the left of the container.
   * @property {string|number} [top] The position to the top of the container.
   * @property {*} [...] Additional css properties that affect position are
   *   allowed.  See the css specification for details.
   */
  
  /**
   * @typedef {object} geo.gui.widget.spec
   *
   * @property {geo.layer} [layer] Layer associated with the widget.
   * @property {geo.gui.widget.position} [position] Location of the widget.
   * @property {geo.gui.widget} [parent] Optional parent widget.
   */
  
  /**
   * Create a new instance of class widget.
   *
   * @class
   * @alias geo.gui.widget
   * @extends {geo.sceneObject}
   * @param {geo.gui.widget.spec} [arg] Options for the widget.
   * @returns {geo.gui.widget}
   */
  var widget = function (arg) {
    'use strict';
    if (!(this instanceof widget)) {
      return new widget(arg);
    }
    arg = arg || {};
    sceneObject.call(this, arg);
  
    var geo_event = require('../event');
  
    var m_this = this,
        s_exit = this._exit,
        m_layer = arg.layer,
        m_canvas = null,
        m_position = arg.position === undefined ? { left: 0, top: 0 } : arg.position;
  
    if (arg.parent !== undefined && !(arg.parent instanceof widget)) {
      throw new Error('Parent must be of type geo.gui.widget');
    } else if (arg.parent) {
      m_this.parent(arg.parent);
    }
  
    /**
     * Initialize the widget.
     *
     * @returns {this}
     */
    this._init = function () {
      m_this.modified();
      return m_this;
    };
  
    /**
     * Clean up the widget.
     */
    this._exit = function () {
      m_this.children().forEach(function (child) {
        m_this.removeChild(child);
        child._exit();
      });
  
      m_this.layer().geoOff(geo_event.pan, m_this.repositionEvent);
      if (m_this.parentCanvas().removeChild && m_this.canvas()) {
        try {
          m_this.parentCanvas().removeChild(m_this.canvas());
        } catch (err) {
          // fail gracefully if the canvas is not a child of the parentCanvas
        }
      }
      s_exit();
    };
  
    /**
     * Return the layer associated with this widget.
     *
     * @returns {geo.layer}
     */
    this.layer = function () {
      return m_layer || (m_this.parent() && m_this.parent().layer());
    };
  
    /**
     * Create the canvas this widget will operate on.
     */
    this._createCanvas = function () {
      throw new Error('Must be defined in derived classes');
    };
  
    /**
     * Get/Set the canvas for the widget.
     *
     * @param {HTMLElement} [val] If specified, set the canvas, otherwise get
     *    the canvas.
     * @returns {HTMLElement|this} If getting the canvas, return the current
     *    value; otherwise, return this widget.
     */
    this.canvas = function (val) {
      if (val === undefined) {
        return m_canvas;
      }
      m_canvas = val;
      return m_this;
    };
  
    /**
     * Appends the canvas to the parent canvas.
     * The widget determines how to append itself to a parent, the parent can
     * either be another widget, or the UI Layer.
     */
    this._appendCanvasToParent = function () {
      m_this.parentCanvas().appendChild(m_this.canvas());
    };
  
    /**
     * Get the parent canvas (top level widgets define their layer as their
     * parent canvas).
     *
     * @returns {HTMLElement} The canvas of the widget's parent.
     */
    this.parentCanvas = function () {
      if (!m_this.parent()) {
        return m_this.layer().canvas();
      }
      return m_this.parent().canvas();
    };
  
    /**
     * Get or set the CSS positioning that a widget should be placed at.
     *
     * @param {geo.gui.widget.position} [pos] If unspecified, return the current
     *    position.  Otherwise, set the current position.
     * @param {boolean} [actualValue] If getting the position, if this is truthy,
     *    always return the stored value, not a value adjusted for display.
     * @returns {geo.gui.widget.position|this} Either the position or the widget
     *    instance.  If this is the position and `actualValue` is falsy,
     *    positions that specify an explicit `x` and `y` parameter will be
     *    converted to a value that can be used by the display css.
     */
    this.position = function (pos, actualValue) {
      if (pos !== undefined) {
        m_this.layer().geoOff(geo_event.pan, m_this.repositionEvent);
        var clearPosition = {};
        for (var attr in m_position) {
          if (m_position.hasOwnProperty(attr)) {
            clearPosition[attr] = null;
          }
        }
        m_position = pos;
        if (m_position.hasOwnProperty('x') && m_position.hasOwnProperty('y')) {
          m_this.layer().geoOn(geo_event.pan, m_this.repositionEvent);
        }
        m_this.reposition($.extend(clearPosition, m_this.position()));
        return m_this;
      }
      if (m_position.hasOwnProperty('x') && m_position.hasOwnProperty('y') && !actualValue) {
        var position = m_this.layer().map().gcsToDisplay(m_position);
  
        return {
          left: position.x,
          top: position.y,
          right: null,
          bottom: null
        };
      }
  
      return m_position;
    };
  
    /**
     * Repositions a widget.
     *
     * @param {geo.gui.widget.position} [position] The new position for the
     *    widget.  `undefined` uses the stored position value.
     * @returns {this}
     */
    this.reposition = function (position) {
      position = position || m_this.position();
      if (m_this.canvas() && m_this.canvas().style) {
        m_this.canvas().style.position = 'absolute';
  
        for (var cssAttr in position) {
          if (position.hasOwnProperty(cssAttr)) {
            // if the property is a number, add px to it, otherwise set it to the
            // specified value.  Setting a property to null clears it.  Setting to
            // undefined doesn't alter it.
            if (/^\s*(-|\+)?(\d+(\.\d*)?|\d*\.\d+)([eE](-|\+)?\d+)?\s*$/.test(position[cssAttr])) {
              // this ensures that the number is a float with no more than 3
              // decimal places (Chrome does this automatically, but doing so
              // explicitly makes testing more consistent).  It will be an
              // integer when possible.
              m_this.canvas().style[cssAttr] = parseFloat(parseFloat(position[cssAttr]).toFixed(3)) + 'px';
            } else {
              m_this.canvas().style[cssAttr] = position[cssAttr];
            }
          }
        }
      }
      return m_this;
    };
  
    /**
     * If the position is based on map coordinates, this gets called when the
     * map is panned to resposition the widget.
     *
     * @returns {this}
     */
    this.repositionEvent = function () {
      return m_this.reposition();
    };
  
    /**
     * Report if the top left of widget (or its current x, y position) is within
     * the viewport.
     *
     * @returns {boolean} True if the widget is within the viewport.
     */
    this.isInViewport = function () {
      var position = m_this.position();
      var layer = m_this.layer();
  
      return ((position.left >= 0 && position.top >= 0) &&
              (position.left <= layer.width() && position.top <= layer.height()));
    };
  
    if (m_position.hasOwnProperty('x') && m_position.hasOwnProperty('y')) {
      this.layer().geoOn(geo_event.pan, m_this.repositionEvent);
    }
  };
  inherit(widget, sceneObject);
  module.exports = widget;
  