Show:
/* Copyright © 2015-2016 David Valdman */

define(function(require, exports, module) {
    var EventHandler = require('../events/EventHandler');
    var Transform = require('./Transform');
    var Stream = require('../streams/Stream');
    var ResizeStream = require('../streams/ResizeStream');
    var SizeNode = require('./SizeNode');
    var LayoutNode = require('./LayoutNode');
    var sizeAlgebra = require('./algebras/size');
    var layoutAlgebra = require('./algebras/layout');

    var usePrefix = !('transform' in document.documentElement.style);
    var devicePixelRatio = 2 * (window.devicePixelRatio || 1);
    var MIN_OPACITY = 0.0001;
    var MAX_OPACITY = 0.9999;
    var EPSILON = 1e-5;
    var _zeroZero = [0,0];

    /**
     * Responsible for committing CSS3 properties to the DOM and providing DOM event hooks
     *  from a provided DOM element. Where Surface's API handles inputs from the developer
     *  from within Samsara, ElementOutput handles the DOM interaction layer.
     *
     *
     * @class ElementOutput
     * @constructor
     * @namespace Core
     * @uses Core.LayoutNode
     * @uses Core.SizeNode
     * @private
     * @param {Node} element document parent of this container
     */
    function ElementOutput(element) {
        this._currentTarget = null;

        this._cachedSpec = {
            transform : null,
            opacity : 1,
            origin : null,
            size : null
        };

        this._eventOutput = new EventHandler();
        EventHandler.setOutputHandler(this, this._eventOutput);

        this._eventForwarder = function _eventForwarder(event) {
            this._eventOutput.emit(event.type, event);
        }.bind(this);

        this._sizeNode = new SizeNode();
        this._layoutNode = new LayoutNode();

        this._size = new EventHandler();
        this._layout = new EventHandler();

        this.size = ResizeStream.lift(function elementSizeLift(sizeSpec, parentSize){
            if (!parentSize) return false; // occurs when surface is never added
            return sizeAlgebra(sizeSpec, parentSize);
        }, [this._sizeNode, this._size]);

        this.layout = Stream.lift(function(parentSpec, objectSpec, size){
            if (!parentSpec || !size) return false;
            return (objectSpec)
                ? layoutAlgebra(objectSpec, parentSpec, size)
                : parentSpec;
        }, [this._layout, this._layoutNode, this.size]);

        this.layout.on('start', function(){
            if (!this._currentTarget){
                var root = this._getRoot();
                this.setup(root.allocator);
            }
        }.bind(this));

        this.layout.on('update', commitLayout.bind(this));
        this.layout.on('end', commitLayout.bind(this));

        this.size.on('resize', function(size){
            if (!this._currentTarget){
                var root = this._getRoot();
                this.setup(root.allocator);
            }
            commitSize.call(this, size);
        }.bind(this));

        this._currentTarget = null;

        this._opacityDirty = true;
        this._originDirty = true;
        this._transformDirty = true;
        this._isVisible = true;

        if (element) this.attach(element);
    }

    function _addEventListeners(target) {
        for (var i in this._eventOutput.listeners)
            target.addEventListener(i, this._eventForwarder);
    }

    function _removeEventListeners(target) {
        for (var i in this._eventOutput.listeners)
            target.removeEventListener(i, this._eventForwarder);
    }

    function _round(value, unit){
        return (unit === 1)
            ? Math.round(value)
            : Math.round(value * unit) / unit
    }

    function _formatCSSTransform(transform, unit) {
        var result = 'matrix3d(';
        for (var i = 0; i < 15; i++) {
            if (Math.abs(transform[i]) < EPSILON) transform[i] = 0;
            result += (i === 12 || i === 13)
                ? _round(transform[i], unit) + ','
                : transform[i] + ',';
        }
        return result + transform[15] + ')';
    }

    function _formatCSSOrigin(origin) {
        return (100 * origin[0]) + '% ' + (100 * origin[1]) + '%';
    }

    function _xyNotEquals(a, b) {
        return (a && b) ? (a[0] !== b[0] || a[1] !== b[1]) : a !== b;
    }

    var _setOrigin = usePrefix
        ? function _setOrigin(element, origin) {
            element.style.webkitTransformOrigin = _formatCSSOrigin(origin);
        }
        : function _setOrigin(element, origin) {
            element.style.transformOrigin = _formatCSSOrigin(origin);
        };

    var _setTransform = (usePrefix)
        ? function _setTransform(element, transform, unit) {
            element.style.webkitTransform = _formatCSSTransform(transform, unit);
        }
        : function _setTransform(element, transform, unit) {
            element.style.transform = _formatCSSTransform(transform, unit);
        };

    var _setSize = function _setSize(target, size){
        if (size[0] === true) size[0] = target.offsetWidth;
        else target.style.width = size[0] + 'px';

        if (size[1] === true) size[1] = target.offsetHeight;
        else target.style.height = size[1] + 'px';
    };

    // {Visibility : hidden} allows for DOM events to pass through the element
    // TODO: use pointerEvents instead. However, there is a bug in Chrome for Android
    // ticket here: https://code.google.com/p/chromium/issues/detail?id=569654
    var _setOpacity = function _setOpacity(element, opacity) {
        if (!this._isVisible && opacity > MIN_OPACITY) {
            //element.style.pointerEvents = 'auto';
            element.style.visibility = 'visible';
            this._isVisible = true;
        }

        if (opacity > MAX_OPACITY) opacity = MAX_OPACITY;
        else if (opacity < MIN_OPACITY) {
            opacity = MIN_OPACITY;
            if (this._isVisible) {
                //element.style.pointerEvents = 'none';
                element.style.visibility = 'hidden';
                this._isVisible = false;
            }
        }

        if (this._isVisible) element.style.opacity = opacity;
    };

    /**
     * Adds a handler to the `type` channel which will be executed on `emit`.
     *
     * @method on
     *
     * @param type {String}         DOM event channel name, e.g., "click", "touchmove"
     * @param handler {Function}    Handler. It's only argument will be an emitted data payload.
     */
    ElementOutput.prototype.on = function on(type, handler) {
        if (this._currentTarget)
            this._currentTarget.addEventListener(type, this._eventForwarder);
        EventHandler.prototype.on.apply(this._eventOutput, arguments);
    };

    /**
     * Removes a previously added handler to the `type` channel.
     *  Undoes the work of `on`.
     *
     * @method removeListener
     * @param type {String}         DOM event channel name e.g., "click", "touchmove"
     * @param handler {Function}    Handler
     */
    ElementOutput.prototype.off = function off(type, handler) {
        EventHandler.prototype.off.apply(this._eventOutput, arguments);
    };

    /**
     * Emit an event with optional data payload. This will execute all listening
     *  to the channel name with the payload as only argument.
     *
     * @method emit
     * @param type {string}         Event channel name
     * @param [payload] {Object}    User defined data payload
     */
    ElementOutput.prototype.emit = function emit(type, payload) {
        EventHandler.prototype.emit.apply(this._eventOutput, arguments);
    };

    /**
     * Assigns the DOM element for committing and to and attaches event listeners.
     *
     * @private
     * @method attach
     * @param {Node} target document parent of this container
     */
    ElementOutput.prototype.attach = function attach(target) {
        this._currentTarget = target;
        _addEventListeners.call(this, target);
    };

    /**
     * Removes the associated DOM element in memory and detached event listeners.
     *
     * @private
     * @method detach
     */
    ElementOutput.prototype.detach = function detach() {
        var target = this._currentTarget;
        if (target) {
            _removeEventListeners.call(this, target);
            target.style.display = '';
        }
        this._currentTarget = null;
    };

    function commitLayout(layout) {
        var target = this._currentTarget;
        if (!target) return;

        var cache = this._cachedSpec;

        var transform = layout.transform || Transform.identity;
        var opacity = (layout.opacity === undefined) ? 1 : layout.opacity;
        var origin = layout.origin || _zeroZero;

        this._transformDirty = Transform.notEquals(cache.transform, transform);
        this._opacityDirty = this._opacityDirty || (cache.opacity !== opacity);
        this._originDirty = this._originDirty || (origin && _xyNotEquals(cache.origin, origin));

        if (this._opacityDirty) {
            cache.opacity = opacity;
            _setOpacity.call(this, target, opacity);
        }

        if (this._originDirty){
            cache.origin = origin;
            _setOrigin(target, origin);
        }

        if (this._transformDirty) {
            cache.transform = transform;
            _setTransform(target, transform, this.roundToPixel ? 1 : devicePixelRatio);
        }

        this._originDirty = false;
        this._transformDirty = false;
        this._opacityDirty = false;
    }

    function commitSize(size){
        var target = this._currentTarget;
        if (!target) return;

        if (size[0] !== true) size[0] = _round(size[0], devicePixelRatio);
        if (size[1] !== true) size[1] = _round(size[1], devicePixelRatio);

        if (_xyNotEquals(this._cachedSpec.size, size)){
            this._cachedSpec.size = size;
            _setSize(target, size);
            this.emit('resize', size);
        }
    }

    module.exports = ElementOutput;
});