text: 'use strict';
  
  var isNumber = require('lodash/lang/isNumber'),
      assign = require('lodash/object/assign'),
      forEach = require('lodash/collection/forEach'),
      every = require('lodash/collection/every');
  
  var Collections = require('../util/Collections');
  
  var Snap = require('../../vendor/snapsvg');
  
  function round(number, resolution) {
    return Math.round(number * resolution) / resolution;
  }
  
  function ensurePx(number) {
    return isNumber(number) ? number + 'px' : number;
  }
  
  /**
   * Creates a HTML container element for a SVG element with
   * the given configuration
   *
   * @param  {Object} options
   * @return {HTMLElement} the container element
   */
  function createContainer(options) {
  
    options = assign({}, { width: '100%', height: '100%' }, options);
  
    var container = options.container || document.body;
  
    // create a <div> around the svg element with the respective size
    // this way we can always get the correct container size
    // (this is impossible for <svg> elements at the moment)
    var parent = document.createElement('div');
    parent.setAttribute('class', 'djs-container');
  
    assign(parent.style, {
      position: 'relative',
      overflow: 'hidden',
      width: ensurePx(options.width),
      height: ensurePx(options.height)
    });
  
    container.appendChild(parent);
  
    return parent;
  }
  
  function createGroup(parent, cls) {
    return parent.group().attr({ 'class' : cls });
  }
  
  var BASE_LAYER = 'base';
  
  
  var REQUIRED_MODEL_ATTRS = {
    shape: [ 'x', 'y', 'width', 'height' ],
    connection: [ 'waypoints' ]
  };
  
  /**
   * The main drawing canvas.
   *
   * @class
   * @constructor
   *
   * @emits Canvas#canvas.init
   *
   * @param {Object} config
   * @param {EventBus} eventBus
   * @param {GraphicsFactory} graphicsFactory
   * @param {ElementRegistry} elementRegistry
   */
  function Canvas(config, eventBus, graphicsFactory, elementRegistry) {
    this._eventBus = eventBus;
    this._elementRegistry = elementRegistry;
    this._graphicsFactory = graphicsFactory;
  
    this._init(config || {});
  }
  
  Canvas.$inject = [ 'config.canvas', 'eventBus', 'graphicsFactory', 'elementRegistry' ];
  
  module.exports = Canvas;
  
  
  Canvas.prototype._init = function(config) {
  
    // Creates a <svg> element that is wrapped into a <div>.
    // This way we are always able to correctly figure out the size of the svg element
    // by querying the parent node.
    //
    // (It is not possible to get the size of a svg element cross browser @ 2014-04-01)
    //
    // <div class="djs-container" style="width: {desired-width}, height: {desired-height}">
    //   <svg width="100%" height="100%">
    //    ...
    //   </svg>
    // </div>
  
    // html container
    var eventBus = this._eventBus,
  
        container = createContainer(config),
        svg = Snap.createSnapAt('100%', '100%', container),
        viewport = createGroup(svg, 'viewport'),
  
        self = this;
  
    this._container = container;
    this._svg = svg;
    this._viewport = viewport;
    this._layers = {};
  
    eventBus.on('diagram.init', function(event) {
  
      /**
       * An event indicating that the canvas is ready to be drawn on.
       *
       * @memberOf Canvas
       *
       * @event canvas.init
       *
       * @type {Object}
       * @property {Snap<SVGSVGElement>} svg the created svg element
       * @property {Snap<SVGGroup>} viewport the direct parent of diagram elements and shapes
       */
      eventBus.fire('canvas.init', { svg: svg, viewport: viewport });
    });
  
    eventBus.on('diagram.destroy', function() {
  
      var parent = self._container.parentNode;
  
      if (parent) {
        parent.removeChild(container);
      }
  
      eventBus.fire('canvas.destroy', { svg: self._svg, viewport: self._viewport });
  
      self._svg.remove();
  
      self._svg = self._container = self._layers = self._viewport = null;
    });
  
  };
  
  /**
   * Returns the default layer on which
   * all elements are drawn.
   *
   * @returns {Snap<SVGGroup>}
   */
  Canvas.prototype.getDefaultLayer = function() {
    return this.getLayer(BASE_LAYER);
  };
  
  /**
   * Returns a layer that is used to draw elements
   * or annotations on it.
   *
   * @param  {String} name
   *
   * @returns {Snap<SVGGroup>}
   */
  Canvas.prototype.getLayer = function(name) {
  
    if (!name) {
      throw new Error('must specify a name');
    }
  
    var layer = this._layers[name];
    if (!layer) {
      layer = this._layers[name] = createGroup(this._viewport, 'layer-' + name);
    }
  
    return layer;
  };
  
  
  /**
   * Returns the html element that encloses the
   * drawing canvas.
   *
   * @return {DOMNode}
   */
  Canvas.prototype.getContainer = function() {
    return this._container;
  };
  
  
  /////////////// markers ///////////////////////////////////
  
  Canvas.prototype._updateMarker = function(element, marker, add) {
    var container;
  
    if (!element.id) {
      element = this._elementRegistry.get(element);
    }
  
    // we need to access all
    container = this._elementRegistry._elements[element.id];
  
    if (!container) {
      return;
    }
  
    forEach([ container.gfx, container.secondaryGfx ], function(gfx) {
      if (gfx) {
        // invoke either addClass or removeClass based on mode
        gfx[add ? 'addClass' : 'removeClass'](marker);
      }
    });
  
    /**
     * An event indicating that a marker has been updated for an element
     *
     * @event element.marker.update
     * @type {Object}
     * @property {djs.model.Element} element the shape
     * @property {Object} gfx the graphical representation of the shape
     * @property {String} marker
     * @property {Boolean} add true if the marker was added, false if it got removed
     */
    this._eventBus.fire('element.marker.update', { element: element, gfx: container.gfx, marker: marker, add: !!add });
  };
  
  
  /**
   * Adds a marker to an element (basically a css class).
   *
   * Fires the element.marker.update event, making it possible to
   * integrate extension into the marker life-cycle, too.
   *
   * @example
   * canvas.addMarker('foo', 'some-marker');
   *
   * var fooGfx = canvas.getGraphics('foo');
   *
   * fooGfx; // <g class="... some-marker"> ... </g>
   *
   * @param {String|djs.model.Base} element
   * @param {String} marker
   */
  Canvas.prototype.addMarker = function(element, marker) {
    this._updateMarker(element, marker, true);
  };
  
  
  /**
   * Remove a marker from an element.
   *
   * Fires the element.marker.update event, making it possible to
   * integrate extension into the marker life-cycle, too.
   *
   * @param  {String|djs.model.Base} element
   * @param  {String} marker
   */
  Canvas.prototype.removeMarker = function(element, marker) {
    this._updateMarker(element, marker, false);
  };
  
  /**
   * Check the existence of a marker on element.
   *
   * @param  {String|djs.model.Base} element
   * @param  {String} marker
   */
  Canvas.prototype.hasMarker = function(element, marker) {
    if (!element.id) {
      element = this._elementRegistry.get(element);
    }
  
    var gfx = this.getGraphics(element);
  
    return gfx && gfx.hasClass(marker);
  };
  
  /**
   * Toggles a marker on an element.
   *
   * Fires the element.marker.update event, making it possible to
   * integrate extension into the marker life-cycle, too.
   *
   * @param  {String|djs.model.Base} element
   * @param  {String} marker
   */
  Canvas.prototype.toggleMarker = function(element, marker) {
    if(this.hasMarker(element, marker)) {
      this.removeMarker(element, marker);
    } else {
      this.addMarker(element, marker);
    }
  };
  
  Canvas.prototype.getRootElement = function() {
    if (!this._rootElement) {
      this.setRootElement({ id: '__implicitroot' });
    }
  
    return this._rootElement;
  };
  
  
  
  //////////////// root element handling ///////////////////////////
  
  /**
   * Sets a given element as the new root element for the canvas
   * and returns the new root element.
   *
   * @param {Object|djs.model.Root} element
   * @param {Boolean} [override] whether to override the current root element, if any
   *
   * @return {Object|djs.model.Root} new root element
   */
  Canvas.prototype.setRootElement = function(element, override) {
  
    this._ensureValid('root', element);
  
    var oldRoot = this._rootElement,
        elementRegistry = this._elementRegistry,
        eventBus = this._eventBus;
  
    if (oldRoot) {
      if (!override) {
        throw new Error('rootElement already set, need to specify override');
      }
  
      // simulate element remove event sequence
      eventBus.fire('root.remove', { element: oldRoot });
      eventBus.fire('root.removed', { element: oldRoot });
  
      elementRegistry.remove(oldRoot);
    }
  
    var gfx = this.getDefaultLayer();
  
    // resemble element add event sequence
    eventBus.fire('root.add', { element: element });
  
    elementRegistry.add(element, gfx, this._svg);
  
    eventBus.fire('root.added', { element: element, gfx: gfx });
  
    this._rootElement = element;
  
    return element;
  };
  
  
  
  ///////////// add functionality ///////////////////////////////
  
  Canvas.prototype._ensureValid = function(type, element) {
    if (!element.id) {
      throw new Error('element must have an id');
    }
  
    if (this._elementRegistry.get(element.id)) {
      throw new Error('element with id ' + element.id + ' already exists');
    }
  
    var requiredAttrs = REQUIRED_MODEL_ATTRS[type];
  
    var valid = every(requiredAttrs, function(attr) {
      return typeof element[attr] !== 'undefined';
    });
  
    if (!valid) {
      throw new Error(
        'must supply { ' + requiredAttrs.join(', ') + ' } with ' + type);
    }
  };
  
  Canvas.prototype._setParent = function(element, parent) {
    Collections.add(parent.children, element);
    element.parent = parent;
  };
  
  /**
   * Adds an element to the canvas.
   *
   * This wires the parent <-> child relationship between the element and
   * a explicitly specified parent or an implicit root element.
   *
   * During add it emits the events
   *
   *  * <{type}.add> (element, parent)
   *  * <{type}.added> (element, gfx)
   *
   * Extensions may hook into these events to perform their magic.
   *
   * @param {String} type
   * @param {Object|djs.model.Base} element
   * @param {Object|djs.model.Base} [parent]
   *
   * @return {Object|djs.model.Base} the added element
   */
  Canvas.prototype._addElement = function(type, element, parent) {
  
    parent = parent || this.getRootElement();
  
    var eventBus = this._eventBus,
        graphicsFactory = this._graphicsFactory;
  
    this._ensureValid(type, element);
  
    eventBus.fire(type + '.add', { element: element, parent: parent });
  
    this._setParent(element, parent);
  
    // create graphics
    var gfx = graphicsFactory.create(type, element);
  
    this._elementRegistry.add(element, gfx);
  
    // update its visual
    graphicsFactory.update(type, element, gfx);
  
    eventBus.fire(type + '.added', { element: element, gfx: gfx });
  
    return element;
  };
  
  /**
   * Adds a shape to the canvas
   *
   * @param {Object|djs.model.Shape} shape to add to the diagram
   * @param {djs.model.Base} [parent]
   *
   * @return {djs.model.Shape} the added shape
   */
  Canvas.prototype.addShape = function(shape, parent) {
    return this._addElement('shape', shape, parent);
  };
  
  /**
   * Adds a connection to the canvas
   *
   * @param {Object|djs.model.Connection} connection to add to the diagram
   * @param {djs.model.Base} [parent]
   *
   * @return {djs.model.Connection} the added connection
   */
  Canvas.prototype.addConnection = function(connection, parent) {
    return this._addElement('connection', connection, parent);
  };
  
  
  /**
   * Internal remove element
   */
  Canvas.prototype._removeElement = function(element, type) {
  
    var elementRegistry = this._elementRegistry,
        graphicsFactory = this._graphicsFactory,
        eventBus = this._eventBus;
  
    element = elementRegistry.get(element.id || element);
  
    if (!element) {
      // element was removed already
      return;
    }
  
    eventBus.fire(type + '.remove', { element: element });
  
    graphicsFactory.remove(element);
  
    // unset parent <-> child relationship
    Collections.remove(element.parent && element.parent.children, element);
    element.parent = null;
  
    eventBus.fire(type + '.removed', { element: element });
  
    elementRegistry.remove(element);
  
    return element;
  };
  
  
  /**
   * Removes a shape from the canvas
   *
   * @param {String|djs.model.Shape} shape or shape id to be removed
   *
   * @return {djs.model.Shape} the removed shape
   */
  Canvas.prototype.removeShape = function(shape) {
  
    /**
     * An event indicating that a shape is about to be removed from the canvas.
     *
     * @memberOf Canvas
     *
     * @event shape.remove
     * @type {Object}
     * @property {djs.model.Shape} element the shape descriptor
     * @property {Object} gfx the graphical representation of the shape
     */
  
    /**
     * An event indicating that a shape has been removed from the canvas.
     *
     * @memberOf Canvas
     *
     * @event shape.removed
     * @type {Object}
     * @property {djs.model.Shape} element the shape descriptor
     * @property {Object} gfx the graphical representation of the shape
     */
    return this._removeElement(shape, 'shape');
  };
  
  
  /**
   * Removes a connection from the canvas
   *
   * @param {String|djs.model.Connection} connection or connection id to be removed
   *
   * @return {djs.model.Connection} the removed connection
   */
  Canvas.prototype.removeConnection = function(connection) {
  
    /**
     * An event indicating that a connection is about to be removed from the canvas.
     *
     * @memberOf Canvas
     *
     * @event connection.remove
     * @type {Object}
     * @property {djs.model.Connection} element the connection descriptor
     * @property {Object} gfx the graphical representation of the connection
     */
  
    /**
     * An event indicating that a connection has been removed from the canvas.
     *
     * @memberOf Canvas
     *
     * @event connection.removed
     * @type {Object}
     * @property {djs.model.Connection} element the connection descriptor
     * @property {Object} gfx the graphical representation of the connection
     */
    return this._removeElement(connection, 'connection');
  };
  
  
  /**
   * Sends a shape to the front.
   *
   * This method takes parent / child relationships between shapes into account
   * and makes sure that children are properly handled, too.
   *
   * @param {djs.model.Shape} shape descriptor of the shape to be sent to front
   * @param {boolean} [bubble=true] whether to send parent shapes to front, too
   */
  Canvas.prototype.sendToFront = function(shape, bubble) {
  
    if (bubble !== false) {
      bubble = true;
    }
  
    if (bubble && shape.parent) {
      this.sendToFront(shape.parent);
    }
  
    forEach(shape.children, function(child) {
      this.sendToFront(child, false);
    }, this);
  
    var gfx = this.getGraphics(shape),
        gfxParent = gfx.parent();
  
    gfx.remove().appendTo(gfxParent);
  };
  
  
  /**
   * Return the graphical object underlaying a certain diagram element
   *
   * @param {String|djs.model.Base} element descriptor of the element
   * @param {Boolean} [secondary=false] whether to return the secondary connected element
   *
   * @return {SVGElement}
   */
  Canvas.prototype.getGraphics = function(element, secondary) {
    return this._elementRegistry.getGraphics(element, secondary);
  };
  
  
  Canvas.prototype._fireViewboxChange = function() {
    this._eventBus.fire('canvas.viewbox.changed', { viewbox: this.viewbox(false) });
  };
  
  
  /**
   * Gets or sets the view box of the canvas, i.e. the area that is currently displayed
   *
   * @param  {Object} [box] the new view box to set
   * @param  {Number} box.x the top left X coordinate of the canvas visible in view box
   * @param  {Number} box.y the top left Y coordinate of the canvas visible in view box
   * @param  {Number} box.width the visible width
   * @param  {Number} box.height
   *
   * @example
   *
   * canvas.viewbox({ x: 100, y: 100, width: 500, height: 500 })
   *
   * // sets the visible area of the diagram to (100|100) -> (600|100)
   * // and and scales it according to the diagram width
   *
   * @return {Object} the current view box
   */
  Canvas.prototype.viewbox = function(box) {
  
    if (box === undefined && this._cachedViewbox) {
      return this._cachedViewbox;
    }
  
    var viewport = this._viewport,
        innerBox,
        outerBox = this.getSize(),
        matrix,
        scale,
        x, y;
  
    if (!box) {
      // compute the inner box based on the
      // diagrams default layer. This allows us to exclude
      // external components, such as overlays
      innerBox = this.getDefaultLayer().getBBox(true);
  
      matrix = viewport.transform().localMatrix;
      scale = round(matrix.a, 1000);
  
      x = round(-matrix.e || 0, 1000);
      y = round(-matrix.f || 0, 1000);
  
      box = this._cachedViewbox = {
        x: x ? x / scale : 0,
        y: y ? y / scale : 0,
        width: outerBox.width / scale,
        height: outerBox.height / scale,
        scale: scale,
        inner: {
          width: innerBox.width,
          height: innerBox.height,
          x: innerBox.x,
          y: innerBox.y
        },
        outer: outerBox
      };
  
      return box;
    } else {
      scale = Math.min(outerBox.width / box.width, outerBox.height / box.height);
  
      matrix = new Snap.Matrix().scale(scale).translate(-box.x, -box.y);
      viewport.transform(matrix);
  
      this._fireViewboxChange();
    }
  
    return box;
  };
  
  
  /**
   * Gets or sets the scroll of the canvas.
   *
   * @param {Object} [delta] the new scroll to apply.
   *
   * @param {Number} [delta.dx]
   * @param {Number} [delta.dy]
   */
  Canvas.prototype.scroll = function(delta) {
  
    var node = this._viewport.node;
    var matrix = node.getCTM();
  
    if (delta) {
      delta = assign({ dx: 0, dy: 0 }, delta || {});
  
      matrix = this._svg.node.createSVGMatrix().translate(delta.dx, delta.dy).multiply(matrix);
  
      setCTM(node, matrix);
  
      this._fireViewboxChange();
    }
  
    return { x: matrix.e, y: matrix.f };
  };
  
  
  /**
   * Gets or sets the current zoom of the canvas, optionally zooming to the specified position.
   *
   * @param {String|Number} [newScale] the new zoom level, either a number, i.e. 0.9,
   *                                   or `fit-viewport` to adjust the size to fit the current viewport
   * @param {String|Point} [center] the reference point { x: .., y: ..} to zoom to, 'auto' to zoom into mid or null
   *
   * @return {Number} the current scale
   */
  Canvas.prototype.zoom = function(newScale, center) {
  
    if (newScale === 'fit-viewport') {
      return this._fitViewport(center);
    }
  
    var vbox = this.viewbox();
  
    if (newScale === undefined) {
      return vbox.scale;
    }
  
    var outer = vbox.outer;
  
    if (center === 'auto') {
      center = {
        x: outer.width / 2,
        y: outer.height / 2
      };
    }
  
    var matrix = this._setZoom(newScale, center);
  
    this._fireViewboxChange();
  
    return round(matrix.a, 1000);
  };
  
  function setCTM(node, m) {
    var mstr = 'matrix(' + m.a + ',' + m.b + ',' + m.c + ',' + m.d + ',' + m.e + ',' + m.f + ')';
    node.setAttribute('transform', mstr);
  }
  
  Canvas.prototype._fitViewport = function(center) {
  
    var vbox = this.viewbox(),
        outer = vbox.outer,
        inner = vbox.inner,
        newScale,
        newViewbox;
  
    // display the complete diagram without zooming in.
    // instead of relying on internal zoom, we perform a
    // hard reset on the canvas viewbox to realize this
    //
    // if diagram does not need to be zoomed in, we focus it around
    // the diagram origin instead
  
    if (inner.x >= 0 &&
        inner.y >= 0 &&
        inner.x + inner.width <= outer.width &&
        inner.y + inner.height <= outer.height &&
        !center) {
  
      newViewbox = {
        x: 0,
        y: 0,
        width: Math.max(inner.width + inner.x, outer.width),
        height: Math.max(inner.height + inner.y, outer.height)
      };
    } else {
  
      newScale = Math.min(1, outer.width / inner.width, outer.height / inner.height);
      newViewbox = {
        x: inner.x + (center ? inner.width / 2 - outer.width / newScale / 2 : 0),
        y: inner.y + (center ? inner.height / 2 - outer.height / newScale / 2 : 0),
        width: outer.width / newScale,
        height: outer.height / newScale
      };
    }
  
    this.viewbox(newViewbox);
  
    return this.viewbox().scale;
  };
  
  
  Canvas.prototype._setZoom = function(scale, center) {
  
    var svg = this._svg.node,
        viewport = this._viewport.node;
  
    var matrix = svg.createSVGMatrix();
    var point = svg.createSVGPoint();
  
    var centerPoint,
        originalPoint,
        currentMatrix,
        scaleMatrix,
        newMatrix;
  
    currentMatrix = viewport.getCTM();
  
  
    var currentScale = currentMatrix.a;
  
    if (center) {
      centerPoint = assign(point, center);
  
      // revert applied viewport transformations
      originalPoint = centerPoint.matrixTransform(currentMatrix.inverse());
  
      // create scale matrix
      scaleMatrix = matrix
                      .translate(originalPoint.x, originalPoint.y)
                      .scale(1 / currentScale * scale)
                      .translate(-originalPoint.x, -originalPoint.y);
  
      newMatrix = currentMatrix.multiply(scaleMatrix);
    } else {
      newMatrix = matrix.scale(scale);
    }
  
    setCTM(this._viewport.node, newMatrix);
  
    return newMatrix;
  };
  
  
  /**
   * Returns the size of the canvas
   *
   * @return {Dimensions}
   */
  Canvas.prototype.getSize = function () {
    return {
      width: this._container.clientWidth,
      height: this._container.clientHeight
    };
  };
  
  
  /**
   * Return the absolute bounding box for the given element
   *
   * The absolute bounding box may be used to display overlays in the
   * callers (browser) coordinate system rather than the zoomed in/out
   * canvas coordinates.
   *
   * @param  {ElementDescriptor} element
   * @return {Bounds} the absolute bounding box
   */
  Canvas.prototype.getAbsoluteBBox = function(element) {
    var vbox = this.viewbox();
    var bbox;
  
    // connection
    // use svg bbox
    if (element.waypoints) {
      var gfx = this.getGraphics(element);
  
      var transformBBox = gfx.getBBox(true);
      bbox = gfx.getBBox();
  
      bbox.x -= transformBBox.x;
      bbox.y -= transformBBox.y;
  
      bbox.width += 2 * transformBBox.x;
      bbox.height +=  2 * transformBBox.y;
    }
    // shapes
    // use data
    else {
      bbox = element;
    }
  
    var x = bbox.x * vbox.scale - vbox.x * vbox.scale;
    var y = bbox.y * vbox.scale - vbox.y * vbox.scale;
  
    var width = bbox.width * vbox.scale;
    var height = bbox.height * vbox.scale;
  
    return {
      x: x,
      y: y,
      width: width,
      height: height
    };
  };
  