/*
 * This class is a derived work from:
 *
 * Notification extension for Ext JS 4.0.2+
 * Version: 2.1.3
 *
 * Copyright (c) 2011 Eirik Lorentsen (http://www.eirik.net/)
 *
 * Follow project on GitHub: https://github.com/EirikLorentsen/Ext.ux.window.Notification
 *
 * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and GPL (http://opensource.org/licenses/GPL-3.0) licenses.
 */
 
/**
 * This class provides for lightweight, auto-dismissing pop-up notifications called "toasts".
 * At the base level, you can display a toast message by calling `Ext.toast` like so:
 *
 *      Ext.toast('Data saved');
 *
 * This will result in a toast message, which displays in the default location at the top
 * of your viewport.
 *
 * You may expand upon this simple example with the following parameters: 
 *
 *      Ext.toast(message, title, align, iconCls);
 *
 * For example, the following toast will appear top-middle in your viewport.  It will display 
 * the 'Data Saved' message with a title of 'Title'  
 *
 *      Ext.toast('Data Saved', 'Title', 't')
 *
 * It should be noted that the toast's width is determined by the message's width. 
 * If you need to set a specific width, or any of the other available configurations for your toast,
 * you can create the toast object as seen below:
 *
 *      Ext.toast({
 *          html: 'Data Saved',
 *          title: 'My Title',
 *          width: 200,
 *          align: 't'
 *      });
 *
 * This component is derived from the excellent work of a Sencha community member, Eirik
 * Lorentsen.
 */
Ext.define('Ext.window.Toast', {
    extend: 'Ext.window.Window',
 
    xtype: 'toast',
    
    isToast: true,
 
    /**
     * @cfg cls
     * @inheritdoc
     */
    cls: Ext.baseCSSPrefix + 'toast',
 
    /**
     * @cfg bodyPadding
     * @inheritdoc
     */
    bodyPadding: 10,
 
    /**
     * @cfg {Boolean} autoClose
     * This config ensures that the Toast is closed automatically after a certain amount of time.
     * If this is set to `false`, closing the Toast will have to be handled some other way
     * (e.g., Setting `closable: true`).
     */
    autoClose: true,
 
    /**
     * @cfg plain
     * @inheritdoc
     */
    plain: false,
 
    /**
     * @cfg draggable
     * @inheritdoc
     */
    draggable: false,
 
    /**
     * @cfg resizable
     * @inheritdoc
     */
    resizable: false,
 
    /**
     * @cfg shadow
     * @inheritdoc
     */
    shadow: false,
    
    focus: Ext.emptyFn,
 
    /**
     * @cfg {String/Ext.Component} anchor
     * The component or the `id` of the component to which the `toast` will be anchored.
     * The default behavior is to anchor a `toast` to the document body (no component).
     */
    anchor: null,
 
    /**
     * @cfg {Boolean} useXAxis
     * Directs the toast message to animate on the x-axis (if `true`) or y-axis (if `false`).
     * This value defaults to a value based on the `align` config.
     */
    useXAxis: false,
 
    /**
     * @cfg {"br"/"bl"/"tr"/"tl"/"t"/"l"/"b"/"r"} align
     * Specifies the basic alignment of the toast message with its {@link #anchor}. This 
     * controls many aspects of the toast animation as well. For fine grain control of 
     * the final placement of the toast and its `anchor` you may set 
     * {@link #anchorAlign} as well.
     * 
     * Possible values:
     * 
     *  - br - bottom-right
     *  - bl - bottom-left
     *  - tr - top-right
     *  - tl - top-left
     *  - t  - top
     *  - l  - left
     *  - b  - bottom
     *  - r  - right
     */
    align: 't',
 
    /**
     * @cfg alwaysOnTop
     * @inheritdoc
     */
    alwaysOnTop: true,
 
    /**
     * @cfg {String} anchorAlign
     * This string is a full specification of how to position the toast with respect to
     * its `anchor`. This is set to a reasonable value based on `align` but the `align`
     * also sets defaults for various other properties. This config controls only the
     * final position of the toast.
     */
    
    /**
     * @cfg {Boolean} [animate=true]
     * Set this to `false` to make toasts appear and disappear without animation.
     * This is helpful with applications' unit and integration testing.
     */
 
    // Pixels between each notification
    /**
     * @cfg {Number} spacing
     * The number of pixels between each Toast notification.
     */
    spacing: 6,
 
    // TODO There should be a way to control from and to positions for the introduction.
    // TODO The align/anchorAlign configs don't actually work as expected.
 
    // Pixels from the anchor's borders to start the first notification
    paddingX: 30,
    paddingY: 10,
 
    /**
     * @cfg {String} slideInAnimation
     * The animation used for the Toast to slide in.
     */
    slideInAnimation: 'easeIn',
 
    /**
     * @cfg {String} slideBackAnimation
     * The animation used for the Toast to slide back.
     */
    slideBackAnimation: 'bounceOut',
 
    /**
     * @cfg {Number} slideInDuration
     * The number of milliseconds it takes for a Toast to slide in.
     */
    slideInDuration: 500,
 
    /**
     * @cfg {Number} slideBackDuration
     * The number of milliseconds it takes for a Toast to slide back.
     */
    slideBackDuration: 500,
 
    /**
     * @cfg {Number} hideDuration
     * The number of milliseconds it takes for a Toast to hide.
     */
    hideDuration: 500,
 
    /**
     * @cfg {Number} autoCloseDelay
     * The number of milliseconds a Toast waits before automatically closing.
     */
    autoCloseDelay: 3000,
    
    /**
     * @cfg {Boolean} stickOnClick
     * This config will prevent the Toast from closing when you click on it. If this is set
     * to `true`, closing the Toast will have to be handled some other way
     * (e.g., Setting `closable: true`).
     */
    stickOnClick: false,
 
    /**
     * @cfg {Boolean} stickWhileHover
     * This config will prevent the Toast from closing while you're hovered over it.
     */
    stickWhileHover: true,
 
    /**
     * @cfg {Boolean} closeOnMouseDown
     * This config will prevent the Toast from closing when a user produces a mousedown event.
     */
    closeOnMouseDown: false,
 
    /**
     * @cfg closable
     * @inheritdoc
     */
    closable: false,
 
    /**
     * @cfg minHeight
     * @inheritdoc
     */
    minHeight: 1,
 
    /**
     * @property focusable
     * @inheritdoc
     */
    focusable: false,
 
    // Private. Do not override!
    isHiding: false,
    isFading: false,
    destroyAfterHide: false,
    closeOnMouseOut: false,
 
    // Caching coordinates to be able to align to final position of siblings being animated
    xPos: 0,
    yPos: 0,
 
    constructor: function(config) {
        config = config || {};
        
        if (config.animate === undefined) {
            config.animate = Ext.isBoolean(this.animate) ? this.animate : Ext.enableFx;
        }
        
        this.enableAnimations = config.animate;
        delete config.animate;
 
        this.callParent([config]);
    },
 
    initComponent: function() {
        var me = this;
 
        // Close tool is not really helpful to sight impaired users
        // when Toast window is set to auto-close on timeout; however
        // if it's forced, respect that.
        if (me.autoClose && me.closable == null) {
            me.closable = false;
        }
        
        me.updateAlignment(me.align);
        me.setAnchor(me.anchor);
        me.callParent();
    },
 
    onRender: function() {
        var me = this;
 
        me.callParent(arguments);
 
        me.el.hover(me.onMouseEnter, me.onMouseLeave, me);
 
        // Mousedown outside of this, when visible, hides it immediately
        if (me.closeOnMouseDown) {
            Ext.getDoc().on('mousedown', me.onDocumentMousedown, me);
        }
    },
 
    /*
     * These properties are keyed by "align" and set defaults for various configs.
     */
    alignmentProps: {
        br: {
            paddingFactorX: -1,
            paddingFactorY: -1,
            siblingAlignment: "br-br",
            anchorAlign: "tr-br"
        },
        bl: {
            paddingFactorX: 1,
            paddingFactorY: -1,
            siblingAlignment: "bl-bl",
            anchorAlign: "tl-bl"
        },
 
        tr: {
            paddingFactorX: -1,
            paddingFactorY: 1,
            siblingAlignment: "tr-tr",
            anchorAlign: "br-tr"
        },
        tl: {
            paddingFactorX: 1,
            paddingFactorY: 1,
            siblingAlignment: "tl-tl",
            anchorAlign: "bl-tl"
        },
 
        b: {
            paddingFactorX: 0,
            paddingFactorY: -1,
            siblingAlignment: "b-b",
            useXAxis: 0,
            anchorAlign: "t-b"
        },
        t: {
            paddingFactorX: 0,
            paddingFactorY: 1,
            siblingAlignment: "t-t",
            useXAxis: 0,
            anchorAlign: "b-t"
        },
        l: {
            paddingFactorX: 1,
            paddingFactorY: 0,
            siblingAlignment: "l-l",
            useXAxis: 1,
            anchorAlign: "r-l"
        },
        r: {
            paddingFactorX: -1,
            paddingFactorY: 0,
            siblingAlignment: "r-r",
            useXAxis: 1,
            anchorAlign: "l-r"
        },
 
        /*
         * These properties take priority over the above and applied only when useXAxis
         * is set to true. Again these are keyed by "align".
         */
        x: {
            br: {
                anchorAlign: "bl-br"
            },
            bl: {
                anchorAlign: "br-bl"
            },
            tr: {
                anchorAlign: "tl-tr"
            },
            tl: {
                anchorAlign: "tr-tl"
            }
        }
    },
 
    updateAlignment: function(align) {
        var me = this,
            alignmentProps = me.alignmentProps,
            props = alignmentProps[align],
            xprops = alignmentProps.x[align];
 
        if (xprops && me.useXAxis) {
            Ext.applyIf(me, xprops);
        }
 
        Ext.applyIf(me, props);
    },
 
    getXposAlignedToAnchor: function() {
        var me = this,
            align = me.align,
            anchor = me.anchor,
            anchorEl = anchor && anchor.el,
            el = me.el,
            xPos = 0;
 
        // Avoid error messages if the anchor does not have a dom element
        if (anchorEl && anchorEl.dom) {
            if (!me.useXAxis) {
                // Element should already be aligned vertically
                xPos = el.getLeft();
            }
            // Using getAnchorXY instead of getTop/getBottom should give a correct placement
            // when document is used as the anchor but is still 0 px high.
            // Before rendering the viewport.
            else if (align === 'br' || align === 'tr' || align === 'r') {
                xPos += anchorEl.getAnchorXY('r')[0];
                xPos -= (el.getWidth() + me.paddingX);
            }
            else {
                xPos += anchorEl.getAnchorXY('l')[0];
                xPos += me.paddingX;
            }
        }
 
        return xPos;
    },
 
    getYposAlignedToAnchor: function() {
        var me = this,
            align = me.align,
            anchor = me.anchor,
            anchorEl = anchor && anchor.el,
            el = me.el,
            yPos = 0;
 
        // Avoid error messages if the anchor does not have a dom element
        if (anchorEl && anchorEl.dom) {
            if (me.useXAxis) {
                // Element should already be aligned horizontally
                yPos = el.getTop();
            }
            // Using getAnchorXY instead of getTop/getBottom should give a correct placement
            // when document is used as the anchor but is still 0 px high.
            // Before rendering the viewport.
            else if (align === 'br' || align === 'bl' || align === 'b') {
                yPos += anchorEl.getAnchorXY('b')[1];
                yPos -= (el.getHeight() + me.paddingY);
            }
            else {
                yPos += anchorEl.getAnchorXY('t')[1];
                yPos += me.paddingY;
            }
        }
 
        return yPos;
    },
 
    getXposAlignedToSibling: function(sibling) {
        var me = this,
            align = me.align,
            el = me.el,
            xPos;
 
        if (!me.useXAxis) {
            xPos = el.getLeft();
        }
        else if (align === 'tl' || align === 'bl' || align === 'l') {
            // Using sibling's width when adding
            xPos = (sibling.xPos + sibling.el.getWidth() + sibling.spacing);
        }
        else {
            // Using own width when subtracting
            xPos = (sibling.xPos - el.getWidth() - me.spacing);
        }
 
        return xPos;
    },
 
    getYposAlignedToSibling: function(sibling) {
        var me = this,
            align = me.align,
            el = me.el,
            yPos;
 
        if (me.useXAxis) {
            yPos = el.getTop();
        }
        else if (align === 'tr' || align === 'tl' || align === 't') {
            // Using sibling's width when adding
            yPos = (sibling.yPos + sibling.el.getHeight() + sibling.spacing);
        }
        else {
            // Using own width when subtracting
            yPos = (sibling.yPos - el.getHeight() - sibling.spacing);
        }
 
        return yPos;
    },
 
    getToasts: function() {
        var anchor = this.anchor,
            alignment = this.anchorAlign,
            activeToasts = anchor.activeToasts || (anchor.activeToasts = {});
 
        return activeToasts[alignment] || (activeToasts[alignment] = []);
    },
 
    setAnchor: function(anchor) {
        var me = this,
            Toast;
 
        me.anchor = anchor = ((typeof anchor === 'string') ? Ext.getCmp(anchor) : anchor);
 
        // If no anchor is provided or found, then the static object is used and the el
        // property pointed to the body document.
        if (!anchor) {
            Toast = Ext.window.Toast;
 
            me.anchor = Toast.bodyAnchor || (Toast.bodyAnchor = {
                el: Ext.getBody()
            });
        }
    },
 
    beforeShow: function() {
        var me = this;
 
        if (me.stickOnClick) {
            me.body.on('click', function() {
                me.cancelAutoClose();
            });
        }
 
        if (me.autoClose) {
            if (!me.closeTask) {
                me.closeTask = new Ext.util.DelayedTask(me.doAutoClose, me);
            }
        }
 
        // Shunting offscreen to avoid flicker
        me.el.setX(-10000);
        me.el.setOpacity(1);
    },
 
    afterShow: function() {
        var me = this,
            el = me.el,
            activeToasts, sibling, length, xy;
 
        me.callParent(arguments);
 
        activeToasts = me.getToasts();
        length = activeToasts.length;
        sibling = length && activeToasts[length - 1];
 
        if (sibling) {
            el.alignTo(sibling.el, me.siblingAlignment, [0, 0]);
 
            me.xPos = me.getXposAlignedToSibling(sibling);
            me.yPos = me.getYposAlignedToSibling(sibling);
        }
        else {
            el.alignTo(
                me.anchor.el, me.anchorAlign,
                [(me.paddingX * me.paddingFactorX), (me.paddingY * me.paddingFactorY)],
                false
            );
 
            me.xPos = me.getXposAlignedToAnchor();
            me.yPos = me.getYposAlignedToAnchor();
        }
 
        Ext.Array.include(activeToasts, me);
 
        if (me.enableAnimations) {
            // Repeating from coordinates makes sure the windows does not flicker
            // into the center of the viewport during animation
            xy = el.getXY();
            el.animate({
                from: {
                    x: xy[0],
                    y: xy[1]
                },
                to: {
                    x: me.xPos,
                    y: me.yPos,
                    opacity: 1
                },
                easing: me.slideInAnimation,
                duration: me.slideInDuration,
                dynamic: true,
                callback: me.afterPositioned,
                scope: me
            });
        }
        else {
            me.setLocalXY(me.xPos, me.yPos);
            me.afterPositioned();
        }
    },
 
    afterPositioned: function() {
        var me = this;
        
        // This method can be called from afteranimation event being fired
        // during destruction sequence.
        if (!me.destroying && !me.destroyed && me.autoClose) {
            me.closeTask.delay(me.autoCloseDelay);
        }
    },
 
    onDocumentMousedown: function(e) {
        if (this.isVisible() && !this.owns(e.getTarget())) {
            this.hide();
        }
    },
 
    slideBack: function() {
        var me = this,
            anchor = me.anchor,
            anchorEl = anchor && anchor.el,
            el = me.el,
            activeToasts = me.getToasts(),
            index = Ext.Array.indexOf(activeToasts, me);
 
        // Not animating the element if it already started to hide itself
        // or if the anchor is not present in the dom
        if (!me.isHiding && el && el.dom && anchorEl && anchorEl.isVisible()) {
            if (index) {
                me.xPos = me.getXposAlignedToSibling(activeToasts[index - 1]);
                me.yPos = me.getYposAlignedToSibling(activeToasts[index - 1]);
            }
            else {
                me.xPos = me.getXposAlignedToAnchor();
                me.yPos = me.getYposAlignedToAnchor();
            }
 
            me.stopAnimation();
            
            if (me.enableAnimations) {
                el.animate({
                    to: {
                        x: me.xPos,
                        y: me.yPos
                    },
                    easing: me.slideBackAnimation,
                    duration: me.slideBackDuration,
                    dynamic: true
                });
            }
        }
    },
 
    update: function() {
        var me = this;
 
        if (me.isVisible()) {
            me.isHiding = true;
            me.hide();
            // TODO offer a way to just update and reposition after layout
        }
 
        me.callParent(arguments);
 
        me.show();
    },
 
    cancelAutoClose: function() {
        var closeTask = this.closeTask;
 
        if (closeTask) {
            closeTask.cancel();
        }
    },
 
    doAutoClose: function() {
        var me = this;
 
        if (!(me.stickWhileHover && me.mouseIsOver)) {
            // Close immediately
            me.close();
        }
        else {
            // Delayed closing when mouse leaves the component.
            me.closeOnMouseOut = true;
        }
    },
    
    doDestroy: function() {
        this.removeFromAnchor();
        this.cancelAutoClose();
        this.callParent();
    },
 
    onMouseEnter: function() {
        this.mouseIsOver = true;
    },
 
    onMouseLeave: function() {
        var me = this;
 
        me.mouseIsOver = false;
 
        if (me.closeOnMouseOut) {
            me.closeOnMouseOut = false;
            me.close();
        }
    },
 
    removeFromAnchor: function() {
        var me = this,
            activeToasts, index;
 
        if (me.anchor) {
            activeToasts = me.getToasts();
            index = Ext.Array.indexOf(activeToasts, me);
            
            if (index !== -1) {
                Ext.Array.erase(activeToasts, index, 1);
 
                // Slide "down" all activeToasts "above" the hidden one
                for (; index < activeToasts.length; index++) {
                    activeToasts[index].slideBack();
                }
            }
        }
    },
 
    getFocusEl: Ext.emptyFn,
 
    hide: function() {
        var me = this,
            el = me.el;
 
        me.cancelAutoClose();
        
        if (me.isHiding) {
            if (!me.isFading) {
                me.callParent(arguments);
                me.isHiding = false;
            }
        }
        else {
            // Must be set right away in case of double clicks on the close button
            me.isHiding = true;
            me.isFading = true;
 
            me.cancelAutoClose();
 
            if (el) {
                if (me.enableAnimations && !me.destroying && !me.destroyed) {
                    el.fadeOut({
                        opacity: 0,
                        easing: 'easeIn',
                        duration: me.hideDuration,
                        listeners: {
                            scope: me,
                            afteranimate: function() {
                                var me = this;
                                
                                me.isFading = false;
                                
                                if (!me.destroying && !me.destroyed) {
                                    me.hide(me.animateTarget, me.doClose, me);
                                }
                            }
                        }
                    });
                }
                else {
                    me.isFading = false;
                    me.hide(me.animateTarget, me.doClose, me);
                }
            }
        }
 
        return me;
    }
}, function(Toast) {
    Ext.toast = function(message, title, align, iconCls) {
        var config = message,
            toast;
 
        if (Ext.isString(message)) {
            config = {
                title: title,
                html: message,
                iconCls: iconCls
            };
            
            if (align) {
                config.align = align;
            }
        }
 
        toast = new Toast(config);
        toast.show();
        
        return toast;
    };
});