/*
 * Ext.ux.WindowBlind v0.1
 * Copyright(c) 2008, http://www.mcdconsultingllc.com
 * 
 * Licensed under the terms of the Open Source LGPL 3.0
 * http://www.gnu.org/licenses/lgpl.html
 * @author Sean McDaniel <sean@mcdconsulting.com>
 *
 * 
 * @class WindowBlind
 * @extends Ext.util.Observable
 * <p>
 * Real simple blind solution for collecting user input or displaying
 * information to a user working in a window.  This class is primarily 
 * responsible for showing an element that is included in the window's 
 * containing element.  A WindowBlind can be autocreated or bound to 
 * existing markup.  
 * </p>
 * <p>
 * The blind's element structure consists of a container div with a class 
 * of 'ux-win-blind' and two child divs with the classes 'ux-win-blind-body' 
 * and 'ux-win-blind-ft'.  The body can contain a simple message, form data,
 * Ext.Panel, etc., the footer is used internally to create a button container.  
 * If autocreate is true then this markup structure will be rendered as a child 
 * of the related window's container element.  
 * </p>
 * @cfg {Boolean} autoCreate True to auto create from scratch (defaults to true)
 * @cfg {Number} minButtonWidth Minimum width of all blind buttons (defaults to win's minButtonWitdh)
 * @cfg {String} buttonAlign  The alignment of any buttons added to this blind.  Valid values are 'right,' 'left' and 'center' (defaults to win.buttonAlign).
 * @cfg {String} text The text to show when the blind is autocreated.  Can be any content 
 * @cfg {Number} fxDuration The duration for the easing effect (defaults to .35 secs).
 * @cfg {Boolean/String} shadow True (or a valid Ext.Shadow {@link Ext.Shadow#mode} value) to display a shadow behind the blind, false to
 * display no shadow (defaults to 'sides').  
 * @cfg {Number} shadowOffset Number of pixels to offset the shadow (defaults to 4) 
 * @constructor
 * Creates a new WindowBlind.
 * @param {String/HTMLElement/Ext.Element} el The container element or DOM 
 * node, or its id
 * @param {Ext.Window} win The window to bind this blind to.
 * @param {Object} config Configuration options
 */
Ext.ux.WindowBlind = function(el, win, config) {
    Ext.apply(this, config ? config : {} , {
        minButtonWidth: win.minButtonWidth,
        buttonAlign: win.buttonAlign
    });     
  
    this.buttons = [];  
    this.winEl = win.getEl();
    this.winBodyEl = win.body;
    
    var dh = Ext.DomHelper;
    this.el = Ext.get(el);
    if (!this.el && this.autoCreate) {
        var cls = ['ux-win-blind'];
        if (this.cls) cls.push(this.cls);
        cls.push('ux-win-blind-inactive');
        this.el = dh.append(
            this.winEl,
            {id: el, cls: cls.join(' ')}, 
            true
        );
    }
    
    var el = this.el;  
    
    // look for the body element and create if necessary
    this.body = el.child('/.ux-win-blind-body');
    if (!this.body) {
        this.body = el.createChild({cls: 'ux-win-blind-body'});
    }

    // if the config contained content update the content el
    if (this.text) {
        this.body.update(this.text);
    }

     // wrap the body and footer for special rendering
    this.bwrap = this.body.wrap({cls: 'ux-win-blind-bwrap'});
    
    // look for the footer element and create if necessary
    this.footer = el.child('/.ux-win-blind-ft');
    if (!this.footer) {
        this.footer = el.createChild({cls: 'ux-win-blind-ft'});
    }
    
    // move the footer to the bwrap
    this.bwrap.dom.appendChild(this.footer.dom);
 
    // create a shadow for a layer effect
    this.shadow = typeof this.shadow != 'undefined' ? this.shadow : true;
    if (this.shadow) {
        this.shadow = new Ext.Shadow({
            mode: typeof this.shadow == 'string' ? this.shadow : 'sides',
            offset: typeof this.shadowOffset == 'number' ? this.shadowOffset : 4
        }); 
    }
    
    // observable events
    this.addEvents({
        /**
         * @event beforeshow
         * Fires before this window is shown.
         * @param {WindowBlind} this
         */ 
        beforeshow: true,
        /**
         * @event show
         * Fires after the blind has been completely eased into view.
         * @param {WindowBlind} this
         */ 
        show: true,
        /**
         * @event beforehide
         * Fires before the blind el is eased out of view.
         * @param {WindowBlind} this
         */ 
        beforehide: true,
        /**
         * @event hide
         * Fires after the blind has been completely eased out of view.
         * @param {WindowBlind} this
         */ 
        hide: true
    });  

    // register for all events so that subtypes can plug into the lifecycle.
    this.on('beforeshow', this.onBeforeShow, this);  
    this.on('show', this.onShow, this);  
    this.on('beforehide', this.onBeforeHide, this);  
    this.on('hide', this.onHide, this);  
};

Ext.extend(Ext.ux.WindowBlind, Ext.util.Observable, {   
    /**
     * Default: autoCreate true is the default.
     */
    autoCreate: true,
    
    /**
     * Default: fxDuration (defaults to .35 sec).  Set this to -1 to remove animation fx.
     */ 
    fxDuration: .35,
    
    /**
     * Show Fx.  The function from Ext.Fx to use when showing this (defaults to Ext.Fx.slideIn).  Optional value
     * is Ext.Fx.fadeIn.  Motion effects are always based on top - center.
     */
    showFx: Ext.Fx.slideIn,
    
    /**
     * Hide Fx.  The function from Ext.Fx to use when hiding this (defaults to Ext.Fx.slideIn).  Optional values
     * include Ext.Fx.fadeOut, Ext.Fx.ghost, Ext.Fx.puff. Motion effects are always based on top - center.
     */
    hideFx: Ext.Fx.slideOut,
    
    /**
     * Show easing.  Easing method to use if using an easing animation (defaults to 'easeOut')
     */
    showEasing: 'easeOut',
    
    /**
     * Hide easing.  Easing method to use if using an easing animation (defaults to 'easeOut')
     */
    hideEasing: 'easeOut',
        
    /**
     * Adds and renders a button to the footer.
     * @param {String/Object} config A string becomes the button text, an object can be a Button config
     * object
     * @param {Function} handler The function called when the button is clicked
     * @param {Object} scope (optional) The scope of the handler function
     * @return {Ext.Button}
     */
    addButton : function(config, handler, scope) {
        if (!this.btnContainer) {
            var tbl = this.footer.createChild({cls:'x-panel-btns-ct', cn: {
                cls:"x-panel-btns x-panel-btns-"+this.buttonAlign,
                html:'<table cellspacing="0"><tbody><tr></tr></tbody></table><div class="x-clear"></div>'
            }}, null, true);

            this.btnContainer = tbl.getElementsByTagName('tr')[0];
        }
                
        var td = document.createElement("td");
        td.className = 'x-panel-btn-td';
        
        var btnConfig = this.getButtonConfig(config, handler, scope);
        btnConfig.renderTo = this.btnContainer.appendChild(td);
        var btn = new Ext.Button(btnConfig);

        this.footer.setHeight(
            this.btnContainer.parentNode.parentNode.offsetHeight 
            + this.footer.getPadding("tb") 
            + this.footer.getMargins("tb")
        );

        return this.registerButton(btn);
    }, 

    /**
     * Construct a button config.
     */
    getButtonConfig: function(config, handler, scope) {
        var btnConfig = {
            handler: handler || this.dismiss,
            scope: scope || this,
            hideParent: true
        };
        
        if (this.minButtonWidth) {
            btnConfig.minWidth = this.minButtonWidth
        }

        if (typeof config == "string") {
            btnConfig.text = config;
        } else {
            Ext.apply(btnConfig, config);
        }
    
        return btnConfig;
    },

    /**
     * Register a new button.
     */
    registerButton: function(btn) {
        if (btn.defaultButton) {
            this.setDefaultButton(btn);
        }

        this.buttons.push(btn); 
        return btn;
    },
    
    /**
     * Private
     * Align the element's top left position to the
     * top left of the window's body. 
     */
    beforeShow: function() {
        // mask the window's el
        this.winEl.mask();    

        // sync the height & width
        this.syncSize();

        // align el's top-center to the window's body top-center
        this.el.alignTo(this.winBodyEl, 't-t');
    },

    /**
     * Show this.  
     */
    show: function() {
        if (this.el.isVisible()) {
            return;
        }
    
        if (this.fireEvent("beforeshow", this) === false){
            return;
        }
    
        this.beforeShow();
        this.animate(this.showFx, {
            easing: this.showEasing,
            duration: this.fxDuration,
            callback: this.afterShow,
            scope: this           
        });
    },

    /**
     * Set the fucus and show the shadow.
     */
    afterShow: function() {
        this.focus();        
        if (this.shadow) {      
            this.shadow.show(this.el);
        }

        if (this.overflowBody) {
           this.body.setStyle('overflow', 'auto');
        }   

        this.el.removeClass('ux-win-blind-inactive');
        this.fireEvent('show', this);
    },    

    /**
     * Private
     * If the body needs to overflow set the overflow style to hidden.
     * Hides the scrollbar that would ease out beyond the window's 
     * containing element.
     */
    beforeHide: function() {
        if (this.overflowBody) {
            this.body.setStyle('overflow', 'hidden');
            delete this.overflowBody;
        }

        if (this.shadow) {
            this.shadow.hide();
        }

        // css override for overflow for divs within the body
        this.el.addClass('ux-win-blind-inactive'); 
    },

    /**
     * Hide this. 
     */
    hide: function() {
        if (!this.el.isVisible()) {
            return;
        }
        
        if (this.fireEvent("beforehide", this) === false) {
            return;
        }

        this.beforeHide();
        this.animate(this.hideFx, {
            easing: this.hideEasing,
            duration: this.fxDuration,
            callback: this.afterHide,
            scope: this                      
        });
    },

    /**
     * Private
     * Unmask the window's el.
     */
    afterHide: function() {
        this.winEl.unmask();    
        if (this.restoreHeight) {
            this.body.setHeight(this.restoreHeight);
            delete this.restoreHeight;
        }   
        this.fireEvent('hide', this);
    },

    /**
     * Sync the height and width of this.
     */
    syncSize: function() {
        this.syncWidth();
        this.syncHeight();
    },

    /**
     * Set el's width to be the window body's el less any padding or borders.
     */
    syncWidth: function() {
        this.el.setWidth(this.winBodyEl.getWidth(true));    
    },

    /**
     * Private
     * Adjust the body el height to ensure this blind only covers the body
     * of the window.   Manage overflow if this height exceeds the window's body.
     * It is important to ensure scrollbars are not visible until after show.
     */
    syncHeight: function() {
        var h = this.winBodyEl.getComputedHeight();
        if (this.el.getComputedHeight() > h) {
            this.overflowBody = true;
            this.restoreHeight = this.body.getHeight();
            this.body.setStyle('overflow', 'hidden');
            this.body.setHeight(h - this.bwrap.getPadding('tb') - this.footer.getComputedHeight());
        }
    },
    
    /**
     * Runs the animation.
     * @param {Function} fx The function that will be invoked by the animate method to either show or hide this.
     * @param {Object} options (optional) Object literal with any of the Fx config options.
     */
    animate: function(fx, options) {
        if (this.isMotionFx(fx)) {
            fx.call(this.el, 't', options);
        } else {
            fx.call(this.el, options);  
        }   
    },

    /**
     * Determines if the passed function is a motion effect that requires an anchor point.
     * @param {Function} fn The function that will be invoked by the animate method to either show or hide this.
     */
    isMotionFx: function(fn) {
        return fn === Ext.Fx.slideIn
            || fn === Ext.Fx.slideOut
            || fn === Ext.Fx.ghost;
    },

    /**
     * Sets the default button to be focused when the blind is displayed.
     * @param {Ext.Button} btn Sets the button
     */
    setDefaultButton: function(btn) {
        this.defaultBtn = btn;
    },

    /**
     * Private.  Focuses the specificed defaultButton.  If a default button
     * wasn't specified then the first button added to this blind will be focused
     */
    focus: function() {
        if (this.defaultBtn) {
            this.defaultBtn.focus.defer(10, this.defaultBtn);
        } else if (this.buttons){
            this.buttons[0].focus();
        }   
    },

    /**
     * Hides (if visible) and destroys this blind.  
     */
    dismiss: function() {
        if (!this.el.isVisible()) {
            this.destroy();
        } else {
            this.on('hide', this.destroy, this);
            this.hide();
        }
    },

    /**
     * Default implementation of the onBeforeShow method.  Subtypes
     * should override this to perform any 'beforeshow' logic.
     */
    onBeforeShow: Ext.emptyFn,
    
    /**
     * Default implementation of the onShow method.  Subtypes
     * should override this to perform any logic after the blind
     * el has been shown.
     */
    onShow: Ext.emptyFn,

    /**
     * Default implementation of the onBeforeHide method.  Subtypes
     * should override this to perform any logic prior to hiding
     * the blind el.
     */
    onBeforeHide: Ext.emptyFn,

    /**
     * Default implementation of the onBeforeHide method.  Subtypes
     * should override this to perform any logic after the blind el
     * has been hidden.
     */
    onHide: Ext.emptyFn,

    /**
     * Cleanup.  
     */
    destroy: function() {
        this.purgeListeners();
        if (this.shadow) {
            this.shadow.hide();
            delete this.shadow;
        }
    
        Ext.Element.uncache(
            this.body,
            this.footer,
            this.bwrap
        );
    
        if (this.buttons) {
           Ext.each(this.buttons, function(btn) {
              btn.destroy();
           });

           delete this.buttons;
        }     
        
        this.el.remove();
    }
});